📅 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初始化

image-20240506164653568

  • 作为结构体字面量初始化

    image-20240506164708693

⭐️ 使用 type定义的类型,在包中必须是唯一的

🌟 结构体的内存布局

⭐️ Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结
构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go 语言中的指针很像

image-20240506165358543

🌟 递归结构体

⭐️ 可以参考十六、Java的数据结构 - 七点半的菜市场 (tanc.fun)来学习

⭐️ 结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节
点包含指向临近节点的链接(地址)。如下所示,链表中的 su ,树中的 ri(右节点) 和 le(左节点) 分别是指向别的节点
的指针

⭐️ 这不就是链表吗,head为头节点不存储数据,带个指针指向下一个节点(Java中为 next)

image-20240506165533276

type LinkedList struct {
	data int
	next *LinkedList //指向下一个LinkedList
}

⭐️双向链表

type LinkedList struct {
	previous *LinkedList //指向前一个LinkedList
	data int
	next *LinkedList //指向下一个LinkedList
}

⭐️ 二叉树

二叉树中每个节点最多能链接至两个节点:左节点 (le) 和右节点 (ri),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点 (root),底层没有子节点的节点叫叶子节点 (leaves),叶子节点的 leri 指针为 nil

image-20240506170109463

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字段,这样就会引发命名冲突

  1. 外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式;
  2. 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。
  3. 如果有相同的字段名字,但是不同的类型,这样不会引发错误,需要读取的时候就是就近原则