What does scanning a column into sql.RawBytes give you, and what rules must you follow to use it safely?
answer
- Scan usually copies; this destination does not
- the driver owns that memory
- valid only until the row moves on
- copy it or lose it
- nil slice means the column was NULL
basics
~20 ssql.RawBytes aliases memory owned by the driver, so Scan skips the copy a *[]byte or *string destination makes. The bytes stay valid only until the next Next, Scan or Close call, must not be mutated, and are nil for NULL.
solid answer
~50 s`sql.RawBytes` is a `[]byte` that, after `Scan`, points at memory the **driver** owns rather than at a copy. That is the whole benefit: on a back-office dump streaming millions of raw column values, scanning into `*[]byte` or `*string` allocates and copies once per column per row, and `sql.RawBytes` does not. The price is a hard lifetime rule — the bytes are only valid until the next `rows.Next`, `rows.Scan` or `rows.Close`, so you must consume or copy them inside the loop iteration, with `string(b)` or `bytes.Clone(b)`. You must also not mutate the slice, it is rejected by the `Scan` of the `*sql.Row` that `QueryRow` returns, and a NULL column simply leaves it `nil` with no error — which makes it one of the few destinations that accepts NULL without a wrapper type. Use it only where you have measured the copy mattering; otherwise a plain destination is safer.
code
go · 18 linesvar status, note sql.RawBytes
for rows.Next() {
if err := rows.Scan(&status, ¬e); err != nil {
return err
}
// A NULL column leaves the RawBytes nil, with no error.
if status == nil {
fmt.Fprint(out, "NULL")
} else {
// Safe: written before the next rows.Next call.
out.Write(status)
}
// Keeping a value past this iteration requires a copy:
// kept = append(kept, bytes.Clone(note))
}go deeper
Know that this destination borrows the driver's memory instead of copying, and that most code should use a plain []byte or string destination unless there is a specific reason not to.
Explain the lifetime rule precisely — valid until the next Next, Scan or Close — plus the no-mutation rule, and show the copy you would make with bytes.Clone or a string conversion to keep a value.
Show the judgment: measure before adopting it, confine it to code that consumes each value within the iteration, and recognise the silent wrong-data failure mode when a retained slice is reused by the driver.
Own whether this optimisation is allowed in the codebase at all. Weigh a measured allocation saving in one exporter against a borrowed-memory hazard that reviewers must catch every time, and decide where it may appear.
## What sql.RawBytes actually is `sql.RawBytes` is declared as a byte slice, but its contract is unusual: after a successful `Scan`, it points at memory **owned by the database driver**, not at a copy made for you. Every other byte-ish destination copies. `*[]byte` gets a fresh slice; `*string` gets a newly allocated string; `*any` gets a copied `[]byte`. `*sql.RawBytes` gets the driver's own buffer. That is the entire reason the type exists: it removes an allocation and a copy per column per row. ## The rules that come with it **Lifetime.** The bytes are valid only until the next call to `rows.Next`, `rows.Scan` or `rows.Close` on the same `*sql.Rows`. After that the driver is free to overwrite the buffer with the next row. So inside the loop body you may read, compare, hash or write the bytes; you may not stash the slice in a struct, append it to a slice of results, or hand it to a goroutine that outlives the iteration. If you need it later, copy: `string(b)` allocates a copy, and `bytes.Clone(b)` gives an independent `[]byte`. **No mutation.** You are looking at the driver's buffer. Writing through the slice corrupts state you do not own. **Not on Row.Scan.** The `*sql.Row` returned by `QueryRow` closes its underlying rows as part of `Scan`, so the borrowed memory would be invalid the instant `Scan` returned. `Row.Scan` therefore refuses a `*sql.RawBytes` destination and returns an error rather than handing you a dangling slice. `sql.RawBytes` is a `Rows`-loop tool only. **NULL is nil.** A NULL column scanned into `sql.RawBytes` leaves it `nil` and returns no error. This makes it, along with `*[]byte` and `*any`, one of the few destinations that accepts NULL without a wrapper — and it distinguishes NULL (`b == nil`) from an empty value (`len(b) == 0` but `b != nil`), though whether a given driver preserves that distinction for empty strings is worth checking before you rely on it. ## Where it earns its place The honest use case is narrow: a tool that streams raw column values straight out again without keeping them. An internal back-office exporter dumping a wide legacy table — half of whose columns are nullable — writes each value to an `io.Writer` inside the loop and never retains anything, which is exactly the shape the lifetime rule permits. At a few million rows across a dozen columns, the copies you avoid are real. Outside that shape it is a poor trade. In a normal handler that reads a row into a struct and returns it, the values must outlive the loop, so you would copy them anyway — at which point `sql.RawBytes` has bought nothing and added a footgun. Reach for it after a benchmark with `-benchmem` shows the scan copies dominating, not before. ## The same rule, without the type The ownership rule is not special to `sql.RawBytes`. Any custom `sql.Scanner` whose `Scan(src any)` receives a `[]byte` is being handed driver-owned memory under the same terms, and must copy anything it retains. `sql.RawBytes` merely makes the borrowing explicit and visible in the destination's type rather than leaving it as a paragraph of documentation people skip. Reviewing a `Scan` implementation, "where does this `[]byte` go after the method returns?" is the question to ask. ## How this fails in production The failure mode is nasty because it is not a crash. Code appends `sql.RawBytes` values into a result slice; under light load the driver happens to allocate a fresh buffer per row and everything looks correct; under load, or after a driver upgrade that starts reusing buffers, every entry in the result slice ends up showing the last row's bytes, or a mixture. There is no panic and no error — just wrong data, appearing only at volume. The race detector will not find it either, because nothing here is a data race between goroutines; it is a lifetime bug in one goroutine. That asymmetry — cheap when correct, silently wrong when not — is why the reviewing question for `sql.RawBytes` is always the same: does this value get used and dropped before the next `rows.Next`, and if not, where is the copy?
- Why does the Scan of the *sql.Row returned by QueryRow reject a *sql.RawBytes destination?Because that `Scan` closes the underlying rows before it returns, releasing the driver memory the slice would alias. Handing back a slice into freed or reused memory would be a silent corruption, so it returns an error instead. `sql.RawBytes` is only usable inside a `rows.Next` loop, where the caller can see the lifetime.
- How would you know whether using sql.RawBytes is worth it here?Measure. Write a benchmark over a representative row count with `-benchmem` and compare the plain destination against `sql.RawBytes`; if allocations per row barely move, or the query time dominates, keep the plain destination. The type is only worth its lifetime hazard when scan-time copying is a measured cost, which in practice means wide rows at high volume.
- Does the same ownership rule apply to the []byte handed to a custom Scan method?Yes. A `[]byte` reaching `Scan(src any)` is driver-owned on exactly the same terms and may be reused for the next row, so anything you retain must be copied — with `string(src)` or `bytes.Clone(src)`. `sql.RawBytes` just makes that borrowing visible in the type instead of leaving it to documentation.
saying these in an interview costs you the question
- Appends sql.RawBytes values into a slice that outlives the loop
- Thinks sql.RawBytes is just a shorter name for []byte
- Mutates the slice in place to normalise the value
- Expects an error when a NULL is scanned into it
- Uses it in a normal handler without measuring the copy cost