My day job currently includes writing and maintaining golang services. It’s not a bad language, and it certainly forces you to understand that everything will fail, far more obviously than Java’s checked exceptions. I’m not going to argue about the utility of checking/returning error instances, but I do find myself writing the same code over and over again, especially when I’m handling any SQL operations. So I’ve come up with a couple of templates to remove a bunch of the boilerplate code for transactions and for looping over SQL query result rows.

Transaction wrapper

Kicks off the transaction, executes the callback function, commits the transaction if the callback does not return an error, or rolls-back the transaction if the callback returns an error. If the transaction rollback fails, return both the error from the callback and the failed rollback error.

import (
	"database/sql"

	"github.com/hashicorp/go-multierror"
)

type txFn func(*sql.Tx) error

func withTX(db *sql.DB, fn txFn) error {
	tx, err := db.Begin()
	if err != nil {
		return err
	}
	err = fn(tx)
	if err != nil {
		if ex := tx.Rollback(); ex != nil {
			return multierror.Append(err, ex)
		}
		return err
	}
	return tx.Commit()
}

Use it like this:

const insertUserSQL = `
insert into users (
  uuid, name, slug, password, email, status, created_by, created_at, updated_by, updated_at
) values (
  ?, ?, ?, ?, ?, 'locked', ?, NOW(), ?, NOW()
)
`

const insertUserRoleSQL = `
insert into roles_users (user_id, role_id) values (?, ?)
`

func (h *holder) CreateAuthor(name string, email string, pwd string, adminID int64) error {
	pwHash, err := hashPassword(pwd)
	if err != nil {
		return err
	}
	roleID, err := h.authorID()
	if err != nil {
		return err
	}
	return withTX(h.db, func(tx *sql.Tx) error {
		r, err := tx.Exec(
			insertUserSQL,
			uuid.New(),
			name,
			createSlug(name),
			pwHash,
			email,
			adminID,
			adminID,
		)
		if err != nil {
			return err
		}
		userID, err := r.LastInsertId()
		if err != nil {
			return err
		}
		_, err = tx.Exec(insertUserRoleSQL, userID, roleID)
		return err
	})
}

Wrapper to iterate over query result rows

During the execution of a query and parsing of its results there is a lot of error checking at each step, and it all gets very noisy to read, and quite error prone to write out each time. This wrapper returns a partial function that is then applied to an eachRowFn callback to scan every row in the result set.

import "database/sql"

type scanFn func(dest ...interface{}) error

type eachRowFn func(row scanFn) error

type partialQuery func(row eachRowFn) error

func queryRows(db *sql.DB, query string, args ...interface{}) partialQuery {
	return func(row eachRowFn) error {
		rows, err := db.Query(query, args...)
		if err != nil {
			return err
		}
		defer rows.Close()
		for rows.Next() {
			if err := row(rows.Scan); err != nil {
				return err
			}
		}
		return rows.Err()
	}
}

Use it like this:

const listUsersSQL = `
select u.id, u.name, u.status, r.name
from users u, roles_users ru, roles r
where u.id = ru.user_id
and r.id = ru.role_id
order by u.name
`

func (h *holder) ListUsers() ([]User, error) {
	var users []User
	err := queryRows(h.db, listUsersSQL)(func(row scanFn) error {
		user := User{}
		if err := row(&user.ID, &user.Name, &user.Status, &user.Role); err != nil {
			return err
		}
		users = append(users, user)
		return nil
	})
	return users, err
}

I usually stick these into a single file somewhere near my database code, and get on with writing the rest of the service without worrying that I’ve again copied & pasted something broken ;-)

Hope this helps.

NOTE: These templates do not use the sql.BeginTx and sql.QueryContext variants that require a context.Context instance. If yours do, then it’s not difficult to modify these templates to include the contexts.

NOTE: I’m not handling panics in the withTX wrapper. Good grief, don’t write code that calls panic inside transactions, so I’ll leave that up to you if you feel that the wrapper should.