当结构体实现一个接口时,可以在方法中设置一个接收者,比如对于以下接口:

type Inter interface {
    foo()
}

结构体在实现它时,方法的接收者类型可以是:值、指针。比如:

type S struct {}

func (s S) foo() {} // 值类型
func (s *S) foo() {} // 或者指针类型

在使用结构体初始化接口变量时,结构体的类型也可以是:值、指针。比如:

var s Inter = S{} // 值类型
s.foo()

var s Inter = &S{} // 指针类型
s.foo()

那么调用接口方法的组合实际有四种情况:

  1. 值类型结构体 -> 赋值给接口 -> 调用接受者类型为值类型的结构体方法
  2. 指针类型结构体 -> 赋值给接口 -> 调用接受者类型为指针类型的结构体方法
  3. 值类型结构体 -> 赋值给接口 -> 调用接受者类型为指针类型的结构体方法
  4. 指针类型结构体 -> 赋值给接口 -> 调用接受者类型为值类型的结构体方法

这四种不同情况只有一种会发生编译不通过的问题,即:结构体类型为值类型、调用了接收者为指针的方法(上面第 3 种)。但是反过来,结构体为指针类型时,却可以调用接收值为值或指针的任何方法。这是为什么呢?

接收者是方法的一个额外的参数,而 Go 在调用函数的时候,参数都是值传递的。将一个指针拷贝,它们还是指向同一个地址,指向一个确定的结构体;将一个值拷贝,它们变成了两个不同的结构体,有着不同的地址。这会导致以下两种情况:

当在一个结构体指针上,通过接口,调用一个接收者为值类型的方法时,Go 首先会创建这个指针的副本,然后将这个指针解引用,再作为接收者参数传递给该方法。这两个指针指向相同的地址,所以它们传递给方法的接收者参数都是相同的:

type Inter interface {
    foo()
}
type S struct {}
func (s S) foo() {} // 接收者为值类型的方法

var a Inter = &S{} // 使用结构体指针初始化一个接口
a.foo() // 调用 foo 方法

// 实际上底层是这样的:
// 首先拷贝 a 的底层值,即 `&S{}`,是一个结构体指针:
var b *S = a.inner_value // a、b 是不同的变量,但是指向同一个结构体
// 然后将 b 解引用,传递给 foo:
foo(*b) // *b 和 *(a.inner_value) 其实都表示同一个结构体

但是,当在一个值类型的结构体上,通过接口,调用一个接收者为指针类型的方法时,假设能够编译通过,将会出现下面这种情况:

type Inter interface {
    foo()
    bar()
}
type S struct {}
func (s *S) foo() { // 接收者为值类型的方法
    s.bar = 100 // 修改接收者的字段
}

var a Inter = S{1} // 声明一个值类型的结构体
a.foo() // 调用 foo 方法

// 如果允许编译通过:
// 首先拷贝 a 的底层值,即一个结构体,存到一个临时变量 b 里:
var b S = a.inner_value // a、b 是不同的变量,指向不同的结构体
// 然后将 b 的地址传递给 foo:
foo(&b) // foo 实际上修改的是临时变量 b 的字段
b.ar == 100 // true
a.bar == 100 // false!

我们在通过值类型调用 foo 方法的时候,明明代码里修改了接收者的某个字段的值,实际上却完全没有生效。这显然与我们的预期不符,因此在值类型上调用指针接收者方法不会编译成功。

总结:

— 值接收者 指针接收者
值类型 ✅ ❌
指针类型 ✅ ✅

行表示方法的接收者类型,列表示结构体变量类型,符号表示编译是否通过。