持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情
1.指针或值方法
鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。
如果想要方法改变接收器的数据,就在接收器的指针类型上定义该方法;否则,就在普通的值类型上定义方法;分别叫做指针方法,值方法。
下面声明一个 T 类型的变量,并调用其方法 M1() 和 M2() 。
package main
import (
"fmt"
)
type T struct {
Name string
}
func (t T) M1() {
t.Name = "name1"
}
func (t *T) M2() {
t.Name = "name2"
}
func main() {
t1 := T{"t1"}
fmt.Println("M1调用前:", t1.Name)
t1.M1()
fmt.Println("M1调用后:", t1.Name)
fmt.Println("M2调用前:", t1.Name)
t1.M2()
fmt.Println("M2调用后:", t1.Name)
}
可见,t1.M2()修改了接收器数据。
分析:
我们姑且认为调用 t1.M1() 时相当于 M1(t1) ,实参和行参都是类型 T,可以接受。此时在M1()中的t只是t1的值拷贝,所以M1()的修改影响不到t1。
同上, t1.M2() => M2(t1),这是将 T 类型传给了 *T 类型,go可能会取 t1 的地址传进去: M2(&t1)。所以 M2() 的修改可以影响 t1 。
上面的例子同时也说明了:
T 类型的变量可以调用这两个方法。
因为对于类型 T,如果在 *T 上存在方法 Meth(),并且 t 是这个类型的变量,那么 t.Meth() 会被自动转换为 (&t).Meth()。
下面声明一个 *T 类型的变量,并调用方法 M1() 和 M2() 。
package main
import (
"fmt"
)
type T struct {
Name string
}
func (t T) M1() {
t.Name = "name1"
}
func (t *T) M2() {
t.Name = "name2"
}
func main() {
t2 := &T{"t2"}
fmt.Println("M1调用前:", t2.Name)
t2.M1()
fmt.Println("M1调用后:", t2.Name)
fmt.Println("M2调用前:", t2.Name)
t2.M2()
fmt.Println("M2调用后:", t2.Name)
}
分析:
t2.M1() => M1(t2), t2 是指针类型, 取 t2 的值并拷贝一份传给 M1。
t2.M2() => M2(t2),都是指针类型,不需要转换。
*T 类型的变量也可以调用这两个方法。
从上面我们可以得知:无论你声明方法的接收器是指针接收器还是值接收器,Go都可以帮你隐式转换为正确的方法使用。
但我们需要记住,值变量只拥有值方法,而指针变量则同时拥有值方法和指针方法。
无论是T类型变量还是*T类型变量,都可调用值方法或指针方法。但如果是接口变量呢,那么这两个方法都可以调用吗?
我们添加一个接口看看:
package main
type T struct {
Name string
}
type Intf interface {
M1()
M2()
}
func (t T) M1() {
t.Name = "name1"
}
func (t *T) M2() {
t.Name = "name2"
}
func main() {
var t1 T = T{"t1"}
t1.M1()
t1.M2()
var t2 Intf = t1
t2.M1()
t2.M2()
}
编译不通过:
cannot use t1 (type T) as type Intf in assignment: T does not implement Intf (M2 method has pointer receiver)
上面代码中我们看到,var t2 Intf 中,t2是Intf接口类型变量,t1是T类型值变量。上面错误信息中已经明确了T没有实现接口Intf,所以不能直接赋值。这是为什么呢?
首先这是Go语言的一种规则,具体如下:
- 规则一:如果使用指针方法来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。
- 规则二:如果使用值方法来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。
按照上面两条规则的规则一,我们稍微修改下代码:
package main
type T struct {
Name string
}
type Intf interface {
M1()
M2()
}
func (t T) M1() {
t.Name = "name1"
}
func (t *T) M2() {
t.Name = "name2"
}
func main() {
var t1 T = T{"t1"}
t1.M1()
t1.M2()
var t2 Intf = &t1
t2.M1()
t2.M2()
}
程序编译通过。
综合起来,接口类型的变量(实现了该接口)调用方法时,我们需要注意方法的接收器,是不是真正实现了接口。结合接口类型断言,我们做下测试:
package main
import (
"fmt"
)
type T struct {
Name string
}
type Intf interface {
M1()
M2()
}
func (t T) M1() {
t.Name = "name1"
fmt.Println("M1")
}
func (t *T) M2() {
t.Name = "name2"
fmt.Println("M2")
}
func main() {
var t1 T = T{"t1"}
// interface{}(t1) 先转为空接口,再使用接口断言
_, ok1 := interface{}(t1).(Intf)
fmt.Println("t1 => Intf", ok1)
_, ok2 := interface{}(t1).(T)
fmt.Println("t1 => T", ok2)
t1.M1()
t1.M2()
_, ok3 := interface{}(t1).(*T)
fmt.Println("t1 => *T", ok3)
t1.M1()
t1.M2()
T, ok4 := interface{}(&t1).(Intf)
fmt.Println("&t1 => Intf", ok4)
t.M1()
t.M2()
_, ok5 := interface{}(&t1).(T)
fmt.Println("&t1 => T", ok5)
_, ok6 := interface{}(&t1).(*T)
fmt.Println("&t1 => *T", ok6)
t1.M1()
t1.M2()
}
执行结果表明,t1 没有实现Intf方法集,不是Intf接口类型;而&t1 则实现了Intf方法集,是Intf接口类型,可以调用相应方法。而t1 这个结构体值变量本身则调用值方法或者指针方法都是可以的。
按照上面的两条规则,那究竟怎么选择是指针接收器还是值接收器呢?
- 何时使用值类型
1.如果接收器是一个 map,func 或者 chan,使用值类型(因为它们本身就是引用类型)。
2.如果接收器是一个 slice,并且方法不执行 reslice 操作,也不重新分配内存给 slice,使用值类型。
3.如果接收器是一个小的数组或者原生的值类型结构体类型(比如 time.Time 类型),而且没有可修改的字段和指针,又或者接收器是一个简单地基本类型像是 int 和 string,使用值类型就好了。
一个值类型的接收器可以减少一定数量的垃圾生成,如果一个值被传入一个值类型接收器的方法,一个栈上的拷贝会替代在堆上分配内存(但不是保证一定成功),所以在没搞明白代码想干什么之前,别因为这个原因而选择值类型接收器。
- 何时使用指针类型
1.如果方法需要修改接收器,接收器必须是指针类型。
2.如果接收器是一个包含了 sync.Mutex 或者类似同步字段的结构体,接收器必须是指针,这样可以避免拷贝。
3.如果接收器是一个大的结构体或者数组,那么指针类型接收器更有效率。(多大算大呢?假设把接收器的所有元素作为参数传给方法,如果你觉得参数有点多,那么它就是大)。
4.从此方法中并发的调用函数和方法时,接收器可以被修改吗?一个值类型的接收器当方法调用时会创建一份拷贝,所以外部的修改不能作用到这个接收器上。如果修改必须被原始的接收器可见,那么接收器必须是指针类型。
5.如果接收器是一个结构体,数组或者 slice,它们中任意一个元素是指针类型而且可能被修改,建议使用指针类型接收器,这样会增加程序的可读性
当你看完这个还是有疑虑,还是不知道该使用哪种接收器,那么记住使用指针接收器。
2.内嵌类型的方法提升
当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型继承了这些方法:将父类型放在子类型中来实现。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果。
当我们嵌入一个类型,这个类型的方法就变成了外部类型的方法,但是当它被调用时,方法的接收器是内部类型(嵌入类型),而非外部类型。
type People struct {
Age int
gender string
Name string
}
type OtherPeople struct {
People
}
func (p People) PeInfo() {
fmt.Println("People ", p.Name, ": ", p.Age, "岁, 性别:", p.gender)
}
因此嵌入类型的名字充当着字段名,同时嵌入类型作为内部类型存在,我们可以使用下面的调用方法:
OtherPeople.People.PeInfo()
这儿我们通过类型名称来访问内部类型的字段和方法。然而,这些字段和方法也同样被提升到了外部类型:
OtherPeople.PeInfo()
Go 语言中内嵌类型方法集提升的规则:
给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:
-
如果 S 包含一个匿名字段 T,S 和 *S 的方法集都包含接收器为 T 的方法提升
这条规则说的是当我们嵌入一个类型,嵌入类型的接收器为值类型的方法将被提升,可以被外部类型的值和指针调用。
-
如果 S 包含一个匿名字段 T, *S 类型的方法集包含接收器为 *T 的方法提升
这条规则说的是当我们嵌入一个类型,可以被外部类型的指针调用的方法集只有嵌入类型的接收器为指针类型的方法集,也就是说,当外部类型使用指针调用内部类型的方法时,只有接收器为指针类型的内部类型方法集将被提升。
-
如果 S 包含一个匿名字段 *T,S 和 *S 的方法集都包含接收器为 T 或者 *T 的方法提升
这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接收器为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
这就是语言规范里方法提升中仅有的三条规则,根据这个推导出一条规则:
- 如果 S 包含一个匿名字段 T,S 的方法集不包含接收器为 *T 的方法提升。
这条规则说的是当我们嵌入一个类型,嵌入类型的接收器为指针的方法将不能被外部类型的值访问。这也是跟我们陈述的接口规则一致。
简单地说也是两条规则:
规则一:如果S包含嵌入字段T,则S和*S的方法集都包括具有接收方T的提升方法。*S的方法集还包括具有接收方*T的提升方法。
规则二:如果S包含嵌入字段*T,则S和*S的方法集都包括具有接收器T或*T的提升方法。
注意:以上规则由于 t.Meth() 会被自动转换为 (&t).Meth() 这个语法糖,导致我们很容易误解上面的规则不起作用,而实际上规则是有效的。
代码验证:
package main
import (
"fmt"
"reflect"
)
type People struct {
Age int
gender string
Name string
}
type OtherPeople struct {
People
}
type NewPeople People
func (p *NewPeople) PeName(pname string) {
fmt.Println("pold name:", p.Name)
p.Name = pname
fmt.Println("pnew name:", p.Name)
}
func (p NewPeople) PeInfo() {
fmt.Println("NewPeople ", p.Name, ": ", p.Age, "岁, 性别:", p.gender)
}
func (p *People) PeName(pname string) {
fmt.Println("old name:", p.Name)
p.Name = pname
fmt.Println("new name:", p.Name)
}
func (p People) PeInfo() {
fmt.Println("People ", p.Name, ": ", p.Age, "岁, 性别:", p.gender)
}
func methodSet(a interface{}) {
t := reflect.TypeOf(a)
for i, n := 0, t.NumMethod(); i < n; i++ {
m := t.Method(i)
fmt.Println(m.Name, m.Type)
}
}
func main() {
p := OtherPeople{People{26, "Male", "张三"}}
p.PeInfo()
p.PeName("Joke")
methodSet(p) // T方法提升
methodSet(&p) // *T和T方法提升
pp := NewPeople{42, "Male", "李四"}
pp.PeInfo()
pp.PeName("Haw")
methodSet(&pp)
}
我们可以从上面输出看到,*OtherPeople 下有两个方法,而OtherPeople只有一个方法。但是在Go中存在一个语法糖,比如上面代码:
p.PeInfo() p.PeName("Joke") methodSet(p) // T方法提升
虽然P 只有一个方法:PeInfo func(main.OtherPeople),但我们依然可以调用p.PeName(“Joke”)。这里Go自动转为(&p).PeName(“Joke”),其调用后结果让我们以为p有两个方法,其实这里p只有一个方法。内嵌方法集的提升