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

2次阅读

共计 4613 个字符,预计需要花费 12 分钟才能阅读完成。

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

前言

这是 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 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)
}

变量 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…
正文完
 0