For a [32]byte digest, how does hex.EncodeToString differ from fmt.Sprintf with %x?
answer
- both print the same 64 characters
- one takes a slice, not an array
- only one of them has a decoder
- case is the giveaway with %X
- %x on an int also looks like hex
basics
~10 sBoth produce the same 64 lowercase hex characters. encoding/hex takes a slice, so an array needs sum[:], and it ships a decoder with named errors; fmt reflects over any value and has no inverse.
solid answer
~40 sFor printing they are interchangeable: `hex.EncodeToString(sum[:])` and `fmt.Sprintf("%x", sum)` both give the same 64 lowercase characters for a 32-byte digest. The differences are about types, direction and cost. `encoding/hex` works on `[]byte`, so a `[32]byte` array has to be sliced, whereas `%x` reflects over whatever you hand it — including an `int`, which prints as hex too, so a wrong-typed argument yields plausible-looking short output instead of a compile error. Only `encoding/hex` has the reverse direction: `hex.DecodeString` returns `hex.ErrLength` for an odd-length string and a `hex.InvalidByteError` for a non-hex character. And `encoding/hex` always emits lowercase, while `%X` gives uppercase — worth pinning down in a contract, since `hex.DecodeString` happily accepts either case.
code
go · 9 linessum := sha256.Sum256([]byte("hello"))
a := hex.EncodeToString(sum[:]) // array must be sliced
b := fmt.Sprintf("%x", sum) // same 64 lowercase characters
c := fmt.Sprintf("%X", sum) // uppercase; encoding/hex never emits this
_ = a
_ = b
_ = cgo deeper
Know that both forms print the same lowercase hex, and that encoding/hex needs a slice — so a fixed-size digest array has to be sliced with sum[:].
Explain the real differences: reflection versus a typed API, the decode side with hex.ErrLength and hex.InvalidByteError, and the lowercase-only encoding against %X.
Show judgment about which representation belongs on a wire: hex when humans compare and paste it, base64 when size matters, and the case convention written into the contract either way.
Treat the representation as an interface other teams build against — once ids are hex and lowercase in logs, dashboards and support tooling, switching to base64 is a migration, not a refactor.
## The two ways to print bytes as hex Go gives you a dedicated codec package and a formatting verb, and for a digest they produce identical text: ```go sum := sha256.Sum256([]byte("hello")) a := hex.EncodeToString(sum[:]) // 64 lowercase characters b := fmt.Sprintf("%x", sum) // the same 64 characters ``` That equivalence is real and worth knowing, because it means a value logged with `%x` can be fed straight to `hex.DecodeString` on the other side. Everything else about the two differs. ## Types `hex.EncodeToString` has the signature `func EncodeToString(src []byte) string`. It takes a **slice**. A `[32]byte` is an array — a different type — so `hex.EncodeToString(sum)` does not compile; you write `sum[:]`. This trips people constantly, because `sha256.Sum256` returns an array, not a slice. `fmt`'s `%x` verb goes through reflection and accepts almost anything: a byte slice, a byte array, a string (hex of its bytes), and an integer (hex of the number). That flexibility is the hazard — `fmt.Sprintf("%x", n)` where `n` is an `int` compiles fine and prints something short and hex-looking, so a mis-typed argument produces a wrong identifier rather than a build failure. ## Direction `fmt` has no decoder. There is a `Sscanf` with `%x`, but nothing that round-trips a digest cleanly. `encoding/hex` owns both directions: - `hex.EncodeToString(src []byte) string` and `hex.DecodeString(s string) ([]byte, error)` - `hex.Encode(dst, src []byte) int` and `hex.Decode(dst, src []byte) (int, error)` for writing into a buffer you already own - `hex.EncodedLen(n) == 2*n` and `hex.DecodedLen(n) == n/2` to size that buffer - `hex.NewEncoder(w io.Writer) io.Writer` and `hex.NewDecoder(r io.Reader) io.Reader` for streams — and note the hex encoder is a plain `io.Writer` with nothing to flush, because one byte always maps to exactly two characters with no residue The decode errors are specific and worth naming in a triage conversation: an odd-length input returns `hex.ErrLength` ("odd length hex string"), and a character outside `0-9a-fA-F` returns a `hex.InvalidByteError` carrying the offending byte. Decoding is case-insensitive; encoding is always lowercase. ## Cost `hex.EncodeToString` allocates one string of exactly `2*len(src)` bytes and fills it with a tight loop. `fmt.Sprintf` additionally builds an argument slice, boxes the value into an `interface{}`, and dispatches on its dynamic type through reflection. For a line of log output, nobody cares. Inside a loop that fingerprints millions of records, use `encoding/hex` — and if even the string allocation matters, `hex.Encode` into a reused `[]byte` sized by `hex.EncodedLen` avoids it. ## Choosing hex over base64 at all Hex costs two characters per byte where base64 costs four per three — 100% inflation against 33%. A 32-byte digest is 64 hex characters, 44 padded base64 characters, or 43 unpadded. You pay that for three things: 1. **Interoperability with everything else that prints hashes.** Checksums, git object ids, database blob literals and debugger output are all hex; a hex digest can be pasted straight into a comparison. 2. **No variant confusion.** There is one hex alphabet. Base64 has two alphabets and a padded and unpadded form of each, and the wrong pick is a decode error at a partner's end. 3. **Byte alignment.** Each byte occupies its own two characters, so a prefix of the hex string is exactly a prefix of the bytes. That is why short prefixes of a hex id are usable as an abbreviation, and why the same trick does not work cleanly on base64, where a character boundary need not be a byte boundary. When the payload is large or the identifier is going into a URL, base64 wins on size. When the value will be read, compared or pasted by a human, hex usually wins. ## What an interviewer is listening for That you know the two produce the same text, that you know why the array needs slicing, that you can name what `hex.DecodeString` rejects, and that you treat `%x` on an untyped argument as a risk rather than a convenience.
- What does hex.DecodeString return for an odd-length string?It returns `hex.ErrLength`, the sentinel for "odd length hex string", along with the bytes it managed to decode from the complete pairs. Treat the result as unusable on error rather than keeping the partial prefix.
- Can hex.DecodeString read an uppercase digest?Yes. Decoding accepts `0-9`, `a-f` and `A-F`, so a digest printed with `%X` round-trips fine. Encoding, however, is always lowercase — `encoding/hex` has no uppercase mode — so a system that must emit uppercase does it with `%X` or an explicit upper-casing step.
- Why is fmt.Sprintf("%x", v) risky when v might not be a byte slice?Because `%x` also formats integers, strings and arrays, all without complaint. Passing a length, an index or a wrapped value instead of the bytes produces short but perfectly plausible hex, so the mistake surfaces as a wrong identifier downstream rather than as a compile or format error.
- How do you hex-encode repeatedly without allocating a string each time?Size a destination once with `hex.EncodedLen(len(src))`, keep that `[]byte` around, and call `hex.Encode(dst, src)` for each value; it writes into `dst` and returns the number of bytes written. This is the path to take inside a hot loop that fingerprints many records.
saying these in an interview costs you the question
- Passes a [32]byte array to hex.EncodeToString
- Thinks encoding/hex can emit uppercase
- Assumes hex.DecodeString rejects uppercase input
- Uses %x on an int and reads it as a digest
- Believes fmt can decode what %x printed