Generic Must function in Go

Avatar of the author Willem Schots
5 Apr, 2024
~3 min.
RSS

If you’re hardcoding data in your code, you as the developer are responsible for correctness. In other words: Invalid hardcoded data is usually a programmer error.

In my opinion it’s valid to panic on these kind of errors. Your program can’t do much about them but shutdown. It’s similar to attempting to access a slice element past the length of the slice.

The must convention

In Go there is the following convention: Functions or methods that are prefixed with Must (or must) will panic instead of returning an error. Apart from that they do the same thing as the function without the Must prefix.

For example, suppose you have the following function:

func ParseXYZ(coords string) ([3]int, error) {
	//... skipped for brevity.
}

The Must function will look like this:

func MustParseXYZ(coords string) [3]int {
	xyz, err := ParseXYZ(coords)
	if err != nil {
		panic(err)
	}
	return xyz
}

This pattern comes in handy when dealing with hardcoded data or test data. It enables you to quickly create results without having to worry about error handling or littering your code with err != nil statements.

This pattern is used in the standard library regexp package. See regexp.Compile and regexp.MustCompile.

When not to use this

The Must functions should only be used with developer provided data. Data coming from other sources (like user input) should always use regular error handling.

A generic Must function

Since Go 1.18 we have access to generics, instead of writing manual Must* functions, we can create a Must function that works for all types.

This Must function will take the results of the target function as input and looks like this:

func Must[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}

You can then use it to simplify the initialization of complex test data:

main.go
package main

import (
	"fmt"
	"strconv"
	"strings"
)

func main() {
	coord1 := Must(ParseXYZ("1,2,3"))
	coord2 := Must(ParseXYZ("-1,-2")) // missing one coordinate by design

	fmt.Println(coord1)
	fmt.Println(coord2)
}

func Must[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}

func ParseXYZ(coords string) ([3]int, error) {
	elements := strings.Split(coords, ",")
	if len(elements) != 2 {
		return [3]int{}, fmt.Errorf("expected %d elements, got %d", 3, len(elements))
	}

	var xyz [3]int
	for i, el := range elements {
		nr, err := strconv.Atoi(el)
		if err != nil {
			return [3]int{}, fmt.Errorf("failed to parse %d element as a number: %w", i, err)
		}

		xyz[i] = nr
	}

	return xyz, nil
}

This Must function assumes that there will be a single result type and a single error returned. This is usually the case, but nothing is stopping you from adding Must2 or Must3 functions when necessary.

🎓

Subscribe to my Newsletter and Keep Learning.

Gain access to more content and get notified of the latest articles:

I send emails every 1-2 weeks and will keep your data safe. You can unsubscribe at any time.

Hello! I'm the Willem behind willem.dev

I created this website to help new Go developers, I hope it brings you some value! :)

You can follow me on Twitter/X, LinkedIn or Mastodon.

Thanks for reading!