Skip to content

Go結構體

結構體可以存儲一組不同類型的數據,是一種復合類型。Go 拋棄了類與繼承,同時也拋棄了構造方法,刻意弱化了面向對象的功能,Go 並非是一個傳統 OOP 的語言,但是 Go 依舊有著 OOP 的影子,通過結構體和方法也可以模擬出一個類。下面是一個簡單的結構體的例子:

go
type Programmer struct {
  Name     string
  Age      int
  Job      string
  Language []string
}

聲明

結構體的聲明非常簡單,例子如下:

go
type Person struct {
   name string
   age int
}

結構體本身以及其內部的字段都遵守大小寫命名的暴露方式。對於一些類型相同的相鄰字段,可以不需要重復聲明類型,如下:

go
type Rectangle struct {
  height, width, area int
  color               string
}

TIP

在聲明結構體字段時,字段名不能與方法名重復

實例化

Go 不存在構造方法,大多數情況下采用如下的方式來實例化結構體,初始化的時候就像map一樣指定字段名稱再初始化字段值

go
programmer := Programmer{
   Name:     "jack",
   Age:      19,
   Job:      "coder",
   Language: []string{"Go", "C++"},
}

不過也可以省略字段名稱,當省略字段名稱時,就必須初始化所有字段,通常不建議使用這種方式,因為可讀性很糟糕。

go
programmer := Programmer{
   "jack",
   19,
   "coder",
   []string{"Go", "C++"}}

如果實例化過程比較復雜,你也可以編寫一個函數來實例化結構體,就像下面這樣,你也可以把它理解為一個構造函數

go
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 語言中一種很常見的設計模式,可以更為靈活的實例化結構體,拓展性強,並且不需要改變構造函數的函數簽名。假設有下面這樣一個結構體

go
type Person struct {
  Name     string
  Age      int
  Address  string
  Salary   float64
  Birthday string
}

聲明一個PersonOptions類型,它接受一個*Person類型的參數,它必須是指針,因為我們要在閉包中對 Person 賦值。

go
type PersonOptions func(p *Person)

接下來創建選項函數,它們一般是With開頭,它們的返回值就是一個閉包函數。

go
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類型的參數。

go
func NewPerson(options ...PersonOptions) *Person {
    // 優先應用options
  p := &Person{}
    for _, option := range options {
        option(p)
    }

  // 默認值處理
  if p.Age < 0 {
    p.Age = 0
  }
  ......

    return p
}

這樣一來對於不同實例化的需求只需要一個構造函數即可完成,只需要傳入不同的 Options 函數即可

go
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 中,結構體之間的關系是通過組合來表示的,可以顯式組合,也可以匿名組合,後者使用起來更類似於繼承,但本質上沒有任何變化。例如:

顯式組合的方式

go
type Person struct {
   name string
   age  int
}

type Student struct {
   p      Person
   school string
}

type Employee struct {
   p   Person
   job string
}

在使用時需要顯式的指定字段p

go
student := Student{
   p:      Person{name: "jack", age: 18},
   school: "lili school",
}
fmt.Println(student.p.name)

而匿名組合可以不用顯式的指定字段

go
type Person struct {
  name string
  age  int
}

type Student struct {
  Person
  school string
}

type Employee struct {
  Person
  job string
}

匿名字段的名稱默認為類型名,調用者可以直接訪問該類型的字段和方法,但除了更加方便以外與第一種方式沒有任何的區別。

go
student := Student{
   Person: Person{name: "jack",age: 18},
   school: "lili school",
}
fmt.Println(student.name)

指針

對於結構體指針而言,不需要解引用就可以直接訪問結構體的內容,例子如下:

go
p := &Person{
   name: "jack",
   age:  18,
}
fmt.Println(p.age,p.name)

在編譯的時候會轉換為(*p).name(*p).age,其實還是需要解引用,不過在編碼的時候可以省去,算是一種語法糖。

標簽

結構體標簽是一種元編程的形式,結合反射可以做出很多奇妙的功能,格式如下

go
`key1:"val1" key2:"val2"`

標簽是一種鍵值對的形式,使用空格進行分隔。結構體標簽的容錯性很低,如果沒能按照正確的格式書寫結構體,那麼將會導致無法正常讀取,但是在編譯時卻不會有任何的報錯,下方是一個使用示例。

go
type Programmer struct {
    Name     string `json:"name"`
    Age      int `yaml:"age"`
    Job      string `toml:"job"`
    Language []string `properties:"language"`
}

結構體標簽最廣泛的應用就是在各種序列化格式中的別名定義,標簽的使用需要結合反射才能完整發揮出其功能。

內存對齊

Go 結構體字段的內存分布遵循內存對齊的規則,這麼做可以減少 CPU 訪問內存的次數,相應的佔用的內存要多一些,屬於空間換時間的一種手段。假設有如下結構體

go
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 個字節是無用的。

再來看下面這個結構體

go
type Num struct {
  A int8
  B int64
  C int8
}

明白了上面的規則後,可以很快的理解它的內存佔用也是 24 個字節,盡管它只有三個字段,足足浪費了 14 個字節。

但是我們可以調整字段,改成如下的順序

type Num struct {
  A int8
  C int8
  B int64
}

如此一來就佔用的內存就變為了 16 字節,浪費了 6 個字節,減少了 8 個字節的內存浪費。

從理論上來說,讓結構體中的字段按照合理的順序分布,可以減少其內存佔用。不過實際編碼過程中,並沒有必要的理由去這樣做,它不一定能在減少內存佔用這方面帶來實質性的提升,但一定會提高開發人員的血壓和心智負擔,尤其是在業務中一些結構體的字段數可能多大幾十個或者數百個,所以僅做了解即可。

TIP

如果你真的想通過此種方法來節省內存,可以看看這兩個庫

他們會檢查你的源代碼中的結構體,計算並重新排布結構體字段來最小化結構體佔用的內存。

空結構體

空結構體沒有字段,不佔用內存空間,我們可以通過unsafe.SizeOf函數來計算佔用的字節大小

go
func main() {
   type Empty struct {}
   fmt.Println(unsafe.Sizeof(Empty{}))
}

輸出

0

空結構體的使用場景有很多,比如之前提到過的,作為map的值類型,可以將map作為set來進行使用,又或者是作為通道的類型,表示僅做通知類型的通道。

Golang學習網由www.golangdev.cn整理維護