skip to content

How do you implement sql.Scanner and driver.Valuer so a custom type round-trips a nullable column?

level: middleimportance: should knowfreq 42%

answer

  1. two interfaces, one per direction
  2. Scan takes any; Value returns driver.Value
  3. nil means NULL on both sides
  4. the driver owns any []byte you receive
  5. pointer receiver to store, value receiver to emit

basics

~20 s

Implement Scan(src any) error on a pointer receiver, type-switching over the driver's values and treating a nil src as NULL. Implement Value() (driver.Value, error) on a value receiver, returning nil for NULL or one of driver.Value's types otherwise.

solid answer

~50 s

The two interfaces are mirror images. `sql.Scanner` is `Scan(src any) error`, declared on a pointer receiver so it can store into the caller's variable; `src` arrives as one of the driver's value types — `int64`, `float64`, `bool`, `[]byte`, `string`, `time.Time` — or `nil` when the column is NULL, so the implementation type-switches and returns a clear error naming `%T` for anything unexpected. Two rules bite: handle the `nil` case explicitly, and never retain a `[]byte` you were handed, because that memory belongs to the driver and is reused — convert it to a `string` or copy it with `bytes.Clone`. `driver.Valuer` is `Value() (driver.Value, error)`, returning `nil` for NULL or one of those same types otherwise; declare it on the value receiver, so a nil pointer of your type is stored as NULL rather than invoking the method with a nil receiver. Pin both with compile-time assertions.

code

go · 31 lines
go
type Status struct {
	Code  string
	Valid bool // false means the column was NULL
}

var _ sql.Scanner = (*Status)(nil)
var _ driver.Valuer = Status{}

func (s *Status) Scan(src any) error {
	switch v := src.(type) {
	case nil:
		*s = Status{}
		return nil
	case string:
		*s = Status{Code: v, Valid: true}
		return nil
	case []byte:
		// string(v) copies: the driver owns and reuses v.
		*s = Status{Code: string(v), Valid: true}
		return nil
	default:
		return fmt.Errorf("scan status: unsupported source type %T", src)
	}
}

func (s Status) Value() (driver.Value, error) {
	if !s.Valid {
		return nil, nil // writes SQL NULL
	}
	return s.Code, nil
}

go deeper

for a junior

Be able to name the two interfaces and their signatures: Scan(src any) error for reading, Value() (driver.Value, error) for writing, with nil meaning SQL NULL on both sides.

for a middle

Explain the mechanics: the small set of driver value types, the type switch with an informative default, why Scan needs a pointer receiver, and why a []byte source must be copied before you keep it.

for a senior

Demonstrate the production details — the value receiver on Value so a nil pointer becomes NULL, compile-time interface assertions, and a round-trip test that covers the NULL case and both string and []byte sources.

for a principal

Own the decision of which domain types get this treatment. Pushing conversion into one Scanner/Valuer pair removes it from every call site, but it also hides behaviour from readers, so set the bar for when a custom type is warranted over a standard wrapper.

## Two interfaces, one per direction `database/sql` converts between driver values and Go values through two tiny interfaces: ```go type Scanner interface { Scan(src any) error } // database/sql type Valuer interface { Value() (Value, error) } // database/sql/driver ``` Implement `Scanner` and your type becomes a legal `Scan` destination. Implement `Valuer` and it becomes a legal query argument. Implement both consistently and the type round-trips. This is exactly how `sql.NullString` and friends are built; writing your own is how you get a domain type — a status enum, a money amount, a tri-state flag — to sit directly in your row structs instead of being unwrapped at every call site. ## The value types you must handle `driver.Value` is deliberately tiny. Drivers produce, and are given, only: - `int64` - `float64` - `bool` - `[]byte` - `string` - `time.Time` - `nil` — which is how SQL NULL is represented So `Scan` should type-switch over the cases the column can actually produce, and return an informative error for the rest. Two practical notes. First, be generous about `string` versus `[]byte`: which one a driver hands you for a text column varies, and code that handles only one breaks when the driver changes. Second, the default branch should include the offending type — `fmt.Errorf("scan status: unsupported source type %T", src)` — because a bare "cannot scan" tells nobody anything at 3am. ## The nil case is the whole point on a nullable column When the column is NULL, `src` is `nil`. Your `Scan` must decide what that means for your type and set it, usually by assigning the zero value or an explicit "unset" state. Forgetting the case is the classic bug: the type-switch falls to `default` and every NULL row fails with "unsupported source type <nil>", which reads like a driver problem and is not. In the other direction, `Value` returns `nil` to write a NULL. Anything else must be one of the types above — returning a custom struct, a `map`, or an `int` (rather than `int64`) is rejected. ## Receivers, and why they differ `Scan` **must** be on a pointer receiver: it stores a result, and a value receiver would mutate a copy. Callers then pass `&v`, and the method set rule means only `*T` satisfies `sql.Scanner`. `Value` should be on a **value** receiver. The reason is a nice piece of `database/sql` behaviour: when it is given a nil pointer whose element type implements `driver.Valuer`, it stores NULL directly instead of calling the method. If you declare `Value` only on the pointer receiver, that shortcut does not apply and your method is invoked with a nil receiver, which panics the moment it touches a field. A value receiver also means both `T` and `*T` can be used as arguments. Lock both down at compile time: ```go var _ driver.Valuer = Status{} var _ sql.Scanner = (*Status)(nil) ``` ## Never keep the bytes you were given When `src` is a `[]byte`, the memory belongs to the driver and may be reused for the next row. Converting with `string(v)` copies, and so does `bytes.Clone(v)`. What is **not** safe is storing the slice itself in your type and reading it later: by then it may hold another row's bytes, or nothing coherent at all. This is the same ownership rule that `sql.RawBytes` makes explicit, and it applies to every custom `Scanner` whether or not you ever mention that type. ## Round-trip fidelity The pair is only useful if it is symmetric. If `Value` writes an uppercase code, `Scan` must accept an uppercase code; if `Value` writes `nil` for the unset state, `Scan` must map `nil` back to that same unset state. The cheap way to keep them honest is a table-driven test that runs each interesting value through `Value` and back through `Scan` and asserts the result equals the original — including the NULL case, which is the one people forget precisely because it has no interesting value to check. ## When not to write one If all you need is "this column may be NULL", the standard wrappers or a pointer destination already do it, with no code to maintain. Write the pair when the type also carries conversion logic — a domain enum stored as text, a duration stored as an integer, a value with an invariant you want enforced at the boundary — so that the conversion lives in exactly one place instead of at every scan site.

  • Why declare Value on a value receiver rather than a pointer receiver?
    Because `database/sql` special-cases a nil pointer whose element type implements `driver.Valuer` and writes NULL without calling the method. With a pointer-receiver-only `Value`, that shortcut does not apply and your method is called with a nil receiver, panicking as soon as it reads a field. A value receiver also lets both `T` and `*T` be passed as arguments.
  • Your Scan receives a []byte. Why can't you just store the slice in your struct?
    The backing array belongs to the driver and may be reused for the next row, so a retained slice can silently change contents or become garbage. Copy before keeping it: `string(v)` allocates a copy, and `bytes.Clone(v)` gives you an independent `[]byte`. Retaining the original is the kind of bug that only shows up under load, when rows arrive fast enough to reuse the buffer.
  • How would you test that the pair really round-trips?
    A table-driven unit test with no database: for each interesting value call `Value()`, feed the result straight into `Scan` on a fresh variable, and assert it equals the original — including the unset case, where `Value` must return `nil` and `Scan` must map `nil` back to unset. Add a case feeding `[]byte` as well as `string` so a driver change does not break you.

saying these in an interview costs you the question

  • Declares Scan on a value receiver and wonders why nothing is stored
  • Omits the nil case, so every NULL row errors as an unsupported type
  • Returns a custom struct or an int from Value
  • Retains the []byte handed to Scan instead of copying it
  • Implements only one of the two interfaces and expects a round trip