Brian Slesinsky's Weblog
 
   

Saturday, 07 Aug 2010

Go Puzzler

Russ Cox explained a while ago how interface types work in the original implementation of the Go language. But reading about an implementation when your grasp of the language is shaky is sometimes more confusing than helpful.

For example, I happened to see the beginning of a dispute on the mailing list [edit: which turned out to be just a misunderstanding] about whether interface values are copied by value or by reference. I had forgotten and couldn't find an answer in the Go spec, so I wrote a test program. See if you can guess what this program does without running it:

package main
  
import "fmt"
  
type scalable interface {
	scale(multiple int) scalable
	String() string
}
  
type point struct {
	x, y, z int
}
  
func (p point) scale(multiple int) scalable {
	p.x = p.x * multiple
	p.y = p.y * multiple
	p.z = p.z * multiple
	return p
}
  
func (p point) String() string {
	return fmt.Sprintf("(%d, %d, %d)", p.x, p.y, p.z)
}
  
func main() {
	p := point{x: 1, y: 2, z: 3}
	var a scalable = p
	fmt.Printf("before: %s\n", a)
	b := a
	result := a.scale(2)
	fmt.Printf("a after: %s\n", a)
	fmt.Printf("b after: %s\n", b)
	fmt.Printf("result: %s\n", result)
	p2 := result.(point)
	p2.x = 42
	fmt.Printf("result: %s\n", result)
}

I'll give the answer after some spoiler space:

.

.

.

.

.

.

.

.

.

  
before: (1, 2, 3)
a after: (1, 2, 3)
b after: (1, 2, 3)
result: (2, 4, 6)
result: (2, 4, 6)

The key to understanding the output is knowing that whenever you call a method in Go, the method sees a copy of its receiver. This is consistent with other languages when the receiver is either a pointer or an immutable value, but it's a surprise when the receiver is a struct. [1]

Here's where knowing a little about the implementation might help you guess wrong. When the value being wrapped by an interface won't fit in a machine word, the "gc" implementation uses a two word header with the second word pointing to some extra data. This allows all interface values to fit in the same size header, and makes them cheaper to pass around since the extra data doesn't need to be copied most of the time.

However, as far as I can tell, that's just an implementation detail. The Go designers have cleverly arranged so that there's no way to modify the extra data of an interface value without copying it first. So the presence of a pointer in the implementation doesn't necessarily mean copy-by-reference happens in the language.

Going back to my opening question, when you implement an interface, you can choose whether it acts like a value type or like a reference type. If you choose a data structure with no pointers, it will behave like an immutable value. If you use pointers then you'll get copy-by-reference semantics for the targets of the pointers. But there's no way to make a struct-like object that can be both copied by value (via assignment) and modified in place by calling methods on it. Even if you implement your objects with a struct, they will behave like an immutable object when you call methods.

So if you have a value of type interface{}, it could behave like a pointer or like a large chunk of immutable data. At a high level, it acts like void * in C or like an Object reference in Java, but the details are different, because you also get immutable objects for free.

[Edit: clarified second-to-last paragraph.]


[1] The rationale for this puzzling behavior is that Go copies structs when it passes them as regular parameters, so it should also copy them when passed in the receiver position. Very consistent, but not all that intuitive, since we ordinarily think of a method call as sending a message to a particular object.

The "mutating your receiver does nothing" behavior is sufficiently weird compared to other languages that I think Go might be easier to learn if it were a compiler error. There's nothing you can do with it that couldn't also be done by explicitly copying the receiver to a local variable first. The compiler could generate the same code and newcomers wouldn't be surprised by the copy. Otherwise, my guess is that we'll eventually have tools that emit a warning for this case.