{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/func-types-and-smis/",
"description": "Implement single-method interfaces with function types instead of structs. Master http.HandlerFunc patterns for middlewares, mocks, and adapters.",
"path": "/go/func-types-and-smis/",
"publishedAt": "2024-12-22T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go",
"API",
"Testing"
],
"textContent": "People love single-method interfaces (SMIs) in Go. They're simple to implement and easy to\nreason about. The standard library is packed with SMIs like io.Reader, io.Writer,\nio.Closer, io.Seeker, and more.\n\nOne cool thing about SMIs is that you don't always need to create a full-blown struct with a\nmethod to satisfy the interface. You can define a function type, attach the interface method\nto it, and use it right away. This approach works well when there's no state to maintain, so\nthe extra struct becomes unnecessary. However, I find the syntax for this a bit abstruse.\nSo, I'm jotting down a few examples here to reference later.\n\nUsing a struct to implement an interface\n\nThis is how interfaces are typically implemented. Here, we'll satisfy the io.Writer\ninterface to create a writer that logs some stats before saving data to an in-memory buffer.\n\nThe standard library defines io.Writer like this:\n\nWe can implement io.Writer by defining a struct type, LoggingWriter, and attaching a\nWrite method with the required signature:\n\nHere's how to use it:\n\nRunning this will log the stats before writing to the buffer:\n\nUsing a function type instead\n\nInstead of defining the LoggingWriter struct, you can use a function type to satisfy\nio.Writer. This works well for SMIs but doesn't make sense for interfaces with multiple\nmethods. In those cases, we need to resort to the methods-on-struct approach.\n\nHere's how it looks:\n\nYou can use WriteFunc like this:\n\nWriteFunc satisfies io.Writer by defining a Write method with the expected signature.\nYou can adapt any function to match the signature (data []byte) (int, error) using\nWriteFunc, so there's no need for a struct when no state is involved.\n\nIn main, an anonymous function logs the number of bytes and writes the data to a buffer.\nWrapping this function with WriteFunc lets it implement the io.Writer interface. The\n.Write method is called on the wrapped function to log stats and write data to the buffer.\nFinally, the buffer's content is printed to verify everything worked.\n\n> [!NOTE]\n>\n> For a simple example like this, using a function type to implement an interface might feel\n> like overkill. But there are cases where it simplifies things. The next sections explore\n> real-world examples where function types make interface implementation a bit more\n> ergonomic.\n\nMocking interfaces for testing\n\nFunction types let you mock interfaces without creating dedicated structs. Here's how it\nworks with an Authenticator interface:\n\nThe AuthFunc type implements the Authenticate method by calling itself with the provided\narguments. This lets you create mock implementations inline in your tests.\n\nHere's how to use it in a test:\n\nAnd in application code:\n\nBuilding HTTP middlewares\n\nThe standard library's http.HandlerFunc demonstrates function types in action. Here's how\nto build a logging middleware that times requests:\n\nhttp.HandlerFunc converts functions into HTTP handlers. The logging middleware wraps the\nnext handler and adds timing and logging.\n\nWe use it as follows:\n\nAdapting function types for database queries\n\nFunction types can abstract database query execution for testing or supporting different\ndatabase implementations:\n\nQueryFunc turns regular functions into QueryExecutor implementations, making it easy to\nswap implementations or create mocks.\n\nThis is how to use it:\n\nImplementing retry logic\n\nFunction types can encapsulate retry behavior without creating configuration structs:\n\nRetryFunc converts functions with the matching signature into a Retryer, letting you\nswap retry strategies or create test versions.\n\nHere's how to use it:\n\nGo lets us define methods on custom types, including function types. While this can be handy\nfor adapting a function type to an interface, it can make the code hard to read at times. So\nI don't always reach for it. It's perfectly fine to define an empty struct with a single\nmethod if that makes the code more readable. Nonetheless, it's a neat trick to keep in your\nrepertoire.",
"title": "Function types and single-method interfaces in Go"
}