转眼加入蚂蚁已经三个多月,这期间主要维护一 Go 写的服务器。虽然用的时间不算长,但还是积累了一些心得体会,这里总结归纳一下,供想尝试 Go 的同学参考。
本文会依次介绍 Go 的设计理念、开发环境、语言特性。本文在谈及语言特性的时也会讨论一些 Go 的不足之处,旨在给读者提供一个全面的视角。
简介
一般来说,编程语言都会有一个 slogan 来表示它们的特点。比如提到 Clojure,一般会想到这么几个词汇:lisp on JVM、immutable、persistent;Java 的话我能想到的是企业级开发、中规中矩。对于 Go ,官网介绍到:
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.
提取几个关键词:open(开放)、simple(简洁)、reliable(可靠)、efficient(高效)。这也可以说是它的设计目标。除了上面这些口号外,初学者还需要知道 Go 是一门命令式的静态语言(是指在编译时检查变量类型是否匹配),与 Java 属于同一类别。
Imperative | Functional | |
---|---|---|
Dynamic | Python/Ruby/java script | Lisp/Scheme/Clojure |
Static | Java/C++/Rust/Go | OCaml/Scala/Haskell |
由于 Hello World 太简洁,不具备展示 Go 的特点,所以下面展示一段访问 httpbin,打印 response 的完整代码。
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 必备功能,需要安装以下命令
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 里的配置,就拥有了一个功能完备的开发环境。
不像 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 里面有:
- pointer Go 里只支持取地址
&
与间接访问*
操作符,不支持对指针进行算术操作 - struct 类似于 C 语言里面的 struct,Java 里面的对象
- function 函数在 Go 里是一等成员
- array 大小固定的数组
- slice 动态的数组
- map 哈希表
- chan 用于在多个 goroutine 内通信
- interface 类似于 Java 里面的接口,但是与 Java 里的用法不一样
下面将重点介绍 Go 里特有或用途最广的数据类型。
struct/interface
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 [