Go 语言方法

方法的定义

Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?

Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数

接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 intboolstring数组的别名类型。

但是接收者不能是一个接口类型,因为接口是一个抽象定义,但是方法却是具体实现,如果这样做会引发一个编译错误:

  1. invalid receiver type

接收者也不能是一个指针类型,但是它可以是任何其他允许类型的指针。

一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。

类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集(method set)

因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:

  1. func (a *denseMatrix) Add(b Matrix) Matrix
  2. func (a *sparseMatrix) Add(b Matrix) Matrix

发方法的定义格式:

  1. func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

方法名之前,func 关键字之后的括号中指定 receiver

如果 recvreceiver 的实例,Method1 是它的方法名,那么方法的调用方式是:recv.Method1()

如果 recv 是一个指针,Go 会自动解引用。

如果方法不需要使用 recv 的值,可以用 _ 替换它,比如:

  1. func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }

recv 就像是面向对象语言中的 thisself,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 thisself 作为 receiver 的名字。下面是一个结构体上的简单方法的例子:

  1. package main
  2. import "fmt"
  3. type TwoInts struct {
  4. a int
  5. b int
  6. }
  7. func main() {
  8. two1 := new(TwoInts)
  9. two1.a = 12
  10. two1.b = 10
  11. fmt.Printf("The sum is: %d\n", two1.AddThem())
  12. fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))
  13. two2 := TwoInts{3, 4}
  14. fmt.Printf("The sum is: %d\n", two2.AddThem())
  15. }
  16. func (tn *TwoInts) AddThem() int {
  17. return tn.a + tn.b
  18. }
  19. func (tn *TwoInts) AddToParam(param int) int {
  20. return tn.a + tn.b + param
  21. }

输出:

  1. The sum is: 22
  2. Add them to the param: 42
  3. The sum is: 7

下面是非结构体类型上方法的例子:

  1. package main
  2. import "fmt"
  3. type IntVector []int
  4. func (v IntVector) Sum() (s int) {
  5. for _, x := range v {
  6. s += x
  7. }
  8. return
  9. }
  10. func main() {
  11. fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6
  12. }

类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 intfloat 或类似这些的类型上定义方法。试图在 int 类型上定义方法会得到一个编译错误:

  1. cannot define new methods on non-local type int

比如,想在 time.Time 上定义如下方法:

  1. func (t time.Time) first3Chars() string {
  2. return time.LocalTime().String()[0:3]
  3. }

类型在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误。

但是有一个间接的方式:可以先定义该类型(比如:intfloat)的别名类型,然后再为别名类型定义方法。或者像下面的例子这样,将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. type myTime struct {
  7. time.Time //anonymous field
  8. }
  9. func (t myTime) first3Chars() string {
  10. return t.Time.String()[0:3]
  11. }
  12. func main() {
  13. m := myTime{time.Now()}
  14. // 调用匿名Time上的String方法
  15. fmt.Println("Full time now:", m.String())
  16. // 调用myTime.first3Chars
  17. fmt.Println("First 3 chars:", m.first3Chars())
  18. }

输出结果:

  1. Full time now: Mon Oct 24 15:34:54 Romance Daylight Time 2011
  2. First 3 chars: Mon

函数和方法的区别

函数将变量作为参数:Function1(recv)

方法在变量上被调用:recv.Method1()

在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。

不要忘记 Method1 后边的括号 (),否则会引发编译器错误:

  1. method recv.Method1 is not an expression, must be called

接收者必须有一个显式的名字,这个名字必须在方法中被使用。

receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。

Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样。这样耦合更加宽松,类型和方法之间的关联由接收者来建立。

方法没有和数据定义(结构体)混在一起,它们是正交的类型,数据行为(方法)是独立的。

通用方法和方法命名

在编程中一些基本操作会一遍又一遍的出现,比如打开(Open)关闭(Close)读(Read)写(Write)排序(Sort)等等,并且它们都有一个大致的意思:打开(Open)可以作用于一个文件、一个网络连接、一个数据库连接等等。具体的实现可能千差万别,但是基本的概念是一致的。在 Go 语言中,通过使用接口、标准库广泛的应用这些规则,在标准库中这些通用方法都有一致的名字,比如 Open()Read()Write()等。想写规范的 Go 程序,就应该遵守这些约定,给方法合适的名字和签名,就像那些通用方法那样。这样做会使 Go 开发的软件更加具有一致性和可读性。比如:如果需要一个 convert-to-string 方法,应该命名为 String(),而不是 ToString()

和其他面向对象语言比较 Go 的类型和方法

在如 C++JavaC#Ruby 这样的面向对象语言中,方法在类的上下文中被定义和继承,在一个对象上调用方法时,运行时会检测类以及它的超类中是否有此方法的定义,如果没有会导致异常发生。

Go 语言中,这样的继承层次是完全没必要的,如果方法在此类型定义了,就可以调用它,和其他类型上是否存在这个方法没有关系。在这个意义上,Go 具有更大的灵活性。

Go 不需要一个显式的类定义,如同 JavaC++C# 等那样,相反地,“类”是通过提供一组作用于一个共同类型的方法集来隐式定义的。类型可以是结构体或者任何用户自定义类型。

比如:我们想定义自己的 Integer 类型,并添加一些类似转换成字符串的方法,在 Go 中可以如下定义:

  1. type Integer int
  2. func (i *Integer) String() string {
  3. return strconv.Itoa(int(*i))
  4. }

JavaC# 中,这个方法需要和类 Integer 的定义放在一起,在 Ruby 中可以直接在基本类型 int 上定义这个方法。