Best Practices for Interfaces in Go
Interfaces in Go allow us to treat different types as the same data type temporarily because both types implement the same kind of behavior. They’re central to a Go programmer’s toolbelt and are often used improperly by new Go developers, which leads to unreadable and often buggy code.
What is an interface in Golang?
In Go, an interface is a custom type that other types are able to implement, which gives Go developers a powerful way to use abstraction. Interfaces are named collections of method signatures, and when other types implement all the required methods, they implicitly implement the interface.
For example, errors in Go are interfaces, and the standard error interface is simple, all a type needs to do to be considered an error is define an Error() method that accepts no parameters and returns a string.
type error interface {
Error() string
}
The simplicity of the error interface makes writing logging and metrics implementations much easier. Let’s define a struct that represents a network problem:
type networkProblem struct {
message string
code int
}
Then we can define an Error() method:
func (np networkProblem) Error() string {
return fmt.Sprintf("network error! message: %s, code: %v", np.message, np.code)
}
Now, we can use an instance of the networkProblem struct wherever an error is accepted.
func handleErr(err error) {
fmt.Println(err.Error())
}
np := networkProblem{
message: "we received a problem",
code: 404,
}
handleErr(np)
// prints "network error! message: we received a problem, code: 404"
Best practices for writing interfaces
Writing clean interfaces is hard. Frankly, anytime you’re dealing with abstractions in code, the simple can become complex very quickly if you’re not careful. Let’s go over some rules of thumb for keeping interfaces clean.
- Keep interfaces small
- Interfaces should have no knowledge of satisfying types
- Interfaces are not classes
1. Keep Interfaces Small
If there is only one piece of advice that you take away from this article, make it this: keep interfaces small! Interfaces are meant to define the minimal behavior necessary to accurately represent an idea or concept.
Here is an example from the standard HTTP package of a larger interface that’s a good example of defining minimal behavior:
type File interface {
io.Closer
io.Reader
io.Seeker
Readdir(count int) ([]os.FileInfo, error)
Stat() (os.FileInfo, error)
}
Any type that satisfies the interface’s behaviors can be considered by the HTTP package as a File. This is convenient because the HTTP package doesn’t need to know if it’s dealing with a file on disk, a network buffer, or a simple []byte.
2. Interfaces Should Have No Knowledge of Satisfying Types
An interface should define what is necessary for other types to classify as a member of that interface. They shouldn’t be aware of any types that happen to satisfy the interface at design time.
For example, let’s assume we are building an interface to describe the components necessary to define a car.
type car interface {
Color() string
Speed() int
IsFiretruck() bool
}
Color() and Speed() make perfect sense, they are methods confined to the scope of a car. IsFiretruck() is an anti-pattern. We are forcing all cars to declare whether or not they are firetrucks. In order for this pattern to make any amount of sense, we would need a whole list of possible subtypes. IsPickup(), IsSedan(), IsTank()… where does it end??
Instead, the developer should have relied on the native functionality of type assertion to derive the underlying type when given an instance of the car interface. Or, if a sub-interface is needed, it can be defined as:
type firetruck interface {
car
HoseLength() int
}
Which inherits the required methods from car and adds one additional required method to make the car a firetruck.
3. Interfaces Are Not Classes
- Interfaces are not classes, they are slimmer.
- Interfaces don’t have constructors or deconstructors that require that data is created or destroyed.
- Interfaces aren’t hierarchical by nature, though there is syntactic sugar to create interfaces that happen to be supersets of other interfaces.
- Interfaces define function signatures, but not underlying behavior. Making an interface often won’t DRY up your code in regards to struct methods. For example, if five types satisfy the error interface, they all need their own version of the
Error()function.
Additional information about interfaces
The empty interface
The empty interface doesn’t specify any methods, and as such every type in Go implements the empty interface.
interface{}
It’s for this reason that developers sometimes use a map[string]interface{} to work with arbitrary JSON data, although I recommend using anonymous structs instead where possible.
Zero value of an interface
Interfaces can be nil, in fact, it’s their zero value. That’s why when we check for errors in Go, we’re always checking if err != nil, because err is an interface.
Interfaces on pointers
It’s a common “gotcha” in Go to implement a method on a pointer type and expect the underlying type to implement the interface, it doesn’t work like that.
type rectangle interface {
height() int
width() int
}
type square struct {
length int
}
func (sq *square) width() int {
return sq.length
}
func (sq *square) height() int {
return sq.length
}
Though you may expect it to, in this example the square type does not implement the rectangle interface. The *square type does. If I wanted the square type to implement the rectangle interface I would just need to remove the pointer receivers.
type rectangle interface {
height() int
width() int
}
type square struct {
length int
}
func (sq square) width() int {
return sq.length
}
func (sq square) height() int {
return sq.length
}
Related Articles
Wrapping Errors in Go - How to Handle Nested Errors
Mar 09, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
Errors in Go are a hot topic. Many newcomers to the language immediately level their first criticism, “errors in go are clunky! Let me just use try/catch!” This criticism is well-meaning but misguided.
JWT Authentication in Golang
Feb 20, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
Go is becoming very popular for backend web development, and JWT’s are one of the most popular ways to handle authentication on API requests. In this article, we’ll go over the basics of JWT’s and how to implement a secure authentication strategy in Go!
BitBanged SPI in Go, An Explanation
Jan 09, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
I’m going to focus mostly on some design decisions and also how I went about writing an SPI interface using Go on a Raspberry Pi. I assume my readers have a basic understanding of what a Raspberry Pi is, and how basic electronics work. If not, read on anyway and I will be sure to include some valuable resources below.
Top 6 Golang Logging Best Practices
Jan 07, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
Let’s discuss a few rules of thumb for logging in Go, as well as some features you may not have heard of that can make debugging easier. Best practices for logging in Go are not so obvious and sometimes we need to look closer to see what is the best choice, considering the unique situation of error handling in Go.