Go语言基础之结构体

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

结构体的定义

使用typestruct关键字来定义结构体,具体代码格式如下:

1
2
3
4
5
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个Person(人)结构体,代码如下:

1
2
3
4
5
type person struct {
name string
city string
age int8
}

同样类型的字段也可以写在一行

1
2
3
4
type person1 struct {
name, city string
age int
}

这样我们就拥有了一个person的自定义类型,它有namecityage三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型

1
var 结构体实例 结构体类型

实例化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type info struct {
// 同类型的声明写在同一行
name, addr string
aget int
}

func main() {
var Info info

// 给结构体的属性赋值
Info.name = "张三"
Info.addr = "上海"
Info.aget = 18

fmt.Println(Info) // {张三 上海 18}
fmt.Printf("%#v\n",Info) // {name:"张三", addr:"上海", aget:18}

fmt.Println(Info.name) // 打印单个值 张三
}

我们通过.来访问结构体的字段(成员变量),例如Info.nameInfo.age等。

取结构体的地址实例化 &{}

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个名为 info的结构体
type info struct {
// 同类型的声明写在同一行
name, addr string
aget int
}

func main() {
// 初始化结构体变量 这里用了 & 是引用类型
Info := &info{}

fmt.Printf("%T\n", Info) // *main.info
fmt.Printf("%#v\n", Info) // &main.info{name:"", addr:"", aget:0}
Info.name = "zhangsan"
Info.aget = 18
Info.addr = "上海"

fmt.Printf("%#v\n", Info) // &main.info{name:"zhangsan", addr:"上海", aget:18}
}

Info.name = "zhangsan"其实在底层是(*Info).name = "zhangsan",这是Go语言帮我们实现的语法糖

匿名结构体

定义匿名结构体时没有 type 关键字,与其他定义类型的变量一样,如果在函数外部需在结构体变量前加上 var 关键字,在函数内部可省略 var 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// 定义后直接赋值
info:= struct {
name string
add string
age int
}{name: "zhangsan",age: 18}

fmt.Printf("%T\n", info) // { name string; add string; age int }
fmt.Printf("%#v\n", info) // {name:"zhangsan", add:"", age:18}
fmt.Println(info.name)
info.name = "张三"
fmt.Printf("%#v\n", info) // {name:"张三", add:"", age:18}
}

结构体初始化

没有初始化的结构体,其成员变量都是对应其类型的零值。

1
2
3
4
5
6
7
8
9
10
11
type info struct {
// 同类型的声明写在同一行
name, addr string
aget int
}

func main() {
var Info info
// 未初始化时成员变量都是对应类型的零值。
fmt.Printf("%#v\n", Info) // main.info{name:"", addr:"", aget:0}
}

通过位置参数初始化

初始化时对应结构体里面的位置传递元素,这种方式必须和声明里面的位置和数据类型一致。

1
2
3
4
5
6
7
8
9
10
11
12
type info struct {
// 同类型的声明写在同一行
name, addr string
aget int
}

func main() {
var Info info

Info = info{"zhangsan","上海",18}
fmt.Println(Info) // {zhangsan 上海 18}
}

通过键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type info struct {
// 同类型的声明写在同一行
name, addr string
aget int
}

func main() {
// 写在一行初始化,为指定的键值将使用零值
a1 := info{name: "zhangsan", aget: 18}
fmt.Printf("%#v\n", a1) // {name:"zhangsan", addr:"", aget:18}

// 写成多行初始化
a2 := info{
name: "zhangsan",
addr: "上海",
aget: 18, // 注意多行时最后一行逗号不能少
}
fmt.Printf("%#v\n", a2) // main.info{name:"zhangsan", addr:"上海", aget:18}

}

也可以对结构体指针进行键值对初始化,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type info struct {
// 同类型的声明写在同一行
name, addr string
aget int
}

func main() {
a3 := &info{
name: "zhangsan",
addr: "上海",
aget: 18,
}
fmt.Printf("%#v\n", a3) // &main.info{name:"zhangsan", addr:"上海", aget:18}
}

初始化字段类型是切片或map时

需要注意的是:当定义结构体的字段类型是 []切片 或者是map[string]string时,结构体初始化时的字段需要make后再进行初始化

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"fmt"
)

// 定义一个名为Person的结构体
type Person struct {
Name string
Age int
Number []int
Hot map[string]string
}

func main() {
// 先声明后赋值 方式一:值类型
var p Person
// 当结构体先声明后赋值时,切片和map数据类型需要make后再使用
// 否则会报以下错误
// 切片报错:panic: runtime error: index out of range [0] with length 0
// map报错:panic: assignment to entry in nil map
p.Number = make([]int, 1)
p.Hot = make(map[string]string)
p.Number[0] = 100
p.Hot["name"] = "张三"
fmt.Println(p)

//先声明后赋值,{}中也可以直接初始化赋值 方式二:值类型
var p21 Person = Person{}
p21.Name = "赵二一"
fmt.Println(p21)
//先声明后赋值,{}中也可以直接初始化赋值 方式二:引用类型
var p2 *Person = &Person{}
p2.Name = "赵六"
fmt.Println(p2)

//先声明后赋值,{}中也可以直接初始化赋值 方式三:引用类型
var p3 = &Person{}
p3.Name = "王七"
fmt.Println(p3)

//声明即赋值 方式一:值类型
p4 := Person{
Name: "李四",
Age: 18,
}
fmt.Println(p4)

//声明即赋值 方式二:引用类型
p5 := &Person{}
p5.Name = "王五"
fmt.Println(p5)
}

值传递和指针传递

在开始前你需要先掌握结构体的定义、声明以及使用方法;

值传递

我们先来看一段代码:

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

// 声明一个名为Info的结构体
type Info struct{
Age int
Name string
}

func change(t Info){
t.Age = 999
}

func main(){
// 结构体变量初始化并赋值
info := Info{Age: 18,Name: "zhangsan"}

change(info)
println(info.Age)
}

程序的输出是 18

因为这种方法传递的是值一个副本,在change() 函数中,你实际上修改的是副本的值;

指针传递

修改程序,让它使用指针进行传递:

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

type Info struct{
Age int
Name string
}

func change(t *Info){
t.Age = 999
}

func main(){
info := Info{Age: 18,Name: "zhangsan"}

change(&info)

println(info.Age)
}

这段程序使用了&取地址操作符来获取结构体的地址,而change()函数期望一个Info结构体的地址类型 *Info,这里*Info的意思是指向类型info值得指针;

程序运行输出 999

但我们需要注意的是,实际上这里传递的依然是一个副本,只不过这个副本是一个地址,它指向原来的值;

所以,你可以修改 info.Age的值,但你无法修改 Info

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

type Info struct{
Age int
Name string
}

func change(t *Info){
t = &Info{Age: 20}
}

func main(){
info := Info{Age: 18,Name: "zhangsan"}

change(&info)

println(info.Age)
}

这段代码输出18

那么,应该如何选择传递方式呢?

很明显,复制一个指针比复制一个结构的消耗要小的多;如果我们的结构非常复杂和庞大,那么复制结构会是一个很消耗性能的操作,在进行大量这样的操作时你的感觉会非常明显;

而使用指针则可避免这个问题;

当你的函数本意是改变原始数据时,那么肯定用指针转递;

当你的结构非常大时,比如包含庞大的切片、map时,也需要用指针转递;

但是如果你的结构体非常小,且不打算修改结构体内容,那么应该考虑使用值传递;

因为你不能保证程序没有bug导致误修改;且在某些多任务环境下,你需要为指针操作添加额外的锁操作,这样有些得不偿失。

结构体方法

  • 结构体方法是一种特殊的函数,它规定只有指定的接收者才能调用该方法,有点像面向对象编程语言中与类绑定的方法,接收者声明在func关键字之后,函数名之前。
  • 方法接收者分为值接收者指针接收者,若方法内部有需要改变接收者的值,需要使用指针接收者
  • 值接收者实参值拷贝,会额外消耗内存空间,当接收者占用内存空间很大时,使用值接收者显然有点浪费空间,因此应尽量使用指针接收者
  • 指针对象调用值接收者的方法时,会自动解引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//声明一个Person结构体
type Person struct {
Name string
Age int
}

//为结构体Person绑定一个名为Test的方法,*Person代表引用类型
//不管调用的形式如何,真正决定是值拷贝还是地址拷贝,看这个方法和哪个类型绑定的
//如果是和值类型,比如(p Person)则是值拷贝,如果是和指针类型,比如(p *Person)则是地址拷贝
func (p *Person) Test() string {
// p.Name = "李四" 当需要改变原有值时 方法的接收者需要用 *Person
fmt.Println("Test方法内的打印:", p.Name)
return p.Name
}

func main() {
//声明并初始化结构体变量
p := &Person{Name: "张三"}
//调用结构体中的方法,因为方法有返回值,所以这里用一个变量去接收
name := p.Test()
fmt.Println("Test方法返回:", name)
}

方法和函数的区别

  • 调用方式不一样:函数的调用方式为 函数名(实参列表)方法的调用方式:变量.方法名(实参列表)
  • 对于普通函数,接收者为值类型时,不能指针类型的数据直接传递,反之亦然。
  • 对于方法,如(struct的方法),接收者为值类型时,可以直接用指针类型变量调用方法,反过来同样也可以。