Go interfaces: small and composable
Interfaces in Go are a topic that tend to trip up people coming from a variety of languages like Java or C#. The oddest thing about them is probably that interfaces are implicit
rather than explicit
. So whereas in most languages there is some type of syntax or a keyword to say you’re implementing an interface, there’s no such thing in Go.
Another difference is that good interfaces in Go should be small — they should contain only a few methods. I know how tempting it is to just add one more method to an interface, and have them grow organically like that. This does create some downsides in terms of having others use your interface and refactor work if you change them later.
Example implementation
Let’s imagine you’re creating a simple File System that you want to share as Open-Source software. You think for some time and come up with a few methods any file system needs. Realizing everyone might benefit from your filesystem implementation, you decide to put everything in an interface, as anyone can now implicitly implement your interface and benefit from your library. That’s exactly one of the powers of Go interfaces!
type FileSystem interface {
Read(File) []byte
Write([]byte, File)
Open(File)
Close(File)
}
So you’re feeling pretty great, and you start implementing some functions for your library. As such you create a function to write logs to the filesystem.
func Log(msg []byte, f File, fs FileSystem)
fs.Write(msg, f, myLogfile)
}
That’s great, and after some time, someone thinks it would be great to take advantage of your log function. As your code accepts an interface, they can create a struct that satisfies the methods. Even though your Log
function only needed a struct that could “Write” bytes to a file, they did have to implement all your other unrelated functions as well.
Often you’ll find that only certain methods of structs are used together. Or even just one of them like in the example above. It’s a better idea if they can just rely on the minimum behaviour they need to work. So in our example, a way to refactor it would be something like this:
type FileWriter Interface {
Write([]byte, File)
}func Log(msg []byte, f File, fw FileWriter) {
fw.Write(msg,f)
}
As interfaces are composable, you can create more complex interfaces from smaller ones, like below:
type FileWriter interface {
Write([]byte, File)
}
type FileReader interface {
Read(File) []byte
}
type FileOpenCloser interface {
Open(File)
Close(File)
}
type ComposedFileSystem interface {
FileWriter
FileOpenCloser
FileReader
}
This gives users of your library the option to create smaller structs — it’s easier to implement only what you need.
This is also how a lot of the standard Go interfaces work, and I believe we can rely on them to write good Go code 😄.
Check out io/io.go:
type Reader interface {
Read(p []byte) (n int, err error)
}type Writer interface {
Write(p []byte) (n int, err error)
}type Closer interface {
Close() error
}type ReadWriter interface {
Reader
Writer
}type WriteCloser interface {
Writer
Closer
}type ReadWriteCloser {
Reader
Writer
Closer
}
As you can see, it’s fine to even have single method interfaces. Composition is central in Go, both in terms of interfaces and structs. 😃
If you liked this post and 💙 Go as well, consider:
- Following me here, on Medium
- Or twitter Twitter
- Or check out workwithgo.com to find cool Go jobs around the world