GORM 上手:一个 main.go 跑通 Go 数据库增删改查

GORM 上手:一个 main.go 跑通 Go 数据库增删改查

这篇文章用一个博客系统的后端做例子,从零开始演示 GORM 的模型定义、数据库连接、CRUD 操作、关联查询和事务处理。所有代码放在一个文件里,复制就能跑。适合有 Go 基础但没用过 ORM 的开发者。


先看对比

database/sql 插入一条记录:

go 复制代码
stmt, err := db.Prepare("INSERT INTO articles(title, content, author, views) VALUES(?, ?, ?, ?)")
if err != nil { return err }
result, err := stmt.Exec("GORM 入门", "正文...", "张三", 0)
if err != nil { return err }
id, _ := result.LastInsertId()

用 GORM 做同样的事:

go 复制代码
db.Create(&Article{Title: "GORM 入门", Content: "正文...", Author: "张三"})

字段少的时候差别不大。等你的表有 15 个字段、还要处理 NULL 和时间格式的时候,手写 SQL 的维护成本会陡增。GORM 把这些事收敛到 struct tag 里,你只管定义结构体。


工作原理

复制代码
┌──────────────┐     ┌──────────┐     ┌────────────┐     ┌──────────────┐
│   你的代码    │ ──▶ │   GORM   │ ──▶ │   Driver   │ ──▶ │   Database   │
│  (struct)    │     │ (链式API) │     │ (mysql/pg) │     │  (MySQL等)   │
└──────────────┘     └──────────┘     └────────────┘     └──────────────┘

GORM 在你的 Go struct 和数据库之间做翻译。你操作 struct,它生成 SQL 发给数据库驱动。切换数据库只需要换 Driver,业务代码不用动。


安装

Go 1.18+,执行:

bash 复制代码
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

其他数据库把 sqlite 换成 mysqlpostgres


定义模型

我们做一个博客系统,先定义文章表:

go 复制代码
type Article struct {
    gorm.Model
    Title   string `gorm:"size:200;not null"`
    Content string `gorm:"type:text"`
    Author  string `gorm:"size:100"`
    Views   int    `gorm:"default:0"`
}

嵌入 gorm.Model 会带上四个字段:ID(主键)、CreatedAt、UpdatedAt、DeletedAt(软删除用)。

对应的表结构:

复制代码
articles
├── id          uint        主键自增
├── title       varchar(200)
├── content     text
├── author      varchar(100)
├── views       int         默认 0
├── created_at  datetime
├── updated_at  datetime
└── deleted_at  datetime    软删除标记

连接数据库 + 建表

go 复制代码
package main

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

func main() {
    db, err := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info), // 开发时打开,能看到生成的 SQL
    })
    if err != nil {
        panic(err)
    }

    db.AutoMigrate(&Article{})
}

换 MySQL 改一行:

go 复制代码
import "gorm.io/driver/mysql"

dsn := "user:pass@tcp(127.0.0.1:3306)/blog?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

AutoMigrate 会建表,表已存在则补齐新加的字段。注意它不会删列也不会改类型,生产环境用 golang-migrate 之类的工具管理 schema 版本。


创建

go 复制代码
article := Article{
    Title:   "Go 并发模式详解",
    Content: "goroutine 和 channel 的实际用法...",
    Author:  "张三",
}
db.Create(&article)

fmt.Println(article.ID) // 插入后 ID 自动回填

批量插入:

go 复制代码
articles := []Article{
    {Title: "文章一", Author: "张三"},
    {Title: "文章二", Author: "李四"},
}
db.Create(&articles)

查询

go 复制代码
var art Article

// 按主键
db.First(&art, 1)

// 条件查询
var list []Article
db.Where("author = ?", "张三").Find(&list)

// 链式:阅读量 > 100,按时间倒序,取前 10 条
db.Where("views > ?", 100).Order("created_at desc").Limit(10).Find(&list)

First 找不到记录时返回 gorm.ErrRecordNotFound

go 复制代码
result := db.First(&art, "title = ?", "不存在")
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
    fmt.Println("没找到")
}

更新

go 复制代码
// 单字段
db.Model(&art).Update("Views", 200)

// 多字段
db.Model(&art).Updates(Article{Title: "新标题", Views: 300})

这里有个坑:用 struct 更新时,Go 的零值(0、""、false)会被 GORM 忽略。比如你想把 Views 设回 0:

go 复制代码
// 这样不行,Views 是零值会被跳过
db.Model(&art).Updates(Article{Views: 0})

// 用 map 或 Select 解决
db.Model(&art).Updates(map[string]interface{}{"Views": 0})
db.Model(&art).Select("Views").Updates(Article{Views: 0})

删除

go 复制代码
// 软删除:设置 deleted_at 字段,记录还在数据库里
db.Delete(&art, 1)

// 查询时默认过滤已软删除的记录
// 想查所有记录(包括已删除的):
db.Unscoped().Find(&list)

// 真删除:从数据库移除
db.Unscoped().Delete(&art, 1)

关联查询

博客系统里,一个用户有多篇文章:

go 复制代码
type User struct {
    gorm.Model
    Name     string
    Articles []Article
}

type Article struct {
    gorm.Model
    Title  string
    UserID uint
}

关系图:

复制代码
┌────────────┐         ┌────────────┐
│    User    │ 1 ──▶ N │  Article   │
├────────────┤         ├────────────┤
│ ID         │         │ ID         │
│ Name       │         │ Title      │
│ CreatedAt  │         │ UserID(FK) │
└────────────┘         │ CreatedAt  │
                       └────────────┘

查询用户时预加载文章:

go 复制代码
var user User
db.Preload("Articles").First(&user, 1)

for _, a := range user.Articles {
    fmt.Println(a.Title)
}

不用 Preload 的话,访问 user.Articles 不会自动查询。如果你在循环里逐个加载关联,就会产生 N+1 问题,数据量大了性能很差。


Hook

在数据操作前后插入逻辑:

go 复制代码
func (a *Article) BeforeCreate(tx *gorm.DB) error {
    if a.Title == "" {
        return errors.New("标题不能为空")
    }
    return nil
}

返回 error 会中止操作并回滚事务。适合做校验、自动填充时间戳、记日志。


事务

多个操作要么全成功要么全失败:

go 复制代码
err := db.Transaction(func(tx *gorm.DB) error {
    if err := tx.Create(&article1).Error; err != nil {
        return err
    }
    if err := tx.Create(&article2).Error; err != nil {
        return err
    }
    return nil
})

回调里返回 error 自动回滚,返回 nil 自动提交。


完整可运行代码

go 复制代码
package main

import (
    "errors"
    "fmt"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

type Article struct {
    gorm.Model
    Title   string `gorm:"size:200;not null"`
    Content string `gorm:"type:text"`
    Author  string `gorm:"size:100"`
    Views   int    `gorm:"default:0"`
}

func (a *Article) BeforeCreate(tx *gorm.DB) error {
    if a.Title == "" {
        return errors.New("标题不能为空")
    }
    return nil
}

func main() {
    db, err := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil {
        panic(err)
    }

    db.AutoMigrate(&Article{})

    // 创建
    db.Create(&Article{
        Title:   "Go 错误处理的几种写法",
        Content: "从 if err != nil 到 errors.Is...",
        Author:  "开发者小王",
    })

    // 查询
    var articles []Article
    db.Where("author = ?", "开发者小王").Find(&articles)
    for _, a := range articles {
        fmt.Printf("#%d %s (views: %d)\n", a.ID, a.Title, a.Views)
    }

    // 更新
    if len(articles) > 0 {
        db.Model(&articles[0]).Update("Views", 42)
    }

    // 软删除
    if len(articles) > 0 {
        db.Delete(&articles[0])
    }

    // 验证软删除生效
    var count int64
    db.Model(&Article{}).Count(&count)
    fmt.Printf("可见文章数: %d\n", count)
}

运行:

bash 复制代码
go mod init blog-demo && go mod tidy && go run main.go

预期输出:

复制代码
#1 Go 错误处理的几种写法 (views: 0)
可见文章数: 0

什么时候不该用 GORM

几种情况下你可能更适合其他方案:

  • 查询逻辑很复杂(多层子查询、窗口函数),GORM 的链式 API 表达起来反而比原生 SQL 更绕
  • 对性能有极致要求,需要精确控制每条 SQL 的执行计划
  • 团队更习惯 SQL 优先的工作方式,可以看看 sqlc(写 SQL 生成 Go 代码)

对于大多数 Web 后端的常规 CRUD,GORM 省事且够用。

相关推荐
lld9510271 小时前
(一)云回测:量化策略上线前的必经之路
java·服务器·数据库
Old Uncle Tom2 小时前
Harness Engineering 综述
java·开发语言·数据库
疯狂打码的少年2 小时前
Cache的三种映射方式(直接/全相联/组相联)
linux·服务器·数据库·笔记
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL备份与恢复Day14(2026年)
数据库·后端·mysql
知彼解己3 小时前
RAG 核心实战:检索增强生成
后端·golang·ai编程
渣渣盟3 小时前
MySQL DDL操作全解析:从入门到精通,包含索引视图分区表等全操作解析
大数据·数据库·mysql
小小工匠3 小时前
Redis - 基本架构:一个键值数据库到底由什么组成
数据库·redis·架构
mN9B2uk173 小时前
为mysql数据库建立索引
数据库·mysql·oracle
SilentSamsara3 小时前
SQLAlchemy 2.x:异步 ORM 与数据库迁移 Alembic 完整指南
开发语言·数据库·python·sql·青少年编程·oracle·fastapi