Multi-valued expressions in Go

Expression describes computation of value and consists of operands (identifiers, literal, another expression between parentheses, …) which are applied to operators or functions (source code):

fmt.Println(1 + 2)
v := 3
fmt.Println(v * 2)
fmt.Println(float64(v * 3) / 2)
fmt.Println(map[string]int{"foo": 1}["foo"])
f := func(n int) int { return n * 2 }
g := func() int { return 4}
fmt.Println(f(g()))

Output:

3
6
4.5
1
8

We’re dealing above with expressions like arithmetic formulas, getting value associated with map’s key or function call. Golang supports more.

multi-valued expression

Single expression can return multiple values at once and then it’s called multi-valued expression:

func f() (int, int) {
return 1, 2
}
func main() {
fmt.Println(f())
}

In the snippet above, expression f() returns two integer values — each expression is separated by comma in corresponding return statement. Type of f’s return value isn’t any container like array or slice but literally two values (source code):

fmt.Println("%T", f())

It gives compilation-time error multiple-value f() in single-value context giving a proof that we’re dealing here with more than one value.

It isn’t required to use comma-separated expressions in return statement to create multi-valued expression from function’s call — it happens while using named result parameters too (source code):

func f(a, b int) int {
return a + b
}
func g() (a, b int) {
a = 1
b = 2
return
}
func main() {
fmt.Println(f(g())) // 3
}

Let’s see it in action where this multi-valued expression thingy might be actually useful…

Errors

One example from the standard library is io.Writer interface:

type Writer interface {
Write(p []byte) (n int, err error)
}

First return value n returns number of written bytes and err is non-nil if not all passed bytes have been written successfully. Another sample is omnipresent fmt.Println. It’s idiomatic code in Go to deal with errors this way — by returning another value which is not-nil when not everything went fine.

multi-valued argument in function call

Multi-valued expression can be used as an argument to function (or method) accepting exactly that number of parameters and their respective types as expression. Let’s see how it works (source code):

func f(a, b int) int {
return a + b
}
func g() (int, int) {
return 1, 2
}
func main() {
fmt.Println(f(g())) // 3
}

Expression g() is multi-valued. Golang allows to use such expressions in function calls and values of multi-valued expression will be passed to respective arguments. There are certain limitations though:

  • g() must have at least one return value. It seems quite clear since otherwise we could pass something as argument which doesn’t provide any value (source code):
func f() int {
return 1
}
func g() {
}
func main() {
fmt.Println(f(g()))
}

The compile will throw two errors: g() used as value and too many arguments in call to f.

  • Only multi-valued expression is passed as an argument and exactly one such expression can be used (source code):
func f(a, b, c int) int {
return a + b + c
}
func g() (int, int) {
return 1, 2
}
func main() {
fmt.Println(f(g(), 3))
}

Two errors while building will be raised: multiple-value g() in single-value context and not enough arguments in call to f. The same happens with passing more than one multi-valued expression (source code):

func f(a, b, c, d int) int {
return a + b + c + d
}
func g() (int, int) {
return 1, 2
}
func h() (int, int) {
return 3, 4
}
func main() {
fmt.Println(f(g(), h()))
}

Check linked playground to see exact errors.

variadic function

If the function which is called with multi-valued expression is variadic then such expression needs to return at least one value (source code):

func f(start int, nums ...int) int {
sum := start
for _, num := range nums {
sum += num
}
return sum
}
func g() int             { return 1 }
func h() (int, int) { return 1, 2 }
func i() (int, int, int) { return 1, 2, 3 }
func main() {
fmt.Println(f(g())) // 1
fmt.Println(f(h())) // 3
fmt.Println(f(i())) // 6
}

If we would have func g() {} then call f(g()) would return g() used as value error while attempting to compile the program.

multi-valued expression in return statement

Thanks to multi-valued expressions handling in function calls the code is more concise (source code):

func f(a, b int) int {
return a + b
}
func g() (int, int) {
return 1, 2
}
func main() {
a, b := g()
fmt.Println(f(a, b))
fmt.Println(f(g()))
}

Two calls f(a, b) and f(g()) have the same effect but former requires use of tuple assignment before.

Similar mechanism can be used in return statement. If multi-valued function call is used then such return statement returns more than one value (source code):

func f() (int, int) {
return g()
}
func g() (int, int) {
return 1, 2
}
func main() {
fmt.Println(f())
}

Other multi-valued expressions

Besides return statement there are other places where multi-valued expressions can be created. However multi-valued variant there is triggered only while using tuple assignment. Let’s see how it works with receive operator (multi-valued variants have also type assertions and retrieving key’s value from map).

The multi-valued receive operator returns sent value (or default value of element type) along with an indication of whether the channel is closed (source code):

c := make(chan int)
close(c)
_, ok := <-c
fmt.Println(ok) // false

Multi-valued version of receive operation cannot be used in function calls as stated earlier. Only tuple assignments hints the compiler that programmer wants to access two values, otherwise compiler will assume single-valued version (source code):

func f(int, bool) {}
func main() {
c := make(chan int)
f(<-c)
}

It gives a compilation error:

not enough arguments in call to f
have (int)
want (int, bool)