Go 1.10中值得關注的幾個變化
曾幾何時, 這是很多Gopher在Go 1.8、Go 1.9時猜測是否存在的那個版本,畢竟minor version即將進化到兩位數。從Go語言第一封設計mail發出到現在的十年間,尤其是Go語言經歷了近幾年的爆發式增長,基本奠定了云原生第一語言的位置之后,人們對Go語言有了更多新的、更為深刻的認知,同時對這門編程語言也有了更多的改進和優化的期望。Go2在Gopher心中的位置日益提升,直到 Russ Cox 在 GopherCon 2017 上公布了Go core team對 Go2的開發策略 ,我們才意識到: 哦,Go1還將繼續一段時間,甚至是一段很長的時間。2018年2月,我們將迎來Go 1.10版本 。
從Go 1.4版本開始,我自己都沒想到我能將 “Go x.x中值得關注的幾個變化” 這個系列一直寫到Go 1.10。不過現在看來,這個系列還會繼續,以后可能還有Go 1.11、Go 1.12…,甚至是進化到Go2之后的各個版本。
Go從 1.0版本發布 之日起,便遵守著自己 “變與不變” 的哲學。不變的是對Go對 “Go1 promise of compatibility” 的嚴格遵守,變化的則是對語言性能、運行時、GC、工具以及標準庫更為精細和耐心地打磨。這次發布的Go 1.10依然延續著這種理念,將重點的改進放在了運行時、工具以及標準庫上。接下來,我就和大家一起看看即將發布的Go 1.10都有哪些值得重點關注的變化。
一、語言
Go language Spec 是當前Go語言的唯一語言規范標準,雖然其嚴謹性與那些以ISO標準形式編寫成的語言規范(比如:C語言、C++語言的規范)還有一定差距。因此,對go spec的優化,就是在嚴謹性方面下功夫。當前spec的主要修訂者是Go語言三個設計者之一的 Robert Griesemer ,他在Go 1.10周期對spec 做了較多語言概念嚴謹性方面的改進 。
1、顯式定義Representability(可表示性)
在 Properties of types and values 章節下,Robert Griesemer顯式引入了一個新的術語 Representability ,這里譯為 可表示性 。這一術語的 引入 并未帶來語法的變化,只是為了更精確的闡釋規范。Representability的定義明確了當規范中出現“a constant x is representable by a value of type T”時成立的幾種條件,尤其是針對浮點類型和復數類型。這里摘錄(不翻譯):
A constant x is representable by a value of type T if one of the following conditions applies:
- x is in the set of values determined by T.
- T is a floating-point type and x can be rounded to T's precision without overflow. Rounding uses IEEE 754 round-to-even rules but with an IEEE negative zero further simplified to an unsigned zero. Note that constant values never result in an IEEE negative zero, NaN, or infinity.
- T is a complex type, and x's components real(x) and imag(x) are representable by values of T's component type (float32 or float64).
2、澄清未指定類型的常量作為shift(移位)非常量位操作的左操作數時在某些特定上下文中的類型
雖然不及ISO標準規范嚴謹,但凡是language spec,理解起來都是有門檻的。這個 改進針對的是那些未指定類型的常量 ,在作為shift非常量位操作的左操作數時,在shift表達式結果作為下標表達式中的下標、切片表達式下標或者make函數調用中的size參數時,這個常量將被賦予int類型。我們還是看個例子更加直觀:
// go1.10-examples/spec/untypedconst.go
package main
var (
s uint = 2
)
func main() {
a := make([]int, 10)
a[1.0<<s] = 4
}
上面的例子中,重點看 a[1.0 << s] = 4 這一行,這一行恰好滿足了幾個條件:
- 1.0 << s 是一個shift表達式,且作為slide表達式的下標;
- shift表達式所移動的位數為s,s是一個變量,非常量,因此這是一個非常量位的移位操作;
- 1.0是未指定類型的常量(untyped const),且作為shift表達式左操作數
在Go 1.9.2下面,上面的程序編譯結果如下:
// go 1.9.2編譯器build:
$go build untypedconst.go
# command-line-arguments
./untypedconst.go:9:7: invalid operation: 1 << s (shift of type float64)
在Go 1.9.2下,1.0這個常量被compiler賦予了float64類型,導致編譯出錯。在Go 1.10下,根據最新的spec,1.0被賦予了int型,編譯則順利通過。
但一旦脫離了下標這個上下文環境,1.0這個常量依舊會被compiler識別為float64類型,比如下面代碼中1.0<<s作為Println的參數就是不符合語法的:
// go1.10-examples/spec/untypedconst.go
package main
import "fmt"
var (
s uint = 2
)
func main() {
a := make([]int, 10)
a[1.0<<s] = 4
fmt.Println(1.0<<s)
}
// go 1.10rc2編譯器build:
$go build untypedconst.go
# command-line-arguments
./untypedconst.go:12:17: invalid operation: 1 << s (shift of type float64)
./untypedconst.go:12:17: cannot use 1 << s as type interface {} in argument to fmt.Println
3、明確預聲明類型(predeclared type)是defined type還是alias type
Go在1.9版本中引入了alias語法,同時引入defined type(以替代named type)和alias type,并使用alias語法對某些predeclared type的實現進行了調整。在Go 1.10 spec中,Griesemer進一步 明確了哪些predeclared type是alias type :
目前內置的predeclared type只有兩個類型是alias type:
byte alias for uint8
rune alias for int32
其余的predeclared type都是defined type。
4、移除spec中對method expression: T.m中T的類型的限制
這次是spec落伍于compiler了。Go 1.9.2就可以順利編譯運行下面的代碼:
//go1.10-examples/spec/methodexpression.go
package main
import "fmt"
type foo struct{}
func (foo)f() {
fmt.Println("i am foo")
}
func main() {
interface{f()}.f(foo{})
}
但在Go 1.9.2的spec中,對Method expression的定義如下:
Go 1.9.2 spec:
MethodExpr = ReceiverType "." MethodName .
ReceiverType = TypeName | "(" "*" TypeName ")" | "(" ReceiverType ")" .
Go 1.9.2的spec說,method expression形式:T.m中的T僅能使用Typename,而非上述代碼中type實現。Go 1.10的spec中 放開了對method expression中T的限制 ,使得type的實現也可以作為T調用method,與編譯器的實際實現行為同步:
Go 1.10rc2 spec:
MethodExpr = ReceiverType "." MethodName .
ReceiverType = Type .
不過目前Go 1.10 rc2 compiler還存在 一個問題 ,我們看一下下面的代碼:
//go1.10-examples/spec/methodexpression1.go
package main
func main() {
(*struct{ error }).Error(nil)
}
使用Go 110rc2構建該源碼,得到如下錯誤:
$go build methodexpression1.go
# command-line-arguments
go.(*struct { error }).Error: call to external function
main.main: relocation target go.(*struct { error }).Error not defined
main.main: undefined: "go.(*struct { error }).Error"
該問題目前已經有 issue 對應,狀態還是Open。
二、工具
Go語言有著讓其他主流編程語言羨慕的工具集,每次Go版本更新,工具集都會得到進一步的加強,無論是功能還是從開發者體驗方面,都有提升。
1、默認的GOROOT
繼Go 1.8版本引入默認的GOPATH后,Go 1.10版本為繼續改進Go工具的開發者體驗,進一步降低新手的使用門檻,引入了默認GOROOT:即開發者無需顯式設置GOROOT環境變量,go程序會自動根據自己所在路徑推導出GOROOT的路徑。這樣一來,Gopher們就可以將下載的Go預編譯好的安裝包解壓放置到任意本地路徑下,唯一要做的就是將go二進制程序路徑放置到PATH環境變量中。比如我們將go1.10rc2的安裝包解壓到下面路徑下:
? /Users/tony/.bin/go1.10rc2 $ls
AUTHORS LICENSE VERSION blog/ lib/ robots.txt
CONTRIBUTING.md PATENTS api/ doc/ misc/ src/
CONTRIBUTORS README.md bin/ favicon.ico pkg/ test/
在設置為PATH后,我們通過go env命令查看go自動推導的GOROOT以及其他相關變量的值:
$go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/tony/Library/Caches/go-build"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/tony/go"
GORACE=""
GOROOT="/Users/tony/.bin/go1.10rc2"
GOTMPDIR=""
GOTOOLDIR="/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -gno-record-gcc-switches -fno-common"
從輸出結果看到,go正確找到了安裝路徑,并得到了GOROOT信息。
2、增加GOTMPDIR變量
在上面的go env命令輸出內容中,我們發現了一個陌生的變量:GOTMPDIR,其值默認為空串。這個 GOTMPDIR變量 是Go 1.10新引入的變量,用于設置Go tool創建和使用的臨時文件的路徑的。有人可能會說:這個變量看似沒什么必要,直接用系統的/tmp路徑就好了啊。但是在/tmp路徑中編譯和執行編譯后的程序至少有兩點問題, 這些問題 實際上在go的issues歷史中已經存在許久了:
- 有些機器上/tmp路徑下被設置了 無執行權限(set noexec)
- 有些機器上/tmp下空間有限
我們知道默認情況下,go build和go run都會在/tmp下設置一個臨時WORK目錄來編譯源碼和執行編譯后的程序的,從下面的一個最簡單的helloworld源碼的編譯執行過程輸出(WORK變量),我們就能看到這點:
// on ubuntu 16.04
# go run -x hello.go
WORK=/tmp/go-build001434392
mkdir -p $WORK/b001/
... ...
mkdir -p $WORK/b001/exe/
cd .
/root/.bin/go1.10rc2/pkg/tool/linux_amd64/link -o $WORK/b001/exe/hello -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=fcYMWp_1J2Xqgzc_Vdga/UpnEUti07R2GzG8dUU3x/MLkSlJVesZhf2kQUaDUU/fcYMWp_1J2Xqgzc_Vdga -extld=gcc /root/.cache/go-build/9f/9f34be2dbcc3f8a62dd6efd6d35be18ecdcbc49e3c8b52b003ecd72b6264e19e-d
$WORK/b001/exe/hello
我個人就遇到過由于IaaS供應商提供的系統盤(不允許定制和修改)過小,導致系統盤空間滿,使得Go應用構建和執行失敗的問題。我們來設置一下GOTMPDIR,看看效果。我們將GOTMPDIR設置為~/.gotmp,生效后,重新build上面的那個helloworld代碼:
# go build -x hello.go
WORK=/root/.gotmp/go-build452283009
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg << 'EOF' # internal
... ...
mkdir -p $WORK/b001/exe/
cd .
/root/.bin/go1.10rc2/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=kO-wBNzMZmfHCKzMDziw/jCGBCt7bcrS5NEN-cR4H/8-du6iTQz8uPH3UC-FtB/kO-wBNzMZmfHCKzMDziw -extld=gcc $WORK/b001/_pkg_.a
/root/.bin/go1.10rc2/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out hello
rm -r $WORK/b001/
可以看到,go tool轉移到我們設置的GOTMPDIR下構建和執行了。
3、通過cache實現增量構建,提高go tools性能
Go語言具有較高的編譯性能是Go語言最初設計時就確定下來的目標,Go編譯器的性能在Go 1.4.3版本達到頂峰,這雖然是得益于其使用C語言實現,但更重要的是其為高性能構建而定義的便于依賴分析的語言構建模型,同時避免了像C/C++那樣的重復多次掃描大量頭文件的負擔。隨著Go自舉的實現,使用Go語言實現的go compiler性能有較大下降,但即便這樣,其編譯速度在主流編程語言中仍然是數一數二的。在經過了Go 1.6到Go1.9等多個版本對compiler的優化后,go compiler的編譯速度已經 恢復到Go 1.4.3 compiler的2/3左右 或是更為接近的水平。在Go 1.9版本引入并行編譯后,Go team在提升工具性能方面的思路發生了些許變化:不再是一味地進行代碼級的性能優化,而是選擇通過Cache,重復利用中間結果,實現增量構建,來減少編譯構建所用的時間。因此,筆者覺得這個功能是 本次Go 1.10最大的變化之一 。
1) 概述
Go 1.10版本以前,我們經常通過go build -i來加快Go項目源碼的編譯速度,其原因在于go build -i首次執行時會將目標所依賴的package安裝到$GOPATH/pkg下面(.a文件),這樣后續執行go build時,構建過程將不會重新編譯目標文件的依賴包,而是直接鏈接首次執行build -i時安裝的依賴包,以實現加速編譯!以gocmpp/examples/client為例,第二次構建所需時間僅為首次構建的四分之一左右:
? $GOPATH/src/github.com/bigwhite/gocmpp/examples/client git:(master) ? $time go build -i client.go
go build -i client.go 1.34s user 0.34s system 131% cpu 1.274 total
? $GOPATH/src/github.com/bigwhite/gocmpp/examples/client git:(master) ? $time go build -i client.go
go build -i client.go 0.38s user 0.16s system 116% cpu 0.465 total
只有當目標文件的依賴包的源文件發生變化時(比對源文件的修改時間與.a文件的修改時間作為是否重新編譯的判斷依據),才會重新編譯安裝這些依賴包。這有些像Makefile的原理:make工具會比較targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的話,那么,make就會執行后續定義的命令。
不過即便這樣,依然至少有兩個問題困擾著Go team和廣大Gopher:
-
基于時間戳的比對,并不“合理”
當某個目標文件的依賴包的源文件內容并未真正發生變化,但“修改時間”發生變化了,比如:添加了一行,保存了;然后又刪除了這一行,保存。在這樣的情況下,理想的操作是不需要重新編譯安裝這個依賴包,但目前的go build -i機制會 重新編譯并安裝這個依賴包 。
-
增量構建并未實現“常態化”
以前版本中,默認的不帶命令行參數的go build命令是不會安裝依賴包的,因此每次執行go build,都會重新編譯一次依賴包的源碼,并將結果放入臨時目錄以供最終鏈接使用。也就是說最為常用的go build并未實現增量編譯,社區需要常態化的“增量編譯”,進一步提高效率。
Go 1.10引入cache機制來解決上述問題。從1.10版本開始,go build tool將維護一個package編譯結果的緩存以及一些元數據,緩存默認位于操作系統指定的用戶緩存目錄中,其中數據用于后續構建重用;不僅go build支持“常態化”的增量構建,go test也支持在特定條件下緩存test結果,從而加快執行測試的速度。
b) go build with cache
我們先來直觀的看看go 1.10 build帶來的效果,初始情況cache為空:
以我的一個小項目gocmpp為例,用go 1.10第一次build該項目:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $time go build
go build 1.22s user 0.43s system 175% cpu 0.939 total
我們再來構建一次:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $time go build
go build 0.12s user 0.16s system 155% cpu 0.182 total
0.12s vs. 1.22s!通過cache進行的build將構建時間壓縮為原來的1/10!為了弄清楚go build幕后行為,我們清除一下cache(go clean -cache),再重新build,這次我們通過-v -x 輸出詳細構建過程:
首次編譯的詳細輸出信息:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go build -x -v
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build735203690
github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier
mkdir -p $WORK/b033/
cat >$WORK/b033/importcfg << 'EOF' # internal
# import config
EOF
cd $(GOPATH)/src/github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b033/_pkg_.a -trimpath $WORK/b033 -p github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier -complete -buildid iZWJNg2FYmWoSCXb640o/iZWJNg2FYmWoSCXb640o -goversion go1.10rc2 -D "" -importcfg $WORK/b033/importcfg -pack -c=4 ./identifier.go ./mib.go
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b033/_pkg_.a # internal
cp $WORK/b033/_pkg_.a /Users/tony/Library/Caches/go-build/14/14223040d851359359b0e531555a47e22f5dbd4bf434acc136a7c70c1fc3663f-d # internal
github.com/bigwhite/gocmpp/vendor/golang.org/x/text/transform
mkdir -p $WORK/b031/
cat >$WORK/b031/importcfg << 'EOF' # internal
# import config
packagefile bytes=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/bytes.a
packagefile errors=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/errors.a
packagefile io=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/io.a
packagefile unicode/utf8=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/unicode/utf8.a
EOF
.... ....
cd $(GOPATH)/src/github.com/bigwhite/gocmpp
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath $WORK/b001 -p github.com/bigwhite/gocmpp -complete -buildid 6LaoHtjkFhandbEhv7zD/6LaoHtjkFhandbEhv7zD -goversion go1.10rc2 -D "" -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/e0/e02a5fec0835ca540b62053fdea82589e686e88bf48f18355ed38d41ad19f334-d # internal
再次編譯的詳細輸出信息:
$go build -x -v
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build906548554
我們來分析一下。首次構建時,我們看到gocmpp依賴的每個包以及自身的包都會被編譯,并被copy到/Users/tony/Library/Caches/go-build/下面的某個目錄下,包括最終的gocmpp包也是這樣。第二次build時,我們看到僅僅輸出一行信息,這是因為go compiler在cache中找到了gocmpp包對應的編譯好的緩存結果,無需進行實際的編譯了。
前面說過,go 1.10 compiler決定是否重新編譯包是content based的,而不是依照時間戳比對來決策。我們來修改一個gocmpp包中的文件fwd.go,刪除一個空行,再恢復這個空行,保存退出。我們再來編譯一下gocmpp:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go build -x -v
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build857409409
可以看到go compiler并沒有重新編譯任何包。如果我們真實改變了fwd.go的內容,比如刪除一個空行,保存后再次編譯:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go build -x -v
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build437927548
github.com/bigwhite/gocmpp
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg << 'EOF' # internal
# import config
packagefile bytes=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/bytes.a
packagefile crypto/md5=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/crypto/md5.a
packagefile encoding/binary=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/encoding/binary.a
packagefile errors=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/errors.a
packagefile fmt=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/fmt.a
packagefile github.com/bigwhite/gocmpp/utils=/Users/tony/Test/GoToolsProjects/pkg/darwin_amd64/github.com/bigwhite/gocmpp/utils.a
packagefile io=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/io.a
packagefile log=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/log.a
packagefile net=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/net.a
packagefile os=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/os.a
packagefile strconv=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/strconv.a
packagefile strings=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/strings.a
packagefile sync=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/sync.a
packagefile sync/atomic=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/sync/atomic.a
packagefile time=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/time.a
EOF
cd /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/gocmpp
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath $WORK/b001 -p github.com/bigwhite/gocmpp -complete -buildid trn5lvvRTk_UP3LcT5CC/trn5lvvRTk_UP3LcT5CC -goversion go1.10rc2 -D "" -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/7a/7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d # internal
Go compiler發現了內容的變動,對gocmpp包的變動內容進行了重新compile。
c) 緩存目錄探索
在增加cache機制時,go tools增加了GOCACHE變量,通過go env GOCACHE查看變量值:
$go env GOCACHE
/Users/tony/Library/Caches/go-build
如果未重設環境變量GOCACHE,那么默認在Linux上,GOCACHE=”~/.cache/go-build”; 在Mac OS X上,GOCACHE=”/Users/UserName/Library/Caches/go-build”。在 OS X上,我們進入$GOCACHE目錄,映入眼簾的是:
? /Users/tony/Library/Caches/go-build $ls
00/ 18/ 30/ 48/ 60/ 78/ 90/ a7/ bf/ d7/ ef/
01/ 19/ 31/ 49/ 61/ 79/ 91/ a8/ c0/ d8/ f0/
02/ 1a/ 32/ 4a/ 62/ 7a/ 92/ a9/ c1/ d9/ f1/
03/ 1b/ 33/ 4b/ 63/ 7b/ 93/ aa/ c2/ da/ f2/
04/ 1c/ 34/ 4c/ 64/ 7c/ 94/ ab/ c3/ db/ f3/
05/ 1d/ 35/ 4d/ 65/ 7d/ 95/ ac/ c4/ dc/ f4/
06/ 1e/ 36/ 4e/ 66/ 7e/ 96/ ad/ c5/ dd/ f5/
07/ 1f/ 37/ 4f/ 67/ 7f/ 97/ ae/ c6/ de/ f6/
08/ 20/ 38/ 50/ 68/ 80/ 98/ af/ c7/ df/ f7/
09/ 21/ 39/ 51/ 69/ 81/ 99/ b0/ c8/ e0/ f8/
0a/ 22/ 3a/ 52/ 6a/ 82/ 9a/ b1/ c9/ e1/ f9/
0b/ 23/ 3b/ 53/ 6b/ 83/ 9b/ b2/ ca/ e2/ fa/
0c/ 24/ 3c/ 54/ 6c/ 84/ 9c/ b3/ cb/ e3/ fb/
0d/ 25/ 3d/ 55/ 6d/ 85/ 9d/ b4/ cc/ e4/ fc/
0e/ 26/ 3e/ 56/ 6e/ 86/ 9e/ b5/ cd/ e5/ fd/
0f/ 27/ 3f/ 57/ 6f/ 87/ 9f/ b6/ ce/ e6/ fe/
10/ 28/ 40/ 58/ 70/ 88/ README b7/ cf/ e7/ ff/
11/ 29/ 41/ 59/ 71/ 89/ a0/ b8/ d0/ e8/ log.txt
12/ 2a/ 42/ 5a/ 72/ 8a/ a1/ b9/ d1/ e9/ trim.txt
13/ 2b/ 43/ 5b/ 73/ 8b/ a2/ ba/ d2/ ea/
14/ 2c/ 44/ 5c/ 74/ 8c/ a3/ bb/ d3/ eb/
15/ 2d/ 45/ 5d/ 75/ 8d/ a4/ bc/ d4/ ec/
16/ 2e/ 46/ 5e/ 76/ 8e/ a5/ bd/ d5/ ed/
17/ 2f/ 47/ 5f/ 77/ 8f/ a6/ be/ d6/ ee/
熟悉git原理的朋友一定覺得這個目錄組織結構似曾相識!沒錯,在每個git項目的./git/object目錄下,我們也能看到下面的結果:
.git/objects git:(master) $ls
00/ 0c/ 18/ 24/ 30/ 3c/ 48/ 54/ 60/ 6c/ 78/ 84/ 90/ 9c/ a8/ b4/ c0/ cc/ d8/ e4/ f0/ fc/
01/ 0d/ 19/ 25/ 31/ 3d/ 49/ 55/ 61/ 6d/ 79/ 85/ 91/ 9d/ a9/ b5/ c1/ cd/ d9/ e5/ f1/ fd/
02/ 0e/ 1a/ 26/ 32/ 3e/ 4a/ 56/ 62/ 6e/ 7a/ 86/ 92/ 9e/ aa/ b6/ c2/ ce/ da/ e6/ f2/ fe/
03/ 0f/ 1b/ 27/ 33/ 3f/ 4b/ 57/ 63/ 6f/ 7b/ 87/ 93/ 9f/ ab/ b7/ c3/ cf/ db/ e7/ f3/ ff/
04/ 10/ 1c/ 28/ 34/ 40/ 4c/ 58/ 64/ 70/ 7c/ 88/ 94/ a0/ ac/ b8/ c4/ d0/ dc/ e8/ f4/ info/
05/ 11/ 1d/ 29/ 35/ 41/ 4d/ 59/ 65/ 71/ 7d/ 89/ 95/ a1/ ad/ b9/ c5/ d1/ dd/ e9/ f5/ pack/
06/ 12/ 1e/ 2a/ 36/ 42/ 4e/ 5a/ 66/ 72/ 7e/ 8a/ 96/ a2/ ae/ ba/ c6/ d2/ de/ ea/ f6/
07/ 13/ 1f/ 2b/ 37/ 43/ 4f/ 5b/ 67/ 73/ 7f/ 8b/ 97/ a3/ af/ bb/ c7/ d3/ df/ eb/ f7/
08/ 14/ 20/ 2c/ 38/ 44/ 50/ 5c/ 68/ 74/ 80/ 8c/ 98/ a4/ b0/ bc/ c8/ d4/ e0/ ec/ f8/
09/ 15/ 21/ 2d/ 39/ 45/ 51/ 5d/ 69/ 75/ 81/ 8d/ 99/ a5/ b1/ bd/ c9/ d5/ e1/ ed/ f9/
0a/ 16/ 22/ 2e/ 3a/ 46/ 52/ 5e/ 6a/ 76/ 82/ 8e/ 9a/ a6/ b2/ be/ ca/ d6/ e2/ ee/ fa/
0b/ 17/ 23/ 2f/ 3b/ 47/ 53/ 5f/ 6b/ 77/ 83/ 8f/ 9b/ a7/ b3/ bf/ cb/ d7/ e3/ ef/ fb/
這里猜測go 1.10使用的應該是與git一類內容摘要算法以及組織存儲模式。在前面的build詳細輸出中,我們找到這一行:
cp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/7a/7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d # internal
這行命令是將gocmpp包復制到cache下,我們到cache的7a目錄下一查究竟:
? /Users/tony/Library/Caches/go-build/7a $tree
.
└── 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d
0 directories, 1 file
我們用nm命令查看一下該文件:
$go tool nm 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d|more
1c319 T %22%22.(*Client).Connect
2e279 T %22%22.(*Client).Connect.func1
3fc22 R %22%22.(*Client).Connect.func1·f
1c79f T %22%22.(*Client).Disconnect
1c979 T %22%22.(*Client).RecvAndUnpackPkt
1c807 T %22%22.(*Client).SendReqPkt
1c8e2 T %22%22.(*Client).SendRspPkt
1e417 T %22%22.(*Cmpp2ConnRspPkt).Pack
... ...
這個文件的確就是gocmpp.a文件。通過比對該文件size與go install后的文件size也可以證實這一點:
? /Users/tony/Library/Caches/go-build/7a $
-rw-r--r-- 1 tony staff 445856 Feb 15 22:34 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d
vs.
? $GOPATH/pkg/darwin_amd64/github.com/bigwhite $ll
-rw-r--r-- 1 tony staff 445856 Feb 15 23:27 gocmpp.a
也就是說go compiler將編譯后的package的.a文件求取摘要值后,將.a文件存儲在$GOCACHE下的某個目錄中,這個目錄名即為摘要值的前兩位(比如”7a”),.a文件名字被換成其摘要值,以便后續查找并做比對。
cache目錄下還有一個重要文件:log.txt,這個文件是用來記錄緩存管理日志的,其內容格式如下:
//log.txt
... ...
1518705271 get 7533a063cd8c37888b19674bf4a4bb7e25fa422041082566530d58538c031516
1518705271 miss b6b9f996fbd14e4fd43f72dc4f9082946cddd0d61d6c6143c88502c8a4001666
1518705271 put b6b9f996fbd14e4fd43f72dc4f9082946cddd0d61d6c6143c88502c8a4001666 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499 445856
1518705271 put f5a641ca081a0d2d794b0b54aa9f89014dbb6ff8d14d26543846e1676eca4c21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0
1518708456 get 899589360d856265a84825dbeb8d283ca84e12f154eefc12ba84870af13e1f63
1518708456 get 8a7fcd97a5f36bd00ef084856c63e4e2facedce33d19a5b557cc67f219787661
該日志文件更多的用途是幫助Russ Cox對其開發的cache進行調試和問題診斷的。當然,如果您對于cache的機制原理也很精通,那么也可以讓log.txt幫你診斷涉及cache的問題。
d) go test with cache
go 1.10版的go test也會維護一個cache,這個cache緩存了go test執行的測試結果。同時在go 1.10中,go test被分為兩種執行模式:local directory mode和package list mode,在不同模式下,cache機制的介入是不同的。
local directory mode,即go test以整個當前目錄作為隱式參數的執行模式,比如在某個目錄下執行”go test”,go test后面不帶任何顯式的package列表參數(當然可以帶著其他命令行flag參數,如-v)。在這種模式下,cache機制不會介入,go test的執行過程與go 1.10版本之前沒有兩樣。還是以gocmpp這個項目為例,我們以local directory mode執行go test:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go test
PASS
ok github.com/bigwhite/gocmpp 0.011s
如果緩存機制介入,輸出的test結果中會出現cached字樣,顯然上面的go test執行過程并沒有使用test cache。
package list mode,即go test后面顯式傳入了package列表,比如:go test math、go test .、go test ./…等,在這種模式下,test cache機制會介入。我們連續兩次在gocmpp目錄下執行go test .:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go test .
ok github.com/bigwhite/gocmpp 0.011s
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go test .
ok github.com/bigwhite/gocmpp (cached)
如果你此時想進一步看看go test執行的詳細輸出,你可以會執行go test -v .:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go test -v .
=== RUN TestTypeString
--- PASS: TestTypeString (0.00s)
=== RUN TestCommandIdString
--- PASS: TestCommandIdString (0.00s)
=== RUN TestOpError
--- PASS: TestOpError (0.00s)
... ...
=== RUN TestCmppTerminateRspPktPack
--- PASS: TestCmppTerminateRspPktPack (0.00s)
=== RUN TestCmppTerminateRspUnpack
--- PASS: TestCmppTerminateRspUnpack (0.00s)
PASS
ok github.com/bigwhite/gocmpp 0.017s
你會發現,這次go test并沒有使用cache。如果你再執行一次go test -v .:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go test -v .
=== RUN TestTypeString
--- PASS: TestTypeString (0.00s)
=== RUN TestCommandIdString
--- PASS: TestCommandIdString (0.00s)
=== RUN TestOpError
--- PASS: TestOpError (0.00s)
... ...
=== RUN TestCmppTerminateRspPktPack
--- PASS: TestCmppTerminateRspPktPack (0.00s)
=== RUN TestCmppTerminateRspUnpack
--- PASS: TestCmppTerminateRspUnpack (0.00s)
PASS
ok github.com/bigwhite/gocmpp (cached)
test cache又起了作用。似乎cache對于go test .和go test -v .是獨立的。沒錯,release note中給出的go test cache的介入條件如下:
- 本次測試的執行程序以及命令行(及參數)與之前的一次test運行匹配;(這就能解釋為何go test -v .沒有使用go test .執行的cache了);
- 上次測試執行時的文件和環境變量在本次沒有發生變化;
- 測試結果是成功的;
- 以package list node運行測試;
- go test的命令行參數使用”-cpu, -list, -parallel, -run, -short和 -v”的一個子集時
就像前面我們看到的,cache介入的go test結果不會顯示test消耗的時間,而是以(cached)字樣替代。
絕大多數Gopher都是喜歡test with cache的,但總有一些情況,cache是不受歡迎的。其實前面的條件已經明確告知gopher們什么條件下test cache是可以不介入的。 一個慣用的關閉test cache的方法 是使用-count=1:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go test -count=1 -v .
=== RUN TestTypeString
--- PASS: TestTypeString (0.00s)
=== RUN TestCommandIdString
--- PASS: TestCommandIdString (0.00s)
=== RUN TestOpError
--- PASS: TestOpError (0.00s)
... ...
=== RUN TestCmppTerminateRspPktPack
--- PASS: TestCmppTerminateRspPktPack (0.00s)
=== RUN TestCmppTerminateRspUnpack
--- PASS: TestCmppTerminateRspUnpack (0.00s)
PASS
ok github.com/bigwhite/gocmpp 0.012s
go 1.10中的go test與之前版本還有一個不同,那就是go test在真正執行test前會自動對被測試的包執行go vet,但這個vet只會識別那些最為明顯的問題。并且一旦發現問題,go test將會視這些問題與build error同級別,阻斷test的執行,并讓其出現在test failure中。當然gopher可以通過go test -vet=off關閉這個前置于測試的vet檢查。
4. pprof
go tool pprof做了一個較大的改變:增加了Web UI,以后可以和go trace一起通過圖形化的方法對Go程序進行調優了。可視化的pprof使用起來十分簡單,我們以gocmpp為例,試用一下go 1.10的pprof,首先我們生成cpu profile文件:
? $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ? $go test -run=^$ -bench=. -cpuprofile=profile.out
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/gocmpp
BenchmarkRecvAndUnpackPkt-4 1000000 1534 ns/op
BenchmarkCmppConnReqPktPack-4 1000000 1398 ns/op
BenchmarkCmppConnReqPktUnpack-4 3000000 450 ns/op
BenchmarkCmpp2DeliverReqPktPack-4 1000000 1156 ns/op
BenchmarkCmpp2DeliverReqPktUnpack-4 3000000 567 ns/op
BenchmarkCmpp3DeliverReqPktPack-4 1000000 1173 ns/op
BenchmarkCmpp3DeliverReqPktUnpack-4 3000000 465 ns/op
BenchmarkCmpp2FwdReqPktPack-4 1000000 2079 ns/op
BenchmarkCmpp2FwdReqPktUnpack-4 1000000 1276 ns/op
BenchmarkCmpp3FwdReqPktPack-4 1000000 2507 ns/op
BenchmarkCmpp3FwdReqPktUnpack-4 1000000 1286 ns/op
BenchmarkCmpp2SubmitReqPktPack-4 1000000 1845 ns/op
BenchmarkCmpp2SubmitReqPktUnpack-4 1000000 1251 ns/op
BenchmarkCmpp3SubmitReqPktPack-4 1000000 1863 ns/op
BenchmarkCmpp3SubmitReqPktUnpack-4 2000000 656 ns/op
PASS
ok github.com/bigwhite/gocmpp 26.621s
啟動pprof web ui:
$go tool pprof -http=:8080 profile.out
pprof會自動打開默認瀏覽器,進入下面頁面:
在view菜單中,我們可以看到”top”、”graph”、”peek”、”source”和”disassemble”幾個選項,這些選項可以幫助你在各種視圖間切換,默認初始為graph view。不過目前view菜單中并沒有”Flame Graph(火焰圖)”選項,要想使用Flame Graph,我們需要使用原生的pprof工具,該工具可通過go get -u github.com/google/pprof獲取,install后原生pprof將出現在$GOROOT/bin下面。
使用原生pprof啟動Web UI:
$pprof -http=:8080 profile.out
原生pprof同樣會自動打開瀏覽器,進入下面頁面:
原生的pprof的web ui看起來比go 1.10 tool中的pprof更為精致,且最大的不同在于VIEW菜單下出現了”Flame Graph”菜單項!我們點擊該菜單項,一幅Flame Graph便呈現在眼前:
關于如何做火焰圖分析不是這里的主要任務,請各位Gopher自行腦補。更多關于Go性能調優問題,可以參考 Go官方提供的診斷手冊 。
四、標準庫
和之前的每次Go版本發布一樣,標準庫的改變是多且細碎的,這里不能一一舉例說明。并且很多涉“專業領域”的包,比如加解密,需要一定專業深度,因此這里僅列舉幾個“通用”的變化^0^。
1、strings.Builder
strings包增加一個新的類型:Builder,用于在“拼字符串”場景中替代bytes.Buffer,由于使用了一些unsafe包的黑科技,在用戶調用Builder.String()返回最終拼成的字符串時,避免了一些重復的、不必要的內存copy,提升了處理性能,優化了內存分配。我們用一個demo來看看這種場景下Builder的優勢:
//go1.10-examples/stdlib/stringsbuilder/builer.go
package builder
import (
"bytes"
"strings"
)
type BuilderByBytesBuffer struct {
b bytes.Buffer
}
func (b *BuilderByBytesBuffer) WriteString(s string) error {
_, err := b.b.WriteString(s)
return err
}
func (b *BuilderByBytesBuffer) String() string{
return b.b.String()
}
type BuilderByStringsBuilder struct {
b strings.Builder
}
func (b *BuilderByStringsBuilder) WriteString(s string) error {
_, err := b.b.WriteString(s)
return err
}
func (b *BuilderByStringsBuilder) String() string{
return b.b.String()
}
針對上面代碼中的BuilderByBytesBuffer和BuilderByStringsBuilder進行Benchmark的Test源文件如下:
//go1.10-examples/stdlib/stringsbuilder/builer_test.go
package builder
import "testing"
func BenchmarkBuildStringWithBytesBuffer(b *testing.B) {
var builder BuilderByBytesBuffer
for i := 0; i < b.N; i++ {
builder.WriteString("Hello, ")
builder.WriteString("Go")
builder.WriteString("-1.10")
_ = builder.String()
}
}
func BenchmarkBuildStringWithStringsBuilder(b *testing.B) {
var builder BuilderByStringsBuilder
for i := 0; i < b.N; i++ {
builder.WriteString("Hello, ")
builder.WriteString("Go")
builder.WriteString("-1.10")
_ = builder.String()
}
}
執行該Benchmark,查看結果:
$go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/go1.10-examples/stdlib/stringsbuilder
BenchmarkBuildStringWithBytesBuffer-4 100000 108471 ns/op 704073 B/op 1 allocs/op
BenchmarkBuildStringWithStringsBuilder-4 20000000 122 ns/op 80 B/op 0 allocs/op
PASS
ok github.com/bigwhite/experiments/go1.10-examples/stdlib/stringsbuilder 13.616s
可以看到StringsBuilder在處理速度和分配優化上都全面強于bytes.Buffer,真實的差距就在Builder.String這個方法上。
2、bytes包
bytes包的幾個方法Fields, FieldsFunc, Split和SplitAfter在底層實現上有變化,使得外部展現的行為有所變化,我們通過一個例子直觀的感受一下:
// go1.10-examples/stdlib/bytessplit/main.go
package main
import (
"bytes"
"fmt"
)
// 來自github.com/campoy/gotalks/blob/master/go1.10/bytes/fields.go
func desc(b []byte) string {
return fmt.Sprintf("len: %2d | cap: %2d | %q\n", len(b), cap(b), b)
}
func main() {
text := []byte("Hello, Go1.10 is coming!")
fmt.Printf("text: %s", desc(text))
subslices := bytes.Split(text, []byte(" "))
fmt.Printf("subslice 0: %s", desc(subslices[0]))
fmt.Printf("subslice 1: %s", desc(subslices[1]))
fmt.Printf("subslice 2: %s", desc(subslices[2]))
fmt.Printf("subslice 3: %s", desc(subslices[3]))
}
我們先用Go 1.9.2編譯運行一下該demo:
$go run main.go
text: len: 24 | cap: 32 | "Hello, Go1.10 is coming!"
subslice 0: len: 6 | cap: 32 | "Hello,"
subslice 1: len: 6 | cap: 25 | "Go1.10"
subslice 2: len: 2 | cap: 18 | "is"
subslice 3: len: 7 | cap: 15 | "coming!"
我們再用go 1.10rc2運行一下該demo:
$go run main.go
text: len: 24 | cap: 32 | "Hello, Go1.10 is coming!"
subslice 0: len: 6 | cap: 6 | "Hello,"
subslice 1: len: 6 | cap: 6 | "Go1.10"
subslice 2: len: 2 | cap: 2 | "is"
subslice 3: len: 7 | cap: 15 | "coming!"
對比兩次輸出結果中cap那一列,你會發現go 1.10輸出的結果中的 每個subslice(除了最后一個)的len與cap值都是相等的 ,而不是將原slice剩下所有cap都作為subslice的cap。這個行為的改變是出于安全的考慮,防止共享一個underlying slice的各個subslice的修改對相鄰的subslice造成影響,因此限制它們的capacity。
在Fields, FieldsFunc, Split和SplitAfter這幾個方法的具體實現上,Go 1.10使用了我們平時并不經常使用的” Full slice expression “,即:a[low, high, max]來指定subslice的cap。
五、性能
對于靜態編譯類型語言Go來說,性能也一直是其重點關注的設計目標,這兩年來發布的Go版本,幾乎每個都給Gopher們帶來驚喜。談到Go性能,Gopher們一般關心的有如下這么幾個方面:
1、編譯性能
Go 1.10的編譯性能正如我們前面所說的那樣,最大的改變在于cache機制的實現。事實證明cache機制的使用在日常開發過程中,會很大程度上提升你的工作效率,越是規模較大的項目越是如此。
2、目標代碼的性能
這些年Go team在不斷優化編譯器生成的目標代碼的性能,比如在Go 1.7版本中引入ssa后端。Go 1.10延續著對目標代碼生成的進一步優化,雖說動作遠不如引入ssa這么大。
3、GC性能
GC的性能一直是廣大Gopher密切關注的事情,Go 1.10在減少內存分配延遲以及GC運行時的負擔兩個方面做了許多工作,但從整體上來看,Go 1.10并沒有引入傳說中的 TOC(Transaction Oritented Collector) ,因此宏觀上來看,GC變化不是很大。推ter上的GC性能測試“專家” Brian Hatfield 在對Go 1.10rc1的GC測試后,也表示與Go 1.9相比,變化不是很顯著。
六、小結
Go 1.10版本又是一個Go team和Gopher社區共同努力的結果,讓全世界Gopher都對Go保持著極大的熱情和期望。當然Go 1.10中的變化還有許多許多,諸如:
- 對Unicode規范的支持升級到 10.0 ;
- 在不同的平臺上,Assembler支持更多高性能的指令;
- plugin支持darwin/amd64等;
- gofmt、go doc在輸出格式上進一步優化和提升gopher開發者體驗;
- cgo支持直接傳遞go string到C代碼中;
… …
很多很多!這里限于篇幅原因,不能一一詳解了。通讀一遍 Go 1.10 Release Note 是每個Gopher都應該做的。
以上驗證在mac OS X, go 1.10rc2上測試,demo源碼可以在 這里下載 。
七、參考資料
- Go 1.10 Release Notes
- The State of Go by campoy
- Go 1.10
- pprof user interface
- cmd/go content-based staleness submitted
- Go 1.10 cmd/go: build cache, test cache, go install, go vet, test vet
來自:http://tonybai.com/2018/02/17/some-changes-in-go-1-10/