Function types and single-method interfaces in Go
Redowan Delowar
December 22, 2024
People love single-method interfaces (SMIs) in Go. They're simple to implement and easy to
reason about. The standard library is packed with SMIs like io.Reader, io.Writer,
io.Closer, io.Seeker, and more.
One cool thing about SMIs is that you don't always need to create a full-blown struct with a
method to satisfy the interface. You can define a function type, attach the interface method
to it, and use it right away. This approach works well when there's no state to maintain, so
the extra struct becomes unnecessary. However, I find the syntax for this a bit abstruse.
So, I'm jotting down a few examples here to reference later.
Using a struct to implement an interface
This is how interfaces are typically implemented. Here, we'll satisfy the io.Writer
interface to create a writer that logs some stats before saving data to an in-memory buffer.
The standard library defines io.Writer like this:
We can implement io.Writer by defining a struct type, LoggingWriter, and attaching a
Write method with the required signature:
Here's how to use it:
Running this will log the stats before writing to the buffer:
Using a function type instead
Instead of defining the LoggingWriter struct, you can use a function type to satisfy
io.Writer. This works well for SMIs but doesn't make sense for interfaces with multiple
methods. In those cases, we need to resort to the methods-on-struct approach.
Here's how it looks:
You can use WriteFunc like this:
WriteFunc satisfies io.Writer by defining a Write method with the expected signature.
You can adapt any function to match the signature (data []byte) (int, error) using
WriteFunc, so there's no need for a struct when no state is involved.
In main, an anonymous function logs the number of bytes and writes the data to a buffer.
Wrapping this function with WriteFunc lets it implement the io.Writer interface. The
.Write method is called on the wrapped function to log stats and write data to the buffer.
Finally, the buffer's content is printed to verify everything worked.
> [!NOTE]
>
> For a simple example like this, using a function type to implement an interface might feel
> like overkill. But there are cases where it simplifies things. The next sections explore
> real-world examples where function types make interface implementation a bit more
> ergonomic.
Mocking interfaces for testing
Function types let you mock interfaces without creating dedicated structs. Here's how it
works with an Authenticator interface:
The AuthFunc type implements the Authenticate method by calling itself with the provided
arguments. This lets you create mock implementations inline in your tests.
Here's how to use it in a test:
And in application code:
Building HTTP middlewares
The standard library's http.HandlerFunc demonstrates function types in action. Here's how
to build a logging middleware that times requests:
http.HandlerFunc converts functions into HTTP handlers. The logging middleware wraps the
next handler and adds timing and logging.
We use it as follows:
Adapting function types for database queries
Function types can abstract database query execution for testing or supporting different
database implementations:
QueryFunc turns regular functions into QueryExecutor implementations, making it easy to
swap implementations or create mocks.
This is how to use it:
Implementing retry logic
Function types can encapsulate retry behavior without creating configuration structs:
RetryFunc converts functions with the matching signature into a Retryer, letting you
swap retry strategies or create test versions.
Here's how to use it:
Go lets us define methods on custom types, including function types. While this can be handy
for adapting a function type to an interface, it can make the code hard to read at times. So
I don't always reach for it. It's perfectly fine to define an empty struct with a single
method if that makes the code more readable. Nonetheless, it's a neat trick to keep in your
repertoire.
Discussion in the ATmosphere