在SoundCloud,我们为客户构建了产品的API。或者说,我们主要的网站、手机客户端和手机应用是该API的第一批客户。该API背后是一个领域性的服务:SoundCloud基本上以面向服务体系结构的形式运作。
我们也是通晓多种语言的组织,因为我们使用了很多语言。并且这些服务(和基础设施支持)的许多部分是使用Golang开发的。事实上,我们都是早期Golang的使用者:目前,我们已在产品中使用Golang有两年半的时间。相关项目包括:
Bazooka,我们内部服务平台;产品思想非常类似于Keroku或Flynn。
我们外围的传输层使用通用的nginx, HAProxy等等,但是它们要和Golang服务协作。
我们的音频存储在AWS S3上,但是上传、转码和生成链接等需要Golang服务协调处理。
搜索采用了Elasticsearch, 探测使用复杂的机器学习模型,但是它们都与由Golang开发的基础设施相集成。
Prometheus,一个早期阶段的遥测系统纯粹是有Golang开发。
当前,流处理采用Cassandra,但是我们正打算(几乎)完全使用Golang代替。
我们也正在试验用Golnag开发的HTTP流媒体直播服务。
许多其他面向产品的小服务。
这些项目大概有六个团队开发,包括十多人的SoundCloud勤杂工,他们中的大部分会全职使用Golang。毕竟在这个时候,这些项目和这样混杂的工程师中,我们已经逐渐形成了在产品中使用Golang的最好实践方法。我们的这些教训将对其他开始大举投资Golang的组织提供帮助。
在我们的笔记本上,我们已经设定了单一、全局的GOPATH。就个人而言,我喜欢使用$HOME,但是许多其他人使用$HOME下的一个子目录。我们克隆仓库进入GOPATH的相对路径,然后就可直接工作。即,
$ mkdir -p $GOPATH/src/github.com/soundcloud $ cd $GOPATH/src/github.com/soundcloud $ git clone git@github.com:soundcloud/roshi
我们中的许多人在早期一直和约定俗成的事情做斗争,以保持我们自己特有的代码组织方法。事实上,它根本不值得如此麻烦。
对于编辑器,许多用户使用Vim以及各种插件。(我使用的vim-go就不错。)还有许多人,包括我自己也是,结合GoSublime使用Sublime Text。也有少数人使用Emacs,但没有人用IDE。我不确定这是不是个最佳的实践,但标出来挺有趣的。
我们的最佳实践是确保任何事情简单。许多服务源码半打包在main包中。
github.com/soundcloud/simple/ README.md Makefile main.go main_test.go support.go support_test.go
比如我们的搜索调度器,两年后仍然是这样。在确定需要前不要创建新结构。
也许在某些时候你需要创建一个新的支持包。在你的main库中使用子目录,并使用完整的限定名导入。如果该包只有一个文件或一个结构,那么它肯定不需要分拆出来。
有时一个仓库中需要包含多个二进制文件;比如这个任务需要一个服务,一个工作进程,或一个监控。在这种情况下,将每个二进制文件放在特定main包的单独的子目录中,并使用其他的子目录(或包)来实现共享的功能。
github.com/soundcloud/complex/ README.md Makefile complex-server/ main.go main_test.go handlers.go handlers_test.go complex-worker/ main.go main_test.go process.go process_test.go shared/ foo.go foo_test.go bar.go bar_test.go
请注意,不要引入asrc目录。由于vendor子目录异常(下面介绍更多内容)不要在仓库中包含src目录,或将其添加到GOPATH。
通常来说,首先配置你的编辑器保存代码交给go fmt(或goimports),使用默认参数。这意味使用tab缩进,用空格对齐。格式不正确的代码将不能提交。
过去的风格指南非常广泛,但谷歌最近发布了他们的 代码审查意见 文档,这几乎就是我们应遵守的公约。因此,我们使用它。
实际上我们把它推进了一点:
避免命名返回参数,除非他们能明确和显着地提高透明度。
避免用 make 和 new,除非他们是必要的(new(int),或 make(Chan int)),或者我们能提前知道要分配的东西的尺寸( make(map[int]string,n),或 make([]int,0,256))。
使用 struct{} 作为标记值,而不是布尔或接口{}。例如,集合是 map[string]struct{};信道是 chan struct{}。它明确标明了信息的明确缺乏。
打断长行的参数也很好。那更象是Java的风格:
// 不要这样。 func process(dst io.Writer, readTimeout, writeTimeout time.Duration, allowInvalid bool, max int, src <-chan util.Job) { // ... }
这样会更好:
func process( dst io.Writer, readTimeout, writeTimeout time.Duration, allowInvalid bool, max int, src <-chan util.Job, ) { // ... }
当构造对象时也同样分为多行:
f := foo.New(foo.Config{? Site: "zombo.com",? Out: os.Stdout,? Dest: conference.KeyPair{? Key: "gophercon", Value: 2014, }, })
另外,当分配新的对象时,在初始化部分传递成员值(如上面)比下面这样过后设置要好。
// 不要这样。 f := &Foo{} // or, even worse: new(Foo) f.Site = "zombo.com" f.Out = os.Stdout f.Dest.Key = "gophercon" f.Dest.Value = 2014
我们尝试了通过多种方式向Go程序传递配置:解析配置文件,用 os.Getenv 直接从环境中提取配置,各种增值flag解析包。最后,最合乎经济原则的就是普通的package flag,它的严格类型和简单语义对我们所需的一切都绝对够用而且够好。
我们主要部署12-Factor 的应用,12-Factor 应用程序通过环境传递配置。但即使这样,我们也使用一个启动脚本来把环境变量转换为flags。Flags作为程序及其运行环境之间的一个明确和全文档化的表面区域。他们对于了解和操作程序来说是非常宝贵的。
一个关于flags的不错的习惯是把他们定义到你的main函数中。这样就能防止你在代码中随意的将他们作为全局变量使用,这使你严格的遵守依赖注入从而方便测试。
func main() { var ( payload = flag.String("payload", "abc", "payload data") delay = flag.Duration("delay", 1*time.Second, "write delay") ) flag.Parse() // ... }
我们尝试过几个日志框架,他们提供像日志级别,调试,路由输出,自定义格式化等等功能。最终我们选定package log。因为我们只记录可操作信息。 这意味着需要人工处理的 serious, panic级别的错误,或者结构化数据会被其他机器消耗。 举个例子,搜索转发器发送每一个它使用上下文信息处理的请求,因此我们的分析工作流可以看到新西兰的人们经常搜索 Lorde, 或者随便什么。
我们考虑到遥测,在一个运行过程中释放出的任何其他量:请求响应时间,QPS,运行错误,队列深度等等。并且遥测基本上包括两种模式:push和pull。
push意味着释放指标到一个已知的系统。例如Graphite, Statsd, and AirBrake
pull意味着在一些已知的位置暴露指标,并允许已知的系统去擦除它们。例如,expvar和Prometheus(或许还有其他的)
当然两种方式都有自己的存在性。当你开始使用时,push是直观和简单的。但是推送指标的增长却有悖常理:你得到的越大,成本越高。我们过去发现在特定规模大小的基础设施上,pull是该尺度下的唯一模型。那也有许多值能反映一个运行的系统。所以,最好的实践是:expvar或者类似风格的。
在一年的过程中我们尝试了许多的测试库和框架,但是很快放弃了他们中的大部分,今天我们所有的测试通过数据驱动(表驱动)测试,用普通的包测试。我们没有强烈或者明确的抱怨测试/检查包,除此之外,他们根本没有提供巨大的价值。有一件事情是有帮助的:reflect.DeepEqual让你更简单的对任意值进行比较(例如expected对got)。
包测试是面向单元测试的,对于集成测试,就会有点麻烦。运行的外部服务依赖于你的集成环境,但是我们找到了一个好的方式集成他们。写一个integration_test.go,给它一个integration的构建标签。定义(全局)标志,比如服务地址和连接字符串,用他们在你的测试中。
// +build integration var fooAddr = flag.String(...) func TestToo(t *testing.T) { f, err := foo.Connect(*fooAddr) // ... }
go test 和 go build 一样建立标签,所以你可以调用 go test -tags=integration 。它也综合了 flag.Parse 包的 main,所以任何被声明和可见的 flags 将被处理和提供给你的测试。
通过验证,我的意思是静态代码验证。幸运的是,Go 有一些很好的工具。我发现当考虑使用哪种工具时考虑编写代码的阶段很有用。
当做这种事时 | 使用这个 |
---|---|
保存 | go fmt(或 goimports) |
构建 | go vet,golint, 或者 go test |
部署 | go test -tags=integration |
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务