Go基础系列:struct和嵌套struct

原文作者:骏马金龙 来源:博客园

struct

struct定义结构,结构由字段(field)组成,每个field都有所属数据类型,在一个struct中,每个字段名都必须唯一。

说白了就是拿来存储数据的,只不过可自定义化的程度很高,用法很灵活,Go中不少功能依赖于结构,就这样一个角色。

Go中不支持面向对象,面向对象中描述事物的类的重担由struct来挑。比如面向对象中的继承,可以使用组合(composite)来实现:struct中嵌套一个(或多个)类型。面向对象中父类与子类、类与对象的关系是is a的关系,例如Horse is a Animal,Go中的组合则是外部struct与内部struct的关系、struct实例与struct的关系,它们是has a的关系。Go中通过struct的composite,可以"模仿"很多面向对象中的行为,它们很"像"。

定义struct

定义struct的格式如下:

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

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

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

每个字段都有类型,可以是任意类型,包括内置简单数据类型、其它自定义的struct类型、当前struct类型本身、接口、函数、channel等等。

如果某几个字段类型相同,可以缩写在同一行:

1type mytype struct {
2    a,b int
3    c string}

构造struct实例

定义了struct,就表示定义了一个数据结构,或者说数据类型,也或者说定义了一个类。总而言之,定义了struct,就具备了成员属性,就可以作为一个抽象的模板,可以根据这个抽象模板生成具体的实例,也就是所谓的"对象"。

例如:

1type person struct{
2    name string
3    age int}// 初始化一个person实例var p person

这里的p就是一个具体的person实例,它根据抽象的模板person构造而出,具有具体的属性name和age的值,虽然初始化时它的各个字段都是0值。换句话说,p是一个具体的人。

struct初始化时,会做默认的赋0初始化,会给它的每个字段根据它们的数据类型赋予对应的0值。例如int类型是数值0,string类型是"",引用类型是nil等。

因为p已经是初始化person之后的实例了,它已经具备了实实在在存在的属性(即字段),所以可以直接访问它的各个属性。这里通过访问属性的方式p.FIELD为各个字段进行赋值。

1// 为person实例的属性赋值,定义具体的personp.name = "longshuai"p.age = 23

获取某个属性的值:

1fmt.Println(p.name) // 输出"longshuai"

也可以直接赋值定义struct的属性来生成struct的实例,它会根据值推断出p的类型。

1var p = person{name:"longshuai",age:23}p := person{name:"longshuai",age:23}// 不给定名称赋值,必须按字段顺序p := person{"longshuai",23}p := person{age:23}
2p.name = "longshuai"

如果struct的属性分行赋值,则必须不能省略每个字段后面的逗号",",否则就会报错。这为未来移除、添加属性都带来方便:

1p := person{
2    name:"longshuai",
3    age:23,     // 这个逗号不能省略}

除此之外,还可以使用new()函数或&TYPE{}的方式来构造struct实例,它会为struct分配内存,为各个字段做好默认的赋0初始化。它们是等价的,都返回数据对象的指针给变量,实际上&TYPE{}的底层会调用new()。

1p := new(person)
2p := &person{}// 生成对象后,为属性赋值p.name = "longshuai"p.age = 23

使用&TYPE{}的方式也可以初始化赋值,但new()不行:

1p := &person{
2    name:"longshuai",
3    age:23,
4}

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

struct的值和指针

下面三种方式都可以构造person struct的实例p:

1p1 := person{}
2p2 := &person{}
3p3 := new(person)

但p1和p2、p3是不一样的,输出一下就知道了:

 1package mainimport (    "fmt")type person struct {
 2    name string
 3    age  int}func main() {
 4    p1 := person{}
 5    p2 := &person{}
 6    p3 := new(person)
 7    fmt.Println(p1)
 8    fmt.Println(p2)
 9    fmt.Println(p3)
10}

结果:

1{ 0}
2&{ 0}
3&{ 0}

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

1 变量名      指针     数据对象(实例)
2-------------------------------
3p1(addr) -------------> { 0}
4p2 -----> ptr(addr) --> { 0}
5p3 -----> ptr(addr) --> { 0}

所以p1和ptr(addr)保存的都是数据对象的地址,p2和p3则保存ptr(addr)的地址。通常,将指向指针的变量(p1、p2)直接称为指针,将直接指向数据对象的变量(p1)称为对象本身,因为指向数据对象的内容就是数据对象的地址,其中ptr(addr)和p1保存的都是实例对象的地址。

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

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

1p4 -> ptr(nil)

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

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

上面的代码将输出:

1&{longshuai 23}

传值 or 传指针

Go函数给参数传递值的时候是以复制的方式进行的。

复制传值时,如果函数的参数是一个struct对象,将直接复制整个数据结构的副本传递给函数,这有两个问题:

  • 函数内部无法修改传递给函数的原始数据结构,它修改的只是原始数据结构拷贝后的副本
  • 如果传递的原始数据结构很大,完整地复制出一个副本开销并不小

所以,如果条件允许,应当给需要struct实例作为参数的函数传struct的指针。例如:

1func add(p *person){...}

既然要传指针,那struct的指针何来?自然是通过&符号来获取。分两种情况,创建成功和尚未创建的实例。

对于已经创建成功的struct实例p,如果这个实例是一个值而非指针(即p->{person_fields}),那么可以&p来获取这个已存在的实例的指针,然后传递给函数,如add(&p)

对于尚未创建的struct实例,可以使用&person{}或者new(person)的方式直接生成实例的指针p,虽然是指针,但Go能自动解析成实例对象。然后将这个指针p传递给函数即可。如:

1p1 := new(person)
2p2 := &person{}add(p1)add(p2)

struct field的tag属性

在struct中,field除了名称和数据类型,还可以有一个tag属性。tag属性用于"注释"各个字段,除了reflect包,正常的程序中都无法使用这个tag属性。

1type TagType struct { // tags
2    field1 bool   "An important answer"
3    field2 string "The name of the thing"
4    field3 int    "How much there are"}

匿名字段和struct嵌套

struct中的字段可以不用给名称,这时称为匿名字段。匿名字段的名称强制和类型相同。例如:

1type animal struct {
2    name string
3    age int}type Horse struct{    int
4    animal
5    sound string}

上面的Horse中有两个匿名字段intanimal,它的名称和类型都是int和animal。等价于:

1type Horse struct{    int int
2    animal animal
3    sound string}

显然,上面Horse中嵌套了其它的struct(如animal)。其中animal称为内部struct,Horse称为外部struct。

以下是一个嵌套struct的简单示例:

 1package mainimport (    "fmt")type inner struct {
 2    in1 int
 3    in2 int}type outer struct {
 4    ou1 int
 5    ou2 int
 6    int
 7    inner
 8}func main() {
 9    o := new(outer)
10    o.ou1 = 1
11    o.ou2 = 2
12    o.int = 3
13    o.in1 = 4
14    o.in2 = 5
15    fmt.Println(o.ou1)  // 1
16    fmt.Println(o.ou2)  // 2
17    fmt.Println(o.int)  // 3
18    fmt.Println(o.in1)  // 4
19    fmt.Println(o.in2)  // 5}

上面的o是outer struct的实例,但o除了具有自己的显式字段ou1和ou2,还具备int字段和inner字段,它们都是嵌套字段。一被嵌套,内部struct的属性也将被外部struct获取,所以o.into.in1o.in2都属于o。也就是说,外部struct has a 内部struct,或者称为struct has a field

输出以下外部struct的内容就很清晰了:

1fmt.Println(o)  // 结果:&{1 2 3 {4 5}}

上面的outer实例,也可以直接赋值构建:

1o := outer{1,2,3,inner{4,5}}

在赋值inner中的in1和in2时不能少了inner{},否则会认为in1、in2是直接属于outer,而非嵌套属于outer。

显然,struct的嵌套类似于面向对象的继承。只不过继承的关系模式是"子类 is a 父类",例如"轿车是一种汽车",而嵌套struct的关系模式是外部struct has a 内部struct,正如上面示例中outer拥有inner。而且,从上面的示例中可以看出,Go是支持"多重继承"的。

嵌套struct的名称冲突问题

假如外部struct中的字段名和内部struct的字段名相同,会如何?

有以下两个名称冲突的规则:

  1. 外部struct覆盖内部struct的同名字段、同名方法
  2. 同级别的struct出现同名字段、方法将报错

第一个规则使得Go struct能够实现面向对象中的重写(override),而且可以重写字段、重写方法。

第二个规则使得同名属性不会出现歧义。例如:

 1type A struct {
 2    a int
 3    b int}type B struct {
 4    b float32
 5    c string
 6    d string}type C struct {
 7    A
 8    B
 9    a string
10    c string}var c C

按照规则(1),直属于C的a和c会分别覆盖A.a和B.c。可以直接使用c.a、c.c分别访问直属于C中的a、c字段,使用c.d或c.B.d都访问属于嵌套的B.d字段。如果想要访问内部struct中被覆盖的属性,可以c.A.a的方式访问。

按照规则(2),A和B在C中是同级别的嵌套结构,所以A.b和B.b是冲突的,将会报错,因为当调用c.b的时候不知道调用的是c.A.b还是c.B.b。

递归struct:嵌套自身

如果struct中嵌套的struct类型是自己的指针类型,可以用来生成特殊的数据结构:链表或二叉树(双端链表)。

例如,定义一个单链表数据结构,每个Node都指向下一个Node,最后一个Node指向空。

1type Node struct {    data string
2    ri   *Node}

以下是链表结构示意图:

1 ------|----         ------|----         ------|-----
2| data | ri |  -->  | data | ri |  -->  | data | nil |
3 ------|----         ------|----         ------|----- 
4

如果给嵌套两个自己的指针,每个结构都有一个左指针和一个右指针,分别指向它的左边节点和右边节点,就形成了二叉树或双端链表数据结构

二叉树的左右节点可以留空,可随时向其中加入某一边加入新节点(像节点加入到树中)。添加节点时,节点与节点之间的关系是父子关系。添加完成后,节点与节点之间的关系是父子关系或兄弟关系。

双端链表有所不同,添加新节点时必须让某节点的左节点和另一个节点的右节点关联。例如目前已有的链表节点A <-> C,现在要将B节点加入到A和C的中间,即A<->B<->C,那么A的右节点必须设置为B,B的左节点必须设置为A,B的右节点必须设置为C,C的左节点必须设置为B。也就是涉及了4次原子性操作,它们要么全设置成功,失败一个则链表被破坏。

例如,定义一个二叉树:

1type Tree struct {    le   *Tree
2    data string
3    ri   *Tree}

最初生成二叉树时,root节点没有任何指向。

1// root节点:初始左右两端为空root := new(Tree)
2root.data = "root node"

随着节点增加,root节点开始指向其它左节点、右节点,这些节点还可以继续指向其它节点。向二叉树中添加节点的时候,只需将新生成的节点赋值给它前一个节点的le或ri字段即可。例如:

1// 生成两个新节点:初始为空newLeft := new(Tree)
2newLeft.data = "left node"newRight := &Tree{nil, "Right node", nil}// 添加到树中root.le = newLeft
3root.ri = newRight// 再添加一个新节点到newLeft节点的右节点anotherNode := &Tree{nil, "another Node", nil}
4newLeft.ri = anotherNode

简单输出这个树中的节点:

1fmt.Println(root)fmt.Println(newLeft)fmt.Println(newRight)

输出结果:

1&{0xc042062400 root node 0xc042062420}
2&{<nil> left node 0xc042062440}
3&{<nil> Right node <nil>}

当然,使用二叉树的时候,必须为二叉树结构设置相关的方法,例如添加节点、设置数据、删除节点等等。

另外需要注意的是,一定不要将某个新节点的左、右同时设置为树中已存在的节点,因为这样会让树结构封闭起来,这会破坏了二叉树的结构。


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2018-11-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Pythonista

Go语言基础

Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。如以下 GO 语句由 6 个标记组成:

10430
来自专栏IT探索

c/c++程序员快速入门python

print arr.remove('a') #按值删除元素,并返回该元素的值。注意:只删除第一次匹配的元素

11740
来自专栏飞雪无情的博客

Go语言实战笔记(七)| Go 类型

Go 语言是一种静态类型的编程语言,所以在编译器进行编译的时候,就要知道每个值的类型,这样编译器就知道要为这个值分配多少内存,并且知道这段分配的内存表示什么。

9130
来自专栏容器云生态

awk-grep-sed简单使用总结(正则表达式的应用)

正则表达式: 匹配一组字符: #[ns]a.\.xls  //[]用于限定字符;“.”用于匹配任意字符; \.用于转义"." 匹配到s/na*.xls  [n...

29090
来自专栏谈补锅

正则限制输入为数字,且最多输入2位小数 之 新写法

  原本小程序需要一个限制文本框输入为数字,且最多保留2位小数的效果,网上找到的例子感觉有点繁琐,就自己写了一个。

66920
来自专栏程序员互动联盟

【答疑释惑第三十五讲】c语言数组指针问题

疑惑一 数组名a与a[0] ? 这个问题其实是非常基础的,a代表的是一个数组,而a[0]只是数组的一个元素。数组a是一个常量,在某些时候可以转化为指针的功能(但...

30960
来自专栏Jackson0714

IL指令速查

36770
来自专栏编程

python中的变量

变量与数据类型 变量 编程语言中为了能够更好的处理数据,都需要使用一些变量。Python 语言的变量可以是各种不同的数据类型,使用变量的时候不需要声明直接使用就...

22900
来自专栏Golang语言社区

Go语言的复合数据类型

Go语言的复合数据类型是基础数据类型的组合,主要包括四个数组,切片(slice),map和结构体。 数组和结构体的大小是固定大小的,数组的元素类型是固定的,结...

28650
来自专栏Python小屋

小议Python列表和元组中的元素地址连续性

众所周知,在Python中字典和集合依赖元素哈希表来存储,并不存在传统意义上的所谓元素“顺序”,当然,如果需要一个有序的字典可以使用collections模块提...

366100

扫码关注云+社区

领取腾讯云代金券