A lot have changed in the past few months!

I’ve started a new job @ CloudCover and have also started contributing to golang projects!

The inspiration for this post comes from a (slightly) confusing topic in golang which I initially struggled to understand, and that’s interfaces!

This post doesn’t cover what interfaces are! There are lots of really good articles on the internet for that.

What I really want to focus on is “how” you can use them and what common pitfalls to avoid (especially if you come from Java / C#)


#1 Define interfaces close to site of use

This allows the code (that makes use of the interface) to depend on abstractions rather than on concrete implementations.

And because interfaces in go are implemented implicitly, your actual implementation would just work auto-magically!

// UserService defines a service that's responsible to manage user profiles
type UserService interface {
    // FetchAll returns a cursor that lists all users on the platform
    FetchAll() UserIterator
    
    // ......
    // this interface could contains more (but related) methods
    // just make sure to not bloat it (see Rule #3)
}

// Now we could utilize UserService abstraction in our code.
// Here we pass it to a function that returns an http.HandlerFunc
// When that handler is executed, the code could utilize the supplied user service
// without having to worry about where the actual implementation came from

func ListAllUser(svc UserService) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var cursor = svc.FetchAll()
        //...
    }
}

This allows you to decouple your code and it’s dependencies enabling you to write modular code.

DO NOT place the interface in the package that provides the dependency and than make the provided service implement that interface. This seems like a common pattern used in other languages (namely, Java) where you’d declare an interface com.example.UserService and than provide a package-private implementation of it com.example.UserServiceImpl (along with a factory perhaps)

The real power of Go interfaces comes from the fact that they can be implemented implicitly! This reduces the overhead of arranging for an implementation before-hand and also allows a single type to implement multiple (but un-related) interfaces or implement interfaces that doesn’t even exist yet (crazy right!)

A common example from the standard library to support this would be the fmt.Stringer interface which is defined close to it’s site of use (the methods in fmt package) yet anyone can provide an implementation by adding a method with compatible signature String() string

#2 If it’s gonna be used at multiple places, declare it only once

If you feel like your interface definition is going to be used in multiple places, it’s better to declare it once (and maybe in a shared package)

Wait a minute.. doesn’t that contradict #1 ?

Umm no, not really. You need to declare them in a single place and make the symbol (the interface) available to your site of use. This doesn’t mean you need to couple it with your actual implementation.

A notable project which makes use of this pattern is drone.io

All the core interfaces used by drone are declared under the core package which is than used by various other packages and satisfied by another set of packages.

#3 Keep it small silly 😛

Try and stick with SOLID!

More specifically one should try to follow Interface Segregation principle.

Quoting from Wikipedia,

The Interface Segregation principle states that no client should be forced to depend on methods it does not use.

One way to achieve this would be to keep your interfaces lean and focused. Model your interfaces around behaviour, and not data, and prefer embedding over large interfaces.

A very popular example from the standard library would be the interfaces in io package

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

#4 Accept interfaces return structs

Lol no, I didn’t come up with it 😅 in fact, no one’s actually sure who did 🙄 but people often cite Postel’s Law

The basic idea is that your method should accept (as arguments) values of interface types while should return values which are concrete types.

A few examples from the standard library would be

  • io.Pipe method whose signature declares the concrete underlying type of the return values

    func Pipe() (*PipeReader, *PipeWriter)
    
  • io.Copy method whose signature declares accepted argument types to be interfaces

    func Copy(dst Writer, src Reader) (written int64, err error)
    

The primary advantage here is that your function is now decoupled and depends on behaviour rather than concrete types of the arguments.

Yet consumers of your function have the flexibility to choose the way they want to use the return type (by casting it appropriately) based on their use case.

This doesn’t mean that you should (or can) always declare methods like that. There are exceptions. For example, io.TeeReader method

func TeeReader(r Reader, w Writer) Reader

Here, it actually makes sense to wrap the underlying implementation and return an io.Reader

Another common use case for returning interface types from method is to ensure that the value being returned actually satisfies the interface. I think this is rather a hack around Go’s philosophy that “interfaces are implemented implicitly”.

If what you actually need is a way to ensure that your custom implementation satisfies an interface you can use interface guards.


Bonus

Interface guards

An interface guard (not an official term) is a simple no-cost hack to add checks in your code to ensure that your implementation satisfies some interface. Use this if you are building a custom implementation for an interface that is exported by a third-party library.

Following snippet is taken from Caddy’s documentation on interface guards

var _ InterfaceName = (*YourType)(nil)

This is simple yet no-cost way of ensuring *YourType implemements InterfaceName at compile-time rather than running into errors at runtime.


Got doubts / queries / suggestions, feel free to open an issue on Github!