GORM is a full-featured, developer-friendly Object-Relational Mapper (ORM) for Go. It lets you work with your database using ordinary Go structs and method chains instead of writing raw SQL — while still giving you an escape hatch to raw SQL whenever you need it. GORM handles associations, transactions, migrations, hooks, soft deletes, prepared-statement caching, and SQL logging out of the box, and supports MySQL, PostgreSQL, SQLite, SQL Server, and other databases through pluggable drivers.
This guide shows you how to install GORM, define your models, and perform every common database operation, with copy-paste-runnable examples throughout.
Note
GORM is the core ORM package (
gorm.io/gorm). To actually talk to a database you also install a driver package (for examplegorm.io/driver/sqliteorgorm.io/driver/postgres). This guide uses SQLite for runnable examples because it needs no server.
1. Quick Start
Here is a complete, runnable program that connects to a database, creates a table, and performs the basic Create / Read / Update / Delete cycle.
package main
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Product is a model. GORM maps it to a `products` table.
type Product struct {
gorm.Model // embeds ID, CreatedAt, UpdatedAt, DeletedAt
Code string
Price uint
}
func main() {
// 1. Connect
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 2. Migrate the schema (creates/updates the `products` table)
db.AutoMigrate(&Product{})
// 3. Create
db.Create(&Product{Code: "D42", Price: 100})
// 4. Read
var product Product
db.First(&product, 1) // find product with integer primary key 1
db.First(&product, "code = ?", "D42") // find product with code D42
// 5. Update — update product's price to 200
db.Model(&product).Update("Price", 200)
// Update multiple fields
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields only
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// 6. Delete — soft delete product (sets deleted_at)
db.Delete(&product, 1)
fmt.Println(product)
}
{{1 2026-06-06 10:00:00 ... } F42 200}
Tip
GORM returns the
*gorm.DBfrom almost every method, so calls chain:db.Where("price > ?", 100).Order("price desc").Limit(10).Find(&products). The chain is only executed when you call a finisher method such asFind,First,Create,Save,Update,Delete,Count, orScan.
2. Installation
2.1 Requirements
| Requirement | Detail |
|---|---|
| Go version | 1.18+ (the core module declares go 1.18; the generics API requires 1.18, and the slog logger requires 1.21+) |
| A database driver | One of the gorm.io/driver/* packages |
| A database | MySQL, PostgreSQL, SQLite, SQL Server, GaussDB, etc. (SQLite needs only a local file) |
2.2 Install the core package and a driver
SQLite
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
PostgreSQL
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
MySQL
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
SQL Server
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlserver
Note
The SQLite driver (
gorm.io/driver/sqlite) depends ongithub.com/mattn/go-sqlite3, which uses cgo — you need a C compiler installed to build it. The MySQL, PostgreSQL, and SQL Server drivers are pure Go.
2.3 Connecting
gorm.Open takes a dialector (created by your driver) and zero or more options. The most common option is *gorm.Config.
SQLite
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
PostgreSQL
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
MySQL
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
Important
Always check the
errreturned bygorm.Open. On success, GORM by default pings the database to verify the connection (you can disable this withDisableAutomaticPing: truein the config).
Tip
Reuse a single
*gorm.DBfor the lifetime of your application. It is safe for concurrent use and manages an underlying connection pool. To tune the pool, get the underlying*sql.DBand call its methods (see Section 12).
3. Defining Models
A model is a regular Go struct. GORM uses reflection and struct tags to map it to a table.
3.1 The conventions
By default GORM applies these conventions, so a plain struct usually needs no tags at all:
- The table name is the snake_case, pluralized struct name (
User→users,OrderItem→order_items). - Column names are the snake_case field names (
UserName→user_name). - A field named
ID(any integer type) is the primary key and is auto-incremented. - Fields named
CreatedAtandUpdatedAt(of typetime.Timeor an integer) are automatically set on create/update.
type User struct {
ID uint
Name string
Email string
Age uint8
CreatedAt time.Time
UpdatedAt time.Time
}
3.2 gorm.Model
GORM ships a convenience struct you can embed to get the four most common fields:
// gorm.Model definition (from the source):
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
Embed it to add ID, CreatedAt, UpdatedAt, and a DeletedAt field that enables soft deletes (see Section 9):
type User struct {
gorm.Model
Name string
}
3.3 Field tags
Control mapping with the gorm:"..." struct tag. Multiple settings are separated by ;. Tag names are case-insensitive.
| Tag | Effect |
|---|---|
column |
Override the column name: gorm:"column:user_name" |
primaryKey |
Mark the field as a primary key |
autoIncrement |
Enable auto-increment |
autoIncrementIncrement |
Set the auto-increment step |
unique |
Add a unique constraint |
default |
Set a default value: gorm:"default:18" |
not null |
Add a NOT NULL constraint |
size |
Column size/length: gorm:"size:255" |
precision / scale |
Numeric precision and scale |
type |
Explicit column data type: gorm:"type:varchar(100)" |
comment |
Column comment |
index |
Create an index (gorm:"index" or named gorm:"index:idx_name") |
uniqueIndex |
Create a unique index |
check |
Add a CHECK constraint: gorm:"check:age > 0" |
embedded |
Embed a struct's fields into this table |
embeddedPrefix |
Prefix embedded column names: gorm:"embedded;embeddedPrefix:author_" |
serializer |
Serialize the field: gorm:"serializer:json" (also gob, unixtime) |
autoCreateTime |
Set the field to the current time on create |
autoUpdateTime |
Set the field to the current time on update |
- |
Ignore the field entirely (gorm:"-") |
-> |
Read-only field (gorm:"->"); <- makes it write-only/creatable |
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:128;not null"`
Email string `gorm:"uniqueIndex;size:255"`
Age uint8 `gorm:"default:18;check:age >= 0"`
Bio string `gorm:"type:text"`
Settings JSON `gorm:"serializer:json"`
Secret string `gorm:"-"` // never read or written
}
Tip
Read/write permissions let you protect fields:
gorm:"->"makes a field read-only (selected but never inserted/updated),gorm:"<-:create"makes it creatable but not updatable, andgorm:"-"ignores it completely.
3.4 Indexes
Indexes are declared on fields. Give several fields the same index name to build a composite index, and use priority to order the columns:
type User struct {
Name string `gorm:"index:idx_name_age,priority:1"`
Age int `gorm:"index:idx_name_age,priority:2"`
}
A single uniqueIndex on one field also marks that field's UniqueIndex automatically. Supported index settings parsed from the tag include CLASS (e.g. UNIQUE, FULLTEXT, SPATIAL), TYPE (e.g. btree, hash, gin), WHERE (partial index), COMMENT, EXPRESSION, SORT, COLLATE, LENGTH, and OPTION.
3.5 Overriding the table name
Implement the Tabler interface to set a fixed table name, or use db.Table("name") per query:
func (User) TableName() string {
return "auth_users"
}
4. Creating Records
4.1 Insert a single record
user := User{Name: "Jinzhu", Age: 18}
result := db.Create(&user) // pass a pointer
user.ID // the primary key is back-filled after insert
result.Error // returns error, if any
result.RowsAffected // number of rows inserted
Important
Always pass a pointer (
&user) toCreate. GORM writes the generated primary key (and other generated columns) back into the struct.
4.2 Insert specific fields only
// Only insert name and age columns
db.Select("Name", "Age").Create(&user)
// Insert everything EXCEPT the listed columns
db.Omit("Age").Create(&user)
4.3 Batch insert
Pass a slice to Create, or use CreateInBatches to chunk large slices:
users := []User{{Name: "a"}, {Name: "b"}, {Name: "c"}}
db.Create(&users)
// Insert in batches of 100
db.CreateInBatches(&users, 100)
You can also set a default batch size globally with CreateBatchSize in the config; Create will then automatically batch large slices.
4.4 Upsert / "on conflict"
Use the clause.OnConflict clause to control what happens when a row already exists:
import "gorm.io/gorm/clause"
// Do nothing on conflict
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&users)
// Update specified columns on conflict (by primary key)
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// Update ALL columns (except primary keys) on conflict
db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&users)
Note
db.Save()on a slice automatically appliesOnConflict{UpdateAll: true}for you (see Section 6.4).
5. Querying Records
5.1 Retrieve one record
| Method | Behaviour |
|---|---|
First(&user) |
First row ordered by primary key; errors with ErrRecordNotFound if none |
Take(&user) |
One row in no particular order; errors with ErrRecordNotFound if none |
Last(&user) |
Last row ordered by primary key (descending); errors with ErrRecordNotFound if none |
var user User
db.First(&user) // SELECT * FROM users ORDER BY id LIMIT 1
db.Take(&user) // SELECT * FROM users LIMIT 1
db.Last(&user) // SELECT * FROM users ORDER BY id DESC LIMIT 1
db.First(&user, 10) // by primary key: WHERE id = 10
db.First(&user, "name = ?", "x") // with a condition
Warning
First,Take, andLastreturngorm.ErrRecordNotFoundwhen nothing matches. Always check for it:errors.Is(result.Error, gorm.ErrRecordNotFound).Find(below) does not raise this error for empty results.
5.2 Retrieve many records
var users []User
result := db.Find(&users) // SELECT * FROM users
result.RowsAffected // number of rows returned
db.Find(&users, []int{1, 2, 3}) // WHERE id IN (1,2,3)
db.Where("age > ?", 18).Find(&users)
5.3 Conditions
Where, Not, and Or accept strings with ? placeholders, structs, or maps. Conditions chain with AND by default.
// String condition
db.Where("name = ?", "jinzhu").First(&user)
db.Where("age > ? AND name <> ?", 18, "admin").Find(&users)
db.Where("name IN ?", []string{"jinzhu", "jinzhu2"}).Find(&users)
db.Where("name LIKE ?", "%jin%").Find(&users)
// Struct condition (only non-zero fields are used)
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// Map condition (zero values ARE used)
db.Where(map[string]interface{}{"name": "jinzhu", "age": 0}).Find(&users)
// NOT and OR
db.Not("name = ?", "jinzhu").First(&user)
db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu2").Find(&users)
Caution
When you query with a struct, GORM ignores zero-value fields (
0,"",false). To filter on a zero value, use a map or aSelectof the field names. This is the single most common GORM surprise.
5.4 Selecting, ordering, limiting
db.Select("name", "age").Find(&users) // only these columns
db.Select("COALESCE(age, ?)", 42).Find(&users)
db.Order("age desc, name").Find(&users)
db.Limit(10).Offset(20).Find(&users) // pagination
db.Distinct("name", "age").Find(&users)
db.Group("name").Having("count(id) > ?", 1).Find(&users)
Tip
Cancel a previously chained
LimitorOffsetby passing-1:db.Limit(10).Find(&a).Limit(-1).Find(&b).
5.5 Pluck and Count
// Pluck a single column into a slice
var names []string
db.Model(&User{}).Pluck("name", &names)
// Count rows
var count int64
db.Model(&User{}).Where("age > ?", 18).Count(&count)
5.6 Scan into a custom struct
Scan reads query results into any struct — handy for aggregates and joins that don't match a model:
type Result struct {
Name string
Total int
}
var res []Result
db.Model(&User{}).
Select("name, sum(age) as total").
Group("name").
Scan(&res)
5.7 Iterating with Rows and FindInBatches
For very large result sets, stream rows or process them in batches:
// Process in chunks of 100 — efficient for large tables
db.Where("age > ?", 0).FindInBatches(&users, 100, func(tx *gorm.DB, batch int) error {
for _, u := range users {
// ... handle each user
}
return nil // returning an error stops processing
})
// Or iterate raw rows
rows, _ := db.Model(&User{}).Rows()
defer rows.Close()
for rows.Next() {
var u User
db.ScanRows(rows, &u)
}
5.8 FirstOrInit / FirstOrCreate
Find a record, or initialize / create one if it doesn't exist.
// FirstOrInit: find or build (in memory only, no INSERT)
db.Where(User{Name: "non_existing"}).FirstOrInit(&user)
// FirstOrCreate: find or INSERT
db.Where(User{Name: "jinzhu"}).FirstOrCreate(&user)
// Attrs: extra attributes used ONLY when the record is not found
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// Assign: attributes applied whether found or not (causes an UPDATE if found)
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user)
6. Updating Records
6.1 Update a single column
db.Model(&user).Update("name", "hello")
// With a condition (no primary key in the model):
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
6.2 Update multiple columns
Updates accepts a struct (only non-zero fields are written) or a map (all keys are written):
// Struct — updates only non-zero fields (name, age)
db.Model(&user).Updates(User{Name: "hello", Age: 18})
// Map — updates exactly these columns, including zero values
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "active": false})
Caution
Because
Updateswith a struct skips zero-value fields,db.Model(&user).Updates(User{Active: false})will not setactiveto false. Use amap, orSelectthe columns explicitly, to update zero values.
6.3 Select / Omit on update
// Update only the selected fields
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 0})
// Update all fields EXCEPT the omitted ones
db.Model(&user).Omit("name").Updates(User{Name: "x", Age: 18})
// Select("*") forces ALL fields (including zero values) from a struct
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Age: 0})
6.4 Save
Save upserts the whole record. If the primary key is set it updates all fields; otherwise it inserts:
db.Save(&user) // INSERT if no PK, otherwise UPDATE every column
6.5 Update without hooks: UpdateColumn(s)
UpdateColumn and UpdateColumns write directly, skipping hooks and the updated_at auto-timestamp:
db.Model(&user).UpdateColumn("name", "hello")
db.Model(&user).UpdateColumns(map[string]interface{}{"name": "hello", "age": 18})
Warning
A "global" update or delete — one with no
WHEREcondition and no primary key — is blocked by default and returnsErrMissingWhereClause, to protect you from accidentally changing every row. To intentionally update all rows, enableAllowGlobalUpdatevia a session:db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&User{}).Update("name", "x").
6.6 Expressions
Use gorm.Expr to update with a SQL expression rather than a literal:
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// UPDATE products SET price = price * 2 + 100 WHERE ...
7. Deleting Records
// Delete by model (uses its primary key)
db.Delete(&user)
// Delete by condition
db.Where("age < ?", 18).Delete(&User{})
// Delete by primary key inline
db.Delete(&User{}, 10)
db.Delete(&User{}, []int{1, 2, 3})
Warning
Like updates, a delete with no condition is blocked with
ErrMissingWhereClause. Provide a primary key or aWhereclause.
If your model has a gorm.DeletedAt field (for example via gorm.Model), Delete performs a soft delete — see the next section.
8. Associations
GORM models relationships between structs as Belongs To, Has One, Has Many, and Many To Many.
type User struct {
gorm.Model
Name string
CompanyID uint
Company Company // Belongs To
CreditCard CreditCard // Has One
Orders []Order // Has Many
Languages []Language `gorm:"many2many:user_languages;"` // Many To Many
}
8.1 Creating with associations
By default, creating or saving a record also creates its associated records (upsert), so you can build an object graph in one call:
db.Create(&User{
Name: "jinzhu",
Orders: []Order{{Price: 10}, {Price: 20}},
Company: Company{Name: "Acme"},
})
Skip auto-creating associations with Omit:
db.Omit("Orders").Create(&user) // skip the Orders association
db.Omit(clause.Associations).Create(&user) // skip ALL associations
8.2 Preloading (eager loading)
Preload loads associations in separate queries to avoid the N+1 problem:
// Load every user together with their orders and company
db.Preload("Orders").Preload("Company").Find(&users)
// Nested preload
db.Preload("Orders.OrderItems").Find(&users)
// Conditional preload
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
8.3 Joins
Joins performs a SQL JOIN. For a Belongs To / Has One relation you can join by association name to load it in a single query:
db.Joins("Company").Find(&users) // LEFT JOIN companies ...
// Raw join with a condition
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "x@y.z").Find(&users)
// INNER JOIN variant
db.InnerJoins("Company").Find(&users)
8.4 Association mode
db.Model(&user).Association("Name") gives you fine-grained control over a single relationship:
// Find associated records
var orders []Order
db.Model(&user).Association("Orders").Find(&orders)
// Append / Replace / Delete / Clear
db.Model(&user).Association("Orders").Append(&Order{Price: 100})
db.Model(&user).Association("Languages").Replace([]Language{en, fr})
db.Model(&user).Association("Orders").Delete(&order1)
db.Model(&user).Association("Orders").Clear()
// Count associated records
count := db.Model(&user).Association("Orders").Count()
Note
By default, association writes only save the foreign-key reference, not the full associated record. Set
FullSaveAssociations: true(in config or via a session) to also upsert the associated records' own fields.
9. Soft Deletes
If a model includes a field of type gorm.DeletedAt (which gorm.Model provides), GORM never physically deletes the row. Instead, Delete sets deleted_at to the current time, and every subsequent query automatically adds WHERE deleted_at IS NULL.
type User struct {
gorm.Model // includes DeletedAt
Name string
}
db.Delete(&user) // UPDATE users SET deleted_at = NOW() WHERE id = ...
db.First(&user, 1) // automatically excludes soft-deleted rows
9.1 Querying soft-deleted records
Use Unscoped to bypass the soft-delete filter:
// Include soft-deleted rows in a query
db.Unscoped().Where("age = ?", 20).Find(&users)
// Permanently delete (real DELETE) a soft-deletable row
db.Unscoped().Delete(&user)
Tip
You can customize the "not deleted" sentinel with the
zeroValuetag, e.g.gorm:"zeroValue:1970-01-01 00:00:01", when your column isNOT NULLand cannot store SQLNULL.
10. Transactions
10.1 The Transaction helper (recommended)
Run a function inside a transaction. Returning an error rolls back; returning nil commits. A panic also rolls back automatically.
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "a"}).Error; err != nil {
return err // rolls back
}
if err := tx.Create(&Order{Price: 10}).Error; err != nil {
return err // rolls back
}
return nil // commits
})
Important
Inside the closure, use the provided
tx, not the outerdb. Operations ondbrun outside the transaction.
10.2 Manual transactions
tx := db.Begin()
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return
}
tx.Commit()
10.3 Nested transactions and save points
GORM supports nested transactions using save points, plus explicit SavePoint / RollbackTo:
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // undoes user2 but keeps user1
tx.Commit()
Note
By default GORM wraps every single
Create,Update, andDeletein its own transaction for data integrity. If you are doing many writes and want more speed, setSkipDefaultTransaction: truein the config (or per session) to turn this off.
11. Raw SQL
When the query builder isn't enough, drop to raw SQL.
// Raw query scanned into a struct/slice
var result Result
db.Raw("SELECT name, age FROM users WHERE id = ?", 3).Scan(&result)
// Named parameters
db.Raw("SELECT * FROM users WHERE name = @name OR email = @name",
sql.Named("name", "jinzhu")).Scan(&users)
// Exec for statements that don't return rows
db.Exec("UPDATE users SET age = age + 1 WHERE id = ?", 10)
11.1 Inspect generated SQL with ToSQL and DryRun
sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB {
return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{})
})
// sql == "SELECT * FROM `users` WHERE id = 100 ORDER BY age desc LIMIT 10"
Caution
The SQL produced by
ToSQL/the logger has parameters interpolated for readability and is not safe to execute directly — running it can introduce SQL injection. Use it only for inspection and debugging.
12. Connection Pooling
GORM uses Go's standard database/sql pool. Get the underlying *sql.DB to tune it:
sqlDB, err := db.DB()
sqlDB.SetMaxIdleConns(10) // idle connections in the pool
sqlDB.SetMaxOpenConns(100) // max open connections
sqlDB.SetConnMaxLifetime(time.Hour) // recycle connections after an hour
// Health check
sqlDB.Ping()
// Close (rarely needed; usually you keep the pool for the app's lifetime)
sqlDB.Close()
13. Sessions, Context, and Debugging
A session produces a new *gorm.DB with overridden settings, without mutating the original.
// Per-request context (for cancellation / deadlines)
db.WithContext(ctx).Find(&users)
// One-off settings
tx := db.Session(&gorm.Session{
PrepareStmt: true, // cache prepared statements for this session
SkipDefaultTransaction: true,
FullSaveAssociations: true,
})
// Print the SQL of a single call
db.Debug().Where("name = ?", "jinzhu").First(&user)
Tip
db.Debug()switches the logger toInfolevel for one chain so you can see the exact SQL and timing in your console — invaluable when a query behaves unexpectedly.
14. Hooks (Lifecycle Callbacks)
Implement the relevant method on your model to run code before/after an operation. Each hook receives the active *gorm.DB and returns an error (returning an error aborts the operation and rolls back the surrounding transaction).
| Hook | When it runs |
|---|---|
BeforeCreate(tx *gorm.DB) error |
Before INSERT |
AfterCreate(tx *gorm.DB) error |
After INSERT |
BeforeUpdate(tx *gorm.DB) error |
Before UPDATE |
AfterUpdate(tx *gorm.DB) error |
After UPDATE |
BeforeSave(tx *gorm.DB) error |
Before any INSERT or UPDATE |
AfterSave(tx *gorm.DB) error |
After any INSERT or UPDATE |
BeforeDelete(tx *gorm.DB) error |
Before DELETE |
AfterDelete(tx *gorm.DB) error |
After DELETE |
AfterFind(tx *gorm.DB) error |
After a row is loaded by a query |
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Name == "admin" {
return errors.New("cannot create reserved user")
}
u.UUID = uuid.New().String()
return nil
}
Note
Hooks do not run for
UpdateColumn(s)or when a session hasSkipHooks: true.
15. Error Handling
Every finisher sets db.Error. GORM exports sentinel errors you can compare with errors.Is.
result := db.First(&user, 100)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// no user with id 100
}
Commonly used sentinel errors:
| Error | Meaning |
|---|---|
gorm.ErrRecordNotFound |
First/Take/Last found no row |
gorm.ErrDuplicatedKey |
Unique-key constraint violation |
gorm.ErrForeignKeyViolated |
Foreign-key constraint violation |
gorm.ErrCheckConstraintViolated |
CHECK constraint violation |
gorm.ErrMissingWhereClause |
A global update/delete was blocked |
gorm.ErrInvalidTransaction |
Commit/Rollback with no active transaction |
gorm.ErrPrimaryKeyRequired |
An operation needed a primary key that was missing |
gorm.ErrInvalidValue |
Destination is not a pointer to a struct or slice |
gorm.ErrEmptySlice |
Tried to create from an empty slice |
Important
gorm.ErrDuplicatedKey,gorm.ErrForeignKeyViolated, andgorm.ErrCheckConstraintViolatedare only returned when error translation is enabled withTranslateError: truein the config and your driver supports it. Without it, you receive the raw driver error.
16. The Generics API (Go 1.18+)
In addition to the classic chainable API, GORM offers a type-safe generics API via gorm.G[T], which carries a context.Context on every finisher and returns typed values instead of writing into a pointer.
import "context"
ctx := context.Background()
// Create
user := User{Name: "jinzhu"}
err := gorm.G[User](db).Create(ctx, &user)
// Query — returns a typed value, plus error
u, err := gorm.G[User](db).Where("id = ?", 1).First(ctx)
users, err := gorm.G[User](db).Where("age > ?", 18).Find(ctx)
// Update / Delete return rows affected
rows, err := gorm.G[User](db).Where("id = ?", 1).Update(ctx, "name", "new")
rows, err = gorm.G[User](db).Where("age < ?", 18).Delete(ctx)
// Capture sql.Result + RowsAffected with WithResult()
result := gorm.WithResult()
err = gorm.G[User](db, result).Create(ctx, &user)
_ = result.RowsAffected
The generics builder supports the same chain methods you already know — Where, Not, Or, Limit, Offset, Select, Omit, Order, Group, Having, Distinct, Joins, Preload, Scopes, Table, and Raw/Exec.
Tip
Use the generics API when you want compile-time type safety and explicit context propagation. Use the classic API for its broader ecosystem of examples and plugins. They share the same engine and can be mixed in one program.
17. Reusable Query Logic with Scopes
A scope is a function that takes and returns a *gorm.DB. Scopes let you package and reuse query fragments:
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("active = ?", true)
}
func OlderThan(age int) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("age > ?", age)
}
}
db.Scopes(ActiveUsers, OlderThan(18)).Find(&users)
18. Troubleshooting
Error: record not found
Cause: First, Take, or Last matched no rows. GORM returns gorm.ErrRecordNotFound.
Solution: Check explicitly and treat it as a normal "missing" case, not a failure:
if err := db.First(&user, id).Error; errors.Is(err, gorm.ErrRecordNotFound) {
// handle "not found"
}
Prevention: Use Find (which returns an empty slice and no error) when zero results are acceptable, and check RowsAffected.
My struct condition / update silently ignores some fields
Cause: When you pass a struct to Where or Updates, GORM skips zero-value fields (0, "", false, nil).
Solution: Use a map[string]interface{}, or Select the column names explicitly.
Prevention: Reserve structs for non-zero filters; use maps whenever a zero value is meaningful.
Error: WHERE conditions required
Cause: You issued an Update or Delete with no primary key and no Where clause — GORM blocks "global" writes (ErrMissingWhereClause).
Solution: Add a condition, or, if you truly mean to affect all rows, enable global updates: db.Session(&gorm.Session{AllowGlobalUpdate: true}).
How do I detect a duplicate-key error? (community: issue #4037)
Cause: Each database reports unique-constraint violations differently, so the raw error string varies.
Solution: Enable TranslateError: true in gorm.Config and compare against gorm.ErrDuplicatedKey:
db, _ := gorm.Open(dialector, &gorm.Config{TranslateError: true})
if errors.Is(db.Create(&user).Error, gorm.ErrDuplicatedKey) { /* ... */ }
How do I do global / centralized error handling? (community: issue #7158)
Solution: Register a custom callback or wrap operations in a helper that inspects db.Error after each call. GORM does not provide a single global error handler; the idiomatic approach is to check result.Error (or use the generics API which returns error directly) and centralize handling in your repository layer.
Joins on two unrelated tables produces no rows / a bad query (community: issue #7745)
Cause: Joining by association name (Joins("Orders")) only works when the two models actually have a defined relationship. Without one, GORM cannot infer the join condition.
Solution: Use a raw join with an explicit ON clause: db.Joins("JOIN orders ON orders.user_id = users.id").Find(&users).
SQLite build fails with a C compiler error
Cause: gorm.io/driver/sqlite relies on cgo (mattn/go-sqlite3).
Solution: Install a C toolchain (e.g. GCC / Xcode command-line tools) and ensure CGO_ENABLED=1. Alternatively use a pure-Go database like PostgreSQL or MySQL for development.
Created record has ID == 0 after Create
Cause: You passed a value instead of a pointer.
Solution: Always pass a pointer: db.Create(&user), not db.Create(user).
19. FAQ
Q: What databases does GORM support?
GORM is database-agnostic via driver packages: MySQL, PostgreSQL, SQLite, SQL Server, and GaussDB are maintained under gorm.io/driver/*, and the community maintains others. You pick the driver at gorm.Open time.
Q: Do I have to write any SQL?
No, the query builder covers the vast majority of needs. But you can always drop to Raw/Exec for arbitrary SQL, and inspect generated SQL with ToSQL or Debug.
Q: How do I see the SQL GORM generates?
Call db.Debug() for a single chain, set the logger to Info level globally, or use db.ToSQL(func...) to capture the statement as a string.
Q: Is a *gorm.DB safe to share across goroutines?
Yes. Create it once at startup and reuse it; it manages a connection pool internally. Per-request state (context, conditions) is isolated via method chaining and sessions.
Q: Why didn't my Updates(User{Active: false}) change anything?
Struct updates skip zero values. Use a map (Updates(map[string]interface{}{"active": false})) or Select("active").
Q: How do I update or delete every row on purpose?
GORM blocks unconditioned writes. Opt in with a session: db.Session(&gorm.Session{AllowGlobalUpdate: true}).
Q: What's the difference between Save, Update, and Updates?
Update sets one column; Updates sets several (struct = non-zero fields, map = exact columns); Save upserts the entire record (insert if no primary key, otherwise update all columns).
Q: What's the difference between First and Find?
First fetches one row ordered by primary key and returns ErrRecordNotFound when empty; Find fetches into a slice (or struct) and returns no error for empty results.
Q: How do I improve performance for repeated queries?
Enable prepared-statement caching with PrepareStmt: true (in config or a session). GORM caches and reuses prepared statements with an LRU + TTL.
Q: How do I permanently delete a soft-deletable record?
Use db.Unscoped().Delete(&record).
Q: Should I use the classic API or the generics API?
Both are fully supported and share the same engine. Choose the generics API (gorm.G[T]) for compile-time type safety and explicit context; choose the classic chainable API for the larger body of examples and plugins. You can mix them.