Go 结构体

语雀同文笔记:14.Go 结构体 · 语雀

结构体也属于复合类型,通常使用一个带属性的结构体来表示现实中的实体。

结构体的目的就是把数据聚集在一起,以便能够更加便捷地操作这些数据,在外部来看就像是一个实体。但结构体依旧是值类型,因此可以通过new()函数来创建。

Go语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。Go语言使用结构体和结构体成员来描述现实的实体和实体所对应的各种属性。

Go语言中的类型可以被实例化,使用new或&构造的实例类型是类型的指针。

1、结构体的定义

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

使用关键字type可以将各种基本类型定义为自定义类型。结构体是一种复合的基本类型,通过type定义为自定义类型后,使结构体更便于使用。

结构体的语法格式:

type 类型名 struct {
    字段1 字段1类型    
    字段2 字段2类型    
    …
}
  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • struct{}:表示结构体类型,type类型名struct{}可以理解为将struct{}结构体定义为类型名的类型。
  • 如果语法写成type T struct {a,b int}也是合法的,它更适用于简单的结构体。
  • 如果字段在代码中从来不会被用到,那可以命名为_(空标识符)。

使用结构体可以表示一个包含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: "李老师"})
}

2、结构体的创建

结构体的定义是一种对内存布局的描述,只有当结构体真正被创建后,才会真正地分配内存,因此,必须在定义并创建结构体后才能使用结构体的字段。Go语言可以通过多种方式来创建结构体,根据实际需要可以选用不同的写法。

a. 基本的创建形式

结构体本身是一种类型,可以像整型、字符串等类型一样,以var的方式声明结构体即可完成创建。

语法格式:var ins T

  • T为结构体类型。
  • ins为结构体的实例

用结构体表示的点结构(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)
}

  • 使用“.”来访问结构体的成员变量,如p.X和p.Y等
  • 结构体成员变量的赋值方法与普通变量一致
b. 创建指针类型结构体

Go语言中,还可以使用new关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

new()函数创建结构体的语法格式:ins := new(T)

  • T为类型,可以是结构体、整型、字符串等。
  • ins:T类型被实例化后保存到ins变量中,ins的类型为*T,属于指针。
  • 使用new()函数创建指针类型的结构体时,也可以使用“.”来访问结构体指针的成员。

Go 结构体_第1张图片

操作错误???
c. 结构体地址的实例化

在Go语言中,对结构体进行“&”取地址操作时,视为对该类型进行一次new的实例化操作。

取地址语法格式:ins := &T{}

  • T表示结构体类型。
  • 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)
}

  • 第3行,定义Command结构体,表示命令行指令。
  • 第5行,命令绑定的变量,使用整型指针绑定一个指针,指令的值可以与绑定的值随时保持同步。
  • 第10行,命令绑定的目标整型变量:版本号。
  • 第11行,对结构体取地址实例化

取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程:

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)
}
3、结构体的使用
a. 递归结构体

结构体类型可以通过引用自身来定义,因此,可以用来定义链表或二叉树的元素,此时节点包含指向邻近节点的链接(地址)。

例如,data字段用于存放有效数据,su指针指向后续节点。

type Node struct{
	data float64
	su *Node
}
  • 链表中的第一个元素称为head,它指向第二个元素;
  • 最后一个元素称为tail,它没有后继元素,所以它的su值为nil。

同样还可以定义一个双向链表,它有一个前驱节点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
}
b. 结构体的转换

当给结构体定义一个别名(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}
}
c. 结构体的参数

结构体类型可以像其他数据类型一样作为参数传递给函数,并以访问成员变量的方式访问结构体变量,有形式参数传输和指针参数传输两种方式:形式传参、指针传参

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)
}
  • 形式参数中employee只传递了一个副本到另一个函数中,函数中操作的是副本,employee没有任何影响;
  • 而在指针参数中employee传递的是地址,函数中的操作会影响employee。

Go 结构体_第2张图片

4、成员变量的初始化

结构体在实例化时可以直接对成员变量进行初始化,初始化有两种形式,分别是以字段“键值对”形式和多个值的列表形式,键值对形式的初始化适合选择性填充字段较多的结构体,多个值的列表形式适合填充字段较少的结构体。

a. 键值对初始化

键值对的填充是可选的,不需要初始化的字段可以不写入初始化列表中。

结构体实例化后字段的默认值是字段类型的默认值,例如,数值为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)
}

b. 值列表初始化

Go语言可以在“键值对”初始化的基础上忽略“键”,也就是说,可以使用多个值的列表初始化结构体的字段。

多个值使用逗号分隔初始化结构体:

  • 必须初始化结构体的所有字段。
  • 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  • 键值对与值列表的初始化形式不能混用。
ins := 结构体类型名{
    字段1的值,    
    字段2的值,    
    …
}

type Address struct {
	Province    string
	City        string
	ZipNumber   int
	PhoneNumber int
}

func main() {
	address := Address{
		"黑龙江",
		"哈尔滨",
		100010,
		1234567,
	}
	
	fmt.Println(address)
}
5、匿名结构体

匿名结构体没有类型名称,无须通过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)
}

  • 使用字符串格式化中的%T动词,将msg的类型名打印出来。
  • 匿名结构体的类型名是结构体包含字段成员的详细描述,匿名结构体在使用时需要重新定义,造成大量重复的代码,因此开发中较少使用。
6、内嵌结构体

结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用。外层结构体通过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

一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。

你可能感兴趣的:(golang,开发语言,后端)