阅读更多
引用

原文: Go Learn : Learning as we Go
作者: Peter Kelly,Teamwork Desk的高级工程师
翻译: 孙薇

摘要: Teamwork团队在去年写了近20万行Go代码,建造了一堆速度奇快的小型HTTP服务,本文列出了他们总结的9条经验教训。

为什么选择Go语言?Go语言,又称Golang,是Google开发的一款静态强类型、编译型、并发型,并具有垃圾回收机制的编程语言,它的运行速度非常之快,同时还有如下特性:具有一流的标准库、无继承关系、支持多核;同时它还有着传说级的设计者与极其优秀的社区支持,更别提还有对于我们这些web应用的编写者异常方便、可以避免事件循环与回调地狱的goroutine-per-request设置了(每次请求处理都需要启动一个独立的goroutine)。目前,Go语言已经成为构建系统、服务器,特别是微服务的热门选择。

正如使用其它新兴语言或技术一样,我们在早期的实验阶段经历了好一阵子的摸索期。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内部服务器错误”。
// PanicFilter wraps the action invocation in a protective defer blanket that
// recovers panics, logs everything, and returns 500.
func PanicFilter(rc *revel.Controller, fc []revel.Filter) {  
    defer func() {
        if err := recover(); err != nil {
            handleInvocationPanic(rc, err) // stack trace, logging. alerting            
        }
    }()
    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{}
    // Body is an io.ReadWriter so we wrap it up in a NopCloser to satisfy that interface
    r.Body = ioutil.NopCloser(bytes.NewBuffer([]byte("test")))

    s, _ := ioutil.ReadAll(r.Body)
    fmt.Println(string(s)) // prints "test"

    s, _ = ioutil.ReadAll(r.Body)
    fmt.Println(string(s)) // prints empty string! 
}

这里包括复制及回填的代码:
content, _ := ioutil.ReadAll(r.Body)  
// Replace the body with a new io.ReadCloser that yields the same bytes
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() // e.g. returns []Tickets, a slice  
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, hmmm...okay, I might know what this is
        return 
    }
    .
    .
    .
    // lots more code
    .
    .
    .
    if countActive > 0 {
        countActive - closedToday()
        // have to scroll back up now just to be sure...
        return
    }
    .
    .
    .
    // Okay, by now I definitely can't remember what I was returning or what values they might have
    return
}

7. 当心作用域与缩略声明
在Go语言中,如果在不同的块区内使用相同的缩略名:=来声明变量时,由于作用域(scope)的存在,会出现一些细微不易察觉的bug,我们称之为shadowing。
func findTickets() (tickets []Ticket, countActive int64) {  
    tickets, countActive := db.GetTickets() // 10 tickets returned, 3 active
    if countActive > 0 {
        // oops, tickets redeclared and used just in this block
        tickets, err := removeClosed() // 6 tickets left after removing closed
        if err != nil {
            // Argh! We used the variables here for logging!, if we didn't we would
            // have received a compile-time error at least for unused variables.
            log.Printf("could not remove closed %s, ticket count %d", err.Error(), len(tickets))
        }
    }
    return // this will return 10 tickets o_O
}

具体在于:=缩略变量的声明与分配问题,一般来说如果在左边使用新变量时,才会编译:=,但如果左边出现其他新变量的话,也是有效的。在上例中,err是新变量,因为在函数返回的参数中已经声明过,你以为ticket会被自动覆盖。但事实并非如此,由于块区作用域的存在,在声明和分配新的ticket变量后,一旦块区闭合,其作用域就会丢失。为了解决这个问题,我们只需声明变量err位于块区之外,再用=来代替:=,优秀的编辑器(比如加入Go插件的Emacs或Sublime就能解决这个shadowing的问题)。
func findTickets() (tickets []Ticket, countActive int64) {  
    var err error
    tickets, countActive := db.GetTickets() // 10 tickets returned, 3 active
    if countActive > 0 {
        tickets, err = removeClosed() // 6 tickets left after removing closed
        if err != nil {
            log.Printf("could not remove closed %s, ticket count %d", err.Error(), len(tickets))
        }
    }
    return // this will return 6 tickets
}

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

// init is run for each package when the app first runs
func init() {  
    Requests = ConcurrentRequestLogMap{items: make(map[interface{}]*RequestLog)}
}

type ConcurrentRequestLogMap struct {  
    sync.RWMutex // We embed the sync primitive, a reader/writer Mutex
    items map[interface{}]*RequestLog
}

func (m *ConcurrentRequestLogMap) Add(k interface{}, v *RequestLog) {  
    m.Lock() // Here we can take a write lock
    m.items[k] = v
    m.Unlock()
}

func (m *ConcurrentRequestLogMap) Get(k interface{}) (*RequestLog, bool) {  
    m.RLock() // And here we can take a read lock
    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项目所采纳。
  • 大小: 3.6 KB
2
0
评论 共 1 条 请登录后发表评论
1 楼 slaser 2016-05-24 05:21
坑太多了就要反思语言本身是不是成熟。

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 信息检索与利用报告

    武科大的信息检索与利用报告 先熟悉百度高级搜索功能,然后完成以下习题。 (1)请检索关于绿色制造与节能减排方面的专业文档,并将你觉得有价值的论文的页面或者链接保存在作业中。 (2)请从腾讯网下载腾讯QQ软件,并将检索过程简单记录在作业中。 2. 先熟悉谷歌高级搜索功能,然后完成以下习题。 (1)请在谷歌学术搜索中检索有关环境友好型新材料与新工艺方面的文献,并将你觉得有价值的论文的页面或者链接保存在作业中。 (2)通过谷歌地图找到武汉科技大学青山校区门口的“建一邮政储蓄ATM”的位置,并将图片记录在作业中。

  • 多媒体信息检索技术简介 (基于内容的视频检索)

    http://www.istis.sh.cn/list/list.asp?id=1532 多媒体技术和Internet的发展给人们带来巨大的多媒体信息海洋,并进一步导致了超大型多媒体信息库的产生,光凭关键词是很难做到对多媒体信息的描述和检索的,这就需要有一种针对多媒体的有效的检索方式。如何有效的帮助人们快速、准确地找到所需要的多媒体信息,成了多媒体信息库所要解决的核心问题。   基于内容的信...

  • 信息检索课题检索报告

    信息检索信息检索信息检索信息检索 课题检索报告

  • 北邮 基于多媒体的信息检索平台设计_小学期

    北邮 基于多媒体的信息检索平台设计_小学期

  • 信息检索实验报告

    信息检索实验基本上是大一新手都要完成的任务。当初我写这个报告的时候就是无从下手,很纠结。我本着善良的心,希望大一的前途一片光明,不要吊死在这颗树上!

  • PHP语言的前景怎么样,为什么我建议你学PHP语言

    世界上没有完美的语言,只有适合的语言,每个语言有自己的应用场景,适合的项目类别和解决方案,通过10多年的开发历程,我相信在今天,我能够说出我心目中我认为 最好,也是我自己最喜欢的语言。就是PHP。

  • 为什么我说Rust是靠谱的编程语言

    作者尽量较少的谈及Rust语言本身,反而尝试从Rust语言周边入手,长时间、大范围、多角度地考察,研判Rust语言是否靠谱,并给出尽可能客观的理由。为写成本文,作者Liigo不惜“卧底”Rust“老巢”长达一年多,收集...

  • 2022年需求量最大的8种编程语言

    当今,我们已知的编程语言多达几百种,但是常被大家使用的只占少数,无论你是刚入行的新手还是一名成熟的开发者,了解编程语言的受欢迎程度都很有必要。 最近,国外网站DevJobsScanner公布了一项数据,他们在过去...

  • HBASE从入门到精通

    HBASE从入门到精通   Hadoop学习教程: http://ihadoop.taobao.com http://item.taobao.com/item.htm?spm=686.1000925.1000774.6.uOwp2B&id=24255708186 一、 HBase技术介绍   HBase简介 HBase – ...

  • 武汉大学信息检索报告【研究生课程】

    面对日益纷繁复杂的网络信息,你是否感觉检索和获取信息有些力不从心?《信息检索》课程从实际操作出发,讲授如何精准、快捷地获取你想要的资源,培养信息素养,使你能够从容面对信息爆炸的挑战。无论是生活、学习、工作还是科研,你都能从本课程中找到对应的信息检索的工具与方法,从而终身受益。

  • 信息检索报告

    利用西华大学图书馆OPAC、超星数字图书馆、独秀学术搜索等检索相关信息并按照信息检索步骤写出操作过程

  • 信息检索报告模板

    综合信息检索报告模板,只是提供一个写报告的模式,仅供参考

  • 信息检索报告_资源|人工智能之信息检索与推荐报告分享

    点击上方“AI遇见机器学习”,选择“星标”公众号重磅干货,第一时间送达一、资源简介今天给大家分享的是一份AMiner发布的《人工智能之信息检索与推荐》报告,这份报告分别从技术、人才、产业应用和趋势等角度,来阐释目前信息检索和推荐领域的现状和发展。信息检索(Information Retrieval)是用户进行信息查询和获取的主要方式,是查找信息的方法和手段。狭义的信息检索仅指信息查询(I...

  • 音乐信息检索综述报告

    参考资料:    1. An Introduction to Audio Content Analysis 2.Fundamentals of Music Processing 3.Music Data Mining 4.Music Recommendation and Discovery:The LongTail Long Fail and Long Playin the Digital

  • 小米技术分享:解密小米抢购系统千万高并发架构的演进和实践

    小米技术分享:解密小米抢购系统千万高并发架构的演进和实践 本文原文内容引用自高可用架构公众号,内容有整理和修订。 1、引言 大家对下面这个排队的场景应该非常熟悉,这个是小米手机抢购的用户排队交互图,...

  • go技术文章梳理(2018)

    Go GraphQL 新手指南: https://tutorialedge.net/golang/go-graphql-beginners-tutorial/ 3. 你需要 Go web 框架吗:https://medium.com/@tusharsoni/do-you-need-a-web-framework-for-go-51171bb0ea8c 4. ...

  • 软件测试基础知识大全(新手入门必备)

    软件测试基础知识大全(新手入门必备) 测 试 基 础 1、 软件测试的目的:证明(表达软件能够工作)→ 检测(发现错误)→ 预防(管  理质量) 2、 测试执行:单元测试(UT执行):一个测试用例的测试...

  • 数百种编程语言,我为什么只学Python?

    如果让你从数百种的编程语言中选择一个入门语言?你会选择哪一个? 是应用率最高、长期霸占排行榜的常青藤 Java?是易于上手,难以精通的 C?还是在游戏和工具领域仍占主流地位的 C++?亦或是占据 Windows ...

  • GitChat · 前端 | 从软件工程角度看大前端技术栈

    GitChat 作者:韩亦乐 关注微信公众号:GitChat 技术杂谈 ,这里一本正经的讲技术 我们都知道,大学几乎是...再者,由于这些选修课的课时短、面向的主要群体是大部分大学生等原因,其最终所涉及的内容层次也很难赶上近

Global site tag (gtag.js) - Google Analytics