Skip to content

Go GORM — ORM for Go with GORM Library and Database Migrations

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Go GORM. We cover key concepts, practical examples, and best practices to help you master this topic.

Go GORM provides an ORM with automatic migrations, association management, query building, and hooks for PostgreSQL, MySQL, and SQLite.

What You'll Learn

  • Model definition and tags
  • Auto Migration
  • CRUD operations
  • Associations and hooks

Why It Matters

GORM speeds up database development. DodaZIP uses GORM for file processing metadata. Many Go web applications use GORM for rapid prototyping.

Real-World Use

Web application backends, content management systems, e-commerce platforms, data administration tools.

flowchart LR
    A["GORM"] --> B["Models"]
    A --> C["AutoMigrate"]
    A --> D["CRUD"]
    A --> E["Associations"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Model Definition

type User struct {
    ID        uint           `gorm:"primarykey"`
    Name      string         `gorm:"size:100;not null"`
    Email     string         `gorm:"uniqueIndex;size:255"`
    Age       int            `gorm:"default:18"`
    Active    bool           `gorm:"default:true"`
    Profile   Profile        `gorm:"foreignKey:UserID"`
    Orders    []Order        `gorm:"foreignKey:UserID"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
}

Connecting and Migration

func main() {
    dsn := "host=localhost user=postgres password=pass dbname=app port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil {
        log.Fatal(err)
    }

    db.AutoMigrate(&User{}, &Profile{}, &Order{})
}

Create Operations

func main() {
    user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    result := db.Create(&user)
    fmt.Println("ID:", user.ID)
    fmt.Println("Rows affected:", result.RowsAffected)
    fmt.Println("Error:", result.Error)

    // Batch insert
    users := []User{
        {Name: "Bob", Email: "bob@example.com"},
        {Name: "Charlie", Email: "charlie@example.com"},
    }
    db.Create(&users)
}

Query Operations

// Find by ID
var user User
db.First(&user, 1)
db.First(&user, "id = ?", 1)

// Find with conditions
db.Where("name = ?", "Alice").First(&user)
db.Where("age > ?", 25).Find(&users)
db.Where("name LIKE ?", "%A%").Find(&users)

// Order, Limit, Offset
db.Order("age desc").Limit(10).Offset(0).Find(&users)

// Select specific fields
db.Select("name", "email").Find(&users)

// Count
var count int64
db.Model(&User{}).Where("active = ?", true).Count(&count)

Update Operations

// Update single field
db.Model(&user).Update("name", "Alice Smith")

// Update multiple fields
db.Model(&user).Updates(User{Name: "Alice", Age: 31})

// Update with map
db.Model(&user).Updates(map[string]interface{}{
    "name": "Alice",
    "age":  31,
})

// Update with conditions
db.Model(&User{}).Where("active = ?", false).Update("active", true)

Delete Operations

// Soft delete (requires DeletedAt field)
db.Delete(&user, 1)

// Hard delete
db.Unscoped().Delete(&user)

// Delete with conditions
db.Where("active = ?", false).Delete(&User{})

Associations

type Profile struct {
    ID     uint `gorm:"primarykey"`
    UserID uint
    Bio    string
    Avatar string
}

type Order struct {
    ID       uint `gorm:"primarykey"`
    UserID   uint
    Product  string
    Amount   float64
}

func main() {
    // Preload associations
    var users []User
    db.Preload("Profile").Preload("Orders").Find(&users)

    // Nested preloading
    db.Preload("Orders.Items").Find(&users)

    // Joins
    db.Joins("Profile").Find(&users)
}

Hooks

type User struct {
    ID        uint
    Password  string
    // ...
}

func (u *User) BeforeCreate(tx *gorm.DB) error {
    hashed, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    u.Password = string(hashed)
    return nil
}

func (u *User) AfterCreate(tx *gorm.DB) error {
    tx.Model(u).Update("name", "User: "+u.Name)
    return nil
}

Common Mistakes

1. Not Handling Duplicate Errors

result := db.Create(&user)
if result.Error != nil {
    // Handle unique constraint violations
    if strings.Contains(result.Error.Error(), "duplicate key") { /* handle */ }
}

2. N+1 Query Problem

// Bad: Accessing relations without preloading causes N+1
for _, u := range users {
    fmt.Println(u.Profile.Bio)  // Executes a query each iteration
}
// Good: db.Preload("Profile").Find(&users)

3. Not Using Transactions

Use db.Transaction for operations that modify multiple records.

4. Ignoring Soft Delete

GORM soft deletes by default. Use Unscoped() for hard deletes.

5. Missing Indexes

Add gorm:"index" to frequently queried fields for performance.

Practice Questions

1. What does AutoMigrate do? Creates/updates database tables to match model structs. Does not delete columns or data.

2. How do you prevent SQL Injection with GORM? GORM uses parameterized queries internally. Use ? placeholders in Where clauses.

3. What is the difference between First and Find? First returns one record, adds ORDER BY id LIMIT 1. Find returns all matching records.

4. How do you implement pagination? Use Scopes: db.Scopes(Paginate(page, pageSize)).Find(&users). Or manually: Limit/Offset.

Challenge: Implement a paginated query scope in GORM.

Solution
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        if page <= 0 { page = 1 }
        if pageSize <= 0 { pageSize = 10 }
        offset := (page - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}

// Usage
db.Scopes(Paginate(1, 20)).Find(&users)

FAQ

{{< faq question="Should I use GORM or raw SQL?" >}} GORM for development speed, associations, and migrations. Raw SQL for complex queries and performance-critical paths. You can use both in the same project. {{< /faq >}}

{{< faq question="How does GORM handle migrations?" >}} AutoMigrate is safe for development. For production, use versioned migrations with golang-migrate or goose. {{< /faq >}}

{{< faq question="Can I use GORM with transactions?" >}} Yes. db.Transaction(func(tx *gorm.DB) error { ... }) handles commit and rollback automatically. {{< /faq >}}

{{< faq question="What is the difference between gorm.Model and custom struct?" >}} gorm.Model includes ID, CreatedAt, UpdatedAt, DeletedAt. Custom structs give you full control over fields and tags. {{< /faq >}}

{{< faq question="How do I log GORM queries?" >}} Configure logger in gorm.Config: gorm.Config{Logger: logger.Default.LogMode(logger.Info)}. Levels: Silent, Error, Warn, Info. {{< /faq >}}

Try It Yourself

package main

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

type Product struct {
    ID    uint   `gorm:"primarykey"`
    Name  string
    Price float64
}

func main() {
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    db.AutoMigrate(&Product{})

    db.Create(&Product{Name: "Widget", Price: 9.99})

    var p Product
    db.First(&p, 1)
    fmt.Printf("%s: $%.2f\n", p.Name, p.Price)
}

Expected output:

Widget: $9.99

What's Next

Now that you understand GORM, explore Go generics for type-safe reusable code.

Topic Description Link
Go Generics Type parameters {{< ref "31-generics" >}}
Go Database SQL Raw SQL operations {{< ref "29-database-sql" >}}
Go HTTP Servers Building web servers {{< ref "27-http-server" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go