方法

方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者

在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。

也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。

来看个例子:

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. age int
  5. }
  6. func (p Person) howOld() int {
  7. return p.age
  8. }
  9. func (p *Person) growUp() {
  10. p.age += 1
  11. }
  12. func main() {
  13. // qcrao 是值类型
  14. qcrao := Person{age: 18}
  15. // 值类型 调用接收者也是值类型的方法
  16. fmt.Println(qcrao.howOld())
  17. // 值类型 调用接收者是指针类型的方法
  18. qcrao.growUp()
  19. fmt.Println(qcrao.howOld())
  20. // ----------------------
  21. // stefno 是指针类型
  22. stefno := &Person{age: 100}
  23. // 指针类型 调用接收者是值类型的方法
  24. fmt.Println(stefno.howOld())
  25. // 指针类型 调用接收者也是指针类型的方法
  26. stefno.growUp()
  27. fmt.Println(stefno.howOld())
  28. }

上例子的输出结果是:

  1. 18
  2. 19
  3. 100
  4. 101

调用了 growUp 函数后,不管调用者是值类型还是指针类型,它的 Age 值都改变了。

实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作,用一个表格来呈现:

- 值接收者 指针接收者
值类型调用者 方法会使用调用者的一个副本,类似于“传值” 使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp()
指针类型调用者 指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld() 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针

值接收者和指针接收者

前面说过,不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。

先说结论:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。

来看一个例子,就会完全明白:

  1. package main
  2. import "fmt"
  3. type coder interface {
  4. code()
  5. debug()
  6. }
  7. type Gopher struct {
  8. language string
  9. }
  10. func (p Gopher) code() {
  11. fmt.Printf("I am coding %s language\n", p.language)
  12. }
  13. func (p *Gopher) debug() {
  14. fmt.Printf("I am debuging %s language\n", p.language)
  15. }
  16. func main() {
  17. var c coder = &Gopher{"Go"}
  18. c.code()
  19. c.debug()
  20. }

上述代码里定义了一个接口 coder,接口定义了两个函数:

  1. code()
  2. debug()

接着定义了一个结构体 Gopher,它实现了两个方法,一个值接收者,一个指针接收者。

最后,我们在 main 函数里通过接口类型的变量调用了定义的两个函数。

运行一下,结果:

  1. I am coding Go language
  2. I am debuging Go language

但是如果我们把 main 函数的第一条语句换一下:

  1. func main() {
  2. var c coder = Gopher{"Go"}
  3. c.code()
  4. c.debug()
  5. }

运行一下,报错:

  1. src/main.go:23:6: cannot use Gopher literal (type Gopher) as type coder in assignment:
  2. Gopher does not implement coder (debug method has pointer receiver)

看出这两处代码的差别了吗?第一次是将 &Gopher 赋给了 coder;第二次则是将 Gopher 赋给了 coder

第二次报错是说,Gopher 没有实现 coder。很明显了吧,因为 Gopher 类型并没有实现 debug 方法;表面上看, *Gopher 类型也没有实现 code 方法,但是因为 Gopher 类型实现了 code 方法,所以让 *Gopher 类型自动拥有了 code 方法。

当然,上面的说法有一个简单的解释:接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。

所以,当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。

最后,只要记住下面这点就可以了:

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

两者分别在何时使用

如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

使用指针作为方法的接收者的理由:

  • 方法能够修改接收者指向的值。
  • 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质

如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header,而 header 本身就是为复制设计的。

如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体

这一段说的比较绕,大家可以去看《Go 语言实战》5.3 那一节。

参考资料

【飞雪无情 Go实战笔记】https://www.flysnow.org/2017/04/03/go-in-action-go-interface.html

【何时使用指针接收者】http://ironxu.com/711

【理解Go Interface】http://lanlingzi.cn/post/technical/2016/0803_go_interface/

【Go语言实战 类型的本置】 图书《Go In Action》