Go Experience report: Go’s type system

When Go’s type system impedes library interoperability

A gopher tries to drink coffee

What I wanted to do

Today’s pkg/errors

Package errors is a popular go error wrapping package. The StackTrace type represents the stack trace with an []Frame. Errors wrapped by package errors implicitly implement the stackTracer interface, which allows users to extract stack information.

type StackTrace []Frame

type Frame uintptr type stackTracer interface {

StackTrace() errors.StackTrace

}

Simultaneously to this, there exist libraries that would like to extract stack frame information from errors, if it’s present. One example is the rollbar logging library, which allows logging errors with the stack trace that created them. The only way for a rollbar logging library to support pkg/errors is to import the library directly and type assert for a StackTrace function.

import “github.com/pkg/errors” type stackTracer interface {

StackTrace() errors.StackTrace

} func logError(err error) {

if s, ok := err.(stackTracer); ok {

// …

}

}

To resolve some issues with pkg/errors I decided to fork it. Because of stackTracer’s implementation, my forked StackTrace function will not resolve when type asserted. Not only is my fork unsupported by the rollbar library, but I’m also forced to vendor and manage dependencies to pkg/errors.

What I actually did

Forking pkg/errors

To support the fork, the rollbar library is now required to also check for my stackTracer implementation.

import errors1 “github.com/pkg/errors”

import errors2 “github.com/cep21/errors” type stackTracer1 interface {

StackTrace() errors1.StackTrace

} type stackTracer2 interface {

StackTrace() errors2.StackTrace

} func logError(err error) {

if s, ok := err.(stackTracer1); ok {

// …

}

if s, ok := err.(stackTracer2); ok {

// …

}

}

It’s easy to see how silly this code could become if scaled to multiple packages. In order to support both implementations, I just forked the rollbar logging library as well and modified the import to my fork, rather than pkg/errors.

Alternative pkg/errors

An alternative implementation would be for pkg/errors to return []uintptr directly and not use a custom type. In this implementation, the rollbar library now doesn’t need to import pkg/errors at all and supports both versions natively.

type stackTracer interface {

StackTrace() []uintptr

} func logError(err error) {

if s, ok := err.(stackTracer); ok {

// …

}

}

This puts no burden on the user of the library to manage dependencies to pkg/errors. The downside is that pkg/errors has now lost type information, or custom functions, on stack trace items.

Why that wasn’t great

It’s easy to see how this becomes unscalable in the open source community as multiple people begin to fork a library. It is also impractical in a large company with multiple codebases where teams may not agree on 100% of the shared libraries, or where teams may want to experiment with alternative shared libraries without dragging the company with them all at once. Ideally, the rollbar library would be able to define a package independent stackTrace implementation it expects. This would now support all forks at once.

type StackTrace interface {

Stack() []uintptr

} type stackTracer interface {

StackTrace() StackTrace

} func logError(err error) {

if s, ok := err.(stackTracer); ok {

// …

}

}

If Go’s type system was flexible enough to allow rollbar to indicate it only required StackTrace().Stack() behavior, it could support multiple forks of the errors package without requiring a dependency.