Go結構體
結構體可以存儲一組不同類型的數據,是一種復合類型。Go 拋棄了類與繼承,同時也拋棄了構造方法,刻意弱化了面向對象的功能,Go 並非是一個傳統 OOP 的語言,但是 Go 依舊有著 OOP 的影子,通過結構體和方法也可以模擬出一個類。下面是一個簡單的結構體的例子:
type Programmer struct {
Name string
Age int
Job string
Language []string
}聲明
結構體的聲明非常簡單,例子如下:
type Person struct {
name string
age int
}結構體本身以及其內部的字段都遵守大小寫命名的暴露方式。對於一些類型相同的相鄰字段,可以不需要重復聲明類型,如下:
type Rectangle struct {
height, width, area int
color string
}TIP
在聲明結構體字段時,字段名不能與方法名重復
實例化
Go 不存在構造方法,大多數情況下采用如下的方式來實例化結構體,初始化的時候就像map一樣指定字段名稱再初始化字段值
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}不過也可以省略字段名稱,當省略字段名稱時,就必須初始化所有字段,通常不建議使用這種方式,因為可讀性很糟糕。
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}如果實例化過程比較復雜,你也可以編寫一個函數來實例化結構體,就像下面這樣,你也可以把它理解為一個構造函數
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}不過 Go 並不支持函數與方法重載,所以你無法為同一個函數或方法定義不同的參數。如果你想以多種方式實例化結構體,要麼創建多個構造函數,要麼建議使用 options 模式。
選項模式
選項模式是 Go 語言中一種很常見的設計模式,可以更為靈活的實例化結構體,拓展性強,並且不需要改變構造函數的函數簽名。假設有下面這樣一個結構體
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}聲明一個PersonOptions類型,它接受一個*Person類型的參數,它必須是指針,因為我們要在閉包中對 Person 賦值。
type PersonOptions func(p *Person)接下來創建選項函數,它們一般是With開頭,它們的返回值就是一個閉包函數。
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}實際聲明的構造函數簽名如下,它接受一個可變長PersonOptions類型的參數。
func NewPerson(options ...PersonOptions) *Person {
// 優先應用options
p := &Person{}
for _, option := range options {
option(p)
}
// 默認值處理
if p.Age < 0 {
p.Age = 0
}
......
return p
}這樣一來對於不同實例化的需求只需要一個構造函數即可完成,只需要傳入不同的 Options 函數即可
func main() {
pl := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}函數式選項模式在很多開源項目中都能看見,gRPC Server 的實例化方式也是采用了該設計模式。函數式選項模式只適合於復雜的實例化,如果參數只有簡單幾個,建議還是用普通的構造函數來解決。
組合
在 Go 中,結構體之間的關系是通過組合來表示的,可以顯式組合,也可以匿名組合,後者使用起來更類似於繼承,但本質上沒有任何變化。例如:
顯式組合的方式
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}在使用時需要顯式的指定字段p
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)而匿名組合可以不用顯式的指定字段
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}匿名字段的名稱默認為類型名,調用者可以直接訪問該類型的字段和方法,但除了更加方便以外與第一種方式沒有任何的區別。
student := Student{
Person: Person{name: "jack",age: 18},
school: "lili school",
}
fmt.Println(student.name)指針
對於結構體指針而言,不需要解引用就可以直接訪問結構體的內容,例子如下:
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)在編譯的時候會轉換為(*p).name ,(*p).age,其實還是需要解引用,不過在編碼的時候可以省去,算是一種語法糖。
標簽
結構體標簽是一種元編程的形式,結合反射可以做出很多奇妙的功能,格式如下
`key1:"val1" key2:"val2"`標簽是一種鍵值對的形式,使用空格進行分隔。結構體標簽的容錯性很低,如果沒能按照正確的格式書寫結構體,那麼將會導致無法正常讀取,但是在編譯時卻不會有任何的報錯,下方是一個使用示例。
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}結構體標簽最廣泛的應用就是在各種序列化格式中的別名定義,標簽的使用需要結合反射才能完整發揮出其功能。
內存對齊
Go 結構體字段的內存分布遵循內存對齊的規則,這麼做可以減少 CPU 訪問內存的次數,相應的佔用的內存要多一些,屬於空間換時間的一種手段。假設有如下結構體
type Num struct {
A int64
B int32
C int16
D int8
E int32
}已知這些類型的佔用字節數
int64佔 8 個字節int32佔 4 個字節int16佔 2 字節int8佔一個字節
整個結構體的內存佔用似乎是 8+4+2+1+4=19 個字節嗎,當然不是這樣,根據內存對齊規則而言,結構體的內存佔用長度至少是最大字段的整數倍,不足的則補齊。該結構體中最大的是int64佔用 8 個字節,那麼內存分布如下圖所示

所以實際上是佔用 24 個字節,其中有 5 個字節是無用的。
再來看下面這個結構體
type Num struct {
A int8
B int64
C int8
}明白了上面的規則後,可以很快的理解它的內存佔用也是 24 個字節,盡管它只有三個字段,足足浪費了 14 個字節。

但是我們可以調整字段,改成如下的順序
type Num struct {
A int8
C int8
B int64
}如此一來就佔用的內存就變為了 16 字節,浪費了 6 個字節,減少了 8 個字節的內存浪費。

從理論上來說,讓結構體中的字段按照合理的順序分布,可以減少其內存佔用。不過實際編碼過程中,並沒有必要的理由去這樣做,它不一定能在減少內存佔用這方面帶來實質性的提升,但一定會提高開發人員的血壓和心智負擔,尤其是在業務中一些結構體的字段數可能多大幾十個或者數百個,所以僅做了解即可。
空結構體
空結構體沒有字段,不佔用內存空間,我們可以通過unsafe.SizeOf函數來計算佔用的字節大小
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}輸出
0空結構體的使用場景有很多,比如之前提到過的,作為map的值類型,可以將map作為set來進行使用,又或者是作為通道的類型,表示僅做通知類型的通道。
