转眼加入蚂蚁已经三个多月,这期间主要维护一 Go 写的服务器。虽然用的时间不算长,但仍是积累了一些心得体会,这里总结概括一下,供想尝试 Go 的同窗参考。
本文会依次介绍 Go 的设计理念、开发环境、语言特性。本文在谈及语言特性的时也会讨论一些 Go 的不足之处,旨在给读者提供一个全面的视角。html
通常来讲,编程语言都会有一个 slogan 来表示它们的特色。好比提到 Clojure,通常会想到这么几个词汇:lisp on JVM、immutable、persistent;Java 的话我能想到的是企业级开发、中规中矩。对于 Go ,官网介绍到:java
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.git
提取几个关键词:open(开放)、simple(简洁)、reliable(可靠)、efficient(高效)。这也能够说是它的设计目标。除了上面这些口号外,初学者还须要知道 Go 是一门命令式的静态语言(是指在编译时检查变量类型是否匹配),与 Java 属于同一类别。github
Imperative | Functional | |
---|---|---|
Dynamic | Python/Ruby/Javascript | Lisp/Scheme/Clojure |
Static | Java/C++/Rust/Go | OCaml/Scala/Haskell |
因为 Hello World 太简洁,不具有展现 Go 的特色,因此下面展现一段访问 httpbin,打印 response 的完整代码。golang
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { // http://httpbin.org/#/Anything/get_anything r, err := http.Get("http://httpbin.org/anything?hello=world") if err != nil { panic(err) } defer r.Body.Close() body, err := ioutil.ReadAll(r.Body) if err != nil { panic(err) } fmt.Printf("body = %s\n", string(body)) }
上面的代码片断包括了 Go 的主要组成:包的声明与引用、函数定义、错误处理、流程控制、defer。算法
经过上面的代码片断,能够看出 Go 语言 simple(简洁)的特色,因此找一个最熟悉的文本编辑器,通常经过配置插件,均可以达到快速开发的目的。好久以前我就已经把全部文本编辑放到 Emacs 上,这里介绍下个人配置。编程
除了 go-mode 这个 major mode,为了配置像 源码跳转、API 自动补全、查看函数文档等现代 IDE 必备功能,须要安装如下命令c#
go get -u github.com/rogpeppe/godef go get -u github.com/stamblerre/gocode # for go-eldoc/company-go go get -u golang.org/x/tools/cmd/goimports go get -u github.com/kisielk/errcheck go get -u github.com/lukehoban/go-outline # for go-imenu
而后再按照 setup-go.el 里的配置,就拥有了一个功能完备的开发环境。api
不像 Java 语言须要运行时,Go 支持直接将整个项目 build 成一个二进制文件,方便部署,而支持交叉编译,不过在开发时,直接 go run XXX.go
更为便利,截止到 Go 1.12,还不支持 REPL,官方有提供在线版的 Playground 供分享、调试代码。数组
我我的的习惯是建一个 go-app 项目,每一个要测试的逻辑放到一个 test 里面去,这样就可使用 go test -v -run XXX
来运行。之因此不选用 go run
,是由于一个目录下只容许有一个 main 的 package,多个 IDE 会提示错误。
通常编程语言,数据类型分为基本的与复杂的两类。
基本的通常比较简单,表示一个值,Go 里面就有 string, bool, int8, int32(rune), int64, float32, float64, byte(uint8) 等基本类型
复杂类型通常表示多个值或具备某些高级用法,Go 里面有:
&
与间接访问 *
操做符,不支持对指针进行算术操做下面将重点介绍 Go 里特有或用途最广的数据类型。
Go 里面的 struct 相似于 Java 里面的 Object,可是并无继承,仅仅是对数据的一层包装(抽象)。相对于其余复杂类型,struct 是值类型,也就是说做为函数参数或返回值时,会拷贝一份值,值类型分配在 stack 上,与之相对的引用类型,分配在 heap 上。
初学者通常会有这样的误区,认为传值比传引用要慢,实则否则,具体涉及到 Go 如何管理内存,这里暂不详述,感兴趣到能够阅读:
BenchmarkByPointer-8 20000000 86.7 ns/op BenchmarkByValue-8 50000000 31.9 ns/op
因此通常推荐直接使用值类型的 struct,若是确认这是瓶颈了,能够再尝试改成引用类型(&struct)
若是说 struct 是对状态的封装,那么 interface 就是对行为的封装,至关于对外的契约(contract)。并且 Go 里面有这么一条最佳实践
Accept interfaces, return concrete structs. (函数的参数尽可能为 interface,返回值为 struct)
这样的好处也很明显,做为类库的设计者,对其要求的参数尽可能宽松,方便使用,返回具体值方便后续的操做处理。一个极端的状况,能够用 interface{}
表示任意类型的参数,由于这个接口里面没有任何行为,因此全部类型都是符合的。又因为 Go 里面不支持范型,因此interface{}
是惟一的解决手段。
相比较 Java 这类面向对象的语言,接口须要显式(explicit)继承(使用 implements 关键字),而在 Go 里面是隐式的(implicit),新手每每须要一段时间来体会这一作法的巧妙,这里举一例子来讲明:
Go 的 IO 操做涉及到两个基础类型:Writer/Reader ,其定义以下:
type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }
自定义类型若是实现了这两个方法,那么就实现了这两个接口,下面的 Example 就是这么一个例子:
type Example struct { } func (e *Example) Write(p byte[]) (n int, err error) { } func (e *Example) Read(p byte[]) (n int, err error) { }
因为隐式继承过于灵活,在 Go 里面可能会看到以下代码:
var _ blob.Fetcher = (*CachingFetcher)(nil)
这是经过将 nil 强转为 *CachingFetcher
,而后在赋值时,指定 blob.Fetcher
类型,保证 *CachingFetcher
实现了 blob.Fetcher
接口。
做为接口的设计者,若是想实现者显式继承一个接口,能够在接口中额外加一个方法。好比:
type Fooer interface { Foo() ImplementsFooer() }
这样,实现者必须实现 ImplementsFooer
方法才能说是继承了 Fooer
接口。因此说隐式继承有利有弊,须要开发者本身去把握。
Map/Slice 是 Go 里面最经常使用的两类数据结构,属于引用类型。在语言 runtime 层面实现,仅有的两个支持范型的结构。
Slice 是长度不固定的数组,相似于 Java 里面的 List。
// map 经过 make 进行初始化 // 若是提早知道 m 大小,建议经过 make 的第二个参数指定,避免后期的数据移动、复制 m := make(map[string]string, 10) // 赋值 m["zhangsan"] = "teacher" // 读取指定值,如不存在,返回其类型的默认值 v := m["zhangsan"] // 判断指定 key 知否在 map 内 v, ok := m["zhangsan"] // slice 经过 make 进行初始化 s := make([]int) // 增长元素 s = append(s, 1) // 也能够经过 make 第二个参数指定大小 s := make([]int, 10) for i:=0;i<10;i++ { s[i] = i } // 也可使用三个参数的 make 初始化 slice // 第二个参数为初始化大小,第三个为最大容量 // 须要经过 append 增长元素 s := make([]int, 0 ,10) s = append(s, 1)
做为一门新语言,Goroutine 是 Go 借鉴 CSP 模型提供的并发解决方案,相比传统 OS 级别的线程,它有如下特色:
for{}
可能会一直不被调度出去。通常可使用 chan/select 来进行 Goroutine 之间的调度。chan 相似于 Java 里面的 BlockingQueue,且能保证 Goroutine-safe,也就是说多个 Goroutine 并发进行读写是安全的。
chan 里面的元素默认为1个,也能够在建立时指定缓冲区大小,读写支持堵塞、非堵塞两种模式,关闭一个 chan 后,再写数据时会 panic。
// chan 与 slice/map 同样,使用 make 初始化 ch := make(chan int, 2) // blocking read v := <-ch // nonblocking read, 须要注意 default 分支不能省略,不然会堵塞住 select { case v:=<-ch: default: } // blocking write ch <- v // nonblocking write select { case ch<-v: default: }
chan 做为 Go 内一重要数据类型,看似简单,实则暗藏玄妙,用时须要多加留意,这里再也不展开叙述,后面打算专门写一篇文章去介绍,感兴趣的能够阅读下面的文章:
Go 相比 Java 来讲,语言特性真的是少太多。推荐 Learn X in Y minutes 这个网站,快速浏览一遍便可掌握 Go 的语法。Go 的简洁程度以为和 JavaScript 差很少,但倒是一门静态语言,具备强类型,这两点又让它区别于通常的脚本语言。
Go 遵循约定大于配置(convention over configuratio)的设计理念,好比在构建一个项目时,直接 go build
一个命令就搞定了,不须要什么 Makefile、pom.xml 等配置文件。下面介绍几个经常使用的约定:
{
放在行末,不然 Go 编辑器会自动插入一个逗号,致使编译错误因为以上种种约定,在看别人代码时很舒服,有种 Python 的感受。另外建议在编辑器中配置 goimports 来自动化格式代码。
Go 内没有 try catch 机制,并且已经明确拒绝了这个 Proposal,而是经过返回值的方式来处理。
f, err := os.Open(filename) if err != nil { return …, err // zero values for other results, if any }
Go 的函数通常经过返回多值的方式来传递 error(且通常是第二个位置),实际项目中通常使用 pkg/errors 去处理、包装 err。
Go 的依赖管理,相比其余语言较弱。
在 Go 1.11 正式引入的 modules 以前,项目必须放在 $GOPATH/src/xxx.com/username/project 内,这样 Go 才能去正确解析项目依赖,并且 Go 社区没有统一的包托管平台,不像 Java 中 maven 同样有中央仓库的概念,而是直接引用 Git 的库地址,因此在 Go 里,通常会使用 github.com/username/package
的方式来表示。
go get
是下载依赖但命令,但一个个去 get 库不只仅繁碎,并且没法固化依赖版本信息,因此 dep 应运而生,添加新依赖后,直接运行 dep ensure
就能够所有下下来,并且会把当前依赖的 commit id 记录到 Gopkg.lock 里面,这就能解决版本不固定的问题。
但 modules 才是正路,且在 1.13 版本会默认开启,因此这里只介绍它的用法。
# 首先导出环境变量 export GO111MODULE=on # 在一个空文件夹执行 init,建立一个名为 hello 的项目 go mod init hello # 这时会在当前文件夹内建立 go.mod ,内容为 module hello go 1.12 # 以后就能够编写 Go 文件,添加依赖后,执行 go run/ # 依赖会自动下载,并记录在 go.mod 内,版本信息记录在 go.sum
更多用法能够参考官方示例,这里只是想说明目前 Go 内的工具链大部分已经支持,可是 godoc 还不支持。
Go 也是具备垃圾回收的语言,但相比于 JVM,Go GC 可能显得及其简单,从 Go 1.10 开始,Go GC 采用 Concurrent Mark & Sweep (CMS) 算法,且不具备分代、compact 特性。读者若是对相关名词不熟悉,能够阅读:
并且 Go 里面调整 GC 的参数只有一个 GOGC
,表示下面的比率
新分配对象 / 上次 GC 后剩余对象
默认 100,表示新分配对象达到以前剩余对象大小时,进行 GC。GOGC=off
能够关闭 GC,SetGCPercent 能够动态修改这个比率。
在启动一个 Go 程序时,能够设置 GODEBUG=gctrace=1
来打印 GC 日志,日志具体含义可参考 pkg/runtime,这里再也不赘述。对调试感兴趣的能够阅读:
Go 最初由 Google 在 2007 为解决软件复杂度、提高开发效率的一试验品,到现在不过十二年,但无疑已经家喻户晓,成为云时代的首选。其面向接口的特有编程方式,也很是灵活,兼具动态语言的简洁与静态语言的高效,推荐你们尝试一下。Go Go Go!