闭包在go中通过捕获变量地址实现资源共享,但易引发并发不安全或循环变量误捕获问题;正确做法是用工厂函数注入依赖、封装状态并确保同步。

闭包是 Go 里实现依赖注入和资源共享最轻量、最可控的方式,但直接用容易踩坑——比如多个 handler 共享同一个 db 却没处理并发安全,或循环中创建闭包导致所有实例都读到终值 i。
闭包共享资源的本质是捕获变量地址
Go 的闭包不是复制值,而是持有对外部变量的指针。这意味着:
- 多个闭包如果引用同一变量(如
db *sql.DB),它们操作的是同一个实例 - 只要闭包还被引用(比如注册到路由、传入 goroutine),该变量就不会被 GC 回收
- 若共享的是大对象(如
[]byte或未释放的连接池),可能引发内存堆积
典型误用:在 for range 中直接把循环变量传进闭包,结果所有闭包都指向最后一个迭代项的地址。
HTTP Handler 中注入数据库连接的正确写法
避免全局 db 变量,用闭包把依赖“带进去”:
func NewUserHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 这里的 db 就是调用 NewUserHandler 时传入的那个实例
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
// ...
}
}
注册时这么写:
r.HandleFunc("/users", NewUserHandler(db))
关键点:
-
NewUserHandler是工厂函数,每次调用返回一个新闭包,但所有返回的闭包共享同一个db地址 - 不能把
db放在闭包内部重新声明(比如写成var db = ...),那就变成局部副本了 - 如果
db需要并发安全,确保它本身已配置好(SetMaxOpenConns等),闭包层不额外加锁
多个闭包之间安全共享状态的两种模式
需要跨 handler 或跨 goroutine 共享计数器、缓存、配置等,又不想暴露全局变量:
- 用指针封装状态:
counter := new(int),然后传给多个闭包,每个闭包通过*counter读写 - 用结构体封装 + 方法闭包:
type Cache struct { mu sync.RWMutex; data map[string]string },再写func (c *Cache) Get(key string) string { ... },最后用func() string { return cache.Get("x") }包一层
注意:第一种方式在并发下必须手动加锁;第二种更推荐,因为状态和操作被绑定在类型内,不易遗漏同步逻辑。
goroutine 中闭包捕获循环变量的经典陷阱
这段代码输出全是 5:
for i := 0; i <p>修复方法只有两个有效解:</p>
- 立即用局部变量隔离:
for i := 0; i - 显式传参:
go func(val int) { fmt.Println(val) }(i)
别用 time.Sleep 拖延主协程来“凑巧”看到 0~4——那是竞态,不是解法。
闭包共享资源本身没有错,错的是忽略它捕获的是地址这个事实。真正难的不是写出来,而是在哪一层做同步、在哪一层做隔离、以及什么时候该换用 context 或 channel 来替代共享。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











