Golang1.18新特性

golang1.18已经发布一年多了,目前最新的版本是1.19,但作为生产,需保持谨慎态度,目前把项目的版本从1.16升级到了1.18,同时针对1.1.8的新特性做了一下记录,当然1.1.8更新了不少内容,本文只提炼了一些本人关注的特性。

1. 泛型

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]elems []velems[]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, "-"))
}
GenericsJoinGenericsJoin

声明泛型类型

GenericsJoin[v int | uint64| int32](elems []v, sep string)
type Numeric interface {
	int | uint64 | int64 | uint | int32
}
interfaceinterface|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()
}
NumericNumericNumerictype myInt32 int32Numeric~
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, "-"))
}
anyinterface{}comparablebool、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库,这里就不再展开,有兴趣的同学可以继续完善。

2. netip

net/netipAddrnet.IPPrefixnet/IPNetnetip.Addrcomparable

我们来编写两个性能测试例子,来对比新的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以后再进行位移,因此效率也会更低。

3. fuzzing测试

1.18新增单元测试fuzzing,其目的是通过随机生成的数据进行测试,找出单元测试(unit test)覆盖不到的场景,进而发现潜在的BUG和安全漏洞。

使用说明

FuzzXXXa *testing.F_test.gostring, 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)
		}
	})
}
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
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支持的参数只是一些基础类型,还不够完善,待到完善以后将会给程序的安全以及稳定性带来显著提升。

4.命令变化

go get命令已弃用,并且被go install替代

#old command
go get example.com/cmd

#new command
go install exmple.com/cmd@latest

go work

replace
# 初始化工作空间
go work init xxx
# 将单个module引入工作空间
go work use xxx
# 将所有的module引入工作空间
go work use -r
# 同步工作空间的module
go work sync