Go 1.20 in a nutshell

Get an overview of Go 1.20's new features and noteworthy changes. Plus: A feature that did not make it into the release.

Go 1.20 contains a wagonload of changes. I will focus on the most interesting features, noteworthy changes, and a dangerous change that did not make it into the Go 1.20 docs.

Interesting features

Why was that context canceled?

The most eagerly awaited feature (according to one commenter on Reddit) is the new WithCancelCause() function of the context package.

It works like WithCancel except that the returned cancel function takes an error value as a parameter. The goroutine that calls cancel() can pass an error to it that describes the cause of the cancelation.

Interested parties can call context.Cause() after the cancelation to retrieve that custom error.

    // WithCancelClause() can be used in place of WithCancel()
    ctx, cancel := context.WithCancelCause(context.Background())

    // cancel() expects an error value
    cancel(errors.New("steam engine transmission belt broken"))

    // ctx.Err() says "context canceled" as before
    fmt.Println(ctx.Err())

    // but now you can retrieve the cause of the cancelation
    fmt.Println(context.Cause(ctx))

Try it in the Go playground

Got multiple errors to report? Let them join together!

In some situations, you might want to collect two or more errors before returning to the caller.

The new function errors.Join() expects a list of errors and joins them into a single error value.

    // first error
    err1 := errors.New("first error: out of luck")

    // try to fix the error, fail again
    err2 := errors.New("second error: failed fixing first error")

    // try to clean up after the errors, get another error
    err3 := errors.New("third error: cleanup failed")

    // combine all the errors
    err := errors.Join(err1, err2, err3)

    fmt.Println(err)

    // err Is() any of err1, err2, or err3
    fmt.Println(errors.Is(err, err1))
    fmt.Println(errors.Is(err, err2))
    fmt.Println(errors.Is(err, err3))

    // The errors do not unwrap through errors.Unwrap()
    // errors.Unwrap() can only return a single error
    // For multiple wrapped errors, it returns nil
    fmt.Println(errors.Unwrap(err)) // <nil>

// Instead, a joined error provides a method "Unwrap() []error" that we can call
    // But first, err must assert that it implments "Unwrap() []error"
    e, ok := err.(interface{ Unwrap() []error })
    if ok {
        fmt.Println("Unwrapped:", e.Unwrap())
    }

With Go 1.20, fmt.Errorf() accepts multiple %w verbs, which is an alternative way of joining errors.

    // To wrap multiple errors, use fmt.Errorf
    errWrapped := fmt.Errorf("multiple errors occurred: %w, %w, %w", err1, err2, err3)
    fmt.Println(errWrapped)

Errors wrapped this way behave the same as those wrapped with Join().

See the full Go playground example.

Optimize your code with Profile-Guided Optimization (preview) and whole-program coverage profiling

Imagine that the Go compiler could take profiling information from application runs and optimize the application even more! This is now possible with

Profile-Guided Optimization (PGO),

at least as a preview. (Expect some rough edges.)

Here is how it works:

  1. Build an initial application binary in the usual way.
  2. Run the application in production.
  3. Collect profiles from production, using net/http/pprof or other suitable ways of generating a profile. (If this is not possible, use a representative benchmark instead.)
  4. Build the next binary by providing the production profile to the compiler.
  5. Rinse and repeat.

It is crucial to take the profiles from representative use cases. Typically, this means collecting them in the production environment. The more the profile represents the actual use of the application, the better the improvements are.

Read more in the official documentation: Profile-guided optimization - The Go Programming Language

Related to PGO is another new optimization feature:

Coverage profiling.

Code coverage profiles help determine if your code is sufficiently covered by unit tests. To generate a coverage profile report, you would run go test -coverprofile=... followed by go tool cover... .

Go 1.20 adds support for collecting coverage profiles from applications and integration tests. The process is slightly more involved than that for unit tests, but it can be broken down into three basic steps:

  1. Build the binary with the -cover flag.
  2. Run the binary one or more times. Each time, the binary will write out profile data into files inside a directory specified by the environment variable GOCOVERDIR.
  3. Generate the profile report.

More details on the official landing page: Coverage profiling support for integration tests - The Go Programming Language

What if your application does not exit on a regular basis? How can you collect profiling data then?

This is also taken care of. The new runtime/coverage package contains functions for writing coverage profile data at runtime from long-running programs.

Noteworthy changes

stdlib is... gone!?

No, not entirely gone, but the pre-compiled archives of the standard library are not shipped anymore with the Go distribution, nor does it get pre-compiled into $GOROOT/pkg. The packages of the standard library are now handled like any other package. The go command will build them as needed, and cache them in the build cache.

Pure Go by default

On systems without a C toolchain, cgo is now disabled by default. When the go command does not find any suitable C compiler like clang or gcc, and when the environment variables CGO_ENABLED and CC are unset, the Go compiler assumes that CGO_ENABLED is set to 0. Packages in the standard library that use cgo will be compiled from alternate pure-Go code.

This change mainly affects net, os/user, and plugin on Unix systems. On macOS and Windows, net and os/user do not use cgo.

Better use of the comparable constraint

Type parameters (a.k.a generics, introduced in Go 1.18) use constraints to define the permissible type of arguments for a type parameter. A special case is the comparable constraint. This constraint cannot be constructed with the usual methods for constructing a type constraint; hence it is a predeclared interface type.

The comparable constraint shall represent all types for which the equal ("==") and not-equal ("!=") operations are defined. Until Go 1.20, however, this did not include any types that are considered comparable at compile time but for which the comparison may panic at runtime. This class of comparable types is called "non-strictly comparable" and includes interface types or composite types containing an interface type.

Go 1.20 now allows using such no-strictly comparable types for instantiating a type parameter constrained by comparable.

Example:

func compare[T comparable](a, b T) bool {
    if a == b {
        return true
    }
    return false
}

func main() {
    var n, m int
    fmt.Println(compare(n, m))

    var i1, i2 any
    fmt.Println(compare(i1, i2))
}

Try it in the playground (as long as Go 1.19 can still be selected). With Go 1.20 or newer, both calls to compare() return true. Switch the playground to Go 1.19 and the build will fail with this error message:

./prog.go:18:21: any does not implement comparable
Go build failed.

Deprecated: math/rand.Seed() and math/rand.Read()

Prior to Go 1.20, the global random number generator in math/rand received a deterministic seed value at startup. To get a random seed at every program start, the usual "trick" was to use the current unix timestamp as the seed value:

rand.Seed(time.Now().UnixNano())

This clumsy seeding is not necessary anymore. The global random number generator of math/rand is now automatically seeded with a random value.

To force the old behavior, call rand.Seed(1) at startup, or set GODEBUG=randautoseed=0 in the application's environment.

If you need a deterministic result sequence from the rand functions, do not use the global random number generator. Instead, create a new generator:

rand.New(NewSource(seed))

(where seed is a manually chosen integer value.) See func Seed for more information.

Also, the top-level Read function has been deprecated. The release notes suggest using crypto/rand.Read instead.

A feature that did not make it into the release

While the Go 1.20 release candidates were available, blog articles started discussing a new package called arena that was supposed to be announced as an experimental feature for Go 1.20.

The arena package allows to create "memory arenas", areas in RAM that the garbage collector would ignore. The goal was to reduce pressure on the garbage collector for certain kinds of high-frequency allocations inside performance-critical paths. Inside an arena, code can manually allocate new objects of diverse size and lifetime. When work is finished, the code can free the whole arena and leave one single memory area for the garbage collector to clean up. If you think "C", yes, this sounds similar to the good old malloc and free, with a bit more memory safety through a tool called "address sanitizer" but still problematic enough to warrant the removal of the experiment from the Go1.20 documentation.

If you are curious, you can dig through the proposal and its discussion thread.

More cool features

Slice-to-array conversion

Converting a slice to an array is now as easy as:

    s := []int{1, 2, 3, 4, 5}
    a := [5]int(s)

Caveat: The slice may be longer than the array, but it must not be shorter, or the conversion panics. The length of an array must be known at compile time, so you cannot do something like, a := [len(s)]int(s). Note that the actual length is relevant here, not the capacity of the slice.

Play with slice conversion

Clear, discoverable response control: HTTP ResponseController

A net/http.ResponseWriter can implement optional interfaces, such as Flusher or Hijacker. This way, a Web server can provide additional functionality to the ResponseWriter. On the downside, these interfaces are not discoverable and inelegant.

This example is from the documentation of type Hijacker:

func main() {
    http.HandleFunc("/hijack", func(w http.ResponseWriter, r *http.Request) {
        hj, ok := w.(http.Hijacker)
        if !ok {
            http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
            return
        }
        conn, bufrw, err := hj.Hijack()
        // ... check err and use the hijacked connection ...

The new net/http.ResponseController makes those interface methods straightforward to use.

A ResponseController is created from a ResponseWriter passed to the ServeHTTP() method. It supplies Flush() and Hijack() methods that call the respective interface method if the ResponseWriter implements that interface, or return an error otherwise.

func main() {
    http.HandleFunc("/hijack", func(w http.ResponseWriter, r *http.Request) {
        c := http.NewResponseController(w)
        conn, bufrw, err := c.Hijack()
        // ... check err and use the hijacked connection ...

New crypto/ecdh package

Go 1.20 adds a new crypto/ecdh package to provide support for Elliptic Curve Diffie-Hellman key exchanges over NIST curves and Curve25519.

This package is recommended over the lower-level functionality in crypto/elliptic for ECDH. For more advanced use cases, the Go team recommends using third-party modules.

Performance

Only good news here:

  • The compiler is fast as 1.17 again
  • Up to 2% better performance and reduced memory overhead through improvements of compiler and garbage collector

Conclusion

Lots of interesting updates and changes! I really can't say what's my favorite one. Time will tell.

Be sure also to visit the official announcement on the Go blog, and definitely check out the full list of changes in the Go 1.20 Release Notes.

Happy coding! ʕ◔ϖ◔ʔ


Applied Go Courses helps you get up to speed with Go without friction. Our flagship product, Master Go, is an online course with concise and intuitive lectures and many practices and exercises. Rich graphic visualizations make complex topics easy to understand and remember. Lectures are short and numerous to save your precious time and serve as quick reference material after the course. Learn more at https://appliedgo.com.


Walnut Photo by Ulrike Leone from Pixabay

Update 2023-02-04: Clarified the status and use of the memory arena feature. Removed the ?v=gotip query from playground links because the playground's default version is now Go1.20.

Categories: Releases