和C語(yǔ)言類似,Go也支持結(jié)構(gòu)體類型。此篇文章將介紹Go中的結(jié)構(gòu)體類型和結(jié)構(gòu)體值做一個(gè)詳細(xì)的解釋。
每個(gè)無(wú)名結(jié)構(gòu)體類型的字面形式均由struct關(guān)鍵字開(kāi)頭,后面跟著用一對(duì)大括號(hào){},其中包裹著的一系列字段(field)聲明。 一般來(lái)說(shuō),每個(gè)字段聲明由一個(gè)字段名和字段類型組成。一個(gè)結(jié)構(gòu)體類型的字段數(shù)目可以為0。下面是一個(gè)無(wú)名結(jié)構(gòu)體類型的字面形式:
struct {
title string
author string
pages int
}
上面這個(gè)結(jié)構(gòu)體類型含有三個(gè)字段。前兩個(gè)字段(?title
?和?author
?)的類型均為?string
?。 最后一個(gè)字段?pages
?的類型為?int
?。
有時(shí)字段也稱為成員變量。
相鄰的同類型字段可以聲明在一起。比如上面這個(gè)類型也可表示成下面這樣:
struct {
title, author string
pages int
}
一個(gè)結(jié)構(gòu)體類型的尺寸為它的所有字段的(類型)尺寸之和加上一些填充字節(jié)的數(shù)目。 常常地,編譯器(和運(yùn)行時(shí))會(huì)在一個(gè)結(jié)構(gòu)體值的兩個(gè)相鄰字段之間填充一些字節(jié)來(lái)保證一些字段的地址總是某個(gè)整數(shù)的倍數(shù)。 我們可以在后面的內(nèi)存布局一文中了解到字節(jié)填充(padding)和內(nèi)存地址對(duì)齊(memory address alignment)。
一個(gè)零字段結(jié)構(gòu)體的尺寸為零。
每個(gè)結(jié)構(gòu)體字段在它的聲明中可以被指定一個(gè)標(biāo)簽(tag)。從語(yǔ)法上講,字段標(biāo)簽可以是任意字符串,它們是可選的,默認(rèn)為空字符串。 但在實(shí)踐中,它們應(yīng)該被表示成用空格分隔的鍵值對(duì)形式,并且每個(gè)標(biāo)簽盡量使用直白字面形式(`...`)表示,而鍵值對(duì)中的值使用解釋型字面形式("...")表示。 比如下例:
struct {
Title string `json:"title" myfmt:"s1"`
Author string `json:"author,omitempty" myfmt:"s2"`
Pages int `json:"pages,omitempty" myfmt:"n1"`
X, Y bool `myfmt:"b1"`
}
注意:上例中的?X
?和?Y
?字段的標(biāo)簽是一樣的(盡管在實(shí)踐中基本上從不會(huì)這樣使用字段標(biāo)簽)。
我們可以使用反射來(lái)檢視字段的標(biāo)簽信息。
每個(gè)字段標(biāo)簽的目的取決于具體應(yīng)用。上面這個(gè)例子中的字段標(biāo)簽用來(lái)幫助?encoding/json
?標(biāo)準(zhǔn)庫(kù)包來(lái)將上面這個(gè)結(jié)構(gòu)體類型的某個(gè)值編碼成JSON數(shù)據(jù)或者從一份JSON數(shù)據(jù)解碼到上面這個(gè)結(jié)構(gòu)體類型的某個(gè)值中。在編碼和解碼過(guò)程中,?encoding/json
?標(biāo)準(zhǔn)庫(kù)包中的函數(shù)將只考慮導(dǎo)出的結(jié)構(gòu)體字段。這是為什么上面這個(gè)結(jié)構(gòu)體的字段均為導(dǎo)出的。
把字段標(biāo)簽當(dāng)成字段注釋來(lái)使用不是一個(gè)好主意。
和C語(yǔ)言不一樣,Go結(jié)構(gòu)體不支持字段聯(lián)合(union)。
上面的例子中展示的結(jié)構(gòu)體類型都是無(wú)名的。在實(shí)踐中,具名結(jié)構(gòu)體類型用得更流行。
只有導(dǎo)出字段可以被使用在其它代碼包中。非導(dǎo)出字段類以于很多其它語(yǔ)言中的私有或者保護(hù)型的成員變量。
一個(gè)結(jié)構(gòu)體類型中的字段標(biāo)簽和字段的聲明順序?qū)Υ私Y(jié)構(gòu)體類型的身份識(shí)別很重要。 如果兩個(gè)無(wú)名結(jié)構(gòu)體類型的各個(gè)對(duì)應(yīng)字段聲明都相同(按照它們的出現(xiàn)順序),則此兩個(gè)無(wú)名結(jié)構(gòu)體類型是等同的。 兩個(gè)字段聲明只有在它們的名稱、類型和標(biāo)簽都等同的情況下才相同。 注意:兩個(gè)聲明在不同的代碼包中的非導(dǎo)出字段將總被認(rèn)為是不同的字段。
一個(gè)結(jié)構(gòu)體類型不能(直接或者間接)含有一個(gè)類型為此結(jié)構(gòu)類型的字段。
在Go中,語(yǔ)法形式?T{...}
?稱為一個(gè)組合字面量形式(composite literal),其中?T
?必須為一個(gè)類型名或者類型字面形式。 組合字面量形式可以用來(lái)表示結(jié)構(gòu)體類型和內(nèi)置容器類型(將在后面的文章中介紹)的值。
注意:組合字面量?T{...}
?是一個(gè)類型確定值,它的類型為?T
?。
假設(shè)S是一個(gè)結(jié)構(gòu)體類型并且它的底層類型為struct{x int; y bool},S的零值可以表示成下面所示的組合字面量?jī)煞N變種形式:
S{0, false}
?。在此變種形式中,所有的字段名稱均不出現(xiàn),但每個(gè)字段的值必須指定,并且每個(gè)字段的出現(xiàn)順序和它們的聲明順序必須一致。S{x: 0, y: false}
?、?S{y: false, x: 0}
?、?S{x: 0}
?、?S{y: false}
?和?S{}
?。 在此變種形式中,字段的名稱和值必須成對(duì)出現(xiàn),但是每個(gè)字段都不是必須出現(xiàn)的,并且字段的出現(xiàn)順序并不重要。 沒(méi)有出現(xiàn)的字段的值被編譯器認(rèn)為是它們各自類型的零值。?S{}
?是最常用的類型?S
?的零值的表示形式。如果?S
?是聲明在另一個(gè)代碼包中的一個(gè)結(jié)構(gòu)體類型,則推薦使用上面所示的第二種變種形式來(lái)表示它的值。 因?yàn)榱硪粋€(gè)代碼包的維護(hù)者今后可能會(huì)在此結(jié)構(gòu)體中添加新的字段,從而導(dǎo)致當(dāng)前使用的第一種變種形式在今后可能編譯不通過(guò)。
當(dāng)然,上面所示的結(jié)構(gòu)體值的組合字面量也可以用來(lái)表示結(jié)構(gòu)體類型的非零值。
對(duì)于類型?S
?的一個(gè)值?v
?,我們可以用?v.x
?和?v.y
?來(lái)表示它的字段。 ?v.x
?(或?v.y
?)這種形式稱為一個(gè)選擇器(selector)。其中的?v
?稱為此選擇器的屬主。 今后,我們稱一個(gè)選擇器中的句點(diǎn)?.
?為屬性選擇操作符。
一個(gè)例子:
package main
import (
"fmt"
)
type Book struct {
title, author string
pages int
}
func main() {
book := Book{"Go語(yǔ)言101", "老貘", 256}
fmt.Println(book) // {Go語(yǔ)言101 老貘 256}
// 使用帶字段名的組合字面量來(lái)表示結(jié)構(gòu)體值。
book = Book{author: "老貘", pages: 256, title: "Go語(yǔ)言101"}
// title和author字段的值都為空字符串"",pages字段的值為0。
book = Book{}
// title字段空字符串"",pages字段為0。
book = Book{author: "老貘"}
// 使用選擇器來(lái)訪問(wèn)和修改字段值。
var book2 Book // <=> book2 := Book{}
book2.author = "Tapir"
book2.pages = 300
fmt.Println(book2.pages) // 300
}
如果一個(gè)組合字面量中最后一項(xiàng)和結(jié)尾的}處于同一行,則此項(xiàng)后的逗號(hào),是可選的;否則此逗號(hào)不可省略。 我們可以閱讀后面的Go代碼斷行規(guī)則一文了解更多斷行規(guī)則。
var _ = Book {
author: "老貘",
pages: 256,
title: "Go語(yǔ)言101", // 這里行尾的逗號(hào)不可省略
}
// 下行}前的逗號(hào)可以省略。
var _ = Book{author: "老貘", pages: 256, title: "Go語(yǔ)言101",}
當(dāng)一個(gè)(源)結(jié)構(gòu)體值被賦值給另外一個(gè)(目標(biāo))結(jié)構(gòu)體值時(shí),其效果和逐個(gè)將源結(jié)構(gòu)體值的各個(gè)字段賦值給目標(biāo)結(jié)構(gòu)體值的各個(gè)對(duì)應(yīng)字段的效果是一樣的。
func f() {
book1 := Book{pages: 300}
book2 := Book{"Go語(yǔ)言101", "老貘", 256}
book2 = book1
// 上面這行和下面這三行是等價(jià)的。
book2.title = book1.title
book2.author = book1.author
book2.pages = book1.pages
}
如果兩個(gè)結(jié)構(gòu)體值的類型不同,則只有在它們的底層類型相同(要考慮字段標(biāo)簽)并且其中至少有一個(gè)結(jié)構(gòu)體值的類型為無(wú)名類型時(shí)(換句話說(shuō),只有它們可以被隱式轉(zhuǎn)換為對(duì)方的類型的時(shí)候,見(jiàn)下)才可以互相賦值。
如果一個(gè)結(jié)構(gòu)體值是可尋址的,則它的字段也是可尋址的;反之,一個(gè)不可尋址的結(jié)構(gòu)體值的字段也是不可尋址的。 不可尋址的字段的值是不可更改的。所有的組合字面量都是不可尋址的。
一個(gè)例子:
package main
import "fmt"
func main() {
type Book struct {
Pages int
}
var book = Book{} // 變量值book是可尋址的
p := &book.Pages
*p = 123
fmt.Println(book) // {123}
// 下面這兩行編譯不通過(guò),因?yàn)锽ook{}是不可尋址的,
// 繼而B(niǎo)ook{}.Pages也是不可尋址的。
/*
Book{}.Pages = 123
p = &Book{}.Pages // <=> p = &(Book{}.Pages)
*/
}
注意:選擇器中的屬性選擇操作符?.
?的優(yōu)先級(jí)比取地址操作符?&
?的優(yōu)先級(jí)要高。
一般來(lái)說(shuō),只有可被尋址的值才能被取地址,但是Go中有一個(gè)語(yǔ)法糖(語(yǔ)法例外):雖然所有的組合字面量都是不可尋址的,但是它們都可被取地址。
例子:
package main
func main() {
type Book struct {
Pages int
}
// Book{100}是不可尋址的,但是它可以被取地址。
p := &Book{100} // <=> tmp := Book{100}; p := &tmp
p.Pages = 200
}
比如,在下面的例子中,為了簡(jiǎn)潔,(*bookN).pages可以被寫(xiě)成bookN.pages。 換句話說(shuō),在這種簡(jiǎn)寫(xiě)形式中,bookN將被隱式解引用。
package main
func main() {
type Book struct {
pages int
}
book1 := &Book{100} // book1是一個(gè)指針
book2 := new(Book) // book2是另外一個(gè)指針
// 像使用結(jié)構(gòu)值一樣來(lái)使用結(jié)構(gòu)體值的指針。
book2.pages = book1.pages
// 上一行等價(jià)于下一行。換句話說(shuō),上一行
// 兩個(gè)選擇器中的指針屬主將被自動(dòng)解引用。
(*book2).pages = (*book1).pages
}
如果一個(gè)結(jié)構(gòu)體類型是可比較的,則它肯定不包含不可比較類型的字段(這里不忽略名為空標(biāo)識(shí)符_的字段)。
和結(jié)構(gòu)體值的賦值規(guī)則類似,如果兩個(gè)不同類型的結(jié)構(gòu)體值均為可比較的,則它們僅在它們的底層類型相同(要考慮字段標(biāo)簽)并且其中至少有一個(gè)結(jié)構(gòu)體值的類型為無(wú)名類型時(shí)(換句話說(shuō),只有它們可以被隱式轉(zhuǎn)換為對(duì)方的類型的時(shí)候,見(jiàn)下)才可以互相比較。
如果兩個(gè)結(jié)構(gòu)體值可以相互比較,則它們的比較結(jié)果等同于逐個(gè)比較它們的相應(yīng)字段(按照字段在代碼中的聲明順序)。 兩個(gè)結(jié)構(gòu)體值只有在它們的相應(yīng)字段都相等的情況下才相等;當(dāng)一對(duì)字段被發(fā)現(xiàn)不相等的或者在比較中產(chǎn)生恐慌的時(shí)候,對(duì)結(jié)構(gòu)體的比較將提前結(jié)束結(jié)束。 在比較中,名為空標(biāo)識(shí)符_的字段將被忽略掉。
兩個(gè)類型分別為?S1
?和?S2
?的結(jié)構(gòu)體值只有在?S1
?和?S2
?的底層類型相同(忽略掉字段標(biāo)簽)的情況下才能相互轉(zhuǎn)換為對(duì)方的類型。 特別地,如果?S1
?和?S2
?的底層類型相同(要考慮字段標(biāo)簽)并且只要它們其中有一個(gè)為無(wú)名類型,則此轉(zhuǎn)換可以是隱式的。
比如,對(duì)于下面的代碼片段中所示的五個(gè)結(jié)構(gòu)體類型:S0、S1、S2、S3和S4:
S0
?的值不能被轉(zhuǎn)換為其它四個(gè)類型中的任意一個(gè),原因是它與另外四個(gè)類型的對(duì)應(yīng)字段名不同(因此底層類型不同)。S1
?、?S2
?、?S3
?和?S4
?的任意兩個(gè)值可以轉(zhuǎn)換為對(duì)方的類型。特別地,
S2
?表示的類型的值可以被隱式轉(zhuǎn)化為類型?S3
?,反之亦然。S2
?表示的類型的值可以被隱式轉(zhuǎn)換為類型?S4
?,反之亦然。但是,
S2
?表示的類型的值必須被顯式轉(zhuǎn)換為類型?S1
?,反之亦然。S3
?的值必須被顯式轉(zhuǎn)換為類型?S4
?,反之亦然。package main
type S0 struct {
y int "foo"
x bool
}
type S1 = struct { // S1是一個(gè)無(wú)名類型
x int "foo"
y bool
}
type S2 = struct { // S2也是一個(gè)無(wú)名類型
x int "bar"
y bool
}
type S3 S2 // S3是一個(gè)定義類型(因而具名)。
type S4 S3 // S4是一個(gè)定義類型(因而具名)。
// 如果不考慮字段標(biāo)簽,S3(S4)和S1的底層類型一樣。
// 如果考慮字段標(biāo)簽,S3(S4)和S1的底層類型不一樣。
var v0, v1, v2, v3, v4 = S0{}, S1{}, S2{}, S3{}, S4{}
func f() {
v1 = S1(v2); v2 = S2(v1)
v1 = S1(v3); v3 = S3(v1)
v1 = S1(v4); v4 = S4(v1)
v2 = v3; v3 = v2 // 這兩個(gè)轉(zhuǎn)換可以是隱式的
v2 = v4; v4 = v2 // 這兩個(gè)轉(zhuǎn)換也可以是隱式的
v3 = S3(v4); v4 = S4(v3)
}
事實(shí)上,兩個(gè)結(jié)構(gòu)體值只有在它們可以相互隱式轉(zhuǎn)換為對(duì)方的類型的時(shí)候才能相互賦值和比較。
匿名結(jié)構(gòu)體類型允許出現(xiàn)在結(jié)構(gòu)體字段聲明中。匿名結(jié)構(gòu)體類型也允許出現(xiàn)在組合字面量中。
一個(gè)例子:
var aBook = struct {
author struct { // 此字段的類型為一個(gè)匿名結(jié)構(gòu)體類型
firstName, lastName string
gender bool
}
title string
pages int
}{
author: struct {
firstName, lastName string
gender bool
}{
firstName: "Mark",
lastName: "Twain",
}, // 此組合字面量中的類型為一個(gè)匿名結(jié)構(gòu)體類型
title: "The Million Pound Note",
pages: 96,
}
通常來(lái)說(shuō),為了代碼可讀性,最好少使用匿名結(jié)構(gòu)體類型。
Go中有一些和結(jié)構(gòu)體類型相關(guān)的進(jìn)階知識(shí)點(diǎn)。這些知識(shí)點(diǎn)將后面的類型內(nèi)嵌和內(nèi)存布局兩篇文章中介紹。
更多建議: