本文参加了思否技术征文,欢送正在浏览的你也退出。

前言

这是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.

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

type Reader interface {    Read(p []byte) (n int, err error)}
Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch space during the call. If some data is available but not len(p) 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)}
Write writes len(p) bytes from p to the underlying data stream. It returns the number of bytes written from p (0 <= n <= len(p)) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len(p). 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 {    // ...}
  • 形式2:应用io.Reader和io.Writer作为参数。因为os.File实现了io.Reader和io.Writer,所以os.File也能够作为上面函数的参数,传参给source和dest。

    func copySourceToDest(source io.Reader, dest io.Writer) error {    // ...}

    办法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}

何时应用interface

上面介绍2个常见的应用interface的场景。

公共行为能够形象为interface

比方下面介绍过的io.Reader和io.Writer就是很好的例子。Go规范库里大量应用interface,感兴趣的能够去查阅源代码。

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

比方上面这段代码示例:

package maintype 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)}

变量o能够间接拜访Halloween构造体里的所有成员变量。

有时候咱们可能想做一些限度,不心愿构造体里的成员变量被随便拜访和批改,那就能够借助interface。

type Country interface {   UK(string) string   US(string) string}func NewHalloween() Country {   o := Halloween { Month: "October", Day: "31" }   return Country(o)}

咱们定义一个新的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,这样会让代码更简洁易懂。

举荐浏览

  • Go面试题系列,看看你会几题?
  • Go常见谬误第1篇:未知枚举值
  • Go常见谬误第2篇:benchmark性能测试的坑
  • Go常见谬误第3篇:go指针的性能问题和内存逃逸
  • Go常见谬误第4篇:break操作的注意事项
  • Go常见谬误第5篇:Go语言Error治理
  • Go常见谬误第6篇:slice初始化常犯的谬误
  • Go常见谬误第7篇:不应用-race选项做并发竞争检测
  • Go常见谬误第8篇:并发编程中Context应用常见谬误
  • Go常见谬误第9篇:应用文件名称作为函数输出
  • Go常见谬误第10篇:Goroutine和循环变量一起应用的坑
  • Go常见谬误第11篇:意外的变量遮蔽(variable shadowing)
  • Go常见谬误第12篇:如何破解箭头型代码
  • Go常见谬误第13篇:init函数的常见谬误和最佳实际
  • Go常见谬误第14篇:适度应用getter和setter办法

开源地址

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

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

集体网站:Jincheng's Blog。

知乎:无忌。

福利

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

关注公众号「coding进阶」,发送音讯 backend 支付材料礼包,这份材料会不定期更新,退出我感觉有价值的材料。

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

References

  • https://livebook.manning.com/...
  • https://github.com/jincheng9/...
  • https://bbs.huaweicloud.com/b...