Golang中该使用指针类型还是值类型
只要从现在开始努力,最坏不过是大器晚成
举个栗子
1 | type Foo struct { |
这里我列出了 3 组方法,分别是指针类型和值类型的示例。这几个方法在编写代码的过程中都会经常遇到,我们从使用者的维度和内存的视角来分析一下这几个方法:
使用区别
大部分人都在讨论函数的入参是指针还是值类型呢?我们先来看看第一组方法,返回值的情况:
1 | s1 := returnValue() |
这两个方法一个返回了指针一个返回值类型,值类型是非 nil 的(在 Go 中所有的值类型都会有 初值),指针类型可以判断是否为 nil。 获取到的数据是相同的,不同之处在于取值的方式,指针类型需要使用 * 号读取数据。
下面尝试传递参数,分别是指针类型参数和值类型参数:
1 | foo := Foo{Name:"biezhi"} |
- modifyNameByPoint 需要指针类型,所以我们取 foo 的指针传入(foo是值类型所以这里用 & 取其地址)。
- nameToUpper 需要一个值类型的参数,所以 foo 直接传入,返回值是转大写的 Name。
- nameToUpper 不会修改 foo.Name 的数据,最后一次输出还是旧数据
Receiver Type
如果你编写 Java 代码的话经常会看到这样的代码
1 | public class Bar { |
可以看到这里有 this 关键字,在 Go 中是没有的,这里的 this 可以调用当前对象的成员变量和实例方法,当使用 this 修改了成员变量就相当于在 Go 中使用了指针,看看下面的 Go 代码:
1 | func (foo *Foo) setNameByPoint(name string) { |
Go 中想要为结构体定义属于自己的方法就使用如上的两种方式,这两个方法在 Go 中称为 Receiver Type(接受者类型),可以使用结构体变量调用,我们今天只讨论结构体这种情况,来看看这两个方法有什么不同:
1 | foo := Foo{Name:"biezhi"} |
根据输出发现一个结构体,如果不使用指针类型的时候值是不会被修改的。这点也很容易理解,在 setName 方法中 foo 变量被作为值传递,所以如果这时候输出 foo 的内存地址会发现和外面调用的是不一样的,来看看:
1 | func (foo Foo) setName(name string) { |
1 | foo := Foo{Name:"biezhi"} |
而 setNameByPoint 方法和前面的指针类型传递是一样的,方法内部内存地址是一份指针的拷贝,修改数据会影响到外部指针变量的数据。
一般而言,工程化的项目中会出现非常多结构体定义方法的代码,这些方法的调用也会很频繁,使用结构体将其封装起来,和 Java 中类封装是一样的,大多数情况下建议都使用指针传递,避免值拷贝的情况。
其他类型
在前面我们有一张图中分了值类型和引用类型,除了那些常用的基本类型,还有像 map 和 slice 这种引用类型,它们在使用上有点像指针(但不用任何操作符如 &、*),来看个例子
1 | func updateMap(mmp map[string]int) { |
如果你尝试 slice 的话是同样的效果,可以看到给方法传递的并非是一个指针类型,但是 map 的值确实被修改了,这是为什么呢
其实拷贝一个 map 或者 slice 的时候并没有拷贝这个类型(引用类型)里面指向的数据,而是拷贝了引用类型(可简单理解为指针),如何验证这一说法呢?我们在 updateMap 中添加一行输出代码:
1 | func updateMap(mmp map[string]int) { |
再次运行代码
1 | src mmp: map[biezhi:1024] address: 0xc000094018 |
你会发现 input mmp 这行的地址发生了变化,正因为拷贝的是这个特殊的“引用类型”,会产生一个新的地址,而这个地址 0xc00000c038 和 0xc000094018 指向的是同一份数据,所以修改后外部的变量也会得到新的数据。
小结
Receiver Type 为什么推荐使用指针?
- 推荐在实例方法上使用指针(前提是这个类型不是一个自定义的 map、slice 等引用类型)
- 当结构体较大的时候使用指针会更高效
- 如果要修改结构内部的数据或状态必须使用指针
- 当结构类型包含 sync.Mutex 或者同步这种字段时,必须使用指针以避免成员拷贝
- 如果你不知道该不该使用指针,使用指针!
结构较大” 到底多大才算大可能需要自己或团队衡量,如超过 5 个字段或者根据结构体内占用来计算。
方法参数该使用什么类型?
- map、slice 等类型不需要使用指针(自带 buf)
- 指针可以避免内存拷贝,结构大的时候不要使用值类型
- 值类型和指针类型在方法内部都会产生一份拷贝,指向不同
- 小数据类型如 bool、int 等没必要使用指针传递
- 初始化一个新类型时(像 NewEngine() *Engine)使用指针
- 变量的生命周期越长则使用指针,否则使用值类型
https://blog.biezhi.me/2018/10/values-or-pointers-in-golang.html