Marker Interfaces in Go

4 min read Original article ↗

Andrei Boar

This article is about Marker Interfaces, or how to go from an idea to a special idea:

Press enter or click to view image in full size

Abstractions are just ideas, and you can compose them

A bit about errors

If someone asked you what an error is in Go, you might reply: An error is a value that satisfies the error interface. And you would be correct. All errors in Go must implement the error interface:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}

You can learn more about errors in my article: A concise guide to error handling in Go. But in short, as long as a type has a method withError() string signature, then that type is an error:

type ValidationError struct {
Field string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("%s is invalid", e.Field)
}

Runtime errors

You deal with runtime errors all the time. For example, the bellow program:

package main

func main() {
var s []string
s[3] = "a"
}

Will panic with:

panic: runtime error: index out of range [3] with length 0

because s is a nil slice, and index 3 doesn’t exist. A similar error would occur if we deal with a divide by zero, nil pointer dereference, invalid type assertions, etc.

These errors are managed by the Go runtime, resulting in a panic. It's Go, making sure it lets you know when your program reached an incorrect state.

Runtime errors are interesting because they are concrete types that implement a special kind of interface, the runtime.Error interface:

// The Error interface identifies a run time error.
type Error interface {
error

// RuntimeError is a no-op function but
// serves to distinguish types that are run time
// errors from ordinary errors: a type is a
// run time error if it has a RuntimeError method.
RuntimeError()
}

Notice here that runtime.Error embeds error, so any type implementing runtime.Error is still an error.
Type embedding is cool because it lets you compose abstractions by building on top of others. io.ReadWriter interface is another good example of how you can use interfaces as bricks to compose other interfaces:

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}

Another thing to notice is that RuntimeError() is a no-op function. That’s a function that does nothing. So why is it there? We find out by reading further: To distinguish types that are run time error from ordinary errors.

With this new interface, we can now distinguish between a runtime.Error and a regular error using errors.As and handle them differently:

package main

import (
"errors"
"fmt"
"runtime"
)

func main() {
fmt.Println(safediv(4, 0))
}

func safediv(a, b int) int {
defer func() {
if err := recover(); err != nil {
if err, ok := err.(error); ok {
if errors.As(err, new(runtime.Error)) {
fmt.Println("This is a runtime error:", err)
} else {
fmt.Println("This is a regular error:", err)
}
}
}
}()

return a / b
}

If you go into the runtime package and check some implementations of this interface, you see they are indeed doing nothing.

Press enter or click to view image in full size

Implementations of runtime.Error interface

So, the purpose of RuntimeError() is not to do something but to be a marker for all runtime errors. It’s grouping all runtime errors under the same flag.
That’s quite clever because the alternative would have been to have a RuntimeError struct with a type field, but then you would have lots of logic based on its type.

The name goes against the advice of writing behavior-driven interfaces, because you could argue that RuntimeError is a thing, not a behavior. However, someone else could say that being “triggerable during runtime” might qualify as a behavior. Or maybe this is just the exception that proves the rule. In the end, error itself is a noun as well.

I haven’t seen this pattern used so far, but it seems there are other packages like the Go ast parser that is using a similar approach to group types:

Press enter or click to view image in full size

http://www.craig-wood.com/nick

There are probably others, but it is good to know that you can group different types using these marker interfaces. Since interfaces in Go are implicitly satisfied, you can group various types spread in different packages without worrying about import cycle issues.

That was all for today! I’m curious what you think about these marker interfaces and if you used them in your projects.

As always, feel free to reach out to me on LinkedIn if you have any questions, and if you enjoyed this article, give it a 👏 or share. Thank you, and Happy New Year!