In a project I defined methods on a type SQLStore (CreateUser, DeleteThing, CheckWhatever, etc.). SQLStore is a struct with a SQLStoreIO member, SQLStoreIO being such common interface. So SQLStore can have inside either a transaction or a *sql.DB, and use all the custom methods with both.
For when, inside a method, I need to make sure I use a transaction, I defined a function:
func ensureTransaction(st *SQLStore) (store SQLStoreIO, commit, rollback func() error, err error)
If st is already a transaction (checked with a type assertion), `commit` and `rollback` are no-ops, as the transaction is supposed to be closed somewhere else, and the returned `store` is the same object than `st`. If not, it starts a new transaction in `store`, `commit` and `rollback` being its actual commit and rollback methods.