比较适用于 PostgreSQL 的 Go ORM:GORM 与 Ent 与 Bun 与 sqlc

对Go语言中ORM的实用且代码密集的探讨

目录

最突出的 Go语言ORM框架 包括 GORM、Ent、Bun 和 sqlc。 这里通过一些使用纯 Go 实现的 CRUD 操作示例,对它们进行简要比较。

golang + postgresql

TL;DR

  • GORM:功能丰富且使用方便;最容易“直接部署”,但运行时开销较大。
  • Ent:通过代码生成实现的类型安全 API;非常适合大型代码库和重构。
  • Bun:轻量级、以 SQL 优先的查询构建器/ORM;速度快,PostgreSQL 功能强大,设计明确。
  • sqlc(虽然不是 ORM,但仍然):编写 SQL,获取类型安全的 Go;性能最佳,控制最精细,没有运行时的“魔法”。

选择标准和快速比较

我的选择标准是:

  • 性能:延迟/吞吐量,可避免的开销,批量操作。
  • 开发体验(DX):学习曲线,类型安全性,调试能力,代码生成的摩擦。
  • 生态系统:文档、示例、活动性、集成(迁移、追踪)。
  • 功能集:关系、预加载、迁移、钩子、原始 SQL 逃生口。
工具 设计范式 类型安全性 关系 迁移 原始 SQL 便捷性 典型使用场景
GORM Active Record 风格的 ORM 中等(运行时) 是(标签、Preload/Joins) 自动迁移(可选) db.Raw(...) 快速交付,功能丰富,传统 CRUD 应用
Ent Schema → 代码生成 → 流畅 API 高(编译时) 一流(边) 生成 SQL(单独步骤) entsql,自定义 SQL 大型代码库,重构密集型团队,严格类型
Bun SQL 优先的查询构建器/ORM 中等–高 明确(Relation 单独的迁移包 自然(构建器 + 原始) 注重性能的服务,PostgreSQL 功能
sqlc SQL → 代码生成函数(不是 ORM) 高(编译时) 通过 SQL 连接 外部工具(如 golang-migrate) 就是 SQL 最大控制与速度;DBA 友好团队

通过示例实现 CRUD

设置(PostgreSQL)

使用 pgx 或工具原生的 PG 驱动。示例 DSN:

export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'

导入(所有 ORM 都通用)

在每个包含 Go代码 示例的文件开头添加:

import (
  "context"
  "os"
)

我们将建模一个简单的 users 表:

CREATE TABLE IF NOT EXISTS users (
  id    BIGSERIAL PRIMARY KEY,
  name  TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE
);

GORM

初始化

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

type User struct {
  ID    int64  `gorm:"primaryKey"`
  Name  string
  Email string `gorm:"uniqueIndex"`
}

func newGorm() (*gorm.DB, error) {
  dsn := os.Getenv("DATABASE_URL")
  return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}

// 自动迁移(可选;生产环境要小心)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

func gormCRUD(ctx context.Context, db *gorm.DB) error {
  // 创建
  u := User{Name: "Alice", Email: "alice@example.com"}
  if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }

  // 读取
  var got User
  if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }

  // 更新
  if err := db.WithContext(ctx).Model(&got).
    Update("email", "alice+1@example.com").Error; err != nil { return err }

  // 删除
  if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }

  return nil
}

备注

  • 通过结构体标签 + Preload/Joins 实现关系。
  • 事务辅助函数:db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

模式定义(在 ent/schema/user.go 中):

package schema

import (
  "entgo.io/ent"
  "entgo.io/ent/schema/field"
)

type User struct {
  ent.Schema
}

func (User) Fields() []ent.Field {
  return []ent.Field{
    field.Int64("id").Unique().Immutable(),
    field.String("name"),
    field.String("email").Unique(),
  }
}

生成代码

go run entgo.io/ent/cmd/ent generate ./ent/schema

初始化

import (
  "entgo.io/ent/dialect"
  "entgo.io/ent/dialect/sql"
  _ "github.com/jackc/pgx/v5/stdlib"
  "your/module/ent"
)

func newEnt() (*ent.Client, error) {
  dsn := os.Getenv("DATABASE_URL")
  drv, err := sql.Open(dialect.Postgres, dsn)
  if err != nil { return nil, err }
  return ent.NewClient(ent.Driver(drv)), nil
}

CRUD

func entCRUD(ctx context.Context, client *ent.Client) error {
  // 创建
  u, err := client.User.Create().
    SetName("Alice").
    SetEmail("alice@example.com").
    Save(ctx)
  if err != nil { return err }

  // 读取
  got, err := client.User.Get(ctx, u.ID)
  if err != nil { return err }

  // 更新
  if _, err := client.User.UpdateOneID(got.ID).
    SetEmail("alice+1@example.com").
    Save(ctx); err != nil { return err }

  // 删除
  if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }

  return nil
}

备注

  • 端到端强类型;使用边实现关系。
  • 生成迁移或使用您选择的迁移工具。

Bun

初始化

import (
  "database/sql"

  "github.com/uptrace/bun"
  "github.com/uptrace/bun/dialect/pgdialect"
  _ "github.com/jackc/pgx/v5/stdlib"
)

type User struct {
  bun.BaseModel `bun:"table:users"`
  ID    int64  `bun:",pk,autoincrement"`
  Name  string `bun:",notnull"`
  Email string `bun:",unique,notnull"`
}

func newBun() (*bun.DB, error) {
  dsn := os.Getenv("DATABASE_URL")
  sqldb, err := sql.Open("pgx", dsn)
  if err != nil { return nil, err }
  return bun.NewDB(sqldb, pgdialect.New()), nil
}

CRUD

func bunCRUD(ctx context.Context, db *bun.DB) error {
  // 创建
  u := &User{Name: "Alice", Email: "alice@example.com"}
  if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }

  // 读取
  var got User
  if err := db.NewSelect().Model(&got).
    Where("id = ?", u.ID).
    Scan(ctx); err != nil { return err }

  // 更新
  if _, err := db.NewUpdate().Model(&got).
    Set("email = ?", "alice+1@example.com").
    WherePK().
    Exec(ctx); err != nil { return err }

  // 删除
  if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }

  return nil
}

备注

  • 使用 .Relation(". ..") 显式连接/预加载。
  • 使用单独的 bun/migrate 包进行迁移。

sqlc

sqlc 不是 ORM。 您编写 SQL;它生成类型安全的 Go 方法。

sqlc.yaml

version: "2"
sql:
  - engine: postgresql
    queries: db/queries
    schema: db/migrations
    gen:
      go:
        package: db
        out: internal/db
        sql_package: "database/sql" # 或 "github.com/jackc/pgx/v5"

查询(db/queries/users.sql

-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email;

-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;

-- name: UpdateUserEmail :one
UPDATE users SET email = $2 WHERE id = $1
RETURNING id, name, email;

-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;

生成

sqlc generate

使用

import (
  "database/sql"
  _ "github.com/jackc/pgx/v5/stdlib"
  "your/module/internal/db"
)

func sqlcCRUD(ctx context.Context) error {
  dsn := os.Getenv("DATABASE_URL")
  sqldb, err := sql.Open("pgx", dsn)
  if err != nil { return err }
  q := db.New(sqldb)

  // 创建
  u, err := q.CreateUser(ctx, db.CreateUserParams{
    Name: "Alice", Email: "alice@example.com",
  })
  if err != nil { return err }

  // 读取
  got, err := q.GetUser(ctx, u.ID)
  if err != nil { return err }

  // 更新
  up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
    ID: got.ID, Email: "alice+1@example.com",
  })
  if err != nil { return err }

  // 删除
  if err := q.DeleteUser(ctx, up.ID); err != nil { return err }

  return nil
}

备注

  • 自行处理迁移(例如,golang-migrate)。
  • 对于动态查询:编写多个 SQL 变体或与小型构建器结合使用。

性能注意事项

  • GORM:方便但增加了反射/抽象开销。适用于典型 CRUD;注意 N+1 查询(优先使用 Joins 或选择性 Preload)。
  • Ent:生成的代码避免了反射;适合复杂模式。通常比运行时“魔法”ORM 更快。
  • Bundatabase/sql 之上轻量;速度快,显式,适合批量操作和大型结果集。
  • sqlc:本质上是原始 SQL 性能,具有编译时安全性。

一般建议

  • 使用 pgx 作为驱动(v5)并在所有地方使用 context
  • 优先使用 批量操作COPY,多行 INSERT)以提高吞吐量。
  • 分析 SQL:EXPLAIN ANALYZE,索引,覆盖索引,避免不必要的往返。
  • 重用连接;根据工作负载调整池大小。

开发者体验和生态系统

  • GORM:最大的社区,很多示例/插件;高级模式的学习曲线较陡。
  • Ent:文档出色;代码生成步骤是主要的思维模型转变;非常适合重构。
  • Bun:可读、可预测的查询;社区较小但活跃;PostgreSQL 功能出色。
  • sqlc:运行时依赖最小;与迁移工具和 CI 集成良好;非常适合熟悉 SQL 的团队。

功能亮点

  • 关系与预加载:所有都支持关系;GORM(标签 + Preload/Joins),Ent(边 + .With...()),Bun(Relation(...)),sqlc(您编写连接)。
  • 迁移:GORM(自动迁移;生产环境要小心),Ent(生成/差异 SQL),Bun(bun/migrate),sqlc(外部工具)。
  • 钩子/可扩展性:GORM(回调/插件),Ent(钩子/中间件 + 模板/代码生成),Bun(类似中间件的查询钩子,易于原始 SQL),sqlc(在应用层组合)。
  • JSON/数组(PostgreSQL):Bun 和 GORM 有不错的辅助工具;Ent/sqlc 通过自定义类型或 SQL 处理。

何时选择什么

  • 选择 GORM 如果您想要最大便利性、丰富功能,并希望快速构建传统 CRUD 服务。
  • 选择 Ent 如果您重视编译时安全性、显式模式,并希望在大型团队中实现长期可维护性。
  • 选择 Bun 如果您想要性能和显式的 SQL 风格查询,并在需要时享受 ORM 的便利。
  • 选择 sqlc 如果您和您的团队更喜欢使用纯 SQL 并希望有类型安全的 Go 绑定,且没有运行时开销。

本地 PostgreSQL 的最小 docker-compose.yml

version: "3.8"
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    ports: ["5432:5432"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 5s
      timeout: 3s
      retries: 5

Go语言中的ORM包和库

其他有用链接