📅 2024年5月6日
📦 使用版本为 1.21.5
结构体和方法
⭐️ Go 通过类型别名(alias types
)和结构体的形式支持用户自定义类型,或者叫定制类型。一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型(composite types
),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 new
函数来创建。
⭐️ 组成结构体类型的那些数据称为 字段(fields
)。每个字段都有一个类型和一个名字;在一个结构体中,
字段名字必须是唯一的
⭐️ 它和 C
语言中的结构体差不多
1️⃣ 结构体的定义
⭐️ 需要先了解 type
关键字,它可以给其他数据类型 "起别名"
type tc int //定义别名为tc
func main() {
var i tc
i = 1
fmt.Printf("Value: %v,Type of: %T", i, i)
}
//输出
Value: 1,Type of: main.tc
⭐️ 可以使用 type
来定义 struct
,结构体的字段可以是任何类型,可以是函数也可以是接口,甚至可能是它本身,如果字段在代码中从来也不会被用到,那么可以命名它为 _。
type Person struct { //定义了一个人的结构体
name string //名字
age int //年龄
sex string //性别
}
⭐️ struct{}
在Go中用来定义一个空的结构体类型,而 struct{}{}
则创建了该类型的一个实例,尽管它不存储任何数据。这些结构在编写并发程序和处理反射时非常有用
🌟 初始化结构体
⭐️ 可以直接使用 var
来声明,任何使用 .
号选择器来,获取修改结构体字段的值,无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation
) 来引用结构体的字段
type Person struct {
name string
age int
sex string
}
func main() {
var p1 Person //声明一个Person类型的p1,你可以理解为创建了一个Person对象p1
p1.name = "张三" //这里和python的类差不多
p1.age = 18
p1.sex = "男"
fmt.Println(p1)
}
⭐️ 还可以使用 new
,不过使用 new
是返回指针类型,需要将 Person
也设置为指针类型
type Person struct {
name string
age int
sex string
}
func main() {
var p1 *Person //需要设置为指针类型,变量 p1 是一个指向 Person 的指针,此时结构体字段的值是它们所属类型的零值。
//可以直接写为 p1 := new(Person)
p1 = new(Person)
p1.name = "张三"
p1.age = 18
p1.sex = "男"
fmt.Println(p1)
}
⭐️ 不仅仅可以通过选择器来初始化,还可以使用混合字面量语法,来初始化,值必须是顺序编写的,new(Person)
和 &Person{}
方法类似
//使用new方法的初始化
func main() {
p1 := &Person{
Name: "张三",
sex: "男",
}
fmt.Println(p1)
}
//使用普通
func main() {
var p1 Person
p1 = Person{
name: "张三",
age: 18,
sex: "男",
}
fmt.Println(p1)
}
还可以简写
func main() {
p1 := new(Person)
p1 = &Person{"张三", 18, "男",}
fmt.Println(p1)
}
假如不出初始化 age
func main() {
p1 := new(Person)
p1 = &Person{
name: "张三",
sex: "男",
}
fmt.Println(p1)
}
⭐️ 在结构体(struct)中字段的定义可以包含公开(导出)和非公开(未导出)的字段
type Person struct {
Name string //公开字段
age int //私有字段
sex string
}
⭐️ 下图说明了结构体类型实例和一个指向它的指针的内存布局
type Point struct { x, y int }
- 使用
new
初始化
-
作为结构体字面量初始化
⭐️ 使用 type
定义的类型,在包中必须是唯一的
🌟 结构体的内存布局
⭐️ Go
语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结
构体,这在性能上带来了很大的优势。不像 Java
中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go
语言中的指针很像
🌟 递归结构体
⭐️ 可以参考十六、Java的数据结构 - 七点半的菜市场 (tanc.fun)来学习
⭐️ 结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节
点包含指向临近节点的链接(地址)。如下所示,链表中的 su
,树中的 ri
(右节点) 和 le
(左节点) 分别是指向别的节点
的指针
⭐️ 这不就是链表吗,head为头节点不存储数据,带个指针指向下一个节点(Java
中为 next
)
type LinkedList struct {
data int
next *LinkedList //指向下一个LinkedList
}
⭐️双向链表
type LinkedList struct {
previous *LinkedList //指向前一个LinkedList
data int
next *LinkedList //指向下一个LinkedList
}
⭐️ 二叉树
二叉树中每个节点最多能链接至两个节点:左节点 (le
) 和右节点 (ri
),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点 (root),底层没有子节点的节点叫叶子节点 (leaves),叶子节点的 le
和 ri
指针为 nil
值
type LinkedList struct {
left *LinkedList //左节点
data int
right *LinkedList //右节点
}
🌟相同结构体的转换
⭐️ 使用 type
定义了一个别名为 test1
的结构体,在使用 type
定义了一个类型为 test1
结构体的 test2
类型,它们两个都是相同的底层,它们两个九可以互相切换
type Person struct {
Name string //公开字段
age int //私有字段
sex string
}
type Xiaoming Person
func main() {
p1 := Person{
Name: "张三",
sex: "男",
}
p2 := Xiaoming{
Name: "李四",
sex: "女",
}
var p3 = Person(p2)
fmt.Println(p3, p2, p1)
}
2️⃣ 使用加工工厂的办法来创建结构体
⭐️ 就是定义一个函数(方法),直接在方法内部返回一个结构体
//指针类型,更加节省内存
func NewPerson(name string, age int, sex string) *Person {
return &Person{Name: name, age: age, sex: sex}
}
//值类型
func NewPerson(name string, age int, sex string) Person {
return Person{Name: name, age: age, sex: sex}
}
//使用
func main() {
test := NewPerson("xm", 18, "男")
test.Name = "xh"
fmt.Println(test)
}
func NewPerson(name string, age int, sex string) *Person {
return &Person{Name: name, age: age, sex: sex}
}
⭐️ 在其他包中,如果不想用户在使用包的时候,强制使用工厂方法,就可以将结构体的类型权限修改为私有即可,这样就需要使用加工工厂方法才可以了
⭐️ 很像 java
的构建函数
type person struct {
Name string //公开字段
age int //私有字段
sex string
}
type person struct {
name string //公开字段
age int //私有字段
sex string
}
func NewPerson(name string, age int, sex string) *person {
return &person{
name: name,
age: age,
sex: sex,
}
}
import (
"fmt"
"var/Test2/normal_bao" //调用结构体所在的包
)
func main() {
p1 := normal_bao.NewPerson("张三", 18, "男") //使用构建函数
fmt.Println(p1)
}
3️⃣ 带标签的结构体
⭐️ 结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag
):它是一个附属于字段的字符串,
可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect
能获取它。我们
将在下一章中深入的探讨 reflect
包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf()
可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过
Field
来索引结构体的字段,然后就可以使用 Tag
属性
package main
import (
"fmt"
"reflect"
)
type TagType struct { // tags
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)
}
4️⃣ 匿名字段和嵌套结构体
⭐️ 结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体
⭐️ 可以粗略地将这个和面向对象语言中的继承概念比较,Go
语言的继承是通过内嵌或组合来实现二点
⭐️ 结构体提供一个简单的继承,这个简单的继承机制提供一种方式,使得可以从另外一个或一些类型继承部分或全部实现
type one struct {
a int
b int
}
type two struct {
b int
int //匿名字段
one //匿名字段 内嵌结构体
}
func main() {
var test two
test.a = 1 //可以继承到one内的元素
test.b = 2
test.int = 3 //匿名字段,在结构体中每一个类型只能有一个匿名字段
test.one = one{4, 5}
}
⭐️ 命名冲突,如果这个结构体继承了另外一个结构体,它们两个结构体中都有一个 a
字段,这样就会引发命名冲突
- 外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式;
- 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。
- 如果有相同的字段名字,但是不同的类型,这样不会引发错误,需要读取的时候就是就近原则