結(jié)構(gòu)體定義的一般方式如下:
type identifier struct {
field1 type1
field2 type2
...
}
type T struct {a, b int}
也是合法的語法,它更適用于簡單的結(jié)構(gòu)體。
結(jié)構(gòu)體里的字段都有 名字,像 field1、field2 等,如果字段在代碼中從來也不會被用到,那么可以命名它為 _。
結(jié)構(gòu)體的字段可以是任何類型,甚至是結(jié)構(gòu)體本身(參考第 10.5 節(jié)),也可以是函數(shù)或者接口(參考第 11 章)??梢月暶鹘Y(jié)構(gòu)體類型的一個變量,然后像下面這樣給它的字段賦值:
var s T
s.a = 5
s.b = 8
數(shù)組可以看作是一種結(jié)構(gòu)體類型,不過它使用下標(biāo)而不是具名的字段。
使用 new
使用 new 函數(shù)給一個新的結(jié)構(gòu)體變量分配內(nèi)存,它返回指向已分配內(nèi)存的指針:var t *T = new(T)
,如果需要可以把這條語句放在不同的行(比如定義是包范圍的,但是分配卻沒有必要在開始就做)。
var t *T
t = new(T)
寫這條語句的慣用方法是:t := new(T)
,變量 t
是一個指向 T
的指針,此時結(jié)構(gòu)體字段的值是它們所屬類型的零值。
聲明 var t T
也會給 t
分配內(nèi)存,并零值化內(nèi)存,但是這個時候 t
是類型T。在這兩種方式中,t
通常被稱做類型 T 的一個實(shí)例(instance)或?qū)ο螅╫bject)。
示例 10.1 structs_fields.go 給出了一個非常簡單的例子:
package main
import "fmt"
type struct1 struct {
i1 int
f1 float32
str string
}
func main() {
ms := new(struct1)
ms.i1 = 10
ms.f1 = 15.5
ms.str= "Chris"
fmt.Printf("The int is: %d\n", ms.i1)
fmt.Printf("The float is: %f\n", ms.f1)
fmt.Printf("The string is: %s\n", ms.str)
fmt.Println(ms)
}
輸出:
The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}
使用 fmt.Println
打印一個結(jié)構(gòu)體的默認(rèn)輸出可以很好的顯示它的內(nèi)容,類似使用 %v 選項(xiàng)。
就像在面向?qū)ο笳Z言所作的那樣,可以使用點(diǎn)號符給字段賦值:structname.fieldname = value
。
同樣的,使用點(diǎn)號符可以獲取結(jié)構(gòu)體字段的值:structname.fieldname
。
在 Go 語言中這叫 選擇器(selector)。無論變量是一個結(jié)構(gòu)體類型還是一個結(jié)構(gòu)體類型指針,都使用同樣的 選擇器符(selector-notation) 來引用結(jié)構(gòu)體的字段:
type myStruct struct { i int }
var v myStruct // v是結(jié)構(gòu)體類型變量
var p *myStruct // p是指向一個結(jié)構(gòu)體類型變量的指針
v.i
p.i
初始化一個結(jié)構(gòu)體實(shí)例(一個結(jié)構(gòu)體字面量:struct-literal)的更簡短和慣用的方式如下:
ms := &struct1{10, 15.5, "Chris"}
// 此時ms的類型是 *struct1
或者:
var ms struct1
ms = struct1{10, 15.5, "Chris"}
混合字面量語法(composite literal syntax)&struct1{a, b, c}
是一種簡寫,底層仍然會調(diào)用 new ()
,這里值的順序必須按照字段順序來寫。在下面的例子中能看到可以通過在值的前面放上字段名來初始化字段的方式。表達(dá)式 new(Type)
和 &Type{}
是等價的。
時間間隔(開始和結(jié)束時間以秒為單位)是使用結(jié)構(gòu)體的一個典型例子:
type Interval struct {
start int
end int
}
初始化方式:
intr := Interval{0, 3} (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)
在(A)中,值必須以字段在結(jié)構(gòu)體定義時的順序給出,& 不是必須的。(B)顯示了另一種方式,字段名加一個冒號放在值的前面,這種情況下值的順序不必一致,并且某些字段還可以被忽略掉,就像(C)中那樣。
結(jié)構(gòu)體類型和字段的命名遵循可見性規(guī)則(第 4.2 節(jié)),一個導(dǎo)出的結(jié)構(gòu)體類型中有些字段是導(dǎo)出的,另一些不是,這是可能的。
下圖說明了結(jié)構(gòu)體類型實(shí)例和一個指向它的指針的內(nèi)存布局:
type Point struct { x, y int }
使用 new 初始化:
作為結(jié)構(gòu)體字面量初始化:
類型 strcut1 在定義它的包 pack1 中必須是唯一的,它的完全類型名是:pack1.struct1
。
下面的例子 Listing 10.2—person.go 顯示了一個結(jié)構(gòu)體 Person,一個方法,方法有一個類型為 *Person
的參數(shù)(因此對象本身是可以被改變的),以及三種調(diào)用這個方法的不同方式:
package main
import (
"fmt"
"strings"
)
type Person struct {
firstName string
lastName string
}
func upPerson(p *Person) {
p.firstName = strings.ToUpper(p.firstName)
p.lastName = strings.ToUpper(p.lastName)
}
func main() {
// 1-struct as a value type:
var pers1 Person
pers1.firstName = "Chris"
pers1.lastName = "Woodward"
upPerson(&pers1)
fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)
// 2—struct as a pointer:
pers2 := new(Person)
pers2.firstName = "Chris"
pers2.lastName = "Woodward"
(*pers2).lastName = "Woodward" // 這是合法的
upPerson(pers2)
fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)
// 3—struct as a literal:
pers3 := &Person{"Chris","Woodward"}
upPerson(pers3)
fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}
輸出:
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD
在上面例子的第二種情況中,可以直接通過指針,像 pers2.lastName="Woodward"
這樣給結(jié)構(gòu)體字段賦值,沒有像 C++ 中那樣需要使用 ->
操作符,Go 會自動做這樣的轉(zhuǎn)換。
注意也可以通過解指針的方式來設(shè)置值:(*pers2).lastName = "Woodward"
結(jié)構(gòu)體的內(nèi)存布局
Go 語言中,結(jié)構(gòu)體和它所包含的數(shù)據(jù)在內(nèi)存中是以連續(xù)塊的形式存在的,即使結(jié)構(gòu)體中嵌套有其他的結(jié)構(gòu)體,這在性能上帶來了很大的優(yōu)勢。不像 Java 中的引用類型,一個對象和它里面包含的對象可能會在不同的內(nèi)存空間中,這點(diǎn)和 Go 語言中的指針很像。下面的例子清晰地說明了這些情況:
type Rect1 struct {Min, Max Point }
type Rect2 struct {Min, Max *Point }
遞歸結(jié)構(gòu)體
結(jié)構(gòu)體類型可以通過引用自身來定義。這在定義鏈表或二叉樹的元素(通常叫節(jié)點(diǎn))時特別有用,此時節(jié)點(diǎn)包含指向臨近節(jié)點(diǎn)的鏈接(地址)。如下所示,鏈表中的 su
,樹中的 ri
和 le
分別是指向別的節(jié)點(diǎn)的指針。
鏈表:
這塊的 data
字段用于存放有效數(shù)據(jù)(比如 float64),su
指針指向后繼節(jié)點(diǎn)。
Go 代碼:
type Node struct {
data float64
su *Node
}
鏈表中的第一個元素叫 head
,它指向第二個元素;最后一個元素叫 tail
,它沒有后繼元素,所以它的 su
為 nil 值。當(dāng)然真實(shí)的鏈接會有很多數(shù)據(jù)節(jié)點(diǎn),并且鏈表可以動態(tài)增長或收縮。
同樣地可以定義一個雙向鏈表,它有一個前趨節(jié)點(diǎn) pr
和一個后繼節(jié)點(diǎn) su
:
type Node struct {
pr *Node
data float64
su *Node
}
二叉樹:
二叉樹中每個節(jié)點(diǎn)最多能鏈接至兩個節(jié)點(diǎn):左節(jié)點(diǎn)(le)和右節(jié)點(diǎn)(ri),這兩個節(jié)點(diǎn)本身又可以有左右節(jié)點(diǎn),依次類推。樹的頂層節(jié)點(diǎn)叫根節(jié)點(diǎn)(root),底層沒有子節(jié)點(diǎn)的節(jié)點(diǎn)叫葉子節(jié)點(diǎn)(leaves),葉子節(jié)點(diǎn)的 le
和 ri
指針為 nil 值。在 Go 中可以如下定義二叉樹:
type Tree strcut {
le *Tree
data float64
ri *Tree
}
結(jié)構(gòu)體轉(zhuǎn)換
Go 中的類型轉(zhuǎn)換遵循嚴(yán)格的規(guī)則。當(dāng)為結(jié)構(gòu)體定義了一個 alias 類型時,此結(jié)構(gòu)體類型和它的 alias 類型都有相同的底層類型,它們可以如示例 10.3 那樣互相轉(zhuǎn)換,同時需要注意其中非法賦值或轉(zhuǎn)換引起的編譯錯誤。
示例 10.3:
package main
import "fmt"
type number struct {
f float32
}
type nr number // alias type
func main() {
a := number{5.0}
b := nr{5.0}
// var i float32 = b // compile-error: cannot use b (type nr) as type float32 in assignment
// var i = float32(b) // compile-error: cannot convert b (type nr) to type float32
// var c number = b // compile-error: cannot use b (type nr) as type number in assignment
// needs a conversion:
var c = number(b)
fmt.Println(a, b, c)
}
輸出:
{5} {5} {5}
練習(xí) 10.1 vcard.go:
定義結(jié)構(gòu)體 Address 和 VCard,后者包含一個人的名字、地址編號、出生日期和圖像,試著選擇正確的數(shù)據(jù)類型。構(gòu)建一個自己的 vcard 并打印它的內(nèi)容。
提示:
VCard 必須包含住址,它應(yīng)該以值類型還是以指針類型放在 VCard 中呢?
第二種會好點(diǎn),因?yàn)樗加脙?nèi)存少。包含一個名字和兩個指向地址的指針的 Address 結(jié)構(gòu)體可以使用 %v 打?。?{Kersschot 0x126d2b80 0x126d2be0}
練習(xí) 10.2 persionext1.go:
修改 persionext1.go,使它的參數(shù) upPerson 不是一個指針,解釋下二者的區(qū)別。
練習(xí) 10.3 point.go:
使用坐標(biāo) X、Y 定義一個二維 Point 結(jié)構(gòu)體。同樣地,對一個三維點(diǎn)使用它的極坐標(biāo)定義一個 Polar 結(jié)構(gòu)體。實(shí)現(xiàn)一個 Abs()
方法來計算一個 Point 表示的向量的長度,實(shí)現(xiàn)一個 Scale
方法,它將點(diǎn)的坐標(biāo)乘以一個尺度因子(提示:使用 math
包里的 Sqrt
函數(shù))(function Scale that multiplies the coordinates of a point with a scale
factor)。
練習(xí) 10.3 rectangle.go:
定義一個 Rectangle 結(jié)構(gòu)體,它的長和寬是 int 類型,并定義方法 Area()
和 Perimeter()
,然后進(jìn)行測試。