Language Design Lessons: Automatic Interfaces
Published: 2023-04-24 19:30:04

After diving a little too heavily into object-orientation, language design has shifted to interfaces as a way to support dynamic dispatch without tying types into a single inheritance tree.

A common example is a "reader" interface.  Functionality which needs to read data (such as a decompression library) only requires that its input implement the reader interface.  Multiple types can implement this interface -- files on disk, network sockets, in-memory buffers, and so on.

Lots of simple interfaces work this way: their behavior is defined by one method, and often the interface is named after this method, with an "er" suffix: "Reader", "Listener", "Watcher", etc.

In most languages, a type must declare that it implements an interface.  In Rust, this might look like this,

```rust
impl Default for MyType {
    fn default() -> Self {
        Self { foos: 0 }
    }
}
```

where `Default` is the interface being implemented (Rust uses the term "trait" instead of "interface", with a slightly different meaning).

Go takes a different approach: an interface defines a set of method signatures, and _any_ type with matching methods implicitly implements the interface.  For example, the io.Writer interface is defined as

```go
type Writer interface {
    Write(p []byte) (n int, err error)
}
```

Any type can implement Writer just by implementing a matching Write method.

In an ecosystem of code written by different people, this has the advantage that a type you don't control can implement an interface you do control.  This can be especially useful for mocking third-party types when testing.

However, this design choice assumes that the nature of an interface is entirely defined by its method signatures. For example, a module framework might define a "module" interface:

```go
type Module interface {
    // Start starts the module's execution, and returns
    // immediately.
    Start()

    // Stop signals the module to stop, blocking until it
    // does so.
    Stop()
} 

func AddModule(module Module) { .. }
```

Here, the comments capture some information about the interface that is not represented in the method declarations, and thus not checked by the compiler.

In fact, lots of types have `Start` and `Stop` methods matching this interface. Are these modules? The compiler thinks so! Perhaps types from a package of service utilities have these methods, but with an additional `Wait` method that waits for the module to actually stop. Mixing such utilities would silently terminate those modules uncleanly.

If you're familiar with duck-typed languages like Python or JS, this probably isn't surprising: it's up to you as the human at the keyboard to ensure that your types have the necessary behaviors, or things will fail at runtime.

But Go is strongly typed, and one of strong typing's benefits is surfacing errors like this at compile time. In fact, it's quite common to use `interface {}` in Go code, and _everything_ satisfies this interface -- hardly strong typing!

The introduction of generics in 2022 has helped this situation before. Before generics, a general-purpose LRU library might have used `interface{}`:

```go
func (lru *Lru) get(key string) interface{} { .. }
```

Here callers would apply a type assertion to downcast that `interface{}` to the expected type. But nothing prevents objects of different types being stored in the same cache, so the type must be checked for every object - a real runtime cost for a performance-sensitive library. With generics, that can be written as

```go
func (lru *Lru[T]) get(key string) *T { .. }
````

ensuring, at compile time, that only T's are stored in the cache.

This may be a case where allowing _too much_ flexibility in a language allows undetected bugs and prevents useful optimizations. Conversely, a carefully constrained language can eliminate entire classes of incorrect code, while also allowing optimizations based on broad, language-enforced invariants.

[cover photo: [@chrisdag](https://www.flickr.com/photos/chrisdag/5488664355/)]

P.S. This will be the last installment in this very short series. I've moved on from the work that had me writing Go every day. Look forward to posts from a Chromium developer!