Video:

Interfaces in Go

June 4, 2021

Course Instructor: Elliot Forbes

Hey Gophers! My name is Elliot and I'm the creator of TutorialEdge and I've been working with Go systems for roughly 5 years now.

Welcome all, in this tutorial we are going to be taking a look at interfaces within the Go programming language.

By the end of this tutorial, we’ll have covered the following topics:

  • The Basics of Interfaces
  • Defining Your Own Interfaces
  • Pointer Receivers vs Value Receivers

Awesome, let’s dive right in!

What Is An Interface?

Before we dive into the code, we need to first take a look at what an interface is and what it provides for us.

Well, I like to think of interfaces as contracts that my code must follow. They allow us to effectively define what our dependencies should look like.

For example, if I’m implementing a comment service that talks to a database, I would want to implement an interface within that comment service that effectively describes the methods I’ll need any database implementation to have.

package comment

// imports

type Comment struct {
	ID string
	Slug string
	Author string
	Body string
}

// The interface that describes what we need any comment store implementation
// to have.
type CommentStore interface {
	SaveComment(ctx context.Context, cmt Comment) error
}

// we then set this within our Service
type Service struct {
	Store *CommentStore
}

func New(cmtStore *CommentStore) *Service {
	return &Service{
		Store: cmtStore,
	}
} 

// Our service might have an add comment method that allows people to add 
// new comments.
func (s *Service) AddComment(ctx context.Context, cmt Comment) error {
	// here we could perform some validation on the incoming comment
	// like potentially checking if the authorID is valid etc

	// here we then call the Store's implementation of SaveComment.
	return s.Store.SaveComment(ctx, cmt)
}

Let’s break this down as it’s quite a complex snippet.

Our New function returns a new service that handles adding comments to our site. This service effectively handles all the ‘business’ logic for adding comments. It does not handle things like saving these comments to the database, this is very important to note.

We are delegating the responsibility of handling our database interactions to another package that we then pass in to our New constructor function.

In order for our code to successfully compile, we need to ensure that this second database package implements the contract we’ve defined at the top of this snippet.

Note - The above code snippet is an example of favouring composition over inheritance and is typically the approach you want to take for all components within your Go systems!

Defining an Interface

Let’s take a step back and look at what it takes to define an interface. We can use the type NAME interface syntax to define a new interface

type Employee interface {}

We can then start to build up this interface and add the methods that we need. Let’s say we needed a GetName() method that returned the name string of the Employee:

type Employee interface {
	GetName() string
}

If we wanted to implement this interface, we could do so by first defining a struct and then implementing the methods off this struct:

type Engineer struct {
	Name string
}

// We implement a method off the struct that exactly
// matches the function signature defined within our Employee
// interface
func (e *Engineer) GetName() string {
	return e.Name
}

Let’s say we wanted to define a function that takes in a struct that implements the Employee interface and prints out their details:

func PrintDetails(e Employee) {
	fmt.Println(e.GetName())
}

We could then pass in an Engineer to this function and it would accept it as it does infact implement the required interface!

Pointer vs Value Receivers

Pointer receivers vs value receivers can be confusing at first, but the distinction between the two is incredibly important.

When we are implementing a method, we specify what type of receiver we are using before the name of the method:

// This executes using a pointer to an address for p
// If we need to change the value of name, we can do this in 
// this method.
func (p *Pointer) GetName() string {
	fmt.Println("this is a pointer receiver method")
	return "Elliot"
}

The pointer receiver will execute using a pointer to the original value of P. This effectively means that we can modify the original value stored at P’s address.

If we switch over to a value receiver, the GetName() method is working on a local copy of the Value.

// This will execute using a local copy of v
// We cannot make changes to the original value using value receivers
func (v Value) GetName() string {
	fmt.Println("this is a value receiver method")
	return "Elliot"
}

Video Source Code

Here you’ll find the source code for the video tutorial above:

main.go
package main

import "fmt"

type Employee interface {
	GetName() string
}

type Engineer struct {
	Name string
}

func (e *Engineer) GetName() string {
	return "Engineer Name: " + e.Name
}

type Manager struct {
	Name string
}

func (m *Manager) GetName() string {
	return "Manager Name: " + m.Name
}

func PrintDetails(e Employee) {
	fmt.Println(e.GetName())
}

func main() {
	engineer := &Engineer{Name: "Elliot"}
	manager := &Manager{Name: "Donna"}
	PrintDetails(engineer)
	PrintDetails(manager)
}

Let’s try running this now:

$ go run main.go
Engineer Name: Elliot
Manager Name: Donna

Conclusion

Hopefully you found this useful, if you did or have any additional comments then please let me know in the comment section below!