Function fitting in Go
Tags: go, programming, coding
Published: 2024-06-14 13:07:39

In Go, as in most programming languages, the return value of a function `g()` can be used as an argument of another function `f()`.

```go
package main

func g() int  { return 42 }
func f(n int) {	println(n) }

func main() {
	f(g()) // prints `42`
}
```

Now, what happens when `g()` returns more than one value, can we still do that?

Short answer is _"yes, but"_

## Some Theory

The Go spec treats this as a special case:

> #### [Calls](https://go.dev/ref/spec#Calls)
>
> As a special case, if the return values of a function or method `g` are equal in number and individually assignable to the parameters of another function or method `f`, then the call `f(g(_parameters_of_g_))` will invoke `f` after binding the return values of `g` to the parameters of f in order.

Effectively this piece of code will work too.

```go
package main

func g() (int, int) { return 0, 0 }
func f(int, int) {}

func main() {
	f(g())
}
```

Both the number and type of arguments that a function returns MUST be equal (or _fit_) into the arguments received by a function, although there is nuance.

> […] If f has a final ... parameter, it is assigned the return values of g that remain after assignment of regular parameters.

This means that we can

```go
package main

func g() (int, int, int) { return 1, 2, 3}
func f(a int, n ...int) {
	fmt.Printf("a: %d, n: %v", a, b)
}

func main() {
	f(g()) // prints `a: 1, n: [2, 3]`
}
```

In the example above, `g()` returns a larger number values than the number of arguments received by `f()`. This will work a long as the types match.

It is important to note that `f()` cannot receive any other argument than those returned by `g()`, this make both function signatures to be linked, meaning that `g()`'s return values fit into `f()`'s

### Practical Usage

There is a common Go idiom that leverages this property by defining a  _Must_ function. A _Must_ function takes the return values of a function `f()` and panics when there is an error. For Must to work `f()` must return zero or more arguments followed by a final error.

Take for instance [template.Must](https://pkg.go.dev/html/template#Must) helper that works in conjunction with the `Parse` function family ([ParseFS](https://pkg.go.dev/html/template#ParseFS), [ParseFiles](https://pkg.go.dev/html/template#ParseFiles), [ParseGlob](https://pkg.go.dev/html/template#ParseGlob) and so on), it will panic when the returned error is not nil.
This idiom can be found outside the standard library too like google's UUID, where [NewRandom()](https://pkg.go.dev/github.com/google/uuid#NewRandom) _fits_ in [Must()](https://pkg.go.dev/github.com/google/uuid#Must).

Although the examples presented use concrete types, making this generic should be trivial:

```go
func Must[T any](t T, err error) {
	if err != nil {
		panic(err)
	}

	return t
}
```

(Un)fortunately we cannot make this generic enough and let our helper handle an arbitrary number of arguments AND types arguments is returned, meaning that if func f() (string, string, error) we’d need to create a new helper with increased arity for every function returning an extra argument.

```go
func Must2[T1, T2 any](t1 T1, t2 T2, err error) (T1, T2) {
	return t1, Must(t2, err)
}

// And we can keep going.
func Must3(T1, T2, T3 any)(t1 T1, t2 T2, t3 T3, err error) (T1, T2, T3) {
	return t1, t2, Must(t3, err)
}
```

### Wrapping up

So why is this useful?
`Must` is intended for use in variable initialization which usually involves some degree of error handling. Error handling in Go has always been a hot topic and there are many angles from which we can look at it, any technique (no matter how small it is) will make our understanding of the problem better. It is also important to notice that being idiomatic in Go is key to writing code that will be easy to maintain, so if there’s is an idiom for handling errors, let’s use it.

#### Further readings:
- must.Do proposal https://github.com/golang/go/issues/54297
- must.Do package: https://pkg.go.dev/tailscale.com/util/must