一、結(jié)構(gòu)體
《快學(xué) Go 語(yǔ)言》第 8 課 —— 結(jié)構(gòu)體
1.結(jié)構(gòu)體類型的定義
結(jié)構(gòu)體和其它高級(jí)語(yǔ)言里的「類」比較類似。下面我們使用結(jié)構(gòu)體語(yǔ)法來(lái)定義一個(gè)「圓」型
type Circle struct {
x int
y int
Radius int
}
Circle 結(jié)構(gòu)體內(nèi)部有三個(gè)變量,分別是圓心的坐標(biāo)以及半徑。特別需要注意是結(jié)構(gòu)體內(nèi)部變量的大小寫,首字母大寫是公開(kāi)變量,首字母小寫是內(nèi)部變量,分別相當(dāng)于類成員變量的 Public 和 Private 類別。內(nèi)部變量只有屬于同一個(gè) package(簡(jiǎn)單理解就是同一個(gè)目錄)的代碼才能直接訪問(wèn)。
2.創(chuàng)建
func main() {
var c Circle = Circle {
x: 100,
y: 100,
Radius: 50, // 注意這里的逗號(hào)不能少
}
fmt.Printf("%+v\n", c)
}
----------
{x:100 y:100 Radius:50}
可以只指定部分字段的初值,甚至可以一個(gè)字段都不指定,那些沒(méi)有指定初值的字段會(huì)自動(dòng)初始化為相應(yīng)類型的「零值」。
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}
結(jié)構(gòu)體的第二種創(chuàng)建形式是不指定字段名稱來(lái)順序字段初始化,需要顯示提供所有字段的初值,一個(gè)都不能少。這種形式稱之為「順序形式」。var c Circle = Circle {100, 100, 50}
結(jié)構(gòu)體變量創(chuàng)建的第三種形式,使用全局的 new() 函數(shù)來(lái)創(chuàng)建一個(gè)「零值」結(jié)構(gòu)體,所有的字段都被初始化為相應(yīng)類型的零值。var c *Circle = new(Circle)注意 new() 函數(shù)返回的是指針類型。
第四種創(chuàng)建形式,這種形式也是零值初始化,就數(shù)它看起來(lái)最不雅觀。var c Circle
3.零值結(jié)構(gòu)體和 nil 結(jié)構(gòu)體
nil 結(jié)構(gòu)體是指結(jié)構(gòu)體指針變量沒(méi)有指向一個(gè)實(shí)際存在的內(nèi)存。這樣的指針變量只會(huì)占用 1 個(gè)指針的存儲(chǔ)空間,也就是一個(gè)機(jī)器字的內(nèi)存大小。
var c *Circle = nil
而零值結(jié)構(gòu)體是會(huì)實(shí)實(shí)在在占用內(nèi)存空間的,只不過(guò)每個(gè)字段都是零值。如果結(jié)構(gòu)體里面字段非常多,那么這個(gè)內(nèi)存空間占用肯定也會(huì)很大。
4.結(jié)構(gòu)體的拷貝
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.無(wú)處不在的結(jié)構(gòu)體
通過(guò)觀察 Go 語(yǔ)言的底層源碼,可以發(fā)現(xiàn)所有的 Go 語(yǔ)言內(nèi)置的高級(jí)數(shù)據(jù)結(jié)構(gòu)都是由結(jié)構(gòu)體來(lái)完成的。
切片頭的結(jié)構(gòu)體形式如下,它在 64 位機(jī)器上將會(huì)占用 24 個(gè)字節(jié)
type slice struct {
array unsafe.Pointer // 底層數(shù)組的地址
len int // 長(zhǎng)度
cap int // 容量
}
字符串頭的結(jié)構(gòu)體形式,它在 64 位機(jī)器上將會(huì)占用 16 個(gè)字節(jié)
type string struct {
array unsafe.Pointer // 底層數(shù)組的地址
len int
}
字典頭的結(jié)構(gòu)體形式
type hmap struct {
count int
...
buckets unsafe.Pointer // hash桶地址
...
}
6.結(jié)構(gòu)體的參數(shù)傳遞
函數(shù)調(diào)用時(shí)參數(shù)傳遞結(jié)構(gòu)體變量,Go 語(yǔ)言支持值傳遞,也支持指針傳遞。值傳遞涉及到結(jié)構(gòu)體字段的淺拷貝,指針傳遞會(huì)共享結(jié)構(gòu)體內(nèi)容,只會(huì)拷貝指針地址,規(guī)則上和賦值是等價(jià)的。下面我們使用兩種傳參方式來(lái)編寫擴(kuò)大圓半徑的函數(shù)。
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}
從上面的輸出中可以看到通過(guò)值傳遞,在函數(shù)里面修改結(jié)構(gòu)體的狀態(tài)不會(huì)影響到原有結(jié)構(gòu)體的狀態(tài),函數(shù)內(nèi)部的邏輯并沒(méi)有產(chǎn)生任何效果。通過(guò)指針傳遞就不一樣。
7.結(jié)構(gòu)體方法
Go 語(yǔ)言不是面向?qū)ο蟮恼Z(yǔ)言,它里面不存在類的概念,結(jié)構(gòu)體正是類的替代品。類可以附加很多成員方法,結(jié)構(gòu)體也可以。
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)
}
// 周長(zhǎng)
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())
// 指針變量調(diào)用方法形式上是一樣的
var pc = &c
fmt.Println(pc.Area(), pc.Circumference())
}
-----------
7853.981633974483 314.1592653589793
7853.981633974483 314.1592653589793
Go 語(yǔ)言不喜歡類型的隱式轉(zhuǎn)換,所以需要將整形顯示轉(zhuǎn)換成浮點(diǎn)型,不是很好看,不過(guò)這就是 Go 語(yǔ)言的基本規(guī)則,顯式的代碼可能不夠簡(jiǎn)潔,但是易于理解。
Go 語(yǔ)言的結(jié)構(gòu)體方法里面沒(méi)有 self 和 this 這樣的關(guān)鍵字來(lái)指代當(dāng)前的對(duì)象,它是用戶自己定義的變量名稱,通常我們都使用單個(gè)字母來(lái)表示。
Go 語(yǔ)言的方法名稱也分首字母大小寫,它的權(quán)限規(guī)則和字段一樣,首字母大寫就是公開(kāi)方法,首字母小寫就是內(nèi)部方法,只能歸屬于同一個(gè)包的代碼才可以訪問(wèn)內(nèi)部方法。
結(jié)構(gòu)體的值類型和指針類型訪問(wèn)內(nèi)部字段和方法在形式上是一樣的。這點(diǎn)不同于 C++ 語(yǔ)言,在 C++ 語(yǔ)言里,值訪問(wèn)使用句點(diǎn) . 操作符,而指針訪問(wèn)需要使用箭頭 -> 操作符。
8.關(guān)于GO如何實(shí)現(xiàn)面對(duì)對(duì)象的繼承、多態(tài),是個(gè)有趣的話題。參考go是面向?qū)ο笳Z(yǔ)言嗎?
9.創(chuàng)建遞歸的數(shù)據(jù)結(jié)構(gòu)
《go語(yǔ)言圣經(jīng)》P145
一個(gè)命名為S的結(jié)構(gòu)體類型將不能再包含S類型的成員:因?yàn)橐粋€(gè)聚合的值不能包含它自身。(該限制同樣適應(yīng)于數(shù)組。)但是S類型的結(jié)構(gòu)體可以包含 *S 指針類型的成員,這可以讓我們創(chuàng)建遞歸的數(shù)據(jù)結(jié)構(gòu),比如鏈表和樹(shù)結(jié)構(gòu)等。在下面的代碼中,我們使用一個(gè)二叉樹(shù)來(lái)實(shí)現(xiàn)一個(gè)插入排序:
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.結(jié)構(gòu)體的比較
《go語(yǔ)言圣經(jīng)》P147
如果結(jié)構(gòu)體的全部成員都是可以比較的,那么結(jié)構(gòu)體也是可以比較的,那樣的話兩個(gè)結(jié)構(gòu)體將可以使用==或!=運(yùn)算符進(jìn)行比較。相等比較運(yùn)算符==將比較兩個(gè)結(jié)構(gòu)體的每個(gè)成員,因此下面兩個(gè)比較的表達(dá)式是等價(jià)的:
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.匿名結(jié)構(gòu)體
《go語(yǔ)言圣經(jīng)》P149
type Point struct {
X, Y int
}
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Circle
Spokes int
}
這樣改動(dòng)之后結(jié)構(gòu)體類型變的清晰了,但是這種修改同時(shí)也導(dǎo)致了訪問(wèn)每個(gè)成員變得繁瑣:
var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
Go語(yǔ)言有一個(gè)特性讓我們只聲明一個(gè)成員對(duì)應(yīng)的數(shù)據(jù)類型而不指名成員的名字;這類成員就叫匿名成員。匿名成員的數(shù)據(jù)類型必須是命名的類型或指向一個(gè)命名的類型的指針。下面的代碼中,Circle和Wheel各自都有一個(gè)匿名成員。我們可以說(shuō)Point類型被嵌入到了Circle結(jié)構(gòu)體,同時(shí)Circle類型被嵌入到了Wheel結(jié)構(gòu)體。
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
得益于匿名嵌入的特性,我們可以直接訪問(wèn)葉子屬性而不需要給出完整的路徑:
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
在右邊的注釋中給出的顯式形式訪問(wèn)這些葉子成員的語(yǔ)法依然有效,因此匿名成員并不是真的無(wú)法訪問(wèn)了。其中匿名成員Circle和Point都有自己的名字——就是命名的類型名字——但是這些名字在點(diǎn)操作符中是可選的。我們?cè)谠L問(wèn)子成員的時(shí)候可以忽略任何匿名成員部分。
不幸的是,結(jié)構(gòu)體字面值并沒(méi)有簡(jiǎn)短表示匿名成員的語(yǔ)法, 因此下面的語(yǔ)句都不能編譯通過(guò):
w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
結(jié)構(gòu)體字面值必須遵循形狀類型聲明時(shí)的結(jié)構(gòu),所以我們只能用下面的兩種語(yǔ)法,它們彼此是等價(jià)的:
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函數(shù)中%v參數(shù)包含的#副詞,它表示用和Go語(yǔ)言類似的語(yǔ)法打印值。對(duì)于結(jié)構(gòu)體類型來(lái)說(shuō),將包含每個(gè)成員的名字。
因?yàn)槟涿蓡T也有一個(gè)隱式的名字,因此不能同時(shí)包含兩個(gè)類型相同的匿名成員,這會(huì)導(dǎo)致名字沖突。同時(shí),因?yàn)槌蓡T的名字是由其類型隱式地決定的,所有匿名成員也有可見(jiàn)性的規(guī)則約束。在上面的例子中,Point和Circle匿名成員都是導(dǎo)出的。即使它們不導(dǎo)出(比如改成小寫字母開(kāi)頭的point和circle),我們依然可以用簡(jiǎn)短形式訪問(wèn)匿名成員嵌套的成員
w.X = 8 // equivalent to w.circle.point.X = 8
但是在包外部,因?yàn)閏ircle和point沒(méi)有導(dǎo)出不能訪問(wèn)它們的成員,因此簡(jiǎn)短的匿名成員訪問(wèn)語(yǔ)法也是禁止的。
到目前為止,我們看到匿名成員特性只是對(duì)訪問(wèn)嵌套成員的點(diǎn)運(yùn)算符提供了簡(jiǎn)短的語(yǔ)法糖。稍后,我們將會(huì)看到匿名成員并不要求是結(jié)構(gòu)體類型;其實(shí)任何命名的類型都可以作為結(jié)構(gòu)體的匿名成員。但是為什么要嵌入一個(gè)沒(méi)有任何子成員類型的匿名成員類型呢?答案是匿名類型的方法集。簡(jiǎn)短的點(diǎn)運(yùn)算符語(yǔ)法可以用于選擇匿名成員嵌套的成員,也可以用于訪問(wèn)它們的方法。實(shí)際上,外層的結(jié)構(gòu)體不僅僅是獲得了匿名成員類型的所有成員,而且也獲得了該類型導(dǎo)出的全部的方法。這個(gè)機(jī)制可以用于將一個(gè)有簡(jiǎn)單行為的對(duì)象組合成有復(fù)雜行為的對(duì)象。