概述

接口类型是由 typeinterface 关键字定义的一组方法集合(唯一确定所代表的接口)
称为方法,而非函数,更多是从这个接口的实现者的角度考虑的

1
2
3
4
type MyInterface interface {
M1(int) error
M2(io.Writer, ...string)
}

方法的参数列表中的形参名和返回值列表中的具名返回值,都不作为区分两个方法的凭据

1
2
3
4
5
// 等价
type MyInterface interface {
M1(a int) error
M2(w io.Writer, args ...string)
}

在接口类型中声明的方法必须是具名的,且方法名在这个接口类型的方法集合中是唯一

类型嵌入:在 Go 1.14 开始,接口类型允许嵌入不同接口类型方法集合存在交集
但需要方法名函数签名也要保持一致,否则会编译报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type I1 interface {
M1()
}

type I2 interface {
M1()
M2()
}

type I3 interface {
I1
I2

M3()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type I1 interface {
M1()
}

type I2 interface {
M1(string)
M2()
}

type I3 interface {
I1
I2 // duplicate method M1

M3()
}

接口类型的方法集合中放入首字母小写非导出方法也是合法的 - 极少使用
如果接口类型的方法集合中包含非导出方法,那么这个接口类型自身通常也是非导出的,仅限于包内使用

context/context.go
1
2
3
4
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

接口类型 - 方法集合为空
通常不需要显式定义空接口类型,而是直接使用 interface{} 的类型字面量

1
type EmptyInterface interface{}

接口类型被定义后,可以用于声明变量,称为接口类型变量,如果没有显式赋予初值,默认值为 nil

1
2
3
4
5
var err error   // err 是一个 error 接口类型的实例变量
var r io.Reader // r 是一个 io.Reader 接口类型的实例变量

fmt.Printf("%#v\n", err) // <nil>
fmt.Printf("%#v\n", r) // <nil>
  1. 类型 T 实现接口类型 I
    • 类型 T 的方法集合是接口类型 I 的方法集合等价集合或者超集
  2. 此时,类型 T 的变量可以作为合法的右值赋值给接口类型 I 的变量

任何类型实现空接口类型的方法集合(),即可以将任何类型的值作为右值,赋值给空接口类型的变量

1
2
3
4
5
6
7
var i interface{} = 15 // ok
i = "hello" // ok

type T struct{}
var t T
i = t // ok
i = &t // ok

类型断言

按 T 为接口类型和非接口类型,语义是不一样的

  1. .(非接口类型)
    • 将一个接口类型变量还原成一个具体的实现类
  2. .(接口类型)
    • 判断是否实现了接口

非接口类型

语义:断言存储在接口类型变量 i 中的值的类型为 T

如果断言失败,则 ok 为 false,而 v 为类型 T 的零值

1
2
// i 为接口类型变量,T 为非接口类型且 T 是想要还原的类型
v, ok := i.(T)
1
2
// 一旦断言失败,就会 panic,不推荐
v := i.(T)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a int64 = 13
var i interface{} = a // i 为空接口类型变量

v1, ok := i.(int64)
fmt.Printf("%v, %T, %v\n", v1, v1, ok) // 13, int64, true

v2, ok := i.(string)
fmt.Printf("%v, %T, %v\n", v2, v2, ok) // , string, false

v3 := i.(int64)
fmt.Printf("%v, %T\n", v3, v3) // 13, int64

v4 := i.([]int) // panic: interface conversion: interface {} is int64, not []int
fmt.Printf("%v, %T\n", v4, v4)

接口类型

语义:断言 i 的值实现了接口类型 T

如果断言成功,v 的类型为 i 的值的类型(更广),并非 T
如果断言失败,v 的类型为 T(更窄),值为 nil

1
2
// T 为接口类型
v, ok := i.(T)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type I interface {
M()
}

type T int

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

func main() {
var t T
var i interface{} = t

v1, ok := i.(I) // 断言成功,v1 的类型依然是 T
if !ok {
panic("the interface value is not of type I")
}

v1.M() // T's M
fmt.Printf("%T\n", v1) // main.T

i = int64(13)
v2, ok := i.(I) // 断言失败,v2 的类型为 I,值为 nil
fmt.Printf("%T, %v\n", v2, ok) // <nil>, false
v2 = 13 // v2 的类型为 I,但 13 为 int 并没有实现 I 接口,所以编译错误
// cannot use 13 (type int) as type I in assignment:
// int does not implement I (missing M method)
}

type switch接口类型类型断言的变种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Animal interface {
shout() string
}

type Dog struct{}

func (d Dog) shout() string {
return "Woof!"
}

type Cat struct{}

func (c Cat) shout() string {
return "Meow!"
}

func main() {
var animal Animal = Dog{}

switch a := animal.(type) { // type switch
case nil:
fmt.Println("nil", a)
case Dog, Cat:
fmt.Println(reflect.TypeOf(a), a.shout()) // main.Dog Woof!
default:
fmt.Println("unknown")
}
}

小接口

接口类型的背后,是通过把类型的行为抽象成契约,降低双方的耦合程度

  1. 隐式契约,自动生效
    • 接口类型和实现者之间的关系是隐式的,无需显式声明 implements
    • 实现者只需要实现接口方法集合中的全部方法,立即生效
  2. 使用小契约
    • 尽量定义小接口,方法个数控制在 1~3
    • 接口越大,抽象程度只会越弱
builtin/builtin.go
1
2
3
type error interface {
Error() string
}
io/io.go
1
2
3
type Reader interface {
Read(p []byte) (n int, err error)
}
net/http/server.go
1
2
3
4
5
6
7
8
9
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}

image-20231119151233381

优势

  1. 接口越小(方法集合小),抽象程度越高(对应的集合空间就越大),极限情况为 interface{}
  2. 易于实现测试
  3. 职责单一,易于复用组合
    • Go 推崇基于接口组合思想
    • 通过嵌入其它已有的接口类型的方式来构建新的接口类型

步骤

  1. 不管接口大小,先抽象出接口
    • 针对问题领域,面向接口进行抽象
  2. 将大接口拆分为小接口
  3. 关注接口的单一职责