Go1.18 generics, fuzzing, workspace

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
}

SumIntsIt is SumFloatsalmost the same as , except that the type of s is different, but it needs to be handled separately.

type parameter

Now: Go1.18Generics 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 Kis comparable, this type can perform comparison operations of equal and not equal, and the key of the map requires this type. while the type Vis int64or 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 anycan 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 ~stringconstraints 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:

Screenshot 2022-03-16 11.07.58

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 Fuzzstarts 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.Addgive 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 testcommand, but only to ensure that the seed example can pass:

$ go test -run=FuzzReverse
PASS
ok      example/fuzz  0.013s

Adding a new -fuzzflag 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 ./testdatain 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 testthe 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 []runehandle 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=Fuzzthe test will continue until you press to ctrl-Cstop:

$ 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-introand murecom-verse-1, which I'm developing right now, murecom-chorus-1needs to call the first two modules.

intro
verse-1
chorus-1

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 -uAfter 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 tidyupdate 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 workwill create a go.workfile similar to go.mod:

go 1.18

use (
    ./chorus-1
    ./intro
    ./verse-1
)

Now, the imported in chorus-1 github.com/cdfmlr/murecom-introwill automatically use the local ./introsource 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)

Guess you like

Origin blog.csdn.net/u012419550/article/details/123544091