16 · GORM — ORM for Go¶
Coming from Java: GORM ≈ a lightweight JPA/Hibernate — struct tags instead of annotations, explicit where Hibernate is "magic". Know both its power and its footguns.
Setup & Models¶
import (
"gorm.io/gorm"
"gorm.io/driver/postgres"
)
type User struct {
ID uint `gorm:"primaryKey"` // or embed gorm.Model
Name string `gorm:"size:100;not null;index"`
Email string `gorm:"uniqueIndex"`
Orders []Order // has-many
CreatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` // soft delete
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn), // log slow SQL
})
db.AutoMigrate(&User{}, &Order{}) // DDL sync — dev only, use migrations in prod
CRUD & queries¶
// Create
db.Create(&User{Name: "Alice", Email: "a@b.c"})
// Read — First vs Find vs Take (First adds ORDER BY id LIMIT 1)
var u User
result := db.First(&u, "email = ?", "a@b.c") // NEVER fmt.Sprintf into SQL
if errors.Is(result.Error, gorm.ErrRecordNotFound) { /* 404 */ }
// Update
db.Model(&u).Update("name", "Alicia") // single field
db.Model(&u).Updates(User{Name: "X"}) // skips zero fields!
db.Model(&u).Updates(map[string]any{"name": ""}) // map = zero values too
// Delete (soft if DeletedAt exists)
db.Delete(&u)
db.Unscoped().Delete(&u) // hard delete
Associations — the N+1 trap¶
type Order struct {
ID uint
UserID uint
Items []Item
}
// BAD: N+1 — one query for users, then N queries for their orders
var users []User
db.Find(&users)
for _, u := range users { db.Find(&u.Orders) } // N queries!
// GOOD: Preload (like JPA fetch join / eager loading)
db.Preload("Orders").Preload("Orders.Items").Find(&users)
// Selective preload
db.Preload("Orders", "status = ?", "shipped").Find(&users)
Transactions¶
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err // any error → rollback
}
if err := tx.Model(&account).Update("balance", gorm.Expr("balance - ?", amt)).Error; err != nil {
return err
}
return nil // nil → commit
})
// Never swallow tx errors; the closure pattern handles rollback for you
Pitfalls — the interview checklist¶
Zero-value blind spot
Updates(struct) skips zero fields (0, "", false, nil time). Setting a field to zero requires map[string]any or Select("*"). #1 GORM bug source.
Scopes
db.Where(...) returns a new chain — reusing a tx := db.Where(...) accumulates conditions across calls. Use Session(&gorm.Session{}) or rebuild per request.
ErrRecordNotFound
First returns gorm.ErrRecordNotFound; Find on slices does not (empty slice, nil error). Know the difference — it's a real bug pattern.
Skip hooks unintentionally
db.Model(&u).UpdateColumn(...) skips BeforeUpdate hooks and UpdatedAt. Sometimes what you want; often a surprise.
Connection pool
sqlDB, _ := db.DB(); sqlDB.SetMaxOpenConns(25) — GORM defaults can starve under load. Tune MaxOpenConns, MaxIdleConns, ConnMaxLifetime.
Alternatives worth naming in an interview¶
sqlx— thin SQL + struct scanning, no magic.sqlc— write SQL, codegen type-safe Go. Many teams prefer this; GORM critics cite "impedance mismatch hidden until prod".ent(Facebook) — entity codegen, type-safe graph queries.pgx— if Postgres-only and want raw performance.
Interview line: "I'd default to sqlc/pgx for new services and use GORM where the domain genuinely benefits from an ORM — always with DryRun/SQL logging in tests to catch N+1s."
db.Model(&u).Updates(User{Name: "", Age: 0}) — what gets updated?
How do you eager-load associations in GORM?
db.Find(&users) finds nothing — what's err?