go结构体

228 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

1.结构体(struct)

Go 通过结构体的形式支持用户自定义类型,一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后(方法)可以访问这些数据,就好像是一个独立实体的一部分。结构体是值类型,因此可以通过 new 函数来创建。

组成结构体类型的那些数据称为字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。 结构体定义的一般方式如下:

type identifier struct {
    field1 type1
    field2 type2
    ...
}

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

使用 new

使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。

var t *T
t = new(T)

写这条语句的惯用方法是:t := new(T),变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。

使用点号符可以获取结构体字段的值:structname.fieldname。在 Go 语言中这叫 选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation) 来引用结构体的字段:

type myStruct struct { i int }
var v myStruct    // v是结构体类型变量
var p *myStruct   // p是指向一个结构体类型变量的指针
v.i
p.i
type Interval struct {
    start int
    end   int
}

初始化方式:

intr := Interval{0, 3}            (A)
intr := Interval{end:5, start:1}    (B)
intr := Interval{end:5}           (C)

初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:

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

或者:

var ms struct1
ms = struct1{10, 15.5, "Chris"}

混合字面量语法(composite literal syntax)

&struct1{a, b, c} 是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。表达式 new(Type) 和 &Type{} 是等价的。结构体类型和字段的命名遵循可见性规则,一个导出的结构体类型中有些字段是导出的,

2.结构体特性
  • 结构体的内存布局 Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。
  • 递归结构体 结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节点包含指向临近节点的链接(地址)。如下所示,链表中的 su,树中的 ri 和 le 分别是指向别的节点的指针。
  • 链表 这块的 data 字段用于存放有效数据(比如 float64),su 指针指向后继节点。

Go 代码:

type Node struct {
    data    float64
    su      *Node
}

链表中的第一个元素叫 head,它指向第二个元素;最后一个元素叫 tail,它没有后继元素,所以它的 su 为 nil 值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。 同样地可以定义一个双向链表,它有一个前趋节点 pr 和一个后继节点 su:

type Node struct {
    pr      *Node
    data    float64
    su      *Node
}
  • 二叉树

    二叉树中每个节点最多能链接至两个节点:左节点(le)和右节点(ri),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点(root),底层没有子节点的节点叫叶子节点(leaves),叶子节点的 le 和 ri 指针为 nil 值。在 Go 中可以如下定义二叉树:

type Tree strcut {
    le      *Tree
    data    float64
    ri      *Tree
}
  • 结构体工厂

Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂”方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 new 或 New 开头。假设定义了如下的 File 结构体类型:

type File struct {
    fd      int     // 文件描述符
    name    string  // 文件名
}

下面是这个结构体类型对应的工厂方法,它返回一个指向结构体实例的指针:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    return &File{fd, name}
}

然后这样调用它:

f := NewFile(10, "./test.txt")

在 Go 语言中常常像上面这样在工厂方法里使用初始化来简便的实现构造函数。

如果 File 是一个结构体类型,那么表达式 new(File) 和 &File{} 是等价的。 这可以和大多数面向对象编程语言中笨拙的初始化方式做个比较:File f = new File(…)。 我们可以说是工厂实例化了类型的一个对象,就像在基于类的OO语言中那样。 如果想知道结构体类型T的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})。

  • 如何强制使用工厂方法

通过应用可见性规则参考,就可以禁止使用 new 函数,强制用户使用工厂方法,从而使类型变成私有的,就像在面向对象语言中那样。

type matrix struct {
    ...
}
func NewMatrix(params) *matrix {
    m := new(matrix) // 初始化 m
    return m
}

在包外,只有通过NewMatrix函数才可以初始化matrix 结构。

  • 带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。reflect包可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。

package main
import (
    "fmt"
    "reflect"
)
type TagType struct { // 结构体标签
    field1 bool   "An important answer"
    field2 string "The name of the thing"
    field3 int    "How much there are"
}
func main() {
    tt := TagType{true, "Barak Obama", 1}
    for i := 0; i < 3; i++ {
        refTag(tt, i)
    }
}
func refTag(tt TagType, ix int) {
    ttType := reflect.TypeOf(tt)
    ixField := ttType.Field(ix)
    fmt.Printf("%v\n", ixField.Tag)
}