《Go語言四十二章經(jīng)》第十八章 Struct 結(jié)構(gòu)體

《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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市烹骨,隨后出現(xiàn)的幾起案子翻伺,更是在濱河造成了極大的恐慌,老刑警劉巖沮焕,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吨岭,死亡現(xiàn)場離奇詭異,居然都是意外死亡峦树,警方通過查閱死者的電腦和手機辣辫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來魁巩,“玉大人急灭,你說我怎么就攤上這事」人欤” “怎么了葬馋?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長肾扰。 經(jīng)常有香客問我畴嘶,道長,這世上最難降的妖魔是什么集晚? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任掠廓,我火速辦了婚禮,結(jié)果婚禮上甩恼,老公的妹妹穿的比我還像新娘蟀瞧。我一直安慰自己,他們只是感情好条摸,可當我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布悦污。 她就那樣靜靜地躺著,像睡著了一般钉蒲。 火紅的嫁衣襯著肌膚如雪切端。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天顷啼,我揣著相機與錄音踏枣,去河邊找鬼昌屉。 笑死,一個胖子當著我的面吹牛茵瀑,可吹牛的內(nèi)容都是我干的间驮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼马昨,長吁一口氣:“原來是場噩夢啊……” “哼竞帽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鸿捧,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤屹篓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后匙奴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體堆巧,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年泼菌,在試婚紗的時候發(fā)現(xiàn)自己被綠了恳邀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡灶轰,死狀恐怖谣沸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情笋颤,我是刑警寧澤乳附,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站伴澄,受9級特大地震影響赋除,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜非凌,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一举农、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧敞嗡,春花似錦颁糟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至箕肃,卻和暖如春婚脱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工障贸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留错森,地道東北人。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓篮洁,卻偏偏與公主長得像涩维,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嘀粱,可洞房花燭夜當晚...
    茶點故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內(nèi)容