# Scan reads SQL rows straight into your structs, without owning the query

> scan is a small Go library that turns database/sql rows into structs, slices and primitive values, with strict tag based scanning and helpers for building queries with squirrel. Its own author calls the project complete and points new users at sqx for a broader solution.

**blockloop/scan** — Tiny lib to scan SQL rows directly to structs, slices, and primitive types

- Repository: https://github.com/blockloop/scan
- Stars: 614 · Forks: 33
- Language: Go
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/blockloop-scan

## A thin layer between database/sql rows and your struct fields

scan sits in the narrow gap between a query result and a Go value. You run a query with whatever database library you like, then hand the *sql.Rows to scan and get a populated struct or slice back. The module path is github.com/blockloop/scan/v2, so the import is short:

```go
import "github.com/blockloop/scan/v2"
```

The distinguishing choice is stated in the project's own Why section: many libraries support similar features, and scan differs by letting you use any database library, including the standard library or squirrel, to write fluent SQL and then pass the resulting rows to scan for the scanning step. The library does not build queries, does not own the connection, and does not know your schema. It only reads rows.

That narrowness is the appeal for anyone who has outgrown raw database/sql but does not want an ORM. It is also the reason the API is small enough to read in an afternoon. The repository root is a flat list of Go files: scanner.go, columns.go, values.go, interface.go and doc.go, with tests beside them and a vendor directory.

The dependency list is short. go.mod requires Go 1.17, github.com/proullon/ramsql for the tests, stretchr/testify for assertions, and golang.org/x/text.

## Rows and Row handle both collections and single values

Four entry points cover the common cases. scan.Rows fills a slice, scan.Row fills a single value, and both have strict variants.

Scanning multiple rows into a slice of structs looks like this:

```go
var persons []Person
err := scan.Rows(&persons, rows)
```

The same call works for a slice of primitives, which is the case that catches people out when they expect a slice of structs:

```go
var names []string
err := scan.Rows(&names, rows)
```

For a single row, scan.Row takes the address of one value. It works with a struct:

```go
var person Person
err := scan.Row(&person, rows)
```

and with a primitive such as an int8:

```go
var age int8
err := scan.Row(&age, rows)
```

Nothing in the API requires a particular query shape beyond the rows object you pass in, so scan.Row does not complain if the query returns zero rows, and it does not complain either if it returns several. What happens in those edge cases is determined by the scanning code rather than by a documented guarantee, so if you need strict single row semantics, check scanner.go rather than assuming.

One configuration knob exists and it is worth knowing about. AutoClose controls whether rows.Close() is called automatically once the scan completes, and it defaults to true.

## Strict scanning uses only db tags and ignores everything else

By default the library maps column names to and from field names using a basic title case conversion, and you can replace that behaviour by setting ColumnsMapper and ScannerMapper to custom functions.

RowsStrict and RowStrict are the safer choice when you want a fixed contract. The documentation states that any fields not tagged with the db tag will be ignored even if columns are found that match the field names. So a struct field named Name that the query returns as a column will silently stay zero unless you tag it, which is the behaviour you want in a service where a renamed column should fail loudly rather than quietly produce empty values.

Nested structs are supported as of v2.0.0. The example in the documentation declares a struct with a db tag on a nested field and scans it with RowStrict:

```go
var person struct {
	ID      int    `db:"person.id"`
	Name    string `db:"person.name"`
	Company struct {
		Name string `db:"company.name"`
	}
}
```

The dot in the tag value is what addresses a nested field, and it works with a joined query where the column aliases match the dotted path. That is a clean way to flatten a relational result into a nested Go value without writing a manual scan loop.

The trade-off with strict mode is that you must tag every field you care about, including fields you might consider obvious. There is no middle setting between the two behaviours.

## Columns and Values turn a struct into an insert statement

Two helpers work in the opposite direction to scanning. Columns scans a struct and returns the assumed column names, using the db tag when present and the field name otherwise. ColumnsStrict returns only the fields carrying a db tag. Both are variadic and both accept a string slice of column names to exclude, and the documentation recommends caching that slice.

```go
var nobody = new(User)
var userInsertCols = scan.Columns(nobody, "ID")
// []string{ "Name", "Age", "bday" }
```

Values does the same for the values themselves:

```go
vals := scan.Values([]string{"ID", "Name"}, user)
// []interface{}{ 1, "Brett" }
```

The documented use case for both is building a query with squirrel:

```go
sq.Insert(userCols...).
        Into("users").
        Values(scan.Values(userCols, &user)...)
```

The pairing works because Columns and Values take the same column list, so the ordering of the values matches the ordering of the column names. That is a nicer property than it first appears: it removes the need for a separate struct-to-value mapping in the write path.

Values also carries a caching mechanism. It uses a sync.Map to cache the exported field locations of a struct, and the first time a struct type is scanned its field locations are cached so later calls are faster. The documentation points at the benchmark section as evidence, noting that this is where the speedup shows up.

A subtlety worth flagging: Values is documented against a pointer to a struct, and the exclusion of a field uses the db tag value, so a field tagged with a dash is treated as not a column at all.

## The benchmarks in the readme are from one laptop, not a claim

The readme includes a benchmark run, and it is worth reading carefully rather than quoting the numbers. The command that produced it is:

```
$ go test -bench=. -benchtime=10s ./...
```

The output lists the machine it ran on: linux on amd64, an 11th Gen Intel Core i7-1165G7 at 2.80GHz. The reported figures cover the parts of the API that do real work. Columns on a large struct is listed at 288.0 ns/op, Values on a large struct at 1807 ns/op, scanning one row with one field at 2074 ns/op and with five fields at 2381 ns/op, and scanning ten rows of ten fields at 50431 ns/op. The run ends with PASS and an ok line for github.com/blockloop/scan at 92.374s.

Read those as one machine on one day, not as a comparison against any other library. There is no competing library in the benchmark, so the numbers tell you the relative cost of Columns against Values and of a single row against ten rows, which is the useful part: Values is roughly six times the cost of Columns on a large struct, and scanning many fields costs more than scanning one.

The benchmark file bench_scanner_test.go sits at the repository root, and the Makefile gives you the commands to reproduce the work locally.

## Development tooling, the integration test tag and a project the author calls complete

The Makefile is small and shows how the project expects to be worked on. The default goal is build, which compiles every Go file to a null output. The test target runs the suite with a build tag:

```
test:
	go test -tags=integration ./...
```

That integration tag matters. The go.mod file lists github.com/proullon/ramsql, an in-process SQL database, which is how the tests exercise real query execution without a server to install. Linting uses golangci-lint through the.bingo directory, and formatting uses gofumpt, also managed by bingo.

The most important fact about this repository is in a note at the top of the readme. The author states that the project is considered complete for all intents and purposes, that no changes are planned in the near future, and that no code has been written there since approximately 2021. He says that as far as he knows everything functions as intended, and he points readers who want a more comprehensive solution to the sqx project.

That note is consistent with the release history. The newest tag is v2.5.0 from 2023-08-31, preceded by v2.4.0 and v2.3.0 in the same week of August 2023, while the last push was on 2026-10-13. The repository is not archived, and the licence is MIT, so you can fork it and continue development as the author suggests. Read it as a frozen, working utility rather than a project awaiting features.

## Conclusion

scan fits a Go service that already speaks database/sql, writes its queries with squirrel, and wants a thin layer that maps columns to struct fields without adopting a whole ORM or a full sqlx style mapper. It does not fit a team that needs an actively developed dependency, because the author states no code has been written here since approximately 2021 and the newest tagged release is v2.5.0 from 2023-08-31. Before adopting it, run go test -tags=integration ./... and read scanner.go and columns.go for the column to field mapping rules, because those rules are the contract and the project explicitly leaves them frozen.

## FAQ

### How do I scan SQL rows into a slice of structs with the scan library?

Query with database/sql or squirrel as usual, then call scan.Rows with a pointer to your slice, for example scan.Rows(&persons, rows). The same function also fills a slice of primitive types such as []string when the query returns a single column.

### What is the difference between scan.Row and scan.RowStrict?

Row uses a title case conversion to match column names to field names, while RowStrict scans strictly based on the db tag. The documentation states that fields without a db tag are ignored in strict mode even when a matching column is found.

### Is the scan Go library still maintained?

The author describes the project as considered complete, states that no code has been written in it since approximately 2021, and points to sqx for a more comprehensive solution. The last push was on 2026-10-13 and the most recent tagged release is v2.5.0 from 2023-08-31, so treat it as a frozen utility you can fork.

## Sources

- [blockloop/scan on GitHub](https://github.com/blockloop/scan)
- [Issues](https://github.com/blockloop/scan/issues)
- [License: MIT](https://github.com/blockloop/scan/blob/master/LICENSE)
- [README](https://github.com/blockloop/scan/blob/master/README.md)
- [Releases](https://github.com/blockloop/scan/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/blockloop-scan
