When refactoring the data layer of a small sidecar project (rex-hugo-sidercat), I had to choose between GORM and Bun. This post records the thinking process, benchmark numbers, and the real trade-offs we found.

1. Why switch away from hand-written SQL?

The project started with raw database/sql and inline schema strings. As tables grew, a few problems surfaced:

  • Field types and structs drifted apart; changing one place often missed another.
  • Raw QueryRow / Exec boilerplate piled up.
  • Schema changes had no version history, so local and production environments could silently diverge.

The goal was clear: bring in a lightweight ORM that is maintainable and gives us controlled schema migrations.

2. Candidates

OptionMaturityLearning curveStyle
GORMHighestGentleChainable object queries, ThinkPHP/Laravel-like
BunMediumGentleCloser to hand-written SQL
EntMediumSteepCode-generation + type safety

The project only has four tables: users, orders, access, and membership. Ent’s code-generation benefit was small; GORM is the most mature but has more “magic”; Bun has a reputation for SQL transparency and performance. So the real comparison became GORM vs Bun.

ORM trade-offs: performance, SQL transparency, ecosystem maturity

3. Syntax: Bun feels more like SQL

GORM uses an object-oriented, chainable style:

var u User
db.Where("phone = ?", phone).First(&u)

db.Model(&User{}).Where("phone = ?", phone).Update("token", newToken)

Bun writes closer to the actual SQL:

var u User
err := db.NewSelect().Model(&u).Where("phone = ?", phone).Scan(ctx)

_, err = db.NewUpdate().
    Model((*User)(nil)).
    Set("token = ?", newToken).
    Where("phone = ?", phone).
    Exec(ctx)

Bun has no First or Save abstractions. The mapping between your code and the final SQL is more direct. If your team prefers the “describe what I want” style of ThinkPHP/Laravel, GORM feels more natural. If you want to keep the SQL in view, Bun is more comfortable.

4. Migrations: Bun has no AutoMigrate

GORM ships with AutoMigrate, which is convenient during development:

db.AutoMigrate(&User{}, &Order{}, &Access{}, &Membership{})

But it is risky in production — easy to drop columns, change types, or create drift between environments.

Bun has no built-in migration tool, so you pair it with a separate one:

ToolStrengthRecommendation
golang-migrateMost popular, SQL files, multi-database⭐⭐⭐
pressly/gooseSupports SQL + Go function migrations⭐⭐⭐
bun/migrateBun-styled, but less community content⭐⭐

The conclusion: regardless of ORM choice, use golang-migrate or goose in production, not AutoMigrate.

5. Performance benchmark

I wrote a small benchmark using the same machine, the same SQLite driver (modernc.org/sqlite), and identical data.

Tests:

  • Single insert
  • Batch insert 1000 rows
  • Single select by unique field
  • Select many (100 rows)
  • Single update

Results (per-operation latency / allocations):

TestGORMBunBun advantage
InsertOne28175 ns/op, 81 allocs14768 ns/op, 25 allocs~1.9x faster, 69% fewer allocs
InsertBatch(1000)7953726 ns/op, 12020 allocs4016759 ns/op, 7187 allocs~2x faster
SelectOne11763 ns/op, 67 allocs10429 ns/op, 37 allocsSlightly faster, fewer allocs
SelectMany(100)161400 ns/op, 1257 allocs148787 ns/op, 1136 allocsSlightly faster
UpdateOne16234 ns/op, 65 allocs6877 ns/op, 15 allocs~2.4x faster, 77% fewer allocs

Bun is noticeably faster on inserts and updates, and slightly faster on selects. The lower allocation count also means less GC pressure.

6. Downsides of Bun

A fair comparison must include the negatives:

  1. Smaller ecosystem than GORM: fewer Chinese-language examples and Stack Overflow answers.
  2. No AutoMigrate: one extra migration step during development.
  3. Lower-level API: if the team is used to GORM’s magic, switching takes adjustment.
  4. Higher hiring/handoff cost: far fewer developers in China know Bun compared to GORM.
  5. No automatic association loading: Bun has no Preload equivalent.

7. Decision

For a small sidecar like rex-hugo-sidercat — four tables, SQLite, WeChat Pay + paid-content permissions:

  • If you prioritize maturity and hiring ease, choose GORM.
  • If you prioritize SQL transparency, performance, and lower reflection overhead, choose Bun and pair it with golang-migrate.

Based on these benchmarks and long-term maintenance, I lean toward Bun + golang-migrate. The schema is small, queries are simple, and the maintainability gain from transparent SQL outweighs GORM’s faster initial development speed.