Go语言基础之结构体
Go语言基础之结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。 Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
1. 类型别名和自定义类型
1.1 自定义类型
无等号-定义自定义类型
在 Go 语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型, Go 语言中可以使用type关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过 struct 定义。例如:
//将MyInt定义为int类型 |
通过type
关键字的定义,MyInt
就是一种新的类型,它具有int
的特性。
1.2 类型别名
有等号-类型仍是Type,只是加了个别名。
类型别名是Go1.9
版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type |
我们之前见过的rune
和byte
就是类型别名,他们的定义如下:
// 源码中设置的别名byte和rune |
1.3 类型定义和类型别名的区别
类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
//类型定义 |
结果显示a的类型是main.NewInt
,表示main包下定义的NewInt
类型。b的类型是int
。MyInt
类型只会在代码中存在,编译完成时并不会有MyInt
类型。
2. 结构体(struct)
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct
。 也就是我们可以通过struct
来定义自己的类型了。
Go语言中通过struct
来实现面向对象。
常规面向对象语言(java,c#)使用类来实现属性和方法的聚合以及继承的概念。
Go语言不是一种 “传统” 的面向对象编程语言,它没有类和继承的概念。Go语言通过结构体来实现上述功能。
结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
2.1 结构体的定义
使用type
和struct
关键字来定义结构体,具体代码格式如下:
type 类型名 struct { |
其中:
类型名:标识自定义结构体的名称,在同一个包内不能重复。
字段名:表示结构体字段名。结构体中的字段名必须唯一。
字段类型:表示结构体字段的具体类型。
举个例子,我们定义一个Person
(人)结构体,代码如下:
type person struct { |
同样类型的字段也可以写在一行:
type person1 struct { |
这样我们就拥有了一个person
的自定义类型,它有name
、city
、age
三个字段,分别表示姓名、城市和年龄。这样我们使用这个person
结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型。
2.2 结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var
关键字声明结构体类型。
var 结构体实例 结构体类型 |
- 结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。
- Go语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。
基本实例化形式
举个例子:
type person struct { |
我们通过.
来访问结构体的字段(成员变量),例如p1.name
和p1.age
等。
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
package main |
创建指针类型结构体
关键字new(返回基本数据类型的指针)
我们还可以通过使用new
关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
var p2 = new(person) |
从打印的结果中我们可以看出p2
是一个结构体指针。
需要注意的是在Go语言中支持对结构体指针直接使用.
来访问结构体的成员。
var p2 = new(person) |
取结构体的地址实例化
关键字&(
&person{}
)
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作。
p3 := &person{} |
p3.name = "七米"
其实在底层是(*p3).name = "七米"
,这是Go语言帮我们实现的语法糖。
2.3 结构体初始化
初始化的结构体,其成员变量都是对应其类型的零值。
type person struct { |
普通结构体
普通结构体在实例化时可以直接对成员变量进行初始化,初始化有两种形式且不能混用:
- 字段“键值对”形式:适合选择性填充字段较多的结构体。
- 多个值的列表形式:适合填充字段较少的结构体。
1. 字段“键值对”形式
- 键值对的填充是可选的,不需要初始化的字段可以不填入到初始化列表中。这些字段的默认值是字段类型的默认值,例如 ,数值为 0、字符串为 “”(空字符串)、布尔为 false、指针为 nil 等。
- 键值之间以
:
分隔,键值对之间以,
分隔。
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
p5 := person{ |
也可以对结构体指针进行键值对初始化(常用),例如:
p6 := &person{ |
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
p7 := &person{ |
2. 多个值的列表形式
类似终端输入
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p8 := &person{ |
使用这种格式初始化时,需要注意:
- 必须初始化结构体的
所有字段
。 - 初始值的填充顺序必须与字段在结构体中的声明
顺序一致
。 该方式不能和键值初始化方式混用。
匿名结构体
匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。
匿名结构体在使用时需要重新定义,造成大量重复的代码,因此开发中较少使用。
匿名结构体的初始化写法由两部分组成:
- 结构体的定义:没有结构体名,只有字段和类型定义。
- 键值对初始化:由可选的多个键值对组成。该部分是可选的。
1. 匿名结构体的定义和初始化
ins := struct { |
2.匿名结构体不初始化成员时
ins := struct { |
2.4 结构体内存布局
结构体占用一块连续的内存。
type test struct { |
输出:
n.a 0xc0000a0060 |
进阶知识点】关于Go语言中的内存对齐推荐阅读:在 Go 中恰到好处的内存对齐
2.5 空结构体
空结构体是不占用空间的。
var v struct{} |
3. 结构体的其他知识点
3.1 模拟构造函数实现
约定成俗以new开头
其他编程语言构造函数的一些常见功能及特性如下:
- 每个类可以添加构造函数,多个构造函数使用函数重载实现。
- 构造函数一般与类名同名,且没有返回值。
- 构造函数有一个静态构造函数,一般用这个特性来调用父类的构造函数。
- 对于 C++ 来说,还有默认构造函数、拷贝构造函数等。
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person
的构造函数。 因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
type person struct { |
调用构造函数:
p1 := newPerson1("hao", "wuhan", 23) |
- Go语言的结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程将参数使用函数传递到结构体构造参数中即可完成构造函数的任务。
- 我们通常通过
New?
或New?By?
来模式构造方法,实现重载。
多种方式创建和初始化结构体
模拟构造函数重载
type Cat struct { |
带有父子关系的结构体的构造和初始化
模拟父级构造调用
type Cat struct { |
3.2 方法和接收者
常用:相当于给
接收者
【接收者的类型可以是任何类型,不仅仅是结构体】添加方法!!!
介绍
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
。
在Go语言中,结构体就像是类的一种简化形式,那么类的方法在哪里呢?Go语言中类方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。一个类型加上它的方法等价于面向对象中的一个类。
在Go语言中,绑定在接收器上的方法可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的。
一个接收器类型上的所有方法的集合叫做该接收器的方法集。
接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。
- 接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误
invalid receiver type…
。 - 接收器不能是一个指针类型,但是它可以是任何其他允许类型的指针。
- 接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误
方法的定义格式如下:
// 在关键字和方法名之间添加了`(接收者变量 接收者类型)` |
其中,
接收者变量:接收者中的参数变量名在命名时,官方建议常用:使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。接收者类型:接收者类型和参数类似,可以是
指针类型和非指针类型
。 两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中:- 指针类型 :大对象因为复制性能较低,在接收器和参数间传递时不进行复制,只是传递指针。
- 非指针类型:小对象由于值复制时的速度较快,所以适合使用非指针接收器。
方法名、参数列表、返回参数:具体格式与函数定义相同。
注意:方法仅支持该结构体构造函数的实例去调用且接收者变量可以代表实例在方法内部使用。
举个例子:
//Person 结构体 |
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
指针类型的接收者
可以修改实例
- 指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。
- 由于指针的特性,调用方法时,修改接收器指针的任意成员变量,都是有效的。
例如我们为Person
添加一个SetAge
方法,来修改实例变量的年龄。
// SetAge 设置p的年龄 |
调用该方法:
func main() { |
值类型的接收者
修改的知识实例的副本,无法影响实例
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
// SetAge2 设置p的年龄 |
什么时候应该使用指针类型接收者?
需要修改接收者中的值
接收者是拷贝代价比较大的大对象
保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
3.3 任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
//MyInt 将int定义为自定义MyInt类型 |
注意事项:
非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
扩展出不能给内置或其他包的类型添加方法,如上述例子给MyInt添加方法实现给int添加方法。
3.4 结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
//Person 结构体Person类型 |
注意:
这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
3.5 嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针,就像下面的示例代码那样。
//Address 地址结构体 |
介绍
结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时内嵌结构体的字段名为类型名。匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。
可以粗略地将这个和面向对象语言中的继承概念相比较,Go语言的结构体内嵌特性就是一种组合特性,使用组合特性可以快速构建对象的不同特性,从而实现类似继承功能。
一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。
嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如,ins.a.b.c的访问可以简化为ins.c。内嵌结构体字段仍然可以使用详细的字段进行一层层访问,
package main |
嵌套匿名字段
上面user结构体中嵌套的Address
结构体也可以采用匿名字段的方式,例如:
//Address 地址结构体 |
当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
在使用内嵌结构体时,Go语言的编译器会非常智能地提醒我们可能发生的歧义和错误。
上述例子中可以省略匿名字段,但是两个匿名字段中存在相同字段名则不能省略。
//Address 地址结构体 |
当使用如上错误方式时,编译阶段会报错:.\main.go:13:3: ambiguous selector user3.CreateTime
3.6 结构体的“继承”
Go语言中没有继承的概念,但是使用结构体也可以实现其他编程语言中面向对象的继承。
通过嵌套匿名结构体模拟实现继承:Dog中没有的属性和方法,会去嵌套的匿名结构体中去找。
比如集成gin框架的路由引擎,再在其上扩展新的路由。
//Animal 动物 |
3.7 结构体字段的可见性
结构体中字段大写
开头表示可公开访问
,小写表示私有
(仅在定义当前结构体的包中可访问)。
3.8 结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""
包裹,使用冒号:
分隔,然后紧接着值;多个键值之间使用英文,
分隔。
通过包 “encoding/json”的 data, err := json.Marshal(c)
和 err = json.Unmarshal([]byte(str), c1)
方法实现序列化和反序列化。原理是字符串类型是由切片类型组成的,两者可以互相转换。序列化时返回的是切片,通过string(切片)转成字符串;反序列化的第二个参数是指针,若不是指针无法存储,因为修改的是副本。
//Student 学生 |
3.9 结构体标签(Tag)
使用场景
在3.7 结构体字段的可见性
中知道第三方包想要使用当前包的值,必须首字母是大写状态。所以使用”encoding/json”格式化时,内部所有字段名必须首字母大写,但是序列化的字符串的字段不是想要的,可以通过Tag指定想要的格式。
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"` |
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注意事项:
为结构体编写Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student
结构体的每个字段定义json序列化时使用的Tag:
//Student 学生 |
3.10 结构体和方法补充知识点
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。我们来看下面的例子:
type Person struct { |
正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。
func (p *Person) SetDreams(dreams []string) { |
同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。
基本数据类型的函数参数是操作副本;而引用数据类型在函数参数和返回值的时候是指针,因此需要copy副本。
参考感谢
Go语言基础之结构体 · 语雀 (yuque.com)
06.结构体 · 语雀 (yuque.com)
struct 结构体 · 语雀 (yuque.com)