【Go基础】方法

219 阅读5分钟

方法

参考阅读:【Golang】方法 Method

介绍

方法即 Method,只要你接触过面对对象思想的语言,都会了解。Go 语言支持为任意类型实现方法。

举个例子🌰:

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())//Hi! eggo
	// 2)更贴近真实实现的写法,和普通函数调用几乎没什么不同
	fmt.Println(A.Name(a))//Hi! eggo
}

上面有两种写法,都能顺利通过编译并且执行,实际上这两种写法会生成同样的机器码。

第一种:a.Name(),比较常规,方便。这是一种语法糖。

第二种:A.Name(a),是一种比较严谨写法的,更贴近于原始。

这两者是等价的,编译器会识别第一种,然后帮我们把第一种转变成第二种。以下是证据:

type A struct {
	name string
}
func (a A) Name() string {
	a.name = "Hi! " + a.name
	return a.name
}
func Name(a A) string {
	a.name = "Hi! " + a.name
	return a.name
}
func main() {
	t1 := reflect.TypeOf(A.Name)
	t2 := reflect.TypeOf(Name)
	// 会输出true,通过反射来验证,两者的类型是相同的
	fmt.Println(t1 == t2)//true
}

输出 True,代表两者类型一致。我们可以发现第二种方式将类型为 A 的变量 a,作为函数第一个参数引入,其余的部分都没有发生改变。说明:方法本质上其实一个普通的函数,只不过会将定义的变量 a 作为一个参数(隐含)。

值接收者

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())//Hi! eggo
	// 2)更贴近真实实现的写法,和普通函数调用几乎没什么不同
	fmt.Println(A.Name(a))//Hi! eggo
	fmt.Println(a.name)//eggo
}

既然和普通函数一致,那么我们分析一下他 a.Name() 的函数调用栈。

main 函数栈帧局部变量为 a 类型 A,有一个 string 类型的成员,所以他的数据会被放到数据段,在局部变量则是地址,a.Name() 会被编译器转化为 A.Name(a) 这样的函数调用。所以参数是 a,会值拷贝到参数空间。

当函数 A.Name(a) 执行时候,改变的是参数空间 astring 成员 name,那么数据段会重新建立一个字符串,将 name 的地址指向这个新的字符串。然后值拷贝成员到返回值空间。所以会输出 Hi! eggo

但是局部变量 a 并没有发生改变。因为作为值接收者去调用方法,传参都是值拷贝,改变的是参数 a,而不是局部变量 a。如果你想改变值,需要利用指针接收者。

指针接收者

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
    //注:这里函数调用了两次所以是两个 Hi!。
	fmt.Println(pa.Name()) //Hi! eggo
	fmt.Println((*A).Name(pa))//Hi! Hi! eggo
	fmt.Println(pa.name)//Hi! Hi! eggo
}

现在是指针接收者,那么函数调用栈又会是怎么样的呢?

main 函数调用栈有两个局部变量分别是 a,papa 是存储变量 a 的地址。pa.Name() 会由编译器转换为(*A).Name(pa) 函数调用,所以参数空间拷贝参数 pa 的值,也就是局部变量 a 的地址。

当函数 (*A).Name(pa) 执行时候,会新建一个字符串,然后参数 &a 的 string 类型成员会重新指向这个新的字符串,然后值拷贝 string 类型的成员到返回值空间,所以会输出 Hi! eggo

因为参数拷贝的是 pa 也就是 a的地址,所以会这里会改变 astring 类型成员地址指向的 string 结构体的值。

语法糖

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())//eggo
	fmt.Println(a.SetName())//Hi! eggo
	fmt.Println(a.name)//Hi! eggo
}

有没有感觉调用的很奇怪!调用者和方法都不太搭!

这里是语法糖,编译期间,会把 pa.GetName() 这种方法调用转换成 (*pa).GetName(),也就等价于执行 A.GetName(*pa)。而 a.SetName() 会被转换成 (&a).SetName(),也相当于执行(*A).SetName(&a)

但是这种语法糖不能用于字面量

fmt.Println((A{name: "eggo"}).SetName())
//报错:Cannot call a pointer method on '(A{name: "eggo"})'

这样是拿不到字面量的地址的,所以转化成指针接收者调用。

fmt.Println((A{name: "eggo"}).GetName())//不会报错,这里使用的是值接收者。

方法的 Function value

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      //方法表达式
	fmt.Println(f1(a))               //eggo

	f2 := a.GetName      //方法变量
	fmt.Println(f2())                  //eggo
}

如果像 f1 这样,把一个类型的方法赋值给他,这样的变量就被称为**“方法表达式”**。其实你可以这样理解:f1 被赋值的是类型 A 的函数,这个函数使用方法 A.Name(a)。所以 f1 的调用方式是传入一个 A 类型变量 a

你可以当作在使用 A.Name(a A) 这个函数。

图片

方法变量

f2 这样,通过 a.GetName 进行赋值,这样的变量被称为**“方法变量”**。通过方法变量执行方法时,我们无需再传入方法接收者作为第一个参数,这是因为编译器替我们做了处理,相当于 f2()==A.Name(a)

作为返回值

type A struct {
	name string
}
func (a A) GetName() string {
	return a.name
}
func GetFunc() func() string {
	a := A{name: "eggo in GetFunc"}
	return a.GetName
}
//等价于
/*
func GetFunc() func() string {
	a := A{name: "eggo in GetFunc"}
	return func() string {
		return a.GetName()
	}
}
*/
//进而等价于
/*
func GetFunc() func() string {
	a := A{name: "eggo in GetFunc"}
	return func() string {
		return A.GetName(a)
	}
}
*/
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
}

我们可以发现多次等价后,发现变量 a 是捕获变量,f3 是闭包对象。f2 这个方法变量,使用的是 main 函数的局部变量 a。这样就很好理解上面这段示例程序的输出结果了。

图片