Go 语言结构体类型

结构体类型概念

Go 通过类型别名(alias types)结构体的形式支持用户自定义类型。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据。结构体也是值类型,因此可以通过 new 函数来创建。

组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字。 在一个结构体中,字段名字必须是唯一的。

结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type),在一些老的编程语言中叫 记录(Record),比如 Cobol,在 C 家族的编程语言中它也存在,并且名字也是 struct,在面向对象的编程语言中,跟一个无方法的轻量级类一样。

结构体类型定义与申明

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

  1. type identifier struct {
  2. field1 type1
  3. field2 type2
  4. ...
  5. }

结构简单的结构体类型可以这样定义:type T struct {a, b int}

结构体里的字段都有 名字,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 _

结构体的字段可以是任何类型,甚至是结构体本身,也可以是函数或者接口(后续章节会讲到)。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值:

  1. var s T
  2. s.a = 5
  3. s.b = 8

数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段。

new一个结构体

使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行。

  1. var t *T
  2. t = new(T)

写这条语句的惯用方法是:t := new(T),变量 t 是一个指向 T指针,此时结构体字段的值是它们所属类型的零值

声明 var t T 也会给t分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)

示例 structs_fields.go 给出了一个非常简单的例子:

  1. package main
  2. import "fmt"
  3. type struct1 struct {
  4. i1 int
  5. f1 float32
  6. str string
  7. }
  8. func main() {
  9. ms := new(struct1)
  10. ms.i1 = 10
  11. ms.f1 = 15.5
  12. ms.str= "Chris"
  13. fmt.Printf("The int is: %d\n", ms.i1)
  14. fmt.Printf("The float is: %f\n", ms.f1)
  15. fmt.Printf("The string is: %s\n", ms.str)
  16. fmt.Println(ms)
  17. }

输出:

  1. The int is: 10
  2. The float is: 15.500000
  3. The string is: Chris
  4. &{10 15.5 Chris}

使用 fmt.Println 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 %v 选项。

可以使用点号符给字段赋值:structname.fieldname = value。同样的,使用点号符可以获取结构体字段的值:structname.fieldname

Go 语言中这叫 选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation) 来引用结构体的字段:

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

初始化一个结构体实例可以使用更简短的方式:

  1. ms := &struct1{10, 15.5, "Chris"}
  2. // 此时ms的类型是 *struct1

或者:

  1. var ms struct1
  2. ms = struct1{10, 15.5, "Chris"}

混合字面量语法(composite literal syntax)&struct1{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)中那样。

结构体类型和字段的命名遵循可见性规则(大写导出,小写不导出),一个导出的结构体类型中有些字段是可以导出的,有一些则不是。

结构体在定义它的包中必须是唯一的,它的完全类型名是:包名.结构体变量名

下面的例子展示了一个结构体 Person,一个方法,方法有一个类型为 *Person 的参数(对象本身是可以被改变的),以及三种调用这个方法的不同方式:

  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. )
  6. type Person struct {
  7. firstName string
  8. lastName string
  9. }
  10. func upPerson(p *Person) {
  11. p.firstName = strings.ToUpper(p.firstName)
  12. p.lastName = strings.ToUpper(p.lastName)
  13. }
  14. func main() {
  15. // 1-struct as a value type:
  16. var pers1 Person
  17. pers1.firstName = "Chris"
  18. pers1.lastName = "Woodward"
  19. upPerson(&pers1)
  20. fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)
  21. // 2—struct as a pointer:
  22. pers2 := new(Person)
  23. pers2.firstName = "Chris"
  24. pers2.lastName = "Woodward"
  25. (*pers2).lastName = "Woodward" // 这是合法的
  26. upPerson(pers2)
  27. fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)
  28. // 3—struct as a literal:
  29. pers3 := &Person{"Chris","Woodward"}
  30. upPerson(pers3)
  31. fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
  32. }

输出:

  1. The name of the person is CHRIS WOODWARD
  2. The name of the person is CHRIS WOODWARD
  3. The name of the person is CHRIS WOODWARD

在上面例子的第二种情况中,可以直接通过指针,像 pers2.lastName="Woodward" 这样给结构体字段赋值,没有像 C++ 中那样需要使用 -> 操作符,Go 会自动做这样的转换。

注意也可以通过解指针的方式来设置值:(*pers2).lastName = "Woodward"

结构体的内存布局

Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go 语言中的指针很像。下面的例子清晰地说明了这些情况:

  1. type Rect1 struct {Min, Max Point }
  2. type Rect2 struct {Min, Max *Point }

结构体转换

Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型时,此结构体类型和它的 alias 类型都有相同的底层类型,它们可以像下面的例子一样互相转换,同时需要注意其中非法赋值或转换引起的编译错误。

  1. package main
  2. import "fmt"
  3. type number struct {
  4. f float32
  5. }
  6. type nr number // alias type
  7. func main() {
  8. a := number{5.0}
  9. b := nr{5.0}
  10. // var i float32 = b // compile-error: cannot use b (type nr) as type float32 in assignment
  11. // var i = float32(b) // compile-error: cannot convert b (type nr) to type float32
  12. // var c number = b // compile-error: cannot use b (type nr) as type number in assignment
  13. // needs a conversion:
  14. var c = number(b)
  15. fmt.Println(a, b, c)
  16. }

输出:

  1. {5} {5} {5}