Go并发场景下与其他库共用ulid的安全写法及示例校验
结论先行
你当前写的ULID生成代码不是并发安全的,高并发场景下会出现随机数序列错乱、ID重复甚至程序panic的问题。
官方提示含义说明
math/rand包提供的rand.Rand结构体本身没有做并发访问保护:
- 多个goroutine同时调用同一个
rand.Rand实例的生成方法时,会触发内部状态竞态 math/rand包暴露的包级随机函数虽然做了并发安全处理,但靠全局锁实现,高并发下锁争用开销很高- 官方给出的两个优化方向本质都是避免多个goroutine共用同一个非线程安全的随机源实例,从根源上消除竞态和不必要的锁开销
兼容ent、gqlgen的并发安全实现
两种方案都只需要暴露统一的NewID方法,第三方库直接调用该方法即可,不需要额外适配。
方案1:sync.Pool管理熵源(推荐,高并发性能最优)
通过对象池复用熵源实例,既避免跨goroutine共用随机源,又减少重复初始化的开销,是生产环境最常用的实现:
import ( "math/rand" "sync" "time" "github.com/oklog/ulid/v2" ) var entropyPool = sync.Pool{ New: func() any { // 每个池内实例绑定独立的随机源,互不干扰 return ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0) }, } // NewID 生成并发安全的ULID字符串 func NewID() string { entropy := entropyPool.Get().(*ulid.MonotonicEntropy) defer entropyPool.Put(entropy) return ulid.MustNew(ulid.Timestamp(time.Now()), entropy).String() }
方案2:互斥锁保护全局熵源(实现简单,适合中低并发场景)
如果业务并发量不高,直接加锁保护全局熵源即可,代码维护成本更低:
import ( "math/rand" "sync" "time" "github.com/oklog/ulid/v2" ) var ( defaultEntropy = ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0) entropyLock sync.Mutex ) // NewID 生成并发安全的ULID字符串 func NewID() string { entropyLock.Lock() defer entropyLock.Unlock() return ulid.MustNew(ulid.Timestamp(time.Now()), defaultEntropy).String() }
注意事项
- 不要直接用
math/rand包的全局包级函数作为ULID熵源,高并发下全局锁的性能损耗非常明显 - 不需要为每个goroutine永久初始化独立熵源,用sync.Pool复用的方式已经能满足性能和安全要求,内存开销更低
- 如果你需要在ent的schema钩子、gqlgen的resolver里生成ULID,直接调用上述
NewID方法即可,不需要额外修改配置
内容的提问来源于stack exchange,提问作者Fred Hors
相关产品推荐
相关产品推荐

