Go's interfaces—static, checked at compile time, dynamic when asked for—are, for me, the most exciting part of Go from a language design point of view. If I could export one feature of Go into other languages, it would be interfaces.
This post is my take on the implementation of interface values in the “gc” compilers: 6g, 8g, and 5g. Over at Airs, Ian Lance Taylor has written two posts about the implementation of interface values in gccgo
. The implementations are more alike than different: the biggest difference is that this post has pictures.
Before looking at the implementation, let's get a sense of what it must support.
Usage
Go's interfaces let you use duck typing like you would in a purely dynamic language like Python but still have the compiler catch obvious mistakes like passing an int
where an object with a Read
method was expected, or like calling the Read
method with the wrong number of arguments. To use interfaces, first define the interface type (say, ReadCloser
):
type ReadCloser interface { Read(b []byte) (n int, err os.Error) Close() }
and then define your new function as taking a ReadCloser
. For example, this function calls Read
repeatedly to get all the data that was requested and then calls Close
:
func ReadAndClose(r ReadCloser, buf []byte) (n int, err os.Error) { for len(buf) > 0 && err == nil { var nr int nr, err = r.Read(buf) n += nr buf = buf[nr:] } r.Close() return }
The code that calls ReadAndClose
can pass a value of any type as long as it has Read
and Close
methods with the right signatures. And, unlike in languages like Python, if you pass a value with the wrong type, you get an error at compile time, not run time.
Interfaces aren't restricted to static checking, though. You can check dynamically whether a particular interface value has an additional method. For example:
type Stringer interface { String() string } func ToString(any interface{}) string { if v, ok := any.(Stringer); ok { return v.String() } switch v := any.(type) { case int: return strconv.Itoa(v) case float: return strconv.Ftoa(v, 'g', -1) } return "???" }
The value any
has static type interface{}
, meaning no guarantee of any methods at all: it could contain any type. The “comma ok” assignment inside the if
statement asks whether it is possible to convert any
to an interface value of type Stringer
, which has the method String
. If so, the body of that statement calls the method to obtain a string to return. Otherwise, the switch
picks off a few basic types before giving up. This is basically a stripped down version of what the fmt package does. (The if
could be replaced by adding case Stringer:
at the top of the switch
, but I used a separate statement to draw attention to the check.)
As a simple example, let's consider a 64-bit integer type with a String
method that prints the value in binary and a trivial Get
method:
type Binary uint64 func (i Binary) String() string { return strconv.Uitob64(i.Get(), 2) } func (i Binary) Get() uint64 { return uint64(i) }
A value of type Binary
can be passed to ToString
, which will format it using the String
method, even though the program never says thatBinary
intends to implement