Go 佳库面面观

ictar · 2018-01-22 18:17:11 · 2488 次点击 · 预计阅读时间 7 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-01-22 18:17:11 的文章,其中的信息可能已经有所发展或是发生改变。

本文将列出从一个好的 Go 库里,我希望得到的东西的一个简短清单(排名不分先后)。这是对高效 Go(effective go)列表、Go 代码评审意见列表和 Go 箴言列表的补充。

一般来说,当做某事有两种合理的方式的时候,选择不违反这些规则的那一项。只有在有非常强力的理由时才违反这些规则。

依赖

加标签的库版本

使用 git 标签来管理你的库版本。语义版本化是一个合理的系统。如果你对语义版本化的反对在意,那么,你就不是本文的目标读者 :smile:

没有非标准库依赖

这个有时候会难以达成,但是,管理一个库的依赖使得升级到最新版本变得轻松,并且允许库用户更好地得出应用中的逻辑。如果你将依赖树维持得相当相当小,那么,你就真的可以让你的程序维护更简单。

抽象非标准库依赖到它们自己的包里

这是上面非标准库依赖要求的必然结果。如果你的库绝对需要非标准库依赖,那么,试着将其分成两个包:一个包用于核心逻辑,而另一个包拥有外部依赖,使用前一个包的逻辑。

打个比方,如果你正在写一个包,它捕获 Go 的堆栈追踪,然后将其上传到 Amazon’s S3,那么,写两个包。

  1. 一个包捕获 Go 的堆栈追踪,然后将其交到一个接口
  2. 第一个包使用的接口的 S3 实现。

第二个包可以为用户简化两部分的粘合。这种抽象层次让用户利用你的核心功能,同时用自己的存储层来替换 S3。而在第二个包中的粘合操作,可以避免给那些想要将他们的数据上传到 S3 的大部分人增加额外的负担。

这里的关键部分是两个分开的包。这使得用户可以注入你的核心逻辑,而不会污染到他们自己的依赖树。

不要使用 /vendor

如果你的库用 vendor 来管理依赖,那么在包管理器试图展开 vendor 的时候,奇奇怪怪的副作用可能会发生;或者 vendor 污染你的公共 API,使得集成变得困难或者不可能。如果你真的需要一个确切的实现,那么请明确复制它

使用依赖管理,这样别人就可以跑你的测试了

你的库应该尝试不要有外部依赖。但是,如果必须有,那么使用某种类型的依赖管理,传达初始运行你的测试的依赖版本,从而使得库用户可以以一种一致的方式运行你的单元测试。

API

没有全局可变状态

全局可变状态使得代码难以被推断、展开、存根、测试和退出。

空对象具有合理的行为

零值有用

nil 实例上的读操作与空实例上的读操作行为一致

许多 Go 的内部结构反映了这种行为。

若非必要,避免构造器函数

这是让零值有用的副作用。

最小化公共函数

做更少事情的库倾向于做得更好。

拥有少数函数的小接口

接口越大,抽象越弱.

接受接口,返回结构

阅读这里,获取更多细节。

在不违反竞争的情况下配置运行时可变

如果你的库维护复杂的状态,那么仅仅重新初始化,允许用户在应用运行的时候修改合理的配置参数,这非简单的过程。

我可以接口化,并且无需导入你的库的 API

你的库很棒,但是终有一天我会想要将其淘汰出去。Go 的类型系统有时会造成阻碍。然而,总的来说,隐式接口胜于过于强大的静态类型。如果有可能的话,最好将标准库类型作为函数参数类型和返回值类型,这样,用户就可以根据你的结构创建接口,以便于后面进行库替换。

type AvoidThis struct {}  
type Key string  
func (a *AvoidThis) Convert(k Key) {... }
type PreferThis struct {}  
func (p *PreferThis) Convert(k string) { ... }

API 调用时创建最小的对象(GC)

CPU 通常是避无可避的,但是,重新考虑你的 API 会使得最小化 API 调用期间的垃圾回收成为可能。例如,创建不强制垃圾回收的 API。事后优化实现很容易,但是事后优化 API 则几乎不可能

type AvoidThis struct {}  
func (a *AvoidThis) Bytes() []byte { ... }
type PreferThis struct {}  
func (p *PreferThis) WriteTo(w Writer) (n int64, err error) { ... }

无副作用导入

我个人不赞同基于导入创建 全局副作用行为Go 标准库 模式。这种行为通常涉及全局可变状态,当多次包含 /vendor 中的库时会引起有趣的问题,并且删除许多自定义选项。

当存在替代方法的时候,避免使用 context.Value

我前一篇文章上有关于这个想法的扩展。

init 中避免使用复杂逻辑

init 函数对默认值的创建很有用,但是也是用户不可能自定义或忽略的逻辑。如果没有好的理由的话,不要从用户那里拿走你的库的控制权。因此,避免在 init 中生成后台 goroutine,而是最好让用户明确地要求后台行为。

允许注入全局依赖

例如,当允许用户提供 http.Client 时,不要强制使用 http.DefaultClient

错误

检查所有错误

如果你的库接受一个接口作为输入,而有人给了你一个返回错误的实现,那么用户会期望你会检查并以某种方式处理错误,或者以某种方式把它传回给调用者。检查错误不只是意味着将其返回给上层调用栈,尽管这样有时是合理的,但是,你可以记录它,改变返回值并将其作为结果,使用后备代码,或者只是增加一个内部统计计数器,这样,用户就会知道发生了一些事情,而不是在不知道有什么不对的情况下失败。

通过行为暴露错误,而不是类型

这是以库为中心的 Dave的 Assert errors for behaviour, not type。的等价说明。更多信息,看这里

不要 panic

就是不要

并发

避免创建 goroutine

这是由 CodeReviewComments 中的同步函数部分推导出的更明确的规则。同步函数为库用户提供更多的控制权。goroutine 有时在并行化逻辑时是有用的,但是,作为一名库作者,你应该从不使用 goroutine 并且找出使用它们的原因开始,而不是先使用 goroutine,然后争论着摆脱它们。

允许后台 goroutine 干净地停止

这是 goroutine 生命周期 反馈的首选限制。应该有一种方式,以一种不会发出虚假错误的方式,结束你的库创建的任意 goroutine。

公开 API 中避免使用 channel

这是一种代码异味,暗示并发发生在库级别,而不是让你的库用户控制并发。

所有长的阻塞操作都采用 context.Context

Context 是让你的库用户控制何时应该中断操作的标准方式。

调试

导出内部统计信息

我需要监控你的库的效率、使用模式和耗时。以某种方式公开这些统计数据,这样,我就可以将其导入到我最爱的 度量系统中。

公开 expvar.Var 信息

通过 expvar 公开内部配置和状态信息,从而允许用户快速调试他们的应用使用你的库的方式,而不仅仅是他们认为他们如何使用它。

支持可调试性

最终,你的库会有错误。或者用户会错误地使用你的库,然后需要弄清楚原因。如果你的库具备任何合理的复杂性,那么请提供调试或者跟踪此信息的方法。可以使用调试日志,或者上下文调试模式

合理的 Stringer 实现

Stringer 是的人们更容易用你的库来调试代码。

轻松可定制的日志器

没有被广泛接受的 Go 日志库。公开一个日志接口,从而不强制我导入你最爱的日志库。

代码的整洁

通过一个合理的 gometalinter 检查子集

Go 简单的语法和优秀的标准库函数允许广泛的静态代码检查器,这些检查器汇总在 gometalinter 中。你的默认状态,特别是当你是 Go 的小萌新的时候,应该只是通过所有这些检查器。只有在你能够提供原因的时候才违反它们,然后提供两种合理的实现,并且选择那个通过 linter 的那个实现。

没有 0% 单元测试覆盖率的函数

100% 测试覆盖率是极端的,而 0% 测试覆盖率几乎不是什么好事。这是一项难以量化的规则,所以我已经决定“没有任何函数应该具备 0% 的测试覆盖率”是最低限度了。你可以使用 Go 的 cover 工具获取每个函数测试覆盖率。

# go test -coverprofile=cover.out context  
ok   context 2.651s coverage: 97.0% of statements
# go tool cover -func=cover.out  
context/context.go:162: Error  100.0%  
context/context.go:163: Timeout  100.0%  
context/context.go:164: Temporary 100.0%  
context/context.go:170: Deadline 100.0%  
context/context.go:174: Done  100.0%  
context/context.go:178: Err  100.0%  
    ...

存储布局

避免将一个结构的函数拆分到多个文件中

Go 允许你把一个结构的函数放到多个文件中。这在使用构建标志时,是非常有用的,但是,如果你把它作为组织结构的方式,那么,这说明你的结构太大了,应该把它分解成多个部分。

使用 /internal

/internal 包严重使用不足。我推荐二进制文件和库都利用 /internal 来隐藏不打算导入的公共函数。隐藏你的公共导入空间也使得用户更清楚应该导入哪些包,以及要到哪里寻找有用的逻辑。


via: https://medium.com/@cep21/aspects-of-a-good-go-library-7082beabb403

作者:Jack Lindamood  译者:ictar  校对:rxcai

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


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

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

2488 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
3 回复  |  直到 2018-08-13 18:38:56
goal
goal · #1 · 7年之前

不错!

swen
swen · #2 · 7年之前

感谢分享,翻译的有点苦涩~

embiid
embiid · #3 · 7年之前

/internal包是什么 有人指点一下吗

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