Go database
Skill Gisellesleeveless396/go-agent-skills/skills/(data)/go-database
Provide Go agent skills for cleaner, idiomatic code across Claude Code, Cursor, Copilot, and more with one install
npx -y skills add Gisellesleeveless396/go-agent-skills --skill go-databaseAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Database patterns for Go services: database/sql, connection management, transactions, migrations, query builders, and ORM usage (sqlc, GORM, ent). Use when: "database access", "SQL query", "connection pool", "transactions", "database migration", "sqlc", "GORM", "ent", "prepared statement", "repository pattern". Do NOT use for: in-memory data structures (use go-coding-standards), security aspects of SQL (use go-security-audit), or performance profiling of queries (use go-performance-review).
SKILL.md
8.5 KB, as published. Nobody here has run it
Go Database Patterns
Database access is where most Go services spend their complexity budget. Get connection management, transactions, and query patterns right.
1. Connection Management
Configure the connection pool explicitly:
func OpenDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)
if err := db.PingContext(context.Background()); err != nil {
return nil, fmt.Errorf("ping db: %w", err)
}
return db, nil
}
Pool sizing rules:
| Setting | Guideline |
|---|---|
MaxOpenConns | Match your DB's max connections / number of app instances |
MaxIdleConns | 40-50% of MaxOpenConns |
ConnMaxLifetime | 5-10 minutes (prevents stale connections behind load balancers) |
ConnMaxIdleTime | 1-2 minutes |
// ❌ Bad — unlimited connections (default)
db, _ := sql.Open("postgres", dsn)
// No pool config → unbounded connections → DB overload under load
Always pass context to database operations:
// ✅ Good — context propagated
row := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", id)
// ❌ Bad — no context, no cancellation support
row := db.QueryRow("SELECT id, name FROM users WHERE id = $1", id)
2. Query Patterns
Use parameterized queries — NEVER string concatenation:
// ✅ Good — parameterized
rows, err := db.QueryContext(ctx,
"SELECT id, name FROM users WHERE status = $1 AND created_at > $2",
status, since,
)
// ❌ Bad — SQL injection vulnerability
rows, err := db.QueryContext(ctx,
fmt.Sprintf("SELECT id, name FROM users WHERE status = '%s'", status),
)
Always close rows:
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("query users: %w", err)
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return fmt.Errorf("scan user: %w", err)
}
users = append(users, u)
}
// ALWAYS check rows.Err() after iteration
if err := rows.Err(); err != nil {
return fmt.Errorf("iterate users: %w", err)
}
Use QueryRowContext for single-row queries:
var user User
err := db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name, &user.Email)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
3. Transactions
Use a transaction helper to ensure rollback on error:
func WithTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
if err := fn(tx); err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return fmt.Errorf("rollback failed: %v (original: %w)", rbErr, err)
}
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit tx: %w", err)
}
return nil
}
Usage:
err := WithTx(ctx, db, func(tx *sql.Tx) error {
if _, err := tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID,
); err != nil {
return fmt.Errorf("debit: %w", err)
}
if _, err := tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, toID,
); err != nil {
return fmt.Errorf("credit: %w", err)
}
return nil
})
Set appropriate isolation levels:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable, // for critical financial operations
})
4. Repository Pattern
Define a repository interface at the consumer side:
type UserRepository interface {
GetByID(ctx context.Context, id string) (*User, error)
List(ctx context.Context, filter UserFilter) ([]User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
}
Implement with concrete database access:
type pgUserRepo struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) UserRepository {
return &pgUserRepo{db: db}
}
func (r *pgUserRepo) GetByID(ctx context.Context, id string) (*User, error) {
var u User
err := r.db.QueryRowContext(ctx,
"SELECT id, name, email, created_at FROM users WHERE id = $1", id,
).Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return &u, nil
}
5. sqlc — Type-Safe SQL
Prefer sqlc for projects that use raw SQL. It generates type-safe Go code from SQL queries.
Write SQL queries with annotations:
-- name: GetUser :one
SELECT id, name, email, created_at
FROM users
WHERE id = $1;
-- name: ListUsers :many
SELECT id, name, email, created_at
FROM users
WHERE status = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email, created_at;
sqlc generates Go code with proper types, eliminating manual Scan calls
and catching query/schema mismatches at build time.
6. Migrations
Use a migration tool — never manual DDL:
Recommended tools: goose, golang-migrate, atlas.
Migration rules:
- One migration per schema change
- Migrations are forward-only in production — never edit applied migrations
- Include both
upanddown(rollback) SQL - Test migrations against a copy of production data before deploying
- Keep migrations small and reversible
-- +goose Up
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- +goose Down
ALTER TABLE users DROP COLUMN phone;
Run migrations at startup or as a separate step, not both:
// ✅ Good — separate migration command
// cmd/migrate/main.go runs migrations
// cmd/server/main.go starts the server
// ❌ Bad — migrations in server startup
func main() {
runMigrations(db) // blocks startup, risky in multi-instance deploys
startServer()
}
7. Common Pitfalls
Null handling:
// ✅ Good — use sql.Null types or pointers
type User struct {
ID string
Name string
Phone sql.NullString // nullable column
}
// Or with pointers:
type User struct {
ID string
Name string
Phone *string // nil = SQL NULL
}
Avoiding N+1 queries:
// ❌ Bad — N+1 query pattern
users, _ := listUsers(ctx)
for _, u := range users {
orders, _ := getOrdersByUser(ctx, u.ID) // 1 query per user
u.Orders = orders
}
// ✅ Good — single query with JOIN or batch
users, _ := listUsersWithOrders(ctx) // JOIN or subquery
Connection leak prevention:
// ❌ Bad — rows not closed on early return
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
// forgot defer rows.Close()
if someCondition {
return nil // rows leaked!
}
Verification Checklist
- Connection pool configured with explicit limits (
MaxOpenConns,MaxIdleConns, lifetimes) - All queries use parameterized placeholders, never string concatenation
- All
QueryContextresults havedefer rows.Close()immediately after error check rows.Err()checked after row iteration loopsql.ErrNoRowshandled explicitly witherrors.Is- Transactions use a helper that guarantees rollback on error
- Context propagated to all database calls (
*Contextvariants) - Nullable columns use
sql.NullString/sql.NullInt64or pointer types - No N+1 query patterns — use JOINs or batch queries
- Migrations are versioned, reversible, and run separately from app startup