深入理解Go语言中的方法

郭遗欢 18/08/28 17:04:52

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?》 对这个问题的深入探讨,可以详细总结出两者的区别与联系,概述如下:

  1. 传值的方法可以通过指针或者值类型的接收器调用,而指针类型的接收器只能通过传指针调用;
  2. 如果接收器是map,func或chan,不要使用指针类型的,应该直接传值;
  3. 如果接收器是slice并且该方法不重新切片或重新分配切片,不要使用指针类型的;
  4. 如果该方法需要改变接收器,则接收器必须是指针;
  5. 如果接收器是包含sync.Mutex或类似同步字段的结构体,则接收器必须是指针类型以避免副本的失效;
  6. 如果接收器是比较大的结构体或数组,则指针类型的接收器更佳高效;
  7. 如果接收器是结构体,数组或切片,且其内部元素指向可变的指针时,使用指针类型的接收器更容易理解;
  8. 如果接收器是一个小型数组或结构体等简单类型(例如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