結(jié)構(gòu)體定義的一般方式如下:
type identifier struct {
field1 type1
field2 type2
...
}
type T struct {a, b int} 也是合法的語(yǔ)法,它更適用于簡(jiǎn)單的結(jié)構(gòu)體。
結(jié)構(gòu)體里的字段都有 名字,像 field1、field2 等,如果字段在代碼中從來(lái)也不會(huì)被用到,那么可以命名它為 _。
結(jié)構(gòu)體的字段可以是任何類型,甚至是結(jié)構(gòu)體本身(參考第 10.5 節(jié)),也可以是函數(shù)或者接口(參考第 11 章)??梢月暶鹘Y(jié)構(gòu)體類型的一個(gè)變量,然后像下面這樣給它的字段賦值:
var s T
s.a = 5
s.b = 8
數(shù)組可以看作是一種結(jié)構(gòu)體類型,不過(guò)它使用下標(biāo)而不是具名的字段。
使用 new
使用 new 函數(shù)給一個(gè)新的結(jié)構(gòu)體變量分配內(nèi)存,它返回指向已分配內(nèi)存的指針:var t *T = new(T),如果需要可以把這條語(yǔ)句放在不同的行(比如定義是包范圍的,但是分配卻沒(méi)有必要在開(kāi)始就做)。
var t *T
t = new(T)
寫(xiě)這條語(yǔ)句的慣用方法是:t := new(T),變量 t 是一個(gè)指向 T的指針,此時(shí)結(jié)構(gòu)體字段的值是它們所屬類型的零值。
聲明 var t T 也會(huì)給 t 分配內(nèi)存,并零值化內(nèi)存,但是這個(gè)時(shí)候 t 是類型T。在這兩種方式中,t 通常被稱做類型 T 的一個(gè)實(shí)例(instance)或?qū)ο螅╫bject)。
示例 10.1 structs_fields.go 給出了一個(gè)非常簡(jiǎn)單的例子:
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 打印一個(gè)結(jié)構(gòu)體的默認(rèn)輸出可以很好的顯示它的內(nèi)容,類似使用 %v 選項(xiàng)。
就像在面向?qū)ο笳Z(yǔ)言所作的那樣,可以使用點(diǎn)號(hào)符給字段賦值:structname.fieldname = value。
同樣的,使用點(diǎn)號(hào)符可以獲取結(jié)構(gòu)體字段的值:structname.fieldname。
在 Go 語(yǔ)言中這叫 選擇器(selector)。無(wú)論變量是一個(gè)結(jié)構(gòu)體類型還是一個(gè)結(jié)構(gòu)體類型指針,都使用同樣的 選擇器符(selector-notation) 來(lái)引用結(jié)構(gòu)體的字段:
type myStruct struct { i int }
var v myStruct // v是結(jié)構(gòu)體類型變量
var p *myStruct // p是指向一個(gè)結(jié)構(gòu)體類型變量的指針
v.i
p.i
初始化一個(gè)結(jié)構(gòu)體實(shí)例(一個(gè)結(jié)構(gòu)體字面量:struct-literal)的更簡(jiǎn)短和慣用的方式如下:
ms := &struct1{10, 15.5, "Chris"}
// 此時(shí)ms的類型是 *struct1
或者:
var ms struct1
ms = struct1{10, 15.5, "Chris"}
混合字面量語(yǔ)法(composite literal syntax)&struct1{a, b, c} 是一種簡(jiǎn)寫(xiě),底層仍然會(huì)調(diào)用 new (),這里值的順序必須按照字段順序來(lái)寫(xiě)。在下面的例子中能看到可以通過(guò)在值的前面放上字段名來(lái)初始化字段的方式。表達(dá)式 new(Type) 和 &Type{} 是等價(jià)的。
時(shí)間間隔(開(kāi)始和結(jié)束時(shí)間以秒為單位)是使用結(jié)構(gòu)體的一個(gè)典型例子:
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)體定義時(shí)的順序給出,& 不是必須的。(B)顯示了另一種方式,字段名加一個(gè)冒號(hào)放在值的前面,這種情況下值的順序不必一致,并且某些字段還可以被忽略掉,就像(C)中那樣。
結(jié)構(gòu)體類型和字段的命名遵循可見(jiàn)性規(guī)則(第 4.2 節(jié)),一個(gè)導(dǎo)出的結(jié)構(gòu)體類型中有些字段是導(dǎo)出的,另一些不是,這是可能的。
下圖說(shuō)明了結(jié)構(gòu)體類型實(shí)例和一個(gè)指向它的指針的內(nèi)存布局:
type Point struct { x, y int }
使用 new 初始化:

作為結(jié)構(gòu)體字面量初始化:

類型 strcut1 在定義它的包 pack1 中必須是唯一的,它的完全類型名是:pack1.struct1。
下面的例子 Listing 10.2—person.go 顯示了一個(gè)結(jié)構(gòu)體 Person,一個(gè)方法,方法有一個(gè)類型為 *Person 的參數(shù)(因此對(duì)象本身是可以被改變的),以及三種調(diào)用這個(gè)方法的不同方式:
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
在上面例子的第二種情況中,可以直接通過(guò)指針,像 pers2.lastName="Woodward" 這樣給結(jié)構(gòu)體字段賦值,沒(méi)有像 C++ 中那樣需要使用 -> 操作符,Go 會(huì)自動(dòng)做這樣的轉(zhuǎn)換。
注意也可以通過(guò)解指針的方式來(lái)設(shè)置值:(*pers2).lastName = "Woodward"
結(jié)構(gòu)體的內(nèi)存布局
Go 語(yǔ)言中,結(jié)構(gòu)體和它所包含的數(shù)據(jù)在內(nèi)存中是以連續(xù)塊的形式存在的,即使結(jié)構(gòu)體中嵌套有其他的結(jié)構(gòu)體,這在性能上帶來(lái)了很大的優(yōu)勢(shì)。不像 Java 中的引用類型,一個(gè)對(duì)象和它里面包含的對(duì)象可能會(huì)在不同的內(nèi)存空間中,這點(diǎn)和 Go 語(yǔ)言中的指針很像。下面的例子清晰地說(shuō)明了這些情況:
type Rect1 struct {Min, Max Point }
type Rect2 struct {Min, Max *Point }

遞歸結(jié)構(gòu)體
結(jié)構(gòu)體類型可以通過(guò)引用自身來(lái)定義。這在定義鏈表或二叉樹(shù)的元素(通常叫節(jié)點(diǎn))時(shí)特別有用,此時(shí)節(jié)點(diǎn)包含指向臨近節(jié)點(diǎn)的鏈接(地址)。如下所示,鏈表中的 su,樹(shù)中的 ri 和 le 分別是指向別的節(jié)點(diǎn)的指針。
鏈表:

這塊的 data 字段用于存放有效數(shù)據(jù)(比如 float64),su 指針指向后繼節(jié)點(diǎn)。
Go 代碼:
type Node struct {
data float64
su *Node
}
鏈表中的第一個(gè)元素叫 head,它指向第二個(gè)元素;最后一個(gè)元素叫 tail,它沒(méi)有后繼元素,所以它的 su 為 nil 值。當(dāng)然真實(shí)的鏈接會(huì)有很多數(shù)據(jù)節(jié)點(diǎn),并且鏈表可以動(dòng)態(tài)增長(zhǎng)或收縮。
同樣地可以定義一個(gè)雙向鏈表,它有一個(gè)前趨節(jié)點(diǎn) pr 和一個(gè)后繼節(jié)點(diǎn) su:
type Node struct {
pr *Node
data float64
su *Node
}
二叉樹(shù):

二叉樹(shù)中每個(gè)節(jié)點(diǎn)最多能鏈接至兩個(gè)節(jié)點(diǎn):左節(jié)點(diǎn)(le)和右節(jié)點(diǎn)(ri),這兩個(gè)節(jié)點(diǎn)本身又可以有左右節(jié)點(diǎn),依次類推。樹(shù)的頂層節(jié)點(diǎn)叫根節(jié)點(diǎn)(root),底層沒(méi)有子節(jié)點(diǎn)的節(jié)點(diǎn)叫葉子節(jié)點(diǎn)(leaves),葉子節(jié)點(diǎn)的 le 和 ri 指針為 nil 值。在 Go 中可以如下定義二叉樹(shù):
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)體定義了一個(gè) alias 類型時(shí),此結(jié)構(gòu)體類型和它的 alias 類型都有相同的底層類型,它們可以如示例 10.3 那樣互相轉(zhuǎn)換,同時(shí)需要注意其中非法賦值或轉(zhuǎn)換引起的編譯錯(cuò)誤。
示例 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,后者包含一個(gè)人的名字、地址編號(hào)、出生日期和圖像,試著選擇正確的數(shù)據(jù)類型。構(gòu)建一個(gè)自己的 vcard 并打印它的內(nèi)容。
提示:
VCard 必須包含住址,它應(yīng)該以值類型還是以指針類型放在 VCard 中呢?
第二種會(huì)好點(diǎn),因?yàn)樗加脙?nèi)存少。包含一個(gè)名字和兩個(gè)指向地址的指針的 Address 結(jié)構(gòu)體可以使用 %v 打?。?{Kersschot 0x126d2b80 0x126d2be0}
練習(xí) 10.2 persionext1.go:
修改 persionext1.go,使它的參數(shù) upPerson 不是一個(gè)指針,解釋下二者的區(qū)別。
練習(xí) 10.3 point.go:
使用坐標(biāo) X、Y 定義一個(gè)二維 Point 結(jié)構(gòu)體。同樣地,對(duì)一個(gè)三維點(diǎn)使用它的極坐標(biāo)定義一個(gè) Polar 結(jié)構(gòu)體。實(shí)現(xiàn)一個(gè) Abs() 方法來(lái)計(jì)算一個(gè) Point 表示的向量的長(zhǎng)度,實(shí)現(xiàn)一個(gè) Scale 方法,它將點(diǎn)的坐標(biāo)乘以一個(gè)尺度因子(提示:使用 math 包里的 Sqrt 函數(shù))(function Scale that multiplies the coordinates of a point with a scale
factor)。
練習(xí) 10.3 rectangle.go:
定義一個(gè) Rectangle 結(jié)構(gòu)體,它的長(zhǎng)和寬是 int 類型,并定義方法 Area() 和 Perimeter(),然后進(jìn)行測(cè)試。