AlgoMaster Logo

sqlx Package

Low Priority16 min readUpdated June 6, 2026

github.com/jmoiron/sqlx is a thin wrapper around the standard database/sql package that adds two things people end up rewriting by hand: scanning rows directly into structs and using named parameters in queries. It's not an ORM, it doesn't manage migrations, and it doesn't hide SQL. sqlx.DB embeds *sql.DB, so every method you've learned in this section still works, plus a few new ones that cut the boilerplate around reads and writes.

The examples in this chapter need two third-party packages. In a real project you'd run go get github.com/jmoiron/sqlx and go get modernc.org/sqlite once, then import them like any standard library package. The schema is the same products and customers tables we've used throughout this section.

What sqlx Adds on Top of database/sql

database/sql gives you four building blocks: Query, QueryRow, Exec, and Prepare. They all work, but every read ends with the same dance: declare variables, call rows.Scan(&a, &b, &c), remember rows.Close(), check rows.Err(). For a struct with ten fields that's ten &p.Field arguments to keep in sync with the SELECT list. sqlx removes that ceremony by reading column names from the *sql.Rows metadata and matching them to struct fields using a tag.

Here is the mapping between the two APIs side by side:

database/sqlsqlx equivalentWhat changes
db.Query(q, args...)db.Queryx(q, args...)Returns *sqlx.Rows with StructScan
db.QueryRow(q, args...)db.Get(&dst, q, args...)Scans the one row directly into a struct
db.Query(q, args...) + loopdb.Select(&dst, q, args...)Scans every row into a slice of structs
db.Exec(q, args...)db.NamedExec(q, structOrMap)Named :foo placeholders pulled from struct fields
db.QueryRow(...).Scan(...)db.NamedQuery(q, structOrMap)Same, but for SELECTs
sql.Open(driver, dsn)sqlx.Open(driver, dsn) / sqlx.Connect(driver, dsn)Connect also pings the database

Everything else, transactions, contexts, prepared statements, connection pooling, behaves the same as plain database/sql because the underlying *sql.DB is the same object.

The first program just opens a database and confirms sqlx.DB is what you'd expect:

sqlx.Connect combines sqlx.Open with a Ping, so if the DSN is wrong or the driver can't reach the database, you find out immediately instead of on the first query. sqlx.Open mirrors sql.Open exactly: it validates the DSN but doesn't actually connect.

db.Get(&version, "SELECT sqlite_version()") shows the smallest possible read. Get runs the query, fetches one row, and writes the single column into the destination variable. The destination doesn't have to be a struct, a *string works fine for a single-column result.

Struct Tags and Column Mapping

Every sqlx read needs to know which column goes into which struct field. The library uses the db struct tag for this. Without tags, sqlx falls back to a lowercased version of the field name, which works often enough until a column is named created_at and the matching field is CreatedAt.

Tags are explicit and survive refactors. If you rename Price to UnitPrice in Go code, the db:"price" tag keeps the SQL mapping intact, so the database column doesn't have to change.

Here's a full program that creates the table, inserts a row, and reads it back with db.Get:

db.Exec is the same Exec you already know from database/sql. sqlx doesn't change its behavior; it inherits it. The new method is db.Get, which takes a pointer to a struct, runs the query, and scans the single result row into the struct using the db tags. If no row matches, Get returns sql.ErrNoRows, just like QueryRow().Scan() does.

Reading Rows: Get and Select

Get and Select are the two methods used most often. They're the whole reason most people adopt sqlx. Get handles one row, Select handles many.

A few details:

  • db.MustExec is a convenience that panics if Exec fails. Use it in tests, schema setup, and tiny scripts; in real handler code stick with Exec and check the error.
  • Select expects a pointer to a slice. It allocates the slice for you and appends each row. If the query returns zero rows, you get an empty slice and a nil error, not sql.ErrNoRows.
  • Get returns sql.ErrNoRows when nothing matches, the same sentinel QueryRow().Scan() returns. The error model is identical, so errors.Is(err, sql.ErrNoRows) still works.

Compare the raw scan loop against the Select one-liner:

The raw path has five steps and at least two defer statements (rows.Close and the error checks). The sqlx path collapses it into one call. The trade-off is that you give up control over the iteration: Select loads the entire result set into memory before returning. For a query that returns 50 rows that's nothing; for a query that returns 5 million rows that's a problem.

For those big-result cases, drop down to Queryx + StructScan and keep the loop:

Queryx is the sqlx version of Query and returns *sqlx.Rows instead of *sql.Rows. The only new method on *sqlx.Rows is StructScan, which fills a struct in place using the db tags. Everything else (Next, Close, Err) is the standard sql.Rows API you already know.

Named Parameters: NamedExec and NamedQuery

The other big convenience sqlx adds is named placeholders. Instead of ? (or $1, $2 for Postgres), you write :name, and sqlx pulls the matching value out of a struct or map[string]any. This eliminates the positional-argument bugs that creep in when you reorder columns in an INSERT and forget to reorder the matching args slice.

NamedExec takes a query containing :fieldname placeholders and a struct (or map). It looks up each named placeholder by db tag on the struct, builds a positional query internally, and runs it. The return value is the standard sql.Result, so LastInsertId and RowsAffected work as usual.

NamedQuery is the SELECT version. It doesn't return a slice the way Select does; it returns a cursor (*sqlx.Rows) and you call StructScan in a loop. The library doesn't ship a NamedSelect, so when you need both named parameters and a slice destination, you call NamedQuery and iterate.

A common pattern is updates that touch many columns:

The same Product value drives both the SET clause and the WHERE clause. Compare that with the positional version:

It's not dramatically shorter, but the named form survives column reordering without bugs. Swap name and price in the SQL and the named query keeps working; the positional one silently writes the wrong value into the wrong column.

You can also pass a map[string]any instead of a struct when you don't have a matching type:

Maps are useful for partial updates and one-off queries; structs are better when the same shape gets used across many calls.

IN-Clause Expansion with sqlx.In

SQL's IN clause takes a fixed number of values, not a slice. If you want to find all products whose IDs are in a []int64, you can't write WHERE id IN (?) and pass the slice; you'd get a syntax error or a single bound parameter holding the whole slice. The standard database/sql answer is to build the ?, ?, ? list yourself with strings.Repeat or a loop. sqlx.In does that for you.

sqlx.In takes a query with a single ? placeholder representing the IN list and a slice. It expands the placeholder into the right number of ? marks and returns the rewritten query plus a flat []any of arguments ready to pass to Select, Get, or Exec. Calling it with ids = []int64{1, 3, 4} rewrites the query to ... WHERE id IN (?, ?, ?) and returns args = []any{1, 3, 4}.

The db.Rebind call exists because sqlx.In always produces ? placeholders, but Postgres uses $1, $2, etc., and Oracle uses :1. Rebind converts to whatever style your driver expects. For SQLite and MySQL it's a no-op; for Postgres it does real work. Calling it unconditionally is the safe habit, so swapping drivers later doesn't break the query.

You can mix IN-expansion with other parameters too, as long as the IN slice's ? is the first argument and the rest follow:

Here args becomes []any{1, 3, 4, 50.0} and the query has four ? placeholders. The slice expands in place; everything else stays positional.

Using sqlx Inside Transactions

Transactions work the same way. The only difference is the method name: db.Beginx() instead of db.Begin(). It returns a *sqlx.Tx that has all the standard *sql.Tx methods (Commit, Rollback, Exec, Query, Prepare) plus the new ones (Get, Select, NamedExec, NamedQuery). You don't need to learn two transaction APIs; you keep the patterns you already know and write less code inside them.

tx.Get and tx.NamedExec work exactly like db.Get and db.NamedExec, just bound to the transaction. The defer tx.Rollback() is the standard idiom: after tx.Commit() succeeds, calling Rollback is a safe no-op that returns sql.ErrTxDone, which the deferred call ignores. If anything before Commit returns early, the rollback runs and undoes whatever the transaction had touched.

sqlx doesn't change the transaction semantics, isolation levels, or context behavior. It just lets you keep using struct scanning inside the transaction.

sqlx, Raw database/sql, and Full ORMs

The Go ecosystem has roughly three tiers for talking to a relational database. Each one trades a different amount of control for a different amount of convenience. The choice is rarely about performance, it's about how much SQL your team wants to write by hand and how much "magic" you're comfortable with.

AspectRaw database/sqlsqlxFull ORM (GORM, ent)
You write SQLYesYesSometimes; the ORM generates most of it
Struct scanningManual rows.Scan(&a, &b)Automatic via db tagsAutomatic
MigrationsExternal toolExternal toolBuilt in (varies)
Relationship loadingManual joinsManual joinsBuilt in (joins, preload)
BoilerplateHighLowLowest
Surprise factorLowestLowMedium to high
Reflection costNonePer-type, cachedPer-call, varies
DependenciesJust the driverjmoiron/sqlx + driverLarge dependency tree
Learning curveSmall, well documentedSmall, just a thin layerMedium to large
Good fit forStrict performance budgets, librariesApp services that own their schemaRapid CRUD, generated APIs

database/sql alone is the right choice when you're writing a library other people depend on (no extra dependency is welcome), when you need every ounce of control over how rows are read, or when your queries are simple enough that struct scanning doesn't save much. The same applies when your team is small and an extra dependency isn't worth a 30-line saving.

sqlx fits the middle ground where most application code lives. You're still writing SQL by hand, you still know exactly what query runs against the database, but you don't waste lines on &p.Field scan arguments and you get named parameters for free. The library is small, well documented, and stable; the dependency is roughly as risky as the database driver itself.

A full ORM like GORM or ent is the right choice when you want generated CRUD, automatic migrations, relationship eager-loading, and you accept that the SQL you see in logs might not be the SQL you'd have written. ORMs trade transparency for productivity. The trade is fine until you hit a query plan problem and have to reverse-engineer the generated SQL, which is when many teams either learn the ORM's escape hatches or migrate part of the codebase back to raw queries.

Start with database/sql for the simplest case, move to sqlx when you find yourself writing the same scan boilerplate three times in a row, and only adopt a full ORM if you're confident the productivity gains outweigh the loss of control. A single project can mix tiers: sqlx for normal CRUD, raw database/sql for that one hot-path query you've tuned by hand, and no ORM if the schema is small enough.

One small follow-up: sqlx exports sqlx.NullString, sqlx.NullInt64, sqlx.NullBool, sqlx.NullFloat64, and sqlx.NullTime as aliases for the matching sql.Null* types. They behave identically, so the null-handling patterns carry over unchanged. The aliases exist so you don't need to import database/sql separately when you're already using sqlx. They aren't a different feature.

Quiz

sqlx Package Quiz

10 quizzes