昨天,go 终于发布了 1.18 的 beta 版本, 带来了大家期待已久的泛型,抓紧时间康康能不能赶上热乎的。
根据社区昨天发的 Go 1.18 Beta 1 is available, with generics 这次版本更新主要带来的新功能有:
go version -m
可以记录构建细节在没有泛型之前,假设我们需要求两个数的和,根据运算数的类型,可能需要写很多个函数,如:
package main
func SumInt64(a, b int64) int64 {return a + b}
func SumFloat64(a, b int64) float64 {return a + b}
有了泛型之后就可以这样写了:
package main
func 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 main
import "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 V
v = 10.0
return 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 main
import "fmt"
func Sum[K comparable, V int64 | float64](m map[K]V) V {
var sum V
for k, v := range m {
sum += v
fmt.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
即可进行模糊测试,用法和普通测试差不多,如果有需要请移步官方文档
这是非常爽的一个功能,想想这样一个场景,为了方便测试,你需要要改某一个功能(有时可能只是一个数值),但这个功能是一个单独的模块,通过 mod 引入,所以你下载了这个包,并用 replace
将其替换成了本地的路径,就像:
module go1.18.1-beta
go 1.18
replace (
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.18
use ./.
在 go.work
中我们可以使用 replace
:
go 1.18
use ./.
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 work
go 1.18
replace (
github.com/520MianXiangDuiXiang520/a => ./a
github.com/520MianXiangDuiXiang520/b => ./b
c => ./c
)
require (
github.com/520MianXiangDuiXiang520/a v0.0.0-00010101000000-000000000000
github.com/520MianXiangDuiXiang520/b v0.0.0-00010101000000-000000000000
c v0.0.0-00010101000000-000000000000
)
使用 workspace mode 后:
cd ~/project/go-beta/work
go work init . ./a ./b ./c
go mod 中可以只写:
module work
go 1.18
因为他们在同一个工作目录下
在 go 1.17 时就针对 X86-64
的处理器增加了这个,据说函数调用性能能提斯 20%,现在拓展到了 arm64 和 PPC64 但我没有这种处理器的电脑,不过可以对比一下旧版的函数调用方式:
package main
func demo(a int64, b int32, c int16, d int8) (int64, int32, int16, int8) {
a += 111
b += 222
c += 333
d += 89
return 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=0x0
0x0000 00000 (E:go汇编\01.go:3) TEXT "".demo(SB), NOSPLIT|ABIInternal, $0-32
0x0000 00000 (E:go汇编\01.go:3) PCDATA $0, $-2
0x0000 00000 (E:go汇编\01.go:3) PCDATA $1, $-2
0x0000 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, $0
0x0000 00000 (E:go汇编\01.go:4) PCDATA $1, $0
0x0000 00000 (E:go汇编\01.go:4) MOVQ "".a+8(SP), AX
0x0005 00005 (E:go汇编\01.go:4) ADDQ $111, AX
0x0009 00009 (E:go汇编\01.go:8) MOVQ AX, "".~r4+24(SP)
0x000e 00014 (E:go汇编\01.go:5) MOVL "".b+16(SP), AX
0x0012 00018 (E:\桌面文件\笔记\Note\g o\go汇编\01.go:5) ADDL $222, AX
0x0017 00023 (E:go汇编\01.go:8) MOVL AX, "".~r5+32(SP)
0x001b 00027 (E:go汇编\01.go:6) MOVWLZX "".c+20(SP), AX
0x0020 00032 (E:go汇编\01.go:6) ADDL $333, AX
0x0025 00037 (E:go汇编\01.go:8) MOVW AX, "".~r6+36(SP)
0x002a 00042 (E:go汇编\01.go:7) MOVBLZX "".d+22(SP), AX
0x002f 00047 (E:go汇编\01.go:7) ADDL $89, AX
0x0032 00050 (E:go汇编\01.go:8) MOVB AL, "".~r7+38(SP)
0x0036 00054 (E:go汇编\01.go:8) RET
0x0000 48 8b 44 24 08 48 83 c0 6f 48 89 44 24 18 8b 44 H.D$.H..oH.D$..D
0x0010 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=0x0
0x0000 00000 (E:\add.go:3) TEXT "".demo(SB), NOSPLIT|ABIInternal, $0-16
0x0000 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, $1
0x0000 00000 (E:\add.go:4) ADDQ $111, AX
0x0004 00004 (E:\add.go:5) ADDL $222, BX
0x000a 00010 (E:\add.go:6) ADDL $333, CX
0x0010 00016 (E:\add.go:7) ADDL $89, DI
0x0013 00019 (E:\add.go:8) RET
0x0000 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=0x0
0x0000 00000 (E:\add.go:3) TEXT "".demo(SB), NOSPLIT|ABIInternal, $0-72
0x0000 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, $1
0x0000 00000 (E:\add.go:14) MOVQ "".j+8(SP), DX
0x0005 00005 (E:\add.go:14) ADDQ $787, DX
0x000c 00012 (E:\add.go:16) MOVQ DX, "".~r9+16(SP)
0x0011 00017 (E:\add.go:5) ADDQ $111, AX
0x0015 00021 (E:\add.go:6) ADDL $222, BX
0x001b 00027 (E:\add.go:7) ADDL $333, CX
0x0021 00033 (E:\add.go:8) ADDL $89, DI
0x0024 00036 (E:\add.go:9) ADDQ $99, SI
0x0028 00040 (E:\add.go:10) ADDQ $88, R8
0x002c 00044 (E:\add.go:11) ADDQ $999, R9
0x0033 00051 (E:\add.go:12) ADDQ $898, R10
0x003a 00058 (E:\add.go:13) ADDQ $989, R11
0x0041 00065 (E:\add.go:16) RET
0x0000 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 版本
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.18beta1
path command-line-arguments
build -compiler=gc
build -gcflags=-l -S
build CGO_ENABLED=1
build CGO_CFLAGS=
build CGO_CPPFLAGS=
build CGO_CXXFLAGS=
build CGO_LDFLAGS=
build GOARCH=amd64
build GOOS=windows
build GOAMD64=v1
Go 1.18 Beta 1 is available, with generics