Go 1.18.1 Beta 嘗鮮
昨天,go 終于發布了 1.18 的 beta 版本, 帶來了大家期待已久的泛型,抓緊時間康康能不能趕上熱乎的。
下載地址
根據社區昨天發的 Go 1.18 Beta 1 is available, with generics 這次版本更新主要帶來的新功能有:
- 泛型
- 模糊測試( fuzzing-based tests)
- workspace mode
- arm64 和 PPC64 也增加了基于寄存器的調用規約
- 增加了一個
go version -m
可以記錄構建細節 - 其他,參見 draft release notes for Go 1.18
泛型
在沒有泛型之前,假設我們需要求兩個數的和,根據運算數的類型,可能需要寫很多個函數,如:
package mainfunc SumInt64(a, b int64) int64 {return a + b}func SumFloat64(a, b int64) float64 {return a + b}
有了泛型之后就可以這樣寫了:
package mainfunc Sum2[V int | int64 | float64 | int32 | float32](a, b V) V {return a + b
}
上面的代碼在 []
中聲明了一個泛型 V 它支持 int, int64, int32, float32, float64
五種類型,函數有兩個 V 類型的參數 a 和 b 此外函數返回值也是 V 類型
我還是挺好奇如果傳入的參數不是這五種會報什么錯:
//go:build go1.18
// +build go1.18
package mainimport "fmt"func Sum2[V int64 | float64 | int32 | float32](a, b V) V {return a + b
}func main() {fmt.Println(Sum2[int](1, 2))
}
編譯時報錯:
# go1.18.1-beta/1.18-beta/generic/generic
.\main.go:20:21: int does not implement int64|float64|int32|float32
注意,在調用 Sum2
時,我們使用 []
顯示地制定了 V
是 int
類型,在編譯器可以推斷類型時,這個是可以省略的,也就是可以寫作
func main() {fmt.Println(Sum2(1, 2))
}
但這并不是一直有用的,比如你要調用一個沒有參數的泛型函數時,如:
func PI[V int | float64]() V {var v Vv = 10.0return v
}func main() {// fmt.Println(PI()) // .\main.go:28:16: cannot infer V fmt.Println(PI[float64]()) // 10
}
此外,都知道 go map 的 key 要求是可比較的類型,因此,go 新增了一個關鍵字 comparable
表明泛型是一個可比較類型, 當泛型參數作為 map 的 key 時,它必須是可比較的。
//go:build go1.18
// +build go1.18
package mainimport "fmt"func Sum[K comparable, V int64 | float64](m map[K]V) V {var sum Vfor k, v := range m {sum += vfmt.Println(k)}return sum
}func main() {fmt.Println(Sum(map[int64]float64{1: 2.3, 2: 3.3}))
}
是不是覺得每次 int | int64 | float64 | int32 | float32
寫太麻煩了,確實,為此 go1.18 提供了泛型接口,你可以像定義接口一樣定義一個泛型類型,就像:
type Number interface {int | int8 | int16 | int32 | int64 | float32 | float64
}
在這之后,你就可以使用 Number
來代替這一長串了
模糊測試
模糊測試 (fuzz testing, fuzzing)是一種軟件測試技術。其核心思想是將自動或半自動生成的隨機數據輸入到一個程序中,并監視程序異常,如崩潰,斷言(assertion)失敗,以發現可能的程序錯誤,比如內存泄漏。模糊測試常常用于檢測軟件或計算機系統的安全漏洞。
—— wikipedia 模糊測試
可以看看官網的這個例子
func FuzzHex(f *testing.F) {for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {f.Add(seed)}f.Fuzz(func(t *testing.T, in []byte) {enc := hex.EncodeToString(in)out, err := hex.DecodeString(enc)if err != nil {t.Fatalf("%v: decode: %v", in, err)}if !bytes.Equal(in, out) {t.Fatalf("%v: not equal after round trip: %v", in, out)}})
}
運行 go test -fuzz=Fuzz
即可進行模糊測試,用法和普通測試差不多,如果有需要請移步官方文檔
workspace mode
這是非常爽的一個功能,想想這樣一個場景,為了方便測試,你需要要改某一個功能(有時可能只是一個數值),但這個功能是一個單獨的模塊,通過 mod 引入,所以你下載了這個包,并用 replace
將其替換成了本地的路徑,就像:
module go1.18.1-betago 1.18replace (github.com/json-iterator/go => /usr/bin/go/json-iterator/go
)
然后你就可以開心的改本地的模塊了,但問題在于你每次提交代碼時都需要回滾改過的 go.mod
否則大家就都用不了了……
workspace mode 就是解決了這樣的問題,它引入了一個 go.work
文件,你可以在項目目錄下執行 go work init .
來生成它,需要注意的是 workspace mode 只能用在 goMod 中,所以目錄下必須有 go.mod
才能生成 go.work
, 剛生成的文件內容類似:
go 1.18use ./.
在 go.work
中我們可以使用 replace
:
go 1.18use ./.replace (github.com/json-iterator/go => /usr/bin/go/json-iterator/go
)
go 會優先選擇 go.work
中的模塊,這樣你把 go.work
加入 .gitignore
就可以舒服地改代碼了
再看看上面的文件,事實上,在提案上,只有三個元素:
The
go.work
file has three directives: thego
directive, thedirectory
directive, and thereplace
directive.
在 beta 版中, directory
被改成了 use
, 這三個元素的作用是:
go
: 指明一個 go 版本use
: 將包含go.mod
文件的目錄的絕對或相對路徑作為參數。路徑的語法與replace
指令中的目錄替換相同。路徑必須是包含go.mod
文件的模塊目錄。go.work
文件必須至少包含一個use
指令。
use (./tools // golang.org/x/tools./mod // golang.org/x/mod
)
replace
: 與 go mod 中的一樣
可以簡單的理解為 go.work
聲明了一個工作目錄,這個目錄下的成員由 use
聲明,在工作目錄下執行構建時,會優先使用工作目錄下的組件。
看這個例子
cd ~/project/go-beta/work
mkdir a b c
cd a
go mod init github.com/520MianXiangDuiXiang520/a
cd ../b
go mod init github.com/520MianXiangDuiXiang520/b
cd ../c
go mod init c
cd ..
go mod init work
當 work 引用 a b 時,由于這兩個項目在 github 上不存在,所以之前只能使用 replace
:
module workgo 1.18replace (github.com/520MianXiangDuiXiang520/a => ./agithub.com/520MianXiangDuiXiang520/b => ./bc => ./c
)require (github.com/520MianXiangDuiXiang520/a v0.0.0-00010101000000-000000000000github.com/520MianXiangDuiXiang520/b v0.0.0-00010101000000-000000000000c v0.0.0-00010101000000-000000000000
)
使用 workspace mode 后:
cd ~/project/go-beta/work
go work init . ./a ./b ./c
go mod 中可以只寫:
module workgo 1.18
因為他們在同一個工作目錄下
基于寄存器的調用規約
在 go 1.17 時就針對 X86-64
的處理器增加了這個,據說函數調用性能能提斯 20%,現在拓展到了 arm64 和 PPC64 但我沒有這種處理器的電腦,不過可以對比一下舊版的函數調用方式:
package mainfunc demo(a int64, b int32, c int16, d int8) (int64, int32, int16, int8) {a += 111b += 222c += 333d += 89return a, b, c, d
}func main() {demo(0, 0, 0, 0)
}
在 go 1.14 的環境下,將上面的代碼編譯并輸出匯編代碼如下:
go build -gcflags="-l -S" main.go
"".demo STEXT nosplit size=55 args=0x20 locals=0x00x0000 00000 (E:go匯編\01.go:3) TEXT "".demo(SB), NOSPLIT|ABIInternal, $0-320x0000 00000 (E:go匯編\01.go:3) PCDATA $0, $-20x0000 00000 (E:go匯編\01.go:3) PCDATA $1, $-20x0000 00000 (E:go匯編\01.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (E:go匯編\01.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (E:go匯編\01.go:3) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (E:go匯編\01.go:4) PCDATA $0, $00x0000 00000 (E:go匯編\01.go:4) PCDATA $1, $00x0000 00000 (E:go匯編\01.go:4) MOVQ "".a+8(SP), AX0x0005 00005 (E:go匯編\01.go:4) ADDQ $111, AX0x0009 00009 (E:go匯編\01.go:8) MOVQ AX, "".~r4+24(SP)0x000e 00014 (E:go匯編\01.go:5) MOVL "".b+16(SP), AX0x0012 00018 (E:\桌面文件\筆記\Note\g o\go匯編\01.go:5) ADDL $222, AX0x0017 00023 (E:go匯編\01.go:8) MOVL AX, "".~r5+32(SP)0x001b 00027 (E:go匯編\01.go:6) MOVWLZX "".c+20(SP), AX0x0020 00032 (E:go匯編\01.go:6) ADDL $333, AX0x0025 00037 (E:go匯編\01.go:8) MOVW AX, "".~r6+36(SP)0x002a 00042 (E:go匯編\01.go:7) MOVBLZX "".d+22(SP), AX0x002f 00047 (E:go匯編\01.go:7) ADDL $89, AX0x0032 00050 (E:go匯編\01.go:8) MOVB AL, "".~r7+38(SP)0x0036 00054 (E:go匯編\01.go:8) RET0x0000 48 8b 44 24 08 48 83 c0 6f 48 89 44 24 18 8b 44 H.D$.H..oH.D$..D0x0010 24 10 05 de 00 00 00 89 44 24 20 0f b7 44 24 14 $.......D$ ..D$.0x0020 05 4d 01 00 00 66 89 44 24 24 0f b6 44 24 16 83 .M...f.D$$..D$..0x0030 c0 59 88 44 24 26 c3 .Y.D$&.
1.18 編譯結果如下:
# command-line-arguments
"".demo STEXT nosplit size=20 args=0x10 locals=0x0 funcid=0x0 align=0x00x0000 00000 (E:\add.go:3) TEXT "".demo(SB), NOSPLIT|ABIInternal, $0-160x0000 00000 (E:\add.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (E:\add.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (E:\add.go:3) FUNCDATA $5, "".demo.arginfo1(SB)0x0000 00000 (E:\add.go:3) FUNCDATA $6, "".demo.argliveinfo(SB)0x0000 00000 (E:\add.go:3) PCDATA $3, $10x0000 00000 (E:\add.go:4) ADDQ $111, AX0x0004 00004 (E:\add.go:5) ADDL $222, BX0x000a 00010 (E:\add.go:6) ADDL $333, CX0x0010 00016 (E:\add.go:7) ADDL $89, DI0x0013 00019 (E:\add.go:8) RET0x0000 48 83 c0 6f 81 c3 de 00 00 00 81 c1 4d 01 00 00 H..o........M...0x0010 83 c7 59 c3
結果一目了然吧,兩個都開了編譯優化 -N
1.14 用的完全是棧, 1.18 用了四個寄存器: AX BX CX DI
,那最多會用多少個寄存器呢?
# command-line-arguments
"".demo STEXT nosplit size=66 args=0x48 locals=0x0 funcid=0x0 align=0x00x0000 00000 (E:\add.go:3) TEXT "".demo(SB), NOSPLIT|ABIInternal, $0-720x0000 00000 (E:\add.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (E:\add.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (E:\add.go:3) FUNCDATA $5, "".demo.arginfo1(SB)0x0000 00000 (E:\add.go:3) FUNCDATA $6, "".demo.argliveinfo(SB)0x0000 00000 (E:\add.go:3) PCDATA $3, $10x0000 00000 (E:\add.go:14) MOVQ "".j+8(SP), DX0x0005 00005 (E:\add.go:14) ADDQ $787, DX0x000c 00012 (E:\add.go:16) MOVQ DX, "".~r9+16(SP)0x0011 00017 (E:\add.go:5) ADDQ $111, AX0x0015 00021 (E:\add.go:6) ADDL $222, BX0x001b 00027 (E:\add.go:7) ADDL $333, CX0x0021 00033 (E:\add.go:8) ADDL $89, DI0x0024 00036 (E:\add.go:9) ADDQ $99, SI0x0028 00040 (E:\add.go:10) ADDQ $88, R80x002c 00044 (E:\add.go:11) ADDQ $999, R90x0033 00051 (E:\add.go:12) ADDQ $898, R100x003a 00058 (E:\add.go:13) ADDQ $989, R110x0041 00065 (E:\add.go:16) RET0x0000 48 8b 54 24 08 48 81 c2 13 03 00 00 48 89 54 24 H.T$.H......H.T$0x0010 10 48 83 c0 6f 81 c3 de 00 00 00 81 c1 4d 01 00 .H..o........M..0x0020 00 83 c7 59 48 83 c6 63 49 83 c0 58 49 81 c1 e7 ...YH..cI..XI...0x0030 03 00 00 49 81 c2 82 03 00 00 49 81 c3 dd 03 00 ...I......I.....0x0040 00 c3
答案是 9 個 超出的部分會按順序放在棧上
go version
這個指令最基本的用法是查看 go 版本
E:\1.18-beta\as>go version
go version go1.18beta1 windows/amd64
但其實它還可以看 go 編譯產物的構建版本信息,這次增加了一個 -m
參數:
E:\1.18-beta\as>go version -m add.exe
add.exe: go1.18beta1path command-line-argumentsbuild -compiler=gcbuild -gcflags=-l -Sbuild CGO_ENABLED=1build CGO_CFLAGS=build CGO_CPPFLAGS=build CGO_CXXFLAGS=build CGO_LDFLAGS=build GOARCH=amd64build GOOS=windowsbuild GOAMD64=v1
參考
Go 1.18 Beta 1 is available, with generics
Tutorial: Getting started with generics
pkg.go.dev#Fuzzing
Proposal: Multi-Module Workspaces in cmd/go