Go接口 - interface 最佳实践

uuid · · 469 次点击 · · 开始浏览    
interface是GO语言中非常重要的类型,它是用来定义一类方法集,只表示对象的行为(Behavior),GO语言的接口和实现不需要显示关联(也就是常说的duck类型),只要实现了接口所有方法,就可以当做该接口的一个实现,赋值给所有引用该接口的变量,从而满足面向对象编程(OOP)中的两个非常重要原则:依赖倒置、里氏替换。 也正由于这个特点,所以GO接口最佳的实践是:接口尽量的小,根据实际的需求定义的接口大小。 例如:io包体的Reader/Writer ```go type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } ``` 更大的接口:net/http ```go type File interface { io.Closer io.Reader io.Seeker Readdir(count int) ([]fs.FileInfo, error) Stat() (fs.FileInfo, error) } ``` 善于合理运用interface,可以使你的代码简洁,更好的解耦,从而提高程序的扩展性,比如:在涉及到input/output设计的时候,引入io.Reader/io.Writer是一个不错的选择。 借用Steve Francia分享的静态网站生成器HUGO的例子: Bad ```go 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)) } ``` Good ```go 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) } ``` 注:saveSource方法中定义的接收参数是io.Reader,清晰简洁, 易扩展,该方法可以接收所有实现该接口的实例。 除了bytes.Buffer实现了Read方法 ```go 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 } ``` 还可以是Conn(http包),实现从网络读取数据: ```go type Conn interface { Read(b []byte) (n int, err error) ``` 或是文件对象File(os包),从本地磁盘读取 ```go 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) } ``` 另外,注意的是接口粒度以满足需求为准,不要有额外的方法,否则会导致依赖不清晰。 例如:我们在网络编程中经常会用到net包的Conn接口: ```go 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 } ``` 通常我们收到一个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等都是在不同包中。 其次:这就关于接口定义的时机。 一般来说是由依赖方驱动。 在当前模块有依赖外部服务的时候,这时候就会定义一个接口,来对外部的依赖资源进行抽象解耦,屏蔽接口的实现。相反,依赖的提供方在不知道使用方需求的时候,定义接口也就没什么意义。 **我的博客**:[Go接口 - interface 最佳实践 | 艺术码农的小栈](https://itart.cn/blogs/2021/explore/go-interface-best-practice.html)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077

469 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传