Golang Interfaces

Steve Gall
12 min readNov 24, 2021

--

Why they’re great, and why that fact is so often missed

Interfaces are one of the most important features in the Go language and also, in my view, one of the hardest things to get your head around when learning how to program using Go.

I think the problem depends on whether you’re approaching the language as a seasoned OO developer, where there is an expectation of how interfaces should behave or you’re learning from scratch and just get either confused or miss how fundamental they are. I think this is, in part, because there is not enough emphasis on how Go differs from other languages due to Go not being an OO language, when introducing interfaces. The problem is exacerbated due to the way in which interfaces are explained in the many tutorials both by not describing how to use Types correctly and in they way the fail to describe from the outset the benefits of Go interfaces. So let me deal with the point on interfaces first:

The single most powerful feature of Go interfaces is that they are not defined by the memory structure or state of a Type, only the signature of the methods of that Type.

or, possibly, in more simple terms:

Interfaces are don’t care what the Type looks like, only what they do and this allows you to write flexible, reusable code easily, quickly and with less bugs.

So let’s investigate this claim and this is where it’s probably time to go back to basics, but, as I do, if you are sitting there thinking “so what?”, remember the two statements above and believe me when I say we’ll get around to that soon.

Before we talk about interfaces, I think is is worth discussing how Types can be used more efficiently to make you code simple — something you should think about before you consider adding an interface.

package demotype name struct {
firstname string
lastname string
}

So, I have created a package demo and a Type name which is private. My main package will not be able to see this Type because it’s name is lower case. That is as much as you need to do in Go to encapsulate a Type’s fields and methods.

A quick note on encapsulation: The method of using upper case / lower case to define public or private only refers to the following:

  1. structs
  2. Type methods
  3. variables
  4. functions

Strings, slices and maps will be hidden but if you use a “constructor” to create them, as defined below, then values of that new Type are directly mutable.

OK, back to our new Type name. Now, if I was using an OO language I would be creating an interface, methods and a constructor, however, Go does not have the concept of constructors and it doesn’t need interfaces. I do, however, now need a way to create this type without actually having direct access to it. There is an idiom in Go to create a “constructor” function with the name of the function prefixed with the word “New” like so:

package demotype name struct {
firstname string
lastname string
}
func NewName(first, last string) name {
return name{firstname: first, lastname: last}
}

I now have a “constructor” function but I need to be able to access the fields. For that I need a “method”:

package demoimport "fmt"type name struct {
firstname string
lastname string
}
func NewName(first, last string) name {
return name{firstname: first, lastname: last}
}
func (n name) Fullname() string {
return fmt.Sprintf("name: %s %s", n.firstname, n.lastname)
}

Now I can create my main() function.

import (
"fmt"
"gitlab.com/box-of-badgers/scr/blog/demo"
)
func main() {
n := demo.NewName("Joe", "Soap")
fmt.Println(n.Fullname())
}

and, if I run my code I get controlled output of the struct name:

Excellent! So, I have a way of making fields private and controlling how to output them. No interfaces required.

So lets’ move on to embedding Types. Let’s say I have two Types of person, an employee and a client. I could produce something like this:

package demoimport "fmt"type name struct {
firstname string
lastname string
}
func NewName(first, last string) name {
return name{firstname: first, lastname: last}
}
func (n name) Fullname() string {
return fmt.Sprintf("name: %s %s", n.firstname, n.lastname)
}
type employee struct {
employeeID string
firstname string
lastname string
}
func NewEmployee(id, first, last string) employee {
return name{firstname: first, lastname: last}
}
func (e employee) ID() string {
return e.employeeID
}
func (e employee) Fullname() string {
return fmt.Sprintf("name: %s %s", e.firstname, e.lastname)
}
type client struct {
customerID string
firstname string
lastname string
}
func NewClient(id, first, last string) client {
return client{customerID: id, firstname: first, lastname: last}
}
func (c client) ID() string {
return c.customerID
}
func (c client) Fullname() string {
return fmt.Sprintf("name: %s %s", c.firstname, c.lastname)
}
package mainimport (
"fmt"
"gitlab.com/box-of-badgers/scr/blog/demo"
)
func main() {
employee1 := demo.NewClient("C0001234", "Joe", "Soap")
client1 := demo.NewEmployee("1122", "Fred", "Bloggs")
fmt.Println(employee1.ID())
fmt.Println(employee1.Fullname())
fmt.Println(client1.ID())
fmt.Println(client1.Fullname())
}

But that is a lot of boilerplate and what happens if, say I want to add a field? I now have three structs and three methods to update. The syntax can quickly get complicated and things can be missed. Right, time to simplify this code. I simply embed name into employee and customer and then I only need one FullName() function:

type employee struct {
employeeID string
name
}
func NewEmployee(id, first, last string) employee {
return employee{
employeeID: id,
name: name{
firstname: first,
lastname: last,
},
}
}
type client struct {
clientID string
name
}
func NewClient(id, first, last string) client {
return client{
clientID: id,
name: name{
firstname: first,
lastname: last,
},
}
}

main() remains unchanged:

package mainimport (
"fmt"
"gitlab.com/box-of-badgers/scr/blog/demo"
)
func main() {
employee1 := demo.NewClient("C0001234", "Joe", "Soap")
client1 := demo.NewEmployee("1122", "Fred", "Bloggs")
fmt.Println(employee1.ID())
fmt.Println(employee1.Fullname())
fmt.Println(client1.ID())
fmt.Println(client1.Fullname())
}

…but we can simplify this further. I now have only one function to worry about but I still have name constructed three times. I already have a name constructor function so I can just pass a name directly into NewEmployee() and NewClient() like so:

func NewEmployee(id string, name name) employee {
return employee{
employeeID: id,
name: name,
}
}
func NewClient(id string, name name) client {
return client{
clientID: id,
name: name,
}
}

Of course, the signature of those functions has now changed, so we need to update main() to reflect that:


employee1 := demo.NewClient("C0001234", demo.NewName("Joe", "Soap"))
client1 := demo.NewEmployee("1122", demo.NewName("Fred", "Bloggs"))

Running this code, we can see our application still works:

Not only that but, let’s say we need to update name by adding a salutation. We now only need to update the name struct and functions, like so:

type name struct {
salutation string
firstname string
lastname string
}
func NewName(sal, first, last string) name {
return name{
salutation: sal,
firstname: first,
lastname: last,
}
}
func (n name) Fullname() string {
return fmt.Sprintf("name: %s %s %s", n.salutation, n.firstname, n.lastname)
}

…and then update main() to reflect the inclusion of a salutation when calling demo.NewName():

employee1 := demo.NewClient("C0001234", demo.NewName("Dr", "Joe", "Soap"))
client1 := demo.NewEmployee("1122", demo.NewName("Mr", "Fred", "Bloggs"))

…but are code is now much easier to maintain. A single change that cascades through our code with minimal effort on our part, and we still haven’t had to used a single interface yet! Now we could do the same thing with our IDs. In our code we have two IDs, client.ClientID and employee.employeeID. At the moment, they’re both of Type string. So let’s create a common Type with a constructor and a method to print it:

package demoimport "fmt"// add new Type id
type id string
func NewID(id id) id {
return id
}
func (i id) ID() string {
return string(i)
}
// embed id into employee and client
type employee struct {
id
name
}
func NewEmployee(id id, name name) employee {
return employee{
id: id,
name: name,
}
}
type client struct {
id
name
}
func NewClient(id id, name name) client {
return client{
id: id,
name: name,
}
}

Great! Now we have simplified our employee and client types and only have one function to return both employee.id and client.id.

in main() we just add the id to NewClient() and NewEmployee by passing the NewID() method.

employee1 := demo.NewClient(demo.NewID("C0001234"),
demo.NewName(("Dr", "Joe", "Soap"))
client1 := demo.NewEmployee(demo.NewID("1122"),
demo.NewName("Mr", "Fred", "Bloggs"))

OK, this blog was supposed to be about interfaces and all we’ve done so far is talked about Types. What do we need interfaces for? Well, so far we have only embedded a single Type, i.e. the memory structure is consistent. So how can we embed different memory structures? Finally, we need an interface but, before we do that, let’s cover some interface basics. Now for this I am going to use a common way of describing Go interfaces using, in this case, animals (the other common one is shapes). Both are trivial and may leave you thinking “meh” but bear with me, I’ll show some practical implementations shortly. OK, cats and dogs; let’s make them talk!

type dog {
name string
talks string
}
func (d dog) talk() string {
return d.talks
}
type cat {
name string
talks string
}
func (c cat) talk() string {
return c.talks
}

OK, so I have two Types, a cat and a dog and a method for each to make them talk. so lets make some cats and dogs:

func main() {
fido := dog{"Fido", "Woof!"}
felix := cat{"Felix", "Miaw!"}
fmt.Println(fido.talk())
fmt.Println(felix.talk())
}

Now this is great and everything but what if I want a function that prints the dog or cat talking? They are different Types with different memory structures so how do I create a common function that I can use with both dog and cat? I use an interface.

type animal interface {
talk() string
}

…and that is all there is to it. Because the interface animal doesn’t care about the memory structure, only the methods of a Type, all I have to do is add a method to animal and any Type that has a method with the same signature automatically satisfies that interface. I can now create a function that accepts our animal interface and I can then pass it either cat or dog. Here’s the full code:

package mainimport "fmt"type dog struct {
name string
talks string
}
func (d dog) talk() string {
return d.talks
}
type cat struct {
name string
talks string
}
func (c cat) talk() string {
return c.talks
}
type animal interface {
talk() string
}
func saySomething(a animal) {
fmt.Printf(" This animal goes %s\n", a.talk())
}
func main() {
fido := dog{"Fido", "Woof!"}
felix := cat{"Felix", "Miaw!"}
saySomething(fido)
saySomething(felix)
}

So, if at this point you are just saying “so what?” I sympathise but let me show you why this is useful. Going back to our first example, let’s say we want our employeeID to be a number and we want to prefix it with a department. Having a simple string Type is no longer useful. We want to be able to store the code and the department. We also want to display employeeID as the two fields, separated with a hyphen. For that we need a struct and some new methods.

type employeeID struct {
department string
id int
}
func NewEmployeeID(department string, id int) employeeID {
return employeeID{
department: department,
id: id,
}
}
func (eid employeeID) ID() string {
return fmt.Sprintf("%s-%d", eid.department, eid.id)
}

I also need to change employee and it’s constructor to embed our new employeeID type. So, how do I do that? Well I could do this:

type employee struct {
employeeID
name
}
func NewEmployee(id employeeID, name name) employee {
return employee{
employeeID: id,
name: name,
}
}

But it is a lot easier for me to add an interface person and just embed that into employee. I can do the same with client

// new Person interface
type Person interface {
ID() string
}
// add person to employee and client structs and constructors
type employee struct {
Person
name
}
func NewEmployee(person Person, name name) employee {
return employee{
Person: person,
name: name,
}
}
type client struct {
Person
name
}
func NewClient(person Person, name name) client {
return client{
Person: person,
name: name,
}
}

Now, that might look a bit confusing. What have I done? Well, I have embedded the functions of the interface Person into my employee and client Types. Clearly, this has now changed the signature of the NewEmployee() so I need to change main() to reflect that. Note, however, that NewClient() does not need changing because we are still just passing the original client Type and this is the beauty of interfaces in Go. I can completely change the memory structure of the employeeID without having to cascade those changes throughout my code. It just makes things so much easier.

employee1 := demo.NewEmployee(demo.NewEmployeeID("Sales", 1234), demo.NewName("Dr", "Joe", "Soap")) client1 := demo.NewClient(demo.NewID("1122"), demo.NewName("Mr", "Fred", "Bloggs"))
id := demo.NewID("foo")

Now, looking at this code, we’ve embedded the interface Person into employee and client which is great but we can simplify things even further. First of all we can do the same thing with the name struct as we did with id by adding an interface and then embed both these interfaces, not into structs but another interface and, once again, yes you can do that. Before I do though, I’d like to introduce another idiom of Go. Interfaces are described by what they do so, it’s common to use a verb for the name, for example, an interface that reads is a Reader, one that writes is a Writer and in interface with embedded Reader and Writer interfaces is called a ReadWriter. Now, like I said, this is an idiom, it’s not mandatory and you can call them whatever you want and, sometimes it makes sense to use a name that best describes what it does.

So let’s do this; I am going to change name of the Person interface to Identifier and I am going to add an interface Namer that has the fullName() function and then I’m going to re-use Person as a new Interface to embed Identifier and Namer like this:

package demoimport "fmt"// new interface embedding the interfaces Identifier and Namer
type Person interface {
Identifier
Namer
}
type Identifier interface {
ID() string
}
// renaming Person as Namer
type Namer interface {
Fullname() string
}
type employee struct {
Identifier
Namer
}
// returning a Type Person
func NewEmployee(id Identifier, name Namer) Person {
return employee{
Identifier: id,
Namer: name,
}
}
type client struct {
Identifier
Namer
}
// returning a Type Person
func NewClient(id Identifier, name Namer) Person {
return client{
Identifier: id,
Namer: name,
}
}

Here’s the final code for reference:

package demoimport "fmt"// new interface embedding the interfaces Identifier and Namer
type Person interface {
Identifier
Namer
}
type Identifier interface {
ID() string
}
// renaming Person as Namer
type Namer interface {
Fullname() string
}
type employeeID struct {
department string
id int
}
func NewEmployeeID(department string, id int) employeeID {
return employeeID{
department: department,
id: id,
}
}
func (eid employeeID) ID() string {
return fmt.Sprintf("%s-%d", eid.department, eid.id)
}
type id string
func NewID(id id) id {
return id
}
func (i id) ID() string {
return string(i)
}
type name struct {
salutation string
firstname string
lastname string
}
func NewName(sal, first, last string) name {
return name{
salutation: sal,
firstname: first,
lastname: last,
}
}
func (n name) Fullname() string {
return fmt.Sprintf("name: %s %s %s", n.salutation, n.firstname, n.lastname)
}
type employee struct {
Identifier
Namer
}
func NewEmployee(id Identifier, name Namer) Person {
return employee{
Identifier: id,
Namer: name,
}
}
type client struct {
Identifier
Namer
}
func NewClient(id Identifier, name Namer) Person {
return client{
Identifier: id,
Namer: name,
}
}

Now my code is much shorter, cleaner and hopefully, more descriptive. Notice too that NewEmployee() and NewClient are now returning the interface Person rather than employee and client as before. So, why is this useful? Well, going back to our interface 101 earlier involving animals, we can now write common functions that accept both employee and client Types using the interface Person, something like so:

func Details(p Person) {fmt.Printf("ID:[%s] Name: %s\n", p.ID(), p.Fullname())}

and then call it from main(), passing the employee1 and client1 vars:

package mainimport (
"fmt"
"gitlab.com/box-of-badgers/scr/blog/demo"
)
func main() {employee1 := demo.NewEmployee(demo.NewEmployeeID("Sales", 1234), demo.NewName("Dr", "Joe", "Soap"))
client1 := demo.NewClient(demo.NewID("1122"), demo.NewName("Mr", "Fred", "Bloggs"))
id := demo.NewID("foo")
fmt.Println(employee1.ID())
fmt.Println(employee1.Fullname())
fmt.Println(client1.ID())
fmt.Println(client1.Fullname())
fmt.Printf("%s\n", id.ID()) demo.Details(employee1)
demo.Details(client1)
}

So, quite a lot to take in but, hopefully, I’ve managed to show how to use Types more effectively and demonstrated how interfaces can make you code easier to maintain.

Where this all becomes particularly useful is when you start using standard library interfaces such as Reader and Writer, mentioned earlier, which, incidentally, are used everywhere in Go. If this blog is well accepted I will write another showing how to use the techniques above to use Reader and Writer effectively. Until then, I hope you have found this helpful.

--

--