Go接口 - interface 最佳实践
interface是GO语言中非常重要的类型,它是用来定义一类方法集,只表示对象的行为(Behavior),GO语言的接口和实现不需要显示关联(也就是常说的duck类型),只要实现了接口所有方法,就可以当做该接口的一个实现,赋值给所有引用该接口的变量,从而满足面向对象编程(OOP)中的两个非常重要原则:依赖倒置、里氏替换。
也正由于这个特点,所以GO接口最佳的实践是:接口尽量的小,根据实际的需求定义的接口大小。
例如:io包体的Reader/Writer
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
2
3
4
5
6
更大的接口:net/http
type File interface {
io.Closer
io.Reader
io.Seeker
Readdir(count int) ([]fs.FileInfo, error)
Stat() (fs.FileInfo, error)
}
2
3
4
5
6
7
善于合理运用interface,可以使你的代码简洁,更好的解耦,从而提高程序的扩展性,比如:在涉及到input/output设计的时候,引入io.Reader/io.Writer是一个不错的选择。
借用Steve Francia分享的静态网站生成器HUGO的例子:
Bad
func (page *Page) saveSourceAs(path) {
b := new(bytes.Buffer)
b.Write(page.Resource.Content)
page.saveSource(b.Bytes(), path)
}
// by 数组需要通过bytes.NewReader转化后储存
func (p *Page) saveSource(by []byte, inPath string) {
saveToDisk(inPath,bytes.NewReader(by))
}
2
3
4
5
6
7
8
9
Good
func (page *Page) saveSourceAs(path) {
b := new(bytes.Buffer)
b.Write(page.Resource.Content)
page.saveSource(b, path)
}
// by 数组需要通过bytes.NewReader转化后储存
func (p *Page) saveSource(b io.Reader, inPath string) {
saveToDisk(inPath, b)
}
2
3
4
5
6
7
8
9
注:saveSource方法中定义的接收参数是io.Reader,清晰简洁, 易扩展,该方法可以接收所有实现该接口的实例。
除了bytes.Buffer实现了Read方法
func (b *Buffer) Read(p []byte) (n int, err error) {
b.lastRead = opInvalid
if b.empty() {
// Buffer is empty, reset to recover space.
b.Reset()
if len(p) == 0 {
return 0, nil
}
return 0, io.EOF
}
n = copy(p, b.buf[b.off:])
b.off += n
if n > 0 {
b.lastRead = opRead
}
return n, nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
还可以是Conn(http包),实现从网络读取数据:
type Conn interface {
Read(b []byte) (n int, err error)
2
或是文件对象File(os包),从本地磁盘读取
func (f *File) Read(b []byte) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
return n, f.wrapErr("read", e)
}
2
3
4
5
6
7
另外,注意的是接口粒度以满足需求为准,不要有额外的方法,否则会导致依赖不清晰。
例如:我们在网络编程中经常会用到net包的Conn接口:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
2
3
4
5
6
7
8
9
10
通常我们收到一个conn的时候,会开启一个协程读取数据:
Bad
func handleConn(c net.Conn) {
这里我们使用net.Conn,实际上只是read数据,不会调用Conn其他方法,用io.Reader接口就足够:
Good
func handleConn(r io.Reader)
如果还涉及关闭连接就用io.ReadCloser
Good
func handerConn(rc io.ReadCloser)
颗粒度小的接口可以清晰依赖的同时,也方便单测对接口进行mock,更好的聚焦于目标逻辑的测试。
那要在哪定义iterface呢?!
首先:一般会把接口定义放在需要使用该接口的package下,而不是实现包下,通常实现类返回的是结构体或是对应指针,这样实现类扩展新的方法就不需要修改对应的接口。同样以io包中的io.Reader/io.Wirter为例,接口定义在io包中,而他的实现bytes.Buffer、os.File等都是在不同包中。
其次:这就关于接口定义的时机。
一般来说是由依赖方驱动。
在当前模块有依赖外部服务的时候,这时候就会定义一个接口,来对外部的依赖资源进行抽象解耦,屏蔽接口的实现。相反,依赖的提供方在不知道使用方需求的时候,定义接口也就没什么意义。