三个维度没毛病,但我最后落地的标准其实是按读模型/写模型拆开选,而不是整个项目二选一——这三者本来就该混着用。

写模型(增删改、领域规则、事务边界)交给 GORM 或 Ent,读模型(列表分页、统计、报表、多表 JOIN)交回 SQLx 或裸 SQL。GORM 有 db.Raw().Scan(),Ent 有 sql/execquery,同一个项目里共存毫无冲突。剩下的硬标准:表关系三层以内的 CRUD 后台,GORM 的 Where + Preload 就够了,别折腾;关系是网状、团队超过 3 人、生命周期两年以上,Ent 的 schema as code + 类型安全省下的沟通成本远超学习曲线;团队 SQL 硬、查询极复杂或已有存过,SQLx 最省心。

补几个帖子里没提但一定会撞上的点。迁移千万别靠 AutoMigrate(这点你说得对),用 goose / golang-migrate / Atlas 单独管版本。连接池必须显式设 SetMaxOpenConns / SetMaxIdleConns / SetConnMaxLifetime,GORM 默认走 database/sql 的无上限配置,压测时容易把 MySQL 打满。Context 一路传下去:GORM 用 WithContext,SQLx 用 SelectContext,不传的话超时和链路追踪全是断的。

两个隐蔽坑:GORM 只要结构体带 gorm.DeletedAt,所有查询自动追加 deleted_at IS NULL,和唯一索引叠在一起经常出现"明明没数据却插不进去";Save 是全字段更新,业务零值会被原样写回,部分更新老实用 Updates(map) 或 Select。

帖子里迁移那部分好像断在"正式"就没了,方便的话补一下,正好把这几个坑串起来讲。另外 GORM 的 Preload 在 Ent 里对应 WithXxx 的 eager load,都是预加载一次查完,两边的写法可以对着看,迁移成本比想象中低。