Go programming language has a new version 1.21.

Release Notes. Each new release contains a lot of great stuff and 1.21 is not an exception. In fact it’s pretty massive.

  • New numbering scheme, so the 21 in 1.21.0 is a Go language version. However still under v1 compatibility promise
  • There are many changes aroung GOTOOLCHAIN and the fact go 1.21 in go.mod has now a meaning
  • The ability to download and use a specific toolchain version.
  • Profile Guided Optimizations
  • Improvements in type inference for generics
  • min, max and clear builtins
  • shiny new slices, maps, log/slog and cmp standard library packages
  • Experiment with a fixing loop variable capture
  • Experimental WASI port (GOOS=wasip1 GOARCH=wasm) and a go:wasmimport directive

It takes a time to appreciate all of those. However the improvements of a context package may be the easiest to grok. And to blog about of course.

WithoutCancel

WithoutCancel returns a copy of a context object, which is not canceled when parent is. But the context retains all the values of its parent. This is handy when passing a data to the further asynchronous processing and want to retain all the stored context values.

Playground

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ctx1, cancel1 := context.WithCancel(context.TODO())
	ctx1 = context.WithValue(ctx1, "key", "value")
	ctx2 := context.WithoutCancel(ctx1)
	time.AfterFunc(250*time.Millisecond, cancel1)
	<-ctx1.Done()
	fmt.Printf("ctx1.Err()=%+v, ctx1.Value(key)=%#v\n",
    ctx1.Err(), ctx1.Value("key"))
	fmt.Printf("ctx2.Err()=%+v, ctx2.Value(key)=%#v\n",
    ctx2.Err(), ctx2.Value("key"))
}
ctx1.Err()=context canceled, ctx1.Value(key)="value"
ctx2.Err()=<nil>, ctx2.Value(key)="value"

Cause

One of the most frustrating experiences with Go and context objects is the ubiquitous request failed: context cancelled errors spread everywhere. Very rarely they provide any clue about what’s happening.

The new Cause mechanism is about to solve that. The WithCancel/WithDeadline and WithTimeout functions has counterparts allowing to add additional information why the cancel was called.

Playground

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx, cancel := context.WithCancelCause(context.TODO())
	cancel(fmt.Errorf("got bored waiting on a response"))
	fmt.Printf("ctx.Err()=%+v\n", ctx.Err())
	fmt.Printf("ctx.Cause()=%+v\n", context.Cause(ctx))
}
ctx.Err()=context canceled
ctx.Cause()=got bored waiting on a response

I’d prefer ctx.Err() to return got bored waiting on a response: context canceled. However I assume this would break soo much code doing if ctx.Err() == context.Canceled, so adding an extra helper is probably safer option.

AfterFunc

AfterFunc is probably the most complicated addition to the stdlib.

  • it runs a function func() after context is Done in own goroutine
  • in case it’s already done, it fires the goroutine immediately
  • returns stop func() bool
  • if stop returns true, the call prevents func() from being run
  • if returns false then the func() has been already started or stopped
  • stop does not wait on func() to finish
  • multiple calls operated independently, so there can be a slice of functions attached to context cancellation

More advanced examples are in documentation. Here is a short example illustrating most of the properties mentioned earlier.

Playground

package main

import (
	"context"
	"log"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.TODO())
	// stop1 called immediatelly
    // will return true, this log won't be printed
	stop1 := context.AfterFunc(ctx, func() {
        log.Print("AfterFunc1: stopped") })
	log.Printf("stop1 = %t", stop1())
	// this will be printed as no stop is going to be called
	_ = context.AfterFunc(ctx, func() {
        log.Print("AfterFunc2: not stopped") })
	time.AfterFunc(50*time.Millisecond, cancel)
	<-ctx.Done()
	// this will be printed as context is done
	stop3 := context.AfterFunc(ctx, func() {
        log.Print("AfterFunc3: after Done") })
	// will print false as context is done
	log.Printf("stop3 = %t", stop3())
	// to give goroutines enough time to print
	time.Sleep(250 * time.Millisecond)
}
2009/11/10 23:00:00 stop1 = true
2009/11/10 23:00:00 AfterFunc2: not stopped
2009/11/10 23:00:00 stop3 = false
2009/11/10 23:00:00 AfterFunc3: after Done