skip to content

What does an exported Go function lose by taking a parameter of type any?

level: middleimportance: should knowfreq 45%

answer

  1. the signature stops saying anything
  2. the contract moved into the doc comment
  3. the compiler cannot help the caller
  4. a type switch with a default branch

basics

~20 s

An any parameter gives up the compile-time contract. The signature no longer says what is accepted, so the rule lives in a doc comment and a runtime type switch, and a caller's wrong type fails in production.

solid answer

~50 s

`any` is an alias for `interface{}` -- the empty interface, which every type satisfies -- so a parameter typed `any` accepts everything and promises nothing. The signature stops documenting the function, the contract migrates into the doc comment and a `switch v := x.(type)` with a `default` branch that returns an error, and a caller's mistake becomes a runtime failure instead of a compile error. Readers lose the ability to know what to pass without reading the body, and the value usually has to be boxed and asserted back out. The fix is normally a named interface stating the behaviour you actually need -- often one method -- or simply the concrete type. `any` earns its place when the value is genuinely opaque to the function, which is why `fmt.Println`, `encoding/json.Marshal`, `context.WithValue` and the `args ...any` of `database/sql` use it.

code

go · 11 lines
go
// Record stores an event. Accepts an Authorization or a Refund.
func Record(ctx context.Context, event any) error {
	switch e := event.(type) {
	case Authorization:
		return insertAuth(ctx, e)
	case Refund:
		return insertRefund(ctx, e)
	default:
		return fmt.Errorf("record: unsupported event type %T", event)
	}
}

go deeper

for a junior

Know that any is an alias for interface{}, that every type satisfies it, and that getting the value back out needs a type assertion or a type switch.

for a middle

Explain what moves when a parameter becomes any: the contract leaves the signature, checking moves from compile time to run time, and the accepted set survives only in a doc comment and a switch statement.

for a senior

Show where you draw the line in review. Distinguish genuinely opaque carriage from a hidden finite set, and name the replacement you would ask for -- a one-method interface, a concrete type, or a closed family enforced by an unexported method.

for a principal

Decide the house rule for any at exported boundaries and be prepared to defend it, including what it costs to remove one later when several teams already call through it.

## What `any` is `any` is a predeclared alias for `interface{}`, the interface with no methods. Every type satisfies it, so `any` is not a wildcard the compiler resolves per call and it is not a type parameter -- it is one specific interface type that happens to be satisfied by everything. A value stored in it is a pair of pointers: one to type information, one to the data. Getting the original value back requires a type assertion or a type switch, and putting a non-pointer value in frequently costs an allocation. ## The concrete losses **The signature stops being the contract.** `func Record(ctx context.Context, event any) error` tells a caller nothing. What is actually accepted lives in the doc comment and in a type switch inside the body, and neither is checked by anything. Two years later the doc comment and the switch disagree. **Mistakes move from compile time to run time.** Passing the wrong type compiles cleanly and fails when that code path executes -- as an error return if the author wrote a `default` branch, or as a panic from an unchecked assertion if they did not. In a payments path that is the difference between a red build and a failed authorisation. **Tooling goes quiet.** Editor completion, refactoring tools and `go vet` all work from types. `go vet` can check `fmt`'s `...any` arguments only because it understands the format verbs; it has no such knowledge of your function. **Callers pay in ceremony.** They construct a value, hand it over as `any`, and on the way back out somebody asserts. Every assertion is a place to get the type wrong, and `v.(T)` without the two-result form panics when it does. **The abstraction is dishonest.** `any` at a parameter reads as flexibility, but usually the body accepts three types. The set is finite; it is just not written down where the compiler can see it. ## What to use instead **A named interface describing the behaviour you need.** If the body only calls one method on the value, that is the parameter type. This is the same discipline as consumer-declared interfaces: the function asks for exactly what it uses, and every caller gets a compile-time answer about whether their type fits. **The concrete type.** If the function really handles one type, say so. Two types often means two functions with clear names, which reads better than one function with a switch. **A type parameter**, when the body is genuinely identical for many types and the function needs to hand the caller its own type back rather than an `any`. **A closed set expressed as an interface with an unexported method.** Declaring `interface { isEvent() }` and implementing `isEvent()` on each accepted type lets the compiler enforce membership of a family defined by your package, with the type switch retained inside the body for dispatch. ## When `any` is right The test is whether the function is genuinely indifferent to the type: - **Formatting and serialisation.** `fmt.Println(a ...any)` and `json.Marshal(v any)` work over arbitrary values by reflection; that is their whole job. - **Opaque carriage.** `context.WithValue(parent, key, val any)` stores a value it never inspects. A cache or a queue that hands back exactly what it was given is the same case. - **Driver-shaped plumbing.** `database/sql`'s `Query(query string, args ...any)` passes arguments through to a driver that decides how to bind them; the package cannot know the set in advance. Notice what these share: the function does not branch on the type to decide behaviour. The moment you write a type switch with a `default` that returns "unsupported type", you have a finite set of accepted types and you are hiding it from the compiler. ## Review heuristic Ask what happens if a caller passes something the body does not handle. If the honest answer is "a runtime error at 3am", the parameter wants a type. If the honest answer is "nothing, we just carry it", `any` is doing its job.

  • Where in the standard library is an any parameter the right choice, and why?
    `fmt.Println`, `json.Marshal`, `context.WithValue` and `database/sql`'s `args ...any`. Each is genuinely indifferent to the type: it formats it, serialises it, carries it or passes it to a driver. None of them branches on the type to decide what the function means, which is the line between opaque carriage and a hidden finite set.
  • How would you enforce a closed set of accepted types without giving up compile-time checking?
    Declare an interface with an unexported method -- `type Event interface{ isEvent() }` -- and implement that method on each type you accept. Only types in your package can satisfy it, so the compiler rejects anything else at the call site, and the body can still type-switch to dispatch.
  • What is the difference between any and an unconstrained type parameter in a signature?
    `any` erases the type at the boundary: the body sees an interface value and the caller gets an interface value back, so both sides assert. A type parameter keeps the caller's type through the call, so the return value is already the right type and no assertion is needed. `any` is a value type; a type parameter is a compile-time substitution.

saying these in an interview costs you the question

  • any keeps the API flexible for future types
  • The doc comment says which types are allowed, so it is fine
  • any is Go's version of a generic parameter
  • A type assertion is cheap, so there is no real cost
  • Only reflection is slow, and we are not using reflection