←Back to Blog
Go Interfaces: One Helper, Multiple Behaviors

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:

go
type HistoryPrize struct {
    PrizeID   string
    Quantity  float64
    HistoryID string
}

type PayoutPrize struct {
    PrizeID       int
    Quantity      float64
    TradeMultiple *float64
}

If the only shared rule is that quantity must be positive, separate helpers repeat the same logic:

go
func validateHistoryPrize(prize HistoryPrize) error {
    if prize.Quantity <= 0 {
        return ErrInvalidPrizeQuantity
    }
    return nil
}

func validatePayoutPrize(prize PayoutPrize) error {
    if prize.Quantity <= 0 {
        return ErrInvalidPrizeQuantity
    }
    return nil
}

Define an interface around what the shared helper needs, then write the rule once:

go
type claimPrize interface {
    GetQuantity() float64
}

func validatePrize(prize claimPrize) error {
    if prize.GetQuantity() <= 0 {
        return ErrInvalidPrizeQuantity
    }
    return nil
}

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:

go
func (p HistoryPrize) GetQuantity() float64 { return p.Quantity }
func (p PayoutPrize) GetQuantity() float64  { return p.Quantity }

Now either value can go straight to the same helper:

go
if err := validatePrize(historyPrize); err != nil {
    return err
}
if err := validatePrize(payoutPrize); err != nil {
    return err
}

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:

go
type claimPrize interface {
    GetPrizeID() string
    GetQuantity() float64
}

func (p HistoryPrize) GetPrizeID() string { return p.PrizeID }
func (p PayoutPrize) GetPrizeID() string  { return strconv.Itoa(p.PrizeID) }

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:

go
func validatePrize(prize claimPrize, enabled map[string]bool) error {
    if !enabled[prize.GetPrizeID()] {
        return ErrPrizeDisabled
    }
    if prize.GetQuantity() <= 0 {
        return ErrInvalidPrizeQuantity
    }
    return nil
}

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:

go
type BonusStrategy interface {
    Calculate(base float64) float64
}

type FixedBonus struct{ Amount float64 }

func (s FixedBonus) Calculate(base float64) float64 {
    return base + s.Amount
}

type PercentageBonus struct{ Rate float64 }

func (s PercentageBonus) Calculate(base float64) float64 {
    return base * (1 + s.Rate)
}

func applyBonus(base float64, strategy BonusStrategy) float64 {
    return strategy.Calculate(base)
}

The caller can choose a strategy at setup time:

go
var strategy BonusStrategy = FixedBonus{Amount: 10}
if percentageCampaign {
    strategy = PercentageBonus{Rate: 0.15}
}

total := applyBonus(100, strategy)

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...