golang1.18已经发布一年多了,目前最新的版本是1.19,但作为生产,需保持谨慎态度,目前把项目的版本从1.16升级到了1.18,同时针对1.1.8的新特性做了一下记录,当然1.1.8更新了不少内容,本文只提炼了一些本人关注的特性。
golang1.18就加入泛型,通过泛型的支持,将减少很多代码量,但也会带来不少可读性问题,因此泛型在生产中仍然需要谨慎使用,特别是目前golang的泛型还比较基础。
我们来看一个例子,我们需要写一个方法,拼接数值为字符串,类似strings.Join方法,将数值切片通过分隔符组合长字符串
func JoinInt(elems []int, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return strconv.Itoa(elems[0])
}
var b strings.Builder
b.WriteString(strconv.Itoa(elems[0]))
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(strconv.Itoa(s))
}
return b.String()
}
这是支持Int类型的切片方法,但是此时我们如果要支持uint64类型的数值,该方法就不支持了,只有再写两个方法
func JoinUint64(elems []uint64, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return strconv.FormatUint(elems[0], 10)
}
var b strings.Builder
b.WriteString(strconv.FormatUint(elems[0], 10))
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(strconv.FormatUint(s, 10))
}
return b.String()
}
此时的调用方法需要针对不同类型调用不同的方法:
func TestNormalJoin(t *testing.T) {
var intArr = []int{1, 2, 3, 4, 5}
var uint64Arr = []uint64{1, 2, 3, 4, 5}
fmt.Println("join int string", JoinInt(intArr, "-"))
fmt.Println("join uint64 string", JoinUint64(uint64Arr, "-"))
}
我们再来看看针对上面问题,使用泛型带来的便捷。
func GenericsJoin[v int | uint64| int32](elems []v, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return strconv.Itoa(int(elems[0]))
}
var b strings.Builder
b.Grow(len(elems))
b.WriteString(strconv.Itoa(int(elems[0])))
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(strconv.Itoa(int(s)))
}
return b.String()
}
我们看到在函数名称GenericsJoin
后面紧跟一个大括号[v int | uint64| int32]
,里面填的是函数支持的类型,即变量v可以是int、uint64、int32中的任意一种类型;入参elems []v
则表示elems
支持的是v类型的切片。此时传入的切换可以是[]int、[]uint64、[]int32
中的任意一种即可。我们再来看调用方法:
func TestGenericsJoin(t *testing.T) {
var intArr = []int{1, 2, 3, 4, 5}
var uint64Arr = []uint64{1, 2, 3, 4, 5}
fmt.Println("generics join int string", GenericsJoin(intArr, "-"))
fmt.Println("generics join uint64 string", GenericsJoin(uint64Arr, "-"))
}
我们发现,调用者只需要调用GenericsJoin
方法即可,虽然是两个类型的切片,但是调用GenericsJoin
都是可以的。这就减少了大量的重复代码,起到易用的作用。
我们看到前面函数GenericsJoin[v int | uint64| int32](elems []v, sep string)
上支持了三种类型,那如果我们要支持更多的类型,岂不是该函数会非常的长,并且不好维护,达不到不用的效果,例如其他的函数也要支持这样的类型。这时候可以声明泛型类型,格式如下:
type Numeric interface {
int | uint64 | int64 | uint | int32
}
Numeric的类型是interface
,但是内部元素跟原来的interface
有一些区别,内部通过|
来分割类型,表明Numeric支持这些类型,
这样在函数调用的时候就不用单独声明每个数值类型了,我们将原来的GenericsJoin
函数调整一下:
func GenericsJoinNumeric[v Numeric](elems []v, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return strconv.Itoa(int(elems[0]))
}
var b strings.Builder
b.Grow(len(elems))
b.WriteString(strconv.Itoa(int(elems[0])))
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(strconv.Itoa(int(s)))
}
return b.String()
}
这样把Numeric
当作函数的入参,调用方也不需要调整,这样如果后续需要支持更多的类型,只需要在Numeric
内添加即可。当然此时Numeric
只支持基础类型,而如果对基础类型进行引用就不支持了,例如我们声明类型type myInt32 int32
,就无法使用,这时候可以将Numeric
进行简单改造即可,在int32前面添加~
表示支持int32的所有类型:
type Numeric interface {
int | uint64 | int64 | uint | ~int32
}
//调用
func TestGenericsJoinNumeric(t *testing.T) {
var intArr = []int{1, 2, 3, 4, 5}
var uint64Arr = []uint64{1, 2, 3, 4, 5}
type myInt32 int32
var myInt32Arr = []myInt32{1, 2, 3, 4, 5}
fmt.Println("generics join int string", GenericsJoinNumeric(intArr, "-"))
fmt.Println("generics join uint64 string", GenericsJoinNumeric(uint64Arr, "-"))
fmt.Println("generics join int32 string", GenericsJoinNumeric(myInt32Arr, "-"))
}
当然1.18还新增了any
类型,该类型其实就是空接口interface{}
的别名。同时新增一个comparable
类型表示课比较类型,包括bool、number、string、pointer、channel
等可比较的类型。
泛型可以与interface结合处很多高级用法,例如数据库操作,可以声明一个接口类型,支持任意类型,该类型只要实现了Scan方法即可。
type DBScanner[T any] interface {
*T
Scan() []any
}
此时声明一个Student的表接口,并且实现Scan方法:
type Student struct {
ID string
CreateTime time.Time
Name string
Age int
Address net.IP
}
func (s *Student) Scan() []any {
return []any{&s.ID, &s.CreateTime, &s.Name, &s.Age, &s.Address}
}
编写数据库查询方法:
import (
"context"
"github.com/jackc/pgx/v5"
)
func QueryGenericsScan[T any, ptr DBScanner[T]](
ctx context.Context,
tx pgx.Tx,
sql string, args []interface{}) ([]T, error) {
return QueryGenerics[T](ctx, tx, func(p ptr) []any {
return p.Scan()
}, sql, args)
}
func QueryGenerics[T any, entity interface{ *T }](
ctx context.Context,
tx pgx.Tx,
scanFun func(entity) []any,
sql string, args []interface{}) ([]T, error) {
rows, err := tx.Query(ctx, sql, args...)
if err != nil {
return nil, err
}
defer rows.Close()
results := make([]T, 0, len(rows.RawValues()))
for rows.Next() {
var result T
if err := rows.Scan(scanFun(&result)...); err != nil {
return nil, err
}
results = append(results, result)
}
return results, nil
}
调用查询student数据方法:
func QueryStudent() error {
pool, err := pgxpool.New(context.Background(), ConnStr)
if err != nil {
return err
}
defer pool.Close()
tx, _ := pool.Begin(context.TODO())
defer tx.Commit(context.TODO())
result, err := QueryGenericsScan[Student](context.TODO(), tx, "select * from gr_student", []interface{}{})
if err != nil {
return err
}
fmt.Printf("len:%d \n", len(result))
return nil
}
这样就避免了通过反射以适配不同的表结构。当然可以在目前的基础上继续封装成一个完整的orm库,这里就不再展开,有兴趣的同学可以继续完善。
golang1.18版本新增一个标准库net/netip
,并增加新IP地址类型Addr
对标原来的net.IP
类型,以及子网Prefix
类型对标原来的net/IPNet
。并且netip.Addr
类型占用更少的内容,其内容是不可改变的,并且支持comparable
可以作为map的key使用。
我们来编写两个性能测试例子,来对比新的neip带来的性能提升
func BenchmarkParseIP(b *testing.B) {
for i := 0; i < b.N; i++ {
net.ParseIP("2001::128")
}
}
func BenchmarkParseAddr(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := netip.ParseAddr("2001::128"); err != nil {
b.Error(err)
}
}
}
func BenchmarkParseCIDR(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _, err := net.ParseCIDR("2001::128/128")
if err != nil {
b.Error(err)
}
}
}
func BenchmarkParseParsePrefix(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := netip.ParsePrefix("2001::128/128"); err != nil {
b.Error(err)
}
}
}
执行性能测试命令go test -v -bench=Parse -run=^# -benchmem
,得到如下执行结果
➜ netip go test -v -bench=Parse -run=^# -benchmem
goos: darwin
goarch: amd64
pkg: gomoduletest/ip/netip
cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
BenchmarkParseIP
BenchmarkParseIP-8 16439040 70.06 ns/op 16 B/op 1 allocs/op
BenchmarkParseAddr
BenchmarkParseAddr-8 27164858 43.28 ns/op 0 B/op 0 allocs/op
BenchmarkParseCIDR
BenchmarkParseCIDR-8 4986140 232.6 ns/op 96 B/op 4 allocs/op
BenchmarkParseParsePrefix
BenchmarkParseParsePrefix-8 19405418 59.40 ns/op 0 B/op 0 allocs/op
PASS
ok gomoduletest/ip/netip 5.514s
可以看到netip解析子网和解析ip的性能明显高于net库,并且没有内存分配。
另外,原来的net库并没有提供IP的偏移方法,导致计算ipv4和ipv6的递增位时都需要先转换成数值类型,然后利用位相加运算实现增减以后再转换成net.IP类型,这就非常麻烦,而新的netip提供了方法Next和Prev来实现了IP的增减,我们看如下例子:
func IpV6ToBigInt(ip net.IP) *big.Int {
value := big.NewInt(0)
for _, b := range ip {
value.Lsh(value, 8)
value.Or(value, big.NewInt(int64(b)))
}
return value
}
func BenchmarkAddNet(b *testing.B) {
bigValue := IpV6ToBigInt(net.ParseIP("2001:1000::120"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
bigValue.Add(bigValue, big.NewInt(1))
}
}
func BenchmarkAddNetip(b *testing.B) {
addr, _ := netip.ParseAddr("2001:1000::120")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = addr.Next()
}
}
运行基准测试以后的结果如下:
go test -v -bench=AddNet -run=^# -benchmem
goarch: amd64
pkg: gomoduletest/ip/netip
cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
BenchmarkAddNet
BenchmarkAddNet-8 28256738 39.65 ns/op 8 B/op 1 allocs/op
BenchmarkAddNetip
BenchmarkAddNetip-8 944860983 1.264 ns/op 0 B/op 0 allocs/op
PASS
ok gomoduletest/ip/netip 2.616s
可以看到netip的IP位增是非常方便的,原来的net库需要将ip转换成big.Int以后再进行位移,因此效率也会更低。
1.18新增单元测试fuzzing,其目的是通过随机生成的数据进行测试,找出单元测试(unit test)覆盖不到的场景,进而发现潜在的BUG和安全漏洞。
FuzzXXX
格式,并且入参是a *testing.F
类型,且函数无返回值_test.go
结尾string, bool,[]byte,int, int8, int16, int32/rune,int64,uint, uint8/byte, uint16, uint32, uint64,float32, float64
~ go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
Failing input written to testdata/fuzz/FuzzReverse/ce2b04545793f5e742b110aaf8543b98bcebeb8c0833965f09b77b499eec3395
FAIL
exit status 1
FAIL gomoduletest/test/fuzz 0.698s
#携带参数
go test -fuzz={FuzzTestName}
#-fuzztime: 设定模糊测试运行的总时长,默认是无时间限制
#-fuzzminimizetime: 运行模糊测试最小尝试时间,默认是60秒,可以设定-fuzzminimizetime 0来禁用最小时间
#-parallel: 并行模糊测试的内核数量,默认是$GOMAXPROCS
#case
go test -fuzz=Fuzz -fuzztime 30s
f.Add
函数进行添加,该阶段不会生成随机数据。首先编写一段Reverse
函数,函数的作用是进行反转字符串
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
编写常规的单元测试
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
显然我们的单元测试用例并没有完全覆盖,而一些未知的例子我,我们无法穷举,因此加入fuzz模糊测试来辅助完善测试用例
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
在fuzz测试中加入单元测试的例子,然后执行命令go test -fuzz=FuzzReverse
,此时会输出如下内容
➜ fuzz go test -fuzz=FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/12 completed
fuzz: elapsed: 0s, gathering baseline coverage: 12/12 completed, now fuzzing with 8 workers
fuzz: minimizing 33-byte failing input file
fuzz: elapsed: 0s, minimizing
--- FAIL: FuzzReverse (0.04s)
--- FAIL: FuzzReverse (0.00s)
fuzz_test.go:37: Reverse produced invalid UTF-8 string "\xad\xc7"
Failing input written to testdata/fuzz/FuzzReverse/31f82ffa1dd61be6ed134da15613dcf7d36ad04dd7bffeb65730b2817f38a122
To re-run:
go test -run=FuzzReverse/31f82ffa1dd61be6ed134da15613dcf7d36ad04dd7bffeb65730b2817f38a122
FAIL
exit status 1
FAIL gomoduletest/test/fuzz 0.724s
表示执行测试失败,并且fuzz已经将失败的例子放入testdata/fuzz/FuzzReverse/31f82ffa1dd61be6ed134da15613dcf7d36ad04dd7bffeb65730b2817f38a122
,就在当前目录下,打开该文件即可看到对应的失败用例
go test fuzz v1
string("\U00056bdc")
该字符明显不是常规的格式,我们打印一下输入内容,然后执行go test -run=FuzzReverse/31f82ffa1dd61be6ed134da15613dcf7d36ad04dd7bffeb65730b2817f38a122
func Reverse(s string) string {
fmt.Printf("input: %q utf-8:%t \n", s, utf8.ValidString(s))
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
此时打印如下信息,说明在生成库的例子中存在中文和非utf8的格式,因此我们要对函数做出调整,支持utf8中文,但是非utf8类型的我们不支持,抛出异常
➜ fuzz go test -run=FuzzReverse/31f82ffa1dd61be6ed134da15613dcf7d36ad04dd7bffeb65730b2817f38a122
input: "\U00056bdc" utf-8:true
input: "\x9c\xaf\x96\xf1" utf-8:false
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/31f82ffa1dd61be6ed134da15613dcf7d36ad04dd7bffeb65730b2817f38a122 (0.00s)
fuzz_test.go:47: Reverse produced invalid UTF-8 string "\x9c\xaf\x96\xf1"
FAIL
exit status 1
FAIL gomoduletest/test/fuzz 0.188s
调整以后的函数如下
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
b := []rune(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b), nil
}
由于多了一个返回参数,因此需要调整单元测试以及模糊测试
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
fuzz模糊测试的作用是非常巨大的,可以发现我们程序中潜在的BUG,特别是人为的测试用例是无法覆盖完全的,借助fuzz可以起到很好的辅助作用,但是目前fuzz支持的参数只是一些基础类型,还不够完善,待到完善以后将会给程序的安全以及稳定性带来显著提升。
#old command
go get example.com/cmd
#new command
go install exmple.com/cmd@latest
新增work工作空间,类似与go.mod,不过这个工作空间可以用于切换本地与远程的调试。特别是引用一些本地包的时候就可以使用。例如我们引用了utils内的方法,但是要修改utils的方法以后再使用utils,原来需要在go.mod内使用replace
进行调试,调试完毕以后还要修改回去。现在只需要使用go work即可。
# 初始化工作空间
go work init xxx
# 将单个module引入工作空间
go work use xxx
# 将所有的module引入工作空间
go work use -r
# 同步工作空间的module
go work sync