• Go常见错误第15篇:interface使用的常见错误和最佳实践


    前言

    这是Go常见错误系列的第15篇:interface使用的常见错误和最佳实践。

    素材来源于Go布道者,现Docker公司资深工程师Teiva Harsanyi

    本文涉及的源代码全部开源在:Go常见错误源代码,欢迎大家关注公众号,及时获取本系列最新更新。

    常见错误和最佳实践

    interface是Go语言里的核心功能,但是在日常开发中,经常会出现interface被乱用的情况,代码过度抽象,或者抽象不合理,导致代码晦涩难懂。

    本文先带大家回顾下interface的重要概念,然后讲解使用interface的常见错误和最佳实践。

    interface重要概念回顾

    interface里面包含了若干个方法,大家可以理解为一个interface代表了一类群体的共同行为。

    结构体要实现interface不需要类似implement的关键字,只要该结构体实现了interface里的所有方法即可。

    我们拿Go语言里的io标准库来说明interface的强大之处。io标准库包含了2个interface:

    • io.Reader:表示从某个数据源读数据
    • io.Writer:表示写数据到目标位置,比如写到指定文件或者数据库
    Figure 2.3 io.Reader reads from a data source and fills a byte slice, whereas io.Writer writes to a target from a byte slice.

    img

    io.Reader这个interface里只有一个Read方法:

    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    
    • 1
    • 2
    • 3

    Read reads up to len§ bytes into p. It returns the number of bytes read (0 <= n <= len§) and any error encountered. Even if Read returns n < len§, it may use all of p as scratch space during the call. If some data is available but not len§ bytes, Read conventionally returns what is available instead of waiting for more.

    如果某个结构体要实现io.Reader,需要实现Read方法。这个方法要包含以下逻辑:

    • 入参:接受元素类型为byte的slice作为方法的入参。
    • 方法逻辑:把Reader对象里的数据读出来赋值给p。比如Reader对象可能是一个strings.Reader,那调用Read方法就是把string的值赋值给p。
    • 返回值:要么返回读到的字节数,要么返回error。

    io.Writer这个interface里只有一个Write方法:

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

    Write writes len§ bytes from p to the underlying data stream. It returns the number of bytes written from p (0 <= n <= len§) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len§. Write must not modify the slice data, even temporarily.

    如果某个结构体要实现io.Writer,需要实现Write方法。这个方法要包含以下逻辑:

    • 入参:接受元素类型为byte的slice作为方法的入参。
    • 方法逻辑:把p的值写入到Writer对象。比如Writer对象可能是一个os.File类型,那调用Write方法就是把p的值写入到文件里。
    • 返回值:要么返回写入的字节数,要么返回error。

    这2个函数看起来非常抽象,很多Go初级开发者都不太理解,为啥要设计这样2个interface?

    试想这样一个场景,假设我们要实现一个函数,功能是拷贝一个文件的内容到另一个文件。

    • 方式1:这个函数用2个*os.Files作为参数,来从一个文件读内容,写入到另一个文件

      func copySourceToDest(source *io.File, dest *io.File) error {
          // ...
      }
      
      • 1
      • 2
      • 3
    • 方式2:使用io.Reader和io.Writer作为参数。由于os.File实现了io.Reader和io.Writer,所以os.File也可以作为下面函数的参数,传参给source和dest。

      func copySourceToDest(source io.Reader, dest io.Writer) error {
          // ...
      }
      
      • 1
      • 2
      • 3

      方法2的实现会更通用一些,source既可以是文件,也可以是字符串对象(strings.Reader),dest既可以是文件,也可以是其它数据库对象(比如我们自己实现一个io.Writer,Write方法是把数据写入到数据库)。

    在设计interface的时候要考虑到简洁性,如果interface里定义的方法很多,那这个interface的抽象就会不太好。

    引用Go语言设计者Rob Pike在Gopherfest 2015上的技术分享Go Proverbs with Rob Pike中关于interface的说明:

    The bigger the interface, the weaker the abstraction.

    当然,我们也可以把多个interface结合为一个interface,在有些场景下是可以方便代码编写的。

    比如io.ReaderWriter就结合了io.Reader和io.Writer的方法。

    type ReadWriter interface {
        Reader
        Writer
    }
    
    • 1
    • 2
    • 3
    • 4

    何时使用interface

    下面介绍2个常见的使用interface的场景。

    公共行为可以抽象为interface

    比如上面介绍过的io.Reader和io.Writer就是很好的例子。Go标准库里大量使用interface,感兴趣的可以去查阅源代码。

    使用interface让Struct成员变量变为private

    比如下面这段代码示例:

    package main
    type Halloween struct {
       Day, Month string
    }
    func NewHalloween() Halloween {
       return Halloween { Month: "October", Day: "31" }
    }
    func (o Halloween) UK(Year string) string {
       return o.Day + " " + o.Month + " " + Year
    }
    func (o Halloween) US(Year string) string {
       return o.Month + " " + o.Day + " " + Year
    }
    func main() {
       o := NewHalloween()
       s_uk := o.UK("2020")
       s_us := o.US("2020")
       println(s_uk, s_us)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    变量o可以直接访问Halloween结构体里的所有成员变量。

    有时候我们可能想做一些限制,不希望结构体里的成员变量被随意访问和修改,那就可以借助interface。

    type Country interface {
       UK(string) string
       US(string) string
    }
    func NewHalloween() Country {
       o := Halloween { Month: "October", Day: "31" }
       return Country(o)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们定义一个新的interface去实现Halloween的所有方法,然后NewHalloween返回这个interface类型。

    那外部调用NewHalloween得到的对象就只能使用Halloween结构体里定义的方法,而不能访问结构体的成员变量。

    乱用Interface的场景

    interface在Go代码里经常被乱用,不少C#或者Java开发背景的人在转Go的时候,通常会先把接口类型抽象好,再去定义具体的类型。

    然而,这并不是Go里推荐的。

    Don’t design with interfaces, discover them.

    —Rob Pike

    正如Rob Pike所说,不要一上来做代码设计的时候就先把interface给定义了。

    除非真的有需要,否则是不推荐一开始就在代码里使用interface的。

    最佳实践应该是先不要想着interface,因为过度使用interface会让代码晦涩难懂。

    我们应该先按照没有interface的场景去写代码,如果最后发现使用interface能带来额外的好处,再去使用interface。

    注意事项

    使用interface进行方法调用的时候,有些开发者可能遇到过一些性能问题。

    因为程序运行的时候,需要去哈希表数据结构里找到interface的具体实现类型,然后调用该类型的方法。

    但是这个开销是很小的,通常不需要关注。

    总结

    interface是Go语言里一个核心功能,但是使用不当也会导致代码晦涩难懂。

    因此,不要在写代码的时候一上来就先写interface。

    要先按照没有interface的场景去写代码,如果最后发现使用interface真的可以带来好处再去使用interface。

    如果使用interface没有让代码更好,那就不要使用interface,这样会让代码更简洁易懂。

    推荐阅读

    开源地址

    文章和示例代码开源在GitHub: Go语言初级、中级和高级教程

    公众号:coding进阶。关注公众号可以获取最新Go面试题和技术栈。

    个人网站:Jincheng’s Blog

    知乎:无忌

    福利

    我为大家整理了一份后端开发学习资料礼包,包含编程语言入门到进阶知识(Go、C++、Python)、后端开发技术栈、面试题等。

    关注公众号「coding进阶」,发送消息 backend 领取资料礼包,这份资料会不定期更新,加入我觉得有价值的资料。

    发送消息「进群」,和同行一起交流学习,答疑解惑。

    References

    • https://livebook.manning.com/book/100-go-mistakes-how-to-avoid-them/chapter-2/
    • https://github.com/jincheng9/go-tutorial/tree/main/workspace/lesson18
    • https://bbs.huaweicloud.com/blogs/348512
  • 相关阅读:
    是时候展示给大家这5款压箱底的软件了
    Java面试时,该如何准备亮点?
    MogaFX外汇市场保持相对稳定
    软件测试面试指导,做到有备无患,offer手到擒来
    Vue2023 面试归纳及复习
    大数据学习06-Spark分布式集群部署
    【Python基础:面向对象之魔法方法】
    Stable diffusion 用DeOldify给黑白照片、视频上色
    quartz-动态任务开发
    前端应用发布到nodejs server后浏览器刷新404问题
  • 原文地址:https://blog.csdn.net/perfumekristy/article/details/128058578