Interfaces in Go
Table of Contents
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.
All the content from our Boot.dev courses are available for free here on the blog. This one is the "Interfaces" chapter of Learn Go. If you want to try the far more immersive version of the course, do check it out!
What Is an Interface in Go?
Interfaces allow you to focus on what a type does rather than how it's built. They can help you write more flexible and reusable code by defining behaviors (like methods) that different types can share. This makes it easy to swap out or update parts of your code without changing everything else.
Interfaces are just collections of method signatures. A type "implements" an interface if it has methods that match the interface's method signatures.
In the following example, a "shape" must be able to return its area and perimeter. Both rect and circle fulfill the interface.
type shape interface {
area() float64
perimeter() float64
}
type rect struct {
width, height float64
}
func (r rect) area() float64 {
return r.width * r.height
}
func (r rect) perimeter() float64 {
return 2*r.width + 2*r.height
}
type circle struct {
radius float64
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
return 2 * math.Pi * c.radius
}
When a type implements an interface, it can then be used as that interface type.
func printShapeData(s shape) {
fmt.Printf("Area: %v - Perimeter: %v\n", s.area(), s.perimeter())
}
Because we say the input is of type shape, we know that any argument must implement the .area() and .perimeter() methods.
The error Interface
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"
Interfaces Are Implemented Implicitly
Interfaces are implemented implicitly.
A type never declares that it implements a given interface. If an interface exists and a type has the proper methods defined, then the type automatically fulfills that interface.
Unlike in many other languages, there is no explicit declaration of intent, there is no "implements" keyword.
Implicit interfaces decouple the definition of an interface from its implementation. You may add methods to a type and in the process be unknowingly implementing various interfaces, and that's okay.
A type can implement any number of interfaces in Go.
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
}
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.
Type Assertions in Go
When working with interfaces in Go, every once in a while you'll need access to the underlying type of an interface value. You can cast an interface to its underlying type using a type assertion.
The example below shows how to safely access the radius field of s when it is not known that s is a circle:
type shape interface {
area() float64
}
type circle struct {
radius float64
}
func (c circle) area() float64 {
// ...
}
func printShapeInfo(s shape) {
c, ok := s.(circle)
if ok {
radius := c.radius
fmt.Printf("s is a circle, radius: %v\n", radius)
return
}
}
In printShapeInfo:
- We want to check if
sis acircleto cast it into its underlying concrete type - We know (from the function signature) that
sis an instance of theshapeinterface, but we do not know if it's also acircle cis a newcirclestruct cast fromsokistrueifsis indeed acircle, orfalseifsis not acircle
Type Switches
A type switch makes it easy to do several type assertions in a series.
A type switch is similar to a regular switch statement, but the cases specify types instead of values.
func printNumericValue(num interface{}) {
switch v := num.(type) {
case int:
fmt.Printf("%T\n", v)
case string:
fmt.Printf("%T\n", v)
default:
fmt.Printf("%T\n", v)
}
}
func main() {
printNumericValue(1)
// prints "int"
printNumericValue("1")
// prints "string"
printNumericValue(struct{}{})
// prints "struct {}"
}
fmt.Printf("%T\n", v) prints the type of a variable.
Best Practices for Writing Interfaces
Writing clean interfaces is hard. Frankly, any time 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.
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. Name Your Interface Parameters
Consider the following interface:
type Copier interface {
Copy(string, string) int
}
This is a valid interface, but based on the code alone, can you deduce what kinds of strings you should pass into the Copy function?
We know the function signature expects 2 string types, but what are they? Filenames? URLs? Raw string data? For that matter, what the heck is that int that's being returned?
Let's add some named parameters and return data to make it more clear.
type Copier interface {
Copy(sourceFile string, destinationFile string) (bytesCopied int)
}
Much better. We can see what the expectations are now. The first parameter is the sourceFile, the second parameter is the destinationFile, and bytesCopied, an integer, is returned.
3. 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. 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 as an embedded interface and adds one additional required method to make the car a firetruck.
4. Interfaces Are Not Classes
- Interfaces are not classes, they are slimmer.
- Interfaces don't have constructors or destructors 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 regard to struct methods. For example, if five types satisfy the
fmt.Stringerinterface, they all need their own version of theString()function.
Frequently Asked Questions
What is an interface in Go?
An interface is a collection of method signatures. It lets code focus on what a type does rather than how the type is built.
How does a type implement an interface in Go?
Interfaces are implemented implicitly. If a type has methods that match an interface's method signatures, it automatically fulfills that interface without an implements keyword.
Can a type implement multiple interfaces in Go?
Yes. A type can implement any number of interfaces in Go as long as it has the methods each interface requires.
What is the empty interface in Go?
The empty interface specifies no methods, so every type in Go implements it. It is written as interface{}.
What is the difference between a type assertion and a type switch?
A type assertion accesses one underlying concrete type from an interface value. A type switch performs several type assertions in a series, with cases that specify types instead of values.
