文章目录
- GoLang之方法调用系列一
- 1.方法
- 2.值接收者
- 3.指针接收者
- 4.更甜的语法糖
- 5.Method Expression&Method Value
- 5.1介绍
- 5.2方法表达式
- 5.3方法变量(作为局部变量)
- 5.4方法变量(作为返回值)
GoLang之方法调用系列一
1.方法
方法即method,Go语言支持为自定义类型实现方法,method在具体实现上与普通的函数并无不同,只不过会通过运行时栈多传递一个隐含的参数,这个隐含的参数就是所谓的接收者。下面通过代码来进行说明:
type A struct {
name string
}
func (a A) Name() string {
a.name = "Hi! " + a.name
return a.name
}
func main() {
a := A{name: "eggo"}
// 1)编译器的语法糖,提供面向对象的语法
fmt.Println(a.Name())
// 2)更贴近真实实现的写法,和普通函数调用几乎没什么不同
fmt.Println(A.Name(a))
}
以上代码展示了两种不同的写法,都能顺利通过编译并正常运行,实际上这两种写法会生成同样的机器码。
第一种:a.Name(),这是我们惯用的写法,很方便;
第二种:A.Name(a),这种写法更底层也更严谨,要求所有的类型必须严格对应,否则是无法通过编译的。
其实编译器会帮我们把第一种转换为第二种的形式,所以我们惯用的第一种写法只是“语法糖”,方便而已。
深入理解这两种写法的等价性是非常重要的,下面再用代码进一步验证:
type A struct {
name string
}
func (a A) Name() string {
a.name = "Hi! " + a.name
return a.name
}
func NameOfA(a A) string {
a.name = "Hi! " + a.name
return a.name
}
func main() {
t1 := reflect.TypeOf(A.Name)
t2 := reflect.TypeOf(NameOfA)
// 会输出true,通过反射来验证,两者的类型是相同的
fmt.Println(t1 == t2)
}
因为Go语言反射获取的函数类型只跟参数和返回值有关,既然t1和t2相等,就说明类型A的方法本质上和函数NameOfA相同。也就进一步验证了:方法本质上就是普通的函数,而接收者就是隐含的第一个参数。
2.值接收者
接下来,我们看看上面第一个示例中,a.Name()执行时函数调用栈是什么情况。
main函数栈帧中局部变量a只有一个string类型的成员,a.Name()会由编译器转换为A.Name(a)这样的函数调用。局部变量a作为要传入的参数,被直接拷贝到参数空间。
A.Name(a)执行时,修改的是参数空间的a.name,string底层指向的字符串内容发生了变化。
函数返回前将返回值写入返回值空间,对应到这个例子,就是拷贝参数a的成员name到返回值空间。
通过值接收者调用方法时,值接收者会作为第一个参数,而Go语言中传参都是值拷贝,所以执行a.Name()修改的并不是局部变量a,而是拷贝过去的参数。要想修改a,还得用指针接收者。
3.指针接收者
我们把上个例子改为指针接收者,然后看看通过指针接收者调用方法时,函数调用栈又会是怎样的情况。
type A struct {
name string
}
func (pa *A) Name() string {
pa.name = "Hi! " + pa.name
return pa.name
}
func main() {
a := A{name: "eggo"}
pa := &a
fmt.Println(pa.Name())
}
main函数栈帧有两个局部变量,pa存储的是a的地址。pa.Name()会由编译器转换为(*A).Name(pa)函数调用,所以参数空间拷贝参数pa的值,也就是局部变量a的地址。
(*A).Name(pa)执行时,修改的是pa指向的结构体,也就是局部变量a.name的值。这个string类型的成员会指向新的底层字符串,而返回值空间被写入的也是pa指向的结构体的成员name。
通过指针类型接收者调用方法时,指针会作为参数传入,传参时拷贝的就是地址,所以这里能够实现对原来变量a的修改。
4.更甜的语法糖
再次修改上面的例子,这一次既有值接收者的方法,又有指针接收者的方法。对于a.GetName()和pa.SetName()这两种形式的方法调用我们已经了然。但是下面这pa.GetName()和a.SetName()也能正常执行是几个意思?
type A struct {
name string
}
func (a A) GetName() string {
return a.name
}
func (pa *A) SetName() string {
pa.name = "Hi! " + pa.name
return pa.name
}
func main() {
a := A{name: "eggo"}
pa := &a
fmt.Println(pa.GetName())
fmt.Println(a.SetName())
}
不用展开函数调用栈,只需告诉你这是语法糖,理解起来也就没有问题了。编译期间,会把pa.GetName()这种方法调用转换成(*pa).GetName(),也就等价于执行A.GetName(*pa)。而a.SetName()会被转换成(&a).SetName(),也相当于执行(*A).SetName(&a)。所以,看似值接收者和指针接收者交叉访问了对方的方法,实际上依然遵循之前介绍的执行过程。
注:“如果定义的方法不涉及到任何接口类型时是这样的,详细情况以后详述,目前这样理解无碍。”
既然这种语法糖是在编译期间发挥作用的,像下面这种编译期间不能拿到地址的字面量,就不能享受语法糖,转换成对应的指针接收者调用了。
func main() {
fmt.Println((A{name: "eggo"}).SetName())
}
编译期间会发生错误:
cannot call pointer method on A literal
cannot take the address of A literal
错误: 进程退出代码 2.
5.Method Expression&Method Value
5.1介绍
我们已经知道,Go语言中函数作为变量、参数和返回值时,都是以Function Value的形式存在的。也知道闭包只是有捕获列表(catch list)的Funtion Value而已。
那么如果把方法赋给一个变量,这个变量又是怎样的存在呢?
type A struct {
name string
}
func (a A) GetName() string {
return a.name
}
func main(){
a := A{name:"eggo"}
f1 := A.GetName //方法表达式
f1(a) //eggo
f2 := a.GetName //方法变量
f2() //eggo
}
5.2方法表达式
如果像f1这样,把一个类型的方法赋给它,这样的变量就被称为“方法表达式”。对f1的处理相当于下面这段代码:
......
func GetName(a A) string{
return a.name
}
func main(){
a := A{name:"eggo"}
f1 := GetName
f1(a)
}
所以,f1实际上就是一个普通的Function Value,执行时需要传入一个A类型的变量作为第一个参数。
5.3方法变量(作为局部变量)
然而,像f2这样,通过a.GetName进行赋值,这样的变量被称为“方法变量”。通过方法变量执行方法时,我们无需再传入方法接收者作为第一个参数,这是因为编译器替我们做了处理。方法变量也是一个Function Value,在这个例子中,编译阶段f2()会被转换为A.GetName(a)。但是这只是方法变量作为局部变量的情况。
5.4方法变量(作为返回值)
如果像下面的GetFunc函数这样,把方法变量作为返回值。这个返回值实际上是一个捕获了局部变量a的Function Value,也就是说f3是一个闭包对象。
......
func GetFunc() func() string {
a := A{name: "eggo in GetFunc"}
return a.GetName
}
func main() {
a := A{name: "eggo in main"}
f2 := a.GetName
fmt.Println(f2()) //这里输出:eggo in main
f3 := GetFunc()
fmt.Println(f3()) //这里输出:eggo in GetFunc
}
上面的GetFunc函数和下面这段代码是等价的,通过它我们能够清晰地看到闭包是如何形成的。
func GetFunc() (func()string) {
a := A{name:"eggo in GetFunc"}
return func()string{
return A.GetName(a) //捕获变量a
}
}
f3是一个闭包对象,它执行时用到的是自己捕获的变量,也就是函数GetFunc的局部变量a。而f2这个方法变量,使用的是main函数的局部变量a。这样就很好理解上面这段示例程序的输出结果了。