Go 学习笔记(14)— 结构体定义、实例化、初始化、匿名结构体、结构体访问、结构体作为形参、结构体指针
Go 語言中沒有 “類” 的概念,也不支持 “類” 的繼承等面向對象的概念。Go 語言不僅認為結構體能擁有方法,且每種自定義類型也可以擁有自己的方法。
1. 結構體的定義
結構體是由一系列具有相同或者不同類型數據組成的集合。結構體的定義需要使用 type 和 struct 關鍵字。含義如下:
-
type關鍵字定義了結構體的名稱; -
struct關鍵字定義新的數據類型,結構體中可以有一個或者多個成員;
結構體成員是由一系列的成員變量構成,這些成員變量也被稱為“字段”。字段有以下特性:
- 字段擁有自己的類型和值。
- 字段名必須唯一。
- 字段的類型也可以是結構體,甚至是字段所在結構體的類型。
type 是自定義類型的關鍵字,不僅支持 struct 類型的創建,還支持任意其它子定義類型(整型、字符串、布爾等)的創建。
結構體的標準格式如下:
type structName struct {member type;member type;...member type;
}
2. 結構體實例化
結構體的定義只是一種內存布局的描述,只有當結構體實例化時,才會真正地分配內存,因此必須在定義結構體并實例化后才能使用結構體的字段。
package mainimport ("fmt"
)type Student struct {Id intName stringAge intMobile intMajor string
}func main() {// 創建Student指針類型變量s1 := new(Student)// 創建Student指針類型變量s2 := &Student{}// 創建Student類型變量s3 := Student{}// 創建Student指針類型變量,并根據字段名附初始值s4 := &Student{Id: 20210901,Name: "wohu",Age: 20,Mobile: 123456,Major: "move brick",}// 創建Student類型變量,并根據字段在結構體中的順序附初始值s5 := Student{20170901,"wohu",20,123456,"move brick",}fmt.Println("s1:", s1)fmt.Println("s2:", s2)fmt.Println("s3:", s3)fmt.Println("s4:", *s4)fmt.Println("s5:", s5)
}
輸出結果:
s1: &{0 0 0 }
s2: &{0 0 0 }
s3: {0 0 0 }
s4: {20210901 wohu 20 123456 move brick}
s5: {20170901 wohu 20 123456 move brick}
通過在結構體后邊加大括號 {} 的方式創建變量與使用 new 關鍵字創建對象不同之處在于 new 創建出來的是指針類型,而結構體名加大括號 {} 創建出來的不是指針類型。
如果在使用結構體名加大括號 {} 創建變量時,在結構體名前邊加上地址操作符 & ,那么這樣創建出來的也是指針類型變量,效果就與 new 關鍵字一樣了,如上邊示例代碼中,s1 與 s2 都是指針類型變量。
在程序中,可以通過結構體類型變量名加上點號 . ,再加上字段名的方式訪問結構體中的字段。如訪問學生姓名的操作是:
s1.Name = "hi hzwy23"
s3.Name = "hello hzwy23"
fmt.Println(s3.Name)
fmt.Println(s1.Name)
不管變量是指針類型,還是非指針類型,操作結構體字段,都是一樣的方法,如上邊示例中 s1 是指針類型,s3 是非指針類型,在訪問字段名為 Name 的字段時,都是采用點號 . 加上字段名的方式訪問。這一點與 C/C++ 語言不同。
2.1 普通實例化
結構體本身是一種類型,可以像整型、字符串等類型一樣,以 var 的方式聲明結構體即可完成實例化。這種用法是為了更明確地表示一個變量被設置為零值。
var phone Phone
// 其中,Phone 為結構體類型,phone 為結構體的實例
用結構體表示的點結構(Point)的實例化過程請參見下面的代碼:
type Point struct {X intY int
}var p Point
p.X = 10
p.Y = 20
在例子中,使用.來訪問結構體的成員變量,如p.X和p.Y等,結構體成員變量的賦值方法與普通變量一致。
與數組類型相同,結構體類型屬于值類型,因此結構體類型的零值不是 nil,上述 Point 的零值就是 Point{}。
2.2 new 實例化
Go 語言中,還可以使用 new 關鍵字對類型(包括結構體、整型、浮點數、字符串等)進行實例化,結構體在實例化后會形成指針類型的結構體。
使用 new 的格式如下:
ins := new(T)
其中:
T為類型,可以是結構體、整型、字符串等。ins:T類型被實例化后保存到ins變量中,ins的類型為*T,屬于指針。
Go 語言讓我們可以像訪問普通結構體一樣使用.來訪問結構體指針的成員。
type Player struct{Name stringHealthPoint intMagicPoint int
}tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300
經過 new 實例化的結構體實例在成員賦值上與普通實例化的寫法一致。
在 C/C++ 語言中,使用 new 實例化類型后,訪問其成員變量時必須使用 -> 操作符。
在 Go 語言中,訪問結構體指針的成員交量時可以繼續使用 . 。 這是因為 Go 語言為了方便開發者訪問結構體指針的成員交量,使用了語法糖技術,將 ins.Name 形式轉換為 (*ins).Name 。
2.3 取地址實例化
在 Go 語言中,對結構體進行 &取地址操作時,視為對該類型進行一次 new 的實例化操作,取地址格式如下:
ins := &T{}
其中:
T: 表示結構體類型。ins:為結構體的實例,類型為*T,是指針類型。
下面使用結構體定義一個命令行指令(Command),指令中包含名稱、變量關聯和注釋等,對 Command 進行指針地址的實例化,并完成賦值過程,代碼如下:
type Command struct {Name string // 指令名稱Var *int // 指令綁定的變量Comment string // 指令的注釋
}var version int = 1cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"
取地址實例化是最廣泛的一種結構體實例化方式,可以使用函數封裝上面的初始化過程,代碼如下:
func newCommand(name string, varref *int, comment string) *Command {return &Command{Name: name,Var: varref,Comment: comment,}
}cmd = newCommand("version",&version,"show version",
)
3. 結構體初始化
結構體在實例化時可以直接對成員變量進行初始化,初始化有兩種形式分別是以字段“鍵值對”形式和多個值的列表形式,鍵值對形式的初始化適合選擇性填充字段較多的結構體,多個值的列表形式適合填充字段較少的結構體。
3.1 鍵值對初始化
結構體可以使用“鍵值對”(Key value pair)初始化字段,每個“鍵”(Key)對應結構體中的一個字段,鍵的“值”(Value)對應字段需要初始化的值。
鍵值對的填充是可選的,不需要初始化的字段可以不填入初始化列表中。
結構體實例化后字段的默認值是字段類型的默認值,例如 ,數值為 0 、字符串為 “”(空字符串)、布爾為 false 、指針為 nil 等。
鍵值對初始化的格式如下:
varName := structName{key1: value1, key2: value2..., keyn: valuen}
// 鍵值之間以:分隔,鍵值對之間以,分隔。
下面示例中描述了家里的人物關聯,正如兒歌里唱的:“爸爸的爸爸是爺爺”,人物之間可以使用多級的 child 來描述和建立關聯,使用鍵值對形式填充結構體的代碼如下:
type People struct {name stringchild *People
}relation := &People{name: "爺爺",child: &People{name: "爸爸",child: &People{name: "我",},},
}
注意:結構體成員中只能包含結構體的指針類型,包含非結構體指針類型會引起編譯錯誤。
3.2 多值列表初始化
Go 語言可以在“鍵值對”初始化的基礎上忽略“鍵”,也就是說,可以使用多個值的列表初始化結構體的字段。多個值使用逗號分隔初始化結構體,例如:
varName := structName{value1, value2...valuen}
使用這種格式初始化時,需要注意:
- 必須初始化結構體的所有字段。
- 每一個初始值的填充順序必須與字段在結構體中的聲明順序一致。
- 鍵值對與值列表的初始化形式不能混用。
type Address struct {Province stringCity stringZipCode intPhoneNumber string
}addr := Address{"陜西","西安",710000,"12345",
}fmt.Println(addr) // {陜西 西安 710000 12345}
3.3 初始化注意事項
初始化復合對象,必須使用類型標簽,且左大括號必須在類型尾部。
package mainfunc main() {var a struct { x int } = { 100 } // syntax error, syntax error: unexpected {, expecting expressionvar b []int = { 1, 2, 3 } // syntax errorc := struct {x int; y string} // syntax error: unexpected semicolon or newline{}var a = struct{ x int }{100}var b = []int{1, 2, 3}}
初始化值以 “,” 分隔。可以分多行,但最后一行必須以 “,” 或 “}” 結尾。
a := []int{1,2 // Error: need trailing comma before newline in composite literal
}a := []int{1,2, // ok
}b := []int{1,2 } // ok
3.4 空結構體
type Empty struct{} // Empty是一個不包含任何字段的空結構體類型
空結構體類型有什么用呢?
var s Empty
println(unsafe.Sizeof(s)) // 0
我們看到,輸出的空結構體類型變量的大小為 0,也就是說,空結構體類型變量的內存占用為 0。基于空結構體類型內存零開銷這樣的特性,我們在日常 Go 開發中會經常使用空結構體類型元素,作為一種“事件”信息進行 Goroutine 之間的通信,就像下面示例代碼這樣:
var c = make(chan Empty) // 聲明一個元素類型為Empty的channel
c<-Empty{} // 向channel寫入一個“事件”
這種以空結構體為元素類建立的 channel,是目前能實現的、內存占用最小的 Goroutine 間通信方式。
4. 匿名結構體
匿名結構體沒有類型名稱,無須通過 type 關鍵字定義就可以直接使用。
4.1 匿名結構體定義和初始化
匿名結構體的初始化寫法由結構體定義和鍵值對初始化兩部分組成,結構體定義時沒有結構體類型名,只有字段和類型定義,鍵值對初始化部分由可選的多個鍵值對組成,如下格式所示:
ins := struct {// 匿名結構體字段定義字段1 字段類型1字段2 字段類型2…
}{// 字段值初始化初始化字段1: 字段1的值,初始化字段2: 字段2的值,…
}
下面是對各個部分的說明:
- 字段1、字段2……:結構體定義的字段名。
- 初始化字段1、初始化字段2……:結構體初始化時的字段名,可選擇性地對字段初始化。
- 字段類型1、字段類型2……:結構體定義字段的類型。
- 字段1的值、字段2的值……:結構體初始化字段的初始值。
鍵值對初始化部分是可選的,不初始化成員時,匿名結構體的格式變為:
ins := struct {字段1 字段類型1字段2 字段類型2…
}
4.2 匿名結構體示例
在本示例中,使用匿名結構體的方式定義和初始化一個消息結構,這個消息結構具有消息標示部分(ID)和數據部分(data),打印消息內容的 printMsg() 函數在接收匿名結構體時需要在參數上重新定義匿名結構體,代碼如下:
package mainimport ("fmt"
)// 定義 printMsgType() 函數,參數為 msg,類型為 *struct{id int data string},
// 因為類型沒有使用 type 定義,所以需要在每次用到的地方進行定義。
func printMsgType(msg *struct {id intdata string
}) {// 使用動詞%T打印msg的類型fmt.Printf("%T\n", msg) // *struct { id int; data string }
}func main() {// 實例化一個匿名結構體msg := &struct { // 定義部分id intdata string}{ // 值初始化部分1024,"hello",}printMsgType(msg)
}
5. 結構體訪問
在訪問結構體變量成員時,使用點號 . 操作符,格式為:
結構體.成員名
結構體類型變量使用 struct 關鍵字定義,示例如下:
package mainimport "fmt"type Phone struct {model stringcolor stringprice int
}func main() {var phone1 Phonevar phone2 Phonevar phone4 Phonefmt.Printf("phone1 init is %v\n", phone1)// 創建一個新的結構體phone1 = Phone{"華為", "black", 1000}fmt.Printf("phone1 is %v\n", phone1)// 也可以使用 key => value 格式phone2 = Phone{model: "華為", color: "black", price: 1000}fmt.Printf("phone2 is %v\n", phone2)// 忽略的字段為 0 或 空phone3 := Phone{model: "華為", price: 1000}fmt.Printf("phone3 is %v\n", phone3)// 訪問結構體成員phone4.price = 8000phone4.color = "blue"phone4.model = "蘋果"fmt.Printf("phone4 is %v\n", phone4)}
輸出:
phone1 init is { 0}
phone1 is {華為 black 1000}
phone2 is {華為 black 1000}
phone3 is {華為 1000}
phone4 is {蘋果 blue 8000}
6. 結構體作為形參
可以將結構體類型作為參數傳遞給函數。
package mainimport "fmt"type Phone struct {model stringcolor stringprice int
}func main() {var phone Phone// 訪問結構體成員phone.price = 8000phone.color = "blue"phone.model = "蘋果"printPhone(phone)}func printPhone(phone Phone) {fmt.Printf("phone price : %d\n", phone.price)fmt.Printf("phone color : %s\n", phone.color)fmt.Printf("phone model : %s\n", phone.model)
}
輸出:
phone price : 8000
phone color : blue
phone model : 蘋果
指針結構體作為值傳遞。
package mainimport "fmt"type InnerData struct {a int
}type Data struct {complax []intinstance InnerDataptr *InnerData
}func passByValue(inFunc Data) Data {fmt.Printf("inFunc value: %+v\n", inFunc)fmt.Printf("inFunc ptr: %p\n", &inFunc)return inFunc
}func main() {in := Data{complax: []int{1, 2, 3},instance: InnerData{5,},ptr: &InnerData{1},}fmt.Printf("in value: %+v\n", in)fmt.Printf("in ptr: %p\n", &in)out := passByValue(in)fmt.Printf("out value: %+v\n", out)fmt.Printf("out ptr: %p\n", &out)
}
輸出結果:
in value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000160e8}
in ptr: 0xc000082150
inFunc value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000160e8}
inFunc ptr: 0xc0000821e0
out value: {complax:[1 2 3] instance:{a:5} ptr:0xc0000160e8}
out ptr: 0xc0000821b0
7. 結構體指針
指向結構體的指針類似于其它指針變量,格式如下:
var structPointer *Phone
上面定義的指針變量可以存儲結構體變量的地址。如果需要查看結構體變量在內存中的地址,可以將 & 符號放置于結構體變量前:
structPointer = &phone1
使用結構體指針訪問結構體成員,使用 . 操作符:
structPointer.title
使用示例
package mainimport "fmt"type Phone struct {model stringcolor stringprice int
}func main() {var p *Phonevar phone Phonep = &phone// 訪問結構體成員p.price = 8000p.color = "blue"p.model = "蘋果"fmt.Printf("p is : %v\n", p)fmt.Printf("*p is : %v\n", *p)fmt.Printf("&p is : %v\n", &p)fmt.Printf("phone is : %v\n", phone)printPhone(p)}func printPhone(phone *Phone) {fmt.Printf("phone price : %d\n", phone.price)fmt.Printf("phone color : %s\n", phone.color)fmt.Printf("phone model : %s\n", phone.model)
}
輸出結果:
p is : &{蘋果 blue 8000}
*p is : {蘋果 blue 8000}
&p is : 0xc00000e028
phone is : {蘋果 blue 8000}
phone price : 8000
phone color : blue
phone model : 蘋果
var p *Phone // 就是說 p 這個指針是 Phone 類型的
p = &phone // phone 是 Phone 的一個實例化的結構,&phone 就是把這個結構體的內存地址賦給了 p,
*p // 那么在使用的時候,只要在 p 的前面加個*號,就可以把 p 這個內存地址對應的值給取出來了
&p // 就是取了 p 這個指針的內存地址,也就是 p 這個指針是放在內存空間的什么地方的。
phone // 就是 phone 這個結構體,打印出來就是它自己。也就是指針 p 前面帶了 * 號的效果。
8. 結構體擁有自身類型的指針類型、以自身類型為元素類型的切片類型
type T struct {t T // error... ...
}
Go 語言不支持這種在結構體類型定義中,遞歸地放入其自身類型字段的定義方式。面對上面的示例代碼,編譯器就會給出 invalid recursive type T 的錯誤信息。
但是下面的是可以的
type T struct {t *T // okst []T // okm map[string]T // ok
}
一個類型,它所占用的大小是固定的,因此一個結構體定義好的時候,其大小是固定的。但是,如果結構體里面套結構體,那么在計算該結構體占用大小的時候,就會成死循環。
但如果是指針、切片、map 等類型,其本質都是一個 int 大小(指針,4字節或者8字節,與操作系統有關),即因為指針、map、切片的變量元數據的內存占用大小是固定的,因此該結構體的大小是固定的,類型就能決定內存占用的大小。
因此,結構體是可以接受自身類型的指針類型、以自身類型為元素類型的切片類型,以及以自身類型作為 value 類型的 map 類型的字段,而自己本身不行。
9. 零值初始化
零值初始化說的是使用結構體的零值作為它的初始值。對于 Go 原生類型來說,這個默認值也稱為零值。Go 結構體類型由若干個字段組成,當這個結構體類型變量的各個字段的值都是零值時,我們就說這個結構體類型變量處于零值狀態。
var book Book // book為零值結構體變量
在 Go 語言標準庫和運行時的代碼中,有很多踐行“零值可用”理念的好例子,最典型的莫過于 sync 包的 Mutex 類型了。
運用“零值可用”類型,給 Go 語言中的線程互斥鎖帶來了什么好處呢?我們橫向對比一下 C 語言中的做法你就知道了。如果我們要在 C 語言中使用線程互斥鎖,我們通常需要這么做:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);pthread_mutex_lock(&mutex);
... ...
pthread_mutex_unlock(&mutex);
我們可以看到,在 C 中使用互斥鎖,我們需要首先聲明一個 mutex 變量。但這個時候,我們不能直接使用聲明過的變量,因為它的零值狀態是不可用的,我們必須使用 pthread_mutex_init 函數對其進行專門的初始化操作后,它才能處于可用狀態。再之后,我們才能進行 lock 與 unlock 操作。
但是在 Go 語言中,我們只需要這幾行代碼就可以了:
var mu sync.Mutex
mu.Lock()
mu.Unlock()
Go 標準庫的設計者很貼心地將 sync.Mutex 結構體的零值狀態,設計為可用狀態,這樣開發者便可直接基于零值狀態下的 Mutex 進行 lock 與 unlock 操作,而且不需要額外顯式地對它進行初始化操作了。
總結
以上是生活随笔為你收集整理的Go 学习笔记(14)— 结构体定义、实例化、初始化、匿名结构体、结构体访问、结构体作为形参、结构体指针的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 月经不调可以去做试管婴儿吗
- 下一篇: Go 学习笔记(13)— 指针定义、指针