读生产环境下go语言最佳实践有感

最近看了一篇关于go产品开发最佳实践的文章,go-in-procution。作者总结了他们在用go开发过程中的很多实际经验,我们很多其实也用到了,鉴于此,这里就简单的写写读后感,后续我也争取能将这篇文章翻译出来。后面我用soundcloud来指代原作者。

开发环境

在soundcloud,每个人使用一个独立的GOPATH,并且在GOPATH直接按照go规定的代码路径方式clone代码。

$ mkdir -p $GOPATH/src/github.com/soundcloud
$ cd $GOPATH/src/github.com/soundcloud
$ git clone [email protected]:soundcloud/roshi

对于go来说,通常的工程管理应该是如下的目录结构:

proj/
    src/
        modulea/
            a.go
        moudleb/
            b.go
        app/
            main.go
    pkg/
    bin/

然后我们在GOPATH里面将proj的路径设置上去,这样就可以进行编译运行了。这本来没啥,但是如果我们要将其代码提交到github,并允许另外的开发者使用,我们就不能将整个proj的东西提交上面,如果提交了,就很蛋疼了。外面的开发者可能这么引用:

import "github.com/yourname/proj/src/modulea"

但是我们自己在代码里面就可以直接:

import "github.com/yourname/proj/modulea"

如果外面的开发者需要按照去掉src的引用方式,只能把GOPATH设置到proj目录,如果import的多了,会让人崩溃的。

我曾今也被这事情给折腾了好久,终于再看了vitess的代码之后,发现了上面这种方式,觉得非常不错。

工程目录结构

如果一个项目中文件数量不是很多,直接放在main包里面就行了,不需要在拆分成多个包了,譬如:

github.com/soundcloud/simple/
    README.md
    Makefile
    main.go
    main_test.go
    support.go
    support_test.go

如果真的有公共的类库,在拆分成单独的包处理。

有时候,一个工程可能会包括多个二进制应用。譬如,一个job可能需要一个server,一个worker或者一个janitor,在这种情况下,建立多个子目录作为不同的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

这点我的做法稍微有一点不一样,主要是参考vitess,我喜欢建立一个总的cmd目录,然后再在里面设置不同的子目录,这样外面就不需要猜测这个目录是库还是应用。

代码风格

代码风格这没啥好说的,直接使用gofmt解决,通常我们也约定gofmt的时候不带任何其他参数。

最好将你的编辑器配置成保存代码的时候自动进行gofmt处理。

Google最近发布了go的代码规范,soundcloud做了一些改进:

  • 避免命名函数返回值,除非能明确的表明含义。
  • 尽量少用make和new,除非真有必要,或者预先知道需要分配的大小。
  • 使用struct{}作为标记值,而不是bool或者interface{}。譬如set我们就用map[string]struct{}来实现,而不是map[string]bool。

如果一个函数有多个参数,并且单行长度很长,需要拆分,最好不用java的方式:

// Don‘t do this.
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,
    },
})

// Don‘t do this.
f := &Foo{} // or, even worse: new(Foo)
f.Site = "zombo.com"
f.Out = os.Stdout
f.Dest.Key = "gophercon"
f.Dest.Value = 2014

如果一些变量是后续通过其他操作才能获取的,我觉得就可以在后续设置了。

配置

soundcloud使用go的flag包来进行配置参数的传递,而不是通过配置文件或者环境变量。

flag的配置是在main函数里面定义的,而不是在全局范围内。

func main() {
    var (
        payload = flag.String("payload", "abc", "payload data")
        delay   = flag.Duration("delay", 1*time.Second, "write delay")
    )
    flag.Parse()
    // ...
}

关于使用flag作为配置参数的传递,我持保留意见。如果一个应用需要特别多的配置参数,使用flag比较让人蛋疼了。这时候,使用配置文件反而比较好,我个人倾向于使用json作为配置,原因在这里

日志

soundcloud使用的是go的log日志,他们也说明了他们的log并不需要太多的其他功能,譬如log分级等。对于log,我参考python的log写了一个,在这里。该log支持log级别,支持自定义loghandler。

soundcloud还提到了一个telemetry的概念,我真没好的办法进行翻译,据我的了解可能就是程序的信息收集,包括响应时间,QPS,内存运行错误等。

通常telemetry有两种方式,推和拉。

推模式就是主动的将信息发送给特定的外部系统,而拉模式则是将其写入到某一个地方,允许外部系统来获取该数据。

这两种方式都有不同的定位,如果需要及时,直观的看到数据,推模式是一个很好的选择,但是该模式可能会占用过多的资源,尤其是在数据量大的情况下面,会很消耗CPU和带宽。

soundcloud貌似采用的是拉模型。

关于这点我是深表赞同,我们有一个服务,需要将其信息发送到一个统计平台共后续的信息,开始的时候,我们使用推模式,每产生一条记录,我们直接通过http推给后面的统计平台,终于,随着压力的增大,整个统计平台被我们发挂了,拒绝服务。最终,我们采用了将数据写到本地,然后通过另一个程序拉取再发送的方式解决。

测试

soundcloud使用go的testing包进行测试,然后也使用flag的方式来进行集成测试,如下:

// +build integration

var fooAddr = flag.String(...)

func TestToo(t *testing.T) {
    f, err := foo.Connect(*fooAddr)
    // ...
}

因为go test也支持类似go build那种flag传递,它会默认合成一个main package,然后在里面进行flag parse处理。

这种方式我现在没有采用,我都是在测试用例里面直接写死了一个全局的配置,主要是为了方便的在根目录进行 go test ./...处理。不过使用flag的方式我觉得灵活性很大,后面如果有可能会考虑。

go的testing包提供的功能并不强,譬如没有提供assert_equal这类东西,但是我们可以通过reflect.DeepEqual来解决。

依赖管理

这块其实也是我非常想解决的。现在我们的代码就是很暴力的用go get来解决依赖问题,这个其实很有风险的,如果某一个依赖包更改了接口,那么我们go get的时候可能会出问题了。

soundcloud使用了一种vendor的方式进行依赖管理。其实很简单,就是把依赖的东西全部拷贝到自己的工程下面,当做自己的代码来使用。不过这个就需要定期的维护依赖包的更新了。

如果引入的是一个可执行包,在自己的工程目录下面建立一个_vendor文件夹(这样go的相关tool例如go test就会忽略该文件夹的东西)。把_vendor作为单独的GOPATH,例如,拷贝github.com/user/dep到_vendor/src/github.com/user/dep下面。然后将_vendor加入自己的GOPATH中,如下:

GO ?= go
GOPATH := $(CURDIR)/_vendor:$(GOPATH)

all: build

build:
    $(GO) build

如果引入的是一个库,那么将其放入vendor目录中,将vendor作为package的前缀,例如拷贝github.com/user/dep到vendor/user/dep,并更改所有的相关import语句。

因为我们并不需要频繁的对这些引入的工程进行go get更新处理,所以大多数时候这样做都很值。

我开始的时候也采用的是类似的做法,只不过我不叫vendor,而叫做3rd,后来为了方便还是决定改成直接go get,虽然知道这样风险比较大。没准后续使用godep可能是一个不错的解决办法。

构建和部署

soundcloud在开发过程中直接使用go build来构建系统,然后使用一个Makefile来处理正式的构建。

因为soundcloud主要部署很多无状态的服务,类似Heroku提供了很简单的一种方式:

$ git push bazooka master
$ bazooka scale -r <new> -n 4 ...
$ # validate
$ bazooka scale -r <old> -n 0 ...

这方面,我们直接使用一个简单的Makefile来构建系统,如下:

all: build 

build:
    go install ${SRC}

clean:
    go clean -i ${SRC}

test:
    go test ${SRC}

应用程序的发布采用最原始的scp到目标机器在重启的方式,不过现在正在测试使用salt来发布应用。而对于应用程序的启动,停止这些,我们则使用supervisor来进行管理。

总结

总的来说,这篇文章很详细的讲解了用go进行产品开发过程中的很多经验,希望对大家有帮助。

读生产环境下go语言最佳实践有感,布布扣,bubuko.com

时间: 2024-10-06 01:10:12

读生产环境下go语言最佳实践有感的相关文章

SpringCloud从入门到进阶(四)——生产环境下Eureka的完全分布式部署

内容 由于前两节的内容我们知道,开启了preferIpAddress后,Eureka的伪分布式部署会提示replica不可用.这一节我们讲解如何在生产环境下部署完全分布式的Eureka集群,确保开启了preferIpAddress后replica的可用性. 版本 IDE:IDEA 2017.2.2 x64 JDK:1.8.0_171 manve:3.3.3 SpringBoot:1.5.9.RELEASE SpringCloud:Dalston.SR1 适合人群 Java开发人员 节点信息: 节

51CTO视频课程上线:如何快速搭建IT试验环境?-VMWare Workstations 最佳实践

51CTO视频课程上线:如何快速搭建IT试验环境?-VMWare Workstations 最佳实践 最近花了一些时间,将同学们在日常自学时常碰到搭建试验环境问题上录了一门课程,希望可以帮到. 课程链接:https://edu.51cto.com/course/17271.html 课程介绍 这是一门手把手指导你基于VMWare Workstation快速搭建满足IT运维试验环境的视频课程. 如果你有过看了N多学习课程后感觉自己好像都会了,但实际操作时脑海瞬间一片空白不知从哪下手.这一点都不奇怪

生产环境下ftp的迁移并构建高可用

说明:这是1个小项目就两台DELL的服务器,和一台IP SAN存储(DELL MD3200i).原来是4台小服务器,而且服务器太老了,经常有问题,这回相当于一次ftp的迁移,以前用的是proftp,这次换成了vsftp.数据量有2.5T. 拓扑很简单: 系统:CENTOS 6.4(64bit) 高可用软件:corosync+pacemaker host:ftp1 192.168.1.190 ftp2  192.168.1.191 stonith(ipmi):ftp1 192.168.1.180

生产环境下的iptables

生产环境下的iptables设置,这是我自己的一点总结,浅显之处望大家指出批评,共同学习. 我的局域网为192.168.1.0/24. 1.先清空所有规则 iptables -F iptables -X iptables -Z iptables -t nat -F iptables -t nat -X iptables -t nat -Z 设置默认规则前开发ssh(6123)端口 iptables -A INPUT -i eth0 -s 192.168.1.0/24 -p tcp --dport

生产环境下was不允许重启,怎么办?

前段时间上线,遇到一个jndi的故障问题,怎么个问题呢?就是原在测试环境下没有问题,而在生产环境下无法连接生产数据库,当时找到问题所在,就是ibm工具自动生成一个在测试环境下连接的jndi的资源文件resources.xml,当时删除了,重启了server,无效.后来我考虑到这肯定是was缓存造成,因此想象缓存造成的原因,最后在测试环境下重启了was,问题解决了,但后来说生产环境是不可能重启was的,因此暂时困老了本人,后来所谓的领导说,他去找总架构师看有没有办法解决,可是时间不等人,过了2天依

[原]生产环境下的nginx.conf配置文件(多虚拟主机)

[原]生产环境下的nginx.conf配置文件(多虚拟主机) 2013-12-27阅读110 评论0 我的生产环境下的nginx.conf配置文件,做了虚拟主机设置的,大家可以根据需求更改,下载即可在自己的机器上使用了,本配置文件摘录自<构建高可用Linux服务器>(机械工业出版社),转载麻烦注明出处,谢谢,配置文件如下: user  www www;worker_processes 8;error_log  /data/logs/nginx_error.log  crit;pid      

自己总结的C#编码规范--3.特定场景下的命名最佳实践

特定场景下的命名最佳实践 命名空间 要使用PascalCasing,并用点号来分隔名字空间中的各个部分. 如Microsof.Office.PowerPoint 要用公司名作为命名空间的前缀,这样就可以避免与另外一家公司使用相同的名字. 要用稳定的,与版本无关的产品名称作为命名空间的第二层 不要使用公司的组织架构来决定命名空间的层次结构,因为内部组织结构经常改变. 不要用相同的名字来命名命名空间和该空间内的类型. 例如,不要先将命名空间命名为Debug,然后又在该空间中提供Debug类.大部分编

EF 第三篇 生产环境下的数据迁移

前言 本文所谓数据迁移,直白点不如说成数据库升级.虽然大部分带服务器型的应用,所有客户端都是连到同一台服务器上,对这样的生产环境,数据库升级起来不是什么难事,用vs自带的Migration也好,执行sql脚本也好,都比较容易.然而在每家客户现场都要部署一台服务器的应用也不少,如果一家家手工地去升级数据库,那将是一个可怕的工作量.那么对于这样的环境要怎么做到自动升级数据库呢?相信大家也在网上搜了不少了EF关于生产环境下的数据迁移方案,然后99%搜到的都是使用vs自带的Migration命令方式迁移

Windows 2008下 rman backup scirpts(备份脚本)--已经在生产环境下通过验证

一.任务计划 windows -控制面板-管理工具-计划任务程序,右击"任务计划程序库","创建任务" 二:相关脚本中的内容: 2.1  back_rman.bat中的内容: set ORACLE_SID=FS F:\oracle\product\10.2.0\db_1\BIN\rman target / cmdfile=H:\worksql\windows_backupscript\backup.rcv log=H:\worksql\windows_backups