Things I'm excited for in Go 1.21

Go, DevEx Posted on

Go 1.21 has some awesome changes that improve the overall developer experience and enhance performance. There's also some great new packages and functions to streamline common operations, and even a new opt-in experiment to fix a common bug. This article highlights my favorite changes in Go 1.21.

New built-in: clear

The new clear built-in function in Go 1.21 deletes all elements from a map or zeros out all elements of a slice, depending on the input.

m := map[string]string{"foo": "bar"}
clear(m) // m={}

s := []string["foo", "bar"]
clear(s) // s=["", ""]

From a developer experience perspective, this is an obvious improvement over the former solution:

m := map[string]string{"foo": "bar"}
for k := range m {
  delete(m, k)
}
// m=[]

s := []string["foo", "bar"]
for i := range s {
  s[i] = ""
}
// s=[]

However, the new language built-in also handles an edge case that prior versions of Go could not: NaN. The following code does not clear the map:

// Does not work:
m := map[float64]string{math.NaN(): "a"}
for k := range m {
  delete(m, k)
} // m={NaN:a}

This is because NaN != NaN, and Go maps keys rely on object equality. In Go 1.21, the following code does work:

m := map[float64]string{math.NaN(): "a"}
clear(m) // m={}

See the language specification on clear for more details.

Loopvar experiment

A very common bug that I've caught during code reviews (and admittedly made myself) is around loop closures; I even wrote about it in my Go 2.0 post. Consider the following code snippet:

type Foo struct {
  bar string
}

func main() {
  list := []Foo{{"A"}, {"B"}, {"C"}}

  cp := make([]*Foo, len(list))
  for i, value := range list {
    cp[i] = &value
  }
}

You would expect the values in cp to be [A B C], but the values are actually [C C C]. This is because Go uses a copy of the value instead of the value itself in the range clause. I thought this would have to wait until Go 2.0, but based on analysis by the Go team, this change may be able to come sooner!

Go 1.21 introduces an opt-in experiment that changes the semantics of loop variables to prevent unintended sharing in per-iteration closures and goroutines. That means the same code above, compiled with Go 1.21 and GOEXPERIMENT=loopvar will work as expected:

$ GOEXPERIMENT=loopvar go1.21rc3 run main.go
[A B C]

In most instances, enabling this experiment will uncover bugs or run tests that were not previously running (especially if you use t.Parallel). In rare cases, this change can break programs, but the data shows this is extremely rare.

The Go team made it very easy to identify places where your code might be affected by this change. Building your Go program with Go 1.21 and a special GC flag will print out the loops that are compiling differently:

go build -gcflags=all=-d=loopvar=2 ./...

This is an extremely welcome change. Finally there's a path to a future where we no longer need to put x := x (or t := t) inside loop closures!

Structured logging

I also wrote about this in my Go 2.0 post, and I've had the pleasure of working with the Go team on this package design. The new log/slog package provides structure logging with levels, emitting key=value pairs that enable fast and accurate machine processing of data with minimal allocations.

The slog package has native support for log levels, grouping, contexts, and custom types. While it works out the box:

logger := slog.New(slog.NewTextHandler(os.Stderr, nil))

It's also extremely flexible and customizable to match your needs. Defining custom attribute replacers allow developers to performantly rewrite log keys and values. For example, suppose you wanted to change the names of the printed levels:

logger := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
  ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
    if a.Key == slog.LevelKey {
      // This could also be a map or other lookup structure.
      switch {
      case level < slog.LevelDebug:
        a.Value = slog.StringValue("TRACE")
      case level < slog.LevelInfo:
        a.Value = slog.StringValue("DEBUG")
      case level < slog.LevelNotice:
        a.Value = slog.StringValue("INFO")
      case level < slog.LevelWarning:
        a.Value = slog.StringValue("NOTICE")
      case level < slog.LevelError:
        a.Value = slog.StringValue("WARNING")
      case level < slog.LevelEmergency:
        a.Value = slog.StringValue("ERROR")
      default:
        a.Value = slog.StringValue("EMERGENCY")
      }
    }
  }
})

Or maybe you have secrets that you want to make sure are never logged:

type Secret string

func (Secret) LogValue() slog.Value {
  return slog.StringValue("[REDACTED]")
}

Overall, I'm very happy with the design and developer experience. I'm hopeful to see the community converge on this single logging solution, which will make library portability much easier. For example, libraries can now accept a *slog.Logger to that upstream developers can control the logging based on their application needs.

For more information, see the log/slog package.

Slies and maps packages

Go 1.21 promotes two new packages into the standard library (although you can use them on earlier versions from golang.org/x/exp): maps and slices. These packages expose functions for very common map and slice operations. Here are a few that I expect to use frequently:

s := []string{"A", "A", "B", "C"}

// Clone returns a shallow-copy clone of the slice.
slices.Clone(s) // => ["A", "A", "B", "C"]

// Contains returns true if the value exists, or false otherwise.
slices.Contains(s, "A") // => true
slices.Contains(s, "Z") // => false

// Compact removes consecutive runs of duplicate elements.
slices.Compact(s) // => [A, B, C]

// Min and max report minimum and maximum values.
slices.Min(s) // => "A"
slices.Max(s) // => "C"
m := map[string]int{"A": 1, "B": 2, "C": 3}

// Clone returns a shallow-copy of the map.
maps.Clone(m) // => {A:1, B:2, C:3}

For more information, see the maps and slices packages.

OnceValue and OnceFunc

The new sync.OnceValue and sync.OnceFunc functions are very useful for lazily initializing a value on first use. For example, suppose I have an expensive operation that I want to defer as long as possible, but should only be computed once:

var reallyHardProblem int = sync.OnceValue[int](func() int {
  // something really hard and complex
})

reallyHardProblem() // invokes the value and returns an int
reallyHardProblem() // returns the int
reallyHardProblem() // returns the int
reallyHardProblem() // returns the int

Wrapping up

Go continues to be one of my favorite programming languages, and I'm really excited to see so many great developer experience and performance changes coming in Go 1.21!

About Seth

Seth Vargo is an engineer at Google. Previously he worked at HashiCorp, Chef Software, CustomInk, and some Pittsburgh-based startups. He is the author of Learning Chef and is passionate about reducing inequality in technology. When he is not writing, working on open source, teaching, or speaking at conferences, Seth advises non-profits.