skip to content

What does hash.Hash's Sum(b []byte) method do to b and to the hash's internal state?

level: middleimportance: nice to knowfreq 30%

answer

  1. the argument is not the input
  2. think append, not allocate
  3. the hash is still usable afterwards
  4. nil is just an empty destination
  5. Reset is a separate call for a reason

basics

~20 s

Sum appends the current digest to the slice b and returns the extended slice; it does not hash b and does not change or reset the running hash. h.Sum(nil) is the usual form and returns just the digest.

solid answer

~40 s

`Sum(b []byte) []byte` treats its argument as a **destination to append to**, exactly like the `append` builtin: it returns `b` followed by the digest of everything written so far. `h.Sum(nil)` is the idiom, and it gives you a fresh `[]byte` of `h.Size()` bytes. Handing it a non-empty slice is occasionally useful - `h.Sum(prefix)` gets you the prefix and the digest in one allocation. The second half matters more: `Sum` **does not modify the hash state**. You can call it mid-stream, keep writing, and call it again - the second result covers everything written, not just the bytes since the first call. That also means reusing a `hash.Hash` for a new message requires an explicit `Reset()`; forgetting it silently digests the concatenation of both inputs.

code

go · 12 lines
go
h := sha256.New()
h.Write([]byte("a"))
d1 := h.Sum(nil) // digest of "a"

h.Write([]byte("b"))
d2 := h.Sum(nil) // digest of "ab": Sum did not reset anything

// Sum appends to whatever slice you hand it.
out := h.Sum([]byte("sha256:"))

// Reset returns the hash to its initial state for a new message.
h.Reset()

go deeper

for a junior

Learn the everyday form, h.Sum(nil), which hands back the digest as a []byte. The one thing to fix in your head: the argument is a destination the digest is appended to, never data to be hashed.

for a middle

State both halves precisely - Sum appends to the slice you pass, and it leaves the running state untouched - and explain that the second half is why reusing a hash across inputs needs an explicit Reset.

for a senior

Point at the bug this API shape produces in real code: a looped or pooled hash that is never reset, yielding digests over concatenated inputs that still look like perfectly valid hex. Say how you would catch it.

for a principal

Decide whether your codebase wraps this API at all. A helper that takes a reader and returns a digest removes a whole class of state bugs, at the cost of one more thing every newcomer has to learn instead of the standard interface.

## The signature that surprises people ```go type Hash interface { io.Writer Sum(b []byte) []byte Reset() Size() int BlockSize() int } ``` Read naively, `Sum(b []byte)` looks like it hashes `b`. It does not. The doc comment is precise: *Sum appends the current hash to b and returns the resulting slice. It does not change the underlying hash state.* Two independent facts live in that sentence, and both are load-bearing. ### Fact one: the argument is a destination `Sum` behaves like `append`. It writes the digest onto the end of whatever slice you pass and returns the grown slice. ```go h := sha256.New() h.Write(body) d := h.Sum(nil) // 32 bytes: just the digest out := h.Sum([]byte("v1:")) // "v1:" + 32 raw bytes, one slice ``` The `nil` form is overwhelmingly the common one: `nil` is a perfectly good empty slice to append to, so `Sum(nil)` allocates a fresh 32-byte result. The non-nil form exists so you can build a framed value without a second allocation, or reuse a scratch buffer across a loop (`buf = h.Sum(buf[:0])`). Ordinary `append` rules apply: if the destination has spare capacity the digest lands in place, otherwise a new array is allocated. The reason the API is shaped this way at all is allocation control. `hash.Hash` is used in hot paths, and a method that always allocated its own return value would be impossible to tune. ### Fact two: the hash keeps running `Sum` computes the finalisation - the padding and length encoding SHA-256 appends before producing its output - on a **copy** of the internal state. Your hash is untouched. ```go h := sha256.New() h.Write([]byte("a")) d1 := h.Sum(nil) // digest of "a" h.Write([]byte("b")) d2 := h.Sum(nil) // digest of "ab", not of "b" ``` This is genuinely useful: you can emit a running digest at checkpoints while continuing to consume a stream. It is also the source of a quiet class of bugs. A loop that hashes many inputs with one reused hash: ```go h := sha256.New() for _, item := range items { h.Write(item) digests = append(digests, h.Sum(nil)) // wrong: each covers all prior items } ``` Every digest after the first is over a growing concatenation. Nothing errors. Every value is a well-formed 32-byte digest and encodes to plausible hex. The fix is one line - `h.Reset()` at the top of each iteration - and `Reset` is a separate method precisely because `Sum` deliberately does not do it. ### Reset, Size and BlockSize - `Reset()` returns the hash to the state it had immediately after construction. For a keyed hash built by `hmac.New`, it resets to the initial **keyed** state - the key is not forgotten. - `Size()` tells you how many bytes `Sum` will append: 32 for SHA-256, matching the `sha256.Size` constant. Write generic code against `h.Size()` rather than a literal. - `BlockSize()` is the algorithm's internal block width (64 for SHA-256) and matters mainly to constructions layered on top of a hash. ### What Sum is not - It is not a comparison, an encoder, or a reset. - It does not consume its argument as input. `h.Sum(data)` is a common misreading and produces a digest that ignores `data` entirely while prepending it to the output. - Its result is not a fixed-size array. If you want a comparable value you must copy into one, or use the one-shot `sha256.Sum256` instead. ### The habit to build Read `h.Sum(x)` as "give me `x` with the digest stuck on the end, and leave the hash alone." Then, whenever a `hash.Hash` outlives a single message - a loop, a long-lived struct field, a pooled value - ask where the `Reset()` is. If you cannot point at it, the digests are wrong.

  • What exactly comes back from h.Sum([]byte("sha256:")) on a SHA-256 hash?
    A single `[]byte` of length 39: the seven bytes of `"sha256:"` followed by the 32 raw digest bytes. Ordinary `append` semantics apply, so if that slice had spare capacity the digest is written in place, otherwise a larger array is allocated. The digest bytes are binary - encode them before they reach a log or a filename.
  • You reuse one hash.Hash across many messages in a loop. What must you call, and why isn't Sum enough?
    `Reset()` at the start of each message. `Sum` is documented not to change the hash state, so without a reset the second message's digest covers message one and message two concatenated. The failure is silent: every digest is still 32 well-formed bytes, so only a comparison against an independently known value exposes it.
  • How do you know how many bytes Sum will append without hardcoding 32?
    Call `h.Size()`. It is part of the `hash.Hash` interface and returns the digest length for whatever algorithm is behind the interface - 32 for SHA-256, matching the `sha256.Size` constant, 64 for SHA-512. Generic helpers that accept a `hash.Hash` should size buffers from `h.Size()` rather than a literal.

saying these in an interview costs you the question

  • Thinks Sum hashes the slice passed to it
  • Thinks Sum resets the hash
  • Believes the hash is unusable once Sum has been called
  • Reuses one hash across messages without calling Reset
  • Expects Sum to return a [32]byte array
  • Hardcodes 32 instead of asking h.Size()