Some evaluation orders in multi value assignments are unspecified
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
- a return statement can be viewed as an assignment statement plus a bare
return
, and - 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)
Please follow the official Twitter account of Go 101, @go100and1, to learn some Go details, facts, tips, etc, and read some Go articles from time to time.
Go 101 is a series of books about Go programming.
Books in Go 101 series
- Go (Fundamentals) 101 focuses on Go syntax/semantics (except custom generics related) and all kinds of runtime related things.
- Go Optimizations 101 provides some code performance optimization tricks, tips, and suggestions.
- Go Details & Tips 101 collects many details and provides several tips in Go programming.
- Go Generics 101 explains Go custom generics in detail.
Tapir started writing the Go 101 books and maintaining the go101.org website since 2016. New contents will continue being added to the books and the website from time to time. If you would like to, you can also support Go 101 by playing Tapir's games (for both Android and iPhone/iPad):
- Color Infection (★★★★★) - a physics based casual puzzle original game. 140+ levels.
- Rectangle Pushers (★★★★★) - a casual puzzle original game. 104+ levels.
- Let's Play With Particles - a casual action original game. Three game modes are included.