How to use GORM

Initialize GORM by opening a connection, defining your data models as structs, and using built-in methods to create, read, update, and delete records.

When SQL becomes a chore

You are building a service that tracks inventory. Every time you add a field to a struct, you update five different SQL strings. You rename a column and three migration scripts break. You want to write Go code that talks to the database without juggling raw queries and manual type conversions. GORM is the bridge that lets you treat your database tables as collections of structs.

GORM stands for Go Object Relational Mapper. It translates Go structs into database rows and database rows back into Go structs. Think of it as a translator that sits between your application logic and the database engine. You define the shape of your data in Go, and GORM generates the SQL behind the scenes. The library uses reflection to read struct tags and types at runtime. This makes it flexible but adds overhead. The code runs slower than raw SQL because GORM has to inspect your types on every call. For most web services, the trade-off is worth it. You gain developer speed and type safety. You lose a small amount of execution time.

ORMs trade a few milliseconds of execution time for hours of developer sanity. Pick the trade that matches your deadline.

The minimal setup

Here is the smallest working setup that connects to a local database, creates a table, and saves a record.

package main

import (
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// User maps to a table named "users".
// GORM pluralizes struct names for table names by default.
type User struct {
	ID   uint   // GORM assumes "id" is the primary key
	Name string
}

func main() {
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{}) // Open local SQLite
	db.AutoMigrate(&User{})                                     // Create table if missing
	db.Create(&User{Name: "Jinzhu"})                           // Insert row, update ID
	var user User
	db.First(&user, 1)                                         // Select first matching row
}

When you run this, GORM inspects the User struct. It looks for a field named ID with a type like uint or int64. It assumes this is the primary key. It checks the struct name User and converts it to users for the table name. AutoMigrate runs a CREATE TABLE IF NOT EXISTS query. If the table exists, it checks for missing columns and adds them. Create generates an INSERT statement. First generates a SELECT with a LIMIT 1. The result is scanned back into the user variable.

The library relies on reflection to map Go types to SQL types. This means you do not write INSERT INTO users (name) VALUES (?). You write db.Create(&User{Name: "Jinzhu"}). The compiler cannot verify the SQL at compile time. The verification happens at runtime. If your struct field does not match a database column, GORM either ignores it or panics depending on the configuration.

Convention aside: db.AutoMigrate is safe for development. In production, migrations usually need a dedicated tool like golang-migrate or goose. ORMs struggle with destructive changes like dropping columns. Auto-migrate protects data by design. It rarely deletes anything.

Reflection is the engine under the hood. It costs CPU cycles, but it buys you type safety and less boilerplate.

Realistic usage with context and errors

Production code handles cancellations, returns errors, and manages relationships without panicking.

// Product represents a row in the products table.
// Tags control column names and constraints.
type Product struct {
	gorm.Model // Embeds ID, CreatedAt, UpdatedAt, DeletedAt
	Code       string `gorm:"uniqueIndex:idx_code"`
	Price      float64
}

// GetProductHandler returns a product by ID.
// It accepts context to respect request cancellation.
func GetProductHandler(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context() // ctx carries the request lifecycle
		var product Product
		result := db.WithContext(ctx).First(&product, 1) // Query respects ctx timeout
		if result.Error != nil { // GORM returns errors, not panics
			if result.Error == gorm.ErrRecordNotFound {
				http.Error(w, "not found", http.StatusNotFound)
			} else {
				http.Error(w, "database error", http.StatusInternalServerError)
			}
			return
		}
		w.Write([]byte(product.Code))
	}
}

GORM methods return a *gorm.DB. The error lives in result.Error. If you chain calls like db.Where(...).Find(&users), the error is only available at the end of the chain. You must check result.Error after the chain completes. If you forget to check the error, you might process an empty struct. The compiler will not stop you. GORM returns a zero-value struct on failure.

Convention aside: context.Context always goes as the first parameter, conventionally named ctx. Functions that take a context should respect cancellation and deadlines. GORM respects context cancellation. If a request times out, the database query stops. Also, if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. GORM follows this pattern. Check errors. Do not swallow them.

Context is plumbing. Run it through every long-lived call site.

Associations and the N+1 trap

GORM handles relationships between tables. You can define one-to-one, one-to-many, and many-to-many associations using struct fields.

type User struct {
	ID    uint
	Name  string
	Posts []Post `gorm:"foreignKey:AuthorID"`
}

type Post struct {
	ID       uint
	Title    string
	AuthorID uint
}

When you query a User, GORM does not load the Posts by default. This prevents accidental performance hits. You must explicitly ask for associations using Preload.

var user User
// Preload fetches associated records in a separate query.
// This avoids the N+1 problem where you query posts for each user.
db.Preload("Posts").First(&user, 1)

If you iterate over users and access user.Posts without preloading, GORM runs a query for each user. This is the N+1 query problem. It kills performance. Always preload associations when you need them. If you need nested associations, chain preloads: db.Preload("Posts").Preload("Posts.Comments").

Convention aside: Foreign keys in GORM are inferred from the association name. Posts implies a foreign key UserID on the Post table. If your database uses a different name, specify it with the foreignKey tag. GORM tries to guess. When it guesses wrong, the query fails silently or returns empty results. Explicit tags save debugging time.

Let the database do the heavy lifting. Fetch related data in one pass, not ten.

Pitfalls and compiler errors

GORM is helpful but hides complexity. The magic can bite you.

If you try to save a struct with a zero-value ID, GORM treats it as a new record and runs INSERT. If you want to update, you must set the ID. The compiler rejects db.Update if you pass a value type instead of a pointer for the destination. You get cannot use user (type User) as *User value in argument. Always pass pointers to structs when querying or creating.

If you forget to capture the loop variable, the compiler rejects the program with loop variable i captured by func literal. This matters when you spawn goroutines inside a loop to process database results. The goroutine will close over the same variable, leading to race conditions and incorrect data.

GORM uses table names that are pluralized struct names. If your database has a table named user, GORM looks for users. Use gorm:"table:user" tag to override. The compiler complains with table users doesn't exist if the names do not match.

The worst ORM bug is the one that returns a zero-value struct silently. Always check result.Error. If you ignore errors, you might think you loaded data when you actually loaded nothing.

Convention aside: _ discards a value intentionally. result, _ := ... says "I considered the second return value and chose to drop it". Use it sparingly with errors. In database code, swallowing errors with _ is a fast track to data corruption.

The worst goroutine bug is the one that never logs. Check result.Error every time.

When to use GORM

GORM is a tool. It fits some jobs and not others.

Use GORM when you are building a CRUD-heavy application and want to move fast. Use GORM when your team is more comfortable with Go structs than SQL dialects. Use GORM when you need built-in migrations and association handling for a standard web service. Use database/sql when you need complex joins, window functions, or strict performance control. Use a query builder like squirrel when you want type safety without the magic of an ORM. Pick raw SQL when the query is too complex for the ORM to express cleanly.

GORM handles the SQL. You handle the logic. Trust the ORM for simple things. Write SQL for the hard things.

Where to go next