go学习记录——第十一天

结构(struct)与方法(method)

Go通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体。它把数据聚集起来,然后访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过new函数来创建

组成结构体类型的哪些数据被称为字段(fields)。每个字段都有一个类型和名字;在一个结构体中,字段名必须是唯一的。

结构体的概念在软件工程上旧的属于叫 ADT(抽象数据类型:Abstaract Data Type),在一些老的编程语言中叫记录(Record),比如Cobol,在C家族的编程语言中也有它的存在,叫 sturct,在面向对象的编程语言中,跟一个无方法的轻量级类一样。不过因为Go语言中没有类的概念,因此在Go中,结构体有着更为重要的地位。

理解

这部分就是我自己在看完上面的内容之后结合AI然后搞出来的一些理解,方便学习吧。

1. 结构体的本质

  • 是一种用户自定义的复合类型
  • 可以把多个不同类型的数据打包成一个整体
  • 代表现实世界中的实体

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个Person结构体
type Person struct {
Name string
Age int
Address string
Birthday time.Time
}

// 创建结构体示例的几种方法
// 1. 直接声明
var p1 Person
// 2. new关键字
p2 := new(Person)
// 3. 字面量形式
p3 := Person {
Name: "张三",
Age: 25,
Address: "北京市",
Birthday: time.Now(),
}

2. 结构体 VS 其他其他概念

  • C语言中的 struct:基本相同,都是组合数据的方式
  • 在面向对象语言中的类:结构体类似于没有方法的轻量级类
  • 在老式语言中的 Record:本质是一样的,都是记录数据的复合结构

3. 结构体的特点

值类型:可以直接复制,会产生副本

字段唯一:同结构体中不能出现重名字段

可组合:支持嵌套其他结构体

1
2
3
4
5
6
7
8
9
10
11
12
// 结构体嵌套的例子
type Adress struch {
City string
Street string
ZipCode string
}

type Employee struct {
Name string
Age int
Contact Address
}

4. 抽象数据类型(ADT)角度

  • 结构体提供了一种抽象化的数据组织方式
  • 封装了数据,使得能够以实体的方式思考问题
  • 让代码更加模块化和易于维护
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ADT的实际应用例子
type BankAccount struct {
accountNumber string
balance float64
owner person
}

// 虽然Go中结构体本身不包含方法,但是可以为其定义关联方法
func (b *BankAccount) Deposit(amount float64) {
b.balance += amount
}

func (b *BankAccount) Withdraw(amount float64) bool {
if b.balance >= amount {
b.balance -= amount
return true
}
return false
}

5. 在Go语言中的重要性

  • 因为Go不是传统的面向对象语言,没有class关键字
  • 结构体承担了定义自定义类型的重要角色
  • 通过方法接收器(method receiver),结构体可以实现类似面向对象的行为

6. 使用场景

  • 需要将多个相关的数字字段组织在一起时
  • 想要创建自定义类型来表示特定实体时
  • 需要实现特定接口时

结构体定义

结构体定义的一般形式如下:

1
2
3
4
5
type identifier struct {
field1 type1
field2 type2
...
}

type T srtuct {a, b int} 也是合法的语法,它更适用于简单的结构体。

结构体里的字段都有名字,像 field1field2 等,如果字段在代码中从来不会被用到,也可以命名为 _

结构体的字段可以是任意类型,甚至是结构体本身(参考第10.5节)(这话前面我好像写过啊,好熟悉的感觉),也可以是函数或接口(参考第11章(我感觉更熟悉了)。可以声明结构体类型的一个变量,然后像瞎买你这样给它的字段赋值:

1
2
3
var s T
s.a = 5
s.b = 8

数组可以看作是结构体类型,不过它使用下标而不是具体的字段。

使用new()

使用 new() 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行(比如定义是包范围的,但分配却没有必要在开始就做)。

1
2
var t *T
t = new(T)

直接做和分开做的区别我的理解就是,直接做是new了一个实例T,然后指向存储的指针,指针又被别名为t(也就是用指针指向了存储的地址,我是反着推导的,意思上差不多)。而分开就是先搞个t指针指向我们的T,然后在分配的时候转头指向了存储地址(也就是分配好的,分开的理解思路就是正常描述思路,我的反着推到在分开做的情况下不成立)

写这个语句的管用方法是:t := new(T),变量 t 是一个指向 T 的指针,此时结构体字段的值是它们所属类型的零值(也就是说分配内存是分配的该种类型的零值,同时也在分配的时候对其进行了初始化,没理解错应该是这样)

声明 var t *T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型 T 。在这两种方式中, t 通常被称做类型 T 的一个实例(instance)或对象(object)。

示例structs_fields.go给出了一个非常简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type struct1 struct {
i1 int
f1 float32
str string
}

func main() {
ms := new(struct1)
ms.i1 = 10
ms.f1 = 15.5
ms.str = "Chris"

fmt.Printf("The int is: %d\n", ms.i1)
fmt.Printf("The float is: %f\n", ms.f1)
fmt.Printf("The string is: %s\n", ms.str)
fmt.Println(ms)
}

输出结果:

1
2
3
4
The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}

使用 fmt.Println() 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 %v 选项。

就像在面向对象语言所作的那样,可以使用点符号给字段赋值:structname.fuekdname = value(看不懂,学不了一点)

同样的,使用点符号可以获取结构体字段的值:structname.fieldname

在Go语言中这叫选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的选择器符(selector-notation)来引用结构体的字段:

1
2
3
4
5
type myStruct struct {i int}
var v myStruct // v 是结构体类型变量
var p *myStruct // p 是指向结构体类型变量的指针
v.i
p.i

初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:

1
ms := &struct1{10, 15.5, "Chris"}

或者

1
2
var ms struct1
ms = struct1{10, 15.5, "Chris"}

混合字面量语法(composite literal syntax) &struct1{a, b, c} 是一种简写,底层仍然会调用 new() ,这里的值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。表达式 new(Type) and &Type{} 是等价的。

时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子:

1
2
3
4
type Interval struct {
start int
end int
}

初始化方式:

1
2
3
intr := Interval{0, 3}   (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)

在(A)中,值必须以地段在结构体定义是给出的顺序,&不是必须的。(B)展示了另一种方式,字段名加一个冒号放在值前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像(C)中那样。

结构体类型和字段的命名遵循可见性规则(第4.2节),一个导出的结构体类型中有些字段是导出的,另一些不是,这都是有可能的。(这段话书上绝对是机翻,割裂感太强了,还得我自己来)

下图说明了结构体类型示例和一个指向它的指针的内存分布:

1
type Point struct {x, y, int}

使用 new() 初始化:

img

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

img

类型 struct1 在定义它的包 pack1 中必须是唯一的,它的完全类型名是: pack1.struct1

下面的例子 Listing 10.2-person.go 展示了一个结构体 Person,一个方法 upPerson(),方法有一个类型为 *Person 的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"strings"
)

type Person struct {
firstName string
lastName string
}

func upPerson(p *Person) {
p.firstName = strings.ToUpper(p.firstName)
p.lastName = strings.ToUpper(p.lastName)
}

func main() {
// 1-struct as a value type:
var pers1 Person
pers1.firstName = "Chris"
pers1.lastName = "Smith"
upPerson(&pers1)
fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)

// 2-struct as a pointer:
pers2 := new(Person)
pers2.firstName = "Chris"
pers2.lastName = "Smith"
(*pers2).lastName = "Johnson"
upPerson(pers2)
fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)

// 3-struct as a literal:
pers3 := &Person{"Chris", "Smith"}
upPerson(pers3)
fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}

输出结果

1
2
3
The name of the person is CHRIS SMITH
The name of the person is CHRIS JOHNSON
The name of the person is CHRIS SMITH

这个看完补一个点就是&p和*p的区别(其实是我自己搞忘了,写一下,后续自己看方便,各位老爷知道的可以跳过)

&p 这个是返回变量 p 的地址,也就是一个指向 p 的指针,换句话说 & 就是取变量的内存地址

*p 这个是用于解析引用指针 p ,也就是返回指针 p 所指向的内容,换句话说 * 就是访问指针所指向的值

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Person struct {
firstName string
lastName string
}

func main() {
var pers1 Person
pers1.firstName = "Chris"
pers1.lastName = "Woodward"

// 获取 pers1 的地址
p := &pers1 // p 是指向 pers1 的指针

// 通过指针 p 修改 pers1 的内容
p.firstName = "UpdatedFirstName" // 通过指针修改内容
p.lastName = "UpdatedLastName"

// 打印修改后的内容
fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)
}

在这个例子中:

  • p := &pers1p 是一个指向 pers1 的指针,&pers1 返回 pers1 的地址。
  • p.firstName = "UpdatedFirstName":通过指针 p 修改了 pers1firstName 字段。
  • p.lastName = "UpdatedLastName":同样,通过指针 p 修改了 pers1lastName 字段。

因此,最终打印出的名字将是更新后的值。

我们现在回到前面的例子中,在第二种情况中我们可以通过指针,像 pers2.lastName = "Smith" 这样给结构体字段赋值,没有像C++中那样的需要使用 -> 操作符,Go会直接进行转换(方便的捏)。

注意,也可以使用通过解指针的方式来设置值 (*pers2).lastName = "Johnson" (例子里我是自动填充的,名字给改了,相对来说比书里的看得明白一些)

递归结构体

结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用(我好像知道是怎么去写了),此时节点包含指向临近节点的链接(地址)。如下所示,链表中的 su, 树中的 rile 分别是指向别的节点的指针。

链表:

img

代码:

1
2
3
4
type Node struct {
data float64
su *Node
}

链表中的第一个元素叫 head ,它指向了第二个元素;最后一个元素叫 tail ,它没有后继元素,所以它的 sunil 值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。

同样地也可以定义一个双向链表,它有一个前趋节点 pr 和一个后继节点 su

1
2
3
4
5
type Node struct {
pr *Node
data float64
su *Node
}

二叉树:

img

二叉树中每个节点最多能链接至两个节点:左节点 le 和右节点 ri ,这两个节点本身又可以有左右节点,依此类推。叶子节点的两个指针均为 nil 值。

代码:

1
2
3
4
5
type Tree struct {
le *Tree
data float64
ri *Tree
}

结构体转换

Go中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型是,此结构体类型和它的 alias 类型都有相同的底层类型,他们可以如实例中的那样进行相互转换,同时需要注意其中非法赋值或转换引起的编译错误。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type number struct {
f float32
}

type nr number //alias type
func main() {
a := number{3.14}
b := nr{2.71}
// var i float32 = b // compile error: cannot use b (type nr) as type number in assignment
// var i - float32(b)) //compile-error: cannot convert b (type nr) to type float32
// var c number = b // compile error: cannot use b (type nr) as type number in assignment
// needs a conversion
var c number = number(b)
fmt.Println(a, b, c)
}

输出结果:

1
{3.14} {2.71} {2.71}

go学习记录——第十一天
https://www.lx02918.ltd/2024/11/29/go-study-eleventh-day/
作者
Seth
发布于
2024年11月29日
许可协议