20.3 指针方法与值方法

有类型T,方法的接收器为(t T)时我们称为值接收器,该方法称为值方法;方法的接收器为(t *T)时我们称为指针接收器,该方法称为指针方法。

如果想要方法改变接收器的数据,就在接收器的指针上定义该方法;否则,就在普通的值类型上定义方法。这是指针方法和值方法最大的区别。

下面声明一个 T 类型的变量,并调用其方法 M1()M2()

package main
import (
	"fmt"
)
type T struct {
	Name string
}
func (t T) M1() {
	t.Name = "name1"
}
func (t *T) M2() {
	t.Name = "name2"
}
func main() {
	t1 := T{"t1"}
	fmt.Println("M1调用前:", t1.Name)
	t1.M1()
	fmt.Println("M1调用后:", t1.Name)
	fmt.Println("M2调用前:", t1.Name)
	t1.M2()
	fmt.Println("M2调用后:", t1.Name)
}
程序输出:
M1调用前: t1
M1调用后: t1
M2调用前: t1
M2调用后: name2

可见,t1.M2()修改了接收器数据。

分析: 由于调用 t1.M1() 时相当于T.M1(t1),实参和形参都是类型 T。此时在M1()中的t只是t1的值拷贝,所以M1()的修改影响不到t1。

同上, t1.M2() => M2(t1),这是将 T 类型传给了 *T 类型,Go会取 t1 的地址传进去:M2(&t1),所以M2()的修改可以影响 t1 。

上面的例子同时也说明了:

 T 类型的变量可以调用M1()和M2()这两个方法。

因为对于类型 T,如果在 *T 上存在方法 M2(),并且 t 是这个类型的变量,那么 t.M2() 会被自动转换为 (&t).M2()

下面声明一个 *T 类型的变量,并调用方法 M1()M2()

package main
import (
	"fmt"
)
type T struct {
	Name string
}
func (t T) M1() {
	t.Name = "name1"
}
func (t *T) M2() {
	t.Name = "name2"
}
func main() {
	t2 := &T{"t2"}
	fmt.Println("M1调用前:", t2.Name)
	t2.M1()
	fmt.Println("M1调用后:", t2.Name)
	fmt.Println("M2调用前:", t2.Name)
	t2.M2()
	fmt.Println("M2调用后:", t2.Name)
}
程序输出:
M1调用前: t2
M1调用后: t2
M2调用前: t2
M2调用后: name2

分析:

t2.M1() => M1(t2),t2 是指针类型,取t2的值并拷贝一份传给M1()。

t2.M2() => M2(t2),都是指针类型,不需要转换。

*T 类型的变量也可以调用M1()和M2()这两个方法。

从上面调用我们可以得知:无论你声明方法的接收器是指针接收器还是值接收器,Go都可以帮你隐式转换为正确的方法使用。

但我们需要记住,值变量只拥有值方法集,而指针变量则同时拥有值方法集和指针方法集。

接口变量上的指针方法与值方法

无论是T类型变量还是 *T 类型变量,都可调用值方法或指针方法。但如果是接口变量呢,那么这两个方法都可以调用吗?

我们添加一个接口看看:

package main
type T struct {
	Name string
}
type Intf interface {
	M1()
	M2()
}
func (t T) M1() {
	t.Name = "name1"
}
func (t *T) M2() {
	t.Name = "name2"
}
func main() {
	var t1 T = T{"t1"}
	t1.M1()
	t1.M2()
	var t2 Intf = t1
	t2.M1()
	t2.M2()
}

编译不通过:cannot use t1 (type T) as type Intf in assignment: T does not implement Intf (M2 method has pointer receiver)

上面代码中我们看到,var t2 Intf 中,t2是Intf接口类型变量,t1是T类型值变量。上面错误信息中已经明确了T没有实现接口Intf,所以不能直接赋值。这是为什么呢?

首先这是Go语言的一种规则,具体如下:

  • 规则一:如果使用指针方法来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。
  • 规则二:如果使用值方法来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。

按照上面两条规则的规则一,我们稍微修改下代码:

package main
type T struct {
	Name string
}
type Intf interface {
	M1()
	M2()
}
func (t T) M1() {
	t.Name = "name1"
}
func (t *T) M2() {
	t.Name = "name2"
}
func main() {
	var t1 T = T{"t1"}
	t1.M1()
	t1.M2()
	var t2 Intf = &t1
	t2.M1()
	t2.M2()
}

程序编译通过。综合起来看,接口类型的变量(实现了该接口的类型变量)调用方法时,我们需要注意方法的接收器,是不是真正实现了接口。结合接口类型断言,我们做下测试:

package main
import (
	"fmt"
)
type T struct {
	Name string
}
type Intf interface {
	M1()
	M2()
}
func (t T) M1() {
	t.Name = "name1"
	fmt.Println("M1")
}
func (t *T) M2() {
	t.Name = "name2"
	fmt.Println("M2")
}
func main() {
	var t1 T = T{"t1"}
	// interface{}(t1) 先转为空接口,再使用接口断言
	_, ok1 := interface{}(t1).(Intf)
	fmt.Println("t1 => Intf", ok1)
	_, ok2 := interface{}(t1).(T)
	fmt.Println("t1 => T", ok2)
	t1.M1()
	t1.M2()
	_, ok3 := interface{}(t1).(*T)
	fmt.Println("t1 => *T", ok3)
	t1.M1()
	t1.M2()
	_, ok4 := interface{}(&t1).(Intf)
	fmt.Println("&t1 => Intf", ok4)
	t1.M1()
	t1.M2()
	_, ok5 := interface{}(&t1).(T)
	fmt.Println("&t1 => T", ok5)
	_, ok6 := interface{}(&t1).(*T)
	fmt.Println("&t1 => *T", ok6)
	t1.M1()
	t1.M2()
}
程序输出:
t1 => Intf false
t1 => T true
M1
M2
t1 => *T false
M1
M2
&t1 => Intf true
M1
M2
&t1 => T false
&t1 => *T true
M1
M2

执行结果表明,t1 没有实现Intf方法集,不是Intf接口类型;而&t1 则实现了Intf方法集,是Intf接口类型,可以调用相应方法。t1 这个结构体值变量本身则调用值方法或者指针方法都是可以的,这是因为语法糖存在的原因。

按照上面的两条规则,那究竟怎么选择是指针接收器还是值接收器呢?

  • 何时使用值类型
    • 如果接收器是一个 map,func 或者 chan,使用值类型(因为它们本身就是引用类型)。
    • 如果接收器是一个 slice,并且方法不执行 reslice 操作,也不重新分配内存给 slice,使用值类型。
    • 如果接收器是一个小的数组或者原生的值类型结构体类型(比如 time.Time 类型),而且没有可修改的字段和指针,又或者接收器是一个简单地基本类型像是 int 和 string,使用值类型就好了。

值类型的接收器可以减少一定数量的内存垃圾生成,值类型接收器一般会在栈上分配到内存(但也不一定),在没搞明白代码想干什么之前,别为这个原因而选择值类型接收器。

  • 何时使用指针类型
    • 如果方法需要修改接收器里的数据,则接收器必须是指针类型。
    • 如果接收器是一个包含了 sync.Mutex 或者类似同步字段的结构体,接收器必须是指针,这样可以避免拷贝。
    • 如果接收器是一个大的结构体或者数组,那么指针类型接收器更有效率。
    • 如果接收器是一个结构体,数组或者 slice,它们中任意一个元素是指针类型而且可能被修改,建议使用指针类型接收器,这样会增加程序的可读性。

最后如果实在还是不知道该使用哪种接收器,那么记住使用指针接收器是最靠谱的。