类型:值还是引用

类型:值还是引用

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

有两个问题?

  1. 既然编译器这么贴心,为什么不抛弃指针,不要 *&
  2. 如果一定要指针,那么对于 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()
}

如果有问题,应该怎么修改?