Working with Databases in Go
Go provides excellent support for database operations through its database/sql
package and various third-party libraries. This guide covers everything you need to know about working with databases in Go.
Database Fundamentals
Basic Database Connection
package main
import (
"database/sql"
_ "github.com/lib/pq" // PostgreSQL driver
)
func main() {
db, err := sql.Open("postgres", "postgres://user:password@localhost/dbname?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatal(err)
}
}
CRUD Operations
Basic CRUD operations using database/sql
:
// Create
func createUser(db *sql.DB, name string, age int) error {
_, err := db.Exec("INSERT INTO users (name, age) VALUES ($1, $2)", name, age)
return err
}
// Read
func getUser(db *sql.DB, id int) (*User, error) {
var user User
err := db.QueryRow("SELECT id, name, age FROM users WHERE id = $1", id).
Scan(&user.ID, &user.Name, &user.Age)
if err != nil {
return nil, err
}
return &user, nil
}
// Update
func updateUser(db *sql.DB, id int, name string) error {
_, err := db.Exec("UPDATE users SET name = $1 WHERE id = $2", name, id)
return err
}
// Delete
func deleteUser(db *sql.DB, id int) error {
_, err := db.Exec("DELETE FROM users WHERE id = $1", id)
return err
}
Connection Management
Connection Pooling
func setupDB() (*sql.DB, error) {
db, err := sql.Open("postgres", "postgres://localhost/dbname?sslmode=disable")
if err != nil {
return nil, err
}
// Set maximum number of open connections
db.SetMaxOpenConns(25)
// Set maximum number of idle connections
db.SetMaxIdleConns(25)
// Set maximum lifetime of connections
db.SetConnMaxLifetime(5 * time.Minute)
return db, nil
}
Connection Monitoring
func monitorDBStats(db *sql.DB) {
ticker := time.NewTicker(time.Minute)
for range ticker.C {
stats := db.Stats()
log.Printf(
"DB Stats: Open=%d, Idle=%d, InUse=%d, WaitCount=%d",
stats.OpenConnections,
stats.Idle,
stats.InUse,
stats.WaitCount,
)
}
}
Database Migrations
Using Migration Tools
Example with golang-migrate
:
func runMigrations(dbURL string) error {
m, err := migrate.New(
"file://migrations",
dbURL,
)
if err != nil {
return err
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return err
}
return nil
}
Migration Files
-- 001_create_users.up.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- 001_create_users.down.sql
DROP TABLE users;
Working with ORMs
GORM Example
type User struct {
gorm.Model
Name string
Email string `gorm:"unique"`
Age int
Articles []Article
}
func main() {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
// Auto Migrate
db.AutoMigrate(&User{}, &Article{})
// Create
user := User{Name: "John", Email: "[email protected]", Age: 30}
db.Create(&user)
// Read
var foundUser User
db.First(&foundUser, "email = ?", "[email protected]")
}
Transactions
Basic Transaction
func transferMoney(db *sql.DB, fromID, toID int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Deduct from source account
if _, err := tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2",
amount, fromID); err != nil {
return err
}
// Add to destination account
if _, err := tx.Exec("UPDATE accounts SET balance = balance + $1 WHERE id = $2",
amount, toID); err != nil {
return err
}
return tx.Commit()
}
Transaction with Context
func transferMoneyContext(ctx context.Context, db *sql.DB, fromID, toID int, amount float64) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return err
}
defer tx.Rollback()
// Transaction operations...
return tx.Commit()
}
Query Building
Using Query Builders
Example with squirrel
:
import "github.com/Masterminds/squirrel"
func getUsersByAge(db *sql.DB, minAge int) ([]User, error) {
query := squirrel.Select("id", "name", "age").
From("users").
Where(squirrel.GtOrEq{"age": minAge}).
OrderBy("name")
sql, args, err := query.ToSql()
if err != nil {
return nil, err
}
rows, err := db.Query(sql, args...)
// Handle results...
}
Best Practices
Error Handling
if err := row.Scan(&user.ID, &user.Name); err != nil { if err == sql.ErrNoRows { return nil, ErrUserNotFound } return nil, fmt.Errorf("scanning user: %w", err) }
Prepared Statements
stmt, err := db.Prepare("INSERT INTO users(name, email) VALUES($1, $2)") if err != nil { return err } defer stmt.Close()
Context Usage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var name string err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id). Scan(&name)
Next Steps
- Learn about SQL Basics
- Explore Connection Pooling
- Study Database Migrations
- Understand ORMs
- Work with NoSQL Databases
- Master Transactions
- Practice Query Building