Go 1.18
Let's learn some new features of Go 1.18.
generic
Generics: Parameterized types
once upon a time
It used to be like this:
package main
import "fmt"
func main() {
ints := map[string]int64{
"first": 34,
"second": 12,
}
floats := map[string]float64{
"first": 35.98,
"second": 26.99,
}
fmt.Printf("Non-Generic Sums: %v and %v\n",
SumInts(ints),
SumFloats(floats))
}
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
SumInts
It is SumFloats
almost the same as , except that the type of s is different, but it needs to be handled separately.
type parameter
Now: Go1.18
Generics support type parameters , which are similar to ordinary parameters, but what is specified at the time of definition is not the type but the generic constraint, and the call is passed to the person instead of the value but the actual type.
When defining a function, you can include type parameters in square brackets:
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
The specified type K
is comparable
, this type can perform comparison operations of equal and not equal, and the key of the map requires this type. while the type V
is int64
or float64
.
When calling this generic function, pass in the actual type of the type parameter—that is, some definite type—in square brackets:
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
Type parameter inference
In many cases (but not all the time), Go can automatically infer the actual type of the generic, in which case you can omit the square brackets and pass in the actual type:
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))
Define type constraints
If the generic constraints are too long, such as to support all numeric types, writing a large list directly in the function definition is not elegant, and it is not convenient for reuse. We can define the type constraints separately:
type Number interface {
int64 | float64
}
The type constraint is also defined with the interface keyword, which contains the type and is separated by a vertical bar.
The defined constraints can then be used when defining type parameters:
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
For convenience, Go1.18 has built-in some constraint definitions, such as K's comparable
.
Also, for example, a new any
can be used in place of the original interface{}
, representing any value.
derived type constraints
Go can derive new types from existing types:
type MyString string
Use ~string
constraints to represent string or the underlying type of string:
func addString[T ~string](x, y T) T {
return x + y
}
You can even pass string and MyString mixed together addString
:
func main() {
var x string = "hello "
var y MyString = "world "
fmt.Println(x, x)
fmt.Println(y, y)
fmt.Println(x, y)
}
operation result:
fuzz testing
Fuzzing: Random fake data brute force test
We write a function like this classic example:
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)
}
(Note: Now you can even use generics to write a ReverseSlice that supports various slices)
Then do unit tests:
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)
}
}
}
You need to manually give a group testcases
, the test examples are very limited, boundary conditions may be missed, and it is easy to encourage your own arrogance for test-oriented programming. (And sometimes handwritten test cases will also calculate the results wrongly)
fuzz testing
Now we can use fuzz testing, the test function Fuzz
starts with and passes in parameters *testing.F
:
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)
t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d",
utf8.RuneCountInString(orig),
utf8.RuneCountInString(rev),
utf8.RuneCountInString(doubleRev))
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 will generate many test cases by itself, you just f.Add
give it a little seed.
But because the input is uncertain, the output cannot be given directly, so some other properties are used here for testing:
- Reverse the string twice to get the original string
- The reversed string should contain all valid UTF-8 characters.
Run the fuzz test
We will also run the FuzzXxx function with the normal go test
command, but only to ensure that the seed example can pass:
$ go test -run=FuzzReverse
PASS
ok example/fuzz 0.013s
Adding a new -fuzz
flag will randomly generate test cases:
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 4 workers
fuzz: minimizing 29-byte failing input file
fuzz: elapsed: 0s, minimizing
--- FAIL: FuzzReverse (0.04s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:36: Reverse produced invalid UTF-8 string "\xa1\xc5"
Failing input written to testdata/fuzz/FuzzReverse/d94f98fa6f5ba011d641a510bbc5227832bc587341ac3bb28e2c883976dedf8f
To re-run:
go test -run=FuzzReverse/d94f98fa6f5ba011d641a510bbc5227832bc587341ac3bb28e2c883976dedf8f
FAIL
exit status 1
FAIL example/fuzz 0.410s
If it fails, Go will put randomly generated test cases ./testdata
in the directory:
.
├── go.mod
├── reverse.go
├── reverse_test.go
└── testdata
└── fuzz
└── FuzzReverse
└── d94f98fa6...8f
You can open Kang, which is a fancy character:
$ cat testdata/fuzz/FuzzReverse/d94f98fa6...8f
go test fuzz v1
string("š")
Once an error occurs, if you use go test
the mark without fuzzing in time, it will test the failed example again.
$ go test
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/d94f98fa6f5ba011d641a510bbc5227832bc587341ac3bb28e2c883976dedf8f (0.00s)
reverse_test.go:36: Reverse produced invalid UTF-8 string "\xa1\xc5"
FAIL
exit status 1
FAIL example/fuzz 0.807s
In fact, this bug only needs to change one line of code to []rune
handle UTF-8 characters. But as a special case, if the string entered at the beginning is not Unicode, you need to report an error and leave early:
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)
}
Since the function signature has been changed, the test function should also be changed accordingly. Terminate the test early if the random case is not Unicode:
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)
}
})
}
Test again, the use case that failed before can pass:
$ go test
PASS
ok example/fuzz 1.103s
In this fuzz test, if no problems are encountered, go test -fuzz=Fuzz
the test will continue until you press to ctrl-C
stop:
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/16 completed
fuzz: elapsed: 0s, gathering baseline coverage: 16/16 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 239347 (79756/sec), new interesting: 25 (total: 41)
fuzz: elapsed: 6s, execs: 541626 (100763/sec), new interesting: 30 (total: 46)
fuzz: elapsed: 9s, execs: 809428 (89265/sec), new interesting: 32 (total: 48)
fuzz: elapsed: 12s, execs: 1104523 (98381/sec), new interesting: 32 (total: 48)
fuzz: elapsed: 15s, execs: 1374247 (89894/sec), new interesting: 33 (total: 49)
fuzz: elapsed: 18s, execs: 1645978 (90597/sec), new interesting: 33 (total: 49)
^Cfuzz: elapsed: 18s, execs: 1650306 (55831/sec), new interesting: 33 (total: 49)
PASS
ok example/fuzz 18.622s
Or you can add a parameter to specify how long to run the fuzz test:
$ go test -fuzz=Fuzz -fuzztime 30s
(Note:testing.?
- T: Test unit test
- B: Benchmark benchmark test
- F: Fuzz fuzz testing)
work area
Workspaces: Develop multiple modules at the same time
I just came across this thing recently. I'm working on something called murecom-xxx
, and currently have implemented two separate modules: murecom-intro
and murecom-verse-1
, which I'm developing right now, murecom-chorus-1
needs to call the first two modules.
In the process of developing chorus-1, for example, I may find that there is a problem in the intro and need to be changed.
go get -u
After the change, I need to submit the version, push to github, and then re -pull the latest version in chorus-1 , and then go mod tidy
update the dependencies. It's very troublesome.
And Go1.18 can simplify this process:
$ ls
intro verse-1 chorus-1
$ go work init ./chorus-1
$ go work use ./intro
$ go work use ./verse-1
go work
will create a go.work
file similar to go.mod
:
go 1.18
use (
./chorus-1
./intro
./verse-1
)
Now, the imported in chorus-1 github.com/cdfmlr/murecom-intro
will automatically use the local ./intro
source code, and there is no need to modify => submit => push => pull =>... a lot of processes. This is convenient for multi-module projects, which can be coordinated locally, debugged and then submitted for push.
(Note: go.work does not need to be submitted to git, this is just for the convenience of the local development environment)