关于 Golang 的十五堂课
像很多开发者一样,我听说过很多关于 Golang(或者 Go,我不确定是哪个)的声音。如果你不熟悉它,你可以认为它是一门 Google 开发的开源编程语言。它比较吸引我是因为,它定位是一个现代的静态编译型语言。
很长一段时间,这就是我对 Golang 认识的程度。我知道我想深入看一下它的某些方面,但是一直有其他更优先的事情。大约 4 个月之前,我意识到 Golang 可以解决我现在面临的 CapsuleCD 的问题, CapsuleCD
是一个适用于任何语言的通用的自动化打包方案(npm, cookbooks, gems, pip, jars 等)。
问题是 CasuleCD
是使用 Ruby gem
分发的可执行程序,意味着使用它的人必须在他们的机器上安装 Ruby 解释器,虽然他们仅仅是想打包一个 Python 库而已。这使得我的 Docker 容器越来越大和开发起来更加复杂。如果只有一个二进制文件,我只需要在容器中下载,岂不是一个好主意?如果只有那个时候在我的脑海中出现这个想法,那时就是我迁移到 Golang 的开始。
在接下来的几个月,我坚持那个想法,几个星期前,我最终坐下来,我开始迁移大约 3000 行代码到 Golang。虽然我完全可以买一本 Golang 的书,但我决定直接进入编码,遇到问题仅仅是读一些博客或者浏览 stack overflow 网站。
我可以听到你的惊讶。说实话,虽然我开发得到了不少乐趣,但是我刚开始的开发是非常缓慢的。我尝试用一门全新的语言写一个程序,对语法一无所知。事实上,我喜欢它。那些激动的时刻和在一个繁重的重构后让程序重新编译起来,都是我前进的动力。
下面是一些意外的 / 非常规的东西,这些是我迁移程序到 Golang 的时候学习到的。
注意,当我以一个熟悉流行的静态和动态类型的语言(C++, C#, Java, Ruby, Python and NodeJS 等)的背景学习的时候,这些事情不是我期望的。这里没必要批评 Golang,我可以用一种全新的语言在两周内用一种全新的语言完成我的软件的工作。如果你问我,那就太棒了。
开始第一行代码
包布局
虽然对于编译型语言不是需要的,我仍然没有准备好这一个事实——看起来 Golang 没有一个标准的目录结构,而 Ruby、Chef 和 Node 一样都有的。似乎有一些很受欢迎的社区结构,我发现我很喜欢 Peter Bourgon’s recommendations。
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 社区先进的地方,Nodejs 社区的所有包的文档都是自己托管的。
GOROOT, GOPATH
Golang 导入包是通过一种怪异的方式实现的。不像大多数的其他语言,Golang 基本上要求你的资源生活在预先配置的文件夹里。 我没有打算去深入细节,但是你应该知道它需要一些设置和习惯。 Dmitri Shuralyov
的 How I use GOPATH with multiple workspaces 是很好的资源。
GOPATH=/landing/workspace/path:/personal/workspace/path:/corporate/workspace/path
一些笔记??
伪类(结构体)继承
当 Golang 的开发者设计继承模型的时候,他们做了一些有趣的事情。没有采用任何一种静态类型语言传统的继承模型,像多继承或者类继承,Golang 遵循一个类似 Ruby 的多种组合模式。如果你不完全理解的话,方法隐藏会导致一些意外的结果。
Duck-Typed 接口
这是另外一个 Golang 的很酷的令人意想不到的特性。接口是 duck-typed,我只在动态类型语言中见过这种语法。 duck-typing
通过 struct 的组合实现。
结构体有成员,接口没有
遗憾的是, struct
没有 interface
相同的 API,因为后者无法定义成员。这不是一个大问题,因为开发者可以在接口上定义一个 getter
和 setter
方法,但是这有些混乱。我相信肯定有解释为什么有这么好的技术理论的答案,但是目前没有。
Public/Private 命名
Golang 采用了 Python 的公共和私有方法的命名方案。当我最初发现以大写字母开始的函数、方法和结构名称是公有的,小写是私有的,我不确定如何使用它。但是说实话,当使用 Golang 两周以后,我真的非常喜欢这个约定。
type PublicStructName struct {}
type privateStructName struct {}
defer
defer 是 Golang 的另外一个令人惊讶的特色。我不确定这是不是因 Golang 的并行处理和错误处理模型而产生的,但是 defer
使得很方便地保持您的清理工作接近原始代码。我把它看为是与使用 C#/Java
时的 try-catch-finally
的 finally
类似的 Golang 版本的替代品,但是我确信它有更多的创造性用途。
go fmt 很棒
你永远不会与 Golang 开发者争论 “tab 还是空格”。有一套标准化的 Golang style,go fmt 会重新格式化你的代码来遵守它。它是一个整洁的工具,阅读它的源代码的过程介绍给了强大的解析器和 ast 库。
GOARCH, GOOS, CGO 和跨平台编译
我开始迁移到 Golang 的原因是想创建一个单独的 CapsuleCD
的二进制包。但是很显然,简单的静态二进制文件不是 Golang 的本质特征(这应该是明显的)。如果你的代码全部是通过 Golang 写的,包括你的所有依赖(以及它们的依赖),那么你可以通过使用 GOOS
和 GOARCH
来构建静态二进制文件使你心满意足。然而,如果你和我一样不幸运,你需要一个在底层调用 C 语言(通过导入一个 C 虚拟包)的依赖,然后你会进入一个痛苦的世界。不要误解我,创建一个动态链接库仍然非常简单。但是为了生成一个没有外部依赖的静态库,意味着你需要确保你的所有 C 依赖(还有它们的依赖)也是都是静态链接的。就像我说的,显然。 C pseudo-packages
是通过 CGO
编译的,你需要查看相关的文档以找到所有需要的 flags,从而帮助 CGO
定位你的静态库。在 Golang docs 有一张所有支持的 GOOS
和 GOARCH
对的表格。
如何测试?
隐藏在光天化日下??
测试文件以 _test.go
结尾,应该与他们测试的代码并排放置,而不是一起放在一个专门的测试目录。这很好,虽然刚开始感觉有些乱。
测试数据放在一个特殊的 testdata
目录。在构建时, 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 是一个最简单的语言来获得很高的覆盖率。不是为错误创建一个独立的执行路径(try-catch-finally),Golang 把所有的错误像标准对象一样对待。Golang 的惯例是,那些可以产生错误的函数应该作为最后的返回参数返回。
这是一个很有趣的模型,这是我想起一些 Nodes
的内建函数。然而,就像 Nodes
一样,很难编写可产生错误的内建函数的单元测试。当你遵循一个代码风格时——错误向上冒泡,然后在更高层次处理它们,这会变得更加讨厌。当这么做的时候,你会写很多像下面的一样代码:
data, err := myfunction(...)
if(err != nil){
return err
}
data2, err2 := myfunction2(...)
if(err2 != nil){
return err
}
这会让你的代码很快变得混乱。这个时候,你们中的有些人可能会考虑 interfaces
和 mocks
可以解决这些问题。虽然在有些情况下这是可行的,当使用 os
和 ioutil
这样的内建库或者把这些库当作参数时,我不认为写大量的 interfaces 有意义,我们可以人为地让 ioutil.WriteFile
和 os.MkdirAll
产生错误。
我认为这肯定是我的心理模型中的一个缺点,但是关于如何为 Golang 写单元测试和代码覆盖率方面我读了大量的文档和博客,我仍然没有找到一个不依赖引擎注入的而实现目的的方式,注入不是 Golang 所提倡的,因为太笨重了。
总结
我希望听到你的声音。我仅仅使用 Golang 几个星期,但是这是一种令人难以置信的学习和愉快的经历。在很短时间内我可以从没有任何经验到构建一个真实可用的程序,而不是仅仅是从书上学习一些例子。我知道我不是很擅长 Golang,在理解 Golang 方面我仍然有些障碍,但是当我走这条无书自通的路子时,我觉得它们比我想象的要远得多。
Golang 确实是以我想要的方式工作的,给我提供了可以很方便地下载到 Docker 容器的二进制文件,不需要 Ruby 解释器。如果你维护其他语言的可执行程序,我绝对会建议你考虑尝试一下 Golang。