警告
本文最后更新于 2022-12-13,文中内容可能已过时。
接口提供了一种指定对象行为的方式。我们使用接口创建通用抽象,使多个对象可以实现。使 Go 接口与众不同的是它们是隐式声明。没有像 implements
这样的显式关键字来标记一个 X
对象实现 Y
接口。
在 Go 的标准库中有对 sort 的抽象。
1
2
3
4
5
|
type Interface interface {
Len() int // 对象的数量
Less(i, j int) bool // 检查两个对象
Swap(i, j int) // 交换对象
}
|
如果需要排序一个结构体的切片,只需要实现上面的接口就可以:
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
30
31
|
var _ sort.Interface = People{} // 检查接口的实现
type Person struct {
Name string
Age int
}
type People []Person
func (p People) Len() int {
return len(p)
}
func (p People) Less(i, j int) bool {
return p[i].Age < p[j].Age
}
func (p People) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
func main() {
people := People{
{"Alice", 25},
{"Bob", 31},
{"Charlie", 22},
{"David", 42},
}
sort.Sort(people)
fmt.Println(people)
}
|
另外,在开源的微服务框架 Kratos 中对许多通用行为进行了抽象。
在标准库中的 io.Reader 和 io.Writer。io包提供了 I/O 的抽象。这些抽象中,io.Reader
从数据源读取数据,而io.Writer
是将数据写入目标。
比如:我们创建一个 copy 的函数,需要将源拷贝到目的。此函数将使用 *os.File
*bytes.Buffer
、net.Conn
等实现了 io.Reader
和 io.Writer
的作为参数传入。
1
2
3
|
func copy(source io.Reader, dest io.Writer) error {
// ...
}
|
为什么要这样做,主要有两个好处:
- 抽象和实践分离。比如 Kratos 的 blog 示例中,
biz
层使用了 data
层的抽象。
- 方便给这个函数做 mock 测试 。比如上面 copy 例子中如果真实的数据源是
*os.File
,但我们 mock 的时候只需要使用 *bytes.Buffer
就可以。
另外,在 Rob Pike 演讲中提到 The bigger the interface, the weaker the abstraction,接口越简单越好。
现在有一个 IntConfig
结构体,有 Get 和 Set 两个方法,现在需要只能有 Get 的权限。
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
30
31
32
33
34
35
|
type IntConfig struct {
a int
}
func (c *IntConfig) Get() int {
return c.a
}
func (c *IntConfig) Set(value int) {
c.a = value
}
// 定义一个新的接口,只有 Get 方法
type intGetter interface {
Get() int
}
type Foo struct {
i intGetter
}
// Foo 实现 intGetter 的方法
func (f Foo) Get() {
fmt.Println(f.i.Get())
}
func NewFoo(i intGetter) Foo {
return Foo{i: i}
}
func main() {
i := &IntConfig{a: 100}
foo := NewFoo(i) // i 实现了 intGetter 接口的 Get 方法
foo.Get() // 100
}
|
那接口应该在生产端声明,还是在消费端声明?
在Go语言中,在大多数情况下,接口应该存在于消费端一侧。但是,在某些特定的情境下(例如标准库中的接口),我们可能需要将其放在生产者的一侧。即使如此,我们也应该尽量使其最小化,以提高其可重用性并使其更易于组合使用。
源码中接口的实现
1
2
3
4
5
6
7
8
9
10
|
// `iface` is for non-empty interface
type iface struct {
tab *itab
data unsafe.Pointer
}
// `eface` is for the empty interface.
type eface struct {
_type *_type
data unsafe.Pointer
}
|
iface
是一个非空接口类型,它用于描述包含方法的接口类型。
tab
:描述了接口定义和实现的信息。
data
:一个指向数据的unsafe.Pointer
。
eface
是一个空接口类型,它用于描述不包含方法的接口类型。
_type
:描述了数据的类型信息,指向数据的指针指向实际的值,它可以是任何类型的值
unsafe.Pointer
:数据的位置。
1
2
3
4
5
6
|
func main() {
var v interface{}
fmt.Println(v == nil) // true
v = (*int)(nil) // 将一个*int类型的nil指针赋值给了一个空接口变量 v
fmt.Println(v == nil) // false
}
|
解决方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func main() {
var data *byte
var in interface{}
in = data
fmt.Println(IsNil(in))
}
func IsNil(i interface{}) bool {
vi := reflect.ValueOf(i)
if vi.Kind() == reflect.Ptr {
return vi.IsNil()
}
return false
}
|
注意接口值的转换,即类型断言:使用x.(T)
形式进行类型断言时,如果x接口不是T类型,则会触发panic异常。
1
2
3
4
5
6
7
|
func main() {
type Stringer interface {
String() string
}
var i interface{} = int64(100)
s, ok := i.(Stringer) // 如果这样写 s := i.(Stringer) 会抛出异常:panic: interface conversion: int64 is not main.Stringer: missing method String,因为 int64 不能转为 Stringer 类型
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func whatIsThis(i interface{}) {
switch t := i.(type) {
case int:
fmt.Println("It's an integer!")
case string:
fmt.Println("It's a string!")
default:
fmt.Printf("Don't know what type %T is!\n", t)
}
}
func main() {
whatIsThis(42)
whatIsThis("hello")
whatIsThis(true)
}
|
由于 Go 的接口是隐式实现,可以通过下面的方法检查接口是否实现。
1
2
3
4
|
// 验证 *People 是否实现 sort.Interface,可隐式转换成 People
var _ sort.Interface = (*People)(nil)
// 验证 People 是否实现 sort.Interface
var _ sort.Interface = People{}
|
Go Data Structures: Interfaces
How to use interfaces in Go
A Guide to Interfaces in Go