顺着你那条『channel 让竞态从源头少一大半』的说法,我倒觉得得拆细一点。channel 改变的是 bug 出现的形态,不等于把竞态按掉大半那么多,『少一大半』这个数本身也没见谁量化过,是直觉还是跑过 benchmark?
具体例子:你把一片 slice 通过 channel 传给另一个 goroutine,传过去的是 slice header,底层数组还是同一份。接收方在遍历、发送方(或别处)在 append 改写,底层数组一冲突,照样是 data race,go run -race 一跑就报红。其实再比如 close 一个 channel 两次会直接 panic,多个 goroutine 抢着关同一个 channel 时,你还是得靠 sync.Once 或者约定唯一的关闭者,绕了一圈又回到同步原语上。
其实
社区里那句『channels orchestrate, mutexes serialize』更实在些。带缓冲的计数器、几个 goroutine 共享一个 struct 字段这类场景,直接上 sync.Mutex 往往比用 channel 来回传指针清晰,也少一层 goroutine 泄漏的坑。Go Proverbs 里那句『don’t communicate by sharing memory, share memory by communicating』本意是劝你别一上来就靠锁堆共享状态,不是说锁从此退休。
真要兜住竞态,靠的其实还是 -race 这种动态检测,而不是『我用了 channel』这个姿势。从某种角度看,channel 更大的价值是让并发结构更好读:谁发谁收一眼能看清,而不是安全性的银弹。
你那句『开几千上万个不喘』我顺手压过,二十来万常驻 goroutine 在普通机器上也没见它喘,但前提是它们大多在等 channel 或 timer,不是真在烧 CPU;真都要卡系统调用上,得看 GMP 里 P 的数量和 sysmon 的抢占。stack 初始 2KB 这点你说得准,现代 Go 走连续栈动态扩,早期分段栈那段历史就不展开了。
你现在是纯 channel 派,还是看场景混用?