Loading

Go语言精进之路读书笔记第24条——方法集合决定接口实现

24.1 方法集合

方法决定接口实现:
如果某个自定义类型T的方法集合是某个接口类型的方法集合的超集,那么就说类型T实现了该接口,并且类型T的变量可以赋值给该接口类型的变量

Go语言规范,对于非接口类型的自定义类型T:

  • 类型T,方法集合由所有receiver为T类型的方法组成
  • 类型*T,方法集合由所有receiver为T和*T类型的方法组成
type Interface interface {
    M1()
    M2()
}

type T struct{}

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

func main() {
    var t T
    var pt *T
    DumpMethodSet(&t) //M1
    DumpMethodSet(&pt) //M1 M2
    DumpMethodSet((*Interface)(nil)) //M1 M2
}

24.2 类型嵌入与方法集合

Go的设计哲学之一是偏好组合,具体的方式就是利用类型嵌入(type embedding)

1.在接口类型中嵌入接口类型

通过嵌入其他接口类型而创建的新接口类型的方法集合包含了被嵌入接口类型的方法集合

例子:基于已有接口类型构建新接口类型,io包的ReadWrite、ReadWriterCloser

func main() {
    DumpMethodSet((*io.Writer)(nil)) //Write
    DumpMethodSet((*io.Reader)(nil)) //Read
    DumpMethodSet((*io.Closer)(nil)) //Close
    DumpMethodSet((*io.ReadWriter)(nil)) //Read Write
    DumpMethodSet((*io.ReadWriteCloser)(nil)) //Read Write Close
}

不过在Go1.14之前的版本中,这种方式有一个约束,那就是被嵌入的接口类型的方法集合不能有交集,同时被嵌入的接口类型的方法集合中的方法不能与新接口中其他方法同名
自Go1.14版本之后,Go语言去除了这个约束

type Interface1 interface {
    M1()
}

type Interface2 interface {
    M1()
    M2()
}

type Interface3 interface {
    Interface1
    Interface2 //Go 1.14之前版本报错:duplicate method M1
}

type Interface4 interface {
    Interface2
    M2() //Go 1.14之前版本报错:duplicate method M2
}

func main() {
    DumpMethodSet((*Interface3)(nil)) //M1 M2
}

2.在结构体类型中嵌入接口类型

在结构体类型中嵌入接口类型后,结构体类型的方法集合包含被嵌入接口类型的方法集合

type Interface interface {
    M1()
    M2()
}

type T struct {
    Interface
}

func (T) M3() {}

func main() {
    DumpMethodSet((*Interface)(nil)) //M1 M2
    var t T
    var pt *T
    DumpMethodSet(&t) //M1 M2 M3
    DumpMethodSet(&pt) //M1 M2 M3
}

当结构体类型中嵌入多个接口类型且这些接口类型的方法集合存在交集时
(1) 优先选择结构体自身实现的方法
(2) 如果结构体自身并未实现,那么将查找结构体中的嵌入接口类型的方法集合中是否有该方法,如果有,则提升(promoted)为结构体的方法

type Interface interface {
    M1()
    M2()
}

type T struct {
    Interface
}

func (T) M1() {
    println("T's M1")
}

type S struct{}

func (S) M1() {
    println("S's M1")
}
func (S) M2() {
    println("S's M2")
}

func main() {
    var t = T{
        Interface: S{},
    }

    t.M1() //T's M1
    t.M2() //S's M2
}

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

type Interface interface {
    M1()
    M2()
    M3()
}

type Interface1 interface {
    M1()
    M2()
    M4()
}

type T struct {
    Interface
    Interface1
}

// 如果T未实现M1和M2,编译器会给出错误提示:ambiguous selector t.M1/t.M2
func (T) M1() { println("T's M1") }
func (T) M2() { println("T's M2") }

func main() {
    t := T{}
    t.M1() //T's M1
    t.M2() //T's M2
}

例子:在单元测试中,通过在结构体中嵌入接口,来测试接口的某一个方法。利用的正是结构体类型在嵌入某接口类型的同时,也实现了该接口的特性。

type Result struct {
    Count int
}

func (r Result) Int() int { return r.Count }

type Rows []struct{}

type Stmt interface {
    Close() error
    NumInput() int
    Exec(stmt string, args ...string) (Result, error)
    Query(args []string) (Rows, error)
}

// 返回男性员工总数
func MaleCount(s Stmt) (int, error) {
    result, err := s.Exec("select count(*) from employee_tab where gender=?", "1")
    if err != nil {
        return 0, err
    }

    return result.Int(), nil
}

//单元测试,快速构建实现了Stmt接口的伪对象fakeStmtForMaleCount,仅需实现Exec方法即可
type fakeStmtForMaleCount struct {
    Stmt
}

func (fakeStmtForMaleCount) Exec(stmt string, args ...string) (Result, error) {
    return Result{Count: 5}, nil
}

func TestEmployeeMaleCount(t *testing.T) {
    f := fakeStmtForMaleCount{}
    c, _ := MaleCount(f)
    if c != 5 {
        t.Errorf("want: %d, actual: %d", 5, c)
        return
    }
}

3.在结构体类型中嵌入结构体类型

外部的结构体类型T可以“继承”嵌入的结构体类型的所有方法的实现,并且无论是T类型的变量实例还是*T类型的变量实例,都可以调用所有“继承”的方法

但是T和*T的方法集合是有差别的:

  • T = T1 + *T2
  • T = *T1 + *T2

疑问:Go语言中两个类型的方法集合不一样有什么影响?

type T1 struct{}

func (T1) T1M1()   { println("T1's M1") }
func (T1) T1M2()   { println("T1's M2") }
func (*T1) PT1M3() { println("PT1's M3") }

type T2 struct{}

func (T2) T2M1()   { println("T2's M1") }
func (T2) T2M2()   { println("T2's M2") }
func (*T2) PT2M3() { println("PT2's M3") }

type T struct {
    T1
    *T2
}

func main() {
    t := T{
        T1: T1{},
        T2: &T2{},
    }

    //无论T类型变量还是*T类型变量实例都可以调用所有“继承”的方法
    println("call method through t:")
    t.T1M1()
    t.T1M2()
    t.PT1M3()
    t.T2M1()
    t.T2M2()
    t.PT2M3()

    println("\ncall method through pt:")
    pt := &t
    pt.T1M1()
    pt.T1M2()
    pt.PT1M3()
    pt.T2M1()
    pt.T2M2()
    pt.PT2M3()
    println("")

    var t1 T1
    var pt1 *T1
    DumpMethodSet(&t1)  //T1M1 T1M2
    DumpMethodSet(&pt1) //PT1M3 T1M1 T1M2

    var t2 T2
    var pt2 *T2
    DumpMethodSet(&t2)  //T2M1 T2M2
    DumpMethodSet(&pt2) //PT2M3 T2M1 T2M2

    DumpMethodSet(&t)  //PT2M3 T1M1 T1M2 T2M1 T2M2
    DumpMethodSet(&pt) //PT1M3 PT2M3 T1M1 T1M2 T2M1 T2M2
}

24.3 defined类型的方法集合

type MyInterface I
type Mystruct T

已有类型(I、T)被称为underlying类型,而新类型被称为defined类型

  • 基于接口创建的defined类型与原类型具有相同的方法集合,而基于自定义非接口类型创建的defined类型的方法集合为空
  • 因此对于基于自定义非接口类型的defined类型,即使原类型实现了某些接口,新defined类型仍需重新实现
type T struct{}

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

type Interface interface {
    M1()
    M2()
}

type T1 T
type Interface1 Interface

func main() {
    var t T
    var pt *T
    var t1 T1
    var pt1 *T1

    DumpMethodSet(&t)  //M1
    DumpMethodSet(&t1) //empty

    DumpMethodSet(&pt)  //M1 M2
    DumpMethodSet(&pt1) //empty

    DumpMethodSet((*Interface)(nil))  //M1 M2
    DumpMethodSet((*Interface1)(nil)) //M1 M2
}

24.4 类型别名的方法集合

// $GOROOT/src/builtin/builtin.go
type byte = uint8
type rune = int32

类型别名与原类型拥有完全相同的方法集合,无论原类型是接口类型还是非接口类型

type T struct{}

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

type Interface interface {
    M1()
    M2()
}

type T1 = T
type Interface1 = Interface

func main() {
    var t T
    var pt *T
    var t1 T1
    var pt1 *T1

    DumpMethodSet(&t)  //M1
    DumpMethodSet(&t1) //M1

    DumpMethodSet(&pt)  //M1 M2
    DumpMethodSet(&pt1) //M1 M2

    DumpMethodSet((*Interface)(nil))  //M1 M2
    DumpMethodSet((*Interface1)(nil)) //M1 M2
}
posted @ 2024-02-13 14:08  brynchen  阅读(4)  评论(0编辑  收藏  举报