Go’s object-oriented model revolves around interfaces. I personally believe that interfaces are the most important language construct and all design decisions should be focused on interfaces first.
In this tutorial, you’ll learn what an interface is, Go’s take on interfaces, how to implement an interface in Go, and finally the limitations of interfaces vs. contracts.
What Is a Go Interface?
A Go interface is a type that consists of a collection of method signatures. Here is an example of a Go interface:
type Serializable interface { Serialize() (string, error) Deserialize(s string) error }
The Serializable
interface has two methods. The Serialize()
method takes no arguments and returns a string and an error, and the Deserialize()
method takes a string and returns an error. If you’ve been around the block, the Serializable
interface is probably familiar to you from other languages, and you can guess that the Serialize()
method returns a serialized version of the target object that can be reconstructed by calling Deserialize()
and passing the result of the original call to Serialize()
.
Note that you don’t need to provide the “func” keyword at the beginning of each method declaration. Go already knows that an interface can only contains methods and doesn’t need any help from you telling it it’s a “func”.
Go Interfaces Best Practices
Go interfaces are the best way to construct the backbone of your program. Objects should interact with each other through interfaces and not through concrete objects. This means that you should construct an object model for your program that consists only of interfaces and basic types or data objects (structs whose members are basic types or other data objects). Here are some of the best practices you should pursue with interfaces.
Clear Intentions
It’s important that the intention behind every method and the sequence of calls is clear and well defined both to callers and to implementers. There is no language-level support in Go for that. I’ll discuss it more in the “Interface vs. Contract” section later.
Dependency Injection
Dependency injection means that an object that interacts with another object through an interface will get the interface from the outside as a function or method argument and will not create the object (or call a function that returns the concrete object). Note that this principle applies to standalone functions too and not just objects. A function should receive all its dependencies as interfaces. For example:
type SomeInterface { DoSomethingAesome() } func foo(s SomeInterface) { s.DoSomethingAwesome() }
Now, you call function foo()
with different implementations of SomeInterface
, and it will work with all of them.
Factories
Obviously, someone has to create the concrete objects. This is the job of dedicated factory objects. Factories are used in two situations:
- At the beginning of the program, factories are used to create all the long-running objects whose lifetime typically matches the lifetime of the program.
- During the program runtime, various objects often need to instantiate objects dynamically. Factories should be used for this purpose too.
It is often useful to provide dynamic factory interfaces to objects to sustain the interface-only interaction pattern. In the following example, I define a Widget
interface and a WidgetFactory
interface that returns a Widget
interface from its CreateWidget()
method.
The PerformMainLogic()
function receives a WidgetFactory
interface from its caller. It is now able to dynamically create a new widget based on its widget spec and invokes its Widgetize()
method without knowing anything about its concrete type (what struct implements the interface).
type Widget interface { Widgetize() } type WidgetFactory interface { CreateWidget(widgetSpec string) (Widget, error) } func PerformMainLogic(factory WidgetFactory) { ... widgetSpec := GetWidgetSpec() widget := factroy.CreateWidget(widgetSpec) widget.Widgetize() }
Testability
Testability is one of the most important practices for proper software development. Go interfaces are the best mechanism to support testability in Go programs. To thoroughly test a function or a method, you need to control and/or measure all inputs, outputs and side-effects to the function under test.
For non-trivial code that communicates directly with the file system, the system clock, databases, remote services and user interface, it is very difficult to achieve. But, if all interaction goes through interfaces, it is very easy to mock and manage the external dependencies.
Consider a function that runs only at the end of the month and runs some code to clean up bad transactions. Without interfaces, you would have to go to extreme measures such as changing the actual computer clock to simulate the end of the month. With an interface that provides the current time, you just pass a struct that you set the desired time to.
Instead of importing time
and directly calling time.Now()
, you can pass an interface with a Now()
method that in production will be implemented by forwarding to time.Now()
, but during testing will be implemented by an object that returns a fixed time to freeze the test environment.
Using a Go Interface
Using a Go interface is completely straightforward. You just call its methods like you call any other function. The big difference is that you can’t be sure what will happen because there may be different implementations.
Implementing a Go Interface
Go interfaces can be implemented as methods on structs. Consider the following interface:
type Shape interface { GetPerimeter() int GetArea() int }
Here are two concrete implementations of the Shape interface:
type Square struct { side uint } func (s *Square) GetPerimeter() uint { return s.side * 4 } func (s *Square) GetArea() uint { return s.side * s.side } type Rectangle struct { width uint height uint } func (r *Rectangle) GetPerimeter() uint { return (r.width + r.height) * 2 } func (r *Rectangle) GetArea() uint { return r.width * r.height }
The square and rectangle implement the calculations differently based on their fields and geometrical properties. The next code sample demonstrates how to populate a slice of the Shape interface with concrete objects that implement the interface, and then iterate over the slice and invoke the GetArea()
method of each shape to calculate the total area of all the shapes.
func main() { shapes := []Shape{&Square{side: 2}, &Rectangle{width: 3, height: 5}} var totalArea uint for _, shape := range shapes { totalArea += shape.GetArea() } fmt.Println("Total area: ", totalArea) }
Base Implementation
In many programming languages, there is a concept of a base class that can be used to implement shared functionality used by all sub-classes. Go (rightfully) prefers composition to inheritance.
You can get a similar effect by embedding a struct. Let’s define a Cache
struct that can store the value of previous computations. When a value is retrieved from the case, it also prints to the screen “cache hit”, and when the value is not in the case, it prints “cache miss” and returns -1 (valid values are unsigned integers).
type Cache struct { cache map[string]uint } func (c *Cache) GetValue(name string) int { value, ok := c.cache[name] if ok { fmt.Println("cache hit") return int(value) } else { fmt.Println("cache miss") return -1 } } func (c *Cache) SetValue(name string, value uint) { c.cache[name] = value }
Now, I’ll embed this cache in the Square and Rectangle shapes. Note that the implementation of GetPerimeter()
and GetArea()
now checks the cache first and computes the value only if it is not in the cache.
type Square struct { Cache side uint } func (s *Square) GetPerimeter() uint { value := s.GetValue("perimeter") if value == -1 { value = int(s.side * 4) s.SetValue("perimeter", uint(value)) } return uint(value) } func (s *Square) GetArea() uint { value := s.GetValue("area") if value == -1 { value = int(s.side * s.side) s.SetValue("area", uint(value)) } return uint(value) } type Rectangle struct { Cache width uint height uint } func (r *Rectangle) GetPerimeter() uint { value := r.GetValue("perimeter") if value == -1 { value = int(r.width + r.height) * 2 r.SetValue("perimeter", uint(value)) } return uint(value) } func (r *Rectangle) GetArea() uint { value := r.GetValue("area") if value == -1 { value = int(r.width * r.height) r.SetValue("area", uint(value)) } return uint(value) }
Finally, the main()
function computes the total area twice to see the cache effect.
func main() { shapes := []Shape{ &Square{Cache{cache: make(map[string]uint)}, 2}, &Rectangle{Cache{cache: make(map[string]uint)}, 3, 5} } var totalArea uint for _, shape := range shapes { totalArea += shape.GetArea() } fmt.Println("Total area: ", totalArea) totalArea = 0 for _, shape := range shapes { totalArea += shape.GetArea() } fmt.Println("Total area: ", totalArea)
Here is the output:
cache miss cache miss Total area: 19 cache hit cache hit Total area: 19
Interface vs. Contract
Interfaces are great, but they don’t ensure that structs implementing the interface actually fulfill the intention behind the interface. There is no way in Go to express this intention. All you get to specify is the signature of the methods.
In order to go beyond that basic level, you need a contract. A contract for an object specifies exactly what each method does, what side effects are performed, and what the state of the object is at each point in time. The contract always exists. The only question is if it’s explicit or implicit. Where external APIs are concerned, contracts are critical.
Conclusion
The Go programming model was designed around interfaces. You can program in Go without interfaces, but you would miss their many benefits. I highly recommend that you take full advantage of interfaces in your Go programming adventures.