Go 1.11中值得關注的幾個變化
轉眼間又近年底,距8月25日 ofollow,noindex" target="_blank">Go 1.11版本 正式釋出已過去快三個月了。由於種種原因,Go語言釋出變化系列的Go 1.11版本沒能及時放出。近期網課釋出上線後,個人時間壓力稍緩和。又恰看到近期 Go 1.12 release note的initial version已經加入到master ,於是這篇文章便上升到個人Todo list的Top3的位置,我也盡一切可能的碎片時間收集素材,撰寫文章內容。這個時候談Go 1.11,總有炒“冷飯”的嫌疑,雖然這碗飯還有一定溫度^_^。
一. Go 1.11版本的重要意義
在Go 1.11版本之前的 Go user官方調查 中,Gopher抱怨最多的三大問題如下:
- 包依賴管理
- 缺少泛型
- 錯誤處理
而Go 1.11開啟了問題1: 包依賴管理 解決的實驗。這表明了社群的聲音在影響Go語言演化的過程中扮演著日益重要的角色了。
同時, Go 1.11 是 Russ Cox 在 GopherCon 2017大會上發表 “Toward Go2″ 之後的第一個Go版本,是為後續 “Go2” 的漸進落地奠定基礎的一個版本。
二. Go 1.11版本變化概述
在”Go2″聲音日漸響亮的今天,相容性(compatibility)也依舊是Go team考慮的Go語言演化的第一原則,這一點通過Rob Pike在9月份的 Go Sydney Meetup 上的有關 Go 2 Draft Specifications 的 Talk 可以證明(油管視訊)。
相容性依然是”Go2″的第一考慮
Go 1.11也一如既往版本那樣,繼續遵守著 Go1相容協議 ,這意味使用從Go1.0到Go1.10編寫的程式碼理論上依舊可以通過Go 1.11版本編譯並正常執行。
隨著Go 1.11版本的釋出,一些老版本的作業系統將不再被支援,比如Windows XP、macOS 10.9.x等。不被支援不意味著完全不能用,只是Go 1.11在這些老舊os上執行時出現問題將不被官方support了。同時根據Go的 release support規定 ,Go 1.11釋出也同時意味著Go 1.9版本將和之前的older go release版本一樣,官方將不再提供支援了(關鍵bug fix、security problem fix等)。
Go 1.11中為近兩年逐漸興起的 RISC-V" rel="nofollow,noindex" target="_blank">RISC-V cpu架構預留了GOARCH值:riscv和riscv64。
Go 1.11中為偵錯程式增加了一個新的實驗功能,那就是允許在除錯過程中動態呼叫Go函式,比如在斷點處呼叫String方法等。 Delve 1.1.0及以上版本可以使用該功能。
在執行時方面,Go 1.11使用了一個稀疏heap佈局,這樣就去掉了以往Go heap最大512G的限制。
通過Go 1.11編譯的Go程式一般來說效能都會有小幅的提升。對於使用math/big包的程式或arm64架構上的Go程式而言,這次的提升尤為明顯。
Go 1.11中最大的變化莫過於兩點:
- module機制的實驗性引入,以試圖解決長久以來困擾Gopher們的包依賴問題;
- 增加對 WebAssembly 的支援,這樣以後Gopher們可以通過Go語言編寫前端應用了。
Go 1.11的change很多,這是core team和社群共同努力的結果。但在我這個系列文章中,我們只能詳細關注少數重要的變化。下面我們就來稍微詳細地說說go module和go support WebAssembly這兩個顯著的變化。
三. go module
在Go 1.11 beta2版本釋出之前,我曾經基於當時的Go tip版本撰寫了一篇《初窺go module》的文章,重點描述了go module的實現機制,包括 Semantic Import Versioning 、 Minimal Version Selection 等,因此對go module(前身為 vgo )是什麼以及實現機制感興趣的小夥伴兒們可以先移步到那篇文章瞭解。在這裡我將通過為一個 已存在的repo 新增go.mod的方式來描述go module。
這裡我們使用的是 go 1.11.2版本 ,repo為 gocmpp 。注意:我們沒有顯式設定GO111MODULE的值,這樣只有在GOPATH之外的路徑下,且當前路徑下有go.mod或子路徑下有go.mod檔案時,go compiler才進入module-aware模式(相比較於gopath模式)。
1. 初始化go.mod
我們先把gocmpp clone到gopath之外的一個路徑下:
# git clone https://github.com/bigwhite/gocmpp.git Cloning into 'gocmpp'... remote: Enumerating objects: 1, done. remote: Counting objects: 100% (1/1), done. remote: Total 950 (delta 0), reused 0 (delta 0), pack-reused 949 Receiving objects: 100% (950/950), 3.85 MiB | 0 bytes/s, done. Resolving deltas: 100% (396/396), done. Checking connectivity... done.
在應用go module之前,我們先來在傳統的gopath模式下build一次:
# go build connect.go:24:2: cannot find package "github.com/bigwhite/gocmpp/utils" in any of: /root/.bin/go1.11.2/src/github.com/bigwhite/gocmpp/utils (from $GOROOT) /root/go/src/github.com/bigwhite/gocmpp/utils (from $GOPATH)
正如我們所料,由於處於GOPATH外面,且GO111MODULE並未顯式設定,Go compiler會嘗試在當前目錄或子目錄下查詢go.mod,如果沒有go.mod檔案,則會採用傳統gopath模式編譯,即在$GOPATH/src下面找相關的import package,因此失敗。
下面我們通過建立go.mod,將編譯mode切換為module-aware mode。
我們通過go mod init命令來為gocmpp建立go.mod檔案:
# go mod init github.com/bigwhite/gocmpp go: creating new go.mod: module github.com/bigwhite/gocmpp # cat go.mod module github.com/bigwhite/gocmpp
我們看到,go mod init命令在當前目錄下建立一個go.mod檔案,內有一行內容,描述了該module為 github.com/bigwhite/gocmpp。
我們再來構建一下gocmpp:
# go build go: finding golang.org/x/text/transform latest go: finding golang.org/x/text/encoding/unicode latest go: finding golang.org/x/text/encoding/simplifiedchinese latest go: finding golang.org/x/text v0.3.0 go: finding golang.org/x/text/encoding latest go: downloading golang.org/x/text v0.3.0
由於當前目錄下有了go.mod檔案,go compiler將工作在module-aware模式下,自動分析gocmpp的依賴、確定gocmpp依賴包的初始版本,並下載這些版本的依賴包快取到特定目錄下(目前是存放在$GOPATH/pkg/mod下面)
# cat go.mod module github.com/bigwhite/gocmpp require golang.org/x/text v0.3.0
我們看到go.mod中多了一行資訊: “require golang.org/x/text v0.3.0″ 。這就是gocmpp這個module所依賴的第三方包以及經過go compiler初始分析確定使用的版本(v0.3.0)。
2. 用於verify的go.sum
go build後,當前目錄下還多出了一個go.sum檔案。
# cat go.sum golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
go.sum記錄每個依賴庫的版本和對應的內容的校驗和(一個雜湊值)。每當增加一個依賴項時,如果go.sum中沒有,則會將該依賴項的版本和內容校驗和新增到go.sum中。go命令會使用這些校驗和與快取在本地的依賴包副本元資訊(比如:$GOPATH/pkg/mod/cache/download/golang.org/x/text/@v下面的v0.3.0.ziphash)進行比對校驗。
如果我修改了$GOPATH/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.ziphash中的值,那麼當我執行下面verify命令時會報錯:
# go mod verify golang.org/x/text v0.3.0: zip has been modified (/root/go/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.zip) golang.org/x/text v0.3.0: dir has been modified (/root/go/pkg/mod/golang.org/x/[email protected])
如果沒有“惡意”修改,則verify會報成功:
# go mod verify all modules verified
3. 用why解釋為何依賴,給出依賴路徑
go.mod中的依賴項由go相關命令自動生成和維護。但是如果開發人員想知道為什麼會依賴某個package,可以通過go mod why命令來查詢原因。go mod why命令預設會給出一個main包到要查詢的packge的最短依賴路徑。如果go mod why使用 -m flag,則後面的引數將被看成是module,並給出main包到每個module中每個package的最短依賴路徑(如果依賴的話):
下面我們通過go mod why命令檢視一下gocmpp module到 golang.org/x/oauth2和golang.org/x/exp兩個包是否有依賴:
# go mod why golang.org/x/oauth2 golang.org/x/exp go: finding golang.org/x/oauth2 latest go: finding golang.org/x/exp latest go: downloading golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 go: downloading golang.org/x/exp v0.0.0-20181112044915-a3060d491354 go: finding golang.org/x/net/context/ctxhttp latest go: finding golang.org/x/net/context latest go: finding golang.org/x/net latest go: downloading golang.org/x/net v0.0.0-20181114220301-adae6a3d119a # golang.org/x/oauth2 (main module does not need package golang.org/x/oauth2) # golang.org/x/exp (main module does not need package golang.org/x/exp)
通過結尾幾行的輸出日誌,我們看到gocmpp的main package沒有對golang.org/x/oauth2和golang.org/x/exp兩個包產生任何依賴。
我們加上-m flag再來執行一遍:
# go mod why -m golang.org/x/oauth2 golang.org/x/exp # golang.org/x/oauth2 (main module does not need module golang.org/x/oauth2) # golang.org/x/exp (main module does not need module golang.org/x/exp)
同樣是沒有依賴的輸出結果,但是輸出日誌中使用的是module,而不是package字樣。說明go mod why將golang.org/x/oauth2和golang.org/x/exp視為module了。
我們再來查詢一下對golang.org/x/text的依賴:
# go mod why golang.org/x/text # golang.org/x/text (main module does not need package golang.org/x/text) # go mod why -m golang.org/x/text # golang.org/x/text github.com/bigwhite/gocmpp/utils golang.org/x/text/encoding/simplifiedchinese
我們看到,如果-m flag不開啟,那麼gocmpp main package沒有對golang.org/x/text的依賴路徑;如果-m flag開啟,則golang.org/x/text被視為module,go mod why會檢查gocmpp main package到module: golang.org/x/text下面所有package是否有依賴路徑。這裡我們看到gocmpp main package依賴了golang.org/x/text module下面的golang.org/x/text/encoding/simplifiedchinese這個package,並給出了最短依賴路徑。
4. 清理go.mod和go.sum中的條目:go mod tidy
經過上述操作後,我們再來看看go.mod中的內容:
# cat go.mod module github.com/bigwhite/gocmpp require ( github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 // indirect golang.org/x/text v0.3.0 )
我們發現go.mod中require block增加了許多條目,顯然我們的gocmpp並沒有依賴到golang.org/x/oauth2和golang.org/x/net中的任何package。我們要清理一下go.mod,使其與gocmpp原始碼中的第三方依賴的真實情況保持一致,我們使用go mod tidy命令:
# go mod tidy # cat go.mod module github.com/bigwhite/gocmpp require ( github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 golang.org/x/text v0.3.0 ) # cat go.sum github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ= github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
我們看到:執行完tidy命令後,go.mod和go.sum都變得簡潔了,裡面的每一個條目都是gocmpp所真實依賴的package/module的資訊。
5. 對依賴包的版本進行“升降級”(upgrade或downgrade)
如果對go mod init初始選擇的依賴包版本不甚滿意,或是第三方依賴包有更新的版本釋出,我們日常開發工作中都會進行對對依賴包的版本進行“升降級”(upgrade或downgrade)的操作。在go module模式下,如何來做呢?由於go.mod和go.sum是由go compiler管理的,這裡不建議手工去修改go.mod中require中module的版本號。我們可以通過 module-aware的go get命令 來實現我們的目的。
我們先來檢視一下golang.org/x/text都有哪些版本可用:
# go list -m -versions golang.org/x/text golang.org/x/text v0.1.0 v0.2.0 v0.3.0
我們選擇將golang.org/x/text從v0.3.0降級到v0.1.0:
# go get golang.org/x/[email protected] go: finding golang.org/x/text v0.1.0 go: downloading golang.org/x/text v0.1.0
降級後,我們test一下:
# go test PASS okgithub.com/bigwhite/gocmpp0.003s
我們這時再看看go.mod和go.sum:
# cat go.mod module github.com/bigwhite/gocmpp require ( github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 golang.org/x/text v0.1.0 ) # cat go.sum github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ= github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= golang.org/x/text v0.1.0 h1:LEnmSFmpuy9xPmlp2JeGQQOYbPv3TkQbuGJU3A0HegU= golang.org/x/text v0.1.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
go.mod中依賴的golang.org/x/text已經從v0.3.0自動變成了v0.1.0了。go.sum中也增加了golang.org/x/text v0.1.0的條目,不過v0.3.0的條目依舊存在。我們可以通過go mod tidy清理一下:
# go mod tidy # cat go.sum github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ= github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= golang.org/x/text v0.1.0 h1:LEnmSFmpuy9xPmlp2JeGQQOYbPv3TkQbuGJU3A0HegU= golang.org/x/text v0.1.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
go 1.11中的go get也是支援兩套工作模式的: 一套是傳統gopath mode的;一套是module-aware的。
如果我們在gopath之外的路徑,且該路徑下沒有go.mod,那麼go get還是迴歸gopath mode:
# go get golang.org/x/[email protected] go: cannot use path@version syntax in GOPATH mode
而module-aware的go get在前面已經演示過了,這裡就不重複演示了。
在module-aware模式下,go get -u會更新依賴,升級到依賴的最新minor或patch release。比如:我們在gocmpp module root path下執行:
# go get -u golang.org/x/text # cat go.mod module github.com/bigwhite/gocmpp require ( github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 golang.org/x/text v0.3.0 //恢復到0.3.0 )
我們看到剛剛降級回v0.1.0的依賴項又自動變回v0.3.0了(注意僅minor號變更)。
如果僅僅要升級patch號,而不升級minor號,可以使用go get -u=patch A 。比如:如果golang.org/x/text有v0.1.1版本,那麼go get -u=patch golang.org/x/text會將go.mod中的text後面的版本號變為v0.1.1,而不是v0.3.0。
如果go get後面不接具體package,則go get僅針對於main package。
處於module-aware工作模式下的go get更新某個依賴(無論是升版本還是降版本)時,會自動計算並更新其間接依賴的包的版本。
6. 相容go 1.11之前版本的reproduceable build: 使用vendor
處於module-aware mode下的go compiler是完全不理會vendor目錄的存在的,go compiler只會使用$GOPATH/pkg/mod下(當前go mod快取的包是放在這個位置,也許將來會更換位置)快取的第三方包的特定版本進行編譯構建。那麼這樣一來,對於採用go 1.11之前版本的go compiler來說,reproduceable build就失效了。
為此,go mod提供了vendor子命令,可以根據依賴在module頂層目錄自動生成vendor目錄:
# go mod vendor -v # github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 github.com/dvyukov/go-fuzz/gen # golang.org/x/text v0.3.0 golang.org/x/text/encoding/simplifiedchinese golang.org/x/text/encoding/unicode golang.org/x/text/transform golang.org/x/text/encoding golang.org/x/text/encoding/internal golang.org/x/text/encoding/internal/identifier golang.org/x/text/internal/utf8internal golang.org/x/text/runes
gopher可以將vendor目錄提交到git repo,這樣老版本的go compiler就可以使用vendor進行reproduceable build了。
當然在module-aware mode下,go 1.11 compiler也可以使用vendor進行構建,使用下面命令即可:
go build -mod=vendor
注意在上述命令中,只有位於module頂層路徑的vendor才會起作用。
7. 國內gopher如何適應go module
對於國內gopher來說,下載go get package的經歷並不是總是那麼愉快!尤其是get golang.org/x/xxx路徑下的package的時候。以golang.org/x/text為例,在傳統的gopath mode下,我們還可以通過下載github.com/golang/text,然後在本地將路徑改為golang.org/x/text的方式來獲取text相關包。但是在module-aware mode下,對package的下載和本地快取管理完全由go tool自動完成,國內的gopher們該如何應對呢?
兩種方法:
1. 用go.mod中的replace語法,將golang.org/x/text指向本地另外一個目錄下已經下載好的github.com/golang/text
2. 使用GOPROXY
方法1顯然具有臨時性,本地改改第三方依賴庫程式碼,用於除錯還可以;第二種方法顯然是正解,我們通過一個proxy來下載那些在qiang外的package。Microsoft工程師開源的 athens專案 正是一個用於這個用途的go proxy工具。不過限於篇幅,這裡就不展開說明了。我將在後續文章詳細談談 go proxy的,尤其是使用athens實現go proxy的詳細方案。
四. 對WebAssembly的支援
1. 簡介
由於長期在 後端 浸淫,對javascript、 WebAssembly 等前端的技能瞭解不多,因此這裡對Go支援WebAssembly也就能介紹個梗概。下圖是對Go支援WebAssembly的一個粗淺的理解:
我們看到滿足WebAssembly標準要求的wasm運行於browser之上,類比於一個amd64架構的binary program運行於linux作業系統之上。我們在x86-64的linux上執行go build,實質執行的是:
GOOS=linux GOARCH=amd64 go build ...
因此為了將Go原始碼編譯為wasm,我們需要執行:
GOOS=js GOARCH=wasm go build ...
同時, _js.go和 *_wasm.go這樣的檔案也和 _linux.go、*_amd64.go一樣,會被go compiler做特殊處理。
2. 一個hello world級別的WebAssembly的例子
例子來自 Go官方Wiki ,程式碼結構如下:
/Users/tony/test/Go/wasm/hellowasm git:(master) ✗ $tree . ├── hellowasm.go ├── index.html └── server.go
hellowasm.go是最終wasm應用對應的原始碼:
// hellowasm.go package main import "fmt" func main() { fmt.Println("Hello, WebAssembly!") }
我們先將其編譯為wasm檔案main.wasm:
$GOOS=js GOARCH=wasm go build -o main.wasm hellowasm.go $ls -F hellowasm.goindex.htmlmain.wasm*server.go
接下來我們從Goroot下面copy一個javascript支援檔案wasm_exec.js:
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
我們建立index.html,並在該檔案中使用wasm_exec.js,並載入main.wasm:
//index.html <html> <head> <meta charset="utf-8"> <script src="wasm_exec.js"></script> <script> const go = new Go(); WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => { go.run(result.instance); }); </script> </head> <body></body> </html>
最後,我們建立server.go,這是一個File server:
//server.go package main import ( "flag" "log" "net/http" ) var ( listen = flag.String("listen", ":8080", "listen address") dir= flag.String("dir", ".", "directory to serve") ) func main() { flag.Parse() log.Printf("listening on %q...", *listen) err := http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir))) log.Fatalln(err) }
啟動該server:
$go run server.go 2018/11/19 21:19:17 listening on ":8080"...
開啟Chrome瀏覽器,右鍵開啟Chrome的“檢查”頁面,訪問127.0.0.1:8080,我們將在console(控制檯)視窗看到下面內容:
我們看到”Hello, WebAssembly”字樣輸出到console上了!
3. 使用node.js執行wasm應用
wasm應用除了可以運行於支援WebAssembly的瀏覽器上之外,還可以通過node.js執行它。
我的實驗環境中安裝的node版本是:
$node -v v9.11.1
我們刪除server.go,然後執行下面命令:
$GOOS=js GOARCH=wasm go run -exec="$(go env GOROOT)/misc/wasm/go_js_wasm_exec" . Hello, WebAssembly!
我們看到通過go_js_wasm_exec命令我們成功通過node執行了main.wasm。
不過每次通過go run -exec來執行,命令列太長,不易記住和使用。我們將go_js_wasm_exec放到$PATH下面,然後直接執行go run:
$export PATH=$PATH:"$(go env GOROOT)/misc/wasm" $which go_js_wasm_exec /Users/tony/.bin/go1.11.2/misc/wasm/go_js_wasm_exec $GOOS=js GOARCH=wasm go run . Hello, WebAssembly!
main.wasm同樣被node執行,並且這樣執行main.wasm程式的命令列長度大大縮短了!
五. 小結
從Go 1.11版本開始,Go語言開始駛入“語言演化”的深水區。Go語言究竟該如何演化?如何在保持語言相容性、社群不分裂的前提下,滿足社群對於錯誤處理、泛型等語法特性的需求,是擺在Go設計者面前的一道難題。但我相信,無論Go如何演化,Go設計者都會始終遵循Go語言安身立命的那幾個根本原則,也是大多數Gopher喜歡Go的根本原因:相容、簡單、可讀和高效。
我的網課“ Kubernetes實戰:高可用叢集搭建、配置、運維與應用 ”在慕課網上線了,感謝小夥伴們學習支援!
我愛發簡訊 :企業級簡訊平臺定製開發專家 https://51smspush.com/
smspush : 可部署在企業內部的定製化簡訊平臺,三網覆蓋,不懼大併發接入,可定製擴充套件; 簡訊內容你來定,不再受約束, 介面豐富,支援長簡訊,簽名可選。
著名雲主機服務廠商DigitalOcean釋出最新的主機計劃,入門級Droplet配置升級為:1 core CPU、1G記憶體、25G高速SSD,價格5$/月。有使用DigitalOcean需求的朋友,可以開啟這個 連結地址 :https://m.do.co/c/bff6eed92687 開啟你的DO主機之路。
我的聯絡方式:
微博:https://weibo.com/bigwhite20xx
微信公眾號:iamtonybai
部落格:tonybai.com
github: https://github.com/bigwhite
微信讚賞:

商務合作方式:撰稿、出書、培訓、線上課程、合夥創業、諮詢、廣告合作。
© 2018,bigwhite. 版權所有.