结构体(struct)

Go 的设计目标是取代 C/C++,所以 Go 里面的 struct 和 C 的类似,与 int/float 一样属于值类型,值类型的特点是内存紧凑,大小固定,对 GC 与内存访问来说都比较友好。

Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。
结构体也是值类型,因此可以通过 new 函数来创建。

定义

结构体是由一系列称为字段(fields)的命名元素组成,每个元素都有一个名称和一个类型。

字段名称可以

  • 显式指定(IdentifierList)
  • 隐式指定(EmbeddedField)

没有显式字段名称的字段称为 匿名(内嵌)字段。

在结构体中,非空字段名称必须是唯一的。

结构体定义的一般方式如下:

  1. type identifier struct {
  2. field1 type1
  3. field2 type2
  4. ...
  5. }
  6. // 或者
  7. type T struct { a, b int }

理论上,每个字段都是有具有唯一性的名字的,但如果确定某个字段不会被使用,可以将其名称定义为空标识符_来丢弃掉:

  1. type T struct {
  2. _ string
  3. a int
  4. }

具名结构体

  1. type Point struct { X, Y int }
  2. type Rect1 struct { Min, Max Point }
  3. type Rect2 struct { Min, Max *Point }

定义了3个个具名结构体分别是 Point Rect1 Rect2 ,内存分布如下图

image.png
struct 内存分布示意图
可以看到, Point Rect1 Rect2 , 在内存中都是连续的。

匿名结构体

  1. person:= struct { // 匿名结构
  2. name string
  3. age int
  4. }{name:"匿名", age:1}
  5. f.Println("person:", person)

空结构体

可以使用 unsafe.Sizeof 计算出一个数据类型实例需要占用的字节数。
空结构体占用空间么

  1. package main
  2. import (
  3. "fmt"
  4. "unsafe"
  5. )
  6. func main() {
  7. fmt.Println(unsafe.Sizeof(struct{}{}))
  8. }
  9. // 运行上面的例子将会输出:
  10. $ go run main.go
  11. 0

也就是说,空结构体 struct{} 实例不占据任何的内存空间。
struct{}表示一个空的结构体,注意,直接定义一个空的结构体并没有意义。
但在某些方面有很多实用的功能,具体可以参考:Go 语言高性能编程 - 使用空结构体节省内存

初始化

内存分配

使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:

  1. type S struct {
  2. a int;
  3. b float64
  4. }
  5. new(S)

new(S)S 类型的变量分配内存,并初始化(a = 0,b = 0.0),返回包含该位置地址的类型 * S的值。

我们一般的惯用方法是:

  1. t := new(T) // 变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。
  2. var t T // 给 t 分配内存,并零值化内存,但是这个时候 t 是类型 T。


两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。

结构体普通变量初始化

这个使用方式并没有为字段赋初始值,因此所有字段都会被自动赋予自已类型的零值
比如name的值为空字符串 "",age 的值为 0。

  1. type Member struct {
  2. id int
  3. name string
  4. email string
  5. gender int
  6. age int
  7. }
  8. // 直接定义变量
  9. var m1 Member // 所有字段均为字段类型的默认值
  10. var m2 = Member{1, "小明", "xiaoming@163.com", 1, 18} // 简短变量声明方式:m2 := Member{1,"小明","xiaoming@163.com",1,18}
  11. var m3 = Member{id:2, "name":"小红"} // 简短变量声明方式:m3 := Member{id:2,"name":"小红"}

使用字面量创建变量,这种使用方式,可以在大括号中为结构体的成员赋初始值,有两种赋初始值的方式:

  • 一种是按字段在结构体中的顺序赋值,上面代码中m2就是使用这种方式,这种方式要求所有的字段都必须赋值,因此如果字段太多,每个字段都要赋值,会很繁琐,
  • 一种则使用字段名为指定字段赋值,如上面代码中变量m3的创建,使用这种方式,对于其他没有指定的字段,则使用该字段类型的零值作为初始化值。

初始化一个结构体实例

  1. type myStruct struct { i int }
  2. var v myStruct // v 是结构体类型变量
  3. var p *myStruct // p 是指向一个结构体类型变量的指针
  4. v.i
  5. p.i

字面量初始化

  1. var st structTest
  2. st = structTest{10, 15.5, "皮肤较黑"}

new 初始化 (指针) &TYPE{}

使用new()函数或&TYPE{}的方式来构造struct实例,它会为struct分配内存,为各个字段做好默认的赋0初始化。

它们是等价的,都返回数据对象的指针给变量,实际上&TYPE{}的底层会调用new()。

  1. type structTest struct {
  2. i1 int
  3. f1 float32
  4. str string
  5. }
  6. // new初始化时不能赋值
  7. st := new(structTest)
  8. st.i1 = 10
  9. st.f1 = 15.5
  10. st.str= "皮肤较黑"
  11. // 等价于
  12. // &TYPE{} 初始化时可以赋值
  13. st := &structTest{
  14. 10,
  15. 15.5,
  16. "皮肤较黑",
  17. }
  18. // 此时st的类型是 *structTest
  19. fmt.Printf("st type is %T\n", st) // st type is *main.structTest

混合字面量语法(composite literal syntax)&structTest{a, b, c} 是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。

在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。

表达式 new(Type) 和 &Type{} 是等价的。
时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子:

  1. type Interval struct {
  2. start int
  3. end int
  4. }

初始化方式:

  1. intr := Interval{0, 3} (A)
  2. intr := Interval{end:5, start:1} (B)
  3. intr := Interval{end:5} (C)
  • (A)中,值必须以字段在结构体定义时的顺序给出,& 不是必须的。
  • (B)显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像(C)中那样。

选择 new() 还是选择 &TYPE{} 的方式构造实例?完全随意,它们是等价的。
但如果想要初始化时就赋值,可以考虑使用 &TYPE{} 的方式。

struct的值和指针

  1. type person struct{
  2. name string
  3. age int
  4. }
  5. // 下面三种方式都可以构造person struct的实例p:
  6. p1 := person{}
  7. p2 := &person{}
  8. p3 := new(person)
  9. fmt.Println(p1)
  10. fmt.Println(p2)
  11. fmt.Println(p3)
  12. // 打印结果
  13. { 0}
  14. &{ 0}
  15. &{ 0}

p1、p2、p3都是person struct的实例,但p2和p3是完全等价的,它们都指向实例的指针,指针中保存的是实例的地址,所以指针再指向实例,p1则是直接指向实例。
这三个变量与person struct实例的指向关系如下:

  1. 变量名 指针 数据对象(实例)
  2. -------------------------------
  3. p1(addr) -------------> { 0}
  4. p2 -----> ptr(addr) --> { 0}
  5. p3 -----> ptr(addr) --> { 0}
  • p1和ptr(addr)保存的都是数据对象的地址
  • p2和p3则保存ptr(addr)的地址,通常,将指向指针的变量(p1、p2)直接称为指针,将直接指向数据对象的变量(p1)称为对象本身,因为指向数据对象的内容就是数据对象的地址

尽管一个是数据对象值,一个是指针,它们都是数据对象的实例。
也就是说,p1.name和p2.name都能访问对应实例的属性。

var p4 *person 呢,它是什么?

该语句表示 p4是一个指针,它的指向对象是person类型的。
但因为它是一个指针,它将初始化为nil,即表示没有指向目标。
但已经明确表示了,p4所指向的是一个保存数据对象地址的指针。也就是说,目前为止,p4的指向关系如下:

  1. p4 -> ptr(nil)

既然 p4是一个指针,那么可以将 &person{}new(person)赋值给p4。

  1. var p4 *person
  2. p4 = &person{
  3. name:"longshuai",
  4. age:23,
  5. }
  6. fmt.Println(p4) // &{longshuai 23}

访问字段

通过变量名,使用逗号.,可以访问结构体类型中的:

  • 字段
  • 为字段赋值
  • 对字段进行取址(&)操作

普通变量

  1. package main
  2. import "fmt"
  3. //定义一个结构体类型
  4. type Student struct {
  5. id int
  6. name string
  7. sex byte //字符类型
  8. age int
  9. addr string
  10. }
  11. func main() {
  12. //定义一个结构体普通变量
  13. var s Student
  14. //操作成员,需要使用点(.)运算符
  15. s.id = 1
  16. s.name = "mike"
  17. s.sex = 'm' //字符
  18. s.age = 18
  19. s.addr = "bj"
  20. fmt.Println("s = ", s) // s = {1 mike 109 18 bj}
  21. fmt.Println(s.age) // 18
  22. }

指针结构体

指针结构体,即一个指向结构体的指针,声明结构体变量时,在结构体类型前加*号,便声明一个指向结构体的指针,如:

注意,指针类型为引用类型,声明结构体指针时,如果未初始化,则初始值为nil,只有初始化后,才能访问字段或为字段赋值。

  1. var p1 *Student
  2. p1.name = "小明" // 错误用法,未初始化,p1为nil
  3. p1 = &Student{}
  4. p1.name = "小明" // 初始化后,结构体指针指向某个结构体地址,才能访问字段,为字段赋值。
  1. package main
  2. import "fmt"
  3. //定义一个结构体类型
  4. type Student struct {
  5. id int
  6. name string
  7. sex byte //字符类型
  8. age int
  9. addr string
  10. }
  11. func main() {
  12. //1、指针有合法指向后,才操作成员
  13. //先定义一个普通结构体变量
  14. var s Student
  15. //在定义一个指针变量,保存s的地址
  16. var p1 *Student
  17. p1 = &s
  18. //通过指针操作成员 p1.id 和(*p1).id完全等价,只能使用.运算符
  19. p1.id = 1
  20. (*p1).name = "mike"
  21. p1.sex = 'm'
  22. p1.age = 18
  23. p1.addr = "bj"
  24. fmt.Println("p1 = ", p1)
  25. //2、通过new申请一个结构体
  26. p2 := new(Student)
  27. p2.id = 1
  28. p2.name = "mike"
  29. p2.sex = 'm'
  30. p2.age = 18
  31. p2.addr = "bj"
  32. fmt.Println("p2 = ", p2)
  33. //顺序初始化,每个成员必须初始化, 别忘了&
  34. var p3 *Student = &Student{1, "mike", 'm', 18, "bj"}
  35. fmt.Println("p1 = ", p3) // p3 = &{1 mike 109 18 bj}
  36. //指定成员初始化,没有初始化的成员,自动赋值为0
  37. p4 := &Student{name: "mike", addr: "bj"}
  38. fmt.Printf("p2 type is %T\n", p4) // p4 type is *main.Student
  39. fmt.Println("p2 = ", p4) // p4 = &{0 mike 0 0 bj}
  40. }

结构体参数

结构体与数组一样,都是值传递

  • 比如当把数组或结构体作为实参传给函数的形参时,会复制一个副本,所以为了提高性能,一般不会把数组直接传递给函数,而是使用切片(引用类型)代替
  • 结构体传给函数时,可以使用指针结构体传递(传引用)。使用频率非常高!!!
  1. package main
  2. import "fmt"
  3. type View struct {
  4. Id int
  5. Ip string
  6. Title string
  7. }
  8. // 引用传递
  9. func TestViewP(p *View) {
  10. p.Ip = "192.168.1.1"
  11. }
  12. // 值拷贝
  13. func TestView(p View) {
  14. p.Ip = "192.168.1.1"
  15. }
  16. func main() {
  17. // 结构体指针
  18. pView := &View{
  19. Id: 1001,
  20. Ip: "127.0.0.1",
  21. Title: "Hello",
  22. }
  23. fmt.Println(pView) // &{1001 127.0.0.1 Hello}
  24. TestViewP(pView) // 传的是地址 引用
  25. fmt.Println(pView) // &{1001 192.168.1.1 Hello}
  26. // 普通结构体
  27. myView := View{
  28. Id: 1001,
  29. Ip: "180.0.0.1",
  30. Title: "World",
  31. }
  32. fmt.Println(myView) // {1001 180.0.0.1 World}
  33. //值传递
  34. TestView(myView)
  35. fmt.Println(myView) // {1001 180.0.0.1 World}
  36. //引用传递
  37. TestViewP(&myView)
  38. fmt.Println(myView) // {1001 192.168.1.1 World}
  39. }
  40. // &{1001 127.0.0.1 Hello}
  41. // &{1001 192.168.1.1 Hello}
  42. // {1001 180.0.0.1 World}
  43. // {1001 180.0.0.1 World}
  44. // {1001 192.168.1.1 World}

字段标记

在定义结构体字段时,除字段名称和数据类型外,还可以使用反引号为结构体字段声明元信息,这种元信息称为Tag。它是一个附属于字段的字符串,可以是文档或其他的重要标记。

这些标记信息通过反射接口可见,并参与结构体的类型标识,但在其他情况下被忽略。

  1. type Member struct {
  2. Id int `json:"id,-"`
  3. Name string `json:"name"`
  4. Email string `json:"email"`
  5. Gender int `json:"gender,"`
  6. Age int `json:"age"`
  7. }

Tag由反引号括起来的一系列用空格分隔的 key:"value" 键值对组成,如:

  1. Id int `json:"id" gorm:"AUTO_INCREMENT"`

标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。

使用 reflect 包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type TagType struct { // tags
  7. field1 bool "An important answer"
  8. field2 string "The name of the thing"
  9. field3 int "How much there are"
  10. }
  11. func main() {
  12. tt := TagType{true, "Barak Obama", 1}
  13. for i := 0; i < 3; i++ {
  14. refTag(tt, i)
  15. }
  16. }
  17. func refTag(tt TagType, ix int) {
  18. ttType := reflect.TypeOf(tt)
  19. ixField := ttType.Field(ix)
  20. fmt.Printf("%v\n", ixField.Tag)
  21. }
  22. // An important answer
  23. // The name of the thing
  24. // How much there are

结构匿名组合

匿名字段

一般情况下,定义结构体的时候是字段名与其类型一一对应,
实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

当匿名字段也是一个结构体的时候,那么这个结构体所拥有的全部字段都被隐式地引入了当前定义的这个结构体。

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别
  6. age int //年龄
  7. }
  8. // 匿名字段 Person
  9. type Student struct {
  10. Person // 只有类型,没有名字,匿名字段,继承了Person的成员
  11. id int
  12. addr string
  13. }
  14. func main() {
  15. //顺序初始化
  16. var s1 Student = Student{Person{"mike", 'm', 18}, 1, "bj"}
  17. fmt.Println("s1 = ", s1) // s1 = {{mike 109 18} 1 bj}
  18. //自动推导类型
  19. s2 := Student{Person{"mike", 'm', 18}, 1, "bj"}
  20. //fmt.Println("s2 = ", s2)
  21. //%+v, 显示更详细
  22. fmt.Printf("s2 = %+v\n", s2) // s2 = {Person:{name:mike sex:109 age:18} id:1 addr:bj}
  23. //指定成员初始化,没有初始化的常用自动赋值为0
  24. s3 := Student{id: 1}
  25. fmt.Printf("s3 = %+v\n", s3) // s3 = {Person:{name: sex:0 age:0} id:1 addr:}
  26. s4 := Student{Person: Person{name: "mike"}, id: 1}
  27. fmt.Printf("s4 = %+v\n", s4) // s4 = {Person:{name:mike sex:0 age:0} id:1 addr:}
  28. //s5 := Student{"mike", 'm', 18, 1, "bj"} //err
  29. }

成员操作

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. type Student struct {
  9. Person //只有类型,没有名字,匿名字段,继承了Person的成员
  10. id int
  11. addr string
  12. }
  13. func main() {
  14. s1 := Student{Person{"mike", 'm', 18}, 1, "bj"}
  15. //给成员赋值
  16. s1.name = "yoyo" // //等价于 s1.Person.name = "mike"
  17. s1.sex = 'f'
  18. s1.age = 22
  19. s1.id = 666
  20. s1.addr = "sz"
  21. fmt.Printf("s1 = %+v\n", s1) // s1 = {Person:{name:yoyo sex:102 age:22} id:666 addr:sz}
  22. s1.Person = Person{"go", 'm', 18}
  23. fmt.Printf("s1 = %+v\n", s1) // s1 = {Person:{name:go sex:109 age:18} id:666 addr:sz}
  24. fmt.Println(s1.name, s1.sex, s1.age, s1.id, s1.addr) // go 109 18 666 sz
  25. }

同名字段

就近原则

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. type Student struct {
  9. Person //只有类型,没有名字,匿名字段,继承了Person的成员
  10. id int
  11. addr string
  12. name string //和Person同名了
  13. }
  14. func main() {
  15. //声明(定义一个变量)
  16. var s Student
  17. //默认规则(纠结原则),如果能在本作用域找到此成员,就操作此成员
  18. // 如果没有找到,找到继承的字段
  19. s.name = "mike" //操作的是Student的name,还是Person的name?, 结论为Student的
  20. s.sex = 'm'
  21. s.age = 18
  22. s.addr = "bj"
  23. //显式调用
  24. s.Person.name = "yoyo" //Person的name
  25. fmt.Printf("s = %+v\n", s) // s = {Person:{name:yoyo sex:109 age:18} id:0 addr:bj name:mike}
  26. }

其它匿名字段

非结构体类型

所有的内置类型和自定义类型都是可以作为匿名字段的:

  1. package main
  2. import "fmt"
  3. type mystr string //自定义类型,给一个类型改名
  4. type Person struct {
  5. name string //名字
  6. sex byte //性别, 字符类型
  7. age int //年龄
  8. }
  9. type Student struct {
  10. Person // 结构体匿名字段
  11. int // 基础类型的匿名字段
  12. mystr
  13. }
  14. func main() {
  15. s := Student{Person{"mike", 'm', 18}, 666, "hehehe"}
  16. fmt.Printf("s = %+v\n", s) // s = {Person:{name:mike sex:109 age:18} int:666 mystr:hehehe}
  17. s.Person = Person{"go", 'm', 22}
  18. fmt.Println(s.name, s.age, s.sex, s.int, s.mystr) // go 22 109 666 hehehe
  19. fmt.Println(s.Person, s.int, s.mystr) // {go 109 22} 666 hehehe
  20. }

结构体指针类型

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string // 名字
  5. sex byte // 性别, 字符类型
  6. age int // 年龄
  7. }
  8. type Student struct {
  9. *Person // 匿名字段,结构体指针类型
  10. id int
  11. addr string
  12. }
  13. func main() {
  14. // 初始化 指针类型取址
  15. s1 := Student{&Person{"mike", 'm', 18}, 666, "bj"}
  16. fmt.Println(s1.name, s1.sex, s1.age, s1.id, s1.addr) // mike 109 18 666 bj
  17. //先定义变量
  18. var s2 Student
  19. s2.Person = new(Person) //分配空间
  20. s2.name = "yoyo"
  21. s2.sex = 'm'
  22. s2.age = 18
  23. s2.id = 222
  24. s2.addr = "sz"
  25. fmt.Println(s2.name, s2.sex, s2.age, s2.id, s2.addr) // yoyo 109 18 222 sz
  26. }

参考