文章目录

  • 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),这种写法更底层也更严谨,要求所有的类型必须严格对应,否则是无法通过编译的。
其实编译器会帮我们把第一种转换为第二种的形式,所以我们惯用的第一种写法只是“语法糖”,方便而已。

golang 调用对方 API restful接口 实例 golang 方法调用_开发语言

深入理解这两种写法的等价性是非常重要的,下面再用代码进一步验证:

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相同。也就进一步验证了:方法本质上就是普通的函数,而接收者就是隐含的第一个参数。

golang 调用对方 API restful接口 实例 golang 方法调用_后端_02

2.值接收者

接下来,我们看看上面第一个示例中,a.Name()执行时函数调用栈是什么情况。
main函数栈帧中局部变量a只有一个string类型的成员,a.Name()会由编译器转换为A.Name(a)这样的函数调用。局部变量a作为要传入的参数,被直接拷贝到参数空间。

golang 调用对方 API restful接口 实例 golang 方法调用_后端_03

A.Name(a)执行时,修改的是参数空间的a.name,string底层指向的字符串内容发生了变化。

golang 调用对方 API restful接口 实例 golang 方法调用_语法糖_04

函数返回前将返回值写入返回值空间,对应到这个例子,就是拷贝参数a的成员name到返回值空间。

golang 调用对方 API restful接口 实例 golang 方法调用_局部变量_05

通过值接收者调用方法时,值接收者会作为第一个参数,而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的地址。

golang 调用对方 API restful接口 实例 golang 方法调用_语法糖_06

(*A).Name(pa)执行时,修改的是pa指向的结构体,也就是局部变量a.name的值。这个string类型的成员会指向新的底层字符串,而返回值空间被写入的也是pa指向的结构体的成员name。

golang 调用对方 API restful接口 实例 golang 方法调用_golang_07

通过指针类型接收者调用方法时,指针会作为参数传入,传参时拷贝的就是地址,所以这里能够实现对原来变量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)。所以,看似值接收者和指针接收者交叉访问了对方的方法,实际上依然遵循之前介绍的执行过程。
注:“如果定义的方法不涉及到任何接口类型时是这样的,详细情况以后详述,目前这样理解无碍。”

golang 调用对方 API restful接口 实例 golang 方法调用_后端_08

既然这种语法糖是在编译期间发挥作用的,像下面这种编译期间不能拿到地址的字面量,就不能享受语法糖,转换成对应的指针接收者调用了。

func main() {
    fmt.Println((A{name: "eggo"}).SetName())
}

编译期间会发生错误:

cannot call pointer method on A literal
cannot take the address of A literal
错误: 进程退出代码 2.

golang 调用对方 API restful接口 实例 golang 方法调用_后端_09

5.Method Expression&Method Value

5.1介绍

我们已经知道,Go语言中函数作为变量、参数和返回值时,都是以Function Value的形式存在的。也知道闭包只是有捕获列表(catch list)的Funtion Value而已。

golang 调用对方 API restful接口 实例 golang 方法调用_golang_10

那么如果把方法赋给一个变量,这个变量又是怎样的存在呢?

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类型的变量作为第一个参数。

golang 调用对方 API restful接口 实例 golang 方法调用_后端_11

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。这样就很好理解上面这段示例程序的输出结果了。

golang 调用对方 API restful接口 实例 golang 方法调用_golang_12