One of the most appealing features of Go is the concept of Interfaces. Go borrows interesting ideas from different systems to make them powerful enough to enable developers to simplify their abstractions and make the system flexible to allow polymorphism.
Basic Concepts
What’s a Go Interface
An interface type is defined as a set of method signatures. [1]
An interface is an abstract concept which enables polymorphism in Go. [2]
In Go, every type has an interface, ‘a set of methods’ defined for that type. Let’s define a struct
S
with a field and two defined methods:
1type S struct {
2 n int
3}
4
5func(p *S) Get() int {
6 return p.n
7}
8
9func(p *S) Set(v int) {
10 p.n = v
11}
As it is, S
already implements the empty interface (more about it below), making our statement that ’every type has an interface’ to be true. However, we can define an interface I
with two method signatures as follows.
1type I interface {
2 Get() int
3 Set(v int)
4}
Implicit interface
As it is, S
also implements the I
interface, because it defines the methods required by I
. Why is that? A type implements an interface by defining the methods it enforces. “There is no explicit declaration of intent, no ‘implements’ keyword”. The design behind the implicit interface is meant to decouple the definition from its implementation. [3]
Interface values
Interface values can be thought of as a tuple of a value and a concrete type. [4]
In Go, interface values have a concrete value and a dynamic type. [2]
1func f(p I) {
2 fmt.Println(p.Get())
3 p.Put(1)
4}
Here, the variable p
will hold a value of interface I
type.
1var s S
2f(&s)
As S
implements I
, we can call f
passing in a pointer to a value of type S
. It is not strictly necessary to pass a pointer; only the Get
would work as expected, whereas Put
won’t. Thus, besides the fact that we have defined it to operate on pointers, we prefer to do so. [5]
This might sound familiar because, in Go, you don’t need to explicitly declare that a type implements an interface, resembling a form of duck typing. However, it’s not purely duck typing, as the Go compiler can statically check if a type implements the interface. This is known as Structural Typing in contrast to Normal Typing as seen in C++, and Duck Typing as in Python. Still, Go has a purely dynamic aspect: you can convert between interfaces for a given type, and this is checked solely at runtime. If the condition isn’t met, the program will produce a runtime error. [5]
Underlying nil values
If the concrete value for an interface is nil
, their methods will be called with a nil
receiver.
In Go, this allows to write methods that gracefully handle being called with a nil
receiver.
Let’s analyze the following code (taken from A Tour of Go - Interface values with nil underlying values)
1type I interface {
2 M()
3}
4
5type T struct {
6 S string
7}
8
9func(t *T) M() {
10 if t == nil {
11 fmt.Println("<nil>")
12 return
13 }
14 fmt.Println(t.S)
15}
16
17func describe(i I) {
18 fmt.Printf("(%v, %T)\n", i, i)
19}
Here it’s clear that T
implements the interface I
as it has a method M
that complies with the given interface’s contract.
Now, let’s assume the following code inside the main
function.
1var i I
2
3i = &T{"hello"}
4describe(i)
5i.M()
This yields the following output.
(&{hello}, *main.T)
hello
It’s quite obvious to understand that the dynamic value is a *T
and its concrete values is an instance of that type.
Now, let’s check the following code.
1var t *T
2i = t
3describe(i)
4i.M()
In contrast, this one yields the following output.
(<nil>, *main.T)
<nil>
Under this circumstance, other languages would throw a nil pointer exception, but in Go, this is perfectly handled by methods by assessing the receiver was called with a nil
receiver. Nonetheless, note that the interface value that holds the nil
concrete type is itself non-nil. [6]
But, how it would be if the dynamic type is also nil
?
Nil interface values
Nil interface values are those that hold neither value nor concrete type. [7]
Let’s suppose the following code in the main
function.
1var i I
2describe(i)
3i.M()
This yields the following output.
(<nil>, <nil>)
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x490339]
goroutine 1 [running]:
main.main()
/tmp/sandbox2509058701/prog.go:12 +0x19
Calling a method on a nil interface yields a run-time error as there is no type inside the interface tuple to indicate which concrete method to call. [7]
The empty interface
The name is self-explanatory: an interface with no methods declared in it. Just that.
1interface{}
An empty interface may hold values of any type. If you think about it, all the types implement at least zero methods 😄.
Why is it so powerful? Sometimes, you simply don’t know what you are dealing with. Take for instance fmt.Println
you may pass a string
, an int
, or struct
s.
Pro tip: any
is an alias for an empty interface 1.
Implementing multiple interfaces in Go
In Go you can implement multiple interfaces at the same time. If all the functions defined by a type complies with all the functions of different interfaces, then the type implements all of those.
1type(
2 Walker interface {
3 Walk()
4 }
5
6 Flyer interface {
7 Fly()
8 }
9
10 Swimmer interface {
11 Swim()
12 }
13)
14
15type Duck struct {
16 S name
17}
18
19func(d *Duck) Walk() {
20 // ...
21}
22
23func(d *Duck) Fly() {
24 // ...
25}
26
27func(d *Duck) Swim() {
28 // ...
29}
Interfaces belong to Consumers, not Producers.
Folks coming from other languages like Java, or C++ are likely to follow the pattern where they first define abstract types or interfaces, and then implement them concretely. This pattern is necessary because these languages generate a static dispatch table (e.g., like a vtable in C++) during compilation. The compiler requires explicit declaration of the interfaces a type implements, allowing it to construct a vtable, storing pointers to each available virtual function. This table enables polymorphism at runtime by dynamically calling the appropriate method based on the actual object’s type.
In Go we don’t have a traditional dispatch table. As we have already discussed, it can rely on the interface values during a method dispatch. The work is done during the interface value assignment through a tiny hash-table lookup to check for the concrete type it’s pointing to [8].
By defining interfaces on the consumer side we provide users with a certain level of “Inversion of Control” [9]. They define their abstractions rather than having a producer forcing them to use their idea of an abstraction. Additionally, we minimize the presumptions about the way a package is consumed.
This approach also makes it easier to add new methods to implementations without affecting any consumers.
“The bigger the interface, the weaker the abstraction” - Rob Pike
Back in 2015 Rob Pike succinctly said that “The bigger the interface, the weaker the abstraction”. It has become one of the most known Go Proverbs.
Let’s review for a moment both the io.Reader
and the io.Writer
interfaces in Go’s standard library.
io.Reader
1type Reader interface {
2 Read(p []byte) (n int, err error)
3}
The io.Reader
interface represents anything that can read a stream of bytes. This is useful for reading from different sources like files, network connections, or even memory. It only has one single method: Read
, which takes a slice of bytes (p []byte)
as an argument, it acts as a container for the data to be read. It fills up p with data and then returns how many bytes were read, or an error if it’s the case.
This interface is extremely powerful because it provides the flexibility to allow any type to implement the Read
wherever an io.Reader
is expected.
io.Writer
1type Writer interface {
2 Write(p []byte) (n int, err error)
3}
The io.Writer
interface represents anything that can receive and store a stream of bytes. This is useful for sending data to files, network connections, memory buffers, or even the console. It only has one single method: Write
, which takes a slice of bytes (p []byte)
as an argument and writes it to a destination.
This interface is extremely powerful because it provides the flexibility to allow writing data to various outputs without needing to know exactly where the data is going.
Mixing io.Reader
and io.Writer
Together
Let’s implement a function that copies the content of a file into another. We might create a function that takes two *os.File
. Or, we could implement a more generic approach using the abstraction that both io.Reader
and io.Writer
provide.
1func Copy(src io.Reader, trgt io.Writer) error {
2 // ...
3}
Does it work to copy from one file into another? It perfectly does! Why? Because *os.File
implements Read
and Write
. So it is, at least, a ReadWriter
.
What about testing?
1func TestCopySourceToDest(t *testing.T) {
2 const input = "foo"
3 source := strings.NewReader(input) // Creates an io.Reader
4 dest := bytes.NewBuffer(make([]byte, 0)) // Creates an io.Writer
5
6 err := copySourceToDest(source, dest) // Calls copySourceToDest from a *strings.Reader and a *bytes.Buffer
7 if err != nil {
8 t.FailNow()
9 }
10
11 got := dest.String()
12 if got != input {
13 t.Errorf("expected: %s, got: %s", input, got)
14 }
15}
Do we need to test reading from an actual file and writing it into another one? No. We only need to test the function’s behavior. We have made clear *os.File
implements both, but it is not the only one (this is the power of abstraction). The package strings
also has a reader that implements io.Reader
and the package bytes
has a buffer that implements io.Writer
. Our test perfectly shows taking a string reader and a buffer to write a given content. The important thing here is not what the source and target are, the important thing is that we take content from a source and write it into a target [10].
The Power of Single Method Interfaces (SMI)
In functional languages, High Order Functions [11] are heavily used to achieve useful abstractions by means of closures. In Go, you can use them, but most Go code prefers to use SMI mainly for two reasons:
Nonetheless, while SMIs are more general and powerful, they are also more verbose.
What to use then? Simple. If all you need is a function, then use a function! Otherwise, explore SMIs as they provide enough abstraction to get the job done.
Interface Pollution
Interfaces are all about creating abstractions. As such, they are meant to be discovered, not designed or created prematurely. We define them when we need them, not when we think we might need them.
Interfaces are powerful, but overusing them can clutter our code with unnecessary complexity. Excessive levels of indirection can do more harm than good, making the code unreadable and harder to reason about.
This also means that you shouldn’t export any interfaces until you have to [8]. If you think that “maybe” you should export a generic functionality, you are likely to pollute your code with useless interfaces no one is going to use. You need to justify such a need. This is precisely why in Go we prefer to accept interfaces but return concrete types 2.
Conclusion
The interfaces concept is a powerful tool in Go that allows us to abstract the behavior of a system. However, overusing them makes the code hard to reason about. We can guarantee a good abstraction level by keeping interfaces as short as possible (hopefully striving for SMIs). To maintain a clean code, we prefer to accept interfaces defined on the consumer side, returning concrete types to avoid polluting our codebase with useless interfaces no one is going to use. In the end, make the coding process enjoyable so you get to discover your interfaces rather than try to force them with worthless designs.
Bibliography
A Tour of Go - Interfaces,
Oct. 2024. [Online]. Available: https://go.dev/tour/methods/9Interfaces in Golang - Golang Docs,
Jan. 2020. [Online]. Available: https://golangdocs.com/interfaces-in-golangA Tour of Go - Interfaces are implemented implicitly,
Oct. 2024. [Online]. Available: https://go.dev/tour/methods/10A Tour of Go - Interface values,
Oct. 2024. [Online]. Available: https://go.dev/tour/methods/11- C. Paar and J. Pelzl,
Go Interfaces,
Oct. 2024. [Online]. Available: https://www.airs.com/blog/archives/277 A Tour of Go - Interface values with nil underlying values,
Nov. 2024. [Online]. Available: https://go.dev/tour/methods/12A Tour of Go - Nil interface values,
Nov. 2024. [Online]. Available: https://go.dev/tour/methods/13Interface pollution in Go rakyll.org,
Oct. 2014. [Online]. Available: https://rakyll.org/interface-pollutionMartin Fowler: Inversion Of Control,
Jun. 2005. [Online]. Available: https://martinfowler.com/bliki/InversionOfControl.htmlInterface pollution (#5) - 100 Go Mistakes and How to Avoid Them,
Oct. 2024. [Online]. Available: https://100go.co/5-interface-pollutionHigher-order Programming,
Nov. 2024. [Online]. Available: https://www.cs.cornell.edu/courses/cs3110/2017fa/l/06-hop/notes.htmlThe power of single-method interfaces in Go - Eli Bendersky’s website,
Mar. 2023. [Online]. Available: https://eli.thegreenplace.net/2023/the-power-of-single-method-interfaces-in-go