|
|
Subscribe / Log in / New account

What's coming in Go 1.18

Please consider subscribing to LWN

Subscriptions are the lifeblood of LWN.net. If you appreciate this content and would like to see more of it, your subscription will help to ensure that LWN continues to thrive. Please visit this page to join up and keep LWN on the net.

February 8, 2022

This article was contributed by Vegard Stikbakke

Go 1.18, the biggest release of the Go language since Go 1.0 in March 2012, is expected to be released in February. The first beta was released in December with two features which, each on their own, would have made the release a big one. It adds support for generic types and native support for fuzz testing. In the blog post announcing the beta, core developer Russ Cox emphasized that the release "represents an enormous amount of work".

Generic types

Go is a statically typed language. Functions and methods in Go may be declared to take any number of arguments and may return any number of values, though all arguments and return values must be declared to have a concrete type.

To give an example, say that we want a function to find the minimum of two arguments of type float32. The standard library's math.Min() function only allows float64 types, as there is no type promotion in Go. A user in need of a min() function for another type must therefore write it or import it from a package that is not in the standard library. So we must define a min() function for float32 values. In Go, this may be written as:

    func min(x float32, y float32) float32 {
	if x < y {
	    return x
	}
	return y
    }

If we want a function to get the minimum of two int values, we need to create yet another function.

However, starting from Go 1.18, functions in Go can accept generic types, which are described by a type parameter list specifying the permissible types for the arguments and return value:

    func min[T float32 | float64 | int](x T, y T) T {
	if x < y {
	    return x
	}
	return y
    }
This means that a function does not need to specify concrete types for its arguments or return values, but may rather specify type parameters (T in the example) for these in a type parameter list, which is in square brackets above. Each type parameter has a corresponding type constraint, which is an interface that specifies the possible type arguments for the type parameter. The permissible types comprise the type parameter's type set.

The type parameter list is surrounded by square brackets, so that it is easy to visually distinguish it from the parameter list, which is surrounded by parentheses. The valid types for a type parameter are delimited by "|" and multiple type parameters are delimited by commas.

This allows defining a generic min() that works for more than one concrete type. Above, we specified that min() takes a type parameter T which is constrained to be one of the types float32, float64, or int. The function takes two arguments, as before. These must be of the same concrete type; the return type is also the same concrete type. In other words, the program would not compile if we attempted to call min() with x being a float32 and y being a float64.

Instead of specifying the types inline, a developer can specify them as an interface:

    type ordered interface {
	float32 | float64 | int
    }
    func min[T ordered](x T, y T) T {
	if x < y {
	    return x
	}
	return y	
    }

Go has more than just these types that have an inherent ordering, such as other numeric types and string. Initially, a new package (constraints) was added to the standard library for 1.18, which defined various interfaces for types that can be ordered, but the package has since been removed due to some questions about its suitability and its name.

Generic types have been a highly requested feature in Go for many years. There have been multiple language proposals; in 2020 LWN wrote about the history of generics in Go, along with a then-recent proposal by core developers Ian Lance Taylor and Robert Griesemer. This language proposal generated much discussion and in general the reception was positive, so the proposal became a formal language change proposal in August 2021. Go 1.18 implements most of the proposal, although some things are kept out of scope for this release. In the initial proposal, the type parameter list was marked with parentheses; that was changed to square brackets before being added to the language.

Many popular, statically typed languages, such as Java, C#, and C++, have generic types or something similar. Go is a decidedly small language, in terms of language features, so it is perhaps not surprising that generic types have not been part of the language until now.

The recommended way to use a generic function is to let the compiler infer the types if possible. This means to use the function as if it were defined for the concrete types we are using. For instance:

    min(1, 3)

The other way to use a generic function is to instantiate the parameterized function by passing a type argument to the generic function. Here we define a function fmin() for float32 values:

    fmin := min[float32]

We can imagine this works by removing the type parameter list in min() and replacing all instances of T with float32. The compiler now knows that fmin() is a non-generic function: func(float32, float32) float32.

Two new predeclared identifiers have been added that are useful as constraints in type parameter lists:

  • any, which is an alias for interface{}, the empty interface. It can be used as a type constraint, but means that the type can be any concrete type. A small set of operations are permitted for the any type.
  • comparable, an interface that denotes the set of all types that can be compared using == or !=.

Since the compiler has been modified to support generics, the Go 1.18 compilation speed can be as much as 15% slower than Go 1.17. Improving the speed of the compiler is a goal for Go 1.19. The execution time of the compiled code is not affected.

For more information, the Go team has published a generics tutorial.

Fuzzing

Fuzz testing (fuzzing) is a type of testing that generates random inputs to a program with the goal of finding bugs. Go 1.18 adds native support for fuzz testing via a new flag, -fuzz, passed to the go test command. LWN wrote about fuzzing in Go as well. The fuzzing that is added to Go in 1.18 stems from a 2020 design draft by Go core developer Katie Hockman, which became a formal Go proposal in February 2021. A fuzz test must be in a *_test.go file as a function of the form FuzzXxx(), where Xxx is a capitalized (by convention) name for what is being fuzzed, that has a single *testing.F pointer argument and has no return value. The following program fragment for fuzzing min() will look somewhat similar to other kinds of tests in Go that use its testing framework:

    func FuzzMin(f *testing.F) {
	f.Fuzz(func(t *testing.T, a float64, b float64) {
	    actual := min(a, b)
	    expected := math.Min(a, b)
	    if actual != expected {
		t.Fatalf("min(%f, %f) was %f instead of %f", a, b, actual, expected)
	    }
	})
    }

Fuzz tests can be invoked in fuzzing mode, go test -fuzz=target, where target is a regular expression specifying which fuzz tests to run. The arguments to mutate are those in the function passed to the fuzzing engine's Fuzz() method. That passed function runs the function under test and checks for incorrect results, reporting those via the usual testing framework mechanisms. Fuzz() repeatedly calls the function with mutated arguments.

Go fuzzing uses "coverage-guided fuzzing" to generate new inputs in a way that increases the test coverage. Coverage-guided fuzzing has been used to great effect on many open-source projects. American fuzzy lop (AFL) is the most well-known fuzzer of this type; its website lists more than one 150 projects that it has found a bug in, including Bash, FFmpeg, and the OpenBSD kernel.

Fuzzing starts by picking an input from a seed corpus, which can be populated in code in the fuzz test or with files representing previously failed inputs. Fuzzing mutates the seed input, executes the target with the new input, and collects code coverage for it. If the new input caused a new path to be executed, the input is added to the seed corpus. If the fuzz target fails for a given input, the fuzzing engine writes the inputs that caused the failure to a file in the directory testdata/fuzz/<Name> within the package directory. As mentioned above, this file will also serve as a seed input.

Running fuzz tests may not only find bugs, they can also double as regression tests. Fuzz tests are run much like a unit test by default, using all of the stored test data as input. Each seed corpus entry will be tested against the fuzz target, reporting any failures before exiting. A fuzz test runs until it encounters a failing input or it times out. A fuzz timeout can be set with the -fuzztime flag to go test. The test process can also be interrupted by a signal, of course.

There are some limitations to native Go fuzzing. The arguments to fuzz are limited to the following types in 1.18:

  • strings: string, []byte
  • signed integers: int, int8, int16, int32/rune, int64
  • unsigned integers: uint, uint8/byte, uint16, uint32, uint64
  • floats: float32, float64
  • booleans: bool

Furthermore, the compiler only supports coverage instrumentation on x86-64 and Arm64 platforms as of 1.18.

As with generics, the Go team has published a fuzzing tutorial, in addition to a new documentation page for fuzzing..

Other changes

Due to Go's compatibility promise, there are no breaking changes to the language in Go 1.18. The compatibility promise is a stated intention that programs written in Go will compile and run for all future versions of Go. However, each Go version typically adds new packages and functions. Two packages are being added in Go 1.18: debug/buildinfo, which provides access to module versions, version-control information, and build flags embedded in the executable built by the go command, and net/netip, which defines a new comparable IP address type, Addr.

There are a number of new functions added to the standard library; one of these seems particularly interesting. strings.Cut() slices a string around a separator. It can replace many common uses of Index() and other similar functions. In the issue proposing it, Cox claimed that out of 285 calls to string.Index() in the Go standard library, "77% of [these] calls are more clearly written using Cut".

The built-in vet linting tool has been updated to detect errors in code using generic functions and types. In most cases, it reports an error in generic code whenever it would report an error in the equivalent non-generic code after substituting for type parameters with a type from their type set. gofmt, which automatically formats Go source code, now reads and formats input files concurrently. This means that gofmt is now significantly faster.

To get the complete picture of everything in Go 1.18, readers are encouraged to seek out the full release notes. Go 1.18 is currently in Beta 2, with a release expected this month. The maintainers will certainly be appreciative of anyone who tries it out and reports any bugs that they find.


Index entries for this article
GuestArticlesGrødem Stikbakke, Vegard


(Log in to post comments)

What's coming in Go 1.18

Posted Feb 8, 2022 23:04 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link]

One other significant addition: "~" operator for constraints. You can now write: "func min[T ~float32 | ~float64 | ~int](x T, y T) T" meaning that T is a type that behaves like float32, float64 or int.

What's coming in Go 1.18

Posted Feb 9, 2022 8:45 UTC (Wed) by taladar (subscriber, #68407) [Link]

> func min(x float32, y float32) float32 {
> if x < y {
> return x
> }
> return y
> }

> type ordered interface {
> float32 | float64 | int
> }
> func min[T ordered](x T, y T) T {
> if x < y {
> return x
> }
> return y
> }

That looks like it is significantly less expressive than proper generics with open traits/type classes/interfaces/...

Can you really only specify a closed, one-time list without being able to add new types to it later?

What's coming in Go 1.18

Posted Feb 9, 2022 11:30 UTC (Wed) by jmaa (guest, #128856) [Link]

I _think_ you can define method interfaces, which should allow full generics, but the example I found of this is a year old. [1] The linked generics tutorial doesn't mention them, so who knows.

[1]: https://www.freecodecamp.org/news/generics-in-golang/

What's coming in Go 1.18

Posted Feb 9, 2022 13:19 UTC (Wed) by tsavola (subscriber, #37605) [Link]

constraints.Ordered was supposed to be shipped in the standard library (I guess it will be added in a later release). Go doesn't support operator overloading, so only primitive types (and ~primitive types) can have the < operator.

For generic code invoking methods, you can define interfaces with methods - you don't have to list concrete types.

What's coming in Go 1.18

Posted Feb 9, 2022 12:14 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link]

One other change that is often overlooked in 1.18 announcements is multi-module workspaces: https://go.googlesource.com/proposal/+/master/design/4571...

This _finally_ allows to do sane local development with multiple modules, so that you don't accidentally commit your "replace" directives in "go.mod".

What's coming in Go 1.18

Posted Feb 11, 2022 17:09 UTC (Fri) by blancogrande (guest, #125002) [Link]

I'm as excited about workspaces as I am about generics. Anyone using a 'monorepo' style development where multiple applications share libraries within the repo will be happy.


Copyright © 2022, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds