Software Development

Using Interfaces in Go

Published: 2024/05/02

9 min read

Interfaces are one of the most important features in Go. Though not unique to Go, the way how these fit into code design is different to languages like C# or Java. Read on to learn how to properly use interfaces in Go and how to avoid the most common pitfalls related to returning an interface.

A common way of thinking

According to Microsoft documentation of C# “An interface defines a contract. Any class, record or struct that implements that contract must provide an implementation of the members defined in the interface”. In other words, an interface there defines what a particular object needs to implement in order to be a valid object. Also, this relation is expressed explicitly – at the very moment of an object’s definition, it must express that it implements a particular interface(s) and needs to actually provide all needed methods. A basic example looks like this:

interface IAnimal  

{ 

  void animalSound(); 

} 

  

class Cat : IAnimal  

{ 

  public void animalSound()  

  { 

    Console.WriteLine("meow meow"); 

  } 

} 

  

class Dog : IAnimal  

{ 

  public void animalSound()  

  { 

    Console.WriteLine("bark bark"); 

  } 

} 

Cat and Dog implement IAnimal, therefore we can be sure that both do what animals do: make some animalSound. Such implementation of interfaces in a language means that there is a clear and explicit relation between the interface and its implementation(s). A benefit of this is that you are assured that the contract between entities is valid.

A similar way of doing things can be seen in other languages, for example, Java. Taking that into consideration, it’s not a surprise that when developers with such a background come to the Go world, they want to do things the same way. Unfortunately, that’s a mistake because…

Go is a bit different

In Go, interfaces are not meant to be entities that ensure some kind of contract. Interfaces in Go define expected behaviour of the parameter. The “expected” word is crucial here because that’s the root nature of the interfaces in Go – interfaces are expected to be fulfilled by passed argument, not implemented explicitly. What’s even more interesting, the struct that fulfils the interface doesn’t need to know about this. This implies another important rule: interface should be defined by the consumer, not by the producer (where consumer means function / method that accepts argument of some type and producer means the function / method that returns a particular type). A basic example looks as follows:

type Cat struct {} 

func (c Cat) MakeSound() { 

  fmt.Println("meow meow") 

} 

  

type Dog struct {} 

func (d Dog) MakeSound() { 

  fmt.Println("bark bark") 

} 

  

type Soundmaker interface { 

  MakeSound() 

} 

  

func makeSound(someone Soundmaker) { 

  someone.MakeSound() 

} 

Cat and Dog have some method. The fact that such a particular implementation is there, implicitly means that both can be used as a Soundmaker argument. Neither need to know about this, they just both happen to possess expected behaviour – and that’s enough. On the other hand, the makeSound function doesn’t care what type of argument will be passed to it as long as it will be able to MakeSound. That’s all that matters here.

The difference between both the described approaches might be subtle, but it’s there and it influences the whole philosophy of using interfaces in the aforementioned languages. It can be summarized this way: interfaces in C# (and similar) are explicitly implemented, interfaces in Go are implicitly fulfilled. The first approach describes how an object can be perceived, the second defines what behaviour the object is expected to implement. With that in mind, let’s answer the main question of this article: how to properly use interfaces in Go?

Interfaces in the wild

Before we dive into the code, a short note – this article is aimed at programmers with different backgrounds who’ve started to work with Go and want to know how to write better and more idiomatic Go code. Therefore, I’ll assume that some basic concepts of both Go and programming in general are known. For examples, I’ll try to use (obviously really simplified) code that you can actually encounter in some form while working with Go on production.

The advice related to the usage of interfaces in Go needs start with a clarification on when to use interfaces at all.

The first use case for interfaces is when you want to abstract some implementation from the actual usage of it. Therefore, the functionality should be used when the structure of your project is settled, and you want to keep it clear as regards the relations between packages. For example, based on the following project structure that we’ll work on:

Go interfaces

If we’d like to preserve boundaries between server and the domain packages and not mix one with the other using the import statements, we can use the interface. The server package shouldn’t care what the domain implementation looks like. The only thing that should matter is the behavior exposed by the object from the domain package.

File: server > server.go
Code
No explicit relation and yet, the server can utilize the domain. What’s also worth noting, with this approach, is that changes in the package do not affect the core logic of the other. Of course, as long as the functionality still works and the interface is fulfilled.

The second use case for interfaces comes into play when any external operation is being performed. If it’s querying some APIs, running some DB operations or even executing external binaries – these are perfect places where interfaces will fit best. This is especially important in the context of unit tests. Take the following example:

File: domain > domain.go
Code 2
If the design of this code wouldn’t use interfaces, any unit test that would like to test DoStuff method would need to call the actual implementation of the database communication – which would probably mean actual calls to the database. And that would be against any good practice related to creating useful unit tests which should be self-contained and independent from externals. Applying the interfaces allows for mocks usage, as follows:

File: domain > domain_test.go
Code 3
The real test would probably use Mockery or some other similar tool but the idea is the same. Interfaces allows to cut tests from the externals therefore when any of such operation is in use, interfaces are needed to handle such case.

Good practices for interfaces

Preserving the clarity of the relations within the project and abstracting external operations – these are key factors that strongly suggest interfaces usage in a Go project. So now let’s answer the question – how to do it properly?

The first thing is to keep the interface’s definition near the place where it will be used. That’s a really important rule because it originates from the core idea of interfaces – providing behavior. The entity that will be using the interface is the best source of knowledge regarding what the interface should provide. Also, if the interface would be imported, that would mitigate one of its benefits, which is reducing the need to import entities from other packages.

The next thing is related to the imports, and more specifically, the structure of packages. Some of you might notice that in the examples, some methods that are expected in interface use third party types. The reason behind this is simple – when we define an interface, we don’t want it to be dependent on types related with a particular implementation. Therefore, the interfaces should use simple types when possible or types from packages which are common across the service / library, not directly related with a particular implementation. The approach used in the examples above is kind of a compromise. Even though the model package is inside the storage package, it already separates the interface defined in domain from its actual implementation. It’s worth mentioning another benefit of taking this approach is a clear separation between the logic of the service / library and the DTOs used in the code. This increases readability and makes code easier to maintain.

The third rule is to keep the interfaces as small as possible. Unfortunately, it’s hard to define what that actually means. For some it will be one method, for others it will be five of them. I guess it depends on the context and in the end, you need to trust your intuition on how many methods are needed. What’s easy to see though, is that all methods in interface should be used. And by that, I mean used directly in the entity that expects some kind of interface. When a method must be present in one interface in order to be available down to the execution path for other calls, that’s a good moment for some redesign.

Last but not least is returning an interface. Or rather, the fact that, in my opinion, this should (almost) never happen.

Why returning an interface is a bad idea

In my opinion, returning an interface is the most common anti-pattern when it comes to the interfaces usage in Go and is almost never a good idea. There are multiple reasons why.

As mentioned before, an interface should be defined by the consumer, not by the producer. In this context, returning an interface does not make any sense. Breaking that rule causes problems and bad designs.

When a producer defines an interface, this creates a situation called preemptive interface – the interface is created before it’s actually used. Therefore. it is difficult to see whether an interface is even needed, let alone what methods it should contain. Secondly, when a consumer and producer are defined in different packages and the producer is the one that defines an interface, it creates unnecessary coupling between packages. This is because, the consumer – in order to expect a given interface – probably would need to import the producer’s package. And if the package didn’t do it and defined its own interface as the same or smaller than the one provided by the producer, that would mean that the interface defined by the producer is not actually in use anyways.

Let’s look at it from a caller’s perspective. A caller calls a function and as a result this function returns something. If this something is a concrete type, then the caller can do whatever he wants. For example, use of all defined methods and fields (sometimes even unexported if the package is the same). Returning an interface decreases this functionality and does not provide any benefit in return. Returning an interface also means that the returned entity can be used only as a parameter if this particular one – or something which is an interface subset – is expected, nothing else. So even if the actual implementation implements other interfaces (which is common, especially in database repositories implementations) it doesn’t matter – returning an interface reduces this functionality.

To sum up, returning an interface is probably a bad idea. That said, there are exceptions.

Exceptions

There are two exceptions come to mind when returning an interface, or in general, defining an interface not by the consumer, could be a good idea.

The first one is about well-known interfaces. These could be ones provided by a standard library like e.g. io.Reader or io.Writer. Widely recognized, with limited functionality, they are used in many situations. Such interfaces give enormous flexibility and enable the ability to provide common functionality in multiple implementations. For example, json.NewDecoder() expects io.Reader which means it can easily decode data from different sources like ordinary file or a HTTP request body. Usually, examples of such interfaces can be found in a standard library, but I can imagine a situation when such an interface is used only across one project.

The second exception is a situation when a creator of, for example, a library, wants to allow a consumer to return whatever they want as long as they implement an interface. A good example of such an interface is the build-in error. Everything can be an error in Go as long as it implements the following interface:

Go interfaces

It’s common to see signatures that return an error and that’s perfectly fine because nothing else than an error functionality is expected from the implementation of the entity that is returned like that. What is also interesting, an entity that is meant to be used only like that does not need to export any of the fields or methods except those required by the interface. It’s because the original returned type is basically lost anyways (not entirely but let’s not discuss it now) and the caller sees only the interface defined in the signature of the function.

For both exceptions there is one thing worth remembering. The rule mentioned before that an interface should be as small as possible is especially important here. One or at most two methods should be acceptable, and only when there is a good reason for that. It’s important in order to avoid dummy implementations only in order to satisfy an interface that needs to be returned.

Conclusion

Interfaces are great tool. The Go implementation of these, in my opinion, fantastically supports designing systems. But with great power comes great responsibility and it’s relatively easy to misuse them. Therefore, use interfaces when you actually need them, not because someone on the Internet told you to do so. Start with concrete types and when you will know where your design is going, that’s the moment to make it better with interfaces.

To get more insights from our Go developers, or to find out how our cross-functional engineering teams can support your business goals, fill out this form.

About the authorMarcin Plata

Senior Software Engineer

A backend developer with 4 years of experience, Marcin first started programming in Python, but eventually fell in love with Go. In his daily work as a senior software engineer, Marcin creates and maintains microservices that work in systems based on event-driven architecture. An avid tech enthusiast and Go disciple, Marcin enjoys keeping up to date with the latest programming developments and sharing new knowledge with colleagues and clients.

Subscribe to our newsletter

Sign up for our newsletter

Most popular posts