【Go 原理】结构体方法:值接收者与指针接收者的区别
优质
小牛编辑
125浏览
2023-12-01
当结构体实现一个接口时,可以在方法中设置一个接收者,比如对于以下接口:
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()
那么调用接口方法的组合实际有四种情况:
- 值类型结构体 -> 赋值给接口 -> 调用接受者类型为值类型的结构体方法
- 指针类型结构体 -> 赋值给接口 -> 调用接受者类型为指针类型的结构体方法
- 值类型结构体 -> 赋值给接口 -> 调用接受者类型为指针类型的结构体方法
- 指针类型结构体 -> 赋值给接口 -> 调用接受者类型为值类型的结构体方法
这四种不同情况只有一种会发生编译不通过的问题,即:结构体类型为值类型、调用了接收者为指针的方法(上面第 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
方法的时候,明明代码里修改了接收者的某个字段的值,实际上却完全没有生效。这显然与我们的预期不符,因此在值类型上调用指针接收者方法不会编译成功。
总结:
— | 值接收者 | 指针接收者 |
---|---|---|
值类型 | ✅ | ❌ |
指针类型 | ✅ | ✅ |
行表示方法的接收者类型,列表示结构体变量类型,符号表示编译是否通过。