如何用好 Go interface


The bigger the interface, the weaker the abstraction.

  1. Concrete types
  2. Abstract types
  3. When & How
    1. 隐藏实现细节
    2. 提供扩展点
  4. 总结

interface 是 Go 语言最精髓的特性之一,一直以来想写一篇关于 interface 的文章,但是一直没敢写。持续几年之久,还是斗胆总结下。

Concrete types

struct 定义数据的内存布局。一些早期建议将方法包含在 struct 中,但是被放弃了。相反,方法如普通函数一样声明在类型之外。描述 (data) 和行为 (methods) 是独立且正交的。

一方面,方法只是一个带有 “receiver” 参数的函数。

type Point struct { x, y float }

func (p Point) Abs() float {
    return math.Sqrt(p.x*p.x + p.y*p.y)
}

func Abs(p Point) float {
	return math.Sqrt(p.x*p.x + p.y*p.y)
}

Abs 编写为一个常规函数,功能没有变化。

什么时候应该使用方法,什么时候应该使用函数呢?如果方法不依赖类型的状态,则应该将其定义为函数。

另一方面,方法在定义其行为时,使用了类型的值时,与所附加的类型紧密关联。方法可以从对应的类型中获取值,如果有指针 “receiver”,还可以操纵其状态。

“类型” 有时候很有用,有时候又很讨厌。因为类型是对底层内存布局的一个抽象,会让代码关注于非业务逻辑上的东西,然而代码又需要在不同类型的数据间做处理。interface 就是其中一种泛型解决方案。

// Package sort provides primitives for sorting slices and user-defined collections.
package sort

// An implementation of Interface can be sorted by the routines in this package.
// The methods refer to elements of the underlying collection by integer index.
type Interface interface {
	// Len is the number of elements in the collection.
	Len() int

	// Less reports whether the element with index i
	// must sort before the element with index j.
	Less(i, j int) bool

	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

// Sort sorts data.
func Sort(data Interface) {
    ...
}

Abstract types

Go 的 interface 仅仅是函数的集合,也定义了行为。 interface 与类型之间没有显式的关系,类型也可以同时满足多个 interface 的要求。

type Abser interface {
    Abs() float
 }
 
 var a Abser
 a = Point{3, 4}
 print(a.Abs())
 a = Vector{1, 2, 3, 4}
 print(a.Abs())

Point 和 Vector 满足 Abser 的要求同时,也符合 interface{} 的要求。不同的是,interface{} 没有任何行为(method)。

When & How

道理我都懂,但是何时使用,如何使用 interface 呢?

答案是,当不需要关心实现细节的时候?

func fn(Parameter) Result

当函数编写者希望隐藏实现细节时,应该把 Result 设定为 interface;当函数编写者希望提供扩展点的时候,应当把 Parameter 设定为 interface;

隐藏实现细节

以 CancelCtx 为例:

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

type cancelCtx struct {
	...
}

newCancelCtx 返回值为 cancelCtx。注意到 cancelCtx 是没有导出的,意味着使用者只能使用 Context 的变量来接收 newCancelCtx 返回值,从而达到隐藏实现的目的。cancelCtx 是否还有其他方法,以及具体如何实现,使用者并无感知。

提供扩展点

当我们需要将文档持久化

type Document struct {
    ...
}

// Save writes the contents of the Document to the file f.
func (d *Document) Save(f *os.File) error

假如实现如上,Save 方法将 *os.File 作为写入的目标。但是此实现存在一些问题:

  1. 该实现排除了将数据写入网络位置的选项。假设网络存储成为需求,则此函数的签名必须更改,从而影响其所有调用者。
  2. 该实现很难测试。为了验证其操作,测试必须在写入文件后读取文件的内容。还必须确保 f 被写入到临时位置,并始终在之后删除。
  3. *os.File 暴露了许多与 Save 无关的方法,比如读取目录和检查路径是否为符号链接。

可以使用接口隔离原则重新定义该方法,优化实现为:

// Save writes the contents of d to the supplied ReadWriterCloser.
func (d *Document) Save(rwc io.ReadWriteCloser) error

然而,此方法仍然违反单一职责原则,它同时负责读取和验证写入的内容。将此部分责任拆分走,继续优化为:

// Save writes the contents of d to the supplied WriteCloser.
func (d *Document) Save(wc io.WriteCloser) error

然而,wc 会在什么情况下关闭。可能 Save 将无条件调用 Close,或者在成功的情况下调用 Close,以上都不是一个好的选择。因此再次优化

// WriteTo writes the contents of d to the supplied Writer.
func (d *Document) WriteTo(w io.Writer) error

接口声明了调用方需要的行为,而不是类型将提供的行为。行为的提供方具有高度的扩展空间,例如:装饰器模式扩展该行为。

type LogWriter struct {
    w  io.Writer
}

func (l *LogWriter)Write(p []byte) (n int, err error) {
    fmt.Printf("write len:%v", len(p))
    return l.w.Write(r)
}

总结

关于 interface,很喜欢以下两句箴言:

Program to an ‘interface’, not an ‘implementation’ —— GoF
Be conservative in what you do, be liberal in what you accept from others —— Robustness Principle

而不是

Return concrete types, receive interfaces as parameter
(由 cancelCtx 的例子可知,如果其类型是导出的 CancelCtx,返回 concrete types 与以上箴言是有出入的)

高级语言赋予了开发者高级的能力,让开发者不要关注具体值、类型,集中精力去处理业务逻辑(行为,method),interface 提供的就是这种能力。除了 interface,其他问题处理也是基于类似的思路:

Don’t just check errors, handle them gracefully
基于行为处理错误,而不是基于值或类型

本文作者:cyningsun
本文地址https://www.cyningsun.com/08-02-2021/using-golang-interface-well.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!

# Golang