In Go, as in most programming languages, the return value of a function g()
can be used as an argument of another function f()
.
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
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 methodf
, then the callf(g(_parameters_of_g_))
will invokef
after binding the return values ofg
to the parameters of f in order.
Effectively this piece of code will work too.
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
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 helper that works in conjunction with the Parse
function family (ParseFS, ParseFiles, 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() fits in Must().
Although the examples presented use concrete types, making this generic should be trivial:
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.
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
Top comments (0)