正如使用其它新兴语言或技术同样,咱们在早期的实验阶段经历了好一阵子的摸索期。Go语言确实有本身的风格与使用习惯,尤为是对于从面向对象语言(好比Java)或脚本语言(好比Python)转过来的开发者而言更是如此。因此咱们非常犯了些错误,在本文中咱们但愿能与你们分享所得。若是在生产环境中使用Go语言,下面这些问题都有可能碰到,但愿本文能为Go语言的初学者提供一些帮助。
1. Revel不是好的选择
对于初学Go语言、须要构建web服务器的用户来讲,他们也许会认为此时须要一个合适的框架。使用MVC框架确有优点,主要是因为惯例优先原则设置了一系列的项目架构与惯例,从而赋予了项目一致性,并下降了跨项目开发的门槛。但咱们发现:自行配置比遵循惯例更为强大,尤为是Go语言已经将编写web应用的难度降到了最低,而咱们的不少web应用都是小型服务。最重要的是:咱们的应用不符合惯例。
Revel的设计初衷在于:尝试将Play或Rails之类的框架引入Go语言,而不是运用Go与stdlib的力量,并以其为基础进行构建。根据Go语言编写者的说法:
引用
最初这只是一个有趣的项目,我想尝试可否在不那么神奇的Go语言中复制神奇的Play框架体验。
公平来说,那时候在一种新语言中采用MVC框架对咱们来讲颇有意义——无需争论架构,同时新团队也能连贯地构建内容。在使用Go语言以前,我所编写的每一个web应用都有着借助MVC框架的痕迹。在C#中使用了ASP.NET MVC,在Java中使用了SpringMVC,在PHP中使用了Symfony,在Python中使用了CherryPy,在Ruby中使用了RoR,但最后咱们终于发现,在Go语言中不须要框架。标准库HTTP包已经包含所需的内容了,通常只要加入多路复用器(好比 mux)来选择路由,再加入lib来处理中间件(好比
negroni)的任务(包括身份验证与登陆等)就足够了。
Go的标准库HTTP包设计让这项工做十分简单,使用者会渐渐发现:Go的强大有一部分缘由就在于其工具链与相关的工具——其中包含各类可运行在代码中的强大命令。但在Revel中,因为项目架构的设定,再加上缺少package main与func main() {}入口(这些都是惯用和必要的Go命令),咱们没法使用这些工具。事实上Revel附带本身的命令包,镜像一些相似run与build之类的命令。
使用Revel后,咱们:
- 没法运行go build;
- 没法运行go install;
- 没法使用 race detector (–race);
- 没法使用go-fuzz或者其它须要可构建Go资源的强大工具;
- 没法使用其它中间件或者路由;
- 热重载虽然简洁,但很缓慢,Revel在源上使用了反射机制(reflection),且从1.4版原本看,编译时间也增长了大约30%。因为并未使用go install,程序包没有缓存;
- 因为在Go 1.5及以上版本中编译速度更慢,所以没法迁移到高版本,为了将内核升级到1.6版,咱们去掉了Revel;
- Revel将测试放置在/test dir下面,违反了Go语言中将_test.go文件与测试文件打包在一块儿的习惯;
- 要想运行Revel测试,须要启动服务器并执行集成测试。
咱们发现Revel的不少方式与Go语言的构建习惯相去甚远,同时也失去了一些强大go工具集的协助。
2. 明智地使用Panics
若是你是从Java或C#转到Go语言的开发者,可能会有些不太习惯Go语言中的错误处理方式(error handling)。在Go语言中,函数可返回多个值,所以在返回其余值时一并返回error是很典型的状况,若是一切运行正常的话,resturnsError返回的值为nil(nil是Go语言中引用类型的默认值)。
- func something() (thing string, err error) {
- s := db.GetSomething()
- if s == "" {
- return s, errors.New("Nothing Found")
- }
- return s, nil
- }
因为咱们想要建立一个error,并在调用栈的更高层级中进行处理,所以最终使用了panic。
- s, err := something()
- if err != nil {
- panic(err)
- }
结果咱们彻底惊呆了:一个error?天啊,运行它!
但在Go中,你会发现error其实也是返回值,在函数调用和响应处理中十分常见,而panic则会拖慢应用的性能,并致使崩溃——相似运行异常时的崩溃。为何要仅仅由于须要函数返回error就这样作呢?这是咱们的教训。在1.6 版本发布前,转储panic的堆栈也负责转储全部运行的Go程序,致使在查找问题起源时很是困难,咱们在一大堆不相关的内容上查找了好久,白费力气。
就算有一个真正不可恢复的error,或是遇到了运行时的panic,极可能你也并不但愿整个web服务器崩溃,由于它也是不少其余服务的中间件(你的数据库也使用事务机制对吧?) 所以咱们学到了处理这些panic的方式:在Revel中添加filter可以让这些panic恢复,还能获取日志文件中的堆栈追踪记录并发送到
Sentry,而后经过电邮以及Teamwork Chat实时聊天工具给咱们发送警告,API向前端返回“500内部服务器错误”。
- func PanicFilter(rc *revel.Controller, fc []revel.Filter) {
- defer func() {
- if err := recover(); err != nil {
- handleInvocationPanic(rc, err)
- }
- }()
- fc[0](rc, fc[1:])
- }
3. 小心不止一次从Request.Body的读取
从http.Request.Body读取内容以后,其Body就被抽空了,随后再次读取会返回空body[]byte{} 。这是由于在读取一个http.Request.Body的数据时,读取器会停在数据的末尾,想要再次读取必须先进行重置。然而,http.Request.Body是一个io.ReadWriter,并未提供Peek或Seek之类能解决这个问题的方法。有一个解决办法是先将Body复制到内存中,读取以后再将本来的内容填回去。若是有大量request的话,这种方式的开销很大,只能算权宜之计。
下面是一段短小而完整的代码:
- package main
-
- import (
- "bytes"
- "fmt"
- "io/ioutil"
- "net/http"
- )
-
- func main() {
- r := http.Request{}
-
- r.Body = ioutil.NopCloser(bytes.NewBuffer([]byte("test")))
-
- s, _ := ioutil.ReadAll(r.Body)
- fmt.Println(string(s))
-
- s, _ = ioutil.ReadAll(r.Body)
- fmt.Println(string(s))
- }
这里包括复制及回填的代码:
- content, _ := ioutil.ReadAll(r.Body)
- r.Body = ioutil.NopCloser(bytes.NewBuffer(content))
- again, _ = ioutil.ReadAll(r.Body)
能够建立一些util函数:
- func ReadNotDrain(r *http.Request) (content []byte, err error) {
- content, err = ioutil.ReadAll(r.Body)
- r.Body = ioutil.NopCloser(bytes.NewBuffer(content))
- return
- }
以替代调用相似ioutil.ReadAll的方式:
- content, err := ReadNotDrain(&r)
固然,如今你已经用no-op替换了r.Body.Close(),在request.Body中调用Close时将不会执行任何操做,这也是httputil.DumpRequest的工做方式。
4. 一些持续优化的库有助于SQL的编写
在Teamwork Desk,向用户提供web应用服务的核心功能常要涉及MySQL,而咱们没有使用存储程序,所以在Go之中的数据层包含一些很复杂的MySQL……并且某些代码所构建的查询复杂程度,足以媲美奥林匹克体操比赛的冠军。一开始,咱们用
Gorm及其可链API来构建SQL,在Gorm中仍可以使用原始的SQL,并让它根据你的结构来生成结果(但在实践中,近来咱们发现这类操做愈来愈频繁,这表明着咱们须要从新调整使用Gorm的方式,以确保找到最佳方式,或者须要多看些替代方案——但也没什么好怕的!)
对于一些人来讲,对象关系映射(ORM)很是糟糕,它会让人失去控制力与理解力,以及优化查询的可能性,这种想法没错,但咱们只是用Gorm做为构建查询(能理解其输出的那部分)的封装方式,而不是看成ORM来彻底使用。在这种状况下,咱们能够像下面这样使用其可链API来构建查询,并根据具体结构来调整结果。它的不少功能方便在代码中手写SQL,还支持Preloading、Limits、Grouping、Associations、Raw SQL、Transactions等操做,若是你要在Go语言中手写SQL代码,那么这种方法值得一试。
- var customer Customer
- query = db.
- Joins("inner join tickets on tickets.customersId = customers.id").
- Where("tickets.id = ?", e.Id).
- Where("tickets.state = ?", "active").
- Where("customers.state = ?", "Cork").
- Where("customers.isPaid = ?", false).
- First(&customer)
5. 无指向的指针是没有意义的
实际上这里特指切片(slice)。你在向函数传值时使用到了切片?在Go语言中,数组(array)也是数值,若是有大量的数组的话,你也不但愿每次传值或者分配时都要复制一下吧?没错,让内存传递数组的开销是很大的,但在Go语言中,99%的时间里咱们处理的都是切片而不是数组。通常来说,切片能够当成数组部分片断的描述(常常是所有的片断),包含指向数组开始元素的指针、切片的长度与容量。
切片的每一个部分只须要8个字节, 所以不管底层是什么,数组有多大都不会超过24个字节。
咱们常常向函数切片发送指针,觉得能节省空间。
- t := getTickets()
- ft := filterTickets(&t)
-
- func filterTickets(t *[]Tickets) []Tickets {}
显而易见,若是没找到ticket,则返回0, 0, error;若是找到了ticket,则返回120, 80, nil之类的格式,具体数值取决于ticket的count。关键在于:若是在函数签名中命名了返回值,就可使用return(naked return),在调用返回时,也会返回每一个命名返回值所在的状态。
然而,咱们有一些大型函数,大到有些笨重的那种。在函数中的,任何长度须要翻页的naked returns都会极大地影响可读性,并容易形成细微不易察觉的bug。特别若是有多个返回点的话,千万不要使用naked returns或者大型函数。
下面是一个例子:
- func findTickets() (tickets []Ticket, countActive int64, err error) {
- tickets, countActive := db.GetTickets()
- if tickets == 0 {
- err = errors.New("no tickets found!")
- } else {
- tickets += addClosed()
-
- return
- }
- .
- .
- .
-
- .
- .
- .
- if countActive > 0 {
- countActive - closedToday()
-
- return
- }
- .
- .
- .
-
- return
- }
7. 小心做用域与缩略声明
在Go语言中,若是在不一样的块区内使用相同的缩略名:=来声明变量时,因为做用域(scope)的存在,会出现一些细微不易察觉的bug,咱们称之为shadowing。
- func findTickets() (tickets []Ticket, countActive int64) {
- tickets, countActive := db.GetTickets()
- if countActive > 0 {
-
- tickets, err := removeClosed()
- if err != nil {
-
-
- log.Printf("could not remove closed %s, ticket count %d", err.Error(), len(tickets))
- }
- }
- return
- }
具体在于:=缩略变量的声明与分配问题,通常来讲若是在左边使用新变量时,才会编译:=,但若是左边出现其余新变量的话,也是有效的。在上例中,err是新变量,由于在函数返回的参数中已经声明过,你觉得ticket会被自动覆盖。但事实并不是如此,因为块区做用域的存在,在声明和分配新的ticket变量后,一旦块区闭合,其做用域就会丢失。为了解决这个问题,咱们只需声明变量err位于块区以外,再用=来代替:=,优秀的编辑器(好比加入Go插件的Emacs或Sublime就能解决这个shadowing的问题)。
- func findTickets() (tickets []Ticket, countActive int64) {
- var err error
- tickets, countActive := db.GetTickets()
- if countActive > 0 {
- tickets, err = removeClosed()
- if err != nil {
- log.Printf("could not remove closed %s, ticket count %d", err.Error(), len(tickets))
- }
- }
- return
- }
8. 映射与随机崩溃
在并发访问时,映射并不安全。咱们曾出现过这个状况:将映射做为应用整个生命周期的应用级变量,在咱们的应用中,这个映射是用来收集每一个控制器统计数据的,固然在Go语言中每一个http request都是本身的goroutine。
你能够猜到下面会发生什么,实际上不一样的goroutine会尝试同时访问映射,也多是读取,也多是写入,可能会形成panic而致使应用崩溃(咱们在Ubuntu中使用了
upstart脚本,在进程中止时重启应用,至少保证应用算是“在线”)。有趣的是:这种状况随机出现,在1.6版本以前,想要找出像这样出现panic的缘由都有些费劲,由于堆栈转储包含全部运行状态下的goroutine,从而致使咱们须要过滤大量的日志。
在并发访问时,Go团队的确考虑过映射的安全性问题,但最终放弃了,由于在大多数状况下这种方式会形成非必要开销,在
golang.org的FAQ中有这样的解释:
在通过长期讨论后,咱们决定在使用映射时,通常不需从多个goroutine执行安全访问。在确实须要安全访问时,映射极可能属于已经同步过的较大数据架构或者计算。所以,若是要求全部映射操做须要互斥锁的话,会拖慢大多数程序,但效果寥寥无几。因为不经控制的映射访问会让程序崩溃,做出这个决定并不容易。
咱们的代码看起来就象这样:
- package stats
-
- var Requests map[*revel.Controller]*RequestLog
- var RequestLogs map[string]*PathLog
咱们对其进行了修改,使用stdlib的同步数据包:在封装映射的结构中嵌入读取/写入互斥锁。咱们为这个结构添加了一些helper:Add与Get方法:
- var Requests ConcurrentRequestLogMap
-
- func init() {
- Requests = ConcurrentRequestLogMap{items: make(map[interface{}]*RequestLog)}
- }
-
- type ConcurrentRequestLogMap struct {
- sync.RWMutex
- items map[interface{}]*RequestLog
- }
-
- func (m *ConcurrentRequestLogMap) Add(k interface{}, v *RequestLog) {
- m.Lock()
- m.items[k] = v
- m.Unlock()
- }
-
- func (m *ConcurrentRequestLogMap) Get(k interface{}) (*RequestLog, bool) {
- m.RLock()
- v, ok := m.items[k]
- m.RUnlock()
-
- return v, ok
- }
如今不再会崩溃了。
9. Vendor的使用
好吧,虽然难以启齿,但咱们恰好犯了这个错误,罪责重大——在将代码部署到生产环境时,咱们竟然没有使用vendor。
简单解释一下,在Go语言中,咱们经过从项目根目录下运行go get ./...来得到依赖, 每一个依赖都须要从主服务器的HEAD上拉取,很显然这种状况很是糟糕,除非在$GOPATH的服务器上保存依赖的准确版本,而且一直不作更新(也不从新构建或运行新的服务器),若是更改无可回避,你会对生产环境中运行的代码失去控制。在Go 1.4版本中,咱们使用了Godeps及其GOPATH来执行vendor;在1.5版本中,咱们使用了GO15VENDOREXPERIMENT环境变量;到了1.6版本,终于不须要工具了——项目根目录下的/vendor能够自动识别为依赖的存放位置。你能够在不一样的vendor工具中选择一个来追踪版本号,让依赖的添加与更新更为简单(移除.git,更新清单等)。
收获良多,但学无止境 上面仅仅列出了咱们初期所犯错误与所获心得的一小部分。咱们只是由5名开发者组成的小团队,建立了Teamwork Desk,尽管去年咱们在Go语言方面所获良多,但还有大批的优秀功能蜂拥而至。今年咱们会出席各类关于Go语言的大会,包括在丹佛举行的GopherCon大会;另外我还在Cork的当地开发者聚会上就Go的使用进行了讨论。 咱们会继续发布Go语言相关的开源工具,并致力于回馈现有的库。目前咱们已经适当提供了一些小型项目(参见列表),所发的Pull Request也被Stripe、Revel以及一些其余的开源Go项目所采纳。