深入理解Go语言中的方法
Go语言中是没有类对象的,所以也就没有继承、虚函数、构造函数和析构函数、隐藏的this指针等诸多OOP方面的东西。与之相似的是结构体struct。所以 struct接收器的功能是实现go方法的方法。那么struct到底是一种怎样的存在呢?
什么是方法
Go 语言中同时有函数和方法。方法(method)就是一个包含了接受者(receiver)的函数,receiver可以是内置类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。方法可以访问其所属的接收器的属性。形式如下:
func (r Type) functionName(...Type) Type {
...
}
当调用方法时,会将receiver作为函数的第一个参数(这有点儿像OOP中的this、python中的self等):
funcName(r, parameters);
另外,函数和方法之间的主要区别是许多方法可以具有相同的名称,而在包中不能定义具有相同名称的两个函数。
指针类型的接收者
使用指针类型的接收者作为参数定义如下,示例如下:
func (r *Type) methodName(...Type) Type { ... }
package main
import "fmt"
type Employee struct {
name string
salary int
}
func (e *Employee) changeName(newName string) {
(*e).name = newName
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
}
// e before name change
fmt.Println("e before name change =", e)
// create pointer to `e`
ep := &e
// change name
ep.changeName("Monica Geller")
// e after name change
fmt.Println("e after name change =", e)
}
在上面的例子中,使用了*来定义接收指针的接收器。 现在方法changeName将是接收指针类型的接收器,也即e的原始值。 在方法内部,我们使用*将接收器的指针转换为接收器的值,所以(* e)将是存储在存储器中的实际值。 因此,对其进行的任何更改都将反映在接收器结构的原始值中。所以,在语法上,changeName操作也可以这样写:(&e).changeName(“Monica Geller”)。
上面的例子看起来也容易理解。那么如果使用下面的语法形式来写呢?changeName方法是否会真正生效呢?
package main
import "fmt"
type Employee struct {
name string
salary int
}
func (e *Employee) changeName(newName string) {
e.name = newName
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
}
// e before name change
fmt.Println("e before name change =", e)
// change name
e.changeName("Monica Geller")
// e after name change
fmt.Println("e after name change =", e)
}
实际上它的效果和上面的例子是一样的,这是Go语言的一个约定俗称的捷径写法:如果方法的接收器是指针类型的,则不需要使用(* e)语法来引用指针或获取指针的值。 可以直接简单的使用e就行,Go编译器会理解你正在尝试对值本身执行操作,并且它将使转换为(* e)。同样,在调用指针类型接收器的方法时,直接按原值操作即可,Go编译器将理解并在底层传递指针。
值类型的接收者
当函数具有值类型的参数时,它将只接受参数的值。 如果您传递了一个指向该值的指针,将会无法运行。示例如下:
ackage main
import "fmt"
type Employee struct {
name string
salary int
}
func (e *Employee) changeName(newName string) {
e.name = newName
}
func (e Employee) showSalary() {
e.salary = 1500
fmt.Println("Salary of e =", e.salary)
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
}
// e before change
fmt.Println("e before change =", e)
// calling `changeName` pointer method on value
e.changeName("Monica Geller")
// calling `showSalary` value method on pointer
(&e).showSalary()
// e after change
fmt.Println("e after change =", e)
}
在上面的程序中,我们定义了接受指针的changeName方法,根据Go的捷径语法,直接调用e是合法的(Go编译器将理解并在底层传递指针)。 这里我们还定义了接受值的showSalary方法,但是传递了指针类型的接收者,当然这也是ok的,因为编译器将在底层将这个指针的值传递过去, 但是它并没有改变e的属性salary的值(即没有改变原始值,这里实际是值的副本)。
两者的区别与联系
总之,接收器是值类型还是指针类型要看该方法的作用。如果要修改对象的值,就需要传递对象的指针。指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。
通常来讲,即使不希望更改接收器的数据,也会使用指针类型接收器的方法,因为不会创建新的内存(值的副本拷贝会产生申请内存的开销)。
根据StackOverflow上《Value receiver vs. Pointer receiver in Golang?》 对这个问题的深入探讨,可以详细总结出两者的区别与联系,概述如下:
- 传值的方法可以通过指针或者值类型的接收器调用,而指针类型的接收器只能通过传指针调用;
- 如果接收器是map,func或chan,不要使用指针类型的,应该直接传值;
- 如果接收器是slice并且该方法不重新切片或重新分配切片,不要使用指针类型的;
- 如果该方法需要改变接收器,则接收器必须是指针;
- 如果接收器是包含sync.Mutex或类似同步字段的结构体,则接收器必须是指针类型以避免副本的失效;
- 如果接收器是比较大的结构体或数组,则指针类型的接收器更佳高效;
- 如果接收器是结构体,数组或切片,且其内部元素指向可变的指针时,使用指针类型的接收器更容易理解;
- 如果接收器是一个小型数组或结构体等简单类型(例如int或string)且没有可变字段和指针,使用值类型的更合理。
无结构体类型的方法
上面讲的例子都是有struct类型的方法,但是从方法的定义来看,只要类型定义和方法定义在同一个包中,它就可以接受任何类型作为接收器。 使用内建类型作为接收器的方法,可以如下使用:
package main
import (
"fmt"
"strings"
)
type MyString string
func (s MyString) toUpperCase() string {
normalString := string(s)
return strings.ToUpper(normalString)
}
func main() {
str := MyString("Hello World")
fmt.Println(str.toUpperCase())
}
匿名组合
Go语言提供了继承,但是采用了组合的语法,我们将其称为匿名组合,例如:
type Base struct {
name string
}
func (base *Base) Set(myname string) {
base.name = myname
}
func (base *Base) Get() string {
return base.name
}
type Derived struct {
Base
age int
}
func (derived *Derived) Get() (nm string, ag int) {
return derived.name, derived.age
}
func main() {
b := &Derived{}
b.Set("sina")
fmt.Println(b.Get())
}
在上面的例子中,在Base类型定义了get()和set()两个方法,而Derived类型继承了Base类,并改写了Get()方法,在Derived对象调用Set()方法,会加载基类对应的方法;而调用Get()方法时,加载派生类改写的方法。这个在OOP上都是相似的,容易理解。
但是,当组合的类型和被组合的类型包含相同名称的成员时,会发生什么情况呢?参考下面的例子:
type Base struct {
name string
age int
}
func (base *Base) Set(myname string, myage int) {
base.name = myname
base.age = myage
}
type Derived struct {
Base
name string
}
func main() {
b := &Derived{}
b.Set("sina", 30)
fmt.Println("b.name =",b.name, "\tb.Base.name =", b.Base.name)
fmt.Println("b.age =",b.age, "\tb.Base.age =", b.Base.age)
}
//
// b.name = b.Base.name = sina
// b.age = 30 b.Base.age = 30
这个例子,其实也是容易理解的。因为派生类(Derived)并没有Set方法,所以他会继承Base()的Set方法。
参考资料:
Go 语言中的方法,接口和嵌入类型: https://studygolang.com/articles/2935
GoLang之方法与接口: https://www.cnblogs.com/chenny7/p/4497969.html
Anatomy of methods in Go: https://medium.com/rungo/anatomy-of-methods-in-go-f552aaa8ac4a