Paul Di Gian
Paul Di Gian's Blog

Paul Di Gian's Blog

Advanced Go – Improve your code using Interfaces effectively

Paul Di Gian's photo
Paul Di Gian

Published on Sep 11, 2021

9 min read

Subscribe to my newsletter and never miss my upcoming articles

In this post we are going to tackle one of the most peculiar go features. Interfaces.

I am not talking about the interface{} type, but on how to use interfaces in the type Fooer interface {} construct.

We will see why go is different from most programming language with its implicit interface, and we will see how to take the most advantage of implicit interface when writing Golang.

Interfaces are types

In Golang, and in many other programming languages, interfaces are types.

In practical terms, this means that they can be used as input parameters for functions or even as output parameters.

The fact that interfaces are types, is given away by their construction.

type Fooer interface {
    Foo() bool
}

From the declaration of the Fooer interface, it is clear that interfaces are types. Indeed, the first token when declaring a new interface is indeed type.

Since interfaces are type, it means that we can write code that looks like this.

type notImportant struct {}

func (n *notImportant) Foo() bool {
    return true
}

func ValidFoo(f Fooer) bool {
    return f.Foo()
}

ValidFoo(&notImportat{})

Note how in the body of the function we use only use methods defines in the interface and not other methods that the struct may implement.

Moreover, we have never declared that the notImportat struct implements the Fooer interface, this is because interfaces in go are implicit.

Interfaces are implicit

In go, differently from most programming languages, interfaces are implicit. This means that concrete types do not have to declare if they implement a specific interface or not.

A concrete type either implements all the methods of an interface, and so implements the interface, or it doesn’t.

If a concrete type misses implementing, even a single method of an interface, the whole type does not implement the interface.

Having implicit interfaces moves the standard equilibrium of interface definition and API development.

In most programming language, the interface is a contract between the implementor of the concrete types and the user of those types.

Look, this type can do A, B, and C. Use it as you see fit.

Developer implementing some concrete type.

In go, the contract is reversed.

If the type can do A, B and C, then you can use it in this function.

Golang developer on interfaces

This means that the interface should be defined just before to be used, by the user of the type. Let me repeat, by the user of the type. Not by the implementor of the type. By the user of the type.

If I am developing a function that takes a parameter that needs to implement two methods, for instance, Foo(int, int) int and Bar(int, float) bool I have two choices.

  1. Either pass to the function a concrete type that implements both the Foo and Bar methods.
  2. Define an interface FooBarer and pass the interface as argument to the function. The concrete type will implicitly implement the interface.

The first option is simpler and faster, and it is what you should reach for in most occasions.

The second option, however, hides the concrete type that implements the functions. This makes it simple (or at least simpler) to write white box tests for the functions.

Moreover, the option of passing an interfaces, allows passing different concrete types and implementation to the same function.

When to declare interfaces

This is a crucial point of interfaces in golang and implicit interfaces in any programming language.

Since the interfaces are implicit, each type that syntactically fulfill the interface, can be used as concrete type.

Here, the emphasis in on syntactically. The compiler looks only to the name of the methods and their signature to decide whenever a type fulfill and interface.

This is different from explicit interfaces, where a developer have to declare that a type fulfills the interfaces and where there is a semantic check on the methods, done by the developer.

Since, there is only a syntactical check on what structure is allowed to implement what interface, you should avoid creating interfaces that provide some capabilities.

On the other hand, you should create interfaces that require capabilities.

When to use interfaces

Interfaces allow picking, at runtime, how a piece of code should behave.

This is useful mostly in two occasions.

There is some code, that either check some condition, or read some configuration and depending on the conditions or on the configuration it should behave differently.

The other very common occasion is testing.

Of course, both occasions, condition dependent and testing can be classified as simply as condition dependent. If we are running a test, use a different concrete type.

Anyway, the mentality to which the code is approached, is often different, hence, I find important to split the two.

Interfaces for condition dependent code

Let’s suppose that our code reads a configuration file and either decide to save its output on disk or on some cloud object store (something like AWS S3).

This is the classical example where we can use an interface.

Moreover, it is also a very real request from the business. At the beginning everybody was happy with the result of our computation being saved on disk, but eventually we wanted to provide automatic backup and storage for those results, so the business decided to allow ingestion directly in AWS S3.

Let’s see how this could be implemented.

At first, we have a simple function, that simply writes on disk.

func path() string {
    return "~/some/random/path.bin"
}

func saveOnDisk(body []byte) error {
    diskPath := path()
    f, err := os.Create(diskPath)
    if err != nil {
        return err
    }
    _, err = f.Write(body)
    return err
}

Now the request from management to allow to save the file also on S3.

In this case we have two option, the first is a simple if:

func path() string {
    return "~/some/random/path.bin"
}

func saveOnDisk(body []byte) error {
    diskPath := path()
    f, err := os.Create(diskPath)
    if err != nil {
        return err
    }
    _, err = f.Write(body)
    return err
}

func s3Bucket() string {
    return "acme-corp"
}

func s3Key() string {
    return fmt.Sprintf("some/random/path-%d.bin", rand.Intn(100000))
}

func saveOnAWSS3(body []byte) error {
    sess := session.Must(session.NewSession())
    uploader := s3manager.NewUploader(sess)

    // Upload the file to S3.
    _, err := uploader.Upload(&s3manager.UploadInput{
        Bucket: aws.String(s3Bucket()),
        Key:    aws.String(s3Key()),
        Body:   bytes.NewBuffer(body),
    })
    return err
}

func main() {
    // ...
    saveOnLocalDisk := shouldSaveOnDisk(configFile)
    saveOnS3 := shouldSaveOnS3(configFile)

    body := []byte{1, 2, 3}
    if saveOnLocalDisk {
        err := saveOnDisk(body)
        if err != nil {
            panic(err)
        }
    } else if saveOnS3 {
        err := saveOnAWSS3(body)
        if err != nil {
            panic(err)
        }
    }
    // ...
}

The code works but it is very hard to maintains and to grow.

An enhancement would be to use an interface.

We can start by defining two structs, with a method each.

type localDiskSaver struct{}

func (l *localDiskSafer) Save(body []byte) error {
    diskPath := path()
    f, err := os.Create(diskPath)
    if err != nil {
        return err
    }
    _, err = f.Write(body)
    return err
}

type awsS3Saver struct{}

func (s3 *awsS3Saver) Save(body []byte) error {
    sess := session.Must(session.NewSession())
    uploader := s3manager.NewUploader(sess)

    // Upload the file to S3.
    _, err := uploader.Upload(&s3manager.UploadInput{
        Bucket: aws.String(s3Bucket()),
        Key:    aws.String(s3Key()),
        Body:   bytes.NewBuffer(body),
    })
    return err
}

Note how the structs are empty, because they don’t need to contain any value. Moreover, note how the implementation of the two Safe functions it is exactly the same of the two initial functions. And note how the Save method of both structs has the same interface: Safe([]byte) error

Now the main could be rewritten like so:

func main() {
    // ...
    saveOnLocalDisk := shouldSaveOnDisk(configFile)
    saveOnS3 := shouldSaveOnS3(configFile)

    body := []byte{1, 2, 3}
    if saveOnLocalDisk {
        saver := localDiskSaver{}
        err := saver.Save(body)
        if err != nil {
            panic(err)
        }
    } else if saveOnS3 {
        saver := awsS3Saver{}
        err := saver.Save(body)
        if err != nil {
            panic(err)
        }
    }
    // ...
}

But this would be hardly an improvement over what we did before, the code is still hard to maintains with too many conditions.

Let’s try with an interface.

Now, it is the moment to define our interface. In this case, we can define a Saver interface.

type Saver interface {
    Save([]byte) error
}

The two types (localDiskSaver and awsS3Saver) will automatically implement the Saver interface.

Now we can use the newly defined Saver interface.

func main() {
    // ...
    saveOnLocalDisk := shouldSaveOnDisk(configFile)
    saveOnS3 := shouldSaveOnS3(configFile)

    var saver Saver
    if saveOnLocalDisk {
        saver = &localDiskSaver{}
    } else if saveOnS3 {
        saver = &awsS3Saver{}
    }

    body := []byte{1, 2, 3}
    err := safer.Save(body)
    if err != nil {
        panic(err)
    }
    // ...
}

You can see how this code is simpler, or at least more encapsulated.

The details on how the saving happens is hidden behind an interface, and picking the correct implementation depends only on interpreting correctly the configuration file, indeed, this can be encapsulated even further.

func getSaver(configFile string) Saver {
    if shouldSaveOnDisk(configFile) {
        return &localDiskSaver{}
    } else if shouldSaveOnS3(configFile) {
        return &awsS3Saver{}
    }
    panic("no saver")
}

func main() {
    // ...
    saver := getSaver(configFile)

    body := []byte{1, 2, 3}
    err := saver.Save(body)
    if err != nil {
        panic(err)
    }
    // ...
}

Now the code is really encapsulated, the creation of the data and the main, move on a completely orthogonal line in respect to how the files are stored.

Beside being better software engineer, this distinction allows to have different teams works on different topics, as long as they agree on the Saver interface.

The cost of this is more indirection in respect of the initial approach based on ifs.

Interfaces for testing

The other common use of interface it is to test code.

For instance we might want to test, that a particular function is called a specific number of time. For instance the Saver interface should be called at least once.

type ComplexStruct struct {
    // ... some other thing
    saver *awsS3Saver
}

func (c *ComplexStruct) DoWork() {
    // ... something complex
    // ... maybe with a complex control flow
    c.saver.Save([]byte{1, 2, 3})
}

In order to test that the Save method is called at least one, we could, first change the type of the ComplexStruct.saver field into a Safer interface

type ComplexStruct struct {
    // ... some other thing
    saver Saver
}

func (c *ComplexStruct) DoWork() {
    // ... something complex
    // ... maybe with a complex control flow
    c.saver.Save([]byte{1, 2, 3})
}

Then create a special Safer for the testing

type saverCount struct {
    count int
}

func (s *saverCount) Save(body []byte) error {
    s.count++
    return nil
}

And now implement our test using the new Saver.

func Test_SaveAtLeastOnce(t *testing.T) {
    saver := saverCount{}
    c := ComplexStruct{
        //...
        saver: &saver,
    }
    c.DoWork()
    if saver.count < 1 {
        t.Error("Save not called even once by ComplexStruct")
    }
}

Another usage could be to check if the content saved is actually the one expected:

type saverStore struct {
    buffer [][]byte
}

func (s *saverStore) Save(body []byte) error {
    s.buffer = append(s.buffer, body)
    return nil
}

func Test_SaveTheExpectedBytes(t *testing.T) {
    saver := saverStore{}
    c := ComplexStruct{
        // ...
        saver: &saver,
    }
    c.DoWork()
    expected := []byte{1, 2, 3}
    if len(saver.buffer[0]) != len([]byte{1, 2, 3}) {
        t.Error("unexpected len in the saved buffer")
    }
    for i := range saver.buffer[0] {
        if saver.buffer[0][i] != expected[i] {
            t.Error("found wrong content in saver buffer")
        }
    }
}

Conclusion

In this article, we explored Golang interfaces and how to be productive with them.

They are a fundamental concept in Golang and in order to write clean and concise code it is paramount to know how to use them well.

 
Share this