Skip to content

结构体

结构体将多个不同类型命名字段序列打包成一个复合类型。

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。
每个值称为结构体的成员。用结构体的经典案例处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。
所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。

字段名必须唯一,可用_补位,支持使用自身指针类型成员。
字段名、排列顺序属类型组成部分。除对齐处理外,编译器不会优化、调整内存布局。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "fmt"
)

type node struct {
    _    int
    id   int
    next *node
}

func main() {
    n1 := node{
        id: 1,
    }

    n2 := node{
        id:   2,
        next: &n1,
    }

    fmt.Println(n1, n2)
}

可按顺序初始化全部字段,或使用命名方式初始化指定字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
)

func main() {
    type user struct {
        name string
        age  byte
    }

    u1 := user{"Tom", 12}
    u2 := user{"Tom"} // 错误: too few values in struct initializer
    fmt.Println(u1, u2)
}

推荐用命名初始化。
这样在扩充结构字段或调整字段顺序时,不会导致初始化语句出错。

可直接定义匿名结构类型变量,或用作字段类型。
但因其缺少类型标识,在作为字段类型时无法直接初始化,稍显麻烦

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
    "fmt"
)

func main() {
    u := struct { // 直接定义匿名结构变量
        name string
        age  byte
    }{
        name: "Tome",
        age:  12,
    }

    type file struct {
        name string
        attr struct { // 定义匿名结构类型
            owner int
            perm  int
        }
    }

    f := file{
        name: "test.dat",
        // attr: {  // 错误: missing type in composite literal
        //  owner: 1,
        //  perm: 0755,
        // },
    }

    f.attr.owner = 1 // 正确方式
    f.attr.perm = 0755

    fmt.Println(u, f)
}

只有在所有字段类型全部支持时,才可做相等操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

func main() {
    type data struct {
        x int
        y map[string]int
    }

    d1 := data{
        x: 10,
    }

    d2 := data{
        x: 100,
    }

    println(d1 == d2) // invalid operation: d1 == d2 (struct containing map[string]int cannot be compared)
}

可使用指针直接操作结构字段,但不能是多级指针

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

func main() {
    type user struct {
        name string
        age  int
    }

    p := &user{
        name: "Tom",
        age:  20,
    }

    p.name = "Mary"
    p.age++

    p2 := &p
    *p2.name = "Jack" // p2.name undefined (type **user has no field or method name)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import "fmt"

type person struct {
    name string
    age  int
}

func newPerson(name string) *person {

    p := person{name: name}
    p.age = 42
    return &p
}

func main() {

    fmt.Println(person{"Bob", 20})

    fmt.Println(person{name: "Alice", age: 30})

    fmt.Println(person{name: "Fred"})

    fmt.Println(&person{name: "Ann", age: 40})

    fmt.Println(newPerson("Jon"))

    s := person{name: "Sean", age: 50}
    fmt.Println(s.name)

    sp := &s
    fmt.Println(sp.age)

    sp.age = 51
    fmt.Println(sp.age)
}

输出:

1
2
3
4
5
6
7
{Alice 30}
{Fred 0}
&{Ann 40}
&{Jon 42}
Sean
50
51

空结构

空结构struct{}是指没有字段的结构类型。
它比较特殊,因为无论是其自身,还是作为数组元素类型,其长度都为零。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "unsafe"

func main() {
    var a struct{}
    var b [100]struct{}

    println(unsafe.Sizeof(a), unsafe.Sizeof(b))
}

输出:

1
0 0

尽管没有分配数组内存,但仍然可以操作元素,对应切片 len、cap 属性也正常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "fmt"
)

func main() {
    var d [100]struct{}
    s := d[:]

    d[1] = struct{}{}
    s[2] = struct{}{}

    fmt.Println(s[3], len(s), cap(s))
}

输出:

1
{} 100 100

实际上,这类“长度”为零的对象通常都指向 runtime.zerobase 变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
    "fmt"
)

func main() {
    a := [10]struct{}{}
    b := a[:] // 底层数组指向 zerobase,而非 slice
    c := [0]int{}

    fmt.Printf("%p, %p, %p\n", &a[0], &b[0], &c)
}

输出:

1
0x118e370, 0x118e370, 0x118e370

空结构可作为通道元素类型,用于事件通知。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

func main() {
    exit := make(chan struct{})

    go func() {
        println("hello,world!")
        exit <- struct{}{}
    }()

    <-exit
    println("end.")
}

匿名字段

所谓匿名字段,是指没有名字,仅有类型的字段,也被称作嵌入字段或嵌入类型

1
2
3
4
5
6
7
8
type attr struct {
    perm int
}

type file struct {
    name string
    attr   // 仅有类型名
}

从编译器角度看,这只是隐式地以类型名作为字段名字。
可直接引用匿名字段的成员,但初始化时须当作独立字段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
    f := file{
        name: "test.dat",
        attr: attr{  // 显式初始化匿名字段
            perm: 0755,
        },
    }

    f.perm = 0644  // 直接设置匿名字段成员
    println(f.perm)  // 直接读取匿名字段成员
}

如嵌入其他包中的类型,则隐式字段名字不包括包名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type data struct {
    os.File
}

func main() {
    d := data{
        File: os.File{},
    }

    fmt.Printf("%#v\n", d)
}

不仅仅是结构体,除接口指针和多级指针以外的任何命名类型都可作为匿名字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type data struct {
    *int  // 嵌入指针类型
    string
}

func main() {
    x := 100
    d := data{
        int: &x,  // 使用基础类型作为字段名
        string: "abc",
    }

    fmt.Printf("%#v\n", d)
}

输出:

1
main.data{int:(*int)(0xc000118000), string:"abc"}

因未命名类型没有名字标识,自然无法作为匿名字段

1
2
3
4
5
6
7
8
9
type a *int
type b **int
type c interface{}

type d struct{
    *a  // 错误: embedded type cannot be a pointer
    b // 错误: embedded type cannot be a pointer
    *c // 错误: embedded type cannot be a pointer to interface
}

不能将基础类型和其指针类型同时嵌入,因为两者隐式名字相同

1
2
3
4
type data struct {
    *int
    int  // 错误: duplicate field int
}

虽然可以像普通字段那样访问匿名字段成员,但会存在重名问题。
默认情况下,编译器从当前显式命名字段开始,逐步向内查找匿名字段成员。
如匿名字段成员被外层同名字段遮蔽,那么必须使用显式字段名。

字段标签

字段标签(tag)并不是注释,而是用来对字段进行描述的元数据。尽管它不属于数据成员,但却是类型的组成部分。

在运行期,可用反射获取标签信息。它常被用作格式校验,数据库关系映射等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "fmt"
    "reflect"
)

type user struct {
    name string `昵称`
    sex  byte   `性别`
}

func main() {
    u := user{"Tom", 1}
    v := reflect.ValueOf(u)
    t := v.Type()

    for i, n := 0, t.NumField(); i < n; i++ {
        fmt.Printf("%s: %v\n", t.Field(i).Tag, v.Field(i))
    }
}

输出:

1
2
昵称: Tom
性别: 1

内存布局

不管结构体包含多少字段,其内存总是一次性分配的,各字段在相邻的地址空间按定义顺序排列。
当然,对于引用类型、字符串和指针,结构内存中只包含其基本(头部)数据。
还有,所有匿名字段成员也被包含在内。

借助 unsafe 包中的相关函数,可输出所有字段的偏移量和长度

JSON

JavaScript对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议。在类似的协议中,JSON并不是唯一的一个标准协议。
XML、ASN.1 和 Google 的 Protocol Buffers 都是类似的协议,并且有各自的特色,但是由于简洁性、可读性和流行程度等原因,JSON是应用最广泛的一个。

Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的 encoding/json、encoding/xml、encoding/asn1 等包提供支持
(Protocol Buffers的支持由 github.com/golang/protobuf 包提供),并且这类包都有着相似的API接口。

考虑一个应用程序,该程序负责收集各种电影评论并提供反馈功能。它的Movie数据类型和一个典型的表示电影的值列表如下所示。
(在结构体声明中,Year和Color成员后面的字符串面值是结构体成员Tag;)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Movie struct {
    Title  string
    Year   int  `json:"released"`
    Color  bool `json:"color,omitempty"`
    Actors []string
}

var movies = []Movie{
    {Title: "Casablanca", Year: 1942, Color: false,
        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
    {Title: "Cool Hand Luke", Year: 1967, Color: true,
        Actors: []string{"Paul Newman"}},
    {Title: "Bullitt", Year: 1968, Color: true,
        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
    // ...
}

这样的数据结构特别适合JSON格式,并且在两种之间相互转换也很容易。
将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用 json.Marshal 函数完成:

1
2
3
4
5
data, err := json.Marshal(movies)
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

Marshal函数返还一个编码后的字节slice,包含很长的字符串,并且没有空白缩进;我们将它折行以便于显示:

1
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingrid Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Actors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"Actors":["Steve McQueen","Jacqueline Bisset"]}]

这种紧凑的表示形式虽然包含了全部的信息,但是很难阅读。
为了生成便于阅读的格式,另一个 json.MarshalIndent 函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

1
2
3
4
5
data, err := json.MarshalIndent(movies, "", "    ")
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

上面的代码将产生这样的输出(在最后一个成员或元素后面并没有逗号分隔符):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[
    {
        "Title": "Casablanca",
        "released": 1942,
        "Actors": [
            "Humphrey Bogart",
            "Ingrid Bergman"
        ]
    },
    {
        "Title": "Cool Hand Luke",
        "released": 1967,
        "color": true,
        "Actors": [
            "Paul Newman"
        ]
    },
    {
        "Title": "Bullitt",
        "released": 1968,
        "color": true,
        "Actors": [
            "Steve McQueen",
            "Jacqueline Bisset"
        ]
    }
]

在编码时,默认使用Go语言结构体的成员名字作为JSON的对象(通过reflect反射技术)。
只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。

你可能已经注意到,其中Year名字的成员在编码后变成了released,还有Color成员编码后变成了小写字母开头的color。
这是因为构体成员Tag所导致的。一个构体成员Tag是和在编译阶段关联到该成员的元信息字符串:

1
2
Year  int  `json:"released"`
Color bool `json:"color,omitempty"`

结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的 key:"value" 键值对序列;因为值中含义双引号字符,因此成员Tag一般用原生字符串面值的形式书写。
json开头键名对应的值用于控制 encoding/json 包的编码和解码的行为,并且 encoding/... 下面其它的包也遵循这个约定。
成员Tag中json对应值的第一部分用于指定JSON对象的名字,比如将Go语言中的 TotalCount 成员对应到 JSON 中的 total_count 对象。
Color成员的Tag还带了一个额外的 omitempty 选项,表示当Go语言结构体成员为空或零值时不生成JSON对象(这里false为零值)。
果然,Casablanca是一个黑白电影,并没有输出Color成员。

编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫 unmarshaling,通过 json.Unmarshal 函数完成。
下面的代码将JSON格式的电影数据解码为一个结构体slice,结构体中只有Title成员。
通过定义合适的Go语言数据结构,我们可以选择性地解码JSON中感兴趣的成员。
当Unmarshal函数调用返回,slice将被只含有Title信息值填充,其它JSON成员将被忽略。

1
2
3
4
5
var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
    log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"

排序

https://www.jiyik.com/tm/xwzj/prolan_5971.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
    "fmt"
    "sort"
)

type Employee struct {
    Name   string
    Salary int
}

func main() {
    // Sort in Ascending order
    employees := []Employee{
        {Name: "John", Salary: 1500},
        {Name: "Joe", Salary: 3000},
        {Name: "Jack", Salary: 3400},
    }

    sort.Slice(employees, func(i, j int) bool {
        return employees[i].Salary < employees[j].Salary
    })

    fmt.Println(employees)

    // Sort in Descending order
    employees1 := []Employee{
        {Name: "John", Salary: 1500},
        {Name: "Joe", Salary: 3000},
        {Name: "Jack", Salary: 3400},
    }

    sort.Slice(employees1, func(i, j int) bool {
        return employees1[i].Salary > employees1[j].Salary
    })

    fmt.Println(employees1)
}