结构体

寒江蓑笠翁大约 11 分钟

结构体

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
}

提示

在声明结构体字段时,字段名与方法名不应该重复


创建

Go不存在构造方法,大多数情况下采用如下的方式来创建。

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

初始化的时候就像map一样指定字段名称再初始化字段值,不过也可以省略字段名称。

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

当省略字段名称时,就必须初始化所有字段,且必须按照声明的顺序初始化。

func NewProgrammer() Programmer {
   return Programmer{
      "jack",
      19,
      "coder",
      []string{"Go", "C++"}}
}

也可以编写一个函数来专门初始化结构体,这类函数通常有另一个名称:工厂方法,这也是为什么Go没有构造方法的原因。


组合

在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来进行使用,又或者是作为通道的类型,即代表一个不发送数据的通道。