Scanning
Scan database rows into Go structs using ScanStructRows, ScanStructSlice, and ScanStructOne.
The scanning API is the main way to convert database rows into typed Go structs.
Mapper reads the column names from Rows.Columns(), builds a scan plan for the target struct, scans each row, and assigns matching values into struct fields.
The main scanning functions are:
| Function | Use case |
|---|---|
ScanStructRows | Stream rows one by one through a callback |
ScanStructSlice | Scan all rows into a []T slice |
ScanStructOne | Scan exactly one row |
ScanStructRowsWithCacheKey | Stream rows using a named scan-plan cache key |
The Rows Interface
All scanning functions work with the mapper.Rows interface.
type Rows interface {
Next() bool
Scan(dest ...any) error
Err() error
Close() error
Columns() ([]string, error)
}Go’s standard *sql.Rows already provides these methods, so it can be used directly with mapper.
Other database drivers can be supported by adapting their row type to this interface.
Example Struct
The examples on this page use the following struct:
type User struct {
ID string `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
Active bool `db:"active"`
CreatedAt time.Time `db:"created_at"`
}The db tags tell mapper which database columns should be assigned to each field.
ScanStructSlice
Use ScanStructSlice when you want to load all rows into memory as a typed slice.
users, err := mapper.ScanStructSlice[User](rows)
if err != nil {
return err
}This returns:
[]UserExample:
rows, err := db.Query(`
SELECT *
FROM users
ORDER BY created_at DESC
`)
if err != nil {
return err
}
defer rows.Close()
users, err := mapper.ScanStructSlice[User](rows)
if err != nil {
return err
}
for _, user := range users {
fmt.Println(user.Name)
}Use this when:
- the result set is reasonably small
- you need all results at once
- the caller expects a
[]T - you want the simplest API
When there are no rows, ScanStructSlice returns an empty slice.
ScanStructRows
Use ScanStructRows when you want to process rows one by one.
err := mapper.ScanStructRows[User](rows, func(user *User) error {
fmt.Println(user.Name)
return nil
})The callback is called once for every row.
Example:
rows, err := db.Query(`
SELECT *
FROM users
ORDER BY created_at DESC
`)
if err != nil {
return err
}
defer rows.Close()
err = mapper.ScanStructRows[User](rows, func(user *User) error {
fmt.Println(user.ID, user.Name)
return nil
})
if err != nil {
return err
}Use this when:
- the result set may be large
- you want to avoid storing all rows in memory
- you want to stream rows into another system
- you want to stop early by returning an error from the callback
If the callback returns an error, scanning stops and that error is returned.
ScanStructOne
Use ScanStructOne when a query must return exactly one row.
user, err := mapper.ScanStructOne[User](rows)
if err != nil {
return err
}ScanStructOne returns a pointer:
*UserExample:
rows, err := db.Query(`
SELECT *
FROM users
WHERE id = ?
LIMIT 1
`, userID)
if err != nil {
return err
}
defer rows.Close()
user, err := mapper.ScanStructOne[User](rows)
if err != nil {
return err
}
fmt.Println(user.Name)When no rows are returned, ScanStructOne returns mapper.ErrNoRows.
user, err := mapper.ScanStructOne[User](rows)
if errors.Is(err, mapper.ErrNoRows) {
return nil
}
if err != nil {
return err
}When more than one row is returned, ScanStructOne returns mapper.ErrTooManyRows.
user, err := mapper.ScanStructOne[User](rows)
if errors.Is(err, mapper.ErrTooManyRows) {
return errors.New("expected only one user")
}
if err != nil {
return err
}Use this when:
- the query is expected to return one row
- you are loading by primary key
- you are checking a unique value
- multiple rows should be treated as an error
Do Not Reuse Consumed Rows
Rows are consumed while scanning.
This means you should not scan the same rows value twice.
users, err := mapper.ScanStructSlice[User](rows)
if err != nil {
return err
}
// Do not do this with the same rows value.
// The rows have already been consumed.
err = mapper.ScanStructRows[User](rows, func(user *User) error {
return nil
})Run the query again if you need to scan the result in a different way.
How Fields Are Assigned
Mapper matches returned column names to exported struct fields.
The field matching rules are described in the Mapping guide.
In short, mapper supports:
dbtagsjsontags- Go field names
- snake_case fallback
- direct scanning for simple field types
- assignment through
AssignValuefor pointers, nullable structs, JSON fields, and other indirect values
Example:
type User struct {
ID string `db:"id"`
CreatedAt time.Time `db:"created_at"`
}Column names:
SELECT id, created_at FROM usersResult:
User{
ID: "u_123",
CreatedAt: time.Now(),
}Direct and Indirect Scanning
Mapper uses a scan plan internally.
Simple fields can be scanned directly into the struct field.
Examples:
stringbool- integer types
- unsigned integer types
- floating point types
time.Time[]byte
More complex fields are scanned into temporary values first and then assigned with AssignValue.
Examples:
- pointer fields
- nullable-style structs
- slices from JSON
- maps from JSON
- values requiring conversion
You do not normally need to manage this manually.
ScanStructRowsWithCacheKey
ScanStructRowsWithCacheKey works like ScanStructRows, but uses a named cache key for the scan plan.
err := mapper.ScanStructRowsWithCacheKey[User](
rows,
"users:list",
func(user *User) error {
fmt.Println(user.Name)
return nil
},
)This is useful for hot paths where the same query shape is scanned repeatedly.
Use a stable cache key for a stable result shape.
More details are covered in the Cache API page.
Choosing the Right Function
| Need | Use |
|---|---|
| Load all rows into a slice | ScanStructSlice |
| Process rows one by one | ScanStructRows |
| Require exactly one row | ScanStructOne |
| Optimize repeated scans with a named key | ScanStructRowsWithCacheKey |
Recommended Usage
For most list queries:
users, err := mapper.ScanStructSlice[User](rows)For large exports or streaming workflows:
err := mapper.ScanStructRows[User](rows, func(user *User) error {
return processUser(user)
})For primary-key lookups:
user, err := mapper.ScanStructOne[User](rows)
if errors.Is(err, mapper.ErrNoRows) {
return nil
}
if err != nil {
return err
}