Jesse A. Green / notebook

GORM User Guide

Generated June 2026 from go-gorm/gorm at d0ee5e2

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 example gorm.io/driver/sqlite or gorm.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.DB from 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 as Find, First, Create, Save, Update, Delete, Count, or Scan.


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 on github.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 err returned by gorm.Open. On success, GORM by default pings the database to verify the connection (you can disable this with DisableAutomaticPing: true in the config).

Tip

Reuse a single *gorm.DB for 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.DB and 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 CreatedAt and UpdatedAt (of type time.Time or 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, and gorm:"-" 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) to Create. 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 applies OnConflict{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, and Last return gorm.ErrRecordNotFound when 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 a Select of 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 Limit or Offset by 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 Updates with a struct skips zero-value fields, db.Model(&user).Updates(User{Active: false}) will not set active to false. Use a map, or Select the 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 WHERE condition and no primary key — is blocked by default and returns ErrMissingWhereClause, to protect you from accidentally changing every row. To intentionally update all rows, enable AllowGlobalUpdate via 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 a Where clause.

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 zeroValue tag, e.g. gorm:"zeroValue:1970-01-01 00:00:01", when your column is NOT NULL and cannot store SQL NULL.


10. Transactions

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 outer db. Operations on db run 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, and Delete in its own transaction for data integrity. If you are doing many writes and want more speed, set SkipDefaultTransaction: true in 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 to Info level 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 has SkipHooks: 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, and gorm.ErrCheckConstraintViolated are only returned when error translation is enabled with TranslateError: true in 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.