Skip to content

Some evaluation orders in multi value assignments are unspecified

Go101 edited this page May 11, 2022 · 14 revisions

Given the following program, what do you expect to be printed?

package main

import "fmt"

func f(p *int) int {
	*p = 123
	return *p
}

func main() {
	var x int
	y, z := x, f(&x)
	fmt.Println(y, z)
}

Many gophers may expect it prints 0 123, which is also the print result of gccgo (v8.1.0). However, the standard Go compiler (v1.10.3, gc) prints 123 123 instead.

Which result is correct? In fact, both ones are correct. The reason is some evaluation orders in multi-value assignments are unspecified.

It is interesting that if the line y, z := x, f(&x) is changed to var y, z = x, f(&x), then both gc and gccgo print 0 123. (The inconsistency of gc doesn't means there is a bug in gc.)

Let's view another a bit modified example:

package main

import "fmt"

func f(p *int) int {
	*p = 123
	return *p
}

func g(x int) (a, b int) {
	return x, f(&x) // <=> a, b = x, f(&x); return
}

func main() {
	fmt.Println(g(0))
	// gc prints 123 123
	// gccgo prints 0 123
}

Then what evaluation order rules in multi-value assignments are specified in Go specification? Go specification defines the usual order.

, when evaluating the operands of an expression, assignment, or return statement, all function calls, method calls, and (channel) communication operations are evaluated in lexical left-to-right order.

Considering that

  1. a return statement can be viewed as an assignment statement plus a bare return, and
  2. method calls and (channel) communication operations can be viewed as function calls,

we can simplify the above description in Go specification as

..., when evaluating the operands of an expression or an assignment statement, all function calls (including method calls, and channel communication operations) are evaluated in lexical left-to-right order.

No other evaluation orders are specified in Go specification. This is why we say both of the print results of gc and gccgo for the first example above are correct.

Ok, we know the rules now, let's view another example:

package main

import "fmt"

func testChange() (int, interface{}, int) {
	m := 0
	i := 0

	f := func() int {
		m = 2
		i = 2
		return 99
	}

	return m, i, f()
}

func main() {
	m, i, j := testChange()
	fmt.Println(m, i, j)
}

What result should the above program print? Yes, it can be any of 0 0 99, 2 2 99, 2 0 99 and 0 2 99, depending on the relative evaluation orders of m, i and f() in the return m, i, f() statement.

In Go programming, we should avoid writing the code which behaviors are compiler dependent. We should split a multi-value assignment into several single-value assignments if there are dependency relations existing between the operands involved in the multi-value assignment. For example, we should rewrite the first example above as

package main

import "fmt"

func f(p *int) int {
	*p = 123
	return *p
}

func main() {
	var x int
	// y, z := x, f(&x)
	y := x
	z := f(&x)
	fmt.Println(y, z) // 0 123
}

Surely, we can also change the all the right operands (sources) into function calls to avoid ambiguities. However, this way is a little more verbose and less efficient. Another reason this way is not recommended is that, now gccgo has a bug (see the last example at the end of this article) which doesn't exactly obey the evaluation orders described above (see the example in the end for details).

func main() {
	var x int
	// y, z := x, f(&x)
	fx := func() int {return x}
	y, z := fx(), f(&x)
	fmt.Println(y, z)
}

BTW, in the end, as an exercise, what result should the following program print? (Hint: gc does correctly here, but gccgo does it wrongly.)

package main

import "fmt"

func f(p *int) int {
	*p++
	return *p
}

func main() {
	var x int
	x, z := f(&x), f(&x)
	fmt.Println(x, z)
}

Update: the title of this wiki page is not accurate. The unspecified behavior is not multi-value assignments specific, For example, it is ok for the following program to print either 1 9 or 9 2, depending on the i on the left is evaluated before f() or not.

package main

var x = []int{1, 2}
var i = 0

func f() int {
	i = 1
	return 9
}

func main() {
	x[i] = f()
	println(x[0], x[1])
}

[Update]: there is an example for argument passing: https://github.com/golang/go/issues/52146.

[Update 2]] another example: https://github.com/golang/go/issues/52811 (a bug of gccgo and gollvm)

Clone this wiki locally