语雀同文笔记:14.Go 结构体 · 语雀
结构体也属于复合类型,通常使用一个带属性的结构体来表示现实中的实体。
结构体的目的就是把数据聚集在一起,以便能够更加便捷地操作这些数据,在外部来看就像是一个实体。但结构体依旧是值类型,因此可以通过new()函数来创建。
Go语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。Go语言使用结构体和结构体成员来描述现实的实体和实体所对应的各种属性。
Go语言中的类型可以被实例化,使用new或&构造的实例类型是类型的指针。
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
使用关键字type可以将各种基本类型定义为自定义类型。结构体是一种复合的基本类型,通过type定义为自定义类型后,使结构体更便于使用。
结构体的语法格式:
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
…
}
使用结构体可以表示一个包含X和Y整型分量的点结构:
type Point struct {
X int
Y int
}
同类型的变量也可以写在一行,颜色的红、绿、蓝3个分量可以使用byte类型表示:
type Color struct {
R, G, B byte
}
自定义一个结构体:
type Books struct {
bookId int
title string
author string
subject string
}
func main() {
// 创建一个新的结构体
fmt.Println(Books{01, "Go语言", "张老师", "Go 语言教程"})
// 使用键值对形式
fmt.Println(Books{bookId: 02, author: "王老师", subject: "Java教程", title: "Java"})
// 忽略的字段为0或kong
fmt.Println(Books{title: "C语言", author: "李老师"})
}
结构体的定义是一种对内存布局的描述,只有当结构体真正被创建后,才会真正地分配内存,因此,必须在定义并创建结构体后才能使用结构体的字段。Go语言可以通过多种方式来创建结构体,根据实际需要可以选用不同的写法。
结构体本身是一种类型,可以像整型、字符串等类型一样,以var的方式声明结构体即可完成创建。
语法格式:var ins T
用结构体表示的点结构(Point)的实例化过程如下:
type Point struct {
X int
Y int
}
func main() {
// 声明结构体变量并赋值
var point1 Point
point1.X = 100
point1.Y = 100
// 声明结构体变量时初始化
point2 := Point{200, 200}
fmt.Println(point1)
fmt.Println(point2)
}
Go语言中,还可以使用new关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。
new()函数创建结构体的语法格式:ins := new(T)
操作错误???
在Go语言中,对结构体进行“&”取地址操作时,视为对该类型进行一次new的实例化操作。
取地址语法格式:ins := &T{}
// 使用结构体定义一个命令行指令(Command),指令中包含名称、变量关联和注释等,
// 对Command进行指针地址的实例化,并完成赋值过程
type Command struct {
Name string // 指令名称
Var *int // 指令绑定的变量
Comment string // 指令的注释
}
func main() {
var version int = 1
cmd := &Command{"version", &version, "show version"}
//cmd := &Command{}
//cmd.Name = "version"
//cmd.Var = &version
//cmd.Comment = "show version"
fmt.Println(cmd.Name, *cmd.Var, cmd.Comment)
}
取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程:
type Command struct {
Name string // 指令名称
Var *int // 指令绑定的变量
Comment string // 指令的注释
}
func newCommand(name string, varRef *int, comment string) *Command {
return &Command{
Name: name,
Var: varRef,
Comment: comment,
}
}
func main() {
var version int = 1
cmd := newCommand("version", &version, "show version")
fmt.Println(cmd.Name, *cmd.Var, cmd.Comment)
}
结构体类型可以通过引用自身来定义,因此,可以用来定义链表或二叉树的元素,此时节点包含指向邻近节点的链接(地址)。
例如,data字段用于存放有效数据,su指针指向后续节点。
type Node struct{
data float64
su *Node
}
同样还可以定义一个双向链表,它有一个前驱节点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
}
当给结构体定义一个别名(alias)类型时,该结构体类型与别名(alias)类型的底层类型都是一样的,可以直接转换,不过需要注意其中由非法赋值或转换引起的编译错误,例如:
type number struct {
f float32
}
// 定义类型别名
type nr number
func main() {
a := number{5.0}
b := nr{5.0}
//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 float 32
//var i float32 (b)
//compile-error:cannot use b (type nr) as type number in assigment
//var c number = b
//此处需要转换
var c = number(b)
fmt.Println(a, b, c) // {5} {5} {5}
}
结构体类型可以像其他数据类型一样作为参数传递给函数,并以访问成员变量的方式访问结构体变量,有形式参数传输和指针参数传输两种方式:形式传参、指针传参
type Employee struct {
Id int
Name string
Address string
Phone string
}
// 形式传参
func operateEmployee1(employee Employee) {
employee.Id = 10010
}
// 指针传参
func operateEmployee2(employee *Employee) {
employee.Id = 10010
}
func main() {
var employee Employee
employee.Id = 10001
employee.Name = "Lisa"
employee.Address = "北京昌平"
employee.Phone = "123456"
fmt.Printf("形式传参之前,employee Id: %d\n", employee.Id)
operateEmployee1(employee)
fmt.Printf("形式传参之后,employee Id: %d\n", employee.Id)
fmt.Println("--------------------------------")
fmt.Printf("指针传参之前,employee Id: %d\n", employee.Id)
operateEmployee2(&employee)
fmt.Printf("指针传参之后,employee Id: %d\n", employee.Id)
}
结构体在实例化时可以直接对成员变量进行初始化,初始化有两种形式,分别是以字段“键值对”形式和多个值的列表形式,键值对形式的初始化适合选择性填充字段较多的结构体,多个值的列表形式适合填充字段较少的结构体。
键值对的填充是可选的,不需要初始化的字段可以不写入初始化列表中。
结构体实例化后字段的默认值是字段类型的默认值,例如,数值为0、字符串为""(空字符串)、布尔为false、指针为nil等。
// 键值之间以“:”分隔,键值对之间以“,”分隔。
ins := 结构体类型名 {
字段1: 字段1的值,
字段2: 字段2的值,
…
}
type People struct {
Name string
child *People
}
func main() {
relation := People{
Name: "爷爷",
child: &People{
Name: "爸爸",
child: &People{
Name: "我",
},
},
}
fmt.Println(relation.Name, "->", *relation.child, "->", *relation.child.child)
}
Go语言可以在“键值对”初始化的基础上忽略“键”,也就是说,可以使用多个值的列表初始化结构体的字段。
多个值使用逗号分隔初始化结构体:
ins := 结构体类型名{
字段1的值,
字段2的值,
…
}
type Address struct {
Province string
City string
ZipNumber int
PhoneNumber int
}
func main() {
address := Address{
"黑龙江",
"哈尔滨",
100010,
1234567,
}
fmt.Println(address)
}
匿名结构体没有类型名称,无须通过type关键字定义就可以直接使用。
匿名结构体定义格式:匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成,结构体定义时没有结构体类型名,只有字段和类型定义,键值对初始化部分由可选的多个键值对组成,格式如下:
ins := struct {
//匿名结构体字段定义
字段1 字段类型1
字段2 字段类型2
…
}{
//字段值初始化
初始化字段1: 字段1的值,
初始化字段2: 字段2的值,
…
}
// 键值对初始化部分是可选的,不初始化成员时,匿名结构体的格式变为:
ins := struct {
字段1 字段类型1
字段2 字段类型2
…
}
使用匿名结构体:使用匿名结构体的方式定义和初始化一个消息结构,这个消息结构具有消息标示部分(ID)和数据部分(data),打印消息内容的printMsg()函数在接收匿名结构体时需要在参数上重新定义匿名结构体:
// 打印消息类型,传入匿名结构体
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)
}
结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用。外层结构体通过outer.in1直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。
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)
}
内嵌结构体的特点如下:
① 内嵌的结构体可以直接访问其成员变量。嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过层层的结构体字段访问到最终的字段。例如,ins.a.b.c的访问可以简化为ins.c。
② 内嵌结构体的字段名是它的类型名。内嵌结构体字段仍然可以使用详细的字段进行层层访问,内嵌结构体的字段名就是它的类型名,代码如下:
var c Color
c.BasicColor.R = 1
c.BasicColor.G = 1
c.BasicColor.B = 0
一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。