When first learning Go, you are bound to run across the interface
keyword pretty quickly.
It pops up all over the place in the language, and even if you didn’t realize it, you have likely either used an interface in one way or another.
As you continue writing code in Go, you will eventually run across a function that takes in a parameter of the type interface{}
and you might start to ask yourself, what the hell is that? To answer that question, we are going to take a step back and look at how interfaces work in Go, and then we will come back to the interface{}
type.
Interfaces serve a few purposes, but the most common ones are:
For example, let’s imagine that you are working on the next big geology SaaS application and you are offering a feature that will help your users compare the density of different rocks.
type Rock struct {
Mass int
Volume int
}
func (r Rock) Density() int {
return r.Mass / r.Volume
}
func IsItDenser(a, b Rock) bool {
return a.Density() > b.Density()
}
This might seem perfectly sane at first, but over time you might discover that you need to compare the density of a bunch of different objects, some of which aren’t necessarily rock.
Rather than writing the same code over and over again, it would be nice if we could write some code that said “I need an argument that has the Density()
method.”
Or in other words, instead of specifying the data type that we want to accept in our IsItDenser()
function, it would be nice if we could specify the behavior of the objects that need to be passed in. Stealing from the effective go, “if something can do this, then it can be used here”.
Interfaces are meant to solve this problem.
Continuing with our Rock example, someone could come along and introduce a Geode
type that has a completely different implementation of the Density()
method.
type Geode struct {
}
func (g Geode) Density() int {
return 100
}
At this point our IsItDenser()
function isn’t going to work with both geodes and rocks types, but there is no real reason why we couldn’t make it work. It isn’t doing anything specific to the Rock
type.
This is where interfaces shine; Interfaces provide us with a way of writing our IsItDenser()
function so that it doesn’t care what type of object you pass in as long as it implements the methods required by the interface.
To make this work in our rock and geode example, we would first start off by creating an interface type.
type Dense interface {
Density() int
}
Now we have both a Geode
and Rock
type, both of which have a Density()
method, so they are both implementations of the Dense
interface by default. No extra code is necessary to implement an interface in Go.
After creating our interface we would then update our IsItDenser()
function to accept parameters of the Dense
type instead of the Rock
type.
func IsItDenser(a, b Dense) bool {
return a.Density() > b.Density()
}
After making those changes our code would be able to compare both rocks and geodes with ease, and if we ever introduce another type we can pass it in to the IsItDenser()
function as long as it has a Density()
method.
How that interface gets implemented is irrelevant; It could be handled by an embedded field, or by explicitly writing a Density()
method. As long as our IsItDenser()
function can call a.Density()
and b.Density()
it does not care.
package main
import "fmt"
type Rock struct {
Mass int
Volume int
}
func (r Rock) Density() int {
return r.Mass / r.Volume
}
func IsItDenser(a, b Dense) bool {
return a.Density() > b.Density()
}
type Geode struct {
}
func (g Geode) Density() int {
return 100
}
type Dense interface {
Density() int
}
func main() {
r := Rock{10, 1}
g := Geode{}
// Returns true because Geode's Density method always
// returns 100
fmt.Println(IsItDenser(g, r))
}
In addition to making our code more versatile, the interfaces also serve another subtle purpose. By accepting the Dense
type in our IsItDenser()
function we have forced our code to be encapsulated.
Even if we wanted to access attributes that we knew were present on the Rock
type, by using an interface we are limited to only calling methods defined in the Dense
interface.
As a result, developers are prevented from designing bad code that calculates the density inside of the IsItDenser()
function, and instead are forced to rely on methods defined on the provided arguments.
interface{}
type?Interfaces in Go are not the same as interfaces in other languages.
Instead, interfaces in Go are somewhere between interfaces in Java, and the approach often taken in dynamic languages like Ruby to define an interface. Let me explain before you call me a loon.
In Java you would declare a class as an implementor of an interface explicitly when declaring the class.
public class X implements InterfaceA, InterfaceB {
// ...
}
Java requires this explicit declaration in order to use class X
as the InterfaceA
type. You can’t pass an instance of X
to a method that requires an InterfaceC
even if X
implements every method required by InterfaceC
.
Once the declaration is made, you can pass instances of that type into any methods that expect InterfaceA
or InterfaceB
.
Ruby on the other hand is a dynamic language with no restrictions at all. You can pass any data type to any method, so you have a few options. One is to not check anything at all and just try to call the method.
def is_it_denser(a, b)
a.density > b.density
end
A slightly safer approach is to check to see if the arguments implement the method that you need them to.
def is_it_denser(a, b)
unless a.respond_to?(:density) && b.respond_to?(:density)
raise ArgumentError, "Invalid arguments. Both must respond to 'density'."
end
a.density > b.density
end
This will at least verify that the method exists on both objects before calling, but it can be a bit annoying if you need a couple methods available or have a few arguments coming in.
To solve this issue, you will often seen code that utilizes the NotImplementedError
as a way of telling other developers what methods they need to implement if they inherit a class or include a module.
class InterfaceA
def do_a
raise NotImplementedError
end
end
class X < InterfaceA
def do_a
# do a
end
end
By inheriting the InterfaceA
class, the X
class also inherits it’s do_a
method. Unless X
implements its own custom version of do_a
, a NotImplementedError
will be raised when the method is called.
This makes it easier to write an is_it_denser
function because you can simply check that all passed in arguments inherit from InterfaceA
. The downside to this approach is that you can’t easily implement multiple interfaces, and you still need to manually verify that arguments passed in have the correct methods or inherit from the correct class.
Go falls somewhere in the middle of these two approaches. You don’t have to explicitly state that a type implements an interface to use it as an instance of that interface, so it is similar to Ruby in that sense, but on the other hand you do have to declare the interface for the compiler, which is more aligned with how interfaces work in a typed language like Java.
This approach might feel weird at first, but there are a lot of benefits that aren’t immediately obvious.
For starters, by declaring interface types we make it much easier for editors to offer useful code completion. Improved developer tools is always a win.
On top of that, the Ruby approach has the flaw that you won’t really know if a class doesn’t implement a method until you run that code. There isn’t a compiler to warn you things like this like there is in Java or Go.
All of these benefits seem to suggest that the Java approach would work just as well, but that also isn’t true.
By not requiring types to explicitly implement an interface, it becomes possible to use a much larger variety of types as an interface, including types that are part of code you didn’t write and can’t edit.
Take the standard library for example; We can’t go edit types in the standard library to tell the compiler that they implement the Dense
interface that we wrote earlier, but if one of the types in the standard library happens to have a Density()
method that returns an integer we could use it in our application as an argument to IsItDenser()
.
While this isn’t possible in Java, it is both possible and incredibly helpful in Go. This is especially true when you are writing a function that accepts a parameter that implements the empty interface.
interface{}
)And with that we have come back to the initial question. What is the empty interface (interface{}
)?
Think back to the example we wrote before. Do you remember writing the Dense
interface, and inside of it we wrote that there was 1 method that needed to be implemented - the Density()
method.
The empty interface is exactly the same as that interface, except we aren’t declaring any methods that need to be implemented. The code is essentially saying “I need an argument, and I don’t care what methods it implements.”
func PrintIt(a interface{}) {
// a can have any methods. We dont care.
fmt.Println(a)
}
Or in other words, when you say you need an argument of the type interface{}
you are saying “I need an argument that implements these methods…” and then you provide it with an empty set of methods.
That might seem odd at first, but it is insanely helpful because every type in Go implements the interface with no required methods.
As a result, any data type can be passed into parameters that are of the type interface{}
, which is why you see packages like encoding/json using this type.
The empty interface adds a ton of value at times because it matches any data type, but matching any data type is a double edged sword.
By using the empty interface in code you are essentially giving your compiler zero knowledge about the data coming in, so all of the benefits of a statically typed language go out the door. The compiler no longer has the ability to tell you that you made a mistake and passed it the wrong data type. You are left to check for errors like that on your own, and you won’t be able to find any bugs until runtime.
One of my favorite parts about Go is having a fairly high degree of confidence that my code will run as I expect it to, and the compile doing type checking is a huge contributor to this.
In Ruby (or Python or JavaScript) it is incredibly easy to pass the wrong argument to a function call and you never know about it until your code gets to that code path and errors, but Go’s type checking will tell you about this long before that ever happens. That is, unless you use the empty interface!
There are a few cases where this isn’t possible - like when you want to use the reflect library to turn arbitrary structs into a JSON string - but those are rare cases and should be avoided if possible. In most cases, rather than using an empty interface, it is almost always preferable to use a specific data type, or to create an interface with some specific methods that you will need. This will, at the very least, ensure that whatever you pass in to this function call has the methods you need, and you won’t have to use the reflect library or do any type assertions inside of your code to access those methods.
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.