Methods and Interfaces

An interesting feature of Go is its use of interfaces. They are designed to provide many of the same benefits of object-oriented programming, but without the need for explicit classes or type inheritance as in C++ or Java.

Methods

Before we can discuss interfaces, we need to look at Go methods. Consider this code:

type Rectangle struct {
   width, height float64
}

func (r Rectangle) area() float64 {       // a method
    return r.width * r.height
}

func (r Rectangle) perimeter() float64 {  // a method
    return 2 * (r.width + r.height)
}

// main is a function
func main() {
   r := Rectangle{width:5, height:3}
   fmt.Printf("dimensions of r: width=%d, height=%d\n", r.width, r.height)
   fmt.Printf("      area of r: %.2f\n", r.area())
   fmt.Printf(" perimeter of r: %.2f\n", r.perimeter())
}

Look at the signatures for area() and perimeter(). The extra parameter in brackets before their names is called the method receiver, i.e. r is the receiver in perimeter(). The receiver is essentially an argument with some special privileges.

In main, dot-notation is used for calling methods, e.g. r.area() calls the area method on r.

Method receivers can, if the programmer desires, be passed as pointers to a type. For example, here r is passed a pointer because inflate modifies the object r points to:

func (r *Rectangle) inflate(scale float64) {
    r.width *= scale
    r.height *= scale
}

Without the *, r would be passed by value, meaning that the code in the body of inflate would modify this copy instead of the original object that calls the method.

Note that there is no change in the syntax for how the fields of r are accessed: the regular dot-notation is used. Similarly, inflate is called the same way, e.g. r.inflate(2.2). There is no special -> operator as in C/C++.

An important concept in Go is that of method sets. The Go language spec defines method sets like this:

A type may have a method set associated with it. The method set of an interface type is its interface. The method set of any other type T consists of all methods declared with receiver type T. The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T). Further rules apply to structs containing embedded fields, as described in the section on struct types. Any other type has an empty method set. …

The method set of a type determines the interfaces that the type implements and the methods that can be called using a receiver of that type.

So, in our example above, the method set of the type Rectangle is: {area, perimeter}, and the method set of *Rectangle is: {area, perimeter, inflate}.

Calling a method is defined like this in the Go spec:

A method call x.m() is valid if the method set of (the type of) x contains m and the argument list can be assigned to the parameter list of m. If x is addressable and &x’s method set contains m, x.m() is shorthand for (&x).m() ….

Interfaces

Here’s an example of a Go interface:

type Shaper interface {
    area() float64
    perimeter() float64
}

The name of this interface is Shaper, and it lists the signatures of the two methods that are necessary to satisfy it.

A type T implements Shaper if Shaper’s method set, {“area”, “perimeter”},is a subset of the method set for T. We saw above that the method set for Rectangle is {area, perimeter}, and the methods listed in Shaper are clearly a subset of this. Similarly, Rectangle* also implements Shape because the methods in Shape are a subset of the method set of Rectangle*, {area, perimeter, inflate}.

Importantly, only the signatures of the methods are listed in the interface. The bodies of the functions are not mentioned at all. The implementation is not part of the interface.

As another example, suppose we add this code:

type Circle struct {
    radius float64
}

func (c Circle) area() float64 {       // a method
    return 3.14 * c.radius * c.radius
}

func (c Circle) perimeter() float64 {  // a method
    return 2 * 3.14 * c.radius
}

func (c Circle) diameter() float64 {   // a method
    return 2 * c.radius
}

The method set of Circle is {area, perimeter, diameter}, and so it implements Shaper because it contains both area and perimeter. The method set of *Circle is also {area, perimeter, diameter}, and so *Circle also implements Shaper.

Now we can write code that works on any object of a type that implements Shaper. For example:

func printShape(s Shaper) {
    fmt.Printf("      area: %.2f\n", s.area())
    fmt.Printf(" perimeter: %.2f\n", s.perimeter())
}

All that printShape knows about s is that the methods in the method set for Shaper can be called on it. If s has methods not in the member set for Shaper, then those methods cannot be called on s.

Here’s how you could use printShape:

func main() {
   r := Rectangle{width:5, height:3}
   fmt.Println("Rectangle ...")
   printShape(r)

   c := Circle{radius:5}
   fmt.Println("\nCircle ...")
   printShape(c)
}

An interesting detail about Go interfaces is that you don’t need to explicitly tell Go that a struct implements an interface: the compiler figures it out for itself. This contrasts with, for example, Java, where you must explicitly indicate when a class implements an interface.

In summary:

  • The method set of an interface is all the methods named in the interface.
  • The method set of a type T (where T is not a pointer type) is all methods with a receiver of type T.
  • The method set of a type *T (where T is not a pointer type) is all methods with a receiver of type *T or T.
  • A type T implements an interface I if the method set of I is a subset of the method set of T.

Example: Using the Sort Interface

Lets see how we can use the standard Go sorting package.

Suppose we want to sort records of people. In practice, such records might contain lots of information, such as a person’s name, address, email address, relatives, etc. But for simplicity we will use this struct:

type Person struct {
    name string
    age int
}

For efficiency, lets sort a slice of pointers to Person objects; this will avoid moving and copying the name strings. To help with this, we define the type People:

type People []*Person  // slice of pointers to People objects

It turns out that it’s essential that we create the type People. The code we write below won’t compile if we use []*Person directly. That’s because the method set of []*Person does not satisfy sort.Interface. So we will add methods on People that make it implement sort.Interface.

For convenience, here’s a String method that prints a People object:

func (people People) String() (result string) {
    for _, p := range people {
        result += fmt.Sprintf("%s, %d\n", p.name, p.age)
    }
    return result
}

The name of this is String(), and it returns a string object. A method with this signature implements the fmt.Stringer interface, which allows it to be called by the print functions in fmt.

Now we can write code like this:

users := People{
            {"Mary", 34},
            {"Lou", 3},
            {"Greedo", 77},
            {"Zippy", 50},
            {"Morla", 62},
}

fmt.Printf("%v\n", users)  // calls the People String method

To sort the items in the users slice, we must create the methods listed in sort.Interface:

type Interface interface {
        // number of elements in the collection
        Len() int

        // returns true iff the element with index i should come
        // before the element with index j
        Less(i, j int) bool

        // swaps the elements with indexes i and j
        Swap(i, j int)
}

This interface is pre-defined in the sort package. Notice that this is a very general interface. It does not even assume that you will be sorting slices or arrays!

Three methods are needed:

func (p People) Len() int {
    return len(p)
}

func (p People) Less(i, j int) bool {
    return p[i].age > p[j].age
}

func (p People) Swap(i, j int) {
    p[i], p[j] = p[j], p[i]
}

Less is the function that controls the order in which the objects will be sorted. By examining Less you can see that we will be sorting people by age, from oldest to youngest.

With these functions written, we can now sort users like this:

users := People{
            {"Mary", 34},
            {"Lou", 3},
            {"Greedo", 77},
            {"Zippy", 50},
            {"Morla", 62},
}

fmt.Printf("%v\n", users)

sort.Sort(users)

fmt.Printf("%v\n", users)

To change the sort order, modify Less. For instance, this will sort users alphabetically by name:

func (p People) Less(i, j int) bool {
    return p[i].name < p[j].name
}

Another way to sort by different orders is shown in the examples section of the Go sort package documentation. The trick there is to create a new type for every different sort order.

Questions

  1. How would you explain the difference between functions and methods to a programmer who is not familiar with Go?
  2. What is a method receiver? How many different receivers can a method have?
  3. When should a method receiver be a pointer type?
  4. What is a method set? How are method sets determined for regular types, pointer types, and interface types?
  5. How would you use the sort package to sort a slice of pointers to ints?