首页 > 编程笔记

GO语言结构体详解(长篇神文)

Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。Go 语言使用结构体和结构体成员来描述现实的实体和实体所对应的各种属性。

Go 语言中的类型可以被实例化,使用 new 或 & 构造的实例类型是类型的指针。

结构体成员是由一系列成员变量构成的,这些成员变量也被称为“字段”。字段有以下特性:

结构体的定义

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

使用关键字 type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。

结构体定义的语法格式如下:
type 类型名 struct {
    字段1 字段1类型
    字段2 字段2类型
    …
}
解释一下:
另外,如果语法写成 type T struct {a,b int} 也是合法的,它更适用于简单的结构体。

结构体中的字段都有名字,如 field1、field2 等,如果字段在代码中从来不会被用到,那可以命名为 _(空标识符)。

结构体的字段可以是任意类型,包括结构体本身,甚至可以是函数或者接口。声明结构体类型的一个变量,并给它的字段赋值,例如:
var S T
s.a= 5
s.b= 8
数组可以看作一种结构体类型,不过它通常使用下标而不是有名字的字段。

使用结构体可以表示一个包含X和Y整型分量的点结构,例如:
type Point struct {
    X int
    Y int
}
同类型的变量也可以写在一行,颜色的红、绿、蓝3个分量可以使用byte类型表示,定义的颜色结构体如下:
type Color struct {
    R, G, B byte
}
定义一个新的结构体:
package main
import "fmt"
type Books struct {
   title   string
   author  string
   subject string
   book_id int
}
func main() {
   //创建一个新的结构体
   fmt.Println(Books{"Go 语言", "张老师", "Go 语言教程", 01})
   //也可以使用 key => value 格式
   fmt.Println(Books{title: "Java", author: "王老师", subject: "Java教程", book_id: 02})
   //忽略的字段为 0 或 空
   fmt.Println(Books{title: "C语言", author: "李老师"})
}
运行结果为:

{Go 语言 张老师 Go 语言教程 1} 
{Java 王老师 Java教程 2} 
{C语言 李老师  0}

结构体的创建

结构体的定义是一种对内存布局的描述,只有当结构体真正被创建后,才会真正地分配内存,因此,必须在定义并创建结构体后才能使用结构体的字段。

Go语言可以通过多种方式来创建结构体,根据实际需要可以选用不同的写法。

1、基本的创建形式

结构体本身是一种类型,可以像整型、字符串等类型一样,以var的方式声明结构体即可完成创建。

创建结构体的语法格式如下:
var ins T

用结构体表示的点结构(Point)的实例化过程如下:
type Point struct {
    X int
    Y int
}
var p Point
p.X = 10
p.Y = 20
在以上代码中,使用“.”来访问结构体的成员变量,如 p.X 和 p.Y 等,结构体成员变量的赋值方法与普通变量一致。

2、使用new()函数创建指针类型的结构体

Go 语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

使用 new() 函数创建结构体的语法格式如下:
ins := new(T)

同样,使用 new() 函数创建指针类型的结构体时,也可以使用“.”来访问结构体指针的成员。

例如,定义一个名为 myStruct 的结构体,使用 new() 函数实例化结构体后,对成员变量进行赋值,代码如下:
package main
import "fmt"
type myStruct struct {
   i1  int
   f1  float32
   str string
}
func main() {
   ms := new(myStruct)
   ms.i1 = 10
   ms.f1 = 15.5
   ms.str = "Google"
   fmt.Printf("int: %d\n", ms.i1)
   fmt.Printf("float: %f\n", ms.f1)
   fmt.Printf("string: %s\n", ms.str)
   fmt.Println(ms)
}
运行结果为:

int: 10
float: 15.500000
string: Google
&{10 15.5 Google}

3、结构体地址的实例化操作

在 Go 语言中,对结构体进行“&”取地址操作时,视为对该类型进行一次 new 的实例化操作。取地址格式如下:
ins := &T{}

例如,使用结构体定义一个命令行指令(Command),指令中包含名称、变量关联和注释等,对 Command 进行指针地址的实例化,并完成赋值过程,代码如下:
type Command struct {
    Name    string        //指令名称
    Var     *int          //指令绑定的变量
    Comment string        //指令的注释
}
var version int = 1
cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"
在以上代码中:
取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程,例如:
func newCommand(name string, varref *int, comment string) *Command {
    return &Command{
        Name:    name,
        Var:     varref,
        Comment: comment,
    }
}
cmd = newCommand(
    "version",
    &version,
    "show version",
)

结构体的使用

1、递归结构体

结构体类型可以通过引用自身来定义,因此,可以用来定义链表或二叉树的元素,此时节点包含指向邻近节点的链接(地址)。

例如,data 字段用于存放有效数据,su 指针指向后续节点。
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),叶子节点的左和右指针为nil值。在 Go 语言中可以如下定义二叉树:
type Tree struct{
    le *Tree
    data float64
    ri *Tree
}

2、结构体的转换

当给结构体定义一个别名(alias)类型时,该结构体类型与别名(alias)类型的底层类型都是一样的,可以直接转换,不过需要注意其中由非法赋值或转换引起的编译错误,例如:
package main
import "fmt"
type number struct {
    f float32
}
type nr number //类型别名
func main() {
    a := number{5.0}
    b := nr{5.0}
    //var i float32 = b
    //编译错误返回:
    //compile-error:cannot use b (type nr) as type float32 in assigment
    //var i float32 (b)
    //编译错误返回:
    //compile-error:cannot convert b (type nr) to type float32
    //var c number =b
    //编译错误返回:
    //compile-error:cannot use b (type nr) as type number in assigment
    //此处需要转换
    var c = number(b)
    fmt.Println(a, b, c)
}
运行结果如下:

{5} {5} {5}

3、结构体参数的传输

结构体类型可以像其他数据类型一样作为参数传递给函数,并以访问成员变量的方式访问结构体变量,有形式参数传输和指针参数传输两种方式,例如:
package main
import "fmt"
type Employee struct {
    ID      int
    Name    string
    Address string
    Phone   string
}
func main() {
    var employee Employee
    employee.ID = 10001
    employee.Name = "Lisa"
    employee.Address = "***"
    employee.Phone = "1234556"
    fmt.Printf("形式传参之前,employee ID : %d\n", employee.ID)
    operateEmployee1(employee)
    fmt.Printf("形式传参之后,employee ID : %d\n", employee.ID)
    fmt.Printf("指针传参之前,employee ID : %d\n", employee.ID)
    operateEmployee2(&employee)
    fmt.Printf("指针传参之后,employee ID : %d\n", employee.ID)
}
//形式传参
func operateEmployee1(employee Employee) {
    employee.ID = 10010
}
//指针传参
func operateEmployee2(employee *Employee) {
    employee.ID = 10010
}
运行结果是:

形式传参之前,employee ID : 10001 
形式传参之后,employee ID : 10001 
指针传参之前,employee ID : 10001 
指针传参之后,employee ID : 10010

从运行结果中可以看出,形式参数中 employee 只传递了一个副本到另一个函数中,函数中操作的是副本,对 employee 没有任何影响;而在指针参数中 employee 传递的是地址,函数中的操作会影响 employee。

成员变量的初始化

结构体在实例化时可以直接对成员变量进行初始化,初始化有两种形式,分别是以字段“键值对”形式和多个值的列表形式。键值对形式的初始化适合选择性填充字段较多的结构体,多个值的列表形式适合填充字段较少的结构体。

1、使用“键值对”初始化结构体

结构体可以使用“键值对”初始化字段,每个“键”(Key)对应结构体中的一个字段,键的“值”(Value)对应字段需要初始化的值。

键值对的填充是可选的,不需要初始化的字段可以不写入初始化列表中。

结构体实例化后字段的默认值是字段类型的默认值,例如,数值为 0、字符串为""(空字符串)、布尔为 false、指针为 nil 等。

1) 键值对初始化的语法格式如下:
ins := 结构体类型名{
    字段1: 字段1的值,
    字段2: 字段2的值,
    …
}

键值之间以“:”分隔,键值对之间以“,”分隔。

2) 使用键值对填充结构体,例如:
type People struct {
    name  string
    child *People
}
relation := &People{
    name: "爷爷",
    child: &People{
        name: "爸爸",
        child: &People{
                name: "我",
        },
    },
}
在以上代码中:

2、使用多个值列表初始化结构体

Go 语言可以在“键值对”初始化的基础上忽略“键”,也就是说,可以使用多个值的列表初始化结构体的字段。

1) 多个值使用逗号分隔初始化结构体,例如:
ins := 结构体类型名{
    字段1的值,
    字段2的值,
    …
}
使用这种格式初始化时,需要注意:
2) 多个值列表初始化结构体,例如:
package main
import "fmt"
func main() {
   type Address struct {
       Province    string
       City        string
       ZipCode     int
       PhoneNumber string
   }
   addr := Address{
       "河南",
       "郑州",
       410000,
       "123",
   }
   fmt.Println(addr)
}
运行结果如下:

{河南 郑州 410000 123}

匿名字段和内嵌结构体

结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必需的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。

1、匿名字段

匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。

1) 匿名结构体定义格式

匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成,结构体定义时没有结构体类型名,只有字段和类型定义,键值对初始化部分由可选的多个键值对组成,格式如下:
ins := struct {
    //匿名结构体字段定义
    字段1 字段类型1
    字段2 字段类型2
    …
}{
    //字段值初始化
    初始化字段1: 字段1的值,
    初始化字段2: 字段2的值,
    …
}
说明:
键值对初始化部分是可选的,不初始化成员时,匿名结构体的格式变为:
ins := struct {
    字段1 字段类型1
    字段2 字段类型2
    …
}

2) 使用匿名结构体

使用匿名结构体的方式定义和初始化一个消息结构,这个消息结构具有消息标示部分(ID)和数据部分(data),打印消息内容的 printMsg() 函数在接收匿名结构体时需要在参数上重新定义匿名结构体,代码如下:
package main
import (
    "fmt"
)
//打印消息类型, 传入匿名结构体
func printMsgType(msg *struct {
    id   int
    data string
}) {
    //使用动词%T打印msg的类型
    fmt.Printf("%T\n", msg)
}
func main() {
    //实例化一个匿名结构体
    msg := &struct {  //定义部分
        id   int
        data string
    }{  //值初始化部分
        1024,
        "hello",
    }
    printMsgType(msg)
}
运行结果如下:

*struct { id int; data string }

在以上代码中:
匿名结构体的类型名是结构体包含字段成员的详细描述,匿名结构体在使用时需要重新定义,造成大量重复的代码,因此开发中较少使用。

2、内嵌结构体

结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用。外层结构体通过outer.in1直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现,例如:
package main
import "fmt"
type A struct {
    ax, ay int
}
type B struct {
    A
    bx, by float32
}
func main() {
    b := B{A{1, 2}, 3.0, 4.0}
    fmt.Println(b.ax, b.ay, b.bx, b.by)
    fmt.Println(b.A)
}
运行结果如下:

1 2 3 4
{1 2}

内嵌结构体的特点如下:

1) 内嵌的结构体可以直接访问其成员变量

嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过层层的结构体字段访问到最终的字段。

例如,ins.a.b.c 的访问可以简化为 ins.c。

2) 内嵌结构体的字段名是它的类型名

内嵌结构体字段仍然可以使用详细的字段进行层层访问,内嵌结构体的字段名就是它的类型名,代码如下:
var c Color
c.BasicColor.R = 1
c.BasicColor.G = 1
c.BasicColor.B = 0
一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。

推荐阅读