在 go 中设计你的 interface

2022-09-29 15:39:30 浏览数 (1)

导语 go 的设计哲学有许多不同于其他语言(java、python),interfaces 更是如此,在 java 中需要明确指明实现了哪个接口,而在 go 中你只要实现了一个接口的方法,那么就认为你实现了这个接口。

go 的 interface 与 java、python 等语言中的对象是如此的相近又如此的不同,所以导致之前从其他语言转过来的人来,会按照他们之前接触过的语言来使用 go 的 interface,这可大大的错误了。

接口大小

首先接口到底该设计成多大?习惯了 java 这种提前设计接口,要声明实现的接口的人会将接口过早设计并且设计的足够丰富。而这恰恰是错误的。go 接口是只要你实现了这个接口的所有方法,那么你就实现了这个接口,接口的实现是隐式的。所以接口设计的越大,封装性也就越弱。你如果看过 go 的源码就会发现:通常,interfaces 只有少数几个(1-2)方法。最出名的莫过于 io.Reader 、io.Closer、 io.Writer 这几个 interface 了:

代码语言:javascript复制
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Closer interface{
    Close() error
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

 而其他的接口则可以直接使用组合来扩展自己的接口,例如,同为 io 包的:

代码语言:javascript复制
type ReadCloser interface {
	Reader
	Closer
}

 以及 http 包中的 File:

代码语言:javascript复制
type File interface {
	io.Closer
	io.Reader
	io.Seeker
	Readdir(count int) ([]fs.FileInfo, error)
	Stat() (fs.FileInfo, error)
}

 所以请记住:接口越大意味着抽象有可能越弱

接口位置

Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values ————CodeReviewComments · golang/go Wiki (github.com)

按常规理解是应该把接口定义在实现的地方,但是 go 中却推荐接口定义在使用的地方。这是因为 go 中不推荐在使用之前就定义接口,因为很难判断一个接口是否有必要使用,更不要说它应该包含哪些方法了(相信写过 java 的深有体会)。而在实际使用的包中定义接口则可以让使用者自己定义它所需要的接口(这也是真实需要的)。这点看 io.Copy 方法就是接受在一个包中定义的 Writer 与 Reader 作为参数,而且实现者应该返回一个具体的类型(pointer or struct) 。这样当需要添加扩展新的方法时也是开放的而非封闭的。

代码语言:javascript复制
func Copy(dst Writer, src Reader) (written int64, err error)

 当然这也不是绝对的,不要拿着某句话当做圣旨!!go 的标准库的 hash 包就是单独将接口抽离出来而其实现是放在其子包 hash/* 中的。

代码语言:javascript复制
type Hash interface {
	// Write (via the embedded io.Writer interface) adds more data to the running hash.
	// It never returns an error.
	io.Writer

	// Sum appends the current hash to b and returns the resulting slice.
	// It does not change the underlying hash state.
	Sum(b []byte) []byte

	// Reset resets the Hash to its initial state.
	Reset()

	// Size returns the number of bytes Sum will return.
	Size() int

	// BlockSize returns the hash's underlying block size.
	// The Write method must be able to accept any amount
	// of data, but it may operate more efficiently if all writes
	// are a multiple of the block size.
	BlockSize() int
}

// Hash32 is the common interface implemented by all 32-bit hash functions.
type Hash32 interface {
	Hash
	Sum32() uint32
}

// Hash64 is the common interface implemented by all 64-bit hash functions.
type Hash64 interface {
	Hash
	Sum64() uint64
}

 这样做一是可以更好命名:hash.Hash 而不是 adler32.Hash,二是提供了更清晰的说明如何实现接口。一个只有接口的单独包提示散列函数应该具有 hash.Hash 接口所需的方法。

类型导出

If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself. Exporting just the interface makes it clear the value has no interesting behavior beyond what is described in the interface. It also avoids the need to repeat the documentation on every instance of a common method. 

---- Effective Go - The Go Programming Language (golang.org)

如果一个类型的存在只是为了实现一个接口并且永远不会在该接口之外导出方法(没有多余的方法),则不需要导出该类型本身。 仅导出接口可以清楚地表明该值除了接口中描述的之外没有其他有趣的行为。 它还避免了对通用方法的每个实例重复文档的需要。

可以看 hash 包中 crc32 的实现。它只实现了 hash.Hash32 接口,所以它是一个非导出的类型:digest, 并且 New 方法返回的是 hash.Hash32 类型。这样对外界来说永远只有 hash.Hash32 而且在使用crc32.New 时明确知道该方法返回的类型中仅有并且唯一实现了 hash.Hash32 的方法,没有任何一个多余的方法。

总结

  • 接口设计时要尽量的!!!
  • 不要过早的设计接口,推迟返回接口,直到你的包中有多重类型仅实现了该接口,这会让你确信你的抽象是正确的。

最后如果你的接口是非导出的则可以不受以上的限制,而且即使是导出的也不一定都适合,例如:net 包的 Conn 接口就拥有多达:8个方法 go/net.go at c170b14c2c1cfb2fd853a37add92a82fd6eb4318 · golang/go (github.com)。

所以还是那句话:永远别迷信一种道理,抱着谨慎的态度去看待每一个标准。

参考文献:

Exposing interfaces in Go | Efe’s Blog (efekarakus.com)

CodeReviewComments · golang/go Wiki (github.com)

Effective Go - The Go Programming Language (golang.org)

0 人点赞