Some evaluation orders in multi value assignments are unspecified

Go101 edited this page Sep 26, 2018 · 7 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) 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.

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(123))
}

This time, both gccgo and gc (the standard Go compiler) print 123 123. It looks gccgo is a little inconsistent with itself (but without violating the specified evaluation order rules in multi-value assignments).

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)
}
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.