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
MappingDynamic RowsEdge Cases
Mapper

Edge Cases

Understand how mapper handles null values, missing columns, extra columns, type conversions, pointers, JSON fields, and row count errors.

Mapper is designed to handle common database scanning edge cases without requiring manual scan logic in every query.

This page summarizes how mapper behaves when values are missing, nullable, extra, converted, or not assignable.

Overview

CaseBehavior
NULL valuesAssigned to pointers, nullable structs, maps, or left as zero value where applicable
Extra columnsIgnored when no matching struct field exists
Missing columnsMatching struct fields keep their zero value
Type conversionsCommon numeric, string, boolean, time, slice, and map conversions are handled by AssignValue
Pointer fieldsMapper allocates and assigns pointer values when the source value is not NULL
JSON fieldsJSON strings or byte slices can be assigned into slices and maps
Empty resultScanStructSlice returns an empty slice, ScanStructOne returns ErrNoRows
Too many rowsScanStructOne returns ErrTooManyRows
Invalid assignmentMapper returns an error when a value cannot be assigned to the target field

Null Values

When a database value is NULL, mapper handles it according to the target field type.

type User struct {
	ID    string  `db:"id"`
	Email *string `db:"email"`
}

If email is NULL, the Email field remains nil.

If email contains a value, mapper allocates the pointer and assigns the value.

if user.Email != nil {
	fmt.Println(*user.Email)
}

For non-pointer primitive fields, a NULL value leaves the field unchanged, usually its zero value.

type User struct {
	ID     string `db:"id"`
	Active bool   `db:"active"`
}

If active is NULL, the field remains false.

Nullable Structs

Mapper supports nullable-style structs with a Valid field and a supported value field.

Supported value field names are:

  • String
  • Time
  • Bool
  • Int64
  • Float64

Example:

type NullString struct {
	String string
	Valid  bool
}

type User struct {
	ID    string     `db:"id"`
	Email NullString `db:"email"`
}

If email is NULL, the nullable struct is reset to its zero value.

If email contains a value, mapper assigns the value and sets Valid to true.

if user.Email.Valid {
	fmt.Println(user.Email.String)
}

This works with nullable structs that follow the same field shape, including standard-library-like nullable types.

Extra Columns

Extra columns are ignored when no matching struct field exists.

type User struct {
	ID   string `db:"id"`
	Name string `db:"name"`
}

Query:

SELECT id, name, created_at FROM users

The created_at column is ignored because the struct does not define a matching field.

This makes mapper safe to use with:

  • SELECT *
  • joined queries
  • aliased queries
  • queries that return additional computed values

Missing Columns

If a result set does not contain a column for a struct field, that field keeps its zero value.

type User struct {
	ID     string `db:"id"`
	Name   string `db:"name"`
	Active bool   `db:"active"`
}

Query:

SELECT id, name FROM users

The Active field is not present in the result set, so it remains false.

This behavior is useful for partial queries and read models.

Type Conversions

Mapper uses AssignValue to assign scanned values into struct fields.

It supports common conversions between database values and Go types.

Examples include:

Source valueTarget field
[]bytestring
stringstring
numeric valuesint, int64, uint, float64, and related numeric types
boolbool
time.Timetime.Time
string or []byte JSONslices and maps
non-NULL valuespointer fields

Example:

type User struct {
	ID        int64     `db:"id"`
	Name      string    `db:"name"`
	CreatedAt time.Time `db:"created_at"`
}

If a value cannot be converted safely, mapper returns an error.

Boolean Values

Boolean assignment depends on the API being used.

When assigning directly into struct fields, mapper expects a real boolean value for bool fields.

type User struct {
	Active bool `db:"active"`
}

For dynamic row maps, the helper functions are more flexible.

active, ok := mapper.AsBool(row["active"])

AsBool accepts booleans, numeric values, and common string values such as:

  • true
  • false
  • 1
  • 0
  • yes
  • no

Pointer Fields

Pointer fields are useful for optional database values.

type User struct {
	ID        string     `db:"id"`
	Email     *string    `db:"email"`
	DeletedAt *time.Time `db:"deleted_at"`
}

When the source value is not NULL, mapper creates and assigns the pointer value.

When the source value is NULL, the pointer remains nil.

if user.DeletedAt == nil {
	fmt.Println("not deleted")
}

JSON Fields

Mapper can assign JSON strings or byte slices into slice and map fields.

type User struct {
	ID       string            `db:"id"`
	Tags     []string          `db:"tags"`
	Metadata map[string]string `db:"metadata"`
}

The database driver must return the JSON value as a string or []byte.

Example database values:

["admin", "active"]
{"source":"import","role":"admin"}

Mapper unmarshals the JSON into the target slice or map.

If the JSON is invalid, mapper returns an error.

Empty Results

ScanStructSlice returns an empty slice when there are no rows.

users, err := mapper.ScanStructSlice[User](rows)
if err != nil {
	return err
}

fmt.Println(len(users)) // 0

ScanStructRows simply does not call the callback when there are no rows.

err := mapper.ScanStructRows[User](rows, func(user *User) error {
	fmt.Println(user.Name)
	return nil
})

ScanStructOne returns mapper.ErrNoRows.

user, err := mapper.ScanStructOne[User](rows)
if errors.Is(err, mapper.ErrNoRows) {
	return nil
}

if err != nil {
	return err
}

Too Many Rows

ScanStructOne expects exactly one row.

If more than one row is returned, it 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
}

fmt.Println(user.Name)

Use ScanStructSlice when multiple rows are expected.

Invalid Assignment

Mapper returns an error when a source value cannot be assigned to the target field.

Example:

type User struct {
	CreatedAt time.Time `db:"created_at"`
}

If the database returns a non-time value that cannot be assigned to time.Time, mapper returns an error.

This helps catch schema mismatches early.

Recommended Practices

Use explicit db tags for stable mapping.

type User struct {
	ID        string    `db:"id"`
	Name      string    `db:"name"`
	CreatedAt time.Time `db:"created_at"`
}

Use pointer fields or nullable structs for optional database values.

type User struct {
	Email *string `db:"email"`
}

Use SQL aliases for joins and computed columns.

SELECT
	u.id AS user_id,
	u.name AS user_name,
	r.name AS role_name
FROM users u
JOIN roles r ON r.id = u.role_id

Use ScanStructOne only when the query is expected to return exactly one row.

Use ScanStructSlice or ScanStructRows when multiple rows are expected.

Dynamic Rows

Work with dynamic row maps, typed row helpers, and manual struct mapping.

About

DB is a shared Go database layer used by NetLifeGuru database drivers for querying, execution, transactions, dialect SQL, and result mapping.

On this page

OverviewNull ValuesNullable StructsExtra ColumnsMissing ColumnsType ConversionsBoolean ValuesPointer FieldsJSON FieldsEmpty ResultsToo Many RowsInvalid AssignmentRecommended Practices