企業(yè)網(wǎng)站怎么做畢業(yè)設(shè)計網(wǎng)站怎么營銷推廣
Go面向“對象”編程(入門——結(jié)構(gòu)體與方法)
- 1 結(jié)構(gòu)體
- 1.1 快速入門
- 1.2 內(nèi)存解析
- 1.3 創(chuàng)建結(jié)構(gòu)體四種方法
- 1.4 注意事項和使用細節(jié)
- 2 方法
- 2.1 方法的聲明和調(diào)用
- 2.2 快速入門案例
- 2.3 調(diào)用機制和傳參原理
- 2.4 注意事項和細節(jié)
- 2.5 方法和函數(shù)區(qū)別
- 3 工廠模式
Golang語言面向?qū)ο缶幊陶f明:
- Golang也支持面向?qū)ο缶幊?OOP),但是和傳統(tǒng)的面向?qū)ο缶幊逃袇^(qū)別,并不是純粹的面向?qū)ο笳Z言。所以我們說Golang支持面向?qū)ο缶幊烫匦?/strong>是比較準確的。
- Golang沒有類(class),Go語言的結(jié)構(gòu)體(struct)和其它編程語言的類(class)有同等的地位,你可以理解Gelang是基于struct來實現(xiàn)OOP特性的。
- Golang面向?qū)ο缶幊谭浅:啙?#xff0c;去掉了傳統(tǒng)OOP語言的方法重載、構(gòu)造函數(shù)和析構(gòu)函數(shù)、隱藏的this指針等等。
- Golang仍然有面向?qū)ο缶幊痰睦^承,封裝和多態(tài)的特性,只是實現(xiàn)的方式和其它OOP語言不一樣,比如繼承:Golang沒有extends 關(guān)鍵字,繼承是通過匿名字段來實現(xiàn)。
- Golang面向?qū)ο?#xff08;OOP)很優(yōu)雅,OOP本身就是語言類型系統(tǒng)(type system)的一部分,通過接口(interface)關(guān)聯(lián),耦合性低,也非常靈活。后面同學(xué)們會充分體會到這個特點。也就是說在Golang中面向接口編程是非常重要的特性。
1 結(jié)構(gòu)體
1.1 快速入門
假設(shè)定義結(jié)構(gòu)體是老師,屬性:名字,年齡,學(xué)校
type Teacher struct {//變量名字大寫外界可以訪問這個屬性Name stringAge intSchool string
}
下面我們嘗試賦值,并打印出來:
type Teacher struct {//變量名字大寫外界可以訪問這個屬性Name stringAge intSchool string
}func main() {var tea1 Teachertea1.Name = "張三"tea1.Age = 28tea1.School = "深圳大學(xué)"fmt.Println("tea1=", tea1)fmt.Println("老師的信息如下:")fmt.Println("name=", tea1.Name)fmt.Println("age=", tea1.Age)fmt.Println("school=", tea1.School)
}
輸出結(jié)果:
tea1= {張三 28 深圳大學(xué)}
老師的信息如下:
name= 張三
age= 28
school= 深圳大學(xué)
通過上面的案例和講解可以看出:
- 結(jié)構(gòu)體是自定義的數(shù)據(jù)類型,代表一類事務(wù)。
- 結(jié)構(gòu)體變量(實例)是具體的,實際的,代表一個具體變量。
1.2 內(nèi)存解析
在Go語言中,結(jié)構(gòu)體的存儲是在堆上。
當我們創(chuàng)建一個結(jié)構(gòu)體實例時,它的內(nèi)存將被分配在堆上。這意味著結(jié)構(gòu)體的生命周期可以超出創(chuàng)建它的函數(shù)的生命周期。
例如,當我們使用new
關(guān)鍵字或make
函數(shù)創(chuàng)建一個結(jié)構(gòu)體實例時,它將被分配在堆上。
type Person struct {Name stringAge int
}func main() {// 使用 new 關(guān)鍵字創(chuàng)建結(jié)構(gòu)體實例p := new(Person)p.Name = "Alice"p.Age = 25// 使用 make 函數(shù)創(chuàng)建結(jié)構(gòu)體實例(只適用于為某些類型分配內(nèi)存,如 map、slice 和 channel)m := make(map[string]int)m["key"] = 42// 結(jié)構(gòu)體實例被分配在堆上,可以在其他函數(shù)中繼續(xù)使用anotherFunc(p)yetAnotherFunc(m)
}
這是因為在Go語言中,所有的變量都是通過傳值而不是通過引用傳遞。在堆上分配結(jié)構(gòu)體實例可以確保結(jié)構(gòu)體數(shù)據(jù)的持久性和可用性。
Go語言中結(jié)構(gòu)體的內(nèi)存布局
Go語言中的結(jié)構(gòu)體是一組值的集合
,這些值被存儲在內(nèi)存中的一段連續(xù)的區(qū)域
。結(jié)構(gòu)體的內(nèi)存布局取決于結(jié)構(gòu)體中的成員變量順序和類型
,以及對齊方式。
結(jié)構(gòu)體的對齊方式
Go語言中使用的是一種稱為Packing的方式
進行對齊。這種方式默認對齊到最多8字節(jié)的倍數(shù),即8字節(jié)對齊??梢酝ㄟ^在結(jié)構(gòu)體成員變量的后面添加逗號
和數(shù)字
的形式手動調(diào)節(jié)對齊方式。
type Person struct {Name stringAge intHeight float64
}func main() {var p Person// 獲取結(jié)構(gòu)體的大小size := unsafe.Sizeof(p)fmt.Printf("結(jié)構(gòu)體大小:%d 字節(jié)\n", size)// 獲取結(jié)構(gòu)體字段的偏移量nameOffset := unsafe.Offsetof(p.Name)ageOffset := unsafe.Offsetof(p.Age)heightOffset := unsafe.Offsetof(p.Height)fmt.Printf("Name 字段的偏移量:%d 字節(jié)\n", nameOffset)fmt.Printf("Age 字段的偏移量:%d 字節(jié)\n", ageOffset)fmt.Printf("Height 字段的偏移量:%d 字節(jié)\n", heightOffset)// 結(jié)構(gòu)體的對齊方式packed := unsafe.Alignof(p)fmt.Printf("結(jié)構(gòu)體的對齊方式:%d 字節(jié)\n", packed)
}
輸出結(jié)果:
結(jié)構(gòu)體大小:20 字節(jié)
Name 字段的偏移量:0 字節(jié)
Age 字段的偏移量:8 字節(jié)
Height 字段的偏移量:12 字節(jié)
結(jié)構(gòu)體的對齊方式:4 字節(jié)
在這個示例中,我們定義了一個Person
結(jié)構(gòu)體,它包括名字、年齡和身高三個字段。我們通過unsafe
包中的函數(shù)來獲取結(jié)構(gòu)體的大小、字段的偏移量以及對齊方式。結(jié)構(gòu)體的大小為20字節(jié),字段的偏移量分別為0字節(jié)、8字節(jié)和12字節(jié),結(jié)構(gòu)體的對齊方式為4字節(jié)。
1.3 創(chuàng)建結(jié)構(gòu)體四種方法
基本介紹
- 從概念或叫法上:結(jié)構(gòu)體字段 = 屬性 = field
- 字段是結(jié)構(gòu)體的一個組成部分,一般是基本數(shù)據(jù)類型、數(shù)組、也可以是引用數(shù)據(jù)類型。
- 指針,slice和map的零值都是nil,即還沒有分配空間。
方式一:直接聲明
案例:var person Person
方式二:{}
案例:var person Person = Person{“Tom”, 18} => person := Person{“Tom”, 18}
方式三:&
案例:var person *Person = new (Person)
type Person struct {Name stringAge int
}func main() {var p *Person = new(Person)// (*p).Name = "smith" 標準寫法// go設(shè)計者,為了程序使用方便,底層對下面這個做了優(yōu)化,實現(xiàn)了這種簡單的寫法// 會給 p 加上 取值運算 =》 (*p).Name = "smith" p.Name = "smith" p.Age = 18fmt.Println(p)fmt.Println(*p)
}
輸出結(jié)果:
&{smith 18}
{smith 18}
方式四:{}
案例:var person *Person = &Person{}
type Person struct {Name stringAge int
}func main() {var p *Person = &Person{}// 標準方式:(*person).Name = "scott"p.Name = "scott"p.Age = 18fmt.Println(p)
}
輸出結(jié)果:&{scott 18}
1.4 注意事項和使用細節(jié)
- 結(jié)構(gòu)體的所有字段在內(nèi)存中是連續(xù)的,指針本身的地址是連續(xù)的,但是指向的地址不一定是連續(xù)的。
//結(jié)構(gòu)體
type Point struct {x inty int
}//結(jié)構(gòu)體
type Rect struct {leftUp, rightDown Point
}//結(jié)構(gòu)體
type Rect2 struct {leftUp, rightDown *Point
}func main() {r1 := Rect{Point{1,2}, Point{3,4}} //r1有四個int, 在內(nèi)存中是連續(xù)分布//打印地址fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p \n", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)//r2有兩個 *Point類型,這個兩個*Point類型的本身地址也是連續(xù)的,//但是他們指向的地址不一定是連續(xù)r2 := Rect2{&Point{10,20}, &Point{30,40}} //打印地址fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p \n", &r2.leftUp, &r2.rightDown)//他們指向的地址不一定是連續(xù)..., 這個要看系統(tǒng)在運行時是如何分配fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p \n", r2.leftUp, r2.rightDown)
}
輸出結(jié)果:
r1.leftUp.x 地址=0x9496080 r1.leftUp.y 地址=0x9496084 r1.rightDown.x 地址=0x9496088 r1.rightDown.y 地址=0x949608c
r2.leftUp 本身地址=0x948a038 r2.rightDown 本身地址=0x948a03c
r2.leftUp 指向地址=0x9496068 r2.rightDown 指向地址=0x94960a0
-
結(jié)構(gòu)體之間可以轉(zhuǎn)換,但是有要求,就是結(jié)構(gòu)體的字段要完全一樣(包括:名字、類型,個數(shù))
type A struct {Num int }type B struct {Num int }func main() {var a Avar b Ba = A(b)fmt.Println(a, b) }
-
結(jié)構(gòu)體進行type重新定義(相當于取別名),Golang認為是新的數(shù)據(jù)類型,但是相互間可以強轉(zhuǎn)。
type Student struct {Name stringAge int
}type Stu Studentfunc main() {var stu1 Studentvar stu2 Stu// stu2 = stu1 // 錯誤,系統(tǒng)認為這是兩個不一樣的類型stu2 = Stu(stu1)fmt.Println(stu1, stu2)
}
-
struct的每個字段上,可以寫上一個標簽tag,該tag可以通過反射機制獲取,常見的使用場景就是序列化和反序列化。
type Student struct {Name string `json:"name"` // 這里就是結(jié)構(gòu)體的標簽tagAge int `json:"age"` }func main() {// 1.創(chuàng)建一個student變量student := Student{"張三", 18}// 2.將monster變量序列化為json格式字符串jsonStr, err := json.Marshal(student) // 這里json.Marshal底層用到了反射if err != nil {fmt.Println("jsonStr報錯")}fmt.Println("jsonStr:", jsonStr)fmt.Println("string(jsonStr):", string(jsonStr)) }
如果這里不加上標簽,生成的json格式,就變成:
string(jsonStr): {"Name":"張三","Age":18}
會發(fā)現(xiàn),這里的Name的首字母是大寫,這又是不可避免,因為小寫就調(diào)用不了
所以這里通過標簽,設(shè)置一個別名,底層用了反射解決這個問題~~~
輸出結(jié)果:
string(jsonStr): {"name":"張三","age":18}
2 方法
在某些情況下,我們需要聲明(定義)方法。比如Person結(jié)構(gòu)體:除了有一些字段外(年齡,姓名…),Person結(jié)構(gòu)體還有一些行為。比如:可以說好,跑步,學(xué)習(xí)等,還可以做算術(shù)題。這時就要用方法才能完成
Golang中的方法是作用在指定的數(shù)據(jù)類型上的(即:和指定的數(shù)據(jù)類型綁定),因此自定義類型,都可以有方法,而不僅僅只是struct。
2.1 方法的聲明和調(diào)用
type A struct {Num int
}func (a A) test() {fmt.Println(a.Num)
}
對上面的語法說明:
- func (a A) test() {} 表示A結(jié)構(gòu)體有一方法,方法名為 test
- (a A) 體現(xiàn)test方法是和A類型綁定的
type Person struct {Name string
}func (p Person) test() {p.Name = "Tom"fmt.Println("test():", p.Name)
}func main() {person := Person{Name: "張三"}person.test() // 調(diào)用方法fmt.Println("main p.Name=", person.Name)
}
輸出結(jié)果:
test(): Tom
main p.Name= 張三
對上面的總結(jié):
-
test方法和Person類型綁定
-
test方法只能通過Person類型的遍歷來調(diào)用,而不能直接調(diào)用,也不嫩更實用其他類型變量來調(diào)用。
-
func (p Person) test() {} 這個p是它的副本,進行的是值傳遞,與函數(shù)很像。
2.2 快速入門案例
-
給Person結(jié)構(gòu)體添加speak 方法,輸出 xxx是一個好人
type Person struct{Name string }//給Person結(jié)構(gòu)體添加speak 方法,輸出 xxx是一個好人 func (p Person) speak() {fmt.Println(p.Name, "是一個goodman~") }
-
給Person結(jié)構(gòu)體添加jisuan 方法,可以計算從 1+…+1000的結(jié)果,
type Person struct {Name string }func (p Person) jisuan() {res := 0for i := 1; i <= 1000; i++ {res += i}fmt.Println(p.Name, "計算的結(jié)果是=", res) }func main() {p := Person{Name: "張三"}p.jisuan() // 輸出:張三 計算的結(jié)果是= 500500 }
-
給Person結(jié)構(gòu)體jisuan2 方法,該方法可以接收一個參數(shù)n,計算從 1+…+n 的結(jié)果
type Person struct {Name string }// 給Person結(jié)構(gòu)體jisuan2 方法,該方法可以接收一個參數(shù)n,計算從 1+..+n 的結(jié)果 func (p Person) jisuan2(n int) {res := 0for i := 1; i <= n; i++ {res += i}fmt.Println(p.Name, "計算的結(jié)果是=", res) }func main() {p := Person{Name: "張三"}p.jisuan2(10) // 輸出:張三 計算的結(jié)果是= 55 }
-
給Person結(jié)構(gòu)體添加getSum方法,可以計算兩個數(shù)的和,并返回結(jié)果
type Person struct {Name string }// 給Person結(jié)構(gòu)體添加getSum方法,可以計算兩個數(shù)的和,并返回結(jié)果 func (p Person) getSum(n1 int, n2 int) int {return n1 + n2 }func main() {p := Person{Name: "張三"}sum := p.getSum(1, 1)fmt.Printf("%v sum=%v", p.Name, sum) //輸出:張三 sum=2 }
2.3 調(diào)用機制和傳參原理
說明:方法的調(diào)用和傳參機制和函數(shù)基本一樣,不一樣的地方是方法調(diào)用時,會將調(diào)用方法的變量,當作實參也傳遞給方法。下面舉例說明:
案例:
type Person struct {Name string
}// 給Person結(jié)構(gòu)體添加getSum方法,可以計算兩個數(shù)的和,并返回結(jié)果
func (p Person) getSum(n1 int, n2 int) int {return n1 + n2
}func main() {p := Person{Name: "張三"}n1 := 10n2 := 20res := p.getSum(n1, n2)fmt.Println("res=", res)
}
- 在通過一個變量去調(diào)用方法時,其調(diào)用機制和函數(shù)一樣
- 不一樣的地方是,變量調(diào)用方法時,該變量本身也會作為一個參數(shù)傳遞到方法(如果變量是值類型,則進行值拷貝,如果是引用類型,則進行地質(zhì)拷貝)
2.4 注意事項和細節(jié)
- 結(jié)構(gòu)體類型是值類型,在方法調(diào)用中,遵守值類型的傳遞機制,是值拷貝傳遞方式
- 如果程序員希望在方法中,修改結(jié)構(gòu)體變量的值,可以通過結(jié)構(gòu)體指針的方式來處理
- Golang中的方法作用在指定的數(shù)據(jù)類型上的(即: 和指定的數(shù)據(jù)類型綁定),因此自定義類型,都可以有方法,而不僅僅是struct,比如int ,float32等都可以有方法
- 方法的訪問范圍控制的規(guī)則,和函數(shù)一樣。方法名首字母小寫,只能在本包4訪問,方法首字母大寫,可以在本包和其它包訪問。
- 如果一個類型實現(xiàn)了String這個方法,那么fmt.Println默認會調(diào)用這個變量的String()進行輸出
上面這些注意事項都比較簡單,就代碼展示一下最后一條:
type Student struct {Name stringAge int
}// 給*Student實現(xiàn)方法String()
func (stu *Student) String() string {str := fmt.Sprintf("Name=[%v] Age=[%v]", stu.Name, stu.Age)return str
}func main() {//定義一個Student變量stu := Student{Name: "tom",Age: 20,}//如果你實現(xiàn)了 *Student 類型的 String方法,就會自動調(diào)用fmt.Println(stu)fmt.Println(&stu)
}
輸出結(jié)果:
{tom 20}
Name=[tom] Age=[20]
2.5 方法和函數(shù)區(qū)別
- 調(diào)用方式不一樣
- 函數(shù)的調(diào)用方式:函數(shù)名(實參列表)
- 方法的調(diào)用方式:變量.方法名(實參列表)
- 數(shù)據(jù)傳遞的限制不一樣
- 對于普通函數(shù),接收者為值類型時,不能將指針類型的數(shù)據(jù)直接傳遞,反之亦然。
- 對于方法(如struct的方法),接收者為值類型時,可以直接用指針類型的遍歷調(diào)用方法,反過來同樣也可以。
這一點很容易迷糊,下面用段代碼解釋一下:
type Person struct {Name string
}//函數(shù)
//對于普通函數(shù),接收者為值類型時,不能將指針類型的數(shù)據(jù)直接傳遞,反之亦然func test01(p Person) {fmt.Println(p.Name)
}func test02(p *Person) {fmt.Println(p.Name)
}//對于方法(如struct的方法),
//接收者為值類型時,可以直接用指針類型的變量調(diào)用方法,反過來同樣也可以func (p Person) test03() {p.Name = "jack"fmt.Println("test03() =", p.Name) // jack
}func (p *Person) test04() {p.Name = "mary"fmt.Println("test03() =", p.Name) // mary
}func main() {p := Person{"tom"}test01(p)test02(&p)p.test03()fmt.Println("main() p.name=", p.Name) // tom(&p).test03() // 從形式上是傳入地址,但是本質(zhì)仍然是值拷貝fmt.Println("main() p.name=", p.Name) // tom(&p).test04()fmt.Println("main() p.name=", p.Name) // maryp.test04() // 等價 (&p).test04 , 從形式上是傳入值類型,但是本質(zhì)仍然是地址拷貝
}
輸出結(jié)果:
tom
tom
test03() = jack
main() p.name= tom
test03() = jack
main() p.name= tom
test03() = mary
main() p.name= mary
test03() = mary
從代碼會發(fā)現(xiàn),僅管傳遞的是一個地址,但是編譯器進行了內(nèi)部優(yōu)化,實際上還是值傳遞,只是支持這種寫法,但并不是進行一個地址值的修改。
3 工廠模式
說明:Golang的結(jié)構(gòu)體沒有構(gòu)造函數(shù),通??梢允褂?strong>工廠模式來解決這個問題
相當于,這個工廠模式,就是以前構(gòu)造函數(shù)的功能。
看個需求:
一個結(jié)構(gòu)體的聲明如下:
package modeltype Student struct {Name string
}
因為這里的 Student 的首字母S是大寫的,如果我們想在其他包串接Student的實例(比如:main包),引入model包后,就可以直接創(chuàng)建Student結(jié)構(gòu)體的變量(實例)。
但是問題來了,如果搜字母是小寫的,比如是: type student struct {…}就不行了,咋辦?-》工廠模式來解決,
Model包
package modeltype student struct {name stringscore float64
}// 因為student結(jié)構(gòu)體首字母是小寫,因此是只能在model使用
// 通過工廠模式來解決
func NewStudent(n string, s float64) *student {return &student{name: n,score: s,}
}// 如果score字段首字母小寫,則,在其他包不可以直接方法,我們可以提供一個方法
func (s *student) GetScore() float64 {return s.score
}// 如果score字段首字母小寫,則,在其他包不可以直接方法,我們可以提供一個方法
func (n *student) GetName() string {return n.name
}
main包
import ("GoStudy_Day1/model""fmt"
)func main() {// 定student結(jié)構(gòu)體是首字母小寫,我們可以通過工廠模式來解決stu := model.NewStudent("Tom", 88.8)fmt.Println(stu)fmt.Println(*stu)// fmt.Println("name=", stu.name) // 報錯,因為是私密的fmt.Println("name=", stu.GetName())fmt.Println("score=", stu.GetScore())
}
輸出結(jié)果:
&{Tom 88.8}
{Tom 88.8}
name= Tom
score= 88.8
其實,這就看出來,就是Java的GetSet,繞了一圈,又回來啦~~~!!!!!