Variadic Functions & Function Types
Go treats functions as values you can name, pass, and return - and lets the final parameter absorb a variable argument list with ...T.
Together, variadic signatures and function types power middleware, options, and small extension points without interface boilerplate.
Summary
A variadic parameter (args ...int) is syntactic sugar for a slice inside the function.
Callers pass zero or more arguments, or expand an existing slice with fn(slice...).
A function type (type Handler func(...)) documents callable contracts and enables higher-order functions that accept behavior as data.
Use variadics for constructors like fmt.Sprintf, logging helpers, and functional options.
Use function types for HTTP middleware, retry wrappers, and predicates.
Prefer small interfaces when you need multiple related methods or mocking across packages.
Recipe
Quick-reference recipe card - copy-paste ready.
type Option func(*Server)
func WithPort(p int) Option {
return func(s *Server) { s.port = p }
}
func New(opts ...Option) *Server {
s := &Server{port: 8080}
for _, opt := range opts {
opt(&s)
}
return s
}
type Middleware func(http.Handler) http.Handler
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}When to reach for this:
- Optional configuration knobs accumulate (
WithTimeout,WithLogger, ...). - A single hook point needs behavior injection (
Retry,Authorize). - You wrap handlers or RPC interceptors in a chain.
- Utilities aggregate unknown argument counts (
sum,concat).
Working Example
package retry
import (
"context"
"errors"
"time"
)
type AttemptFunc func(ctx context.Context) error
func Do(ctx context.Context, attempts int, backoff time.Duration, fn AttemptFunc) error {
var err error
for i := 0; i < attempts; i++ {
if err = fn(ctx); err == nil {
return nil
}
if ctx.Err() != nil {
return ctx.Err()
}
time.Sleep(backoff)
backoff *= 2
}
return err
}
func ExampleUsage(ctx context.Context, call func(context.Context) error) error {
return Do(ctx, 3, 100*time.Millisecond, func(ctx context.Context) error {
if err := call(ctx); err != nil {
return err
}
return nil
})
}
var ErrPermanent = errors.New("retry: permanent failure")What this demonstrates:
AttemptFuncnames the callable contract instead of repeatingfunc(context.Context) error.- Higher-order
Doaccepts behavior and owns loop/backoff policy. - Context cancellation checked between attempts - function values still respect boundaries.
- Closure in
ExampleUsageadapts a simplercallintoAttemptFunc.
Deep Dive
How It Works
- Variadic parameters compile to slice parameters;
...at call site splices a slice into discrete arguments. - Function values are pointers to code plus captured environment for closures.
- Named function types are distinct from their underlying signature for method sets - you cannot assign
func(int) inttotype Op func(int) intwithout conversion, but conversion is allowed when signatures match. nilfunction values must be checked before call - invokingnilpanics like a nil interface method call.
Variadic Rules at a Glance
| Rule | Detail |
|---|---|
| Position | Variadic parameter must be last |
| Inside body | Type is []T, use len, range normally |
| Zero args | Valid - slice is non-nil, length 0 |
| Spread | fn(slice...) requires slice type []T exactly |
| Mixing | Cannot pass non-variadic arg after variadic capture |
Function Types vs Interfaces
| Mechanism | Strength | Limit |
|---|---|---|
func type | One method, zero boilerplate | No named multi-method grouping |
| Interface | Mocking, evolution via embedding | Wider test doubles |
| Generics + func | Reusable algorithms | Heavier signatures |
Go Notes
var fn func()
if fn != nil {
fn()
}Always guard optional callbacks.
Middleware chains should treat nil as identity: if mw == nil { return next }.
Gotchas
- Calling nil function value - Panics at runtime. Fix: check
if fn != nilbefore invoke, or require non-nil in API docs. - Variadic with empty slice vs nil - Inside function,
args == nilis false for zero-arg call (empty non-nil slice). Fix: uselen(args) == 0when distinguishing absent vs empty matters. - Spreading wrong slice type -
[]bytecannot splice into...int. Fix: convert or change signature. - Capturing mutating state in options - Options run in order; later options may observe earlier mutations unexpectedly. Fix: document order dependence or validate in
New. - Function type conversion surprises - Named types differ from identical underlying func types. Fix: explicit conversion
Op(fn)when needed. - Variadic in public API stability - Adding non-variadic parameters breaks callers; adding new optional behavior via functional options does not. Fix: prefer options for evolving constructors.
- Passing variadic functions to reflect - Easy to mis-invoke without
CallSlice. Fix: stick to typed calls unless building generic tooling.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Small interface | Multiple methods, generated mocks | Single ServeHTTP-style hook |
| Struct options | Need validation across fields | Only one or two knobs |
Builder pattern (Chain().Use()) | Fluent middleware registration | Simple []Middleware slice suffices |
Generics func F[T any](...) | Type-safe map/filter utilities | Runtime plugin loading |
FAQs
What does ... mean in a parameter list?
It declares a variadic parameter of type slice []T.
The function receives all trailing arguments as one slice.
How do I pass a slice to a variadic function?
Use fn(items...) where items is []T.
Without ..., you pass the slice as a single argument (type error if signature is variadic).
Can variadic functions accept zero arguments?
Yes - unless you validate len(args) == 0 and return an error.
fmt.Println() is a familiar example.
What is a function type?
A named type whose underlying type is a function signature.
It enables methods on functions (see http.HandlerFunc) and clearer APIs.
How do higher-order functions help testing?
Inject AttemptFunc or clock functions to simulate failures without interfaces.
Tests pass closures recording invocations.
When should middleware be func vs interface?
func(http.Handler) http.Handler is the ecosystem standard for net/http.
gRPC uses interceptor function types similarly.
Are variadic parameters copied?
The slice header is passed by value; elements are referenced.
Mutating args[0] inside the function affects caller elements if they share backing array.
Can methods be variadic?
Yes - receivers coexist with variadic params: func (l *Logger) Info(msg string, kv ...any).
How do functional options compare to Config structs?
Options scale optional fields without breaking callers.
Large required configuration still fits a struct validated once in New.
Does Go support default parameter values?
No - use functional options, config structs, or overload-style wrappers (Dial() calling DialContext(context.Background(), ...)).
Can I store variadic functions in maps?
Yes - value type is the function signature or named type.
Remember map keys cannot be slices; keys must be comparable.
How does this relate to generics?
Generics reduce duplication for slice algorithms; function types still express runtime injection and I/O hooks.
Related
- Function Literals & Closures - capturing variables in function values
- Functions & Methods Basics - first variadic and function type examples
- Defining and Implementing Interfaces - when interfaces beat function types
- Methods: Value vs Pointer Receivers - methods on function types (
HandlerFunc) - Functions, Methods & Interfaces Best Practices - middleware and options conventions
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).