diff --git a/backend/pkg/batchwriter/writer.go b/backend/pkg/batchwriter/writer.go index 431bbd69..46aed750 100644 --- a/backend/pkg/batchwriter/writer.go +++ b/backend/pkg/batchwriter/writer.go @@ -7,6 +7,7 @@ package batchwriter import ( + "Wavelet/pkg/util" "context" "sync" "sync/atomic" @@ -96,7 +97,7 @@ func (w *Writer[T]) Start(parent context.Context) { w.ch = make(chan T, w.cfg.QueueSize) w.workerCtx = context.WithoutCancel(parent) - go w.run() + util.Go(w.run) }) } diff --git a/backend/pkg/cache/ram/manager.go b/backend/pkg/cache/ram/manager.go index f316d88d..d961187e 100644 --- a/backend/pkg/cache/ram/manager.go +++ b/backend/pkg/cache/ram/manager.go @@ -4,6 +4,7 @@ package ram import ( + "Wavelet/pkg/util" "context" "errors" "sync" @@ -73,7 +74,7 @@ func Get(configType, key string) (CacheItem, bool) { // Check expiration if entry.item.TTL != -1 && !entry.expireAt.IsZero() && time.Now().After(entry.expireAt) { // Asynchronously remove the expired item from the map and write back - go deleteKeyIfExpired(configType, key, entry.expireAt) + util.Go(func() { deleteKeyIfExpired(configType, key, entry.expireAt) }) return CacheItem{}, false } diff --git a/backend/plugins/domain/cap/pow/store.go b/backend/plugins/domain/cap/pow/store.go index d24fdf10..4606b546 100644 --- a/backend/plugins/domain/cap/pow/store.go +++ b/backend/plugins/domain/cap/pow/store.go @@ -4,6 +4,7 @@ package pow import ( + "Wavelet/pkg/util" "context" "errors" "sync" @@ -43,7 +44,7 @@ func NewMemoryStore(cleanupInterval time.Duration) *MemoryStore { items: make(map[string]memoryItem), } if cleanupInterval > 0 { - go store.startCleanupLoop(cleanupInterval) + util.Go(func() { store.startCleanupLoop(cleanupInterval) }) } return store } diff --git a/backend/plugins/infra/storage/diskcache/cache.go b/backend/plugins/infra/storage/diskcache/cache.go index 177a87a4..744d8ba5 100644 --- a/backend/plugins/infra/storage/diskcache/cache.go +++ b/backend/plugins/infra/storage/diskcache/cache.go @@ -11,6 +11,7 @@ import ( "time" pkgcache "Wavelet/pkg/cache/disk" + "Wavelet/pkg/util" ) // Status represents the runtime cache statistics. @@ -49,7 +50,7 @@ func GetGlobalCache() *DiskCache { // Load initial configs from database globalCache.ReloadConfig(context.Background()) // Start background routine to clean expired items every 10 minutes - go globalCache.StartCleanupWorker(defaultCleanupInterval * time.Minute) + util.Go(func() { globalCache.StartCleanupWorker(defaultCleanupInterval * time.Minute) }) }) return globalCache } diff --git a/scripts/check_cordis_architecture.sh b/scripts/check_cordis_architecture.sh index 77df1624..3c040d25 100755 --- a/scripts/check_cordis_architecture.sh +++ b/scripts/check_cordis_architecture.sh @@ -182,13 +182,16 @@ fi # ============================================================================== # 6. 并发安全规范 (Goroutine Concurrency Safety) # ============================================================================== -log_check "6. 检查并发安全规范 (禁止生产代码中使用裸 go func())..." +log_check "6. 检查并发安全规范 (禁止生产代码中使用裸 go 启动 goroutine)..." -BARE_GO_ROUTINES=$(rg -n '\bgo func\(' "${BACKEND_DIR}" \ +# 同时覆盖 `go func() {...}()` 匿名形式与 `go worker.run()` / `go loop()` 命名调用形式: +# 两者都不具备 panic 恢复能力,被调方一旦 panic 会直接击穿整个进程。 +# 例外:util.Go 自身的实现与事件总线 (它们内部已 recover)。 +BARE_GO_ROUTINES=$(rg -n --pcre2 '^[[:space:]]*go\s+(func\s*[\w{]|\w+(\.\w+)*\s*[\({])' "${BACKEND_DIR}" \ --glob '*.go' -g '!*_test.go' -g '!goroutine.go' -g '!events.go' || true) if [ -n "${BARE_GO_ROUTINES}" ]; then - log_fail "生产代码严禁使用裸 'go func()',必须使用 'util.Go' 确保 panic 恢复与调用栈追踪:" + log_fail "生产代码严禁裸 'go' 启动 goroutine(含 'go func()' 与 'go xxx()' 命名调用),必须使用 'util.Go' 确保 panic 恢复与调用栈追踪:" echo "${BARE_GO_ROUTINES}" >&2 else log_pass "并发调用统一使用 util.Go 具备 panic 恢复能力"