自定义类型的方法和接口(interface)都是 Go 语言中的重要概念,并且它们之间存在千丝万缕的联系。我们来看一个例子:

  1. // method_set_1.go
  2. package main
  3. type Interface interface {
  4. M1()
  5. M2()
  6. }
  7. type T struct{}
  8. func (t T) M1() {}
  9. func (t *T) M2() {}
  10. func main() {
  11. var t T
  12. var pt *T
  13. var i Interface
  14. i = t
  15. i = pt
  16. }

我们运行一下该示例程序:

  1. $ go run method_set_1.go
  2. # command-line-arguments
  3. ./method_set_1.go:18:4: cannot use t (type T) as type Interface in assignment:
  4. T does not implement Interface (M2 method has pointer receiver)

1. 方法集合(Method Set)

在“理解方法本质以正确选择 receiver 类型”一节中我们曾提到过选择 receiver 类型除了考量是否需要对类型实例进行修改、类型实例值拷贝导致的性能损耗之外,还有一个重要考量因素,那就是类型是否要实现某个接口(interface)类型

要判断一个自定义类型是否实现了某接口类型,我们首先要识别出自定义类型的方法集合以及接口类型的方法集合。但有些时候它们并非那么明显(比如:若存在结构体嵌入、接口嵌入、类型别名时)。这里我们实现了一个工具函数可以方便输出一个自定义类型或接口类型的方法集合。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. // method_set_utils.go
  7. func DumpMethodSet(i interface{}) {
  8. v := reflect.TypeOf(i)
  9. elemTyp := v.Elem()
  10. n := elemTyp.NumMethod()
  11. if n == 0 {
  12. fmt.Printf("%s's method set is empty!\n", elemTyp)
  13. return
  14. }
  15. fmt.Printf("%s's method set:\n", elemTyp)
  16. for j := 0; j < n; j++ {
  17. fmt.Println("-", elemTyp.Method(j).Name)
  18. }
  19. fmt.Printf("\n")
  20. }

接下来,我们就用该工具函数输出一下本节开头那个示例中的接口类型和自定义类型的方法集合:

  1. // method_set_2.go
  2. package main
  3. type Interface interface {
  4. M1()
  5. M2()
  6. }
  7. type T struct{}
  8. func (t T) M1() {}
  9. func (t *T) M2() {}
  10. func main() {
  11. var t T
  12. var pt *T
  13. DumpMethodSet(&t)
  14. DumpMethodSet(&pt)
  15. DumpMethodSet((*Interface)(nil))
  16. }

运行上述代码:

  1. $ go run method_set_2.go method_set_utils.go
  2. main.T's method set:
  3. - M1
  4. *main.T's method set:
  5. - M1
  6. - M2
  7. main.Interface's method set:
  8. - M1
  9. - M2

T 类型没有直接实现 M1,但 M1 仍出现在T 类型的方法集合中了。这符合 Go 语言规范中的说法:

  • 对于非接口类型的自定义类型 T,其方法集合为所有 receiver 为 T 类型的方法组成;
  • 而类型T 的方法集合则包含所有 **receiver 为 T 和T** 类型的方法。也正因为如此,pt 才能成功赋值给 Interface 类型变量。

2. 类型嵌入与方法集合

Go 的设计哲学之一就是偏好组合,Go 支持用组合的思想来实现一些面向对象领域经典的机制,比如:继承。而具体的方式就是利用类型嵌入(type embeding)

Go 支持三种类型嵌入:接口类型中嵌入接口类型、结构体类型中嵌入接口类型以及结构体类型中嵌入结构体类型,下面我们分别看一下经过类型嵌入后的类型的方法集合是什么样子的。

1) 接口类型中嵌入接口类型

按 Go 语言惯例,Go 中的接口类型中仅包含少量方法,并且常常仅是一个方法。通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法,比如 io 包中的 ReadWriter、ReadWriteCloser 等接口类型就是通过嵌入 Reader、Writer 或 Closer 三个基本的接口类型组合而成的:

  1. // $GOROOT/src/io/io.go
  2. type Reader interface {
  3. Read(p []byte) (n int, err error)
  4. }
  5. type Writer interface {
  6. Write(p []byte) (n int, err error)
  7. }
  8. type Closer interface {
  9. Close() error
  10. }
  11. // 以上为三个基本接口类型
  12. // 下面的接口类型通过嵌入上面基本接口类型而形成
  13. type ReadWriter interface {
  14. Reader
  15. Writer
  16. }
  17. type ReadCloser interface {
  18. Reader
  19. Closer
  20. }
  21. type WriteCloser interface {
  22. Writer
  23. Closer
  24. }
  25. type ReadWriteCloser interface {
  26. Reader
  27. Writer
  28. Closer
  29. }

我们再来看看通过嵌入接口类型后的新接口类型的方法集合是什么样的,我们就以 Go 标准库中 io 包中的几个接口类型为例:

  1. // method_set_3.go
  2. package main
  3. import "io"
  4. func main() {
  5. DumpMethodSet((*io.Writer)(nil))
  6. DumpMethodSet((*io.Reader)(nil))
  7. DumpMethodSet((*io.Closer)(nil))
  8. DumpMethodSet((*io.ReadWriter)(nil))
  9. DumpMethodSet((*io.ReadWriteCloser)(nil))
  10. }

运行该示例得到以下结果:

  1. $ go run method_set_3.go method_set_utils.go
  2. io.Writer's method set:
  3. - Write
  4. io.Reader's method set:
  5. - Read
  6. io.Closer's method set:
  7. - Close
  8. io.ReadWriter's method set:
  9. - Read
  10. - Write
  11. io.ReadWriteCloser's method set:
  12. - Close
  13. - Read
  14. - Write

不过这种通过嵌入其他接口类型创建新接口类型的方式有一个约束,那就是被嵌入的接口类型的方法集合不能有交集(如下面例子中的 Interface1 和 Interface2 的方法集合有交集,交集是方法 M1),同时被嵌入的接口类型的方法集合中的方法名字不能与新接口中其他方法名同名(如下面例子中的 Interface2 的 M2 与 Interface4 的 M2 重名):

  1. // method_set_4.go
  2. package main
  3. type Interface1 interface {
  4. M1()
  5. }
  6. type Interface2 interface {
  7. M1()
  8. M2()
  9. }
  10. type Interface3 interface {
  11. Interface1
  12. Interface2 // Error: duplicate method M1
  13. }
  14. type Interface4 interface {
  15. Interface2
  16. M2() // Error: duplicate method M2
  17. }
  18. func main() {
  19. DumpMethodSet((*Interface3)(nil))
  20. }

接口嵌入接口需要考虑命名冲突.

2) 结构体类型中嵌入接口类型

在结构体类型中嵌入接口类型后,该结构体类型的方法集合中将包含被嵌入的接口类型的方法集合。比如下面这个例子:

  1. // method_set_5.go
  2. package main
  3. type Interface interface {
  4. M1()
  5. M2()
  6. }
  7. type T struct {
  8. Interface
  9. }
  10. func (T) M3() {}
  11. func main() {
  12. DumpMethodSet((*Interface)(nil))
  13. var t T
  14. var pt *T
  15. DumpMethodSet(&t)
  16. DumpMethodSet(&pt)
  17. }

运行该示例得到以下结果:

  1. $ go run method_set_5.go method_set_utils.go
  2. main.Interface's method set:
  3. - M1
  4. - M2
  5. main.T's method set:
  6. - M1
  7. - M2
  8. - M3
  9. *main.T's method set:
  10. - M1
  11. - M2
  12. - M3

但有些时候结果并非总是这样,比如:当结果体嵌入多个接口类型且这些接口类型的方法集合存在交集时。为了方便后续说明,这里不得不提一下嵌入了其他接口类型的结构体类型的实例在调用方法时,Go 选择方法的次序:

  1. 优先选择结构体自身实现的方法;
  2. 如果结构体自身并未实现,那么将查找结构体中的嵌入接口类型的方法集中是否有该方法,如果有,则提升(promoted)为结构体的方法;

比如下面例子:

  1. // method_set_6.go
  2. package main
  3. type Interface interface {
  4. M1()
  5. M2()
  6. }
  7. type T struct {
  8. Interface
  9. }
  10. func (T) M1() {
  11. println("T's M1")
  12. }
  13. type S struct{}
  14. func (S) M1() {
  15. println("S's M1")
  16. }
  17. func (S) M2() {
  18. println("S's M2")
  19. }
  20. func main() {
  21. var t = T{
  22. Interface: S{},
  23. }
  24. t.M1()
  25. t.M2()
  26. }

下面是上面程序的输出结果:

  1. $ go run method_set_6.go
  2. T's M1
  3. S's M2

如果结构体嵌入了多个接口类型且这些接口类型的方法集合存在交集,那么编译器将报错,除非结构体自己实现了交集中的所有方法。

  1. // method_set_7.go
  2. package main
  3. type Interface interface {
  4. M1()
  5. M2()
  6. M3()
  7. }
  8. type Interface1 interface {
  9. M1()
  10. M2()
  11. M4()
  12. }
  13. type T struct {
  14. Interface
  15. Interface1
  16. }
  17. func main() {
  18. t := T{}
  19. t.M1()
  20. t.M2()
  21. }

运行该例子:

  1. $ go run method_set_7.go
  2. # command-line-arguments
  3. ./method_set_7.go:22:3: ambiguous selector t.M1
  4. ./method_set_7.go:23:3: ambiguous selector t.M2

为了让编译器能找到 M1/M2,我们可以为 T 增加 M1 和 M2 的实现,这样编译器便会直接选择 T 自己实现的 M1 和 M2,程序也就能顺利通过编译并运行了:

  1. // method_set_8.go
  2. ... ...
  3. type T struct {
  4. Interface
  5. Interface1
  6. }
  7. func (T) M1() { println("T's M1") }
  8. func (T) M2() { println("T's M2") }
  9. func main() {
  10. t := T{}
  11. t.M1()
  12. t.M2()
  13. }
  14. $ go run method_set_8.go
  15. T's M1
  16. T's M2

结构体类型在嵌入某接口类型的同时,它也实现了这个接口。这一特性在单元测试时尤为有用,尤其是应对在下面的场景中:

  1. // method_set_9.go
  2. package employee
  3. type Result struct {
  4. Count int
  5. }
  6. func (r Result) Int() int { return r.Count }
  7. type Rows []struct{}
  8. type Stmt interface {
  9. Close() error
  10. NumInput() int
  11. Exec(stmt string, args ...string) (Result, error)
  12. Query(args []string) (Rows, error)
  13. }
  14. // 返回男性员工总数
  15. func MaleCount(s Stmt) (int, error) {
  16. result, err := s.Exec("select count(*) from employee_tab where gender=?", "1")
  17. if err != nil {
  18. return 0, err
  19. }
  20. return result.Int(), nil
  21. }

现在我们要对 MaleCount 方法编写单元测试代码。对于这种依赖外部数据库操作的方法,我们的惯例是使用“伪对象(fake object)”来冒充真实的 Stmt 接口实现。不过现在有一个问题,那就是 Stmt 接口类型的方法集合中有四个方法,如果我们针对每个测试用例所用的伪对象都实现这四个方法,那么这个工作量有些大,我们需要的仅仅是 Exec 这一个方法。如何快速建立伪对象呢?结构体类型嵌入接口类型便可以帮助我们:

  1. // method_set_9_test.go
  2. package employee
  3. import "testing"
  4. type fakeStmtForMaleCount struct {
  5. Stmt
  6. }
  7. func (fakeStmtForMaleCount) Exec(stmt string, args ...string) (Result, error) {
  8. return Result{Count: 5}, nil
  9. }
  10. func TestEmployeeMaleCount(t *testing.T) {
  11. f := fakeStmtForMaleCount{}
  12. c, _ := MaleCount(f)
  13. if c != 5 {
  14. t.Errorf("want: %d, actual: %d", 5, c)
  15. return
  16. }
  17. }

无需实现其它方法.

3) 结构体类型中嵌入结构体类型

在结构体类型中嵌入结构体类型为 Gopher 提供了一种“实现继承”的手段,外部的结构体类型 T 可以“继承”嵌入的结构体类型的所有方法的实现,并且无论是 T 类型的变量实例还是*T 类型变量实例,都可以调用所有“继承”的方法。

  1. // method_set_10.go
  2. package main
  3. type T1 struct{}
  4. func (T1) T1M1() { println("T1's M1") }
  5. func (T1) T1M2() { println("T1's M2") }
  6. func (*T1) PT1M3() { println("PT1's M3") }
  7. type T2 struct{}
  8. func (T2) T2M1() { println("T2's M1") }
  9. func (T2) T2M2() { println("T2's M2") }
  10. func (*T2) PT2M3() { println("PT2's M3") }
  11. type T struct {
  12. T1
  13. *T2
  14. }
  15. func main() {
  16. t := T{
  17. T1: T1{},
  18. T2: &T2{},
  19. }
  20. println("call method through t:")
  21. t.T1M1()
  22. t.T1M2()
  23. t.PT1M3()
  24. t.T2M1()
  25. t.T2M2()
  26. t.PT2M3()
  27. println("\ncall method through pt:")
  28. pt := &t
  29. pt.T1M1()
  30. pt.T1M2()
  31. pt.PT1M3()
  32. pt.T2M1()
  33. pt.T2M2()
  34. pt.PT2M3()
  35. println("")
  36. var t1 T1
  37. var pt1 *T1
  38. DumpMethodSet(&t1)
  39. DumpMethodSet(&pt1)
  40. var t2 T2
  41. var pt2 *T2
  42. DumpMethodSet(&t2)
  43. DumpMethodSet(&pt2)
  44. DumpMethodSet(&t)
  45. DumpMethodSet(&pt)
  46. }

示例运行结果如下:

  1. $ go run method_set_10.go method_set_utils.go
  2. call method through t:
  3. T1's M1
  4. T1's M2
  5. PT1's M3
  6. T2's M1
  7. T2's M2
  8. PT2's M3
  9. call method through pt:
  10. T1's M1
  11. T1's M2
  12. PT1's M3
  13. T2's M1
  14. T2's M2
  15. PT2's M3
  16. main.T1's method set:
  17. - T1M1
  18. - T1M2
  19. *main.T1's method set:
  20. - PT1M3
  21. - T1M1
  22. - T1M2
  23. main.T2's method set:
  24. - T2M1
  25. - T2M2
  26. *main.T2's method set:
  27. - PT2M3
  28. - T2M1
  29. - T2M2
  30. main.T's method set:
  31. - PT2M3
  32. - T1M1
  33. - T1M2
  34. - T2M1
  35. - T2M2
  36. *main.T's method set:
  37. - PT1M3
  38. - PT2M3
  39. - T1M1
  40. - T1M2
  41. - T2M1
  42. - T2M2

通过输出结果可以看出:虽然通过 T 还是T 变量实例,都可以调用所有“继承”的方法(这也是 Go 语法甜头),但是 T 和T 类型的方法集合是有差别的:

  • 类型 T 的方法集合 = T1 的方法集合 + *T2 的方法集合;
  • 类型T 的方法集合 = T1 的方法集合 + *T2 的方法集合;

3. 类型别名的方法集合

类型别名与原类型几乎可以理解为是完全等价的。Go 预定义标识符 rune、byte 就是通过 type alias 语法定义的:

  1. // $GOROOT/src/builtin/builtin.go
  2. // byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
  3. // used, by convention, to distinguish byte values from 8-bit unsigned
  4. // integer values.
  5. type byte = uint8
  6. // rune is an alias for int32 and is equivalent to int32 in all ways. It is
  7. // used, by convention, to distinguish character values from integer values.
  8. type rune = int32

但是在方法集合上面,类型别名与原类型是有差别的。我们看一个例子:

  1. // method_set_11.go
  2. package main
  3. type T struct{}
  4. func (T) M1() {}
  5. func (*T) M2() {}
  6. type Interface interface {
  7. M1()
  8. M2()
  9. }
  10. type T1 T
  11. type Interface1 Interface
  12. func main() {
  13. var t T
  14. var pt *T
  15. var t1 T1
  16. var pt1 *T1
  17. DumpMethodSet(&t)
  18. DumpMethodSet(&t1)
  19. DumpMethodSet(&pt)
  20. DumpMethodSet(&pt1)
  21. DumpMethodSet((*Interface)(nil))
  22. DumpMethodSet((*Interface1)(nil))
  23. }

运行该示例程序得到如下结果:

  1. $ go run method_set_11.go method_set_utils.go
  2. main.T's method set:
  3. - M1
  4. main.T1's method set is empty!
  5. *main.T's method set:
  6. - M1
  7. - M2
  8. *main.T1's method set is empty!
  9. main.Interface's method set:
  10. - M1
  11. - M2
  12. main.Interface1's method set:
  13. - M1
  14. - M2

从例子的输出结果上来看,Go 对于接口类型和自定义类型的类型别名给出了“不一致”的结果:

  • 接口类型的别名类型与原接口类型的方法集合是一致的,如上面的 Interface 和 Interface1;
  • 自定义类型的别名类型则并没有“继承”原类型的方法集合,别名类型的方法集合是空的。

方法集合决定接口实现:自定义类型的别名类型的方法集合为空的事实也决定了即便原类型实现了某些接口,其别名类型也没有“继承”这一隐式关联。别名类型要想实现那些接口,仍需重新实现接口的所有方法。

第三节作者有误, 示例代码中是类型定义

  1. type T1 T
  2. type Interface1 Interface

自己实测代码:

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type T struct{}
  7. func (T) M1() {}
  8. func (*T) M2() {}
  9. type Interface interface {
  10. M1()
  11. M2()
  12. }
  13. type T1 = T
  14. type Interface1 = Interface
  15. func main() {
  16. var t T
  17. var pt *T
  18. var t1 T1
  19. var pt1 *T1
  20. DumpMethodSet(&t)
  21. DumpMethodSet(&t1)
  22. DumpMethodSet(&pt)
  23. DumpMethodSet(&pt1)
  24. DumpMethodSet((*Interface)(nil))
  25. DumpMethodSet((*Interface1)(nil))
  26. }
  27. func DumpMethodSet(i interface{}) {
  28. v := reflect.TypeOf(i)
  29. elemTyp := v.Elem()
  30. n := elemTyp.NumMethod()
  31. if n == 0 {
  32. fmt.Printf("%s's method set is empty!\n", elemTyp)
  33. return
  34. }
  35. fmt.Printf("%s's method set:\n", elemTyp)
  36. for j := 0; j < n; j++ {
  37. fmt.Println("-", elemTyp.Method(j).Name)
  38. }
  39. fmt.Printf("\n")
  40. }

实测结果:

  1. main.T's method set:
  2. - M1
  3. main.T's method set:
  4. - M1
  5. *main.T's method set:
  6. - M1
  7. - M2
  8. *main.T's method set:
  9. - M1
  10. - M2
  11. main.Interface's method set:
  12. - M1
  13. - M2
  14. main.Interface's method set:
  15. - M1
  16. - M2