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/Execboilerplate 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
| Option | Maturity | Learning curve | Style |
|---|---|---|---|
| GORM | Highest | Gentle | Chainable object queries, ThinkPHP/Laravel-like |
| Bun | Medium | Gentle | Closer to hand-written SQL |
| Ent | Medium | Steep | Code-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.

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:
| Tool | Strength | Recommendation |
|---|---|---|
| golang-migrate | Most popular, SQL files, multi-database | ⭐⭐⭐ |
| pressly/goose | Supports SQL + Go function migrations | ⭐⭐⭐ |
| bun/migrate | Bun-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):
| Test | GORM | Bun | Bun advantage |
|---|---|---|---|
| InsertOne | 28175 ns/op, 81 allocs | 14768 ns/op, 25 allocs | ~1.9x faster, 69% fewer allocs |
| InsertBatch(1000) | 7953726 ns/op, 12020 allocs | 4016759 ns/op, 7187 allocs | ~2x faster |
| SelectOne | 11763 ns/op, 67 allocs | 10429 ns/op, 37 allocs | Slightly faster, fewer allocs |
| SelectMany(100) | 161400 ns/op, 1257 allocs | 148787 ns/op, 1136 allocs | Slightly faster |
| UpdateOne | 16234 ns/op, 65 allocs | 6877 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:
- Smaller ecosystem than GORM: fewer Chinese-language examples and Stack Overflow answers.
- No AutoMigrate: one extra migration step during development.
- Lower-level API: if the team is used to GORM’s magic, switching takes adjustment.
- Higher hiring/handoff cost: far fewer developers in China know Bun compared to GORM.
- No automatic association loading: Bun has no
Preloadequivalent.
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.
