NetLife Guru

Open source Go packages for fast, maintainable web systems. Built with a documentation-first approach.

Product
OverviewGolang packagesNews
Documentation
DocumentationGo LoggerGo RouterGo DB Form
Company
OverviewContactNewsGitHub
Community / Support
Supportinfo@netlife.guru
© 2026 NetLife Guru. All rights reserved.
GitHubinfo@netlife.guru
NetLife GuruNetLife GuruNetLife Guru
NetLife GuruNetLife GuruNetLife Guru
OverviewDocumentationNewsSupportContact

Golang packages

About
ScanningMaps and RowsConvertersCacheErrors
MappingDynamic RowsEdge Cases
MapperAPI

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:

FunctionUse case
ScanStructRowsStream rows one by one through a callback
ScanStructSliceScan all rows into a []T slice
ScanStructOneScan exactly one row
ScanStructRowsWithCacheKeyStream 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:

[]User

Example:

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:

*User

Example:

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:

  • db tags
  • json tags
  • Go field names
  • snake_case fallback
  • direct scanning for simple field types
  • assignment through AssignValue for 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 users

Result:

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:

  • string
  • bool
  • 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

NeedUse
Load all rows into a sliceScanStructSlice
Process rows one by oneScanStructRows
Require exactly one rowScanStructOne
Optimize repeated scans with a named keyScanStructRowsWithCacheKey

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
}

FillFromMap

Use FillFromMap to fill a Go struct from an existing map.

Maps and Rows

API reference for scanning rows into maps, filling structs from maps, and using typed row helpers.

On this page

The Rows InterfaceExample StructScanStructSliceScanStructRowsScanStructOneDo Not Reuse Consumed RowsHow Fields Are AssignedDirect and Indirect ScanningScanStructRowsWithCacheKeyChoosing the Right FunctionRecommended Usage