Go (Golang) - understanding the object oriented features with structs, methods, and interfaces

Published on 2019-11-02. Modified on 2023-11-03.

Object-oriented programming is just a programming paradigm that organize code in a different way from that of procedural programming. It is an extension of procedural programming, and it is about avoiding a global scope and about sharing resources between functions. Let's take a look at some of the "object-oriented" features of Go.

Table of contents

A small trip down memory lane of programming paradigms

Let's take a small trip down memory lane of programming paradigms. It will help us firmly grasp what "objects" and their related aspects are all about.

Non-structered programming

Non-structered programming is the earliest way of programming. Assembly language and early versions of FOCAL, BASIC, Fortran, and COBOL, among others, are examples of programming languages that follow a non-structured way in coding.

This means that code is written in a sequential manner with unstructured jumps to labels or addresses of instruction. The lines are typically numbered or labeled.

This is a simple example in BASIC.

10 INPUT "What is your name: "; N$
20 INPUT "How old are you: "; A$
30 PRINT "Hello "; N$
40 IF A$ > 80 THEN GOTO 60
50 PRINT "You are still young!"
60 PRINT "You are getting old!"
70 END

Non-structured programming is not suitable for the development of large programs and it does not allow re-usability of code. You cannot avoid breaking the rule of DRY. Non-structured programming became famous for creating spaghetti code and as a result "structured programming" was invented.

Structured programming

Structured programming, in contrast to non-structured programming, tries to improve the clarity, quality, and development time by making more extensive use of control flow constructs such as loops and conditions with if, then, else, endif statements, and iterations of blocks with for, while, do..until statements.

Blocks are also used to group statements together.

This is a simple example of a conditional statement in C:

if (age < 80) {
    printf("You are still young!\n");
} else {
    printf("You are getting old!\n");
}

Procedural programming

Procedual programming takes structured programming one step further by adding functions. Functions are also called "procedures" based on the concept of a "procedure call".

Sometimes people call procedural programming "structured procedural programming" because they want to emphasize that "procedures", i.e. functions, was added to the structured way of programming.

With functions it becomes possible to organize code into unique blocks that can be reused and it becomes much easier to avoid the problems of DRY. This makes the overall organization of code much better.

This is a very simple example of the usage of a function in PHP:

function add_integers(int $a, int $b): int
{
    return $a + $b;
}

echo add_integers(3, 5);

In procedural programming all functions are available to any part of a program as global functions. In small programs this isn't a problem, but as the size of a program grows, a small change to one function can affect many other functions if they are depending on that particular function. This can particularly be a problem when huge teams of people work together on the same code base.

Functional programming

Functional programming takes the concept of functions a step further. In functional programming, functions are treated as "first-class citizens", meaning that they can be assigned to variables, passed as arguments to other functions, and returned from other functions. Functional programming encourages that programs are written mostly using functions.

In functional programming a pure function is one that relies only on its inputs to generate its result, it cannot have access to any global state, and given the same input, it will always produce the same result. Furthermore, it must not produce any side effects (any change outside the function's own local scope).

Functional programming also relies on the idea that code modularity and the absence of side effects makes it easier to identify and separate responsibilities within the codebase. This therefore improves the code maintainability.

Functional programming has its roots in academia and it has historically been less popular, but many functional languages are seeing use today in industry.

Object-oriented programming

Object-oriented programming takes procedural programming a couple of steps further.

Object-oriented programming started out as a new technique which allowed data to be divided into separated scopes called "objects". Only specific functions belonging to the same scope could access the same data. This is called encapsulation.

In the beginning objects where not called objects, they where just viewed upon as separate scopes. Later when dependencies were reduced and connections between functions and variables inside these scopes where viewed upon as isolated segments, the result gave birth to the concepts of "objects" and "object-oriented programming".

Later, I believe it was mainly due to the development of Java, certain terms, jargon and buzz-words arose and a function was no longer called a function when it resided inside a separate scope, instead it was called "a method". Variables was also no longer called variables, but was termed "attributes" when they resided inside a separate scope.

An object is in essence simply a collection of functions and variables now referred to as "methods and attributes".

Encapsulation is the process of putting data, such as variables, constant, arrays, lists, and other types of data, and functions into a single unit. Encapsulation means to "encase" or "enclose" something, and it is achieved in, for example, Java, C++, and PHP by the class concept. By putting related data and functions into a single unit, we can better organize our code into separate blocks with related features and functionality.

Abstraction is the process of hiding and securing data from other functions. In Java, C++, and PHP, classes are used to encapsulate and keywords such as private and public determine exactly how the scope of classes and methods work, they hide or expose the attributes and methods of the class. Abstraction focuses on just the relevant data of an object and hides all the details which may or may not be for generic or specialized behavior. It hides the background details and emphasizes on the essential points to reduce complexity and increase efficiency.

Inheritance is the process of obtaining the attributes and methods from one class to another class. With inheritance you avoid DRY as you can avoid duplicating code across multiple classes and functions. Instead classes can inherit functions of other classes and even expand their functionality. Inheritance is a way to reuse code and allow independent extension of the software via public classes and interfaces. The relationships of objects give rise to a hierarchy. Inheritance was invented in 1967 for the programming language Simula 67.

Polymorphism is the ability in programming to present the same interface for different functionality. Poly means "many" and "morph" means change or form. It's the ability to have multiple functions with the same name, yet with different functionality or implementation. With polymorphism we can have 3 different functions all called printOut, depending on the context one function could output HTML, another JSON, and the last XML.

All-in-all we can say that object-oriented programming is all about organizing code. It is an extension of procedural programming, and it is about avoiding a global scope and about sharing resources between classes and functions. You extend functions by "borrowing" their blueprints without actually affecting the original code. And you override functions without affecting the original code. And you isolate scope by putting methods (functions) and attributes (variables) into classes.

So object-oriented programming relies on these four characteristics: Encapsulation, abstraction, inheritance, and polymorphism.

How these different ideas are implemented vary from programming language to programming language.

In my humble opinion, object-oriented programming has some good ideas, but in general its usefulness is highly exaggerated. Often object-oriented code is very verbose and complex whereas procedural code seems to be the sweet spot.

I cannot avoid to think about a Danish proverb that says:

Too little and too much spoils everything.

Enter the world of Go

When Ken Thompson, Rob Pike and Robert Griesemer, began working on the Go programming language they thought long and hard about how the world of programming had evolved, all the different concepts that existed, and how the different programming languages implemented these concepts. They decided to only add features which usefulness they all agreed upon.

NOTE: Robert Griesemer gave a really good talk about The Evolution of Go at GopherCon 2015. The slides from his talk can be found at https://talks.golang.org/2015/gophercon-goevolution.slide

I have done a lot of programming in PHP, from back when PHP didn't have any object-oriented features. In the beginning some of the object-oriented features benefited PHP programming. However, the majority of the PHP community suddenly got frantic about object-oriented programming, so much so that so-called "modern PHP" became synonym to "object-oriented PHP". Rather than keeping a balanced approach many PHP developers became obsessed and they started changing projects with well structured procedural code into object-oriented code. Not only that, but it became almost Taboo to do coding in the procedural paradigm.

In my humble opinion, this has been a very big mistake. It has not only contributed greatly to added complexity, but it also got people obsessed with design patterns and object-oriented theory. Today many PHP projects perform horribly because of the added object-oriented mess. Rather than having discrete function calls in well structured code, we now have tons of classes that inherit classes with tons of nested methods.

Abstraction is powerful. What I'm really allergic to, and what I had a reaction to in the '90s, was all the CORBA, COM, DCOM, object-oriented nonsense. Every startup of the day had some crazy thing that would take 200.000 method calls to start up and print "Hello world". That's a travesty! You don't want to be a programmer associated with that sort of thing.

― Brendan Eich in Coders at work - Reflections on the Craft of Programming

I felt frustrated and ended up creating the website PHP The Wrong Way.

When I found Go it was love at first sight. A lot of the frustration I had been dealing with in the PHP community did not exist in Go, and the best part was that that was by design.

Robert Griesemer says it beautifully in his talk (also shown on page 18 in his slides):

Complex object oriented code is modern analog to unstructured spaghetti code of 1970.

The Go FAQ explains the following to the question about whether Go is an object-oriented language:

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of "interface" in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous, but not identical, to sub-classing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, "unboxed" integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes "objects" in Go feel much more lightweight than in languages such as C++ or Java.

We can say that Go has a very lightweight feature set for object-oriented programming, yet it captures the very best of object-oriented programming and discards all the things that makes object-oriented programming complex. We get the fundamental useful features of object-oriented programming, without any unnecessary bloat.

In Go we can do encapsulation, abstraction, and polymorphism, and to a minor degree inheritance, but the way we do it is nothing like how it is done in other programming languages, and what we use to do it are simple extensions of the features we also use for procedural programming. This makes object-oriented programming simple in Go, but at the same time just as powerful.

However, it is also important to note that just because Go has object-oriented features doesn't mean that we automatically must use them whenever we code. If what we do fits perfectly well in a procedural structure, we don't need to go out of our way to turn it into an object-oriented implementation. We should use the object-oriented features when they make really good sense, not just because they are there.

Object-oriented programming is about better code organization, not about principles that must be implemented at the cost of simplicity. If we do object-oriented programming when it isn't needed, the resulting code doesn't become better organized, and it certainly doesn't become more performing.

Structs in Go

NOTE: Please keep in mind that the examples below are ridiculously simple. Also, in order for types and methods to be useful outside of their package scope, they need to be exported, I haven't done that in the examples. The examples are only meant to briefly touch the subject of some of the object-oriented features in Go.

The idea of encapsulating data together with functions existed before the object-oriented languages were developed. Objects can be implemented in classical languages even such as C using separate compilation or structs to provide the encapsulation.

Structs in Go is a way to effectively organize elements together into one block. It is a way to "compose a structure" of different types of data together. Each element of a struct is called "a field", and each field has a name and a type attached to it.

An defined struct with multiple fields looks like this:

type person struct {
    firstName string
    lastName  string
    age       int
    isVegan   bool
}

Because we define a "struct type" with the type keyword, we can define other structs with the same field names:

type superhero struct {
    firstName string
    lastName  string
    age       int
    isVegan   bool
    canFly    bool
    isFast    bool
}

The fields has been encapsulated in their respective structures and we now need to address each separately.

Every person has a name and every superhero has a name, but not every person is a superhero. As such it makes sense to keep these items separated. Especially if we want to make modifications to the structures by adding unique fields later.

In object-oriented programming, inheritance is a way to reuse the code of existing objects, or to establish a subtype from an existing object. Objects are defined by classes. Classes can inherit attributes and behavior from pre-existing classes called base classes or super classes. The resulting classes are known as derived classes or sub-classes. A sub-class inherits all the attributes (methods, etc) of the parent class. This means that a sub-class will have everything that its "parent" class have. We can then change (override) some or all of the attributes to change the behavior. We can also add new attributes to extend the behavior.

Go doesn't have classes, but as the Go FAQ stated, we can also do something which is analogous, but not identical, to sub-classing or inheritance in Go. This is called embedding.

Since every superhero is a person, we can have the superhero struct embed the common fields from the person struct. When we take one struct and embed it in another struct, the inner type gets promoted to the outer type.

Let's see how we can do that and assign values to the embedded fields and use them.

package main

import "fmt"

type person struct {
    firstName string
    lastName  string
    age       int
    isVegan   bool
}

type superhero struct {
    person   // Embedded from the person struct.
    canFly   bool
    isFast   bool
}

func main() {

    p1 := person {
        firstName: "Lois",
        lastName: "Lane",
        age: 34,
        isVegan: false,
    }

    // This is how we can assign values to the embedded fields.
    p2 := superhero {
        person: person {
            firstName: "Clark",
            lastName: "Kent",
            age: 37,
            isVegan: false,
        },
        canFly: true,
        isFast: true,
    }

    fmt.Println(p1)
    fmt.Println(p2)
    fmt.Println(p2.firstName, p2.lastName, "is really Superman!")
}

Try on The Go Playground

This will print:

{Lois Lane 34 false}
{{Clark Kent 37 false} true true}
Clark Kent is really Superman!

As mentioned in the Go FAQ embedded structs in Go is not identical to object-oriented inheritance, it is only analogous to it. The reason is that a defined struct type may have methods associated with it, but it does not inherit any methods bound to a given embedded type of another struct.

Embedding and inheritance are not synonymous.

In Effective Go it is stated that:

Go does not provide the typical, type-driven notion of sub-classing, but it does have the ability to "borrow" pieces of an implementation by embedding types within a struct or interface.

If we insist on using the term inheritance, we have to consider the restrictions that Go places on it. In order to avoid any confusion it is much better to use the Go terminology. In Go the above example is called embedding structs, not inheritance.

You can read more about embedding in the Go language specification and Effective Go.

Methods in Go

We can work with a normal function in Go like this:

package main

import "fmt"

// Declare a function that takes a string as an argument
func printName(name string) {
    fmt.Println(name, "is the name.")
}

func main() {

    // Declare some variables
    a := "Clark Kent"
    b := "Lois Lane"

    // Call the function with the variables as its argument
    printName(a)
    printName(b)
}

Try on The Go Playground

We can also encapsulate a bit using structs:

package main

import "fmt"

type superhero struct {
    name string
}

type person struct {
    name string
}

func printName(name string) {
    fmt.Println(name, "is the name.")
}

func main() {

    // The variable "a" gets declared and assigned the value
    // of the field "name" of the type "superhero"
    a := superhero {name: "Clark Kent"}

    // The variable "b" gets declared and assigned the value
    // of the field "name" of the type "person"
    b := person {name: "Lois Lane"}

    printName(a.name)
    printName(b.name)
}

Try on The Go Playground

We can also turn our function into two separate functions each dealing with their own data structure.

package main

import "fmt"

type superhero struct {
    name string
}

type person struct {
    name string
}

func printSuperheroName(name string) {
    fmt.Println(name, "- this person is a superhero")
}

func printPersonName(name string) {
    fmt.Println(name , "- this person is just a normal person")
}

func main() {
    a := superhero {name: "Clark Kent"}
    b := person {name: "Lois Lane"}

    printSuperheroName(a.name)
    printPersonName(b.name)
}

Try on The Go Playground

In the above example the only difference between the two functions is that they print out different messages, but it's just a simple example. Imagine each function doing much more.

We have a problem however. We have used two different structures in order to encapsulate data of the different types of people, yet we could mess up by inputting the wrong data into the wrong function. Imagine a huge code base with something like this happening:

printSuperheroName(b.name)
printPersonName(a.name)

Rather than printing:

Clark Kent - this person is a superhero
Lois Lane - this person is just a normal person

We now get this result:

Lois Lane - this person is a superhero
Clark Kent - this person is just a normal person

In order to better organize our code and at the same time prevent the mistake above, we can turn each function into a "method".

In Go a method is a function with a receiver. The receiver receives data from a struct. We can specify a specific type for the receiver in order to make the function belong to that structure. When a function has a receiver we can say that it "receives it's data types from a specific structure" and it becomes attached to that structure.

A normal function declaration without a return value looks like this:

func function(arguments) {}

A function declaration without a return value, but with a receiver looks like this:

func (receiver) function(arguments) {}

The function is now called "a method".

NOTE: You can only declare a function with a receiver whose type is defined in the same package as the function. You cannot declare a function with a receiver whose type is defined in another package (which includes the built-in types such as int).

Let's try to use some of this.

package main

import "fmt"

type superhero struct {
    name string
}

type person struct {
    name string
}

func (r superhero) printName() {
    fmt.Println(r.name, "- this person is a superhero")
}

func (r person) printName() {
    fmt.Println(r.name, "- this person is just a normal person")
}

func main() {
    a := superhero {name: "Clark Kent"}
    b := person {name: "Lois Lane"}

    superhero.printName(a)
    person.printName(b)
}

Try on The Go Playground

Notice that we have changed the name of the methods so they have the same name, but different receivers. Both methods are now called printName, which makes sense because they do the same thing. However, they work on different types of data, a person and a superhero is two different types. By attaching specific receiver types we encapsulate and abstract the different data they work on.

The output becomes:

Clark Kent - this person is a superhero
Lois Lane - this person is just a normal person

If we switch the input by mistake:

superhero.printName(b)
person.printName(a)

The compiler will now complain:

./prog.go:25:21: cannot use b (type person) as type superhero in argument to superhero.printName
./prog.go:26:18: cannot use a (type superhero) as type person in argument to person.printName

Try on The Go Playground

A method can still take normal arguments, but they come after the receiver argument.

package main

import "fmt"

type superhero struct {
    name string
}

type person struct {
    name string
}

func (r person) printName() {
    fmt.Println(r.name, "- this person is just a normal person")
}

func (r superhero) printName() {
    fmt.Println(r.name, "- this person is a superhero")
}

// This method has a reveiver but also take an argument.
func (r superhero) whois(age int) {
    if r.name == "Clark Kent" {
        fmt.Println(r.name, "is really Superman")
    }
    if r.name == "Bruce Wayne" {
        fmt.Println(r.name, "is really Batman")
    }
    fmt.Println("He is", age, "years old")
}

func main() {
    a := superhero {name: "Clark Kent"}
    b := person {name: "Lois Lane"}

    superhero.printName(a)
    superhero.whois(a, 89)
    person.printName(b)
}

Try on The Go Playground

Have you noticed that even though the methods printName doesn't take any arguments, we still call the methods with arguments?

That is a bit strange, but the Go language specification, under method values, explains the following:

If the expression x has static type T and M is in the method set of type T, x.M is called a method value. The method value x.M is a function value that is callable with the same arguments as a method call of x.M.

This basically means that the method MyType.Foo is treated as a function that accepts an argument of type MyType, and then calls it's Foo method on that object.

This:

a := person{name: "Clark Kent"}
person.printName(a)

Is the same as:

person.printName(person{name: "Clark Kent"})

But this also work:

a.printName()

Why does that work? It works because the type person has been assigned to the variable a and the variable a is attached to the method printName via its receiver. As such the method printName has full access to all the fields of the type person and it belongs to the type person because it has a receiver of type person.

If we change the example from before and use the dot notation, it looks like this:

func main() {
    a := superhero {name: "Clark Kent"}
    b := person {name: "Lois Lane"}

    // We use the dot notation
    a.printName()
    a.whois(89)
    b.printName()
}

This prints out:

Clark Kent - this person is a superhero
Clark Kent is really Superman
He is 89 years old
Lois Lane - this person is just a normal person

Try on The Go Playground

We still cannot switch the variables by mistake because each method is attached to the appropriate receiver, and each variable belongs to the structure of that receiver.

Most people prefer the a.foo() version as it looks cleaner. However, sometimes this form person.foo makes it more clear what the code is intended to do.

Interfaces in Go

So in the above we have managed to achieve some levels of data encapsulation and data abstraction. What else can we do?

In Java, C++, and PHP, we can bind a specific class to a specific set of methods by the usage of an interface. In Go we bind a specific struct to a specific set of methods using an interface.

In PHP, for example, you define an interface like this:

interface foo {
    // Method signatures goes here.
}

class bar implements foo {}

This means that whatever requirements interface foo has must be fulfilled by class bar. If the interface foo has a method called printName, the class bar must now also have a method called printName. Not only that, but the number of arguments the methods takes (if any) must also match.

In other words, the interface serves as a blueprint for the classes that implements that interface.

In Go everything is about types. Since an interface is a type, we declare a variable of the interface type. Then we define one or more methods signatures in the interface, and we then create a struct and make it implement the interface using method receivers.

Huh? Yeah, it's not that obvious.

Let's see an example of how we can use interfaces.

package main

import (
    "fmt"
)

type animal interface {
    speak() string
    eat() string
}

In the above we have create an interface of type animal. This animal interface has two method signatures that says that, any type that belongs to the animal interface must implement two methods, a speak() method and an eat() method.

We then need to create a type we can use to implement the animal interface. We can do that with a struct.

type dog struct {
    saying string
    food string
}

The saying and food fields have nothing to do with the requirements of the interface, that's just me adding some extra fields to the struct. We could have used an empty struct if we wanted, like this:

type dog struct {}

Now we need two methods that can take a "dog" as a receiver.

func (d dog) speak() string {
    s := "I like to say: "
    return s + d.saying
}

func (d dog) eat() string {
    s := "I like to eat: "
    return s + d.food
}

Currently nothing ensures that the two methods fulfill the requirements of the interface. It is first when we decide to declare a variable with the interface value that any method that has the same "signature" as the interface, gets bound to the interface.

Let's combine the above into one program, but leave out the eat() method just to see what happens.

package main

import (
    "fmt"
)

// We define an interface with two method signatures.
type animal interface {
    speak() string
    eat() string
}

// We create a structure of type dog.
type dog struct {
    saying string
    food string
}

// We declare a method that takes a type dog as it's receiever.
func (d dog) speak() string {
    s := "I like to say: "
    return s + d.saying
}

func main() {

    // We declare a variable and assign the animal interface to it.
    // We make the interface implement the the type dog.
    a := animal(dog{
        saying: "Wooof!",
        food: "Roastbeef",
    })
    fmt.Println(a.speak())
}

If we run the above program the type dog does not meet the requirements of the animal interface, as it does not implement both a speak() method and an eat() method. We get the following error when we try to run the program:

cannot convert dog{…} (value of type dog) to type animal: dog does not implement animal (missing method eat)

Try on The Go Playground

This means that we have created a blueprint, or a contract, that requires that any type that belongs to the animal interface MUST implement two methods, a speak() method and an eat() method.

The place where we "bind" the "type dog" together with the "animal interface" is in the main function where we declare a variable a. This is comparable to class dog implements animal in PHP.

// Synonymous to "class dog implements animal" in PHP.
a := animal(dog{
...
})

NOTE: If a variable has an interface type, we can call the methods that are in the named interface.

Let's make everything work.

package main

import (
    "fmt"
)

// We define an interface with two method signatures.
type animal interface {
    speak() string
    eat() string
}

// We declare a struct of type dog.
type dog struct {
    saying string
    food string
}

// We declare a method that takes a type dog as it's receiever.
func (d dog) speak() string {
    s := "I like to say: "
    return s + d.saying
}

// We declare a second method that takes a type dog as it's receiever.
func (d dog) eat() string {
    s := "I like to eat: "
    return s + d.food
}

func main() {

    // We declare a variable and assign the animal interface to it.
    // We make the interface implement the the type dog.
    a := animal(dog{
        saying: "Wooof!",
        food: "Roastbeef",
    })
    fmt.Println(a.speak())
    fmt.Println(a.eat())
}

If we run the above program we get the following output:

I like to say: Wooof!
I like to eat: Roastbeef

Try on The Go Playground

Let's add a "type cat" to the program.

package main

import (
    "fmt"
)

// We define an interface with two method signatures.
type animal interface {
    speak() string
    eat() string
}

// We declare a struct of type dog.
type dog struct {
    saying string
    food string
}

// We declare a struct of type cat.
type cat struct {
    saying string
    food string
}

// We declare a method that takes a type dog as it's receiever.
func (d dog) speak() string {
    s := "The dog likes to say: "
    return s + d.saying
}

// We declare a second method that takes a type dog as it's receiever.
func (d dog) eat() string {
    s := "The dog likes to eat: "
    return s + d.food
}

// We declare a method that takes a type cat as it's receiever.
func (d cat) speak() string {
    s := "The cat likes to say: "
    return s + d.saying
}

// We declare a second method that takes a type cat as it's receiever.
func (d cat) eat() string {
    s := "The cat want's to catch a "
    s2 := " for dinner!"
    return s + d.food + s2
}

func main() {

    // We declare the variable a and assign the animal interface to it.
    // We make the interface implement the the type dog.
    a := animal(dog{
        saying: "Wooof!",
        food: "Roastbeef",
    })

    // We declare the variable b and assign the animal interface to it.
    // We make the interface implement the the type cat.
    b := animal(cat{
        saying: "Meoow!",
        food: "mouse",
    })

    // The dog.
    fmt.Println(a.speak())
    fmt.Println(a.eat())

    // The cat.
    fmt.Println(b.speak())
    fmt.Println(b.eat())
}

This will print out:

The dog likes to say: Wooof!
The dog likes to eat: Roastbeef
The cat likes to say: Meoow!
The cat want's to catch a mouse for dinner!

Try on The Go Playground

We don't have to have anything in the structs and we can simplify the example like this:

package main

import (
    "fmt"
)

type animal interface {
    speak() string
    eat() string
}

type dog struct {}

type cat struct {}

func (d dog) speak() string {
    return "Woof!"
}

func (d dog) eat() string {
    return "I want roastbeef!"
}

func (d cat) speak() string {
    return "Meow!"
}

func (d cat) eat() string {
    return "Can I get some tuna?"
}

func main() {
    a := animal(dog{})
    fmt.Println(a.speak())
    fmt.Println(a.eat())

    b := animal(cat{})
    fmt.Println(b.speak())
    fmt.Println(b.eat())
}

This prints:

Woof!
I want roastbeef!
Meow!
Can I get some tuna?

Try on The Go Playground

So let's recap.

The "struct", "method", and "interface", each provide their own unique features in Go. By combining them we can achieve encapsulation, abstraction, and polymorphism.

So what we do in Go is the following:

  1. Define an interface.
  2. Create one or more struct(s).
  3. Define methods with signatures that matches the interface and make the method receivers match the relevant struct(s).
  4. Declare a variable and assign the interface to it and make it implement the relevant struct type.

Conclusion

We have become so used to working with other programming languages that use classes for type hierarchies that when we deal with object-oriented programming we think of a class right away. We have made the "class" synonym to "object-oriented" programming. But this is a mistake. A class is just one way to implement some of the object-oriented concepts, it is not in and by itself object-oriented.

In PHP, for example, you can choose between procedural and object-oriented programming, and you can mix your code and make use of both paradigms within the same program. It is easy to encapsulate and abstract with classes.

In Go you can also choose between procedural and object-oriented programming and you can mix your code and make use of both paradigms within the same program, but Go has no type hierarchy and it doesn't use classes, instead it has structs and method receivers. Structs do not explicitly declare which interfaces they implement, this is done implicitly by matching the method signatures.

The two implementations are very different.

In the Go FAQ we find the following interesting question: Why doesn't Go have "implements" declarations?

The answer is that:

A Go type satisfies an interface by implementing the methods of that interface, nothing more. This property allows interfaces to be defined and used without needing to modify existing code. It enables a kind of structural typing that promotes separation of concerns and improves code re-use, and makes it easier to build on patterns that emerge as the code develops. The semantics of interfaces is one of the main reasons for Go's nimble, lightweight feel.

One of the big advantages of Go's object-oriented implementation is that it isn't as easy to make excessive use of the object-oriented features. In e.g. PHP people often go overboard and start putting everything into classes even when there is no substantial benefit. And it is possible to find a code base that has nothing but classes and methods, yet the result of the approach is actually pure structured and procedural programming. Just because something gets stuffed into a class doesn't make it object-oriented.