Golang 學習筆記四 結構體 介面
一、結構體
1.結構體型別的定義
結構體和其它高階語言裡的「類」比較類似。下面我們使用結構體語法來定義一個「圓」型
type Circle struct { x int y int Radius int }
Circle 結構體內部有三個變數,分別是圓心的座標以及半徑。特別需要注意是結構體內部變數的大小寫,首字母大寫是公開變數,首字母小寫是內部變數,分別相當於類成員變數的 Public 和 Private 類別。內部變數只有屬於同一個 package(簡單理解就是同一個目錄)的程式碼才能直接訪問。
2.建立
func main() { var c Circle = Circle { x: 100, y: 100, Radius: 50,// 注意這裡的逗號不能少 } fmt.Printf("%+v\n", c) } ---------- {x:100 y:100 Radius:50}
可以只指定部分欄位的初值,甚至可以一個欄位都不指定,那些沒有指定初值的欄位會自動初始化為相應型別的「零值」。
func main() { var c1 Circle = Circle { Radius: 50, } var c2 Circle = Circle {} fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) } ---------- {x:0 y:0 Radius:50} {x:0 y:0 Radius:0}
結構體的第二種建立形式是不指定欄位名稱來順序欄位初始化,需要顯示提供所有欄位的初值,一個都不能少。這種形式稱之為「順序形式」。var c Circle = Circle {100, 100, 50}
結構體變數建立的第三種形式,使用全域性的 new() 函式來建立一個「零值」結構體,所有的欄位都被初始化為相應型別的零值。var c *Circle = new(Circle)
注意 new() 函式返回的是指標型別。
第四種建立形式,這種形式也是零值初始化,就數它看起來最不雅觀。var c Circle
3.零值結構體和 nil 結構體
nil 結構體是指結構體指標變數沒有指向一個實際存在的記憶體。這樣的指標變數只會佔用 1 個指標的儲存空間,也就是一個機器字的記憶體大小。
var c *Circle = nil
而零值結構體是會實實在在佔用記憶體空間的,只不過每個欄位都是零值。如果結構體裡面欄位非常多,那麼這個記憶體空間佔用肯定也會很大。
4.結構體的拷貝
func main() { var c1 Circle = Circle {Radius: 50} var c2 Circle = c1 fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) c1.Radius = 100 fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) var c3 *Circle = &Circle {Radius: 50} var c4 *Circle = c3 fmt.Printf("%+v\n", c3) fmt.Printf("%+v\n", c4) c3.Radius = 100 fmt.Printf("%+v\n", c3) fmt.Printf("%+v\n", c4) } --------------- {x:0 y:0 Radius:50} {x:0 y:0 Radius:50} {x:0 y:0 Radius:100} {x:0 y:0 Radius:50} &{x:0 y:0 Radius:50} &{x:0 y:0 Radius:50} &{x:0 y:0 Radius:100} &{x:0 y:0 Radius:100}
5.無處不在的結構體
通過觀察 Go 語言的底層原始碼,可以發現所有的 Go 語言內建的高階資料結構都是由結構體來完成的。
切片頭的結構體形式如下,它在 64 位機器上將會佔用 24 個位元組
type slice struct { array unsafe.Pointer// 底層陣列的地址 len int // 長度 cap int // 容量 }
字串頭的結構體形式,它在 64 位機器上將會佔用 16 個位元組
type string struct { array unsafe.Pointer // 底層陣列的地址 len int }
字典頭的結構體形式
type hmap struct { count int ... buckets unsafe.Pointer// hash桶地址 ... }
6.結構體的引數傳遞
函式呼叫時引數傳遞結構體變數,Go 語言支援值傳遞,也支援指標傳遞。值傳遞涉及到結構體欄位的淺拷貝,指標傳遞會共享結構體內容,只會拷貝指標地址,規則上和賦值是等價的。下面我們使用兩種傳參方式來編寫擴大圓半徑的函式。
package main import "fmt" type Circle struct { x int y int Radius int } func expandByValue(c Circle) { c.Radius *= 2 } func expandByPointer(c *Circle) { c.Radius *= 2 } func main() { var c = Circle {Radius: 50} expandByValue(c) fmt.Println(c) expandByPointer(&c) fmt.Println(c) } --------- {0 0 50} {0 0 100}
從上面的輸出中可以看到通過值傳遞,在函式裡面修改結構體的狀態不會影響到原有結構體的狀態,函式內部的邏輯並沒有產生任何效果。通過指標傳遞就不一樣。
7.結構體方法
Go 語言不是面向物件的語言,它裡面不存在類的概念,結構體正是類的替代品。類可以附加很多成員方法,結構體也可以。
package main import "fmt" import "math" type Circle struct { x int y int Radius int } // 面積 func (c Circle) Area() float64 { return math.Pi * float64(c.Radius) * float64(c.Radius) } // 周長 func (c Circle) Circumference() float64 { return 2 * math.Pi * float64(c.Radius) } func main() { var c = Circle {Radius: 50} fmt.Println(c.Area(), c.Circumference()) // 指標變數呼叫方法形式上是一樣的 var pc = &c fmt.Println(pc.Area(), pc.Circumference()) } ----------- 7853.981633974483 314.1592653589793 7853.981633974483 314.1592653589793
Go 語言不喜歡型別的隱式轉換,所以需要將整形顯示轉換成浮點型,不是很好看,不過這就是 Go 語言的基本規則,顯式的程式碼可能不夠簡潔,但是易於理解。
Go 語言的結構體方法裡面沒有 self 和 this 這樣的關鍵字來指代當前的物件,它是使用者自己定義的變數名稱,通常我們都使用單個字母來表示。
Go 語言的方法名稱也分首字母大小寫,它的許可權規則和欄位一樣,首字母大寫就是公開方法,首字母小寫就是內部方法,只能歸屬於同一個包的程式碼才可以訪問內部方法。
結構體的值型別和指標型別訪問內部欄位和方法在形式上是一樣的。這點不同於 C++ 語言,在 C++ 語言裡,值訪問使用句點 . 操作符,而指標訪問需要使用箭頭 -> 操作符。
8.關於GO如何實現面對物件的繼承、多型,是個有趣的話題。參考go是面嚮物件語言嗎?
9.建立遞迴的資料結構
《go語言聖經》P145
一個命名為S的結構體型別將不能再包含S型別的成員:因為一個聚合的值不能包含它自身。(該限制同樣適應於陣列。)但是S型別的結構體可以包含 *S指標型別的成員,這可以讓我們建立遞迴的資料結構,比如連結串列和樹結構等。在下面的程式碼中,我們使用一個二叉樹來實現一個插入排序:
type tree struct { value int left, right *tree } // Sort sorts values in place. func Sort(values []int) { var root *tree for _, v := range values { root = add(root, v) } appendValues(values[:0], root) } // appendValues appends the elements of t to values in order // and returns the resulting slice. func appendValues(values []int, t *tree) []int { if t != nil { values = appendValues(values, t.left) values = append(values, t.value) values = appendValues(values, t.right) } return values } func add(t *tree, value int) *tree { if t == nil { // Equivalent to return &tree{value: value}. t = new(tree) t.value = value return t } if value < t.value { t.left = add(t.left, value) } else { t.right = add(t.right, value) } return t }
10.結構體的比較
《go語言聖經》P147
如果結構體的全部成員都是可以比較的,那麼結構體也是可以比較的,那樣的話兩個結構體將可以使用==或!=運算子進行比較。相等比較運算子==將比較兩個結構體的每個成員,因此下面兩個比較的表示式是等價的:
type Point struct{ X, Y int } p := Point{1, 2} q := Point{2, 1} fmt.Println(p.X == q.X && p.Y == q.Y) // "false" fmt.Println(p == q) // "false"
11.匿名結構體
《go語言聖經》P149
type Point struct { X, Y int } type Circle struct { Center Point Radius int } type Wheel struct { Circle Circle Spokes int }
這樣改動之後結構體型別變的清晰了,但是這種修改同時也導致了訪問每個成員變得繁瑣:
var w Wheel w.Circle.Center.X = 8 w.Circle.Center.Y = 8 w.Circle.Radius = 5 w.Spokes = 20
Go語言有一個特性讓我們只宣告一個成員對應的資料型別而不指名成員的名字;這類成員就叫匿名成員。匿名成員的資料型別必須是命名的型別或指向一個命名的型別的指標。下面的程式碼中,Circle和Wheel各自都有一個匿名成員。我們可以說Point型別被嵌入到了Circle結構體,同時Circle型別被嵌入到了Wheel結構體。
type Circle struct { Point Radius int } type Wheel struct { Circle Spokes int }
得益於匿名嵌入的特性,我們可以直接訪問葉子屬性而不需要給出完整的路徑:
var w Wheel w.X = 8 // equivalent to w.Circle.Point.X = 8 w.Y = 8 // equivalent to w.Circle.Point.Y = 8 w.Radius = 5 // equivalent to w.Circle.Radius = 5 w.Spokes = 20
在右邊的註釋中給出的顯式形式訪問這些葉子成員的語法依然有效,因此匿名成員並不是真的無法訪問了。其中匿名成員Circle和Point都有自己的名字——就是命名的型別名字——但是這些名字在點操作符中是可選的。我們在訪問子成員的時候可以忽略任何匿名成員部分。
不幸的是,結構體字面值並沒有簡短表示匿名成員的語法, 因此下面的語句都不能編譯通過:
w = Wheel{8, 8, 5, 20} // compile error: unknown fields w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
結構體字面值必須遵循形狀型別宣告時的結構,所以我們只能用下面的兩種語法,它們彼此是等價的:
gopl.io/ch4/embed w = Wheel{Circle{Point{8, 8}, 5}, 20} w = Wheel{ Circle: Circle{ Point: Point{X: 8, Y: 8}, Radius: 5, }, Spokes: 20, // NOTE: trailing comma necessary here (and at Radius) } fmt.Printf("%#v\n", w)
Output:Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
需要注意的是Printf函式中%v引數包含的#副詞,它表示用和Go語言類似的語法列印值。對於結構體型別來說,將包含每個成員的名字。
因為匿名成員也有一個隱式的名字,因此不能同時包含兩個型別相同的匿名成員,這會導致名字衝突。同時,因為成員的名字是由其型別隱式地決定的,所有匿名成員也有可見性的規則約束。在上面的例子中,Point和Circle匿名成員都是匯出的。即使它們不匯出(比如改成小寫字母開頭的point和circle),我們依然可以用簡短形式訪問匿名成員巢狀的成員
w.X = 8 // equivalent to w.circle.point.X = 8
但是在包外部,因為circle和point沒有匯出不能訪問它們的成員,因此簡短的匿名成員訪問語法也是禁止的。
到目前為止,我們看到匿名成員特性只是對訪問巢狀成員的點運算子提供了簡短的語法糖。稍後,我們將會看到匿名成員並不要求是結構體型別;其實任何命名的型別都可以作為結構體的匿名成員。但是為什麼要嵌入一個沒有任何子成員型別的匿名成員型別呢?答案是匿名型別的方法集。簡短的點運算子語法可以用於選擇匿名成員巢狀的成員,也可以用於訪問它們的方法。實際上,外層的結構體不僅僅是獲得了匿名成員型別的所有成員,而且也獲得了該型別匯出的全部的方法。 這個機制可以用於將一個有簡單行為的物件組合成有複雜行為的物件。
二、介面
1.介面定義
Go 語言的介面型別非常特別,它的作用和 Java 語言的介面一樣,但是在形式上有很大的差別。Java 語言需要在類的定義上顯式實現了某些介面,才可以說這個類具備了介面定義的能力。但是 Go 語言的介面是隱式的,只要結構體上定義的方法在形式上(名稱、引數和返回值)和介面定義的一樣,那麼這個結構體就自動實現了這個介面,我們就可以使用這個介面變數來指向這個結構體物件。下面我們看個例子
package main import "fmt" // 可以聞 type Smellable interface { smell() } // 可以吃 type Eatable interface { eat() } // 蘋果既可能聞又能吃 type Apple struct {} func (a Apple) smell() { fmt.Println("apple can smell") } func (a Apple) eat() { fmt.Println("apple can eat") } // 花只可以聞 type Flower struct {} func (f Flower) smell() { fmt.Println("flower can smell") } func main() { var s1 Smellable var s2 Eatable var apple = Apple{} var flower = Flower{} s1 = apple s1.smell() s1 = flower s1.smell() s2 = apple s2.eat() } -------------------- apple can smell flower can smell apple can eat
上面的程式碼定義了兩種介面,Apple 結構體同時實現了這兩個介面,而 Flower 結構體只實現了 Smellable 介面。我們並沒有使用類似於 Java 語言的 implements 關鍵字,結構體和介面就自動產生了關聯。
2.空介面
如果一個接口裡面沒有定義任何方法,那麼它就是空介面,任意結構體都隱式地實現了空介面。
Go 語言為了避免使用者重複定義很多空介面,它自己內建了一個,這個空介面的名字特別奇怪,叫 interface{} ,初學者會非常不習慣。之所以這個型別名帶上了大括號,那是在告訴使用者括號裡什麼也沒有。我始終認為這種名字很古怪,它讓程式碼看起來有點醜陋。
空接口裡面沒有方法,所以它也不具有任何能力,其作用相當於 Java 的 Object 型別,可以容納任意物件,它是一個萬能容器。比如一個字典的 key 是字串,但是希望 value 可以容納任意型別的物件,類似於 Java 語言的 Map 型別,這時候就可以使用空介面型別 interface{}。
package main import "fmt" func main() { // 連續兩個大括號,是不是看起來很彆扭 var user = map[string]interface{}{ "age": 30, "address": "Beijing Tongzhou", "married": true, } fmt.Println(user) // 型別轉換語法來了 var age = user["age"].(int) var address = user["address"].(string) var married = user["married"].(bool) fmt.Println(age, address, married) } ------------- map[age:30 address:Beijing Tongzhou married:true] 30 Beijing Tongzhou true
程式碼中 user 字典變數的型別是 map[string]interface{},從這個字典中直接讀取得到的 value 型別是 interface{},需要通過型別轉換才能得到期望的變數。
3.用介面來模擬多型
package main import "fmt" type Fruitable interface { eat() } type Fruit struct { Name string// 屬性變數 Fruitable// 匿名內嵌介面變數 } func (f Fruit) want() { fmt.Printf("I like ") f.eat() // 外結構體會自動繼承匿名內嵌變數的方法 } type Apple struct {} func (a Apple) eat() { fmt.Println("eating apple") } type Banana struct {} func (b Banana) eat() { fmt.Println("eating banana") } func main() { var f1 = Fruit{"Apple", Apple{}} var f2 = Fruit{"Banana", Banana{}} f1.want() f2.want() } --------- I like eating apple I like eating banana
使用這種方式模擬多型本質上是通過組合屬性變數(Name)和介面變數(Fruitable)來做到的,屬性變數是物件的資料,而介面變數是物件的功能,將它們組合到一塊就形成了一個完整的多型性的結構體。
4.介面的組合繼承
介面的定義也支援組合繼承,比如我們可以將兩個介面定義合併為一個介面如下
type Smellable interface { smell() } type Eatable interface { eat() } type Fruitable interface { Smellable Eatable }
這時 Fruitable 介面就自動包含了 smell() 和 eat() 兩個方法,它和下面的定義是等價的。
type Fruitable interface { smell() eat() }
5.介面變數的賦值
變數賦值本質上是一次記憶體淺拷貝,切片的賦值是拷貝了切片頭,字串的賦值是拷貝了字串的頭部,而陣列的賦值呢是直接拷貝整個陣列。介面變數的賦值會不會不一樣呢?接下來我們做一個實驗
package main import "fmt" type Rect struct { Width int Height int } func main() { var a interface {} var r = Rect{50, 50} a = r var rx = a.(Rect) r.Width = 100 r.Height = 100 fmt.Println(rx) } ------ {50 50}