学习Go语言的15条经验
原文:15 Lessons in Golang
作者:Jason Kulatunga
翻译:雁惊寒
摘要:本文作者在很短的时间内就从对Golang一无所知到开发出真正的产品。在学习Golang的过程中,他总结出十五条编程经验以分享给读者。以下是译文。
像许多其他的开发者一样,我听到过很多有关Golang的传闻。也许你还不熟悉它,那么我告诉你,它是Google开发的开源语言。我之所以对Golang感兴趣,是因为它是一种静态类型编译的现代语言。
长久以来,这就是我所知道的有关Golang的所有信息。我本打算在有空的时候详细了解一下的,但一直都有其他优先级更高的事情要做。大约4个月前,我意识到Golang也许可以用来解决我在CapsuleCD
中遇到的一个问题,CapsuleCD
是一款我写的可用于任何语言的通用自动化软件包发布工具(npm,cookbooks,gems,pip,jar等)。
我遇到了这么一个问题:CapsuleCD
是一个基于Ruby的可执行文件,这意味着任何想要使用CapsuleCD
的人都需要在他们的机器上安装Ruby解释器,即使他们想做的只是打包一个Python库。这使得我的Docker容器更加臃肿,开发起来更加复杂。如果只需要把单个二进制文件下载到容器中就好了。所以,把项目移植到Golang这个想法在那一刻突然出现在我的脑海里。
在接下来的几个月里,我反复思考这个想法,几周后,我终于坐了下来,开始把我的3000行Ruby应用程序移植到Golang。虽然我刚刚买了一本类似于Golang傻瓜教程这样的书,但我还是决定直接进行编码,只有在遇到问题卡住的时候才去找博客帖子和stack overflow。
我已经听到一些人劝我放弃的声音。说实话,虽然我玩得很开心,但我最开始开发得非常慢。我是在不知道任何约定的情况下尝试着用一门新的语言来编写这个应用程序。事实是,我喜欢它。那些“啊哈!”的时刻,以及在一此巨大的重构之后再次编译成功的喜悦是一种令人难以置信的动力。
下面是我在把应用程序移植到Golang的过程中学到的一些意想不到的以及非常规的事情。
请注意,这些是我在写Golang代码的过程中遇到的未曾料想到的东西,我以前使用的都是流行语言以及动态类型语言(C ++,C#,Java,Ruby,Python和NodeJS)。这些观点不一定就是对Golang的批评。我能够在两周内从对这个语言零基础到发布应用程序,我真是太牛逼了。
在写第一行代码之前
包的布局
虽然这对于需要编译的语言来说并不是必需的,但Golang需要,只是我并没有找到一个像Ruby、Chef或Node那样的标准目录结构。有一些比较流行的社区,但我本人还是最喜欢Peter Bourgon的建议。
github.com/peterbourgon/foo/
circle.yml
Dockerfile
cmd/
foosrv/
main.go
foocli/
main.go
pkg/
fs/
fs.go
fs_test.go
mock.go
mock_test.go
merge/
merge.go
merge_test.go
api/
api.go
api_test.go
不支持循环依赖
当你发现Golang不支持包之间的循环依赖时,包的布局就变得尤为重要。如果A导入B,B导入A,Golang将会报错。我开始有点喜欢上它了,因为这迫使我更多地去思考应用程序的领域模型。
import cycle not allowed
package github.com/AnalogJ/dep/a
imports github.com/AnalogJ/dep/b
imports github.com/AnalogJ/dep/a
依赖管理
npm
、pypi
和bundler
,这每一个包管理器都是他们对应编程语言的代名词。然而,Golang还没有官方的包管理器。社区提供了一些不错的选择,但问题是他们都很好,要选出一个合适的博爱管理器有点困难。我最终选择了Glide,因为感觉它跟bundler
和npm
有点类似。
文档
这应该是Golang做得最好的事情之一了。 go docs
和godoc.org
这两个网站都非常得棒,并且所有的库的文档进行了标准化。这比包文档都是自定义和自托管的NodeJS社区前进了一大步。
GOROOT, GOPATH
Golang的import机制有点奇特。与大多数其他的语言不同,Golang要求把源代码放在预先配置好的文件夹中。我没有深入研究这个细节,但你应该知道这需要做一些设置,你要习惯这个。 Dmitri Shuralyov的我如何在多个工作区中使用GOPATH是一个很好的资源。
GOPATH=/landing/workspace/path:/personal/workspace/path:/corporate/workspace/path
挠痒痒
伪结构体继承
在设计继承模型时,Golang开发人员做了一些有趣的事情。Golang遵循类似于Ruby的多重组合模式, 而不是使用类型语言规定的更为传统的继承模型(多继承或经典继承)。 如果不能完全理解Method-Shadowing,可能会出现一些意想不到的结果。
鸭式接口
这是Golang的另一个很酷的意想不到的功能。我仅在动态类型语言中看到过鸭式接口。这个鸭式与struct
密切相关。
结构体中可以定义字段,但接口不行
不幸的是,structs
与interfaces
不能具有相同的API,因为interfaces
无法定义字段。这个问题并算很大,因为可以在接口中定义getter
和setter
方法,虽然这有点混乱。我相信,应该有一个技术上或者计算机理论上的解释能够回答为什么要这么做。
Public和Private命名
Golang将Python的public
和private
方法命名方案做了进一步发展。当我最初发现以大写字母开头的函数、结构体是public
,而小写开头的则是private
的时候,我哑口无言。但老实说,在用Golang开发了两个星期之后,我真的很喜欢这种习惯。
type PublicStructName struct {}
type privateStructName struct {}
defer
这是另外一个非常有用的Golang特性。我形象这是Golang实现并行处理和错误模型的结果,但defer
可以很容易地让源代码看起来更清晰。从某种意义上来说,我把它看做是try-catch-finally
模式下的finally
方法,或是C#/Java
中的using
代码块。但我相信它还有更多更有创造性的用法。
go fmt
很不错
你不会跟一个Golang开发者进行有关“Tab vs 空格”的争论。Golang有标准的代码风格,go fmt
会对代码进行重新格式化。通过阅读它的源代码,我了解到了强大的parser
和ast
库。
GOARCH、GOOS、CGO和交叉编译
我创建CapsuleCD
独立二进制文件的目的是要将端口启动到Golang上。但是,很明显,简单的静态二进制文件并不是Golang的内在特性。如果你的代码及其相关的依赖全部是用Golang写的,那么你可以用GOOS
和GOARCH
来构建静态二进制文件。但是,如果你像我一样不幸运,存在某个依赖需要在底层调用C代码的话,那么你将会陷入痛苦之中。不要误会我,创建一个动态链接库还是比较容易的。但是,要生成一个没有外部依赖关系的静态二进制文件,需要确保所有的C
依赖项(及其依赖项)都是静态链接的。GOOS
和GOARCH
支持的对应值组合表可以在Golang文档中找到。
如何进行测试?
藏在眼皮底下
测试文件的后缀为_test.go
,并且应该跟被测试的代码放在同一个目录中,而不要放在某个特殊的测试目录中。这还好,虽然一开始看着有点混乱。
测试数据放在一个特殊的testdata
目录中。 使用go build
时,testdata
目录和_test.go
文件都会被编译器忽略。
go list
和vendor
目录
依赖关系管理对于Golang来说是相当新鲜的,并不是所有的工具都能理解特殊的vendor
文件夹。因此,当你运行go test
时,默认情况下会发现它运行了所有依赖项的测试。使用go list | grep -v /vendor
可以让Golang忽略vendor
目录。
go fmt $(go list ./... | grep -v /vendor/)
if err != nil
我是一个很看重代码覆盖率的人。我一直在努力让我的开源项目达到80%以上的代码覆盖率,但是,在使用Golang的时候,我感觉这很困难。那些已经熟悉Golang的人可能会说,Golang是一个能达到较高代码覆盖率的语言之一。Golang将所有错误都视为标准对象,而不是为错误创建一个独立的执行路径(try-catch-finally
)。 Golang约定,对于可能产生错误的函数,应该在最后的return
参数中返回这个错误对象。
这是一个非常有意思的模型,这让我想起了Node
的内置函数。然而,就像Node
一样,把会生成错误的单元测试写入到内置函数中可能会很困难。当你按照编码模式抛出错误,然后在上层处理错误时,就会变得很烦人,如下所示:
data, err := myfunction(...)
if(err != nil){
return err
}
data2, err2 := myfunction2(...)
if(err2 != nil){
return err
}
这会很快弄乱你的代码。在这一点上,有些人可能会认为interfaces
和mock
能解决这些问题。虽然在某些情况下是这样,但是针对内置的库(如os
和ioutil
)来编写大量的interfaces
,或者将这些库作为参数来传递,我认为并不合适,这样做只是让我们能够合理地生成ioutil.WriteFile
和os.MkdirAll
的错误而已。
我认为这绝对是我心智上一个缺陷。我已经阅读了大量关于如何对Golang进行单元测试和提高代码覆盖的文档和博客文章,但是我还是没有找到一个不需要依赖注入的模式。Golang似乎很讨厌过于繁琐。
结论
我很乐意听到你的想法。我刚刚用Golang工作了几个星期,但这是一个令人难以置信的受教育并且很愉快的经历。我能够在很短的时间内从没有任何经验到开发出一个真正的产品,而不是教科书上的一个例子。我知道我并不是Golang专家,而且对于Golang的了解还存在理论上的差距,但是,当我写下这篇文章的时候,我发现自己走的比预想的要远得多。
Golang按照我原来设想的做了,给了我一个二进制文件,我不再需要Ruby解释器,可以很轻松地下载到Docker容器中。如果你还在用其他语言维护可执行程序,我建议你考虑一下Golang,给Golang一次尝试。
你也许感兴趣的:
- Go语言有个“好爹”反而被程序员讨厌?
- 【外评】为什么人们对 Go 1.23 的迭代器设计感到愤怒?
- 【译文】Go语言性能从 1.0 版到 1.22 版
- Go 语言程序员的进化
- 【译文】面试时,有人问我喜欢Go语言什么?
- 4 秒处理 10 亿行数据! Go 语言的 9 大代码方案,一个比一个快
- 【译文】Go语言设计:我们做对了什么,做错了什么
- 最好的 Go 框架就是不用框架?
- 吵翻了!到底该选 Rust 还是 Go,成 2023 年最大技术分歧
- “Go 语言的优点、缺点和平淡无奇之处”的十年
你对本文的反应是: