Go语言值、引用、指针之间的区别

前言

先看一段代码

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

func (addr address) String() string {
return fmt.Sprintf("the addr is %s%s", addr.province, addr.city)
}
func main() {
add := address{province: "山东省", city: "济南市"}
printString(add)
printString(&add)
}
func printString(s fmt.Stringer) {
fmt.Println(s.String())
}

运行结果:
the addr is 山东省济南市
the addr is 山东省济南市

这段代码中先声明了一个结构体address,结构体有两个string类型的元素组成,分别是省份和城市

然后为addres这个结构体实现了String方法 返回值类型为string 并且进行了格式化

在main函数中先创建一个addres对象并初始化了值 然后分别使用变量和他的指针作为参数调用了printString函数
这个函数的形参是fmt.Stringer 看一下这个fmt.Stringer的解释

fmt.Stringer

Stringer 由任何具有 String 方法的值实现,该方法定义了该值的“native”格式。 String 方法用于将作为操作数传递的值打印到任何接受字符串的格式或未格式化的printer(例如 Print)

因为address实现了String方法,所以可以将addr传入printString函数

printString函数就是将传入变量调用String方法的结果打印出来 也就是上面接口实现时的返回值
由此可见在代码

1
2
3
func (addr address) String() string {
return fmt.Sprintf("the addr is %s%s", addr.province, addr.city)
}

中,不仅address实现了String方法 *address 也实现了String方法

如果我们将这个实现方法改成下面的样子结果会怎样呢?

1
2
3
func (addr *address) String() string {
return fmt.Sprintf("the addr is %s%s", addr.province, addr.city)
}


改成这样之后我们发现 printString(add)报错了,根据提示可以发现使用指针作为方法接收者,则只有指针实现了该方法,使用值作为方法接收者,则值和指针都实现了该方法。

再看下面的代码

在这个示例中,因为类型 address 已经实现了接口 fmt.Stringer,所以它的值可以被赋予变量 si,而且 si 也可以作为参数传递给函数 printString。
接着你可以使用 sip:=&si 这样的操作获得一个指向接口的指针,这是没有问题的。不过最终你无法把指向接口的指针 sip 作为参数传递给函数 printString,Go 语言的编译器会提示如上图所示
于是可以总结为:虽然指向具体类型的指针可以实现一个接口,但是指向接口的指针永远不可能实现该接口。
所以在go语言中就不要用一个指针去指向另一个指针了。

参数传递

下面进入正题,学习一下值、引用、指针在参数传递时的区别
看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type person struct {
name string
age int
}

func modifyPerson(p person) {
p.name = p.name+"6"
p.age = p.age+1
}
func main() {
p := person{name: "张三", age: 18}
modifyPerson(p)
fmt.Println("person name:", p.name, ",age:", p.age)
}
运行结果:
person name: 张三 ,age: 18

在这段代码中声明了一个结构体和修改结构体中元素值的方法,然后再main函数中创建一个结构体对象并修改值然后打印这个对象
但程序运行的结果却不是修改后的结果。这是因为在调用modifyPerson函数的时候传过去的只是p的拷贝而不是p本身,所以此时修改并没有改变原始p的值
那怎么修改才能得到我们想要的结果呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type person struct {
name string
age int
}

func modifyPerson(p *person) {
p.name = p.name + "6"
p.age = p.age + 1
}
func main() {
p := person{name: "张三", age: 18}
modifyPerson(&p)
fmt.Println("person name:", p.name, ",age:", p.age)
}
运行结果:
person name: 张三6 ,age: 19

只需要将modifyPerson的参数改为person的指针类型即可,同时将modifyPerson(p)改为modifyPerson(&p)

下面修改一下代码来看下p在内存中的地址
先看值传递的

modifyPerson(p person) {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	fmt.Printf("modifyPerson函数中p的地址  %p\n", &p)
p.name = p.name + "6"
p.age = p.age + 1
}
func main() {
p := person{name: "张三", age: 18}
fmt.Printf("main函数modifyPerson之前p的地址 %p\n", &p)
modifyPerson(p)
fmt.Printf("main函数modifyPerson之后p的地址 %p\n", &p)
fmt.Println("person name:", p.name, ",age:", p.age)
}
运行结果
main函数modifyPerson之前p的地址 0xc00000c030
modifyPerson函数中p的地址 0xc00000c048
main函数modifyPerson之后p的地址 0xc00000c030
person name: 张三 ,age: 18

通过结果我们可以发现在main函数和modifyPerson函数中p的地址是不同的,modifyPerson函数中p只是main中p的一份拷贝,所以在modifyPerson中修改后,main函数中的p是不受影响的。
导致这种结果的原因是 Go 语言中的函数传参都是值传递。 值传递指的是传递原来数据的一份拷贝,而不是原来的数据本身
除了 struct 外,还有浮点型、整型、字符串、布尔、数组,这些都是值类型。

再看指针传递的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func modifyPerson(p *person) {
fmt.Printf("modifyPerson函数中p的地址 %p\n", &p)
p.name = p.name + "6"
p.age = p.age + 1
}
func main() {
p := person{name: "张三", age: 18}
fmt.Printf("main函数modifyPerson之前p的地址 %p\n", &p)
modifyPerson(&p)
fmt.Printf("main函数modifyPerson之后p的地址 %p\n", &p)
fmt.Println("person name:", p.name, ",age:", p.age)
}
运行结果
main函数modifyPerson之前p的地址 0xc00000c030
modifyPerson函数中p的地址 0xc00000e030
main函数modifyPerson之后p的地址 0xc00000c030
person name: 张三6 ,age: 19

通过运行结果可以发现 modifyPerson函数和main函数中p的地址是一样的。

小提示:值传递的是指针,也是内存地址。通过内存地址可以找到原数据的那块内存,所以修改它也就等于修改了原数据。

引用类型

在Go语言中map 和 chan是引用类型

map:

讲上面的例子修改一下,不使用结构体改用map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func modifyMap(p map[string]int) {
p["张三"] = p["张三"] + 1
}
func main() {
m := make(map[string]int)
m["张三"] = 18
fmt.Println("张三的年龄为", m["张三"])
modifyMap(m)
fmt.Println("张三的年龄为", m["张三"])
}
运行结果
张三的年龄为 18
张三的年龄为 19

没有使用指针,只是用了 map 类型的参数,按照 Go 语言值传递的原则,modifyMap 函数中的 map 是一个副本,怎么会修改成功呢?
要想解答这个问题,就要从 make 这个 Go 语言内建的函数说起。在 Go 语言中,任何创建 map 的代码(不管是字面量还是 make 函数)最终调用的都是 runtime.makemap 函数。

小提示:用字面量或者 make 函数的方式创建 map,并转换成 makemap 函数的调用,这个转换是 Go 语言编译器自动帮我们做的。

从下面的代码可以看到,makemap 函数返回的是一个 *hmap 类型,也就是说返回的是一个指针,所以我们创建的 map 其实就是一个 *hmap。

1
2
3
4
// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap{
//省略无关代码
}

因为 Go 语言的 map 类型本质上就是 *hmap,所以根据替换的原则,我刚刚定义的 modifyMap(p map) 函数其实就是 modifyMap(p *hmap)。这是不是和之前说的指针类型的参数调用一样了。这也是通过 map 类型的参数可以修改原始数据的原因,因为它本质上就是个指针。

所以在这里,Go 语言通过 make 函数或字面量的包装为我们省去了指针的操作,让我们可以更容易地使用 map。其实就是语法糖。

注意:这里的 map 可以理解为引用类型,但是它本质上是个指针,只是可以叫作引用类型而已。在参数传递时,它还是值传递,并不是其他编程语言中所谓的引用传递。

chan:

通过下面的源代码可以看到,所创建的 chan 其实是个 *hchan,所以它在参数传递中也和 map 一样。

1
2
3
func makechan(t *chantype, size int64) *hchan {
//省略无关代码
}

严格来说,Go 语言没有引用类型,但是我们可以把 map、chan 称为引用类型,这样便于理解。除了 map、chan 之外,Go 语言中的函数、接口、slice 切片都可以称为引用类型。

函数类型也是引用就可以很好的解释之前学习匿名函数的代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func creatFunc1() func() int {
i := 0
return func() int {
i++
return i
}
}

func main() {
addFunc1 := creatFunc1()
fmt.Println(addFunc1())
fmt.Println(addFunc1())
fmt.Println(addFunc1())

}
运行结果:
1
2
3

类型的零值

在 Go 语言中,定义变量要么通过声明、要么通过 make 和 new 函数,不一样的是 make 和 new 函数属于显式声明并初始化。如果我们声明的变量没有显式声明初始化,那么该变量的默认值就是对应类型的零值。
各种类型的零值

总结

在 Go 语言中,函数的参数传递只有值传递,而且传递的实参都是原始数据的一份拷贝。如果拷贝的内容是值类型的,那么在函数中就无法修改原始数据;如果拷贝的内容是指针(或者可以理解为引用类型 map、chan 等),那么就可以在函数中修改原始数据。

坚持技术分享,您的支持将鼓励我继续创作