值接收者和指针接收者#
指定了接收者的函数称为方法。其中接收者可以是值接收者Animal
, 也可以是指针接收者 *Animal
:
type Animal struct {
name string
}
func (a Animal) valueFn() {} // 值接收者
func (a *Animal) ptrFn() {} // 指针接收者
在调用方法的时候,调用者可以是该类型的值类型也可以是指针类型。
- 值接收者方法不管调用者是指类型还是指针类型,传递的始终是调用者的副本,方法内部不会对调用者本身做修改:
// eg1.
func (a Animal) valueFn() {
a.name = "new_" + a.name
}
func main() {
a := Animal{"cat"} // 值类型
a.valueFn()
println("this is name", a.name)
// => this is name cat
b := &Animal{"dog"} // 指针类型
b.valueFn()
println("this is name", b.name)
// => this is name dog
}
- 指针接收者方法传递的是调用者的引用,所以可以对调用者做修改:
// eg2.
func (a *Animal) ptrFn() {
a.name = "new_" + a.name
}
func main() {
a := Animal{"cat"} // 值类型
a.ptrFn()
println("this is name", a.name)
// => this is name new_cat
b := &Animal{"dog"} // 指针类型
b.ptrFn()
println("this is name", b.name)
// => this is name new_dog
}
这背后是编译器做了一些工作,如下表:
- | 值接收者 | 指针接收者 |
---|---|---|
值类型调用者 | 方法会使用调用者的一个副本,类似于 “传值” | 使用值的引用来调用方法,eg2 实际上是 (&a).ptrFn() |
指针类型调用者 | 指针被解引用为值,eg1 实际上是 (*b).valueFn() | 实际上也是 “传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针 |
类型的本质#
在声明一个新类型之后,声明一个该类型的方法之前,需要先回答一个问题:这个类型的本质是什么。如果给这个类型增加或者删除某个值,是要创建一个新值,还是要更改当前的值?如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。这个答案也会影响程序内部传递这个类型的值的方式:是按值做传递,还是按指针做传递。保持传递的一致性很重要。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。
是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。
—— 《Go 语言实战 5.3》
- 内置类型:数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型。因此,当对这些值进行增加或者删除的时候,会创建一个新值。基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。
- 原始类型:切片、映射、通道、接口和函数类型。后边没看懂 😅
- 结构类型:用来描述一组值。遵循如有修改就按指针传递。如果类型的值具备非原始的本质,就应该被共享,而不是被复制。即使方法没有修改接收者的值,依然要用指针接收者来声明的。
这一节没有看明白,大概是说对于非结构类型,应当参考标准库的习惯;对于结构类型,一般情况下如果不需要对调用者做修改,就按值传递,如果需要修改则按指针传递。
接口#
虽然对不同类型的调用者,值接收者和指针接收者方法都可以调用,但在接口的实现上略有不同,实现接收者是值类型的方法,会隐式地实现了接收者是指针类型的方法,而实现接收者是指针类型的方法时,不会自动实现接收者是值类型的方法。
// eg3. 编译通过
type IAnimal interface {
valueFn()
ptrFn()
}
func (a Animal) valueFn() {}
func (a *Animal) ptrFn() {}
func main() {
var a IAnimal = &Animal{"cat"}
a.valueFn()
a.ptrFn()
}
// eg4. 编译失败
func main() {
var a IAnimal = Animal{"cat"} // 改为值类型
a.valueFn()
a.ptrFn()
}
// => .\main.go:22:18: cannot use Animal{…} (value of type Animal) as IAnimal value in variable declaration: Animal does not implement IAnimal (method ptrFn has pointer receiver)
方法集#
要了解用指针接收者来实现接口时为什么 user 类型的值无法实现该接口,需要先了解方法集。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。
- 规范描述的方法集:
values | methods receivers |
---|---|
T | (t T) |
* T | (t T) and (t * T) |
意即 T 类型的值的方法集只包含值接收者声明的方法。而指向 T 类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。
- 接收者的角度来看方法集规范:
methods receivers | values |
---|---|
(t T) | T and * T |
(t * T) | * T |
这个视角是说,如果使用值接收者来实现一个接口,值类型值和指针都能够实现对应的接口。如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。
所以 eg4 中的值类型 Animal
会提示没有实现接口 IAnimal
。
嵌入类型#
嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
type Cat struct {
Animal
color string
}
func main() {
c := Cat{Animal{"miao"},"yellow"}
c.Animal.valueFn()
}
内部类型的属性和方法也可以被提升到外部类型直接访问:
func main() {
//...
c.valueFn()
println("this is c name", c.name)
}
参考#
- 《Go 语言实战》第五章
- 值接收者和指针接收者的区别 | Go 程序员面试笔试宝典