摘要:本文從Go的語法,型別系統,編碼風格,語言工具,編碼工具和使用案例等幾方面對Go語言進行了學習和探討。
Go語言釋出之後,很多公司特別是雲廠商也開始用Go語言重構產品的基礎架構,而且很多企業都是直接採用Go語言進行開發,最近熱火朝天的Docker就是採用Go語言進行開發的。本文我們一起來探討和學習一下Go語言的技術特點。先來看個例子:
package main
import (
"fmt"
"time"
)
// 要在goroutine中執行的函數。done通道將被用來通知工作已經完成。
func worker(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")
// 通知完成。
done <- true
}
func main() {
// 建立一個通道
done := make(chan bool, 1)
go worker(done)
// 等待done變為true
<-done
}
上例中是一個在Go語言中使用goroutine和通道的例子。 其中:
go 關鍵字是用來啟動一個goroutine
done <- true, 向通道傳值
<-done, 讀取通道值
Go是由RobertGriesemer、RobPike和KenThompson在Google設計的一種靜態型別化的、須編譯後才能執行的程式語言。
Go在語法上類似於C語言,但它具有C語言沒有的優勢,如記憶體安全、垃圾回收、結構化的型別和CSP風格的並行性。
它的域名是http://golang.org,所以通常被稱為"Golang",但正確的名稱是Go。
Go的設計受C語言的影響,但更加簡單和安全。該語言套件括如下特點:
Go的語法包含C語言中保持程式碼簡潔性和可讀性的語法特點。
引入了一個聯合宣告/初始化操作符,允許程式設計師寫出i := 3或s :="Hello, world!",而不需要指定使用的變數型別。
這與C語言中的int i= 3; 和 const char *s = "Hello, world!";形成鮮明對比。
分號仍然是終止語句,但在行結束時是隱含的。
在Go中,一個函數方法可以返回多個值,返回一個結果和錯誤err組合對是向呼叫者提示錯誤的常規方式。
Go的範圍表示式允許在陣列、動態陣列、字串、字典和通道上進行簡潔的迭代,在C語言中,有三種迴圈來實現這個功能。
Go有許多內建的型別,包括數位型別(byte、int64、float32等)、booleans和字串(string)。
字串是不可更改的。
內建的運運算元和關鍵字(而不是函數)提供了串聯、比較和UTF-8編碼/解碼。
記錄型別可以用struct關鍵字定義。
對於每個型別T和每個非負整數常數n,都有一個陣列型別,表示為[n]T,因此,不同長度的陣列有不同的型別。
動態陣列可以作為"Slice"使用,如對於某型別T,表示為[]T。這些陣列有一個長度和一個容量,容量規定了何時需要分配新的記憶體來擴充套件陣列。若干個Slice可以共用它們的底層記憶體。
所有型別都可以定義指標, T型別的指標可定義為*T。地址抽取和隱式存取使用&和*操作符,這跟C語言一樣,或者隱式的通過方法呼叫或屬性存取使用。
除了標準庫中的特殊的unsafe.Pointer型別,一般指標沒有指標運算。
對於一個組合對型別K、V,型別map[K]V是將型別K鍵對映到型別V值的雜湊表的型別。
chan T是一個通道,允許在並行的Go程序之間傳送T型別的值。
除了對介面的支援外,Go的型別系統是顯示的:型別關鍵字可以用來定義一個新的命名型別,它與其他具有相同佈局的命名型別(對於結構體來說,相同的成員按相同的順序排列)不同。型別之間的一些轉換(如各種整數型別之間的轉換)是預先定義好的,新增一個新的型別可以定義額外的轉換,但命名型別之間的轉換必須始終顯式呼叫。例如,型別關鍵字可以用來定義IPv4地址的型別,基於32位元無符號整數:
type ipv4addr uint32
通過這個型別定義,ipv4addr(x)將uint32值x解釋為IP地址。如果簡單地將x分配給型別為ipv4addr的變數將會是一個型別錯誤。
常數表示式既可以是型別化的,也可以是 "非型別化的";如果它們所代表的值通過了編譯時的檢查,那麼當它們被分配給一個型別化的變數時,就會被賦予一個型別。
函數型別由func關鍵字表示;它們取0個或更多的引數並返回0個或更多的值,這些值都是要宣告型別的。
引數和返回值決定了一個函數的型別;比如,func(string, int32)(int, error)就是輸入一個字串和一個32位元有符號的整數,並返回一個有符號的整數和一個錯誤(內建介面型別)的值的函數型別。
任何命名的型別都有一個與之相關聯的方法集合。上面的IP地址例子可以用一個檢查其值是否為已知標準的方法來擴充套件:
// ZeroBroadcast報告addr是否為255.255.255.255.255。
func (addr ipv4addr) ZeroBroadcast() bool {
return addr == 0xFFFFFFFF
}
以上的函數在ipv4addr上增加了一個方法,但這個方法在uint32上沒有。
Go提供了兩個功能來取代類繼承。
首先是嵌入方法,可以看成是一種自動化的構成形式或委託代理。
第二種是介面,它提供了執行時的多型性。
介面是一型別,它在Go的型別系統中提供了一種有限的結構型別化形式。
一個介面型別的物件同時也有另一種型別的定義對應,這點就像C++物件同時具有基礎類別和派生類的特徵一樣。
Go介面是在Smalltalk程式語言的協定基礎上設計的。
在描述Go介面時使用了鴨式填充這個術語。
雖然鴨式填充這個術語沒有精確的定義,它通常是說這些物件的型別一致性沒有被靜態檢查。
由於Go介面的一致性是由Go編譯器靜態地檢查的,所以Go的作者們更喜歡使用結構型別化這個詞。
介面型別的定義按名稱和型別列出了所需的方法。任何存在與介面型別I的所需方法匹配的函數的T型別的物件也是型別I的物件。型別T的定義不需要也不能識別型別I。例如,如果Shape、Square和Circle被定義為:
import "math"
type Shape interface {
Area() float64
}
type Square struct { // 注:沒有 "實現 "宣告
side float64
}
func (sq Square) Area() float64 { return sq.side * sq.side }
type Circle struct { // 這裡也沒有 "實現 "宣告
radius float64
}
func (c Circle) Area() float64 { return math.Pi * math.Pow(c.radius, 2) }
一個正方形和一個圓都隱含著一個形狀(Shape)型別,並且可以被分配給一個形狀(Shape)型別的變數。
Go的介面系統使用了了結構型別。介面也可以嵌入其他介面,其效果是建立一個組合介面,而這個組合介面正是由實現嵌入介面的型別和新定義的介面所增加的方法來滿足的。
Go標準庫在多個地方使用介面來提供通用性,這包括基於Reader和Writer概念的輸入輸出系統。
除了通過介面呼叫方法,Go還允許通過執行時型別檢查將介面值轉換為其他型別。這就是型別斷言和型別切換。
空介面{}是一個重要的基本情況,因為它可以參照任何型別的選項。它類似於Java或C#中的Object類,可以滿足任何型別,包括像int這樣的內建型別。
使用空介面的程式碼不能簡單地在被參照的物件上呼叫方法或內建操作符,但它可以儲存interface{}值,通過型別斷言或型別切換嘗試將其轉換為更有用的型別,或者用Go的reflect包來檢查它。
因為 interface{} 可以參照任何值,所以它是一種擺脫靜態型別化限制的有效方式,就像C 語言中的 void*,但在執行時會有額外的型別檢查。
介面值是使用指向資料的指標和第二個指向執行時型別資訊的指標來實現的。與Go中其他一些使用指標實現的型別一樣,如果未初始化,介面值是零。
在Go的包系統中,每個包都有一個路徑(如"compress/bzip2 "或"golang.org/x/net/html")和一個名稱(如bzip2或html)。
對其他包的定義的參照必須始終以其他包的名稱作為字首,並且只有其他包的大寫的名稱才能被存取:io.Reader是公開的,但bzip2.reader不是。
go get命令可以檢索儲存在遠端資源庫中的包,鼓勵開發者在開發包時,在與源資源庫相對應的基礎路徑
(如http://example.com/user_name/package_name)內開發程式包,從而減少將來在標準庫或其他外部庫中名稱碰撞的可能性。
有人提議Go引入一個合適的包管理解決方案,類似於CPANfor Perl或Rust的Cargo系統或Node的npm系統。
在電腦科學中,通訊順序過程(communicating sequential processes,CSP)是一種描述並行系統中互動模式的正式語言,它是並行數學理論家族中的一個成員,被稱為過程演演算法(process algebras),或者說過程計算(process calculate),是基於訊息的通道傳遞的數學理論。
CSP在設計Oceam程式語言時起了很大的影響,同時也影響了Limbo、RaftLib、Go、Crystal和Clojure的core.async等程式語言的設計。
CSP最早是由TonyHoare在1978年的一篇論文中描述的,後來有了很大的發展。
CSP作為一種工具被實際應用於工業上,用於指定和驗證各種不同系統的並行功能,如T9000Transputer以及安全的電子商務系統。
CSP本身的理論目前也仍然是被積極研究的物件,包括增加其實際適用範圍的工作,如增加可分析的系統規模。
Go語言有內建的機制和庫支援來編寫並行程式。並行不僅指的是CPU的並行性,還指的是非同步性處理:讓相對慢的操作,如資料庫或網路讀取等操作在做其他工作的同時執行,這在基於事件的伺服器中很常見。
主要的並行構造是goroutine,這是一種輕量級處理型別。一個以go關鍵字為字首的函數呼叫會在一個新的goroutine中啟動這個函數。
語言規範並沒有指定如何實現goroutine,但目前的實現將Go程序的goroutine複用到一個較小的作業系統執行緒集上,類似於Erlang中的排程。
雖然一個標準的庫包具有大多數經典的並行控制結構(mutex鎖等),但Go並行程式更偏重於通道,它提供了goroutines之間的訊息傳功能。
可選的緩衝區以FIFO順序儲存訊息,允許傳送的goroutines在收到訊息之前繼續進行。
通道是型別化的,所以chan T型別的通道只能用於傳輸T型別的訊息。
特殊語法約定用於對它們進行操作;<-ch是一個表示式,它使執行中的goroutine在通道ch上阻塞,直到有一個值進來,而ch<- x則是傳送值x(可能阻塞直到另一個goroutine接收到這個值)。
內建的類似於開關的選擇語句可以用來實現多通道上的非阻塞通訊。Go有一個記憶體模型,描述了goroutine必須如何使用通道或其他操作來安全地共用資料。
通道的存在使Go有別於像Erlang這樣的actor模型式的並行語言,在這種語言中,訊息是直接面向actor(對應於goroutine)的。在Go中,可以通過在goroutine和通道之間保持一對一的對應關係來,Go語言也允許多個goroutine共用一個通道,或者一個goroutine在多個通道上傳送和接收訊息。
通過這些功能,人們可以構建像workerpools、流水線(比如說,在下載檔案時,對檔案進行解壓縮和解析)、帶超時的後臺呼叫、對一組服務的"扇出"並行呼叫等並行構造。
通道也有一些超越程序間通訊的常規概念的用途,比如作為一個並行安全的回收緩衝區列表,實現coroutines和實現迭代器。
Go的並行相關的結構約定(通道和替代通道輸入)來自於TonyHoare的交談循序程式模型。
不像以前的並行程式語言,如Occam或Limbo(Go的共同設計者RobPike曾在此基礎上工作過的語言),Go沒有提供任何內建的安全或可驗證的並行概念。
雖然在Go中,上述的通訊處理模型是推薦使用的,但不是唯一的:一個程式中的所有goroutines共用一個單一的地址空間。這意味著可突變物件和指標可以在goroutines之間共用。
有一項研究比較了一個不熟悉Go語言的老練程式設計師編寫的程式的大小(以程式碼行數為單位)和速度,以及一個Go專家(來自Google開發團隊)對這些程式的修正,對Chapel、Cilk和IntelTBB做了同樣的研究。
研究發現,非專家傾向於用每個遞迴中的一條Go語句來寫分解-解決演演算法,而專家則用每個處理器的一條Go語句來寫分散式工作同步程式。Go專家的程式通常更快,但也更長。
Goroutine對於如何存取共用資料沒有限制,這使得條件競賽成為可能的問題。
具體來說,除非程式通過通道或其他方式顯式同步,否則多個goroutine共用讀寫一個記憶體區域可能會發生問題。
此外,Go的內部資料結構,如介面值、動態陣列頭、雜湊表和字串頭等內部資料結構也不能倖免於條件競賽,因此在多執行緒程式中,如果修改這些型別的共用範例沒有同步,就會存在影響型別和記憶體安全的情況。
gc工具鏈中的連結器預設會建立靜態連結的二進位制檔案,因此所有的Go二進位制檔案都包括Go執行所需要的內容。
Go故意省略了其他語言中常見的一些功能,包括繼承、通用程式設計、斷言、指標運算、隱式型別轉換、無標記的聯合和標記聯合。
Go作者在Go程式的風格方面付出了大量的努力:
主要的Go發行版包括構建、測試和分析程式碼的工具。
go build,它只使用原始檔中的資訊來構建Go二進位制檔案,不使用單獨的makefiles。
gotest,用於單元測試和微基準
go fmt,用於格式化程式碼
go get,用於檢索和安裝遠端包。
go vet,靜態分析器,查詢程式碼中的潛在錯誤。
go run,構建和執行程式碼的快捷方式
godoc,用於顯示檔案或通過HTTP
gorename,用於以型別安全的方式重新命名變數、函數等。
go generate,一個標準的呼叫程式碼生成器的方法。
它還包括分析和偵錯支援、執行時診斷(例如,跟蹤垃圾收集暫停)和條件競賽測試器。
第三方工具的生態系統增強了標準的釋出系統,如:
gocode,它可以在許多文字編輯器中自動完成程式碼,
goimports(由Go團隊成員提供),它可以根據需要自動新增/刪除包匯入,以及errcheck,它可以檢測可能無意中被忽略的錯誤程式碼。
流行的Go程式碼工具:
GoLand:JetBrains公司的IDE。
VisualStudio Code
LiteIDE:一個"簡單、開源、跨平臺的GoIDE"
Vim:使用者可以安裝外掛:
vim-go
用Go編寫的一些著名的開源應用包括:
Caddy,一個開源的HTTP/2web伺服器,具有自動HTTPS功能。
CockroachDB,一個開源的、可生存的、強一致性、可延伸的SQL資料庫。
Docker,一套用於部署Linux容器的工具。
Ethereum,以太幣虛擬機器器區塊鏈的Go-Ethereum實現。
Hugo,一個靜態網站生成器
InfluxDB,一個專門用於處理高可用性和高效能要求的時間序列資料的開源資料庫。
InterPlanetaryFile System,一個可內容定址、對等的超媒體協定。
Juju,由UbuntuLinux的包裝商Canonical公司推出的服務協調工具。
Kubernetes容器管理系統
lnd,位元幣閃電網路的實現。
Mattermost,一個團隊聊天系統
NATSMessaging,是一個開源的訊息傳遞系統,其核心設計原則是效能、可延伸性和易用性。
OpenShift,雲端計算服務平臺
Snappy,一個由Canonical開發的UbuntuTouch軟體包管理器。
Syncthing,一個開源的檔案同步使用者端/伺服器應用程式。
Terraform,是HashiCorp公司的一款開源的多雲基礎設施設定工具。
其他使用Go的知名公司和網站包括:
Cacoo,使用Go和gRPC渲染使用者儀表板頁面和微服務。
Chango,程式化廣告公司,在其實時競價系統中使用Go。
CloudFoundry,平臺即服務系統
Cloudflare,三角編碼代理Railgun,分散式DNS服務,以及密碼學、紀錄檔、流處理和存取SPDY網站的工具。
容器Linux(原CoreOS),是一個基於Linux的作業系統,使用Docker容器和rkt容器。
Couchbase、Couchbase伺服器內的查詢和索引服務。
Dropbox,將部分關鍵元件從Python遷移到了Go。
谷歌,許多專案,特別是下載伺服器dl.google.com。
Heroku,Doozer,一個提供鎖具服務的公司
HyperledgerFabric,一個開源的企業級分散式分類賬專案。
MongoDB,管理MongoDB範例的工具。
Netflix的伺服器架構的兩個部分。
Nutanix,用於其企業雲作業系統中的各種微服務。
Plug.dj,一個互動式線上社交音樂串流媒體網站。
SendGrid是一家位於科羅拉多州博爾德市的事務性電子郵件傳送和管理服務。
SoundCloud,"幾十個系統"
Splice,其線上音樂共同作業平臺的整個後端(API和解析器)。
ThoughtWorks,持續傳遞和即時資訊的工具和應用(CoyIM)。
Twitch,他們基於IRC的聊天系統(從Python移植過來的)。
Uber,處理大量基於地理資訊的查詢。
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
package main
import (
"fmt"
"time"
)
func readword(ch chan string) {
fmt.Println("Type a word, then hit Enter.")
var word string
fmt.Scanf("%s", &word)
ch <- word
}
func timeout(t chan bool) {
time.Sleep(5 * time.Second)
t <- false
}
func main() {
t := make(chan bool)
go timeout(t)
ch := make(chan string)
go readword(ch)
select {
case word := <-ch:
fmt.Println("Received", word)
case <-t:
fmt.Println("Timeout.")
}
}
沒有測試的程式碼是不完整的,因此我們需要看看程式碼測試部分的編寫。
程式碼:
func ExtractUsername(email string) string {
at := strings.Index(email, "@")
return email[:at]
}
測試案例:
func TestExtractUsername(t *testing.T) {
type args struct {
email string
}
tests := []struct {
name string
args args
want string
}{
{"withoutDot", args{email: "r@google.com"}, "r"},
{"withDot", args{email: "jonh.smith@example.com"}, "jonh.smith"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ExtractUsername(tt.args.email); got != tt.want {
t.Errorf("ExtractUsername() = %v, want %v", got, tt.want)
}
})
}
}
接下來我寫一個例子建立REST API後端服務:
我們的服務提供如下的API:
###
GET http://localhost:10000/
###
GET http://localhost:10000/all
###
GET http://localhost:10000/article/1
###
POST http://localhost:10000/article HTTP/1.1
{
"Id": "3",
"Title": "Hello 2",
"desc": "Article Description",
"content": "Article Content"
}
###
PUT http://localhost:10000/article HTTP/1.1
{
"Id": "2",
"Title": "Hello 2 Update",
"desc": "Article Description Update",
"content": "Article Content Update"
}
完整程式碼:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/mux"
)
type Article struct {
Id string `json:"Id"`
Title string `json:"Title"`
Desc string `json:"desc"`
Content string `json:"content"`
}
var MapArticles map[string]Article
var Articles []Article
func returnAllArticles(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: returnAllArticles")
json.NewEncoder(w).Encode(Articles)
}
func homePage(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the HomePage!")
fmt.Println("Endpoint Hit: homePage")
}
func createNewArticle(w http.ResponseWriter, r *http.Request) {
reqBody, _ := ioutil.ReadAll(r.Body)
var article Article
json.Unmarshal(reqBody, &article)
Articles = append(Articles, article)
MapArticles[article.Id] = article
json.NewEncoder(w).Encode(article)
}
func updateArticle(w http.ResponseWriter, r *http.Request) {
reqBody, _ := ioutil.ReadAll(r.Body)
var article Article
json.Unmarshal(reqBody, &article)
found := false
for index, v := range Articles {
if v.Id == article.Id {
// Found!
found = true
Articles[index] = article
}
}
if !found {
Articles = append(Articles, article)
}
MapArticles[article.Id] = article
json.NewEncoder(w).Encode(article)
}
func returnSingleArticle(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
key := vars["id"]
fmt.Fprintf(w, "Key: %s \n", key)
json.NewEncoder(w).Encode(MapArticles[key])
}
func handleRequests() {
myRouter := mux.NewRouter().StrictSlash(true)
myRouter.HandleFunc("/", homePage)
myRouter.HandleFunc("/all", returnAllArticles)
myRouter.HandleFunc("/article", createNewArticle).Methods("POST")
myRouter.HandleFunc("/article", updateArticle).Methods("PUT")
myRouter.HandleFunc("/article/{id}", returnSingleArticle)
log.Fatal(http.ListenAndServe(":10000", myRouter))
}
func main() {
fmt.Println("Rest API is ready ...")
MapArticles = make(map[string]Article)
Articles = []Article{
Article{Id: "1", Title: "Hello", Desc: "Article Description", Content: "Article Content"},
Article{Id: "2", Title: "Hello 2", Desc: "Article Description", Content: "Article Content"},
}
for _, a := range Articles {
MapArticles[a.Id] = a
}
handleRequests()
}
呼叫新增,更新API以後返回所有資料的測試結果:
MicheleSimionato對Go大加讚揚:
介面系統簡潔,並刻意省略了繼承。
EngineYard的DaveAstels寫道:
Go是非常容易上手的。很少的基本語言概念,語法也很乾淨,設計得很清晰。Go目前還是實驗性的,還有些地方比較粗糙。
2009年,Go被TIOBE程式設計社群指數評選為年度最佳程式語言。
到2010年1月,Go的排名達到了第13位,超過了Pascal等成熟的語言。
但到了2015年6月,它的排名跌至第50位以下,低於COBOL和Fortran。
但截至2017年1月,它的排名又飆升至第13位,顯示出它的普及率和採用率有了顯著的增長。
Go被評為2016年TIOBE年度最佳程式語言。
BruceEckel曾表示:
C++的複雜性(在新的C++中甚至增加了更多的複雜性),以及由此帶來的對生產力的影響,已經沒有任何理由繼續使用C++了。C++程式設計師為了克服C語言的一些問題而做出的增強初衷目前已經沒有了意義,而Go此時顯得更有意義。
2011年一位Google工程師R.Hundt對Go語言及其GC實現與C++(GCC)、Java和Scala的對比評估發現。
Go提供了有趣的語言特性,這也使得Go語言有了簡潔、標準化的特徵。這種語言的編譯器還不成熟,這在效能和二進位制大小上都有體現。
這一評價收到了Go開發團隊的快速反應。
IanLance Taylor因為Hundt的評論改進了Go程式碼;
RussCox隨後對Go程式碼以及C++程式碼進行了優化,並讓Go程式碼的執行速度比C++略快,比評論中使用的程式碼效能快了一個數量級以上。
2009年11月10日,也就是Go!程式語言全面釋出的當天,Go!程式語言的開發者FrancisMcCabe(注意是感嘆號)要求更改Google的語言名稱,以避免與他花了10年時間開發的語言混淆。
McCabe表示了對谷歌這個'大塊頭'最終會碾壓他"的擔憂,這種擔憂引起了120多名開發者的共鳴,他們在Google官方的問題執行緒上評論說他們應該改名,有些人甚至說這個問題違背了Google的座右銘:"不要作惡。"
2010年10月12日,谷歌開發者RussCox關閉了這個問題,自定義狀態為"不幸",並附上了以下評論:
"有很多計算產品和服務都被命名為Go。在我們釋出以來的11個月裡,這兩種語言的混淆度極低。"
Go的批評家們的觀點:
本文從Go的語法,型別系統,編碼風格,語言工具,編碼工具和使用案例等幾方面對Go語言進行了學習和探討,希望可以拋磚引玉,對Go語言感興趣的同仁有所裨益。