Go语言精进之路读书笔记第23条——理解方法的本质以选择正确的receiver类型
和函数相比,Go语言中的方法在声明形式上仅仅多了一个参数,Go称之为receiver参数。receiver参数是方法与类型之间的纽带。
Go方法特点:
- 方法名的首字母是否大写决定了该方法是不是导出方法。
- 方法定义要与类型定义放在同一个包内。由此可以推出,不能为原生类型(如int/float64/map等)添加方法,只能为自定义类型定义方法。不能横跨Go包为其他包内的自定义类型定义方法。
- 每个方法只能有一个receiver参数,不支持多receiver参数列表,不支持变长receiver参数,不支持同时绑定多个类型。
- receiver参数的基类型本身不能是指针类型或接口类型
23.1 方法的本质
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(a int) int {
t.a = a
return t.a
}
// 等价转换,将receiver作为第一个参数传入方法的参数列表,转换之后就是方法的原型
func Get(t T) int {
return t.a
}
func Set(t *T, a int) int {
t.a = a
return t.a
}
// 使用方法
var t T
t.Get()
t.Set(1)
// 等价替换为
var t T
T.Get(t)
(*T).Set(&t, 1)
直接以类型名T调用方法的表达方式被称为方法表达式(Method Expression)
Go方法的本质:一个以方法所绑定类型实例为第一个参数的普通函数
23.2 选择正确的receiver类型
func (t T) M1() <=> M1(t T)
func (t *T) M2() <=> M2(t *T)
Go函数的参数采用的是值复制传递,M1函数体中的t是T类型实例的一个副本,M2函数体中的t是T类型实例的地址
无论是T类型实例还是*T类型实例,都可以调用receiver为T类型或*T类型的方法。实际上是Go语法糖,Go编译器在编译和生成代码时为我们做了自动转换
初步结论:
- 如果要对类型实例进行修改,使用*T类型
- 如果没有修改的需求,使用T类型和*T类型均可。如果考虑Go方法调用时,receiver是以值复制的形式传入方法中的,选择*T类型会减少损耗
23.3 基于对Go方法本质的理解巧解难题
- data1输出:one two tree
- 迭代data1时,由于data1中的元素类型是field指针(*field),因此赋值后v就是元素地址,每次调用print时传入的参数(v)实际上也是各个field元素的地址
- data2输出:six six six
- 迭代data2时,由于data2中的元素类型是field(非指针),需要将其取地址后再传入,这样每次调用print时传入的参数(&v)实际上是变量v的地址,而不是切片data2中各元素的地址
- 在整个for range过程中v只有一个,因此data2迭代完成之后,v是元素“six”的副本
这个问题和Go语言精进之路读书笔记第19条——理解Go语言表达式的求值顺序中19.2 for range的避"坑"指南的1.迭代变量的重用还是不太一样
- 19条中的是闭包函数使用了外层的变量,而外层变量只有一份,导致最终输出的结果相同
- 上面的问题是调用print时传入的参数实际内容不一样,data1中传入的是各个field元素的地址,每次循环都不一样;而data2中传入的是变量v的地址,每次循环都一样
type field1 struct {
name string
}
func (p *field1) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field1{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field1{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}