On March 15, 2022, Google released the much-anticipated Golang 1.18, which brings several major new features.

  1. a workspace to solve some of the problems associated with developing multiple repositories locally at the same time
  2. a Fuzzing Test that automatically detects code branches, generates random input, and checks to see if the code panics
  3. generic support that many developers have been waiting for.

This article will briefly describe these three features.

Go Workspace Mode

Realistic situation

Multi-repository development

In practice, we often modify multiple modules that have dependencies at the same time, for example, if we implement a requirement on a service module, we also need to make changes to a common module of the project team, the whole workflow will look like this.

workflow

As you can see, every time you modify the Common library, you need to push the code to the remote end, then modify the dependency of the local service repository, and then pull the Common code from the remote end via go mod tidy, which is a lot of trouble.

Some people may ask, In this case, can’t we just add a replace clause to go.mod in the service repository?

However, if you use replace in go.mod, you need to pay extra mental cost in maintenance, and if the go.mod with replace is pushed to the remote code base, others will be confused.

Multiple new repositories start development

Suppose at this point I am developing two new modules, as follows.

1
2
code.byted.org/SomeNewProject/Common
code.byted.org/SomeNewProject/MyService

And MyService relies on Common.

During development, for various reasons, the code may not be pushed to the remote end immediately. So if I need to compile MyService locally, go build (or go mod tidy) will fail to automatically download the dependency because the Common library is not published to the codebase at all.

For the same reason as the “multiple repository simultaneous development” above, replace should not be added to MyService’s go.mod file either.

What is the Workspace Pattern

The Go workspace pattern first emerged from a proposal by Go developer Michael Matloob in April 2021 called “Multi-Module Workspaces in cmd/go”.

In this proposal, a new go.work file is created, and a series of local paths are specified in this file. The go modules under these local paths together form a workspace, and go commands can manipulate the go modules under these paths, and these go modules will be used first when compiling.

A workspace can be initialized and an empty go.work file generated with the following command.

1
go work init .

The contents of the newly generated go.work file are as follows.

1
2
3
go 1.18

directory ./.

In the go.work file, the directory indicates the module directories in the workspace, and when compiling code, modules under the same workspace are used first.

In go.work, there is also support for using replace to specify the local code base, but in most cases it is better to add the path of the dependent local code base to the directory.

Because go.work describes a local workspace, it cannot be committed to a remote repository. Although you can add this file to .gitignore, the most recommended approach is to use go.work in the upper level of the local repository.

For example, in the “multiple new repositories to start development” example above, let’s say that the local paths of my two repositories are as follows.

1
2
/Users/bytedance/dev/my_new_project/common
/Users/bytedance/dev/my_new_project/my_service

Then I can generate a go.work in the “/Users/bytedance/dev/my_new_project” directory with the following content.

1
2
3
4
5
6
7
8
/Users/bytedance/dev/my_new_project/go.work:

go 1.18

directory (
    ./common
    ./my_service
)

You can also organize multiple directories into a workspace by placing go.work in the top-level directory, and since the top-level directory itself is not managed by git, you don’t have to worry about gitignore or anything like that, which is a relatively painless way to go.

Points to note when using

Currently (go 1.18) only go build makes judgments about go.work, and go mod tidy does not care about Go workspaces.

Go Fuzzing Test

Why Golang supports fuzzing tests

Since 1.18, Fuzzing Test has been added to Golang’s testing standard library as a part of language security, for the obvious reason that security is an essential and increasingly important consideration for programmers building software.

Golang has so far provided a number of features and tools for language security, such as enforcing explicit type conversions, disabling implicit type conversions, checking for out-of-bounds access to arrays and slices, hashing dependency packages with go.sum, and more.

As we enter the cloud-native era, Golang has become one of the head languages for cloud-native infrastructure and services. The security requirements of these systems are naturally self-evident. Especially for user input, it is one of the basic requirements for these systems not to be handled exceptionally, crash, or be manipulated by user input.

This requires our systems to be stable when handling any user input, but traditional quality assurance tools, such as Code Review, static analysis, manual testing, Unit Test, and so on. In the face of increasingly complex systems, it is naturally impossible to exhaust all possible combinations of inputs, especially some very obscure corner cases.

Fuzzy testing is one of the best practices in the industry to solve this problem, so it is not hard to understand why Golang chooses to support it.

What is fuzzy testing

Fuzzy testing is a way to test a program by automatically constructing some random data as input to the program through a data construction engine, supplemented by some initial data that the developer can provide. Fuzzy testing can help developers find hard-to-find errors in stability, logic, and even security, especially as the system under test becomes more complex.

Instead of relying on a data set defined by the development tester, fuzzy testing is usually implemented as a set of random data constructed by the data construction engine itself. The fuzzy tests provide this data as input to the program under test and monitor it for panic, assertion failure, infinite loops, or any other exceptions. The data generated by the data construction engine is called corpus, and fuzzy tests are also a means of continuous testing, because if there is no limit on the number of executions or the maximum time of execution, it will keep on executing.

Golang’s fuzzing tests are implemented in the compiler toolchain, so they use an entry generation technique called “coverage guided fuzzing”, which runs roughly as follows.

Golang’s fuzzing tests

How Golang’s fuzzy tests are used

Golang’s fuzzy tests can simply be used directly when using them, or you can provide some initial corpus yourself.

The simplest practical example

The fuzzy test functions are also placed in xxx_test.go. To write the simplest fuzzy test example (with the obvious divide-by-0 error).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "testing"
import "fmt"

func FuzzDiv(f *testing.F) {
        f.Fuzz(func(t *testing.T, a, b int) {
                fmt.Println(a/b)
        })
}

As you can see, similar to unit tests, the function names for fuzz tests are in FuzzXxx format and accept a testing.F pointer object.

The fuzz test is then performed on the specified function using f.Fuzz in the function. The first argument to the function being tested must be of type *testing.T, and can be followed by any number of arguments of the basic type.

After writing, use the following command to start the fuzz test.

1
go test -fuzz .

The fuzz test will continue by default, as long as the function being tested is not panic or error free. The “-fuzztime” option can be used to limit the duration of the fuzz test.

1
go test -fuzztime 10s -fuzz .

When you test the above code with fuzzy test, you will encounter a panic situation, and the fuzzy test will output the following message.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
warning: starting with empty corpus
fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)
fuzz: elapsed: 0s, execs: 1 (65/sec), new interesting: 0 (total: 0)
--- FAIL: FuzzDiv (0.02s)
    --- FAIL: FuzzDiv (0.00s)
        testing.go:1349: panic: runtime error: integer divide by zero
            goroutine 11 [running]:
            runtime/debug.Stack()
                    /Users/bytedance/.mytools/go/src/runtime/debug/stack.go:24 +0x90
            testing.tRunner.func1()
                    /Users/bytedance/.mytools/go/src/testing/testing.go:1349 +0x1f2
            panic({0x1196b80, 0x12e3140})
                    /Users/bytedance/.mytools/go/src/runtime/panic.go:838 +0x207
            mydev/fuzz.FuzzDiv.func1(0x0?, 0x0?, 0x0?)
                    /Users/bytedance/Documents/dev_test/fuzz/main_test.go:8 +0x8c
            reflect.Value.call({0x11932a0?, 0x11cbf68?, 0x13?}, {0x11be123, 0x4}, {0xc000010420, 0x3, 0x4?})
                    /Users/bytedance/.mytools/go/src/reflect/value.go:556 +0x845
            reflect.Value.Call({0x11932a0?, 0x11cbf68?, 0x514?}, {0xc000010420, 0x3, 0x4})
                    /Users/bytedance/.mytools/go/src/reflect/value.go:339 +0xbf
            testing.(*F).Fuzz.func1.1(0x0?)
                    /Users/bytedance/.mytools/go/src/testing/fuzz.go:337 +0x231
            testing.tRunner(0xc000003a00, 0xc00007e3f0)
                    /Users/bytedance/.mytools/go/src/testing/testing.go:1439 +0x102
            created by testing.(*F).Fuzz.func1
                    /Users/bytedance/.mytools/go/src/testing/fuzz.go:324 +0x5b8


    Failing input written to testdata/fuzz/FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
    To re-run:
    go test -run=FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
FAIL
exit status 1
FAIL        mydev/fuzz        0.059s

Of which.

1
Failing input written to testdata/fuzz/FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c

This line indicates that the fuzzy test has saved the panic test input to this file, and tries to output the contents of this file at this time.

1
2
3
go test fuzz v1
int(-60)
int(0)

You can see the input that triggered the panic, and then we can check what is wrong with our code based on the input. Of course, this simple example is a deliberate write of a divide-by-0 error.

provides a custom corpus

Golang’s fuzz testing also allows developers to provide their own initial corpus, either through the “f.Add” method or by writing the corpus in the same format as the “Failing input” above, to You can also write the corpus in the same format as “Failing input” above, in “testdata/fuzz/FuzzXXX/custom corpus filename”.

Notes on use

Golang’s fuzzy tests currently only support these types of arguments for the function being tested.

1
2
[]byte, string, bool, byte, rune, float32, float64,
int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64

According to the documentation of the standard library, more type support will be added later.

Go’s Generics

Golang finally added support for generics in 1.18. With generics, we can write some public library code like this.

Old code (reflection).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func IsContainCommon(val interface{}, array interface{}) bool {
    switch reflect.TypeOf(array).Kind() {
    case reflect.Slice:
        lst := reflect.ValueOf(array)
        for index := 0; index < lst.Len(); index++ {
            if reflect.DeepEqual(val, lst.Index(index).Interface()) {
                return true
            }
        }
    }
    return false
}

New code (generic).

1
2
3
4
5
6
7
8
func IsContainCommon[T any](val T, array []T) bool {
    for _, item := range array {
        if reflect.DeepEqual(val, item) {
            return true
        }
    }
    return false
}

Generics add three important new features to Golang: 1.

  1. support for using Type parameters when defining functions and types
  2. redefine interface as a “collection of types”.
  3. generic type support type derivation

The following is a brief explanation of each of these elements.

Type Parameters

The list of type parameters is very similar to the list of function parameters, except that it uses square brackets.

1
2
3
4
5
6
func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

The above code defines a parameter type T for the Min function, which is very similar to template < typename T > in C++, except that in Golang, you can specify the “constraints” that it needs to satisfy for this parameter type “. In this example, the “constraint” used is constraints.Ordered

The function can then be used as follows.

1
2
x := Min[int](1, 2)
y := Min[float64](1.1, 2.2)

The process of specifying type parameters for a generic function is called “instantiation”, and the instantiated function can also be saved as a function object and used further.

1
2
f := Min[int64] // 这一步保存了一个实例化的函数对象
n := f(123, 456)

Similarly, custom types support generic types.

1
2
3
4
5
6
7
8
type TreeNode[T interface{}] struct {
    left, right *TreeNode[T]
    value T
}

func (t *TreeNode[T]) Find(x T) { ... }

var myBinaryTree TreeNode[int]

As in the above code, the struct type supports its own member variables holding the same generic type as itself when using generics.

Type Sets

Let’s go a little deeper into the “constraints” mentioned in the above example. The “int”, “float64”, and “int64” in the above example are actually passed as “parameters” when instantiated. is passed to the “type parameter list”. That is, [T constraints.Ordered] in the example above.

Just like passing a normal parameter requires verifying the type of the parameter, passing a type parameter requires verifying the type of the parameter being passed, to check if the type being passed meets the requirements.

For example, in the example above, when instantiating the Min function with the types “int”, “float64”, and “int64”, the compiler will check whether these parameters satisfy the constraint “constraints.Ordered”. Ordered” constraint, which describes the set of all types that can be compared using “<”, and is itself an interface.

In Go’s generics, a type constraint must be an interface, and the “traditional” Golang definition of an interface is “an interface that defines a collection of methods”, and any type that implements this collection of methods implements the interface. Any type that implements this set of methods implements this interface.

go Type Sets

But here’s where the problem arises: the “<” comparison is clearly not a method (there are no C++ operator overloads in Go), and the constraints.Ordered describing the constraint is indeed an interface itself.

So starting with 1.18, Golang redefines an interface as “a collection of types”. As previously thought about interfaces, an interface can also be thought of as “a collection of all types that implement the set of methods of the interface”.

go interface

The two views are actually similar, but the latter is obviously more flexible, specifying a set of concrete types as an interface directly, even if those types don’t have any methods.

For example, in 1.18, an interface can be defined like this.

1
2
3
type MyInterface interface {
    int|bool|string
}

This definition means that int/bool/string can be used as MyInterface.

Ordered, which is actually defined as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type Ordered interface {
    Integer|Float|~string
}

type Float interface {
    ~float32|~float64
}

type Integer interface {
    Signed|Unsigned
}

type Signed interface {
    ~int|~int8|~int16|~int32|~int64
}

type Unsigned interface {
    ~uint|~uint8|~uint16|~uint32|~uint64
}

where the leading “~” symbol means “any underlying type is the type of the type that follows”, e.g.

1
type MyString string

The MyString defined in this way satisfies the “~string” type constraint.

Type Inference

Finally, type inference, which is common to all languages that support generics, is not absent. Type Inference allows users to call generic functions without specifying all type parameters. An example is the following function.

1
2
3
4
5
6
7
8
9
// 将F类型的slice变换为T类型的slice
// 关键字 any 等同于 interface{}
func Map[F, T any](src []F, f func(F) T) []T {
    ret := make([]T, 0, len(src))
    for _, item := range src {
        ret = append(ret, f(item))
    }
    return ret
}

It can be used in the following way.

1
2
3
4
5
6
var myConv := func(i int)string {return fmt.Sprint(i)}
var src []int
var dest []string
dest = Map[int, string](src, myConv) // 明确指定F和T的类型
dest = Map[int](src, myConv) // 仅指定F的类型,T的类型交由编译器推导
dest = Map(src, myConv) // 完全不指定类型,F和T都交由编译器推导

Generic functions can be used without specifying specific type arguments, or by specifying only some of the types to the left of the type argument list. The compiler will report an error when automatic type derivation fails.

Type derivation in Golang generics is divided into two main parts.

  1. function argument type derivation: the specific types corresponding to the type arguments are derived from the function’s input parameters.
  2. Constraint type derivation: Inferring the specific type of an unknown type parameter from the type parameter of a known specific type.

Both type derivations rely on a technique called “Type Unification”.

Type Unification

Type unification is the comparison of two types, which may themselves be a type parameter or may contain a type parameter.

The process of comparison is a comparison of the “structure” of the two types and requires that the two types being compared satisfy the following conditions:

  1. the “structure” of the two types must match after eliminating the type parameter
  2. the remaining specific types in the structure must be the same after the type parameters are removed
  3. if neither type parameter is included, then both types must be identical, or the underlying data types must be identical

By “structure”, we mean slice, map, function, etc. in the type definition, and any nesting between them.

When these conditions are met, the type uniformity comparison is successful and the compiler can further speculate on the type parameters, for example.

If we have two type parameters “T1” and “T2” at this time, then []map[int]bool can match the following types.

1
2
3
4
[]map[int]bool // 它本身
T1 // T1被推断为 []map[int]bool
[]T1 // T1被推断为 map[int]bool
[]map[T1]T2 // T1被推断为 int, T2被推断为 bool

go parameters

As a counterexample, []map[int]bool clearly does not match these types.

1
2
3
4
5
int
struct{}
[]struct{}
[]map[T1]string
// etc...

Function Argument Type Inference

Function Argument Type Inference, as the name implies, is a generalized function that is called without all type arguments being fully specified, and then the compiler infers the specific types of the type arguments based on the types of the actual function inputs, such as the Min function at the beginning of this article.

1
2
3
4
5
6
7
8
func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

ans := Min(1, 2) // 此时类型参数T被推导为int

Like other languages that support generics, Golang’s function argument type derivation only supports “type arguments that can be derived from the input”, and if the type argument is used to mark the return type, then the type argument must be explicitly specified when it is used.

1
2
3
4
5
6
7
func MyFunc[T1, T2, T3 any](x T1) T2 {
    // ...
    var x T3
    // ...
}

ans := MyFunc[int, bool, string](123) // 需要手动指定

Functions like this, where part of the type parameter appears only in the return value (or only in the function body, not as an input or output parameter), cannot use function parameter type derivation, but must explicitly specify the type manually.

derived algorithm with examples

derived algorithm with examples

Using the Min function as an example, we can explain the process of deriving function parameter types.

1
2
3
4
5
6
func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

Let’s look at the first scenario first.

1
Min(1, 2)

At this point, both input parameters are untyped literal constants, so the first round of type unification is skipped, and the specific type of the input parameters is not determined. At this point, the compiler tries to use the default type int for both parameters, and since the type of both input parameters at the function definition is “T”, and both use the default type int, T is successfully inferred to be int.

Then look at the second case.

1
Min(1, int64(2))

At this point, the second argument has an explicit type int64, so in the first round of type unification, T is inferred to be int64, and when trying to determine the type for the first argument “1” that was missed in the first round, T is successfully inferred to be int64 because “1” is a legal int64, T is successfully inferred as int64.

Let’s look at the third case.

1
Min(1.5, int64(2))

At this point, the second argument has a clear type int64, so in the first round of type unification, T is inferred to be int64, and when trying to determine the type for the first argument “1.5”, which was missed in the first round, the type inference fails because “1.5” is not is not a legal int64 type value, the type derivation fails and the compiler reports an error.

Finally, look at the fourth case.

1
Min(1, 2.5)

Similar to the first case, the first round of type unification is skipped and the specific types of the two input parameters are not determined, so the compiler starts trying to use the default types. The default types of the two parameters are int and float64, and since the same type parameter T can only be determined as one type in type derivation, type derivation fails at this point as well.

Constraints Type Inference

Constraint type inference is another powerful weapon of Golang generics, allowing the compiler to derive the specific type of a type parameter from another type parameter, and to preserve the caller’s type information by using type parameters.

Constraint type derivation can allow constraints to be specified for a type parameter using other type parameters. Such constraints are called “structured constraints”. Such constraints define the data structure that a type parameter must satisfy, e.g.

1
2
3
4
5
6
7
8
// 将一个整数slice中的每个元素都x2后返回
func DoubleSlice[S ~[]E, E constraints.Integer](slice S) S {
    ret := make(S, 0, len(slice))
    for _, item := range slice {
        ret = append(ret, item + item)
    }
    return ret
}

In the definition of this function, “~[]E” is a shorthand for the structured constraint on S. It is written in its entirety as “interface{~[]E}”, i.e., an interface defined as a collection of types, and which contains only one definition of “~[]E”, meaning “all types whose underlying data type is []E”.

Note that the set of types corresponding to a legal structured constraint should satisfy any of the following conditions: 1.

  1. the set of types contains only one type
  2. the underlying data types of all types in the type set are identical

In this example, the structured constraint used by S is a legal structured constraint because the underlying data types of all types that satisfy the constraint are []E.

The compiler attempts constraint type derivation when there are type parameters that cannot be determined by function parameter type derivation for specific types and the list of type parameters contains structured constraints.

Deduction algorithm with examples

Deduction algorithm with examples

Simple example

In conjunction with our example of the “DoubleSlice” function, let’s talk about the specific process of constraint type derivation.

1
2
3
type MySlice []int

ans := DoubleSlice(MySlice{1, 2, 3})

In this call, the first thing performed is the normal function argument type derivation, and this step will result in a derivation like this

1
S => MySlice

At this point the compiler finds that there is a type parameter E that has not been derived and that there is currently a type parameter S that uses structural constraints, so it starts constraining the type derivation.

The first thing to look for is a type parameter whose type derivation has been completed, in this case S, whose type has been derived as MySlice.

Then the actual type of S, “MySlice”, is unified with the structured constraint of S, “~[]E”, and since the underlying type of MySlice is []int, the structured match gives this match Result.

1
E => int

At this point all type parameters have been inferred and conform to their respective constraints, and type derivation is over.

A more complex example

Suppose there is a function such as

1
2
3
func SomeComplicatedMethod[S ~[]M, M ~map[K]V, K comparable, V any](s S) {
    // comparable 是一个内置的约束,表示所有可以使用 == != 运算符的类型
}

Then we call it like this.

1
SomeComplicatedMethod([]map[string]int{})

The type derivation process generated at compile time is as follows, starting with the result of the function parameter type derivation.

1
S => []map[string]int

Then using constrained type derivation for S, comparing []map[string]int and ~[]M, we get

1
M => map[string]int

Continuing with the constrained type derivation for M, comparing map[string]int with ~map[K]V, we get

1
2
K => string
V => int

The type derivation is now successfully completed.

Preserving Type Information with Constraint Type Inference

Another useful aspect of constraint type derivation is that it preserves the type information of the caller’s original arguments.

Using the “DoubleSlice” function from this section as an example, suppose we now implement a more “simple” version of it.

1
2
3
4
5
6
7
func DoubleSliceSimple[E constraints.Integer](slice []E) []E {
    ret := make([]E, 0, len(slice))
    for _, item := range slice {
        ret = append(ret, item + item)
    }
    return ret
}

This version has only one type parameter, E. At this point, we call it as we did before.

1
2
3
type MySlice []int

ans := DoubleSliceSimple(MySlice{1, 2, 3}) // ans 的类型是 []int !!!

The type derivation at this point is just the most basic type derivation of the function parameters, where the compiler does a direct structured comparison between MySlice and []E and concludes that the actual type of E is int.

The DoubleSliceSimple function returns []E, which is []int, not MySlice as passed in by the caller, whereas the previous DoubleSlice function, by defining a type parameter S with a structured constraint and using S to match the type of the input directly, and returning a value of type S, preserves The original argument type of the caller is preserved.

Limitations of using generics

There are still a number of limitations to Golang generics, some of the major ones include.

  1. member functions cannot use generics
  2. you cannot use a method that is not specified in the constraint definition, even if all the types in the type set implement the method
  3. you cannot use a member variable even if all types in the type set have the member

The following are examples of each.

member functions cannot use generics

1
2
3
4
5
6
7
type MyStruct[T any] struct {
    // ...
}

func (s *MyStruct[T]) Method[T2 any](param T2) { // 错误:成员函数无法使用泛型
    // ...
}

In this example, the member function Method of MyStruct[T] defines a function parameter T2 that belongs only to itself, however such an operation is currently not supported by the compiler (and most likely will not be supported in the future).

cannot use methods other than those defined by constraints

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type MyType1 struct {
    // ...
}
func (t MyType1) Method() {}

type MyType2 struct {
    // ...
}
func (t MyType2) Method() {}

type MyConstraint interface {
    MyType1 | MyType2
}

func MyFunc[T MyConstraint](t T) {
    t.Method() // 错误:MyConstraint 不包含 .Method() 方法
}

In this example, the two members of the MyConstraint collection, MyType1 and MyType2, cannot be called directly from a generic function, even though they both implement the .Method() function.

If they need to be called, MyConstraint should be rewritten in the following form.

1
2
3
4
type MyConstraint interface {
    MyType1 | MyType2
    Method()
}

Unable to use member variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type MyType1 struct {
    Name string
}

type MyType2 struct {
    Name string
}

type MyConstraint interface {
    MyType1 | MyType2
}

func MyFunc[T MyConstraint](t T) {
    fmt.Println(t.Name) // 错误:MyConstraint 不包含 .Name 成员
}

In this example, although MyType1 and MyType2 both contain a Name member and are both of type string, they cannot be used directly in a generic function in any way.

This is because the type constraint itself is an interface, and the definition of an interface can only contain a collection of types, and a list of member functions.

Summary

Golang 1.18 brings the above three very important new features, among them.

  1. workspace mode allows for a smoother workflow for local development.
  2. fuzzy tests can find some corner cases and improve the robustness of the code.
  3. generalization can make the code of some public libraries more elegant, avoiding the old way of having to use reflection for “generality”, which is not only difficult to write and read, but also increases the runtime overhead, because reflection is dynamic information at runtime, while generalization is static information at compile time.

This article also briefly covers these aspects and hopefully gives you a basic understanding of these new things in Golang.