Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/uhex"
Encodes unsigned integers and fixed-size byte arrays as zero-padded lowercase hexadecimal, at widths known at compile time.
id := uhex.Hex64(traceID) // 16 bytes
sum := uhex.Hex32(checksum) // 8 bytes
var dst [16]byte
uhex.Hex64UB(traceID, &dst) // no allocation
The naming pattern
Four suffixes, orthogonal along two axes:
| Suffix | Input | Output |
|---|---|---|
| none | unsigned integer | returns []byte |
UB | unsigned integer | writes into a caller buffer |
B | fixed-size byte array | returns []byte |
BB | fixed-size byte array | writes into a caller buffer |
Integer helpers always zero-pad to the full width of the type: Hex64 gives 16 characters, Hex32 8, Hex16 4, Hex8 2. Byte-array helpers encode each input byte in order, two characters per byte.
Measured
| Function | ns/op | B/op | allocs/op |
|---|---|---|---|
Hex8UB | 0.66 | 0 | 0 |
Hex8B | 0.68 | 0 | 0 |
Hex8 | 0.71 | 0 | 0 |
Hex8BB | 0.72 | 0 | 0 |
Hex16BB | 0.87 | 0 | 0 |
Hex32UB | 1.27 | 0 | 0 |
Hex32BB | 1.53 | 0 | 0 |
Hex16UB | 1.69 | 0 | 0 |
Hex64UB | 2.07 | 0 | 0 |
Hex64BB | 2.56 | 0 | 0 |
Hex16 | 4.33 | 0 | 0 |
Hex16B | 4.50 | 0 | 0 |
Hex32 | 4.74 | 0 | 0 |
Hex32B | 4.79 | 0 | 0 |
Hex64B | 5.61 | 0 | 0 |
Hex64 | 5.84 | 0 | 0 |
Hex64Escaping | 12.37 | 16 | 1 |
Hex64BEscaping | 12.66 | 16 | 1 |
Measured with go test -bench on Go 1.27.1, linux/amd64, Intel Core i7-1260P. Run go test -bench=. ./pkg/uhex/ to reproduce them on your own hardware; the absolute values will differ, the ratios much less so.
Escape analysis is the whole story
Look at the last two rows against the rest.
Hex64 costs 5.84 ns and zero allocations when the returned slice does not escape the caller. The same function costs 12.37 ns and one 16-byte allocation when it does. The slice-returning helpers fill a local array and return a slice over it; whether that array lands on the stack or the heap is decided by escape analysis at each call site.
So the cost of Hex64 is not a property of Hex64. It depends on what you do with the result. Read the bytes and discard them, and it is free. Store the slice in a longer-lived structure, return it, send it on a channel, or pass it to a function the compiler cannot inline, and you pay an allocation.
The UB and BB variants remove the question. They write into a caller-owned array and never allocate, regardless of the call site. Hex64UB at 2.07 ns is roughly a third the cost of Hex64 even in the non-escaping case, because there is no slice header to construct.
For code that must not allocate, use the buffer-writing forms and stop reasoning about escape analysis.
Why it is fast
Every width is known at compile time, so the encoders are fully unrolled: no loops, no length checks, no argument validation. Each input byte is translated through a 256-entry lookup table and written with a single 16-bit store.
The cost scales linearly with output width and is independent of the input value, because there are no data-dependent branches. Hex8UB at 0.66 ns is a couple of instructions.
Against the standard library
The buffer-writing helpers run roughly two to three times faster than encoding/hex, which also allocates nothing in its Encode form.
Part of that gap is structural rather than a matter of implementation quality. encoding/hex encodes byte slices only, so encoding an integer with it requires serialising the value into a scratch buffer first. The integer helpers here read the value directly.
Against fmt.Sprintf("%x", v) the difference is about an order of magnitude, plus the allocations reflection-based formatting incurs.
encoding/hex covers what this does not: arbitrary-length data, streaming, and decoding. This package is specialised for small fixed widths up to eight bytes, and for that range it is unrolled in a way a general encoder cannot be.
Dependencies
This package reaches no external module: it uses only the Go standard library.