轉眼加入螞蟻已經三個多月,這期間主要維護一 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!