In this article we are going to do a crash course on Go’s interfaces, then in the rest of this series we will continue exploring how interfaces work, how to use them effectively, and more.
Let’s start by talking a bit about how Go differs from dynamic languages like JavaScript, Ruby, and Python. When creating functions in dynamic languages, you don’t define the type you expect to receive. Instead, anything can be passed in. Below is an example of this using JavaScript.
function Greeting(name) {
return "Hello, " + name
}
// The following is all valid in JS, even if it doesn't make much sense.
Greeting("Jon")
// Output: "Hello, Jon"
Greeting(123)
// Output: "Hello, 123"
Greeting(document)
// Output: "Hello, [object HTMLDocument]"
In Go you have to explicitly state types, which means when you are creating a function you have to state that you want the name argument to be a string.
func Greeting(name string) string {
return fmt.Sprintf("Hello, %v!", name)
}
For the most part this is a good thing. It makes it clear to anyone reading your code exactly what you expect, and it prevents other developers from accidentally passing in the wrong thing.
If you have ever used a dynamic language, chances are you have experienced errors because someone passed the wrong thing into a function – like passing in the entire document in the JS example! This type of error is far less likely to occur in Go because of the static type system.
Unfortunately, static typing also limits our code quite a bit. For instance, imagine you had some code that wrote our greeting to a file.
func WriteGreeting(name string, f *os.File) error {
greeting := Greeting(name)
_, err := f.Write([]byte(greeting))
if err != nil {
return err
}
return nil
}
This code works perfectly, but it is limited to writing to files. What if we wanted to instead write the greeting to a strings.Builder?
Without interfaces, we would be forced to rewrite the function to accept a strings.Builder
!
func WriteGreeting(name string, sb *strings.Builder) error {
greeting := Greeting(name)
_, err := sb.Write([]byte(greeting))
if err != nil {
return err
}
return nil
}
Sidenote: Yes, I know strings.Builder
has a WriteString method. It will make a bit more sense why I am opting to instead use Write in a bit.
Looking at the code for both of these functions, you will notice that they are very similar. Ignoring the variable name change, the only real difference is that in the first code sample we are using File.Write and in the second we are using Builder.Write, but in both of these cases the method we are using has the following definition:
Write(p []byte) (int, error)
It turns out, we don’t really care if we are dealing with a strings.Builder
or an os.File
. All we really care is that we are given something with the Write
method. With that our code would continue to work.
Luckily, Go has something called interfaces that enable us to express exactly this. Below is an example of an interface that would work for this particular case.
type Writer interface {
Write([]byte) (int, error)
}
With the interface defined, we could rewrite our WriteGreeting
function like so:
type Writer interface {
Write([]byte) (int, error)
}
func WriteGreeting(name string, w Writer) error {
greeting := Greeting(name)
_, err := w.Write([]byte(greeting))
if err != nil {
return err
}
return nil
}
Now any type with the Write method can be passed into WriteGreeting! In fact, this interface is so common that it is defined in the standard library 👉 io.Writer
One of the neatest parts about Go is that it uses duck typing. To quote Mat (and whoever originally coined the term), “If it looks like a duck, and it quacks like a duck, then it is a duck”.
(okay, technically it isn’t duck typing, but “structural typing” doesn’t have a catchy “quacks like a duck” quote! 😂)
Or in the context of Go, if it looks like a Writer interface, has the methods defined by the Writer interface, then it is a Writer interface implementation.
I mention duck typing because it isn’t true in all statically typed languages. For instance, in Java you need to explicitly state that a class implements an interface, otherwise the compiler won’t let you use it as an interface implementation. This has a big impact on how interfaces are used in Go compared to languages like Java, and we will discuss that a bit more in a future email.
Getting into more concrete details, any type that has every method defined by an interface will implement that interface. If a type has more methods than the interface defines that is okay too, but those additional methods will NOT be accessible while inside a function that accepts the interface.
func main() {
var sb strings.Builder
WriteGreeting("Jon", &sb)
}
func WriteGreeting(name string, w Writer) error {
greeting := Greeting(name)
// We can access Write here because it is defined by the Writer interface,
// but we CANNOT access the Builder.WriteString method here even if we passed
// in a strings.Builder because WriteString is not defined as part of the
// Writer interface.
_, err := w.Write([]byte(greeting))
if err != nil {
return err
}
return nil
}
type Writer interface {
Write([]byte) (int, error)
}
Every method defined by the interface must be implemented for a type to implement an interface. The following example will result in a compiler error because our Demo type doesn’t define all of the methods defined by BigInterface:
type BigInterface interface {
A()
B()
C()
}
type Demo struct {}
func (Demo) A() {}
func (Demo) B() {}
func Test(bi BigInterface) {}
func main() {
var demo Demo
Test(demo) // errors because Demo doesn't implement the C() method.
}
Because of this, it is common to limit interfaces to specifically the methods you need rather than a large set of methods, as this makes it easier for other types to implement the interface.
Hopefully this helped shine some light on what interfaces are and why we might want to use them. In the next few articles I will dive into interfaces a bit more exploring topics like:
- The downsides to interfaces, including a story about how the
io.Writer
andio.Reader
interfaces confused me when I first started learning Go. - Common naming patterns for interfaces
- How pointer methods affect whether a type implements an interface.
- How duck typing changes the way we use interfaces in Go compared to languages like Java
- Info about naming arguments for interface methods
- Examples of where using interfaces can make your code worse.
- How interfaces make testing easier.
That is a lot though, so it will be broken into a few posts and will be gradually released. Until then, give what you learned here a try in your code. Explore a bit and see what you discover 😁