互相填a微goa标签分享微信代码码的有吗

理解Golang包导入 - Tony Bai
理解Golang包导入
三月 9, 2015
使用包(package)这种语法元素来组织源码,所有语法可见性均定义在package这个级别,与Java 、python等语言相比,这算不上什么创新,但与C传统的include相比,则是显得&先进&了许多。
中包的定义和使用看起来十分简单:
通过package关键字定义包:
&&& package xxx
使用import关键字,导入要使用的标准库包或第三方依赖包。
&& import &a/b/c&
&& import &fmt&
&& c.Func1()
&& fmt.Println(&Hello, World&)
很多Golang初学者看到上面代码,都会想当然的将import后面的&c&、&fmt&当成包名,将其与c.Func1()和 fmt.Println()中的c和fmt认作为同一个语法元素:包名。但在深入Golang后,很多人便会发现事实上并非如此。比如在使用实时分布式消 息平台nsq提供的go client api时:
我们导入的路径如下:
&& import &github.com/bitly/go-nsq&
但在使用其提供的export functions时,却用做前缀包名:
&& q, _ := nsq.NewConsumer(&write_test&, &ch&, config)
人们不禁要问:import后面路径中的最后一个元素到底代表的是啥? 是包名还是仅仅是一个路径?我们一起通过试验来理解一下。& 实验环境:darwin_amd64 , go 1.4。
初始试验环境目录结果如下:
GOPATH = /Users/tony/Test/Go/pkgtest/
& &&&& libproj1/
&&& &&&&&& foo/
&&& & & & &&& foo1.go
&&& && app1/
& &&& &&&& main.go
一、编译时使用的是包源码还是.a
我们知道一个非main包在编译后会生成一个.a文件(在临时目录下生成,除非使用go install安装到$GOROOT或$GOPATH下,否则你看不到.a),用于后续可执行程序链接使用。
比如Go标准库中的包对应的源码部分路径在:$GOROOT/src,而标准库中包编译后的.a文件路径在$GOROOT/pkg/darwin_amd64下。一个奇怪的问题在我脑袋中升腾起来,编译时,编译器到底用的是.a还是源码?
我们先以用户自定义的package为例做个小实验。
$GOPATH/src/
&&& libproj1/foo/
&&& &&& &&& – foo1.go
&&& &&& &&& – main.go
package foo
import &fmt&
func Foo1() {
&&& fmt.Println(&Foo1&)
// main.go
package main
&&& &libproj1/foo&
func main() {
&&& foo.Foo1()
执行go install libproj1/foo,Go编译器编译foo包,并将foo.a安装到$GOPATH/pkg/darwin_amd64/libproj1下。
编译app1:go build app1,在app1目录下生成app1*可执行文件,执行app1,我们得到一个初始预期结果:
现在我们无法看出使用的到底是foo的源码还是foo.a,因为目前它们的输出都是一致的。我们修改一下foo1.go的代码:
package foo
import &fmt&
func Foo1() {
&&& fmt.Println(&Foo1 – modified&)
重新编译执行app1,我们得到结果如下:
Foo1 – modified
实际测试结果告诉我们:(1)在使用第三方包的时候,当源码和.a均已安装的情况下,编译器链接的是源码。
那么是否可以只链接.a,不用第三方包源码呢?我们临时删除掉libproj1目录,但保留之前install的libproj1/foo.a文件。
我们再次尝试编译app1,得到如下错误:
$go build app1
main.go:5:2: cannot find package &libproj1/foo& in any of:
&&& /Users/tony/.Bin/go14/src/libproj1/foo (from $GOROOT)
&&& /Users/tony/Test/Go/pkgtest/src/libproj1/foo (from $GOPATH)
编译器还是去找源码,而不是.a,因此我们要依赖第三方包,就必须搞到第三方包的源码,这也是Golang包管理的一个特点。
其实通过编译器的详细输出我们也可得出上面结论。我们在编译app1时给编译器传入-x -v选项:
$go build -x -v app1
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build
libproj1/foo
mkdir -p $WORK/libproj1/foo/_obj/
mkdir -p $WORK/libproj1/
cd /Users/tony/Test/Go/pkgtest/src/libproj1/foo
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/libproj1/foo.a -trimpath $WORK -p libproj1/foo -complete -D _/Users/tony/Test/Go/pkgtest/src/libproj1/foo -I $WORK -pack ./foo1.go ./foo2.go
mkdir -p $WORK/app1/_obj/
mkdir -p $WORK/app1/_obj/exe/
cd /Users/tony/Test/Go/pkgtest/src/app1
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/app1.a -trimpath $WORK -p app1 -complete -D _/Users/tony/Test/Go/pkgtest/src/app1 -I $WORK -I /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -pack ./main.go
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -extld=clang $WORK/app1.a
mv $WORK/app1/_obj/exe/a.out app1
可以看到编译器6g首先在临时路径下编译出依赖包foo.a,放在$WORK/libproj1下。但我们在最后6l链接器的执行语句中并未显式看到app1链接的是$WORK/libproj1下的foo.a。但是从6l链接器的-L参数来看:-L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64,我们发现$WORK目录放在了前面,我们猜测6l首先搜索到的时$WORK下面的libproj1/foo.a。
为了验证我们的推论,我们按照编译器输出,按顺序手动执行了一遍如上命令,但在最后执行6l命令时,去掉了-L $WORK:
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -extld=clang $WORK/app1.a
这样做的结果是:
编译器链接了$GOPATH/pkg下的foo.a。(2)到这里我们明白了所谓的使用第三方包源码,实际上是链接了以该最新源码编译的临时目录下的.a文件而已。
Go标准库中的包也是这样么?对于标准库,比如fmt而言,编译时,到底使用的时$GOROOT/src下源码还是$GOROOT/pkg下已经编译好的.a呢?
我们不妨也来试试,一个最简单的hello world例子:
import &fmt&
func main() {
&&& fmt.Println(&Hello, World&)
我们先将$GOROOT/src/fmt目录rename 为fmtbak,看看go compiler有何反应?
go build -x -v ./
$go build -x -v ./
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build
main.go:4:8: cannot find package &fmt& in any of:
&&& /Users/tony/.Bin/go14/src/fmt (from $GOROOT)
&&& /Users/tony/Test/Go/pkgtest/src/fmt (from $GOPATH)
找不到fmt包了。显然标准库在编译时也是必须要源码的。不过与自定义包不同的是,即便你修改了fmt包的源码(未重新编译GO安装包),用户源码编译时,也不会尝试重新编译fmt包的,依旧只是在链接时链接已经编译好的fmt.a。通过下面的gc输出可以验证这点:
$go build -x -v ./
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build
mkdir -p $WORK/app1/_obj/
mkdir -p $WORK/app1/_obj/exe/
cd /Users/tony/Test/Go/pkgtest/src/app1
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/app1.a -trimpath $WORK -p app1 -complete -D _/Users/tony/Test/Go/pkgtest/src/app1 -I $WORK -pack ./main.go
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L $WORK -extld=clang $WORK/app1.a
mv $WORK/app1/_obj/exe/a.out app1
可以看出,编译器的确并未尝试编译标准库中的fmt源码。
二、目录名还是包名?
从第一节的实验中,我们得知了编译器在编译过程中依赖的是包源码的路径,这为后续的实验打下了基础。下面我们再来看看,Go语言中import后面路径中最后的一个元素到底是包名还是路径名?
本次实验目录结构:
&&& && libproj2/
&&&& &&& &&& foo/
&&& && &&& &&& foo1.go
&&&&&& app2/
&&&&&&& &&&& main.go
按照Golang语言习惯,一个go package的所有源文件放在同一个目录下,且该目录名与该包名相同,比如libproj1/foo目录下的package为foo,foo1.go、 foo2.go…共同组成foo package的源文件。但目录名与包名也可以不同,我们就来试试不同的。
我们建立libproj2/foo目录,其中的foo1.go代码如下:
package bar
import &fmt&
func Bar1() {
&&& fmt.Println(&Bar1&)
注意:这里package名为bar,与目录名foo完全不同。
接下来就给app2带来了难题:该如何import bar包呢?
我们假设import路径中的最后一个元素是包名,而非路径名。
//app2/main.go
package main
&&& &libproj2/bar&
func main() {
&&& bar.Bar1()
编译app2:
$go build -x -v app2
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build
main.go:5:2: cannot find package &libproj2/bar& in any of:
&&& /Users/tony/.Bin/go14/src/libproj2/bar (from $GOROOT)
&&& /Users/tony/Test/Go/pkgtest/src/libproj2/bar (from $GOPATH)
编译失败,在两个路径下无法找到对应libproj2/bar包。
我们的假设错了,我们把它改为路径:
//app2/main.go
package main
&&& &libproj2/foo&
func main() {
&&& bar.Bar1()
再编译执行:
$go build app2
这回编译顺利通过,执行结果也是OK的。这样我们得到了结论:(3)import后面的最后一个元素应该是路径,就是目录,并非包名。
go编译器在这些路径(libproj2/foo)下找bar包。这样看来,go语言的惯例只是一个特例,即恰好目录名与包名一致罢了。也就是说下面例子中的两个foo含义不同:
import &libproj1/foo&
func main() {
&&& foo.Foo()
import中的foo只是一个文件系统的路径罢了。而下面foo.Foo()中的foo则是包名。而这个包是在libproj1/foo目录下的源码中找到的。
再类比一下标准库包fmt。
import &fmt&
fmt.Println(&xxx&)
这里上下两行中虽然都是&fmt&,但同样含义不同,一个是路径 ,对于标准库来说,是$GOROOT/src/fmt这个路径。而第二行中的fmt则是包名。gc会在$GOROOT/src/fmt路径下找到fmt包的源文件。
三、import m &lib/math&
Go language specification中关于import package时列举的一个例子如下:
Import declaration&&&&&&&&& Local name of Sin
import&& &lib/math&&&&&&&&& math.Sin
import m &lib/math&&&&&&&&& m.Sin
import . &lib/math&&&&&&&&& Sin
我们看到import m &lib/math&& m.Sin一行。我们说过lib/math是路径,import语句用m替代lib/math,并在代码中通过m访问math包中的导出函数Sin。
那m到底是包名还是路径呢?既然能通过m访问Sin,那m肯定是包名了,Right!那import m &lib/math&该如何理解呢?&
根据上面一、二两节中得出的结论,我们尝试理解一下m:(4)m指代的是lib/math路径下唯一的那个包。
一个目录下是否可以存在两个包呢?我们来试试。
我们在libproj1/foo下新增一个go源文件,bar1.go:
package bar
import &fmt&
func Bar1() {
&&& fmt.Println(&Bar1&)
我们重新构建一下这个目录下的包:
$go build libproj1/foo
can't load package: package libproj1/foo: found packages bar1.go (bar) and foo1.go (foo) in /Users/tony/Test/Go/pkgtest/src/libproj1/foo
我们收到了错误提示,编译器在这个路径下发现了两个包,这是不允许的。
我们再作个实验,来验证我们对m含义的解释。
我们建立app3目录,其main.go的源码如下:
package main
import m &libproj2/foo&
func main() {
&&& m.Bar1()
libproj2/foo路径下的包的包名为bar,按照我们的推论,m指代的就是bar这个包,通过m我们可以访问bar的Bar1导出函数。
编译并执行上面main.go:
$go build app3
执行结果与我们推论完全一致。
附录:6g, 6l文档位置:
6g – $GOROOT/src/cmd/gc/doc.go
6l – $GOROOT/src/cmd/ld/doc.go
& 2015, . 版权所有.
Related posts:
添加新评论
如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时。十分感谢!
欢迎使用邮件订阅我的博客输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!
的个人Blog,欢迎访问、订阅和留言!
订阅Feed请点击上面图片。
如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!
如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:
如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:
如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
著名主机提供商Linode 10$优惠码:linode10,在
即可免费获 得。阿里云推荐码:
我的业余项目我的分享码是:64b56互相开通一下留下你的【avgo吧】_百度贴吧
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&签到排名:今日本吧第个签到,本吧因你更精彩,明天继续来努力!
本吧签到人数:0可签7级以上的吧50个
本月漏签0次!成为超级会员,赠送8张补签卡连续签到:天&&累计签到:天超级会员单次开通12个月以上,赠送连续签到卡3张
关注:798贴子:
我的分享码是:64b56互相开通一下留下你的
我的分享码是:64b56互相开通一下留下你的
帮你了,帮帮我
我怎么输入不了分享码啊
贴吧热议榜
使用签名档&&
保存至快速回贴Go语言实践:从新手入门到上线真实的小型服务所遇到的那些坑 - 编程语言 - ITeye资讯
引用
原文:
作者: Peter Kelly,Teamwork Desk的高级工程师
翻译: 孙薇
摘要: Teamwork团队在去年写了近20万行Go代码,建造了一堆速度奇快的小型HTTP服务,本文列出了他们总结的9条经验教训。
为什么选择?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来处理中间件(比如 )的任务(包括身份验证与登录等)就足够了。
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恢复,还能获取日志文件中的堆栈追踪记录并发送到,然后通过电邮以及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
"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))
以替代调用类似ioutil.ReadAll的方式:
content, err := ReadNotDrain(&r)
当然,现在你已经用no-op替换了r.Body.Close(),在request.Body中调用Close时将不会执行任何操作,这也是httputil.DumpRequest的工作方式。
4. 一些持续优化的库有助于SQL的编写
在Teamwork Desk,向用户提供web应用服务的核心功能常要涉及MySQL,而我们没有使用存储程序,因此在Go之中的数据层包含一些很复杂的MySQL……而且某些代码所构建的查询复杂程度,足以媲美奥林匹克体操比赛的冠军。一开始,我们用及其可链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!")
tickets += addClosed()
// return, hmmm...okay, I might know what this is
// lots more code
if countActive & 0 {
countActive - closedToday()
// have to scroll back up now just to be sure...
// Okay, by now I definitely can't remember what I was returning or what values they might have
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中使用了脚本,在进程停止时重启应用,至少保证应用算是“在线”)。有趣的是:这种情况随机出现,在1.6版本之前,想要找出像这样出现panic的原因都有些费劲,因为堆栈转储包含所有运行状态下的goroutine,从而导致我们需要过滤大量的日志。
在并发访问时,Go团队的确考虑过映射的安全性问题,但最终放弃了,因为在大多数情况下这种方式会造成非必要开销,在中有这样的解释:
在经过长期讨论后,我们决定在使用映射时,一般不需从多个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项目所采纳。}

我要回帖

更多关于 a标签分享微信代码 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信