《Go語言四十二章經(jīng)》第十八章 Struct 結(jié)構(gòu)體
作者:李驍
18.1結(jié)構(gòu)體(struct)
Go 通過結(jié)構(gòu)體的形式支持用戶自定義類型诗赌,或者叫定制類型遗嗽。
Go 語言結(jié)構(gòu)體是實現(xiàn)自定義類型的一種重要數(shù)據(jù)類型婴程。
結(jié)構(gòu)體是復(fù)合類型(composite types),它由一系列屬性組成荠锭,每個屬性都有自己的類型和值的蜜暑,結(jié)構(gòu)體通過屬性把數(shù)據(jù)聚集在一起。
結(jié)構(gòu)體類型和字段的命名遵循可見性規(guī)則返十。
方法(Method)可以訪問這些數(shù)據(jù)妥泉,就好像它們是這個獨立實體的一部分。
結(jié)構(gòu)體是值類型洞坑,因此可以通過 new 函數(shù)來創(chuàng)建盲链。
結(jié)構(gòu)體是由一系列稱為字段(fields)的命名元素組成,每個元素都有一個名稱和一個類型。 字段名稱可以顯式指定(IdentifierList)或隱式指定(EmbeddedField)刽沾,沒有顯式字段名稱的字段稱為匿名(內(nèi)嵌)字段本慕。在結(jié)構(gòu)體中,非空字段名稱必須是唯一的侧漓。
結(jié)構(gòu)體定義的一般方式如下:
type identifier struct {
field1 type1
field2 type2
...
}
結(jié)構(gòu)體里的字段一般都有名字锅尘,像 field1、field2 等布蔗,如果字段在代碼中從來也不會被用到藤违,那么可以命名它為 _。
空結(jié)構(gòu)體如下所示:
struct {}
具有6個字段的結(jié)構(gòu)體:
struct {
x, y int
u float32
_ float32 // 填充
A *[]int
F func()
}
對于匿名字段纵揍,必須將匿名字段指定為類型名稱T或指向非接口類型名稱* T的指針顿乒,并且T本身可能不是指針類型。
struct {
T1 // 字段名 T1
*T2 // 字段名 T2
P.T3 // 字段名 T3
*P.T4 // f字段名T4
x, y int // 字段名 x 和 y
}
使用 new 函數(shù)給一個新的結(jié)構(gòu)體變量分配內(nèi)存骡男,它返回指向已分配內(nèi)存的指針:
type S struct { a int; b float64 }
new(S)
new(S)為S類型的變量分配內(nèi)存淆游,并初始化(a = 0,b = 0.0)隔盛,返回包含該位置地址的類型* S的值犹菱。
我們一般的慣用方法是:t := new(T),變量 t 是一個指向 T的指針吮炕,此時結(jié)構(gòu)體字段的值是它們所屬類型的零值腊脱。
也可以這樣寫:var t T ,也會給 t 分配內(nèi)存龙亲,并零值化內(nèi)存陕凹,但是這個時候 t 是類型T。
在這兩種方式中鳄炉,t 通常被稱做類型 T 的一個實例(instance)或?qū)ο螅╫bject)杜耙。
使用點號符“.”可以獲取結(jié)構(gòu)體字段的值:structname.fieldname。在 Go 語言中“.”叫選擇器(selector)拂盯。無論變量是一個結(jié)構(gòu)體類型還是一個結(jié)構(gòu)體類型指針佑女,都使用同樣的選擇表示法來引用結(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
type Interval struct {
start int
end int
}
結(jié)構(gòu)體變量有下面幾種初始化方式,前面一種按照字段順序谈竿,后面兩種則對應(yīng)字段名來初始化賦值:
intr := Interval{0, 3} (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)
復(fù)合字面量是構(gòu)造結(jié)構(gòu)體团驱,數(shù)組,切片和字典的值空凸,并每次都創(chuàng)建新值嚎花。聲明和初始化一個結(jié)構(gòu)體實例(一個結(jié)構(gòu)體字面量:struct-literal)方式如下:
定義結(jié)構(gòu)體類型Point3D和Line:
type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }
聲明并初始化:
origin := Point3D{} // Point3D 是零值
line := Line{origin, Point3D{y: -4, z: 12.3}} // line.q.x 是零值
這里 Point3D{}以及 Line{origin, Point3D{y: -4, z: 12.3}}都是結(jié)構(gòu)體字面量。
表達式 new(Type) 和 &Type{} 是等價的呀洲。&struct1{a, b, c} 是一種簡寫紊选,底層仍然會調(diào)用 new ()啼止,這里值的順序必須按照字段順序來寫。也可以通過在值的前面放上字段名來初始化字段的方式兵罢,這種方式就不必按照順序來寫了族壳。
結(jié)構(gòu)體類型和字段的命名遵循可見性規(guī)則,一個導(dǎo)出的結(jié)構(gòu)體類型中有些字段是導(dǎo)出的趣些,也即首字母大寫字段會導(dǎo)出仿荆;另一些不可見,也即首字母小寫為未導(dǎo)出坏平,對外不可見拢操。
18.2 結(jié)構(gòu)體特性
結(jié)構(gòu)體的內(nèi)存布局
Go 語言中,結(jié)構(gòu)體和它所包含的數(shù)據(jù)在內(nèi)存中是以連續(xù)塊的形式存在的舶替,即使結(jié)構(gòu)體中嵌套有其他的結(jié)構(gòu)體令境,這在性能上帶來了很大的優(yōu)勢。遞歸結(jié)構(gòu)體
結(jié)構(gòu)體類型可以通過引用自身(指針類型)來定義顾瞪。這在定義鏈表或二叉樹的節(jié)點時特別有用舔庶,此時節(jié)點包含指向臨近節(jié)點的鏈接。
type H struct {
int
*H
}
- 使用工廠方法
通過參考應(yīng)用可見性規(guī)則陈醒,我們可以設(shè)定結(jié)構(gòu)體名不能導(dǎo)出惕橙,就可以達到使用 new 函數(shù),強制使用工廠方法的目的钉跷。
type matrix struct {
...
}
func NewMatrix(params) *matrix {
m := new(matrix) // 初始化 m
return m
}
在包外弥鹦,只有通過NewMatrix函數(shù)才可以初始化matrix 結(jié)構(gòu)。
- 帶標簽的結(jié)構(gòu)體
結(jié)構(gòu)體中的字段除了有名字和類型外爷辙,還可以有一個可選的標簽(tag):它是一個附屬于字段的字符串彬坏,可以是文檔或其他的重要標記。標簽的內(nèi)容不可以在一般的編程中使用膝晾,只有 reflect 包能獲取它栓始。
reflect包可以在運行時自省類型、屬性和方法血当,如變量是結(jié)構(gòu)體類型幻赚,可以通過 Field 來索引結(jié)構(gòu)體的字段,然后就可以使用 Tag 屬性歹颓。
package main
import (
"fmt"
"reflect"
)
type Student struct {
name string "學(xué)生名字" // 結(jié)構(gòu)體標簽
Age int "學(xué)生年齡" // 結(jié)構(gòu)體標簽
Room int `json:"Roomid"` // 結(jié)構(gòu)體標簽
}
func main() {
st := Student{"Titan", 14, 102}
fmt.Println(reflect.TypeOf(st).Field(0).Tag)
fmt.Println(reflect.TypeOf(st).Field(1).Tag)
fmt.Println(reflect.TypeOf(st).Field(2).Tag)
}
程序輸出:
學(xué)生名字
學(xué)生年齡
json:"Roomid"
從上面代碼中可以看到坯屿,通過reflect我們很容易得到結(jié)構(gòu)體字段的標簽油湖。
18.3 匿名成員
Go語言結(jié)構(gòu)體中可以包含一個或多個匿名(內(nèi)嵌)字段巍扛,即這些字段沒有顯式的名字,只有字段的類型是必須的乏德,此時類型就是字段的名字(這一特征決定了在一個結(jié)構(gòu)體中撤奸,每種數(shù)據(jù)類型只能有一個匿名字段)吠昭。
匿名(內(nèi)嵌)字段本身也可以是一個結(jié)構(gòu)體類型,即結(jié)構(gòu)體可以包含內(nèi)嵌結(jié)構(gòu)體胧瓜。
type Human struct {
name string
}
type Student struct { // 含內(nèi)嵌結(jié)構(gòu)體Human
Human // 匿名(內(nèi)嵌)字段
int // 匿名(內(nèi)嵌)字段
}
Go語言結(jié)構(gòu)體中這種含匿名(內(nèi)嵌)字段和內(nèi)嵌結(jié)構(gòu)體的結(jié)構(gòu)矢棚,可近似地理解為面向?qū)ο笳Z言中的繼承概念。
Go 語言中的繼承是通過內(nèi)嵌或者說組合來實現(xiàn)的府喳,所以可以說蒲肋,在 Go 語言中,相比較于繼承钝满,組合更受青睞兜粘。
18.4 嵌入與聚合
結(jié)構(gòu)體中包含匿名(內(nèi)嵌)字段叫嵌入或者內(nèi)嵌;而如果結(jié)構(gòu)體中字段包含了類型名弯蚜,還有字段名孔轴,則是聚合。聚合的在JAVA和C++都是常見的方式碎捺,而內(nèi)嵌則是Go 的特有方式路鹰。
type Human struct {
name string
}
type Person1 struct { // 內(nèi)嵌
Human
}
type Person2 struct { // 內(nèi)嵌, 這種內(nèi)嵌與上面內(nèi)嵌有差異
*Human
}
type Person3 struct{ // 聚合
human Human
}
嵌入在結(jié)構(gòu)體中廣泛使用收厨,在Go語言中如果只考慮結(jié)構(gòu)體和接口的嵌入組合方式晋柱,一共有下面四種:
- 1.在接口中嵌入接口:
這里指的是在接口中定義中嵌入接口類型,而不是接口的一個實例诵叁,相當于合并了兩個接口類型定義的全部函數(shù)趣斤。下面只有同時實現(xiàn)了Writer和 Reader 的接口,才可以說是實現(xiàn)了Teacher接口黎休,即可以作為Teacher的實例浓领。Teacher接口嵌入了Writer和 Reader 兩個接口,在Teacher接口中势腮,Writer和 Reader是兩個匿名(內(nèi)嵌)字段联贩。
type Writer interface{
Write()
}
type Reader interface{
Read()
}
type Teacher interface{
Reader
Writer
}
- 2.在接口中嵌入結(jié)構(gòu)體:
這種方式在Go語言中是不合法的,不能通過編譯捎拯。
type Human struct {
name string
}
type Writer interface {
Write()
}
type Reader interface {
Read()
}
type Teacher interface {
Reader
Writer
Human
}
存在語法錯誤泪幌,并不具有實際的含義,編譯報錯:
interface contains embedded non-interface Base
Interface 不能嵌入非interface的類型署照。
- 3.在結(jié)構(gòu)體中內(nèi)嵌接口:
初始化的時候祸泪,內(nèi)嵌接口要用一個實現(xiàn)此接口的結(jié)構(gòu)體賦值;或者定義一個新結(jié)構(gòu)體建芙,可以把新結(jié)構(gòu)體作為receiver没隘,實現(xiàn)接口的方法就實現(xiàn)了接口(先記住這句話,后面在講述方法時會解釋)禁荸,這個新結(jié)構(gòu)體可作為初始化時實現(xiàn)了內(nèi)嵌接口的結(jié)構(gòu)體來賦值右蒲。
package main
import (
"fmt"
)
type Writer interface {
Write()
}
type Author struct {
name string
Writer
}
// 定義新結(jié)構(gòu)體阀湿,重點是實現(xiàn)接口方法Write()
type Other struct {
i int
}
func (a Author) Write() {
fmt.Println(a.name, " Write.")
}
// 新結(jié)構(gòu)體Other實現(xiàn)接口方法Write(),也就可以初始化時賦值給Writer 接口
func (o Other) Write() {
fmt.Println(" Other Write.")
}
func main() {
// 方法一:Other{99}作為Writer 接口賦值
Ao := Author{"Other", Other{99}}
Ao.Write()
// 方法二:簡易做法瑰妄,對接口使用零值陷嘴,可以完成初始化
Au := Author{name: "Hawking"}
Au.Write()
}
程序輸出:
Other Write.
Hawking Write.
- 4.在結(jié)構(gòu)體中嵌入結(jié)構(gòu)體:
在結(jié)構(gòu)體嵌入結(jié)構(gòu)體很好理解,但不能嵌入自身值類型间坐,可以嵌入自身的指針類型即遞歸嵌套灾挨。
在初始化時,內(nèi)嵌結(jié)構(gòu)體也進行賦值竹宋;外層結(jié)構(gòu)自動獲得內(nèi)嵌結(jié)構(gòu)體所有定義的字段和實現(xiàn)的方法涨醋。
下面代碼完整演示了結(jié)構(gòu)體中嵌入結(jié)構(gòu)體,初始化以及字段的選擇調(diào)用:
package main
import (
"fmt"
)
type Human struct {
name string // 姓名
Gender string // 性別
Age int // 年齡
string // 匿名字段
}
type Student struct {
Human // 匿名字段
Room int // 教室
int // 匿名字段
}
func main() {
//使用new方式
stu := new(Student)
stu.Room = 102
stu.Human.name = "Titan"
stu.Gender = "男"
stu.Human.Age = 14
stu.Human.string = "Student"
fmt.Println("stu is:", stu)
fmt.Printf("Student.Room is: %d\n", stu.Room)
fmt.Printf("Student.int is: %d\n", stu.int) // 初始化時已自動給予零值:0
fmt.Printf("Student.Human.name is: %s\n", stu.name) // (*stu).name
fmt.Printf("Student.Human.Gender is: %s\n", stu.Gender)
fmt.Printf("Student.Human.Age is: %d\n", stu.Age)
fmt.Printf("Student.Human.string is: %s\n", stu.string)
// 使用結(jié)構(gòu)體字面量賦值
stud := Student{Room: 102, Human: Human{"Hawking", "男", 14, "Monitor"}}
fmt.Println("stud is:", stud)
fmt.Printf("Student.Room is: %d\n", stud.Room)
fmt.Printf("Student.int is: %d\n", stud.int) // 初始化時已自動給予零值:0
fmt.Printf("Student.Human.name is: %s\n", stud.Human.name)
fmt.Printf("Student.Human.Gender is: %s\n", stud.Human.Gender)
fmt.Printf("Student.Human.Age is: %d\n", stud.Human.Age)
fmt.Printf("Student.Human.string is: %s\n", stud.Human.string)
}
程序輸出:
stu is: &{ {Titan 男 14 Student} 102 0}
Student.Room is: 102
Student.int is: 0
Student.Human.name is: Titan
Student.Human.Gender is: 男
Student.Human.Age is: 14
Student.Human.string is: Student
stud is: { {Hawking 男 14 Monitor} 102 0}
Student.Room is: 102
Student.int is: 0
Student.Human.name is: Hawking
Student.Human.Gender is: 男
Student.Human.Age is: 14
Student.Human.string is: Monitor
內(nèi)嵌結(jié)構(gòu)體的字段逝撬,例如我們即可以stu.Human.name這樣來選擇使用浴骂,而如果外層結(jié)構(gòu)體中沒有同名的name字段,也可以stu.name直接來選擇使用宪潮。對于嵌入和聚合結(jié)構(gòu)體而言溯警,我們在選擇調(diào)用內(nèi)部字段時,可以不用多層選擇調(diào)用狡相,在不同名情況下可直接調(diào)用梯轻。比如stu.name這樣效果實際上與stu.Human.name一樣。
我們通過對結(jié)構(gòu)體使用new(T)尽棕,struct{filed:value}兩種方式來聲明初始化喳挑,這兩種方式分別得到*T,和T滔悉。
我們從輸出stu is: &{ {Titan 男 14 Student} 102 0} 可以得知伊诵,stu 是個指針,但是我們在隨后調(diào)用字段時并沒有使用指針回官,這是在Go語言中這里的 stu.name 相當于(*stu).name曹宴,這是一個語法糖,一般我們都使用stu.name方式來調(diào)用歉提,但我們要知道有這個語法糖存在笛坦。
18.5 命名沖突
當兩個字段擁有相同的名字(可能是繼承來的名字)時該怎么辦呢?外層名字會覆蓋內(nèi)層名字(但是兩者的內(nèi)存空間都保留)苔巨。
如果相同的名字在同一級別出現(xiàn)了兩次版扩,如果這個名字被程序使用了,將會引發(fā)一個錯誤侄泽,但不使用沒關(guān)系礁芦。沒有好辦法來解決這種問題引起的二義性,一般由程序員完整寫出來避免錯誤蔬顾。
下面代碼中如果寫成 c.a 是錯誤的宴偿,因為我們不知道到底是要調(diào)用 c.A.a 還是 c.B.a。其實只要我們完整寫出來(如:c.B.a)就不存在這個問題诀豁。
type A struct {a int}
type B struct {a, b int}
type C struct {A; B}
var c C
本書《Go語言四十二章經(jīng)》內(nèi)容在github上同步地址:https://github.com/ffhelicopter/Go42
本書《Go語言四十二章經(jīng)》內(nèi)容在簡書同步地址: http://www.reibang.com/nb/29056963雖然本書中例子都經(jīng)過實際運行窄刘,但難免出現(xiàn)錯誤和不足之處,煩請您指出舷胜;如有建議也歡迎交流娩践。
聯(lián)系郵箱:roteman@163.com