In Go, interfaces take the center stage. They are more flexible and more powerful than their counterparts in other languages. When coming to Go from another language, this may not be immediately obvious, yet this realization is quite important for our ability to write well-structured and decoupled code. Let’s explore exactly how flexible they are and how we can make the best use of them.
Preamble
I’ll assume that you have at least a passing familiarity with the Go programming language and interfaces. In the following, I’ll be referring to both functions and methods. They are not the same, so you should know the difference[1][2]. Also, I’ll be referring to both parameters and arguments - again, not the same[3]. With that said, let’s Go!
Introduction
Interfaces enable polymorphism[4], i.e. they allow us to ignore what type a variable has and instead focus on what we can do with it (the behavior that it offers us via its methods), we gain the ability to work with any type, provided that it satisfies the interface. Go’s interfaces are structurally typed, meaning that any type is compatible with an interface if that type is structurally equivalent to the interface. An interface and a type are structurally equivalent if they both define a set of methods of the same name, and where methods from each share the same number of parameters and return values, of the same data type. This stands in contrast to explicit interfaces that must be referenced by name from the type that implements it[5]. In essence, we say that the type satisfies the interface.
A type is said to satisfy an interface if, and only if, it implements all the methods described by that interface. A quick example:
type foobar interface {
foo()
bar()
}
type itemA struct{}
func (a *itemA) foo() {
fmt.Println("foo on A")
}
func (a *itemA) bar() {
fmt.Println("bar on A")
}
type itemB struct{}
func (a *itemB) foo() {
fmt.Println("foo on B")
}
func (a *itemB) bar() {
fmt.Println("bar on B")
}
func doFoo(item foobar) {
item.foo()
}
func main() {
doFoo(&itemA{}) // Prints "foo on A"
doFoo(&itemB{}) // Prints "foo on B"
}
Code snippet 01 Example in the Playground
Here, we define an interface foobar
, and two items that both satisfy that interface by implementing the two methods mandated by the interface: foo
and bar
.
Function doFoo
can work with both itemA
and itemB
. Generally speaking, doFoo
will not know whether it received a function parameter of type itemA
or itemB
, nor should it care. doFoo
accepts anything that has the two methods foo
and bar
which implies that it can call those two methods on its argument. itemA
and itemB
may both implement other methods beyond what is mandated by the interface, but doFoo
can not see them nor invoke them. At least not while it knows nothing else about the parameter item
. But as we shall see, we can attempt both type- and interface conversions to expand the limits of what we can do with interface values.
The following characteristics are worth highlighting:
itemA
anditemB
both implement (satisfy)foobar
and can therefore be used interchangeably anywhere wherefoobar
is accepted.- We can invent a new type
itemC
(or whatever we want to call it), anddoFoo
will be able to use it without modification as long asitemC
also satisfiesfoobar
. doFoo
accepts any argument that implementsfoo
andbar
, and can thus invoke both these methods on such types.- Interfaces only mandate which methods a type must implement, not what they do. It’s up to each type to implement all the mandated methods.
- Aside from mandating which methods must be implemented, interfaces also mandate what their arguments and return values should look like.
That last point is worth exploring a bit. Here are two interfaces that are identical in every aspect:
type student interface {
register(context.Context, string, string, int)
enrollCourse(uint32, time.Time) error
pass(uint32) error
fail(uint32) error
}
type alsoStudent interface {
register(ctx context.Context, name, profession string, age int)
enrollCourse(courseID uint32, start time.Time) error
fail(courseID uint32) error
pass(courseID uint32) error
}
Code snippet 02
Again, these two interfaces are identical. They both define the same four methods, which take the same parameters, of the same type and in the same order. It’s irrelevant that the alsoStudent
interface has switched the order of the two last methods, and that it includes the names of the method arguments. Why? Because this doesn’t matter when Go’s compiler needs to determine if some type is structurally equivalent to the interface.
That being said, alsoStudent
is more readable to other developers because we’ve added a name to each argument, so it’s usually preferable to do so.
Finally, the argument names don’t have to match their counterparts in the implementations, but there’s no reason to confuse our fellow coders by not matching them up.
Implicit and structurally typed
When I was first introduced to interfaces in Java, they seemed immediately useful: they allowed polymorphism without requiring each type to inherit from the same base class, as in C++. But they were also oddly rigid and constrained to your own code base. It wasn’t until I picked up Go that I realized how much more elegant and flexible Go’s interface support is. Go’s interfaces are implicit and structurally typed[6], which makes them very powerful. Let’s look at a (simplified) example in Java[7]:
public interface Vehicle {
public void start();
public void stop();
}
public class Car implements Vehicle {
public void start() {
System.out.println("starting engine...");
}
public void stop() {
System.out.println("stopping engine...");
}
}
Code snippet 03
Car
must explicitly declare that it “implements Vehicle
”. The implication is that our own interfaces only work against our own code base. In Go, there is no such restriction. This is how the above example would look in Go:
type Vehicle interface {
Start()
Stop()
}
type Car struct {}
func (c *Car) Start() {
fmt.Println("starting engine...")
}
func (c *Car) Stop() {
fmt.Println("stopping engine...")
}
Code snippet 04
If Car
was defined in a third-party package (outside of our immediate control), Vehicle
could still be used in our own code base anywhere where a Car
might be accepted. In Java, we would have to first write a class that implements the interface and wraps the third-party package’s type by having each method of that class wrap and call the respective methods of the wrapped type, as in the Decorator design pattern[8].
Interface compatibility
If Go’s interfaces are implicit, i.e. a type doesn’t need to declare that it must satisfy the interface, then how do we know if the type satisfies the interface? As in Java, Go’s compiler checks them for us. Let’s look at two scenarios:
type toaster interface {
toast()
}
type acmeToaster struct {}
func (a *acmeToaster) toast() { fmt.Println("Commencing toasting of bread...") }
func doToast(t toaster) {
t.toast()
}
func maybeDoToast(i interface{}) {
if t, ok := i.(toaster); ok {
t.toast()
}
}
func main() {
t := new(acmeToaster)
doToast(t) // Prints "Commencing toasting of bread..."
maybeDoToast(t) // Prints "Commencing toasting of bread..."
}
Code snippet 05 Example in the Playground
In the first line of main
, we create a new acmeToaster
. In the second line, we pass acmeToaster
to doToast
. doToast
takes an argument of type toaster
. Go can check at compile time if acmeToaster
satisfies toaster
.
In line three, though, we pass acmeToaster
to maybeDoToast
, which takes an empty interface argument. The interface conversion inside maybeDoToast
is a runtime check that may fail. The point here is not that the check may fail, but ask yourself how we will know for sure that acmeToaster
satisfies toaster
if acmeToaster
is only ever used in these sorts of runtime checks?
You could write a unit test that checks it, but there is a quicker and more elegant way which forces a compile-time check that allows us to catch problems even earlier: Simply assign an empty value of your custom type to an unused variable of the interface type.
If that didn’t make sense, then just read on. In Go, the compiler is quick to complain if a variable is unused, but if we assign to a variable named _
(underscore), the compiler won’t complain as that special variable is used specifically for discarding unused values. We can also use it to check if a type satisfies an interface at compile time without incurring unnecessary allocations, like this:
// Verify that acmeToaster satisfies interface toaster.
var _ toaster = acmeToaster{}
Code snippet 06
This is quite common in Go, and it’s an easy way to check interface compatibility.
The empty interface
What do we get if we define an interface with no methods on it? The empty interface:
type empty interface {}
Code snippet 07
The empty interface has no requirements, and we can therefore assign anything to a variable or parameter of this type. This also means that we lose type safety. Since we don’t know what we’ve got, we can’t make any assumptions about it. Sure, if we always know what we’re passing along then we can assume a type and force a type conversion. But programs change and what works today may crash tomorrow. The empty interface says nothing and should be avoided except in rare situations where we really don’t know what we are receiving.
Even when we interact with a third-party API we will have some kind of expectation about the response and unmarshal e.g. from JSON into a custom type with some expected fields. JSON unmarshaling is an entire topic in itself and out of scope for this blog post.
Empty interfaces are difficult to reason about but not entirely unsafe. The trick is to not make any assumptions about them and check them every step of the way. Empty interfaces can be converted into custom types and named interfaces just like regular interfaces can. Example:
type monster struct {
damage int
}
func (m *monster) attack() int {
return m.damage
}
type attacker interface {
attack() int
}
type defender interface {
defend() int
}
func attackOrDefend(attackerDefender interface{}) {
// Inside this function, we don't know what we're getting, but we can check!
if attacker, ok := attackerDefender.(attacker); ok {
fmt.Printf("Attacking with damage %d\n", attacker.attack())
} else if defender, ok := attackerDefender.(defender); ok {
fmt.Printf("Defending with damage %d\n", defender.defend())
}
}
func main() {
var a attacker = &monster{200}
attackOrDefend(a) // Prints "Attacking with damage 200"
attackOrDefend("Hello") // This is allowed, but does nothing.
}
Code snippet 08 Example in the Playground
In the first line of main
, we know that we have an attacker
. However, in line two we call attackOrDefend
which takes an argument of type interface{}
. Since we don’t know what we’re getting inside our function, we have to check.
In line three of main
, we call attackOrDefend
with an invalid argument - except that technically it’s not really invalid since in Go anything satisfies the empty interface. The argument “Hello” is however not of a type that we expect. We can handle that scenario by returning an error or ignoring it, depending on the situation. Here we do the latter.
Empty interfaces are sometimes used when working with multiple different types that have nothing in common, i.e. no shared methods. With generics being introduced in Go 1.18, we should see fewer use cases for the empty interface although it won’t disappear completely.
The nil interface
Interfaces have an underlying type. An interface represents a set of methods that can be called on any given type that satisfies the interface. But in order for the runtime to be able to perform a method call on an interface value, the actual value must be reachable from the interface itself.
For example, in a previous section about interface compatibility, we had an interface toaster
that was satisfied by acmeToaster
:
type toaster interface {
toast()
}
type acmeToaster struct {}
func (a *acmeToaster) toast() { fmt.Println("Commencing toasting of bread...") }
Code snippet 9
When assigning acmeToaster
to toaster
(as when passing it as an argument to a function using a toaster
parameter):
func doToast(t toaster) {
t.toast()
}
Code snippet 10
Then doToast
receives an interface value of type toaster
, but the underlying type is acmeToaster
. It’s the underlying type that always changes, based on which value we provide in place of the interface. Whenever the runtime needs to check if an interface value is compatible with another interface or when it needs to check if an interface can be converted to a specific type, it refers to the underlying type.
This becomes important when we check for nil
values. The reason is that the interface value itself can be nil
, or the underlying type can have a nil
value. In the first case, nil
means that there is no underlying type assigned to the interface. In the second case, it means that there is an underlying type, but that the value of that type is nil
(such as a pointer or an uninitialized slice).
Let’s take a look at a tricky case:
type myError struct{}
func (m *myError) Error() string {
return "failure"
}
func doSomething() (string, *myError) {
return "", nil
}
func main() {
var result string
var err error
if result, err = doSomething(); err != nil {
fmt.Println("We have received an error!") // Prints "We have received an error!"
} else {
fmt.Println(result) // Is never called.
}
}
Code snippet 11 Example in the PlayGround
Even though myError
clearly satisfies the error
interface, and doSomething
clearly returns nil
in its place, we still get "We have received an error!"
. Why? Because we’ve assigned *myError
to the variable err
, which is an interface type. So we’re essentially checking if the interface value is nil
, which it isn’t. err
is assigned an underlying type (*myError
), so it can’t be nil.
However, the underlying type does have value nil
. Here’s the fix:
type myError struct{}
func (m *myError) Error() string {
return "failure"
}
func doSomething() (string, error) {
return "", nil
}
func main() {
var result string
var err error
if result, err = doSomething(); err != nil {
fmt.Println("We have received an error!") // Is never called.
} else {
fmt.Println(result) // Prints "".
}
}
Code snippet 12 Example in the PlayGround
We’ve changed the return type of doSomething
from *myError
to error
. Basically, we’re now returning the interface value nil
instead of a nil *myError
.
An interface is like a box. The box can be empty (nil
), but it can also contain an item. Since that item can be anything, the item could also be empty (nil
).
Then how do you check if the item in the box (the value of the underlying type) is nil
? This is where it gets complicated. You can check via reflection or typed nils of various types. The fact that there isn’t an easy way to cover all your bases, though, is something that has been a source of debate in the Go community for a while. Check this language proposal, for example, which goes all the way back to 2017 and is still being debated.
As a closing remark on the issue, I’ll quote Ian Lance Taylor, a member of the Go core team:
Go makes a clear and necessary distinction between an interface that is nil and an interface that holds a value of some type where that value is nil. -Ian Lance Taylor
More specifically, Go allows us to call methods on nil
pointers if the type is implemented to support it. So perhaps there isn’t much value in checking the underlying type for a nil
value without also knowing what type it is?
Generally, it’s probably best to take measures to ensure that the underlying values of interfaces won’t ever be nil
, or that they work well with nil
pointer receivers.
Interfaces as values
Interfaces are not concrete values. They are more like a contract that dictates that we can use whatever value we’d like, as long as that value satisfies the interface. Thus, it makes no sense to use an interface as a pointer. A function that takes an interface parameter can receive both a pointer or a discrete (non-pointer) value as the underlying type, and calling methods on such an argument works in both cases. Interfaces can’t be pointers.
In the example below, cat
and dog
both satisfy the speaker
interface, although cat
does it via a pointer receiver and dog
does it via a value receiver. Despite this difference in implementation, function doSpeak
is able to call speak
on both arguments, without discrimination.
type speaker interface {
speak() string
}
type cat struct{}
func (c *cat) speak() string { return "Miau!" }
type dog struct{}
func (d dog) speak() string { return "Woof!" }
func doSpeak(s speaker) string {
return s.speak()
}
func main() {
var s speaker
s = &cat{}
fmt.Println(doSpeak(s)) // Prints "Miau!"
s = dog{}
fmt.Println(doSpeak(s)) // Prints "Woof!"
}
Code snippet 13 Example in the Playground
But it matters when we would like to convert an interface argument into a concrete type. If you’ve followed the examples closely until now, you should already have an idea of how to set up a type conversion that checks for both a value and a pointer.
Keeping interfaces small
Interfaces in Go don’t have to be larger than we need them to be in any given situation. Here is a contrived example from a fantasy role playing game:
type player struct {
// unexported fields
}
func (p *player) status() string {
return "Player is eating"
}
func (p *player) sleep() {
// implementation goes here
}
type monster struct {
// unexported fields
}
func (m *monster) status() string {
return "Monster is sleeping"
}
func (m *monster) sleep() {
// implementation goes here
}
type character interface {
status() string
sleep()
}
func getStatus(c character) string {
return c.status()
}
func main() {
cyclops := new(monster)
playerOne := new(player)
fmt.Println(getStatus(cyclops)) // Prints "Monster is sleeping"
fmt.Println(getStatus(playerOne)) // Prints "Player is eating"
}
Code snippet 14 Example in the Playground
Although this works, function getStatus
only accepts parameters that has two methods: status
and sleep
. Forcing it to require both methods is probably not necessary, and the required extra method sleep
would make getStatus
both harder to test (more methods to mock) and harder to call with a valid type. Let’s say that we introduced a new type that we also wanted to get the status of:
type sword struct {}
func (s *sword) status() string {
return "Sword is damaged"
}
Code snippet 15
But sword
is not a character
, and it makes little sense to provide sword
with a sleep
method, which would be a requirement for it to work with the getStatus
function. That’s a problem. It’s therefore preferable to always use the smallest possible interface that still has a meaningful use case. This is a safe approach, because we can always combine interfaces later via composition.
Interface composition
Go supports composing interfaces, i.e. constructing a named interface from one or more other named interfaces. Note that composing interfaces has nothing to do with inheritance. It’s a way of reusing smaller interfaces to create larger ones via composition. This can be particularly useful to avoid unnecessary runtime conversions for types that share common behavior.
To demonstrate, I’ll avoid the mundane ReadWriteCloser
example and instead continue the computer game example from before. Imagine various implementations of weapons that share common traits, or behavior. We define two interfaces that cover melee and ranged weapons: meleeWeapon
and rangedWeapon
. Also, since all weapons can be equipped/unequipped, we can define a separate interface equippable
that describes this behavior and embed it into both meleeWeapon
and rangedWeapon
:
type equippable interface{
equip()
unequip()
}
type meleeWeapon interface {
equippable // Embeds equippable
slash()
}
type rangedWeapon interface {
equippable // Embeds equippable
shoot()
}
Code snippet 16
If we hadn’t embedded equippable
in this way, we would have to perform a runtime interface conversion each and every time we wanted to equip or unequip a weapon, which is tedious and repetitive work. By embedding equippable
, we ensure that meleeWeapon
and rangedWeapon
gain both of its methods.
Note: If you are curious about what would happen if an interface embeds two others that contain the same method signature (i.e. they overlap each other), then the answer is that this would be an error up until Go 1.14 where overlapping interfaces were permitted[9].
Interface conversion
A function or method that accepts an interface argument is not restricted to the methods defined on that interface. Generally, we can do three things with an interface argument:
- Call any method defined as part of its interface
- Convert it into a type
- Convert it into another interface
We’ve already covered the first item. Let’s take a deeper look at the other two.
Conversion into a type
When a value is represented as an interface, Go’s runtime is still aware of the underlying type. This is necessary information when the runtime needs to determine whether or not the interface value can be converted into another interface, or a specific type. Here’s a simple example of type conversion:
type car struct {}
func (c *car) start() { return }
func (c *car) stop() { return }
type vehicle interface {
start()
stop()
}
func isCar(v vehicle) bool {
_, ok := v.(*car)
return ok
}
func main() {
var v = new(car)
fmt.Println(isCar(v)) // Prints "true"
}
Code snippet 17 Example in the Playground
The conversion from interface to type happens on the first line of isCar
. This works because the runtime knows that v
is a *car
. After all, we assigned a *car
(a pointer to a car
) to v
in the first line of main
. For this to work, we’ll need to know if the underlying type is a pointer or not. If you don’t know for sure, you can test for both cases… Just don’t convert a pointer into a discrete value unless you know what you’re doing since you’ll lose the ability to modify the value that it was pointing to.
It’s important to be aware of what the consequences of the type conversion are. Once we’ve converted an interface type (vehicle
) into *car
, we are no longer working with any vehicle
, but a specific vehicle
. In the scope of the conversion, we lose polymorphism.
At this point onwards, the section of code where we’re branching off our logic based on the type of vehicle - i.e. if we have a car
, do A, if we have a bus
, do B etc. - becomes volatile. It works today, but if we introduce a new vehicle
(say, a truck
) into the code base tomorrow, we’ll need to manually find and update these volatile sections and make sure that we handle truck
accurately in each case, before the code returns to a reliable operational state.
Conversion into another interface
Type assertions can also convert an interface into another, which is a powerful feature. Go’s runtime will check if the underlying type satisfies the interface we wish to convert to, and will let us know if the conversion was possible. Example:
type car struct {}
func (c *car) start() { return }
func (c *car) stop() { return }
func (c *car) recycle() { fmt.Println("Recycling...") }
type vehicle interface {
start()
stop()
}
type recyclable interface {
recycle()
}
func main() {
var v vehicle = new(car)
if r, ok := v.(recyclable); ok {
// In the scope of this code block, we've now asserted that v satisfies
// the recyclable interface. Therefore, we can call recycle() on it!
r.recycle() // Prints "Recycling..."
}
// v.recycle() // <- This is not possible! In the outer code block, we know
// that v is a vehicle, but the assertion applies only to the code block
// containing r. Here, v is still just a vehicle.
}
Code snippet 18 Example in the Playground
This is already pretty neat, but we can also check dynamically for the availability of certain methods. This should not be surprising, and there is nothing complex about it as we achieve this via anonymous interfaces. Here is a variation of the example from above:
type car struct {}
func (c *car) start() { return }
func (c *car) stop() { return }
func (c *car) recycle() { return }
func (c *car) convertToBatMobile() { fmt.Println("I am now a BatMobile!") }
type vehicle interface {
start()
stop()
}
type recyclable interface {
recycle()
}
func main() {
var v vehicle = new(car)
if b, ok := v.(interface { convertToBatMobile() }); ok {
b.convertToBatMobile() // Prints "I am now a BatMobile!"
}
// v is now a BatMobile! It still satisfies the vehicle interface.
}
Code snippet 19 Example in the Playground
While the exciting part - the actual conversion of a regular car
into a BatMobile
- has been left out, this example should demonstrate the power and flexibility of Go’s structurally typed interfaces.
Finally, type switches can be used for interfaces, even anonymous interfaces! Example:
type movie string
func (m movie) Movie() string { return string(m) }
type movieStar string
func (m movieStar) Star() string { return string(m) }
func movieOrStar(m interface{}) string {
switch v := m.(type) {
case interface{ Star() string }:
return v.Star()
case interface{ Movie() string }:
return v.Movie()
default:
return ""
}
}
func main() {
var value interface{}
value = movieStar("Sylvester Stallone")
fmt.Println(movieOrStar(value)) // Prints "Sylvester Stallone
value = movie("Rambo")
fmt.Println(movieOrStar(value)) // Prints "Rambo"
}
Code snippet 20 Example in the Playground
Inside the scope of each case
statement, we’ve asserted that v
satifies the interface that we’re checking against, which makes the method calls valid.
Interface embedding
A struct may embed an interface, which is similar to type embedding. But instead of a concrete type, we can embed any type that satisfies the interface. Here is an example from our role playing game:
type persona interface {
strength() int
intelligence() int
stamina() int
}
type dwarf struct {}
func (d *dwarf) strength() int { return 80 }
func (d *dwarf) intelligence() int { return 55 }
func (d *dwarf) stamina() int { return 90 }
type wizard struct {}
func (w *wizard) strength() int { return 50 }
func (w *wizard) intelligence() int { return 90 }
func (w *wizard) stamina() int { return 60 }
type player struct {
persona
}
func main() {
p := player{&wizard{}}
fmt.Println(p.intelligence()) // Prints 90.
}
Code snippet 21 Example in the Playground
In this example, the persona
interface is embedded in player
. Both dwarf
and wizard
satisfy the persona
interface. Consequently, both of these types can be embedded in player
(and interchanged). This form of embedding is particularly useful for composing larger structs from independent and interchangeable parts.
What happens if we create a player
but omit embedding a persona
? As it turns out, this still works, but the method call is treated as having a nil
pointer receiver, which panics when we try to call one of the unimplemented methods:
func main() {
p := player{} // Equivalent to p := player{nil}
fmt.Println(p.intelligence()) // panic: runtime error: invalid memory
// address or nil pointer dereference
}
Code snippet 22 Example in the Playground_
Interestingly, it turns out that a type can embed an interface and thus satisfy an interface even if only a subset of those methods are actually implemented. This is useful for mocks where we only test specific functionality. For example, knowing that function summarize
only uses the add
method of its argument calc
of interface type calculator
, we could test it with a mock of calculator
that only implements the add
method:
type calculator interface {
add(a, b int) int
sub(a, b int) int
div(a, b int) int
mul(a, b int) int
}
type mockCalculator struct {
calculator // Embeds the calculator interface
}
func (m *mockCalculator) add(a, b int) int { return a + b }
func summarize(calc calculator, values ...int) int {
var result int
for _, val := range values {
result = calc.add(result, val)
}
return result
}
func main() {
mock := new(mockCalculator)
result := summarize(mock, 10, 12, 9, 11)
fmt.Println(result) // Prints 42
}
Code snippet 23 Example in the Playground
Calling any of the other methods (sub
, div
, mul
) would panic, however, so this technique should probably only be used for unit tests, not production code. For writing mocks, though, it can be quite useful.
Another interesting use case comes from Uber’s logging package Zap, where interfaces are used to extend existing functionality by wrapping a Core
struct in a struct that embeds the Core
interface and then implements the necessary methods to provide new functionality for adding hooks (function callbacks) to the core data type. As we’ll see, it’s not necessary to wrap all the interface methods. Those methods that aren’t redefined by the wrapper type simply expose the method of the embedded Core type.
// In zap/zapcore/core.go
type Core interface {
LevelEnabler
With([]Field) Core
Check(Entry, *CheckedEntry) *CheckedEntry
Write(Entry, []Field) error
Sync() error
}
// In zap/zapcore/hook.go
type hooked struct {
Core // Embeds the Core interface
funcs []func(Entry) error
}
// RegisterHooks wraps a Core and runs a collection of user-defined callback
// hooks each time a message is logged. Execution of the callbacks is blocking.
//
// This offers users an easy way to register simple callbacks (e.g., metrics
// collection) without implementing the full Core interface.
func RegisterHooks(core Core, hooks ...func(Entry) error) Core {
funcs := append([]func(Entry) error{}, hooks...)
return &hooked{
Core: core,
funcs: funcs,
}
}
func (h *hooked) Check(ent Entry, ce *CheckedEntry) *CheckedEntry { ... }
func (h *hooked) With(fields []Field) Core { ... }
func (h *hooked) Write(ent Entry, _ []Field) error { ... }
Code snippet 24
References: [10] [11]. Comments and implementation details from the original source code has been left out for brevity, but feel free to visit the public repository on GitHub if you are curious about how the methods are implemented.
RegisterHooks
takes an argument of type Core
, which is the interface defined in core.go
. RegisterHooks
then creates a slice of functions (using variadic function arguments[12]) and stores them in hooked
, a struct that embeds the Core
interface and redefines some of the Core
methods in order to make use of the hook functions (not shown).
Note how the Sync
error method from the Core
interface is never redefined. This is perfectly valid. If we call it, it’s the Sync
method of the embedded type that gets called due to method promotion. This is pretty clever and quite powerful. It also shows that a single type doesn’t have to satisfy an interface on its own; we can compose types that won’t satisfy the interface individually, but do so once they are composed together.
See this post from Ardan Labs for more details on how this works.
Partially exported interfaces
An interface that is unexported (first letter is lowercase) can only be referenced from its own package. Exported interfaces can be referenced from other packages, but what about exported interfaces that contain both exported and unexported methods?
Take interface School
as an example:
// School is defined in package school
type School interface {
Name() string
Location() string
students() []string
}
// University is defined in package schools
type University struct {}
func (u University) Name() string { return "Copenhagen University" }
func (u University) Location() string { return "Copenhagen City Center" }
func (u University) students() []string { return []string{"George", "Ben", "Louise", "Calvin"} }
var _ school.School = University{} // Error!
Code snippet 25
Type University
aims to satisfy interface School
, but fails to do so. It’s not possible to satisfy School
because it contains an unexported method, students
. The error given in the last line of the example states:
cannot use University literal (type University) as type school.School in assignment:
University does not implement school.School (missing school.students method)
have students() []string
want school.students() []string
Code snippet 26
Basically, this means that we can implement interface School
in package school
, but not in package schools
due to the unexported method.
What is the use case for partially exported interfaces, then? Let’s try embedding interface School
:
// University is defined in package schools
type University struct {
school.School
}
func (u University) Name() string { return "Copenhagen Highschool" }
func (u University) Location() string { return "Copenhagen City Center" }
// func (u University) students() []string { // Can't redefine.
// return []string{"George", "Ben", "Louise", "Calvin"}
// }
var _ school.School = University{} // Works!
Code snippet 27
This works as long as we don’t attempt to redefine method students
.
So by defining an interface that contains unexported methods we can guarantee that any type which wants to satisfy that interface doesn’t redefine those methods. In essence, we force implementations to embed types exported by our own package and any call to such a method will always be handled internally, by our own package’s implementation.
If you decide to use this trick, then it’s worth emphasizing that implementations can’t call unexported methods either. Only your own code can do that. Therefore, partially exported interfaces are mostly a mechanism for ensuring that your own package provides some useful methods for internal use. External users of your package can reference the interface but can’t embed it in their own types.
Applied interfaces
When applying interfaces in the code base, it can prove tricky to get method parameters right. It did to me - at least until I realized that I need to think about them a bit differently in Go.
We kind of have to turn the concept on its head in order to handle the part of the interface methods that varies between implementations.
For example, consider an interface EntityLoader
that loads various entities from the database by ID. The interface is used as a way of abstracting differences between repository implementations (one per entity).
A first draft might look like this:
type EntityLoader interface {
Load(id uint32) (interface{}, error)
}
Code snippet 28
This doesn’t quite work. We lose type safety, and we would have to manually convert the first return value into the correct entity, whether it’s User
, Address
, PurchaseOrder
or other. To get accurate types on everything, we might do this instead:
type EntityLoader interface {
LoadUser(id uint32) (User, error)
LoadAddress(id uint32) (Address, error)
LoadPurchaseOrder(id uint32) (PurchaseOrder, error)
}
Code snippet 29
This defeats the purpose of interfaces altogether. Worse, each repository would need to implement all three methods and return an error for the two methods that aren’t implemented.
We need to realize that the method receiver is not part of the interface contract and is therefore a good candidate for handling the varying part. To achieve this, the repository method can accept a named interface value that can be implemented by any type.
Let’s try it. We’ll call the new interface Hydrater
because its argument is a database row and it must be implemented by populating/hydrating the individual fields with their respective columns from that row:
type EntityLoader interface {
Load(id uint32, h Hydrater) error
}
type Hydrater interface {
Hydrate(database.Row) error
}
Code snippet 30
And take a look at the implementation for entity User
:
type UserRepository struct { }
func (u *UserRepository) Load(id uint32, h Hydrater) error {
row := ... // Run a SELECT by id and retrieve the result in a database.Row.
return h.Hydrate(row) // h must be a pointer.
}
type User struct {
ID uint32
Name string
// Other fields...
}
func (u *User) Hydrate(row database.Row) error {
return row.Scan(u)
}
Code snippet 31
That’s it, we can use it like so:
var u User
repo := new(UserRepository)
err := repo.Load(42, &u)
if err != nil { ... }
Code snippet 32
Thus avoiding this ugliness:
maybeUser, err := repo.LoadUser(42) // Returns interface{}, error
if err != nil { ... }
user, ok := maybeUser.(*User) // Ugly conversion
if !ok { ... }
Code snippet 33 Example in the Playground using a custom Row and Scan method
Notice how we’ve inverted the relationship between UserRepository
and User
. Instead of relying on the repository to populate/hydrate User
, we empower User
to hydrate itself based on some common parameter.
In this case, I’ve used database.Row
from Go’s standard library (which we can call Scan
on), but this approach should work well in general, and you’ll find that it’s used heavily throughout Go’s standard library. Some examples are json.Unmarshal
in the encoding
package and Row.Scan
in the database
package. Although these both take their arguments as empty interfaces, we declare the variable ourselves and pass it in, which removes the need for us to do an interface conversion after the method call.
It may take a little trial and error to get this concept down, but it works quite well in practice.
Conclusion
Interfaces in Go are implicit and structurally typed. As we’ve seen, they are also extremely versatile. The empty interface matches anything and non-empty interfaces can be converted into both specific types and named interfaces, but also anonymous interfaces and ad-hoc interfaces with a different structure.
Interface values have an underlying type. They can be both pointers and discrete values. They can be composed and used without fully implementing their methods. They can also be satisfied by several different types that each satisfy only part of the interface but when composed satisfies it fully.
And to fully grasp their usefulness, it may be useful to realize that Go applies interfaces a bit differently from what we’re used to from other languages.
I hope you had fun reading this and maybe even learned something along the way.
Credits
Thanks to Eik Madsen for proof-reading.
References
- [1]. https://golangbyexample.com/difference-between-method-function-go/
- [2]. https://stackoverflow.com/questions/155609/whats-the-difference-between-a-method-and-a-function
- [3]. https://stackoverflow.com/questions/1788923/parameter-vs-argument
- [4]. https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
- [5]. https://en.wikipedia.org/wiki/Structural_type_system
- [6]. https://en.wikipedia.org/wiki/Go_(programming_language)#Interface_system
- [7]. https://www.freecodecamp.org/news/java-interfaces-explained-with-examples/
- [8]. https://www.javatpoint.com/decorator-pattern
- [9]. https://go.googlesource.com/proposal/+/master/design/6977-overlapping-interfaces.md
- [10]. https://github.com/uber-go/zap/blob/master/zapcore/core.go#L25
- [11]. https://github.com/uber-go/zap/blob/master/zapcore/hook.go
- [12]. https://golangdocs.com/variadic-functions-in-golang
Other resources
- https://golangbyexample.com/interface-in-golang/#Inner_Working_of_Interface
- https://www.tapirgames.com/blog/golang-interface-implementation
- https://golangbot.com/interfaces-part-1/
- https://golangbot.com/interfaces-part-2/
- http://www.hydrogen18.com/blog/golang-embedding.html