Introduction - the Dog theory

Interfaces are a paragon feature of Go as a language, but they are rarely described in detail, or rather, explained by real code examples. I had a hard time getting productive with them and implementing them properly, just because I didn't see a sensible application example. I'll try to inspect interfaces in more depth, in a 3 article series going from theory, to real life example, finally ending with the ways in which popular packages handle interfaces.

Everyone who had anything to do with programming knows this oldie. Almost every book on Object oriented programming comes with a derivated form of Terrier extends Dog and bark() methods to explain the concept clearly. The caveat of that approach becomes apparent when you try to apply that to your stock market CLI application. Bear with me, while we get the theory in this post and embark on a journey to something way more concrete.

Definition

An acceptable definition I managed to find on Medium defines an interface as:

An interface is a collection of method signatures that an Object can implement. Hence interface defines the behavior of the object.

In Go, the interface takes the form of:

type SomeInterface interface {  
   DoSomething(arg string) string
   MethodTwo(arg int) bool
}

We now have an interface SomeInterface with two methods that define the behavior. And? What do I do with it? How do I implement it?

This is the most important piece of the puzzle. Something needs to implement that interface, and implementing interface means having an object that has at least these two methods with the same input arguments and return types. The big distinction in Go, compared to other languages, is that the implementation is implicit. There is no special keyword, you just either have the methods required or don't.

So in the classic Animal, Dog conundrum, a Go interface defining a Dog can be represented as follows:

type Dog interface {  
    Bark() string
    Name() string
    WillEat(string) bool
}

We now have an interface, but we still need some type that will implement it, i.e. a type that will have these methods (it can have 100 more, but in order to conform to the interface, the bare minimum is what is defined above).

Most of the applications of the interface type are related to structs. Which makes sense, they have some properties that can easily be translated into methods. So let's implement our interface for a dog breed.

type GermanShepherd struct {  
    name       string
    barkStyle  string
    likeTreats []string
}

To make our good boy implement our Dog interface we need him to be able to to Bark(), return what his Name() is determine whether he WillEat(string) what we give him. By using a struct for this, we can easily define all these things. Do we have to use struct? No! It makes sense in this example, since we want to give him all the properties, but the very same interface can be implemented if our good boy is just a simple string

type Maltese string  

It doesn't matter what object implements interface, but if it implements it. So let's take all good bois and gals and... make them implement the Dog interface.

type GermanShepherd struct {  
    name       string
    barkStyle  string
    likeTreats []string
}

func (gs GermanShepherd) Name() string {  
    return gs.name
}

func (gs GermanShepherd) Bark() string {  
    return gs.barkStyle
}

func (gs GermanShepherd) WillEat(offer string) bool {  
    for _, like := range gs.likeTreats {
        if like == offer {
            return true
        }
    }
    return false
}

So our GermanShepherd now implements Dog interface by means of having method Name() string, Bark() string and WillEat(string) bool). Our GermanShepherd is not a picky eater, but the implementation of that interface and the WillEat() method signature for a type Pomeranian struct might be completely different.

Let's quickly give the same to our little type Maltese string:

// as our little boy is just a string, we have to
// take a different approach

type Maltese string

func (m Maltese) Name() string {  
    // we assume that the each Maltese
    // is a string with name defined
    return string(m)
}

func (m Maltese) Bark() string {  
    return "Bark, bark..."
}

func (m Maltese) WillEat(offer string) bool {  
    if offer == "smelly dog treat" || offer == "chicken" {
        return true
    }
    return false
}

So now both Maltese and GermanShepherd implement our interface. Which means that they will be accepted as a Dog regardless of differences in how we actually coded programmed their behaviour. Let's confirm that compiler agrees by writing DogHeaven function.

func DogHeaven(dog Dog) {  
    fmt.Printf("Hey, %s. Welcome to dog heaven!\n", dog.Name())
    fmt.Println(dog.Bark())
    offeredTreat := "smelly dog treat"
    fmt.Printf("Have a %s, do you like it?\n", offeredTreat)
    f := dog.WillEat(offeredTreat)
    if f {
        fmt.Println("Good boy!")
    } else {
        fmt.Println("Sorry you don't like it...")
    }
}

The function accepts Dog type, i.e. any type that implements our interface. It doesn't care that Maltese is just a string, or that GermanShepherd has a way more sophisticated implementation. Let's initialize our dogs, give them some identity and see how they fit into the DogHeaven:

func main() {  
    bigDog := GermanShepherd{
        name:       "Hans",
        barkStyle:  "Rüff!",
        likeTreats: []string{"beef", "pork", "poultry"},
    }

    var smallDog Maltese = "Luca"

    DogHeaven(bigDog)
    fmt.Println()
    DogHeaven(smallDog)
}

And the result is:

Hey, Hans. Welcome to dog heaven!  
Rüff!  
Have a smelly dog treat, do you like it?  
Sorry you don't like it...

Hey, Luca. Welcome to dog heaven!  
Bark, bark...  
Have a smelly dog treat, do you like it?  
Good boy!

Program exited.  

What happens if we try to sneak in somebody in disguise like:

type SneakyCat string  
var cat SneakyCat = "Henry"  
DogHeaven(cat)  
./prog.go:83:11: cannot use cat (type SneakyCat) as type Dog in argument to DogHeaven:
    SneakyCat does not implement Dog (missing Bark method)

Go build failed.  

The only hope for Henry is that there is a Cat interface and a CatHeaven(cat Cat) function that can accept him...

Why is it useful?

Imagine that our DogHeaven would not use an interface. We would still want to accept all dogs. What we could do is to accept something generic, like an interface{}

func DogHeaven(dog interface{}) {  
}

Our new function would accept our Dog interface, our Maltese, GermanShepherd... but it would also accept var cat Cat = "Henry" an int and a bool. We would have to assert the type of each of them to be able to get rid of intruders like 15 or false.

Interfaces make Go programming easier, by offering a way of focusing on what entities do, rather than what they are.

In Part 2 we will solve an actual problem for a library that's used for downloading URLs.

Tags: