Go 方法

admin
admin 2020年03月19日
  • 在其它设备中阅读本文章

方法 其实就是一个 函数 ,在 func 这个关键字和方法名中间加入了一个特殊的 接收器 类型。接收器 可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的

一、方法声明

在 Go 中,我们可以为类型 T 和 * T 显式地声明一个 方法 ,其中类型 T 必须满足四个条件:

  • T 必须是一个定义类型;
  • T 必须和此方法声明定义在同一个代码包中;
  • T 不能是一个指针类型;
  • T 不能是一个接口类型。接口类型将在下一篇文章中讲解。 类型 T 和 * T 称为它们各自的方法的属主类型(receiver type)。

注意:我们也可以为满足上列条件的类型 T 和 T 的别名声明方法。这样做的效果和直接为类型 T 和 T 声明方法是一样的。
如果我们为某个类型声明了一个方法,以后我们可以说此类型拥有此方法。 从上面列出的条件,我们得知我们不能为下列类型(显式地)声明方法:

  • 内置基本类型。比如 int 和 string。因为这些类型声明在内置 builtin 标准包中,而我们不能在标准包中声明方法。
  • 接口类型。但是接口类型可以拥有方法。详见下一篇文章。
  • 除了满足上面条件的形如 * T 的指针类型之外的非定义组合类型。 一个方法声明和一个函数声明很相似,但是比函数声明多了一个额外的参数声明部分。此额外的参数声明部分只能含有一个类型为此方法的属主类型的参数,此参数称为此方法声明的属主参数(receiver parameter)。此属主参数声明必须包裹在一对小括号 () 之中。此属主参数声明部分必须处于 func 关键字和方法名之间。

    // Age和int是两个不同的类型。我们不能为int和*int
    // 类型声明方法,但是可以为Age和*Age类型声明方法。
    type Age int
    func (age Age) LargerThan(a Age) bool {
      return age > a
    }
    func (age *Age) Increase() {
      *age++
    }
    // 为自定义的函数类型FilterFunc声明方法。
    type FilterFunc func(in int) bool
    func (ff FilterFunc) Filte(in int) bool {
      return ff(in)
    }
    // 为自定义的映射类型StringSet声明方法。
    type StringSet map[string]struct{}
    func (ss StringSet) Has(key string) bool {
      _, present := ss[key]
      return present
    }
    func (ss StringSet) Add(key string) {
      ss[key] = struct{}{}
    }
    func (ss StringSet) Remove(key string) {
      delete(ss, key)
    }
    // 为自定义的结构体类型Book和它的指针类型*Book声明方法。
    type Book struct {
      pages int
    }
    func (b Book) Pages() int {
      return b.pages
    }
    func (b *Book) SetPages(pages int) {
      b.pages = pages
    }

    我们可以为各种种类(kind)的类型声明方法,而不仅仅是结构体类型。指针类型的属主参数称为 指针类型属主 ,非指针类型的属主参数称为 值类型属主
    方法名可以是空标识符_。一个类型可以拥有若干名可以是空标识符的方法,但是这些方法无法被调用。只有导出的方法才可以在其它代码包中调用。

二、方法的隐式声明

1、每个方法对应着一个隐式声明的函数

对每个方法声明,编译器将自动隐式声明一个相对应的函数。比如对于上一节的例子中为类型 Book 和 *Book 声明的两个方法,编译器将自动声明下面的两个函数:

func Book.Pages(b Book) int {
    return b.pages // 此函数体和Book类型的Pages方法体一样
}
func (*Book).SetPages(b *Book, pages int) {
    b.pages = pages // 此函数体和*Book类型的SetPages方法体一样
}

在上面的两个隐式函数声明中,它们各自对应的方法声明的属主参数声明被插入到了普通参数声明的第一位。它们的函数体和各自对应的显式方法的方法体是一样的。 两个隐式函数名 Book.Pages 和 (*Book).SetPages 都是 aType.MethodName 这种形式的。我们不能显式声明名称为这种形式的函数,因为这种形式不属于合法标识符。这样的函数只能由编译器隐式声明。但是我们可以在代码中调用这些隐式声明的函数:

package main
import "fmt"
type Book struct {
    pages int
}
func (b Book) Pages() int {
    return b.pages
}
func (b *Book) SetPages(pages int) {
    b.pages = pages
}
func main() {
    var book Book
    // 调用这两个隐式声明的函数。
    (*Book).SetPages(&book, 123)
    fmt.Println(Book.Pages(book)) // 123
}

事实上,在隐式声明上述两个函数的同时,编译器也将改写这两个函数对应的显式方法(至少,我们可以这样认为),让这两个方法在体内直接调用这两个隐式函数:

func (b Book) Pages() int {
    return Book.pages(b)
}
func (b *Book) SetPages(pages int) {
    (*Book).SetPages(b, pages)
}

2、值类型属主方法声明的同时隐式声明同名的指针类型属主方法

对每一个为值类型属主 T 声明的方法,编译器将自动隐式地为其对应的指针类型属主 * T 声明一个相应的同名方法。以上面的为类型 Book 声明的 Pages 方法为例,编译器将自动为类型 Book 声明一个同名方法:

func (b *Book) Pages() int {
    return Book.Pages(*b) // 调用上节中隐式声明的函数
}

当我们为一个非指针类型显式声明一个方法的时候,事实上两个方法被声明了。一个方法是为非指针类型显式声明的,另一个是为指针类型隐式声明的。每一个方法对应着一个编译器隐式声明的函数。所以对于刚提到的隐式方法,编译器也将隐式声明一个相应的函数:

func (*Book).Pages(b *Book) int {
    return Book.Pages(*b)
}

换句话说,对于每一个为值类型属主显式声明的方法,同时将有一个隐式方法和两个隐式函数被自动声明。

三、方法原型(method prototype)和方法集(method set)

一个方法原型可以看作是一个不带 func 关键字的函数原型。我们可以把每个方法声明看作是由一个 func 关键字、一个属主参数声明部分、一个方法原型和一个方法体组成。 比如,上面的例子中的 Pages 和 SetPages 的原型如下:

Pages() int
SetPages(pages int)

每个类型都有个方法集。一个非接口类型的方法集由所有为它声明的(不管是显式的还是隐式的,但不包含方法名为空标识符的)方法的原型组成。接口类型将在下一篇文章详述。 比如,在上面的例子中,Book 类型的方法集为:

Pages() int

而 *Book 类型的方法集为:

Pages() int
SetPages(pages int)

方法集中的方法原型的次序并不重要。对于一个方法集,如果其中的每个方法原型都处于另一个方法集中,则我们说前者方法集为后者(即另一个)方法集的子集,后者为前者的超集。如果两个方法集互为子集(或超集),则这两个方法集必等价。

给定一个类型 T,假设它既不是一个指针类型也不是一个接口类型,因为上一节中提到的原因,类型 T 的方法集总是类型 T 的方法集的子集。比如,在上面的例子中,Book 类型的方法集为 Book 类型的方法集的子集。方法集在 Go 中的多态特性中扮演着重要的角色。下列类型的方法集总为空:

  • 内置基本类型;
  • 定义的指针类型;
  • 基类型为指针类型或者接口类型的指针类型;
  • 非定义的数组 / 切片 / 映射 / 函数 / 通道类型。

四、方法值和方法调用

方法事实上是特殊的函数。方法也常被称为成员函数。当一个类型拥有一个方法,则此类型的每个值将拥有一个不可修改的函数类型的成员(类似于结构体的字段)。此成员的名称为此方法名,它的类型和此方法的声明中不包括属主部分的函数声明的类型一致。一个值的成员函数也可以称为此值的方法。
一个方法调用其实是调用了一个值的成员函数。假设一个值 v 有一个名为 m 的方法,则此方法可以用选择器语法形式 v.m 来表示。

package main
import "fmt"
type Book struct {
    pages int
}
func (b Book) Pages() int {
    return b.pages
}
func (b *Book) SetPages(pages int) {
    b.pages = pages
}
func main() {
    var book Book
    fmt.Printf("%T \n", book.Pages)       // func() int
    fmt.Printf("%T \n", (&book).SetPages) // func(int)
    // &book值有一个隐式方法Pages。
    fmt.Printf("%T \n", (&book).Pages)    // func() int
    // 调用这三个方法。
    (&book).SetPages(123)
    book.SetPages(123)           // 等价于上一行
    fmt.Println(book.Pages())    // 123
    fmt.Println((&book).Pages()) // 123
}

上例中的 (&book).SetPages(123) 一行为什么可以被简化为 book.SetPages(123)呢?毕竟,类型 Book 并不拥有一个 SetPages 方法。啊哈,这是 Go 中为了让代码看上去更简洁而特别设计的的语法糖。此语法糖只对可寻址的值类型的属主有效。编译器会自动将 book.SetPages(123)改写为(&book).SetPages(123)。 如上面刚提到的,当为一个类型声明了一个方法后,每个此类型的值将拥有一个和此方法同名的成员函数。此类型的零值也不例外,不论此类型的零值是否用 nil 来表示。

package main
type StringSet map[string]struct{}
func (ss StringSet) Has(key string) bool {
    _, present := ss[key] // 永不会产生恐慌,即使ss为nil。
    return present
}
type Age int
func (age *Age) IsNil() bool {
    return age == nil
}
func (age *Age) Increase() {
    *age++ // 如果age是一个空指针,则此行将产生一个恐慌。
}
func main() {
    _ = (StringSet(nil)).Has   // 不会产生恐慌
    _ = ((*Age)(nil)).IsNil    // 不会产生恐慌
    _ = ((*Age)(nil)).Increase // 不会产生恐慌
    _ = (StringSet(nil)).Has("key") // 不会产生恐慌
    _ = ((*Age)(nil)).IsNil()       // 不会产生恐慌
    // 下面这行将产生一个恐慌,但是此恐慌不是在调用方法的时
    // 候产生的,而是在此方法体内解引用空指针的时候产生的。
    ((*Age)(nil)).Increase()
}

五、值类型属主和指针类型属主选择

对于值类型属主还是指针类型属主都可以接受的方法声明,下面列出了一些考虑因素:

  • 太多的指针可能会增加垃圾回收器的负担。
  • 如果一个值类型的尺寸太大,那么属主参数在传参的时候的复制成本将不可忽略。指针类型都是小尺寸类型。
  • 在并发场合下,同时调用为值类型属主和指针类型属主方法比较易于产生数据竞争。
  • sync 标准库包中的类型的值不应该被复制,所以如果一个结构体类型内嵌了这些类型,则不应该为这个结构体类型声明值类型属主的方法。
    如果实在拿不定主意在一个方法声明中应该使用值类型属主还是指针类型属主,那么请使用指针类型属主。