Function Types Implementing Interfaces
How Go function types can satisfy interfaces by defining methods on them - turning plain functions into full interface implementations without wrapper structs
A Go interface expects a type with methods. You write a struct, attach the required methods, and pass it in. But sometimes the behavior you need is already captured in a single function. Creating an entire struct just to wrap that function feels like ceremony the language should spare you.
Go spares you. The language lets you define methods on any named type you create — including function types. This means a function type can carry a method that calls itself, and that one addition turns it into a legitimate interface implementation. The http.HandlerFunc pattern in the standard library is the most famous example, but the technique applies anywhere a single-method interface meets a function that already does the right thing.
The Problem This Pattern Solves
Picture an interface with one method — a validator, a mapper, a handler, a retry executor. You have a function that matches the method signature perfectly. The function does exactly what the interface demands. But you cannot pass the function directly to any code that asks for the interface, because a bare function is not a type with methods.
The traditional workaround is a wrapper struct:
type Validator interface {
Validate(input string) bool
}
// You have this function:
func checkEmail(input string) bool {
return strings.Contains(input, "@")
}
// You're forced to write this struct:
type emailChecker struct{}
func (e emailChecker) Validate(input string) bool {
return checkEmail(input)
}
// Then use it:
var v Validator = emailChecker{}
This works. It is also tedious. Every new validation function needs its own struct type, or you stuff multiple functions into one struct as fields — which is still extra bookkeeping. When the interface has exactly one method, the struct adds no value beyond holding a reference to the function. The function type approach eliminates the middleman.
How Function Types Can Have Methods
In Go, you can define methods on any type you declare in your package. This includes:
- Struct types
- Named primitive types (
type Celsius float64) - Function types (
type HandlerFunc func(ResponseWriter, *Request))
The last one is less obvious because most languages do not let you attach methods to a function signature. Go does. The syntax is identical to defining a method on a struct:
type MapperFunc func(int) int
func (mf MapperFunc) Map(input int) int {
return mf(input)
}
The receiver mf is a variable of type MapperFunc. Since MapperFunc is a function type, mf is a callable function value. The method body calls mf(input) — it invokes the underlying function with the argument and returns the result.
Named Types Are the Key:
You can only define methods on types declared in your own package. You cannot add a method directly to func(int) int — the unnamed function signature. You must first create a named type with type MapperFunc func(int) int, then attach the method to MapperFunc. This is the same rule that prevents you from adding methods to int directly — you need type MyInt int first.
Once the method exists, any function that shares the signature can be converted to MapperFunc. The conversion gives the function the method it needs to satisfy the interface. The function itself does not change; it just gains the method through the type conversion.
The Bridge Pattern Step by Step
A concrete example makes the transformation clear. Start with an interface and a standalone function that should satisfy it:
package main
import "fmt"
// An interface with one method.
type Transformer interface {
Transform(value int) int
}
// A standalone function that matches the method signature.
func double(n int) int {
return n * 2
}
func triple(n int) int {
return n * 3
}
The function double cannot be passed where a Transformer is expected. It lacks the Transform method. Now introduce the bridge — a function type with the method attached:
// Define a function type matching the method signature.
type TransformFunc func(int) int
// Attach the interface method to the function type.
func (tf TransformFunc) Transform(value int) int {
return tf(value)
}
TransformFunc is a function type. Its Transform method calls the underlying function tf with the supplied value. The method signature matches Transformer.Transform, so TransformFunc implicitly satisfies Transformer.
Now any function with the signature func(int) int can be converted to TransformFunc and used as a Transformer:
func applyTransform(values []int, t Transformer) []int {
result := make([]int, len(values))
for i, v := range values {
result[i] = t.Transform(v)
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
// Convert the function to TransformFunc at the call site.
doubled := applyTransform(nums, TransformFunc(double))
tripled := applyTransform(nums, TransformFunc(triple))
fmt.Println("Doubled:", doubled) // [2 4 6 8 10]
fmt.Println("Tripled:", tripled) // [3 6 9 12 15]
}
The conversion TransformFunc(double) does not allocate a struct, does not create a closure wrapper, and does not change how double executes. It merely tags the function value with the method set of TransformFunc, making it acceptable to anything that expects a Transformer. The call t.Transform(v) inside applyTransform reaches the Transform method on the function type, which delegates to the original double or triple function.
The Pattern Is Working When:
You can pass TransformFunc(yourFunction) anywhere a Transformer is required, and the code compiles without a wrapper struct. If the compiler accepts the conversion at the call site, the bridge is set up correctly.
This is the complete mechanism: name a function type, give it the interface method whose body calls itself, and convert compatible functions at the point of use. No structs, no boilerplate, no indirection beyond the method call.
http.HandlerFunc — The Canonical Example
The most widely used instance of this pattern lives in Go's net/http package. Every Go web server interacts with it, often without the programmer realizing the function type bridge is at work.
The http.Handler interface defines one method:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
To handle HTTP requests, you need something that implements Handler. The standard library provides http.HandlerFunc, a function type with a ServeHTTP method:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
The method body is a single line: f(w, r). It calls the underlying function f with the response writer and request, exactly as the Handler interface requires. This means any function with the signature func(ResponseWriter, *Request) can become an http.Handler by wrapping it in http.HandlerFunc.
Here is how it looks in practice:
package main
import (
"fmt"
"net/http"
)
func main() {
// Define a handler as a plain function.
helloHandler := func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}
// Convert it to an http.Handler via HandlerFunc.
http.Handle("/greet/", http.HandlerFunc(helloHandler))
http.ListenAndServe(":8080", nil)
}
http.Handle expects an http.Handler as its second argument. http.HandlerFunc(helloHandler) performs the conversion, and the function type's ServeHTTP method bridges the call. Without this pattern, every trivial handler would require a struct type with a ServeHTTP method — an unnecessary layer of indirection for handlers that carry no state.
The same bridge enables middleware chaining. A middleware function takes an http.Handler and returns an http.Handler:
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Request: %s %s\n", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
The anonymous function inside LoggingMiddleware is converted to http.HandlerFunc, which satisfies http.Handler. The middleware wraps the original handler, logs the request, and delegates. Without the function type bridge, this would require a named struct to hold next and a method on that struct — more code for no additional clarity.
HandlerFunc Is Not a Handler Until Converted:
A bare function with the signature func(ResponseWriter, *Request) is not an http.Handler. You must explicitly wrap it with http.HandlerFunc(). Forgetting the conversion is a compile error: "cannot use myFunc (type func(http.ResponseWriter, *http.Request)) as type http.Handler in argument to http.Handle." The fix is always http.HandlerFunc(myFunc).
Custom Use Cases Beyond HTTP
The function type bridge is not exclusive to the standard library. It solves the same class of problem — "I have a function, but the API demands an interface" — across many domains.
Mocking for Tests
When an interface has one method, creating mock implementations for tests can be done inline with function types instead of dedicated mock structs:
// Production interface.
type Authenticator interface {
Authenticate(user, pass string) (bool, error)
}
// Function type bridge.
type AuthFunc func(user, pass string) (bool, error)
func (af AuthFunc) Authenticate(user, pass string) (bool, error) {
return af(user, pass)
}
// In tests:
func TestLogin(t *testing.T) {
// Mock that always succeeds.
successAuth := AuthFunc(func(u, p string) (bool, error) {
return true, nil
})
// Mock that always fails.
failAuth := AuthFunc(func(u, p string) (bool, error) {
return false, nil
})
// Pass directly to code expecting Authenticator.
err := Login("alice", "secret", successAuth)
if err != nil {
t.Fatalf("expected success, got error: %v", err)
}
err = Login("alice", "secret", failAuth)
if err == nil {
t.Fatal("expected failure, got nil")
}
}
Each test case defines the mock behavior inline, directly above the test assertion. There is no separate mock struct with boolean fields to control return values, no constructor to set up. The function literal carries all the logic in a few lines. For table-driven tests, you can include the AuthFunc directly in the test case struct — each row supplies its own implementation.
Query Executor Abstraction
A function type can wrap database operations so application code depends on an interface rather than a concrete driver:
type QueryExecutor interface {
Execute(query string, args ...any) (sql.Result, error)
}
type QueryFunc func(query string, args ...any) (sql.Result, error)
func (qf QueryFunc) Execute(query string, args ...any) (sql.Result, error) {
return qf(query, args...)
}
Production code uses the real database handle wrapped in QueryFunc. Tests use a function that records the query string and returns a canned result — no database required. The calling code only sees the QueryExecutor interface.
Retry Logic
A retry strategy can be captured as a function type that satisfies a Retryer interface:
type Retryer interface {
Retry(operation func() error) error
}
type RetryFunc func(operation func() error) error
func (rf RetryFunc) Retry(operation func() error) error {
return rf(operation)
}
A simple retry implementation:
func main() {
retry := RetryFunc(func(op func() error) error {
for attempt := 0; attempt < 3; attempt++ {
if err := op(); err == nil {
return nil
}
time.Sleep(time.Duration(attempt+1) * time.Second)
}
return fmt.Errorf("operation failed after 3 attempts")
})
err := retry.Retry(func() error {
// Attempt something that might fail transiently.
return nil
})
if err != nil {
fmt.Println("All retries exhausted:", err)
}
}
The retry strategy is a single function, not a struct with configuration fields and a method. For more complex retry behavior — exponential backoff with jitter, circuit breaking — the function type still works, though at some complexity threshold a struct becomes easier to test and configure independently.
Custom Data Processing Pipelines
Any single-method interface that represents a transformation, filter, or mapping operation is a candidate:
type Filter interface {
Keep(item string) bool
}
type FilterFunc func(item string) bool
func (ff FilterFunc) Keep(item string) bool {
return ff(item)
}
func filterItems(items []string, f Filter) []string {
var result []string
for _, item := range items {
if f.Keep(item) {
result = append(result, item)
}
}
return result
}
func main() {
words := []string{"go", "rust", "typescript", "zig", "python"}
longWords := filterItems(words, FilterFunc(func(s string) bool {
return len(s) > 4
}))
fmt.Println(longWords) // [typescript python]
}
Function Types Cannot Hold Independent State Fields:
A function type carries exactly one function value. If you need multiple independent pieces of state — a timeout duration, a maximum retry count, a logger instance, and a circuit breaker flag — a struct is the right tool. Attempting to stuff all of that into closure variables captured by a function literal creates code that is hard to test in isolation. Use function types when the behavior is stateless or when the only state is naturally captured by the closure.
The Single-Method Interface Constraint
Function type bridges work best with single-method interfaces. A function type is, after all, a single function. When an interface has multiple methods with genuinely different semantics, a single underlying function cannot serve them all cleanly.
Consider an interface with two methods:
type Parser interface {
Parse(input string) (AST, error)
Validate(input string) error
}
You could, technically, define a function type with both methods:
type ParseFunc func(input string) (AST, error)
func (pf ParseFunc) Parse(input string) (AST, error) {
return pf(input)
}
func (pf ParseFunc) Validate(input string) error {
_, err := pf(input)
return err
}
This compiles. Validate calls pf(input), discards the AST, and returns the error. It works if validation is synonymous with "parse and check for errors." But if Parse and Validate are truly distinct operations — if validation checks structural constraints that parsing does not — a single function cannot represent both accurately. The semantic mismatch makes the code misleading.
Multi-Method Interfaces Require Structs:
If an interface has two or more methods with independent behaviors, a function type bridge forces all methods through the same underlying function. This creates an abstraction that either lies about its capabilities or silently conflates distinct operations. For multi-method interfaces, use a struct with separate fields or methods. The function type pattern is designed for the single-method case, and forcing it beyond that produces brittle, confusing code.
The standard library follows this rule. http.Handler has one method. io.Reader has one method. io.Writer has one method. fmt.Stringer has one method. Each has a corresponding function type adapter (http.HandlerFunc, but note that io.ReaderFunc and io.WriterFunc do not exist in the standard library — they would be valid if defined, but the standard library chose not to provide them). The pattern is so closely associated with single-method interfaces that the community often uses the term "function type adapter" interchangeably with "single-method interface adapter."
Choosing Between a Struct and a Function Type
When you need to satisfy a single-method interface, you have two options. The choice depends on whether the implementation carries state that is independent of the function logic.
type Validator interface {
Validate(input string) bool
}
// Struct approach: state lives in fields.
type LengthValidator struct {
MinLength int
MaxLength int
}
func (lv LengthValidator) Validate(input string) bool {
return len(input) >= lv.MinLength && len(input) <= lv.MaxLength
}
// Usage: create with configuration, reuse.
v := LengthValidator{MinLength: 3, MaxLength: 20}
fmt.Println(v.Validate("go")) // false (too short)
fmt.Println(v.Validate("golang")) // true
fmt.Println(v.Validate("a very long string")) // false (too long)
Use a struct when the implementation has configuration that should be visible, inspectable, and independently testable — like MinLength and MaxLength as exported fields. A struct also works better when you need to create multiple instances with different configurations from the same type, or when the implementation grows additional methods over time.
Use a function type when the implementation is a single behavior that can be expressed cleanly as a function literal, especially when the configuration is captured naturally by the closure or when different call sites need completely different logic. Inline function literals in tests are the strongest use case — they keep the mock behavior in the same visual context as the assertion.
A third path exists: sometimes neither is perfect, and the right answer is to define the interface differently. If every implementation requires substantial state, the interface might be too low-level, and the caller should depend on a higher-level abstraction.
Common Mistakes
Forgetting the Type Conversion
The most frequent error is passing a bare function where an interface is expected without wrapping it in the function type:
type Handler interface {
Handle(event string)
}
type HandlerFunc func(event string)
func (hf HandlerFunc) Handle(event string) {
hf(event)
}
func processEvent(h Handler) {
h.Handle("user_signup")
}
func main() {
myHandler := func(event string) {
fmt.Println("Processing:", event)
}
// WRONG: compiler error.
// processEvent(myHandler)
// RIGHT: explicit conversion.
processEvent(HandlerFunc(myHandler))
}
The compiler error says the function does not implement the interface. The fix is always the conversion. There is no implicit wrapping in Go.
Capturing Mutable State in a Closure and Expecting It to Be Safe
A function type adapter created from a closure captures variables by reference. If those variables change after the adapter is created, the behavior changes too:
threshold := 10
vf := ValidatorFunc(func(input string) bool {
return len(input) > threshold
})
fmt.Println(vf.Validate("hello")) // false (5 > 10 is false)
threshold = 3
fmt.Println(vf.Validate("hello")) // true (5 > 3 is true) — changed behavior!
This is expected closure semantics, not a bug in the pattern. It becomes a problem when the adapter is passed to concurrent code and the captured variable is modified elsewhere without synchronization. If the closure captures a map, slice, or pointer, mutations from other goroutines create data races.
Overusing the Pattern for Complex Behavior
When the function literal grows beyond a few lines — especially when it contains loops, conditionals for error handling, or interaction with external resources — a struct is clearer. A 30-line function literal wrapped in a type conversion and passed as an argument is hard to read, hard to test in isolation, and hard to reuse. The pattern shines for short, focused behaviors; it becomes a liability for complex ones.
Summary
Function types implementing interfaces is Go's answer to a specific friction: single-method interfaces are the language's preferred abstraction unit, but creating a struct for every trivial implementation of such an interface adds unnecessary indirection. By letting function types carry methods, Go allows a plain function to step directly into the role of an interface implementation with a one-line type conversion.
The pattern is not a general replacement for structs. It works when the interface has exactly one method whose behavior is fully captured by a single function — no independent configuration, no multiple distinct operations, no lifecycle management. The http.HandlerFunc adapter in the standard library is the definitive reference implementation and the one you will encounter most often in practice.
When you find yourself writing a struct whose only field is a function and whose only method calls that function, you are looking at a candidate for this pattern. Convert the struct to a named function type, attach the method, and delete the boilerplate. The code that results is shorter, more direct, and no less clear.