Go 语言中的方法和面向对象中的方法并不是一样的。Go 引入方法这一元素,并不是要支持面向对象编程范式,而是 Go 践行组合设计哲学的一种实现层面的需要。

Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数

1,Go 方法的定义

Go 方法的形式,比函数多了一个 receiver

在这里插入图片描述

receiver 参数也是方法与类型之间的纽带,也是方法与函数的最大不同。

Go 中的方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型。

注意 ListenAndServeTLS 是 *Server 类型的方法,而不是 Server 类型的方法。

方法的一般声明形式:

func (t *T或T) MethodName(参数列表) (返回值列表) {
    // 方法体
}
  • 无论 receiver 参数的类型为 *T 还是 T,我们都将 T 叫做 t 的基类型
  • 如果 t 的类型为 T,那么说这个方法是类型 T 的一个方法
  • 如果 t 的类型为 *T,那么就说这个方法是类型 *T 的一个方法
  • receiver 参数的基类型本身不能为指针类型或接口类型
  • 每个方法只能有一个 receiver 参数
    • Go 不支持在方法的 receiver 部分放置包含多个 receiver 参数的参数列表,或者变长 receiver 参数
  • 方法声明要与 receiver 参数的基类型声明放在同一个包内
    • 我们无法为原生类型(诸如 int、float64、map 等)添加方法
    • 不能跨越 Go 包为其他包的类型声明新方法

Go 方法的调用形式:

type T struct{}

func (t T) M(n int) {
}

func main() {
    var t T
    t.M(1) // 通过类型T的变量实例调用方法M

    p := &T{}
    p.M(2) // 通过类型*T的变量实例调用方法M
}

我们可以将方法作为右值,赋值给一个函数类型的变量,比如下:

type T struct { 
    a int
}

func (t T) Get() int {  
    return t.a 
}

func (t *T) Set(a int) int { 
    t.a = a 
    return t.a 
}

func main() {
    var t T
    f1 := (*T).Set // f1的类型,也是*T类型Set方法的类型:func (t *T, int)int
    f2 := T.Get    // f2的类型,也是T类型Get方法的类型:func(t T)int
    
    fmt.Printf("the type of f1 is %T\n", f1) // the type of f1 is func(*main.T, int) int
    fmt.Printf("the type of f2 is %T\n", f2) // the type of f2 is func(main.T) int
    
    f1(&t, 3)
    fmt.Println(f2(t)) // 3
}

2,receiver 参数的类型问题

来看下面例子中的两个 Go 方法,以及它们等价转换后的函数:

func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)
  • M1 方法是 receiver 参数类型为 T 的一类方法的代表
    • T 类型实例的 receiver 参数以值传递方式传递到 M1 方法体中
    • 实际上是 T 类型实例的副本,M1 方法体中对副本的任何修改操作,都不会影响到原 T 类型实例
  • M2 方法是 receiver 参数类型为 *T 的另一类
    • *T 类型实例的 receiver 参数以值传递方式传递到 M2 方法体中
    • 实际上是 T 类型实例的地址,M2 方法体通过该地址可以对原 T 类型实例进行任何修改操作

一个示例:

package main
  
type T struct {
    a int
}

func (t T) M1() {
    t.a = 10
}

func (t *T) M2() {
    t.a = 11
}

func main() {
    var t T
    println(t.a) // 0

    t.M1()
    println(t.a) // 0

    p := &t
    p.M2()
    println(t.a) // 11
}

选择 receiver 参数类型的原则:

  1. 如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型。
  2. 如果我们不需要在方法中对类型实例进行修改呢?这个时候我们是为 receiver 参数选择 T 类型还是 *T 类型呢?
    1. 一般情况下,我们会为 receiver 参数选择 T 类型,这样可以减少外部修改类型实例内部状态的“渠道”
    2. 如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时我们选择 *T 作为 receiver 类型可能更好些
  3. T 类型是否要实现某一接口
    1. 如果 T 类型需要实现某一接口的全部方法,那么我们就需要使用 T 作为 receiver 参数的类型来满足接口类型方法集合中的所有方法。
    2. 如果 T 类型不需要实现某一接口,那么我们就可以参考原则一和原则二来为 receiver 参数选择类型了。

无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法 。这都是 Go 编译器在背后做的转换,当 Go 发现类型不一致时,会自动转换。

示例如下:

  type T struct {
      a int
  }
  
  func (t T) M1() {
      t.a = 10
  }
 
 func (t *T) M2() {
     t.a = 11
 }
 
 func main() {
     var t1 T
     println(t1.a) // 0
     t1.M1()
     println(t1.a) // 0
     t1.M2()
     println(t1.a) // 11
 
     var t2 = &T{}
     println(t2.a) // 0
     t2.M1()
     println(t2.a) // 0
     t2.M2()
     println(t2.a) // 11
 }

3,一个思考题

第一个:

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

// 其结果是
one
two
three
six
six
six

第二个:

type field struct {
    name string
}

func (p field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}
// 其结果是
one
two
three
four
five
six

4,方法集合

Go 中任何一个类型都有属于自己的方法集合,或者说方法集合是 Go 类型的一个“属性”。

方法集合是用来判断一个类型是否实现了某接口类型的唯一手段。

但不是所有类型都有自己的方法,比如 int 类型就没有。所以,对于没有定义方法的 Go 类型,我们称其拥有空方法集合。

方法集合可以分两种来讨论:

  • 接口类型
  • 非接口类型

接口类型相对特殊,它只会列出代表接口的方法列表,不会具体定义某个方法,它的方法集合就是它的方法列表中的所有方法。

函数 dumpMethodSet,用于输出一个非接口类型的方法集合:

func dumpMethodSet(i interface{}) {
    dynTyp := reflect.TypeOf(i)

    if dynTyp == nil {
        fmt.Printf("there is no dynamic type\n")
        return
    }

    n := dynTyp.NumMethod()
    if n == 0 {
        fmt.Printf("%s's method set is empty!\n", dynTyp)
        return
    }

    fmt.Printf("%s's method set:\n", dynTyp)
    for j := 0; j < n; j++ {
        fmt.Println("-", dynTyp.Method(j).Name)
    }
    fmt.Printf("\n")
}

再看下面代码:

type T struct{}

func (T) M1() {}
func (T) M2() {}

func (*T) M3() {}
func (*T) M4() {}

func main() {
    var n int
    dumpMethodSet(n)
    dumpMethodSet(&n)

    var t T
    dumpMethodSet(t)
    dumpMethodSet(&t)
}

输出如下:

int's method set is empty!
*int's method set is empty!
main.T's method set:
- M1
- M2

*main.T's method set:
- M1  // 初学者不容易理解的地方
- M2  // 初学者不容易理解的地方
- M3
- M4

Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。

方法集合决定接口实现的含义就是:如果某类型 T 的方法集合与某接口类型的方法集合相同,或者类型 T 的方法集合是接口类型 I 方法集合的超集,那么我们就说这个类型 T 实现了接口 I。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口

... method has pointer receiver 问题

看代码:

type QuackableAnimal interface {
	Quack()
}

type Duck struct{}

// d 的类型是 Duck
func (d Duck) Quack() {
}

func AnimalQuackInForest(a QuackableAnimal) {
}

func main() {
	AnimalQuackInForest(Duck{})	    // 传  T 类型没问题
	AnimalQuackInForest(&Duck{})        // 传 *T 类型没问题
}

查看方法集:

dumpMethodSet(Duck{})
dumpMethodSet(&Duck{})

// 输出如下
main.Duck's method set:
- Quack
*main.Duck's method set:
- Quack

再看代码:

type QuackableAnimal interface {
  Quack()
}

type Duck struct{}

// d 的类型是 *Duck
func (d *Duck) Quack() {
}

func AnimalQuackInForest(a QuackableAnimal) {
}

func main() {
  AnimalQuackInForest(Duck{})	// 传  T 类型有问题,编译异常:Quack method has pointer receiver
  AnimalQuackInForest(&Duck{})	// 传 *T 类型没问题
}

此时 AnimalQuackInForest(Duck{}) 出问题的原因是,Duck 类型没有实现 QuackableAnimal 接口。

通过 dumpMethodSet(Duck{}) 查看 Duck 的方法集,可知 Duck 的方法集为空。

dumpMethodSet(Duck{})
dumpMethodSet(&Duck{})

// 输出如下
main.Duck's method set is empty!
*main.Duck's method set:
- Quack

(完。)