作者:龚magnett_672 | 来源:互联网 | 2023-01-15 09:25
golang关于valuereceiver和pointerreceiver-关于valuereceiver和pointerreceivervaluereceiver其实说的主要是
关于value receiver和pointer receiver
value receiver其实说的主要是基于结构体的方法实现,pointer receiver说的是基于结构体指针的实现,
我们可以先用例子开始说起:
package main
import "fmt"
// Person ...
type Person interface {
SetAge()
SetName()
}
// Teacher ...
type Teacher struct {
Name string
Age int
}
// SetAge ...
func (p *Teacher) SetAge() {
p.Age = 24
}
// SetName ...
func (p *Teacher) SetName() {
p.Name = "teacher"
}
// Student ...
type Student struct {
Name string
Age int
}
// SetAge ...
func (p Student) SetAge() {
p.Age = 16
}
// SetName ...
func (p Student) SetName() {
p.Name = "student"
}
func main() {
var p1 Person = &Teacher{}
p1.SetAge()
p1.SetAge()
fmt.Printf("teacher: %+v \n", p1) // teacher: &{Name: Age:24}
var p2 Person = Student{}
p2.SetAge()
p2.SetAge()
fmt.Printf("Student: %+v \n", p2) // Student: {Name: Age:0}
var p3 Person = &Student{}
p3.SetAge()
p3.SetAge()
fmt.Printf("Student: %+v \n", p3) // Student: {Name: Age:0}
var p4 Person = Teacher{}
p4.SetAge()
p4.SetAge()
fmt.Printf("teacher: %+v \n", p4) // cannot use Teacher literal (type Teacher) as type Person in assignment: Teacher does not implement Person (SetAge method has pointer receiver)
}
由此,我们可以得出一个结论如下:
| 结构体实现接口 | 结构体指针实现接口 |
---|
结构体初始化变量 | 通过 | 不通过 |
结构体指针初始化变量 | 通过 | 通过 |
其中,我们可以明确的知道有2种是肯定可以通过的,那就是:
- 结构体初始化的变量,和方法接收者也是结构体
- 结构体指针初始化的变量,和方法接受者也是结构体指针
接着, 结构体指针初始化的变量,方法接受者为结构体也可以编译通过,即上面的p3,
// Person ...
type Person interface {
SetAge()
SetName()
}
// Student ...
type Student struct {
Name string
Age int
}
// SetAge ...
func (p Student) SetAge() {
p.Age = 16
}
// SetName ...
func (p Student) SetName() {
p.Name = "student"
}
func main() {
var p3 Person = &Student{}
p3.SetAge()
p3.SetAge()
}
那么为什么可以编译通过呢? 首先,我们知道,golang中,对于结构体方法的调用,例如
func (p Student) SetAge() {
p.Age = 16
}
方法,编译器展开后,其实是等价于
func SetAge(p) {
p.Age = 16
}
而在go中,所有的参数传递都是值拷贝
,所以,当我们的初始化变量p为一个指针的时候,传递给SetAge方法的也是一个指针的值拷贝,这个拷贝的指针,和原来的指针指向的是同一个结构体,最终,我们的编译器可以隐式的通过解引用的方式,获取到这个指针指向的结构体。
那为什么反过来就不行呢?即:
type Person interface {
SetAge()
SetName()
}
type Student struct {
Name string
Age int
}
func (p *Student) SetAge() {
p.Age = 16
}
func (p *Student) SetName() {
p.Name = "student"
}
func main() {
var p3 Person = Student{}
p3.SetAge()
p3.SetName()
}
这个时候,我们发现编译会报错:
// cannot use Teacher literal (type Teacher) as type Person in assignment: Teacher does not implement Person (SetAge method has pointer receiver)
带着这个问题,我们先来试试另外一种方式,我们把初始化的变量p3
定义为结构体类型Student
,而非接口类型Person
如下:
type Person interface {
SetAge()
SetName()
}
type Student struct {
Name string
Age int
}
func (p *Student) SetAge() {
p.Age = 16
}
func (p *Student) SetName() {
p.Name = "student"
}
func main() {
var p3 Student = Student{}
p3.SetAge()
p3.SetName()
fmt.Printf("p3: %+v \n", p3)
}
你会发现,编译成功,毫无压力的输出
p3: {Name:student Age:16}
为什么会这样子呢?查看我们golang的官方文档,我们看到这样的一段说明:
- This rule arises because pointer methods can modify the receiver; invoking them on a value would cause the method to receive a copy of the value, so any modifications would be discarded. The language therefore disallows this mistake. There is a handy exception, though. When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically. In our example, the variable b is addressable, so we can call its Write method with just b.Write. The compiler will rewrite that to (&b).Write for us.
- 这个规则的产生是因为指针方法可以修改接收者;对值调用它们将导致方法接收值的副本,因此任何修改都将被丢弃。语言不允许这种错误。不过,有一个例外。当值是可寻址的时,该语言通过自动插入address操作符来处理对值调用指针方法的常见情况。在我们的例子中,变量b是可寻址的,所以我们可以用b.Write调用它的Write方法。编译器将把它重写为(&b)。
所以,当我们var p3 Student = Student{}
把初始化的结构体Student赋值给变量类型为结构体类型Student
的时候,我们可以通过&p3隐式的获取到它的地址,从而实现方法的调用。
但是,当我们var p3 Person = Student{}
把初始化的结构体Student赋值给变量类型为接口Person
的时候,为什么编译器就报错了呢?
根据上面的文档,我们知道,当p3是可寻址的时候,编译器可以隐式的通过(&p3)获取到它的地址,那么,这里编译器报错的唯一原因,那就是类型为Person接口的p3是不可寻址的,或者说编译器本身就禁止了去获取接口类型p3的地址的这种行为
,从而导致边我们编译不通过,出现:
- cannot use Teacher literal (type Teacher) as type Person in assignment: Teacher does not implement Person (SetAge method has pointer receiver)
通过官方的文档我们确认了这一点:
The concrete value stored in an interface is not addressable
, in the same way that a map element is not addressable. Therefore, when you call a method on an interface, it must either have an identical receiver type or it must be directly discernible from the concrete type: pointer- and value-receiver methods can be called with pointers and values respectively, as you would expect. Value-receiver methods can be called with pointer values because they can be dereferenced first. Pointer-receiver methods cannot be called with values, however, because the value stored inside an interface has no address
. When assigning a value to an interface, the compiler ensures that all possible interface methods can actually be called on that value, and thus trying to make an improper assignment will fail on compilation.
那么,编译器为什么要这么做? 我们可以举个栗子:
type I interface{}
type A int
type B string
func main() {
var a A = 5
var i I = a
fmt.Printf("i is of type %T \n", i)
var aPtr *A
//aPtr = &(i.(A))
//fmt.Printf("%p \n", aPtr)
var b B = "hello"
i = b
fmt.Printf("i is of type %T, aPtr is of type %T \n", i, aPtr)
}
假如一个接口类型是可寻址的,即假如上面注释的aPtr = &(i.(A))
这行代码可以运行成功,那么,当我们把变量类型为B的字符串变量b赋值给i的时候,这个时候aPtr它指向的是啥?aPtr被声明为指向A,但i现在包含B,并且aPtr不再是指向A的有效指针(我的理解就是,你不能再基于类型A去使用aPtr这个指针,因为此时你已经不知道它指向的是什么类型)
所以,通篇下来,我们围绕的一个问题就是:
- 为什么一个包含非指针值的接口不能成为一个带有指针接收器的方法的接收器呢?
- 因为存储在接口中的非指针值是不可寻址的,所以编译器不能将其地址传递给带有指针接收器的方法。
参考的连接:
blog.golang.org/laws-of-ref…
colobu.com/2018/02/27/…
golang.org/ref/spec#Ad…
golang.org/doc/effecti…
github.com/golang/go/w…
stackoverflow.com/questions/4…
stackoverflow.com/questions/4…
stackoverflow.com/questions/3…
colobu.com/2018/02/27/…