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.
Search across all documentation pages
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.
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.
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:
WithTimeout, WithLogger, ...).Retry, Authorize).sum, concat).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:
AttemptFunc names the callable contract instead of repeating func(context.Context) error.Do accepts behavior and owns loop/backoff policy.ExampleUsage adapts a simpler call into AttemptFunc.... at call site splices a slice into discrete arguments.func(int) int to type Op func(int) int without conversion, but conversion is allowed when signatures match.nil function values must be checked before call - invoking nil panics like a nil interface method call.| 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 |
| 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 |
var fn func()
if fn != nil {
fn()
}Always guard optional callbacks.
Middleware chains should treat nil as identity: if mw == nil { return next }.
if fn != nil before invoke, or require non-nil in API docs.args == nil is false for zero-arg call (empty non-nil slice). Fix: use len(args) == 0 when distinguishing absent vs empty matters.[]byte cannot splice into ...int. Fix: convert or change signature.New.Op(fn) when needed.CallSlice. Fix: stick to typed calls unless building generic tooling.
| 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 |
It declares a variadic parameter of type slice []T.
The function receives all trailing arguments as one slice.
Use fn(items...) where items is []T.
Without ..., you pass the slice as a single argument (type error if signature is variadic).
Yes - unless you validate len(args) == 0 and return an error.
fmt.Println() is a familiar example.
A named type whose underlying type is a function signature.
It enables methods on functions (see http.HandlerFunc) and clearer APIs.
Inject AttemptFunc or clock functions to simulate failures without interfaces.
Tests pass closures recording invocations.
func(http.Handler) http.Handler is the ecosystem standard for net/http.
gRPC uses interceptor function types similarly.
The slice header is passed by value; elements are referenced.
Mutating args[0] inside the function affects caller elements if they share backing array.
Yes - receivers coexist with variadic params: func (l *Logger) Info(msg string, kv ...any).
Options scale optional fields without breaking callers.
Large required configuration still fits a struct validated once in New.
No - use functional options, config structs, or overload-style wrappers (Dial() calling DialContext(context.Background(), ...)).
Yes - value type is the function signature or named type.
Remember map keys cannot be slices; keys must be comparable.
Generics reduce duplication for slice algorithms; function types still express runtime injection and I/O hooks.
HandlerFunc)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).
Reviewed by Chris St. John·Last updated Jul 19, 2026