类型:值还是引用
Go 的基本数据类型大部分是值类型; 但也经常看到 Go 代码里有 *
和 &
出现,毕竟, Go 与 C 渊源较深。
值类型和引用类型的区别是什么,各自适合的场景是什么?
值类型
int, float, bool, string 等内建类型以及数组和自定义结构体
变量直接存储值,内容通常在栈中分配
var i = 5 // i --> 5
引用类型
指针,slice, map, chan 以及自定义结构体前加个 *
等都是引用类型
变量存储的是一个地址,该地址存储最终的值,内容通常在堆上分配,通过 GC 回收
ref r ------> 内存地址 -----> 值
指针的限制
之所以叫引用类型而不是指针类型,是因为指针的使用被极大限制了
a := 10
ptr := &a
(*ptr)++ // <=> a++
ptr++ // 编译错误,不允许指针运算
可以看到,Go 里的指针和 Java 等语言里的引用类似,通过指针得到的变量叫做引用类型
方法接收者的类型
这个问题更一般的问法:什么时候用值类型,什么时候用引用类型?
假设有一个结构体 Person:
type Person struct {
Age int
Name string
}
Person 有一个 Speak 方法:
申明方式 1:
func (p Person) Speak() {
println("Hi, I'm", p.Name)
}
当前,Speak 方法的接收者是 Person
类型
有两个使用这个结构体的示例
1:
tom := Person{Name: "Tom", Age: 10}
tom.Speak() // 没有问题, Speak 就是 Person 类型的一个方法,可以直接调用
2:
ptr := &Person{Name: "Tom", Age: 10}
// ptr 调 Speak 也没有问题,虽然 ptr 的类型是 *Person,
// 但这里编译器会把 ptr 隐式转换成 *ptr, 即一个 Person 类型
ptr.Speak()
现在,做一个小改动,让 Speak 方法接收者类型为 *Person
申明方式 2:
func (p *Person) Speak() {
println("Hi, I'm", p.Name)
}
当前,Speak 方法的接收者是 *Person
类型
同时,上边两个使用示例也都可以正常运行。
示例 2 能正常运行可以理解,示例 1 为什么也行?
原因也在编译器,在示例 1 中, tom 这个变量被编译器隐式转换成了 &tom
有两个问题?
- 既然编译器这么贴心,为什么不抛弃指针,不要
*
和&
? - 如果一定要指针,那么对于 Speak 方法,到底应该让接收者是值类型还是引用类型?
为什么不完全抛弃指针?
值类型最大的优点是安全,可以给多个函数传递一个值类型的参数,无论那些函数里边怎么实现,调用完成后,原来变量的值是不变的
而引用类型传递给函数作为参数,函数是可以通过这个引用(指针)修改被引用对象的属性(字段)的。
比如我们给 Person 结构体加一个方法,用于让一个人长大一岁:
func (p Person) Grow() {
p.Age++
}
现在接续上面的示例1,让 tom 长大一岁:
tom := Person{Name: "Tom", Age: 10}
tom.Speak()
tom.Grow()
println(tom.Age) // 发现 tom 还是 10 岁!
这次 Grow 方法并没有预期的效果,把方法接收者改成引用类型试试:
func (p *Person) Grow() {
p.Age++
}
现在 tom 可以正常长到 11 岁了~
这就是 Go 没有完全抛弃指针的一个原因。
还有一个原因,假设结构体非常庞大,值类型变量作为函数参数传递时发生值拷贝,这个耗费也是非常可观的,这种情况下用指针就会比较好
方法接收者应该是什么类型?
从上边的实验,现在就可以比较明确地知道,什么时候方法接收者应该是引用类型了:
1.要修改属性
2.结构体太大,要减少拷贝消耗
实际上大多数情况下,方法接收者都是引用类型。
那什么情况下应该是值类型呢?
值类型变量内存一般在栈中分配,不像堆里分配的引用类型会给 GC 带来压力,所以以下情况适合使用值类型
无需修改结构体属性,且结构体比较小
即只读小对象适合用值类型
扩展
如果增加接口,一个结构体到底有没有没实现这个接口,也跟结构体的方法定义时的方法接收者类型息息相关,如:
type Grower interface {
Grow()
}
type Person struct {
Age int
Name string
}
func (p *Person) Grow() {
p.Age++
}
Person 到底有没有实现 Grower 接口?可以用以下示例验证:
func main() {
tom := Person{Name: "Tom", Age: 10}
var g Grower = tom
g.Grow()
}
如果有问题,应该怎么修改?