方法
参考阅读:【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) 执行时候,改变的是参数空间 a 的 string 成员 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,pa。pa 是存储变量 a 的地址。pa.Name() 会由编译器转换为(*A).Name(pa) 函数调用,所以参数空间拷贝参数 pa 的值,也就是局部变量 a 的地址。
当函数 (*A).Name(pa) 执行时候,会新建一个字符串,然后参数 &a 的 string 类型成员会重新指向这个新的字符串,然后值拷贝 string 类型的成员到返回值空间,所以会输出 Hi! eggo。
因为参数拷贝的是 pa 也就是 a的地址,所以会这里会改变 a 的 string 类型成员地址指向的 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。这样就很好理解上面这段示例程序的输出结果了。