skip to content

When would you declare a `[32]byte` parameter in a Go API instead of `[]byte`?

level: seniorimportance: should knowfreq 40%

answer

  1. what does the type say that a comment cannot
  2. who owns the bytes after the call returns
  3. count the copies per call
  4. the standard library speaks slices at every edge

basics

~20 s

Use a fixed-size array parameter when the size is part of the contract and you want the compiler to enforce it: a digest, a key, a fixed frame. It also gives the callee an independent copy. Pay for it in copying and in conversions at every slice boundary.

solid answer

~50 s

A `[32]byte` parameter moves a length check from run time into the type system: nothing but exactly 32 bytes can be passed, there is no nil case, and no caller can hand you a 31-byte key by accident. It is also copied on the way in, so the callee cannot mutate the caller's bytes and the caller cannot mutate the value after the call — useful for a digest or a key that should not change under you. The costs are real: every call copies the whole array, so a large one in a hot path is a measurable memcpy; and because the standard library speaks `[]byte`, every boundary needs `arr[:]` or a conversion. The length is also baked into the signature, so supporting a second size means a second function. I use arrays where a specification fixes the size and slices everywhere else.

code

go · 10 lines
go
// The compiler rejects anything that is not exactly 32 bytes.
func VerifyArr(key [32]byte, msg []byte) bool

// The check is yours to write, and every caller must be trusted.
func VerifySlice(key []byte, msg []byte) (bool, error) {
	if len(key) != 32 {
		return false, fmt.Errorf("key must be 32 bytes, got %d", len(key))
	}
	// ...
}

go deeper

for a junior

Know that a fixed-size array type in a signature accepts exactly that many elements and copies them, while a slice parameter accepts any length and shares the caller's elements.

for a middle

Explain the mechanics behind the choice: copy cost per call, an array field stored inline in its struct versus a slice field pointing elsewhere, and the conversions needed at every standard-library boundary.

for a senior

Show judgment. Reach for a fixed-size array where an external specification pins the size and the value is small, keep the conversions at the package edge, and be able to say why a large array parameter in a hot path is a defect.

for a principal

Own the consequence: the length is welded into an exported signature, so a second size means a second API. Decide up front whether size is a type-level contract or a validated input, because callers will build on whichever you ship.

## Choosing a fixed-size array in a signature Most Go APIs take `[]byte`. Choosing `[N]byte` instead is a deliberate decision with a clear payoff and a clear bill, and it is one of the first design calls an engineer arriving from another language gets wrong in both directions. ### What the array buys you **A compile-time length guarantee.** A function declared `func Verify(key [32]byte, msg []byte) bool` cannot be called with 31 bytes. The whole class of "wrong-length key" bugs disappears, and it disappears at the call site rather than inside your function. With `[]byte` you must write and test a length check, and every caller must be trusted to have read the doc comment. **No nil, no aliasing.** An array parameter has no nil form, so there is no nil branch to write. And because it is copied on the way in, the callee holds bytes nobody else can change: the caller cannot mutate them afterwards, and the callee cannot scribble on the caller's buffer. With a `[]byte` parameter both of those are possible, and "who owns this buffer after the call" becomes something your documentation has to answer. **Inline storage.** A `[32]byte` field sits inside its struct with no separate allocation and no pointer to chase. A `[]byte` field is a reference to elements that live somewhere else, which is a second allocation and an extra indirection per access. For a value you keep millions of — a digest per record, a fixed-size id — that difference is measurable. **Self-documenting size.** The signature says 32, so the reader does not have to find out whether it means SHA-256 or something else. The standard library uses exactly this: `sha256.Sum256` returns `[32]byte`, precisely because the size is fixed by the algorithm. ### What it costs **Copying.** Every assignment and every call copies all N bytes. At 32 bytes that is nothing; at 4096 it is a per-call memcpy the compiler will not remove, and it is easy to leave in a hot loop without noticing because the syntax gives no hint. A slice parameter passes a small reference regardless of element count. **Friction at every boundary.** Readers, writers, hashers, encoders and network code all speak `[]byte`. So you spend `arr[:]` on the way out and a checked conversion on the way in, and the array being sliced must be addressable — a function result has to land in a variable first. **The size is welded into the signature.** Supporting 64-byte digests as well means a second type and a second function; Go has no way to make the length a parameter, so there is no generic "array of any length" to fall back on. If you can foresee more than one size, `[]byte` plus a validated length is the shape that survives. ### How this plays out in a fixed-frame daemon Consider a long-running agent that reads fixed 32-byte sensor frames off a device and forwards them. Internally, holding each frame as `[32]byte` is a good fit: the size is fixed by the device protocol, each frame is a self-contained value that can be handed to another goroutine without worrying about who else holds the buffer, and a ring of frames is one contiguous block rather than N little allocations. At the edges it is all slices — the read fills `buf[:]`, the write takes `frame[:]` — and exactly one place converts, with an explicit length check. The opposite arrangement is what the engineer porting from C tends to write: a `[]byte` everywhere plus a comment saying "must be 32 bytes", with the length re-checked in four places and missed in a fifth. And the mirror-image mistake, from the same background, is assuming a `[4096]byte` parameter behaves like C's decay to a pointer and is free to pass. ### The rule of thumb Use `[N]byte` when an external specification fixes N and the value is small — digests, keys, nonces, fixed-width ids, protocol frames. Use `[]byte` when the length is data, when the value is large, or when the type sits on an interface your callers implement. And when you do expose an array, keep the conversions at the package boundary rather than sprinkling `[:]` through the internals.

  • What does a `[32]byte` parameter cost that a `[]byte` parameter does not?
    A copy of all 32 bytes on every call, versus a small fixed-size reference for the slice. It also costs conversion friction: readers, writers and encoders all take `[]byte`, so you spend `arr[:]` going out and a length-checked conversion coming in. At 32 bytes the copy is negligible; at a few kilobytes in a hot loop it is not.
  • How would you support both 32-byte and 64-byte digests in one function?
    You cannot parameterise an array's length in Go — there is no const generic — so a single function over both array types is not available. Either take `[]byte` and validate the length once at the boundary, or expose two functions, one per size, and share an unexported implementation that works on slices.
  • Why does the standard library offer both `sha256.Sum256` returning `[32]byte` and a streaming `Sum` returning `[]byte`?
    `Sum256` hashes a complete input, so the size is known and an array conveys it exactly, with no allocation. The streaming interface has to append the digest to a caller-supplied slice so callers can control allocation and build up buffers, which only a slice return can do.

saying these in an interview costs you the question

  • Says a [32]byte parameter is passed by reference, as in C
  • Thinks a [4096]byte parameter costs nothing to pass
  • Uses []byte plus a comment saying the length must be 32
  • Believes the callee can mutate the caller's array parameter
  • Claims Go generics can parameterise an array's length