Cherry-picked features from Go 1.20

Go 1.20 brought a lot of new features and improvements. In this post, I'd like to review the ones that caught my eye. This is by no means an exhaustive list; for that, see the official release notes.

These are the topics for review:

Each section has a playground link, so check those out.

Multi-errors

The "errors as values" concept (as opposed to exceptions) has gained renewed popularity in modern languages such as Go and Rust. You know this well because it's impossible to take a step without tripping over an error value in Go.

Go 1.20 has brought us new joy — the combination of errors through errors.Join():

errRaining := errors.New("it's raining")
errWindy := errors.New("it's windy")
err := errors.Join(errRaining, errWindy)

Now err is both errRaining and errWindy at the same time. The standard functions errors.Is() and errors.As() can work with this:

if errors.Is(err, errRaining) {
    fmt.Println("ouch!")
}
// ouch!

fmt.Errorf() has also learned to combine errors:

err := fmt.Errorf(
    "reasons to skip work: %w, %w",
    errRaining,
    errWindy,
)

To accept multiple errors in your own error type, return []error from the Unwrap() method:

type RefusalErr struct {
    reasons []error
}

func (e RefusalErr) Unwrap() []error {
    return e.reasons
}

func (e RefusalErr) Error() string {
    return fmt.Sprintf("refusing: %v", e.reasons)
}

If you love errors, this change will definitely be to your liking. If not... well, you always have panic :)

playground

'Context Canceled' Cause

A context.Canceled error occurs when the context is canceled. This is no news:

ctx, cancel := context.WithCancel(context.Background())
cancel()

fmt.Println(ctx.Err())
// context canceled

Starting from 1.20, we can create a context using context.WithCancelCause(). Then cancel() will take one parameter — the root cause of the error:

ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("the night is dark"))

context.Cause() extracts the cause of the error from the context:

fmt.Println(ctx.Err())
// context canceled

fmt.Println(context.Cause(ctx))
// the night is dark

You may ask — why context.Cause()? It seems logical to add the Cause() method to the context itself, similar to the Err() method.

Sure. But Context is an interface. And any change to the interface breaks backward compatibility. That's why it was done differently.

playground

New Date Formats

Please don't be offended by this section if you are a North American. We love you people, but sometimes your view of the world can be a bit... biased.

You surely know that the Go authors chose a quite unorthodox format for the date and time layout.

For example, parsing the date 2023-01-25 09:30 looks like this:

const layout = "2006-01-02 15:04"
t, _ := time.Parse(layout, "2023-01-25 09:30")
fmt.Println(t)
// 2023-01-25 09:30:00 +0000 UTC

While 01/02 03:04:05PM '06 may be a nice mnemonic in the US, it's entirely cryptic for the European (or Asian) eye.

The Go authors have thoughtfully provided 12 standard date/time masks, of which only RFC3339 and RFC3339Nano are suitable for non-Americans. Others are as mysterious as the imperial measurement system:

Layout      = "01/02 03:04:05PM '06 -0700"
ANSIC       = "Mon Jan _2 15:04:05 2006"
UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
RFC822      = "02 Jan 06 15:04 MST"
RFC822Z     = "02 Jan 06 15:04 -0700"
RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700"
Kitchen     = "3:04PM"

Ten years have passed, and the Go development team began to suspect something. They learned that there are several other popular date formats worldwide. And, starting with version 1.20, they added three new masks:

DateTime = "2006-01-02 15:04:05"
DateOnly = "2006-01-02"
TimeOnly = "15:04:05"

Now we can finally do this:

t, _ := time.Parse(time.DateOnly, "2023-01-25")
fmt.Println(t)
// 2023-01-25 00:00:00 +0000 UTC

Nice!

playground

Slice to Array Conversion

Starting from version 1.17, we can get a pointer to an array under the slice:

s := []int{1, 2, 3}
arrp := (*[3]int)(s)

By changing the array through the pointer, we are also changing the slice:

arrp[2] = 42
fmt.Println(s)
// [1 2 42]

In Go 1.20, we can also get a copy of the array under the slice:

s := []int{1, 2, 3}
arr := [3]int(s)

Changes in such an array are not reflected in the slice:

arr[2] = 42
fmt.Println(arr)
// [1 2 42]
fmt.Println(s)
// [1 2 3]

This is, in essence, syntactic sugar because we could get a copy of the array before:

s := []int{1, 2, 3}
arr := *(*[3]int)(s)

The new notation is cleaner, of course.

playground

Other Notable Changes

bytes.Clone() function clones a byte slice:

b := []byte("abc")
clone := bytes.Clone(b)

math/rand package now automatically initializes the random number generator with a random starting value, so there is no need for a separate rand.Seed() call.

strings.CutPrefix() and strings.CutSuffix() functions trim a prefix/suffix from a string similarly to TrimPrefix/TrimSuffix, but they also indicate whether the prefix was present in the string:

s := "> go!"
s, found := strings.CutPrefix(s, "> ")
fmt.Println(s, found)
// go! true

sync.Map now has atomic methods Swap, CompareAndSwap, and CompareAndDelete:

var m sync.Map
m.Store("name", "Alice")
prev, ok := m.Swap("name", "Bob")
fmt.Println(prev, ok)
// Alice true

time.Compare() function compares two times and returns -1/0/1 based on the comparison results:

t1 := time.Now()
t2 := t1.Add(10 * time.Minute)
cmp := t2.Compare(t1)
fmt.Println(cmp)
// 1

Overall, a great release! Looking forward to trying everything in production.

★ Subscribe to keep up with new posts.