Go: Methods, Interfaces and Generics

Martin Sulzmann

Overview

Methods

We can define methods on named types where we can use the “dot” notation to call methods.

Named types

type rectangle struct {
    length int
    width  int
}

Named types are types defined via type. Via the type keyword we introduce a (fresh) name for an existing type.

Structurally rectangle and the (unamed) type

struct {
    length int
    width  int
}

are the same but rectangle is a new type.

Hence, we cannot assign a variable of the above (unamed) type to a variable of type rectangle (and vice versa).

    var r rectangle
    var r1 struct {
        length int
        width  int
    }

    // r = r1  // NOT OK
    r = (rectangle)(r1)  // OK cast required

Method definitions

func (r rectangle) area() int {
    return r.length * r.width
}

The named type argument preceeds all other arguments (in this example, area has no further arguments). There is no self or this. Struct values are always referenced by name.

Method calls

We call methods by using the “dot” notation.

r.area()

In the above definition of method area on named type rectangle, the argument r is passed to area as a value. We must use call-by reference if we wish to update r’s field values.

Methods and call-by reference

func (r *rectangle) scale(s int) {
    r.length = r.length * s
    r.width = r.width * s
}

The * indicates that we pass r to scale by reference. Thus, the update is globally visible.

In Go, the compiler inserts the * and & operator automatically. For example,

r.scale(3)
(&r).scale(3)

The above statements are equivalent. Go will automatically perform the conversion.

Complete example

package main

import "fmt"

type rectangle struct {
    length int
    width  int
}

func (r rectangle) area() int {
    return r.length * r.width
}

func (r *rectangle) scale(s int) {
    r.length = r.length * s
    r.width = r.width * s
}

func main() {
    var r1 rectangle = rectangle{1, 2}
    var r2 rectangle = rectangle{width: 2, length: 1}
    r3 := rectangle{width: 2, length: 1}
    r3.scale(3)

    fmt.Printf("%d \n", r1.area()+r2.area()+r3.area())

}

Overloaded methods

Go supports method overloading where method dispatch is based on the “receiver” type. The receiver is simply the object on which the method is called.

type rectangle struct {
    length int
    width  int
}

type square struct {
    length int
}


func (r rectangle) area() int {
    return r.length * r.width
}

func (r square) area() int {
    return r.length * r.length
}

Limitations

Suppose we have several geometric objects (such as rectangle and square). Each geometric object supports the area method.

We wish to sum up the area of an arbitrary object. Our current best bet is to enumerate all the cases.

func sum1(r1 rectangle, r2 rectangle) int {
    return r1.area() + r2.area()
}

func sum2(r rectangle, s square) int {
    return r.area() + s.area()
}

func sum3(s square, r rectangle) int {
    return r.area() + s.area()
}

func sum4(s1 square, s2 square) int {
    return s1.area() + s2.area()
}

This is rather tedious. There is lots of code duplication and the programmer needs to manually select the appropriate “sum” function. As we will see shortly, interfaces allows us to provide for a common interface to objects that support common methods.

Interfaces and structural subtyping

Interface:

Structural subtyping:

Subtyping principle:

Geometric shapes example

type shape interface {
    area() int
}

The receiver is left implicit in the interface declaration.

Interfaces are types.

func sumArea(x, y shape) int {
    return x.area() + y.area()
}

What are possible values of type shape?

Any value of type s that is a structural subtype of shape? Any two values passed to the sumArea function must s

Recall that rectangle and square both implement the area method.

func (r rectangle) area() int {
    return r.length * r.width
}

func (s square) area() int {
    return s.length * s.length
}

Hence, values of type rectangle and square are also of the interface type shape. That is, we find

For example, consider the following.

    var r rectangle = rectangle{1, 2}
    var s square = square{3}

    x2 := sumArea(r, s)  // applied on (rectangle, square)
    x2b := sumArea(r, r) // applied on (rectangle, rectangle)

    fmt.Printf("%d %d \n", x2, x2b)

We say that sumArea is a generic function because sumArea can be applied on values of different types.

Geometric shapes that scale

An interface may consist of several (overloaded) methods. We can also extend an existing interface.

type shapeExt interface {
    shape
    scale()
}

Here is a function that makes use of the extended shape interface.

func sumAreaScaleBefore(n int, x, y shapeExt) int {
    x.scale(n)
    y.scale(n)
    return x.area() + y.area()
}

Can we call sumAreaScaleBefore with rectangles and squares?

Recall the earlier method definitions.

func (r rectangle) area() int {     // A1
    return r.length * r.width
}

func (s square) area() int {        // A2
    return s.length * s.length
}

func (r *rectangle) scale(x int) {  // S1
    r.length = r.length * x
    r.width = r.width * x
}

func (s *square) scale(x int) {     // S2
    s.length = s.length * x
}

What about the following?

sumAreaScaleBefore(3, r, s)

This will not compile and yields a type error.

The following works!

sumAreaScaleBefore(3, &r, &s)

Why?

We can argue that *rectangle <= shapeExt and *square <= shapeExt for the following reason

  1. *rectangle implements the scale method, see S1

  2. rectangle implements the area method, see A1

  3. Any value receiver definition implies a pointer receiver.

  4. Hence, *rectangle implements the area method as well.

  5. Hence, *rectangle <= shapeExt

Regarding 3. Consider

    var rPtr *rectangle = &r
    rPtr.area()

The above method call will be transformed to

    (*rPtr).area()

Hence, we can argue that *rectangle (pointer to rectangle) the area method as well.

Structural subtyping boils down to set membership

Among interfaces, the subtype relation is structural. Hence, the following interface declarations are all equivalent.

type shapeExt interface {
    shape
    scale(int)
}

type shapeExt2 interface {
    scale(int)
    shape
}

type shapeExt3 interface {
    scale(int)
    area() int
}

type shapeExt4 interface {
    area() int
    scale(int)
}

Complete example

package main

import "fmt"

// Example

type rectangle struct {
    length int
    width  int
}

type square struct {
    length int
}

func (r rectangle) area() int {
    return r.length * r.width
}

func (s square) area() int {
    return s.length * s.length
}

func (r *rectangle) scale(x int) {
    r.length = r.length * x
    r.width = r.width * x
}

func (s *square) scale(x int) {
    s.length = s.length * x
}

type shape interface {
    area() int
}

type shapeExt interface {
    shape
    scale(int)
}

func sumArea(x, y shape) int {
    return x.area() + y.area()
}

func sumAreaScaleBefore(n int, x, y shapeExt) int {
    x.scale(n)
    y.scale(n)
    return x.area() + y.area()
}

func test() {
    var r rectangle = rectangle{1, 2}
    var s square = square{3}
    var rPtr *rectangle = &r

    x0 := rPtr.area()
    // (&rPtr).area()
    fmt.Printf("%d \n", x0)

    x1 := r.area() + s.area()

    fmt.Printf("%d \n", x1)

    x2 := sumArea(r, s)  // applied on (rectangle, square)
    x2b := sumArea(r, r) // applied on (rectangle, rectangle)

    fmt.Printf("%d %d \n", x2, x2b)

    pt := &r

    x3 := pt.area()
    // Implicit conversion to
    // (*pt).area()
    //
    // Hence, any "value" receiver also implies the corresponding "pointer" receiver.

    fmt.Printf("%d \n", x3)

    //  x3 := sumAreaScaleBefore(3, r, s)
    //
    // "rectangle does not implement shapeExt (scale method has pointer receiver)"
    // same applies to square

    x4 := sumAreaScaleBefore(3, &r, &s)
    // Works because any method definition for a value,
    // implies a method definition for the pointer.

    fmt.Printf("%d \n", x4)

}

func main() {

    test()

}

Structural subyping versus nominal subtyping (Java)

// Java example (pseudo-code)
//////////////////////////////////////

class Shape {
  int area();
}

class ShapeExt extends Shape {
 scale(int);
}

class ShapeExt2 extends Shape {
 scale(int);
}

From the above class declarations we derive

where <= denotes the subtype relation.

This form of subtyping is referred to as nominal subtyping. The subtype relations are explicitly declared via class hierarchies.

“Any” Interface and type assertions

    interface{}

The “any” interface. For any type t we find that t <= interface{}. This is similar to the type Object in Java.

A value of type interface{} can be anything. So, we need to perform some run-time type casts to access the actual value.

func any(anything interface{}) {
    switch v := anything.(type) {
    case int:
        fmt.Printf("some int %d \n", v)
    case rectangle:
        fmt.Println(v)
        r := anything.(rectangle)
        fmt.Printf("length = %d, width = %d \n", r.length, r.width)
    default:
        fmt.Println("don't know")
    }

}

We can also cast to a specific type, see anything.(rectangle)

Such a cast may fail

We can catch failure via

    r, ok := anything.(rectangle)

ok equals false in case the cast fails

BTW, Go automatically performs a break after each case.

Why would you need interface{} in Go (or Object in Java)?

Complete example

package main

import "fmt"

type rectangle struct {
    length int
    width  int
}

func any(anything interface{}) {
    switch v := anything.(type) {
    case int:
        fmt.Printf("some int %d \n", v)
    case rectangle:
        fmt.Println(v)
        r := anything.(rectangle)
        fmt.Printf("length = %d, width = %d \n", r.length, r.width)
    default:
        fmt.Println("don't know")
    }

}

func main() {
    any(1)

    any(rectangle{1, 2})

}

Generic Go

“Generics” allow us to abstract over type parameters.

Thus, we can write more type-safe programs.

Without generics

Some collection.

type pair struct {
    left  interface{}
    right interface{}
}

Some convenience functions.

func first(x pair) interface{} {
    return x.left
}

func second(x pair) interface{} {
    return x.right
}

Some application.

    var s string
    var f float32

    s = "Erdinger"
    f = 0.9

    product := pair{s, f}

    var price float32

    price = second(product).(float32) // Type assertions!

Type assertions may fail!

price = first(product).(float32)

With generics

Generic collection.

type pairG[T any, S any] struct {
    left  T
    right S
}

Making the above code more type-safe.

    var s string
    var f float32

    s = "Erdinger"
    f = 0.9

    product := pairG[string, float32]{s, f}

    var price float32

    price = secondG(product)

Consider

    price = secondG(product)

Complete source code

package main

import (
    "fmt"
)

type pair struct {
    left  interface{}
    right interface{}
}

func first(x pair) interface{} {
    return x.left
}

func second(x pair) interface{} {
    return x.right
}

func testPair() {

    var s string
    var f float32

    s = "Erdinger"
    f = 0.9

    product := pair{s, f}
    // string <= interface{}
    // float32 <= interface{}

    var price float32

    price = second(product).(float32) // Type assertions!
    // price = first(product).(float32)
    // Type checks but fails at run-time.

    fmt.Printf("%f", price)

}

// Generics

type pairG[T any, S any] struct {
    left  T
    right S
}

func firstG[T any, S any](x pairG[T, S]) T {
    return x.left
}

func secondG[T any, S any](x pairG[T, S]) S {
    return x.right
}

func testPairG() {

    var s string
    var f float32

    s = "Erdinger"
    f = 0.9

    product := pairG[string, float32]{s, f}

    var price float32

    price = secondG(product)
    // price = firstG(product)
    // Fails to type check!

    fmt.Printf("%f", price)

}

func main() {
    testPair()
    testPairG()
}

Summary

Traditional OO approach

// OO sketch Java style

class Shape {
  int area();
}

class Square : Shape {
  int x;
  int area() { return x * x; }
}

class Rectangle : Shape {
  int x, y;
  int area() { return x * y; }
}


// Virtual method resolution
int shapeTwo(Shape sh1, sh2) {
    return sh1.area() + sh2.area()
}

Go’s approach

Method overloading.

Thanks to interfaces we can model abstract data types.

Interfaces + overloaded methods can be used to mimic algebraic data types + pattern matching.