Go 语言“十诫”

2023-05-26 0 941

Go 语言“十诫”

译者 | 袁钢明的赞许帐户 白眉林 | 李德姝黎

Go 语言“十诫”

责任编辑译者自 John Arundel 的《Ten commandments of Go》[1]。并谓:

作为一位兼职的 Go 词汇小说家[2]和同学[3],我花了很多时间和小学生们一起,协助她们写下更明晰、更快、更管用的 Go 流程。我发现,我给她们的提议可以概括归纳为两套通用型准则,在这里我将那些准则撷取给大家。

你如果是无趣的

Go 街道社区讨厌一致意见(consensus)。比如说:Go 源标识符有两个由 gofmt 禁制的标准化的标识符文件格式规范化。反之亦然,不论你要化解什么问题,一般来说都有两个国际标准的、近似于 Go 处事艺术风格的方式来化解。有时候它是国际标准的形式,即使它是最合适的形式,但一般来说它只是最合适的形式,即使它是国际标准的形式。

要杯葛住创意结构设计、风尚或(最差劲的是)精明的驱使,那些并非 Go 的处事艺术风格。Go 处事艺术风格的标识符单纯、无趣,一般来说十分罗嗦,而且最关键的是隐式的艺术风格(由于这个其原因,有些人把 Go 称作面向全国隐式(obviousness-oriented)艺术风格的编程词汇)。

布季谢疑点时,请遵从最轻精采准则[4]。谋求努力做到一清二楚[5]。要隐晦,要单纯,要隐式,要无趣。

这并并非说在计算机科学微观没有展现更让人瞠目结舌的典雅和艺术风格的内部空间了;总之有。但那是在结构设计微观上,而并非一般而言标识符行。标识符并不关键,它如果以被即时代替。关键的是流程。

你如果以试验为重

在 Go 中,两个常见的错误是先写了一些函数(比如说:GetDataFromAPI),然后在考虑如何试验它时不知所措。函数通过网络进行了真正的 API 调用,它向终端打印东西,它写磁盘文件了,这是两个可怕的的不可试验性的坑。

不要先写那个函数,而是先写两个试验(比如说:TestGetDataFromAPI)。如何写这样两个试验呢?它必须为函数的调用提供两个本地的 TLS 试验服务器,所以你需要一种方式来注入这种依赖。它要写数据到 io.Writer,你反之亦然需要为此注入两个模拟外部世界的本地依赖,比如说:bytes.Buffer。

现在,当你开始编写 GetDataFromAPI 函数时,一切都将变得很容易了。它的所有依赖关系都被注入,所以它的业务逻辑与它与外部世界的交互和监听形式完全脱钩。

HTTP handler 也是如此。两个 HTTP handler 的唯一工作是解析请求中的数据,将其传递给某个业务逻辑函数来计算结果,并将结果文件格式化到ResponseWriter。这几乎不需要试验,所以你的大部分试验将在业务逻辑函数本身,而并非handler。我们知道HTTP的工作原理。

你如果试验行为,而并非函数

如果你想知道如何在不实际调用 API 的情况下试验这个函数,那么答案很单纯:”不要试验这个函数”。

你需要试验的并非一些函数,而是一些行为。例如,两个可能是”给定一些用户输入,我可以正确地组合 URL 并以正确的参数调用 API。” 另两个可能是”给定 API 返回的一些 JSON 数据,我可以正确地将其解包到某个 Go 结构体中。”

当你沿着这样的思路考量问题的化解方式的时候,写试验就容易多了:你可以想象一些这类函数,它们每个函数都会接受一些输入,并产生一些输出,并且很容易给它们编写单元试验。有些事情它们是不会做的,例如进行任何 HTTP 调用。

反之亦然,当你试图实现”数据可以持久地存储在数据库中并从数据库中检索”这样的行为时,你可以将其分解成更小的、更可试验的行为。例如,”给定两个 Go 结构体,我可以正确地生成 SQL 查询,并将其内容存储到 Postgres 表中”,或者 “给定两个对象,我可以正确地将结果解析到 Go 结构体切片中”。不需要 mock 数据库,不需要真正的数据库!

你不应制造文书工作

所有的流程都会在某一点上涉及到一些繁琐的、不可避免的数据倒换重组活动;我们可以把所有这类活动归入文书工作的范畴。对流程员来说,唯一的问题是,那些文书工作在 API 边界的哪一边?

如果是放在用户侧,那就意味着用户必须编写大量的标识符来为你的库准备文书工作,然后再编写大量的标识符来将结果解压成管用的文件格式。

相反(将文书工作放在 API 实现侧),写零文书工作的库,可以在一行中调用:

game.Run()

只要让一切在她们直接调用时发生就可以了。如果有可配置的设置,请设置合理的默认值,这样用户根本不用考虑,除非她们即使某些其原因需要覆盖默认值。功能选项(functional option)[6]是两个很好的模式。

这是另两个先写试验的好理由,如果你写的 API 中创造了文书工作,那么在试验时你将不得不自己做所有的文书工作,以便使用你自己的库。如果这被证明是笨拙、罗嗦和耗时的,可以考虑将那些文书工作移到 API 边界内。

你不如果杀死流程

你的库没有权利终止用户的流程。不要在你的包中调用像 os.Exit、log.Fatal、panic 这样的函数,这并非你能决定的。相反,如果你遇到了不可恢复(recover)的错误,将它们返回给调用者。

为什么不呢?即使它迫使任何想使用你的库的人去写标识符,不管 panic 是否真的被触发。出于反之亦然的其原因,你永远不如果使用会引起 panic 的第三方库,即使一旦你用了,你就需要 recover 它们。

所以你千万不要隐式调用(那些可以杀死流程的函数),但是隐式调用呢?你所做的任何操作,在某些情况下可能会panic(比如说:索引两个空的片断,写入两个空 map,类型断言失败)都如果先检查一下是否正常,如果不正常就返回两个错误。

你不要泄露资源

对于两个打算永远运行而不崩溃或出错的流程来说,对其的要求要比对单次命令行工具要严格一些。例如,想想太空探测器:在关键时刻意外重启制导系统,可能会让价值数十亿美元的飞行器驶向星系间的虚空。对于负责的计算机科学师来说,这很可能会导致一场没有咖啡的面谈,让人有些不舒服。

我们并非都在为太空器写软件,但我们如果像太空工程师一样思考。自然,我们的流程如果永远不会崩溃(最坏的情况下,它们如果典雅地退化,并提出退出过程的详实信息),但它们也需要是可持续的。这意味着不能泄露内存、goroutines、文件句柄或任何其他稀缺资源。

每当你有一些可泄漏的资源时,当你知道你已经成功获得它的那一刻,你如果想着释放它。不论函数如何退出或何时退出,保证将其清理掉,我们可以用Go带给我们的礼物:defer[7]。

任何时候启动两个 goroutine,你都如果知道它是如何结束的。启动它的同两个函数如果负责停止它。使用 waitgroups 或者 errgroups,并且总是向两个可能被取消的函数传递两个 context.Context。

你不如果限制用户的选择

我们如何编写友好、灵活、强大、易用的库呢?一种方式是避免不必要地限制用户对库的操作。两个常见的 Gopherism(Go 主义)是 “接受接口,返回结构”。但为什么这是个好提议呢?

假设你有两个函数,接受近似于两个*os.File 的参数 ,并向其写入数据。也许被写入的东西是两个文件并不关键,具体来说,它只需要是两个 “你可以写入的东西”(这个想法由国际标准库接口,如 io.Writer 表达)。有很多这样的东西:网络连接、HTTP response writer、bytes.Buffer 等等。

通过强迫用户传递给你两个文件,你限制了她们对你的库的使用。通过接受两个接口(如 io.Writer)来代替,你将打开新的可能性,包括尚未被创造的类型,后续它们仍然可以满足(接口) ,可以与你的标识符 io.Writer 一起工作。

为什么要 “返回结构体”?好吧,假设你返回一些接口类型。这极大地限制了用户对该值的操作(她们能做的就是调用其上的方式)。即使她们事实上可以用底层的具体类型做她们需要做的事情,她们也必须先用类型断言来解包它。换句话说,这就是额外的文书工作(如果避免)。

另一种避免限制用户选择的方式是不要使用只有当前 Go 版本才有的功能。相反,考虑至少支持最近两个主要的 Go 版本:有些人不能立即升级。

你如果设定边界

他组件中。这一点对于与其他人的标识符的边界来说,是双倍的。

边界是那些与你的标识符接触的点:例

一旦你让一点外来数据在你的流程内部自由运行,它很快就会到处乱跑。你的其他包都需要导入那些外来类型,这很烦人,而且标识符将会有一股差劲的味道。

相反,你的 airlock 函数如果做两件事:它如果将外来数据转化为你自己的内部文件格式,而且如果确保数据是有效的。现在,你的所有其他标识符只需要处理你的内部类型,它不需要担心数据是否会出错、丢失或不完整。

另一种执行良好边界的方式是始终检查错误。如果你不这样做,无效的数据可能会泄露进来。

你不如果在内部使用接口

一个接口值说:”我不知道这个东西到底是什么,但也许我知道有些事情我可以用它来做。” 这在 Go 流程中是一种超级不方便的值,即使我们不能做任何没有被接口指定的事情。

对于空接口(interface{})来说,这也是双倍的,即使我们对它一无所知。因此,根据定义,如果你有两个空的接口值,你需要把它类型化为具体的东西才能使用它。

在处理任意数据(也就是在运行时类型或模式未知的数据)时,不得不使用它们是很常见的,比如说无处不在的map[string]interface{}[8]。但是,我们如果尽快使用 airlock 将这一团无知转化为某种具体类型的管用的 Go 值。

特别是,不要用 interface{}类型值来模拟泛型(Go有泛型[9])。不要写两个函数,接受一些可以是七种具体类型之一的值,然后对其进行类型转换,为该类型找到合适的操作。相反,写七个函数,每个具体类型两个。

不要仅即使你可以在试验中注入 mock,就创建两个公共的接口,这是两个错误。创建两个真正的用户在调用你的函数之前必须实现的接口,这违反了“无文书工作准则”。不要在一般情况下写 mock;Go 不适合这种艺术风格的试验。(当 Go 中的某些东西很困难时,这一般来说是你做错事的标志。)

你不要盲目地遵从诫命,而要自己思考

人们说:”告诉我们什么是最佳做法”,仿佛有一本小秘籍,里面有任何技术或组织问题的正确答案。(是有的,但不要说出去。我们不希望每个人都成为顾问)。

小心任何看似清楚、明确、单纯地告诉你在某种情况下该怎么做的提议。它不会适用于每一种情况,在适用的地方,它都需要告诫,需要细微的差别,需要澄清。

每个人都希望得到的是不需要真正理解就能应用的提议。但这样的提议比它能带来的协助更危险:它能让你走到桥的一半,然后你会发现桥是纸做的,而且刚开始下雨。

参考资料

[1] 《Ten commandments of Go》: https://bitfieldconsulting.com/golang/commandments

[2] 小说家: https://bitfieldconsulting.com/books

[3] 同学: https://bitfieldconsulting.com/golang/learn

[4] 最轻精采准则: https://en.wikipedia.org/wiki/Principle_of_least_astonishment

[5] 一清二楚: https://www.youtube.com/watch?v=8TLiGHJTlig

[6] 功能选项(functional option): https://www.imooc.com/read/87/article/2424

[7] Go带给我们的礼物:defer: https://www.imooc.com/read/87/article/2421

[8] map[string]interface{}: https://bitfieldconsulting.com/golang/map-string-interface

[9] Go有泛型: https://bitfieldconsulting.com/golang/map-string-interface

CSDN 问答上线《冲榜分奖金》活动!每周采纳榜前五名的答主可获得现金和会员卡,剩余用户会随机抽取送出冲榜

☞BAT 等 34 家企业签署合规经营承诺书;美团被判赔偿饿了么 35.2 万元;FreeBSD 13.0 发布|极客头条 ☞AI 工程师的崩溃是在哪一瞬间 ☞大厂竞业协议是“巨坑”?科大讯飞前员工、团队创始人跳槽腾讯被判赔 1200 万

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务