Go接口 - interface 最佳实践

2021/11/15 探索Go

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)
}
1
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)
}
1
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))
}
1
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)
}
1
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
}
1
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)
1
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)
}
1
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
}
1
2
3
4
5
6
7
8
9
10

通常我们收到一个conn的时候,会开启一个协程读取数据:

Bad

func handleConn(c net.Conn) {
1

这里我们使用net.Conn,实际上只是read数据,不会调用Conn其他方法,用io.Reader接口就足够:

Good

func handleConn(r io.Reader)
1

如果还涉及关闭连接就用io.ReadCloser

Good

func handerConn(rc io.ReadCloser)
1

颗粒度小的接口可以清晰依赖的同时,也方便单测对接口进行mock,更好的聚焦于目标逻辑的测试。

那要在哪定义iterface呢?!

首先:一般会把接口定义放在需要使用该接口的package下,而不是实现包下,通常实现类返回的是结构体或是对应指针,这样实现类扩展新的方法就不需要修改对应的接口。同样以io包中的io.Reader/io.Wirter为例,接口定义在io包中,而他的实现bytes.Buffer、os.File等都是在不同包中。

其次:这就关于接口定义的时机。

一般来说是由依赖方驱动。

在当前模块有依赖外部服务的时候,这时候就会定义一个接口,来对外部的依赖资源进行抽象解耦,屏蔽接口的实现。相反,依赖的提供方在不知道使用方需求的时候,定义接口也就没什么意义。