Deep vs Shallow Go interfaces

I recently readA Philosophy of Software Design by John Ousterhout (of Tcl/Tk, Raft, Sprite fame).

One of the core concepts explored in this book is the distinction between“deep” vs “shallow” modules (in the author’s terms a module is any kind ofabstraction, separated into the user-facing interface and the underlyingimplementation).

The author argues that “the best modules are those that provide powerfulfunctionality yet have simple interfaces”. The argument isnot about absolutesize, but rather the ratio of utility afforded by the abstraction compared tothe size of the abstraction itself, in other words, a cost/benefit tradeoff.

In our case, the main mechanism for composable abstractions in Go is theinterface type, so let’s examine the concept through this lens.

(Deep versus shallow abstractions)

A deep interface

To me, maybe the best example of a deep interface isio.Reader.

// Reader is the interface that wraps the basic Read method.//// Read reads up to len(p) bytes into p. It returns the number of bytes// read (0 <= n <= len(p)) and any error encountered. Even if Read// returns n < len(p), it may use all of p as scratch space during the call.// If some data is available but not len(p) bytes, Read conventionally// returns what is available instead of waiting for more.// ...// Implementations of Read are discouraged from returning a// zero byte count with a nil error, except when len(p) == 0.// Callers should treat a return of 0 and nil as indicating that// nothing happened; in particular it does not indicate EOF.//// Implementations must not retain p.typeReaderinterface{Read(p[]byte)(nint,errerror)}

It couldn’t possibly getany smaller than that, right? It’s simple enoughthat you won’t ever need to look it up again. Searching the Go standard library, one will findnumerous implementationsincluding reading from files, from network connections, compressors, ciphersand more.

This abstraction is both easy to understand and use; the docstring tells youeverything you, as a user, need to know. The underlying implementation can bebuffered, may allow reading from streams or remote locations like an S3bucket. But crucially, consumers of this API don’t need to worry abouthowreading happens — implementation can be deep and non-trivial,but a user doesn’t have to care. Furthermore, it allows for very littleambiguity when reasoning about what the code does.

All these properties areespecially important for core functionality that’sused frequently.

A shallow interface

On the other hand, an example of a shallow interface I’ve used recently is fromtheredis-go client.

I’ve trimmed it down for the purposes of this post, but you can see itherein its entirety. It contains 45 methodsand uses 19 other interfaces asextensions for a total of ~200 methods.

typeCmdableinterface{Pipeline()PipelinerPipelined(ctxcontext.Context,fnfunc(Pipeliner)error)([]Cmder,error)TxPipelined(ctxcontext.Context,fnfunc(Pipeliner)error)([]Cmder,error)TxPipeline()PipelinerCommand(ctxcontext.Context)*CommandsInfoCmdCommandList(ctxcontext.Context,filter*FilterBy)*StringSliceCmdCommandGetKeys(ctxcontext.Context,commands...interface{})*StringSliceCmdCommandGetKeysAndFlags(ctxcontext.Context,commands...interface{})*KeyFlagsCmdInfo(ctxcontext.Context,section...string)*StringCmdLastSave(ctxcontext.Context)*IntCmdSave(ctxcontext.Context)*StatusCmdShutdown(ctxcontext.Context)*StatusCmdShutdownSave(ctxcontext.Context)*StatusCmdShutdownNoSave(ctxcontext.Context)*StatusCmd......StringCmdableStreamCmdableTimeseriesCmdableJSONCmdable}

While the functionality provided by Redis is much larger than just ‘reading’,each of these methods has a much simpler implementation; they do exactly onething, and they’re small enough you could possibly replicate them just by theirname and arguments. The ratio of the functionality provided to the size ofthe abstraction is very different than before.

func(ccmdable)CommandGetKeys(ctxcontext.Context,commands...interface{})*StringSliceCmd{args:=make([]interface{},2+len(commands))args[0]="command"args[1]="getkeys"copy(args[2:],commands)cmd:=NewStringSliceCmd(ctx,args...)_=c(ctx,cmd)returncmd}

This also shifts the responsibility of doing the right thing towards theuser, as they have to understand the nuances between individual methods.In a code review, this makes it harder to reason about what happens at aglance.

func(ccmdable)Get(ctxcontext.Context,keystring)*StringCmdfunc(ccmdable)MGet(ctxcontext.Context,keys...string)*SliceCmdfunc(ccmdable)Set(ctxcontext.Context,keystring,valueinterface{},expirationtime.Duration)*StatusCmdfunc(ccmdable)SetEX(ctxcontext.Context,keystring,valueinterface{},expirationtime.Duration)*StatusCmdfunc(ccmdable)SetNX(ctxcontext.Context,keystring,valueinterface{},expirationtime.Duration)*BoolCmd

Comparison

So, is this another post criticizing other dev practices? Not really. Asalways, things exist on a spectrum and these previous examples show the twoextremes.

As a developer, it can often feel more natural to write shallower interfaces.Similar ‘shallow’ examples (that are not strictly interfaces) are theaws-sdk-go’s session Options orViper’spublic API. Why?

In contrast,io.Reader offers additional advantages:

type ReadCloser interface {ReaderCloser}

Thego-kit/log.Loggerand thehttp.Handler interfaces areprime showcases of these concepts in the real world.

This not afair comparison, as you will rarely write such core functionalityas the Reader from scratch, and well, Cmdable is not an abstraction but rathera driver covering the entirety of Redis operations.

But still, does that client APIneed five different methods for saving andshutting down? Does a user of the client need to deal with both runningcommands and getting meta-information around the DB connection and runtimemetrics at the same time? Is each of the datatypes different enough to havetheir own interface? And do I as a reviewer, need to know beforehand whetherthe code needs toPing,Echo orHello?

So, next time you design or review an abstraction, take a closer look.How “deep” is your API?In what ways could you mold it into something simpler that hides complexityfrom the user and reduces cognitive load?

Outro

And that’s all for today! If you have any comments, remarks or ideas, feel freeto reach out to me onBluesky.

What are your favorite interfaces? Any specific one that you think touches thePlatonic ideal? Any that disgusts you beyond imagination and makes you wannaquit, move to the countryside and grow tomatoes? Let me know!

Until next time, bye!


Written on March 21, 2025