轉眼間又近年底,距8月25日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-Vcpu架構預留了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中的值,那麼當我執行下面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
ok github.com/bigwhite/gocmpp 0.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.go index.html main.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的根本原因:兼容、簡單、可讀和高效。

講師主頁:tonybai_cn

實戰課:《Kubernetes實戰:高可用集群搭建,配置,運維與應用》免費課:《Kubernetes基礎:開啟雲原生之門》

作者:tonybai_cn鏈接:imooc.com/article/detai來源:慕課網

推薦閱讀:

慕課網:如何自學 Android 編程?

接手別人的代碼,死的心有嗎?

30行Javascript代碼實現圖片懶載入

Github上發布一天Star數破4K的項目了解一下

你試過不用if擼代碼嗎?

慕課網:如何快速打好java基礎?

慕課網:有哪些視頻堪稱有毒?

CopyOnWriteArrayList你都不知道,怎麼拿offer?

IntelliJ IDEA 最常用配置,應用、永久激活

拋開 Vue、React、JQuery 這類第三方js,我們該怎麼寫代碼?


推薦閱讀:
相关文章