Golang Interface

警告
本文最后更新于 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.Readerio.Writer。io包提供了 I/O 的抽象。这些抽象中,io.Reader 从数据源读取数据,而io.Writer 是将数据写入目标。

比如:我们创建一个 copy 的函数,需要将源拷贝到目的。此函数将使用 *os.File *bytes.Buffernet.Conn 等实现了 io.Readerio.Writer 的作为参数传入。

1
2
3
func copy(source io.Reader, dest io.Writer) error { 
    // ... 
}

为什么要这样做,主要有两个好处:

  1. 抽象和实践分离。比如 Kratos 的 blog 示例中,biz 层使用了 data 层的抽象。
  2. 方便给这个函数做 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

相关内容