10/24/2026 • 9 min read
Go Interfaces: One Helper, Multiple Behaviors
Go interfaces are useful when different types can do the same job. A small interface lets one helper work with several structs, or lets a caller choose behavior without a type switch.
One helper for different structs
Suppose a claim flow checks prizes from two sources. The structs have different fields and even different types for their IDs, but both have a quantity:
If the only shared rule is that quantity must be positive, separate helpers repeat the same logic:
Define an interface around what the shared helper needs, then write the rule once:
Both structs belong to this package, so give each a method that satisfies the interface. Go checks interface satisfaction implicitly; there is no implements declaration:
Now either value can go straight to the same helper:
If validation also needs the prize ID, add that method to both types. Since their ID fields have different types, each method can convert its own value to the common representation:
You only need a local wrapper when the concrete type is defined in another package. Go does not allow a package to add methods to a type it does not define. If you own HistoryPrize, implementing the interface directly is simpler.
Now a single helper can check both values without knowing which struct it received:
Keep the interface as small as the shared helper requires. A payout-only rule that uses TradeMultiple belongs in payout-specific code, not in the common contract.
Select behavior without a type switch
Interfaces can also represent a choice of behavior. This is the Strategy pattern: each strategy implements the same operation, and the caller selects one. The code using it calls the interface, not a switch over concrete types.
For example, different prize policies might calculate a bonus differently:
The caller can choose a strategy at setup time:
The conditional chooses a policy once; applyBonus does not need to inspect a type on every call. With several implementations, this keeps branching out of the operation that uses the behavior.
A type switch is still a good fit when the cases are a small, closed set and each case is genuinely different. Use an interface when the variants share an operation, behavior should be selected or supplied independently, or new implementations should work without editing a central switch.
For the broader patterns behind these techniques, see Go Design Patterns: Start With the Problem, Not the Pattern, especially Strategy and dependency injection.
When to introduce an interface
Start with the concrete code. Add an interface when a second type needs the same helper, when behavior needs interchangeable implementations, or when the consumer needs a small boundary for a dependency. Define it near the consumer and include only the methods that consumer calls.
Avoid an interface that just renames a struct or hides a single implementation without a concrete benefit. The useful part is the shared behavior; the interface should make that simpler to express.
Prize names and errors in the examples are illustrative. ErrInvalidPrizeQuantity and ErrPrizeDisabled stand for application-defined errors. The ID example needs import "strconv".
More: Go interface types · Strategy pattern.
Other posts that might interest you...
Go Design Patterns: Start With the Problem, Not the Pattern
Six practical Go patterns, the problems they solve, runnable examples, and when a simpler solution is enough.
Layered caching in Go: local memory, Redis, and Badger
Compose L1, L2, and L3 cache stores through one interface, with explicit read, write, TTL, and failure policies.
Keeping Write Paths Fast with Batching and Aggregation in Go
How gbuffer moves repeated writes off hot request paths with bounded batching, keyed aggregation, and a shared priority worker pool.