Web 服务器天然是并发的——多个请求同时处理,每个请求可能在不同的 goroutine 中。理解 Gin 的并发安全特性,对编写正确的并发代码至关重要。
Gin 的 Context 设计为每个请求一个实例,不是线程安全的:
// 危险:在 goroutine 中直接使用 Context
func handler(c *gin.Context) {
go func() {
// 数据竞争!
c.Set("key", "value")
c.JSON(200, gin.H{})
}()
}
func handler(c *gin.Context) {
cCp := c.Copy()
go func() {
// 安全:使用副本读取数据
userID := cCp.GetInt("userID")
processAsync(userID)
}()
c.JSON(200, gin.H{"status": "accepted"})
}
func handler(c *gin.Context) {
// 先提取需要的数据
userID := c.GetInt("userID")
path := c.Request.URL.Path
go func() {
// 安全:使用局部变量
processAsync(userID, path)
}()
c.JSON(200, gin.H{"status": "accepted"})
}
func handler(c *gin.Context) {
resultCh := make(chan Result)
go func() {
result := doWork()
resultCh <- result
}()
select {
case result := <-resultCh:
c.JSON(200, result)
case <-time.After(5 * time.Second):
c.JSON(408, gin.H{"error": "timeout"})
}
}
var (
counter int
mu sync.Mutex
)
func handler(c *gin.Context) {
mu.Lock()
counter++
current := counter
mu.Unlock()
c.JSON(200, gin.H{"count": current})
}
var requestCounts sync.Map
func handler(c *gin.Context) {
ip := c.ClientIP()
// 原子操作
count, _ := requestCounts.LoadOrStore(ip, 0)
requestCounts.Store(ip, count.(int)+1)
c.JSON(200, gin.H{"count": count})
}
var requestCount int64
func handler(c *gin.Context) {
// 原子增加
count := atomic.AddInt64(&requestCount, 1)
c.JSON(200, gin.H{"count": count})
}
数据库连接池本身是线程安全的:
var db *sql.DB
func main() {
var err error
db, err = sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
// 设置连接池参数
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
// db 可以在多个 goroutine 中安全使用
}
func handler(c *gin.Context) {
// 安全:db 是线程安全的
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
// ...
}
type Cache struct {
data map[string]interface{}
mu sync.RWMutex
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
var cache = &Cache{data: make(map[string]interface{})}
func handler(c *gin.Context) {
// 读锁
if val, ok := cache.Get("key"); ok {
c.JSON(200, val)
return
}
// 获取数据并缓存
data := fetchData()
cache.Set("key", data)
c.JSON(200, data)
}
// 错误:数据竞争
var counter int
func handler(c *gin.Context) {
go func() {
counter++ // 数据竞争
}()
c.JSON(200, gin.H{"count": counter})
}
// 正确:使用 atomic
func handler(c *gin.Context) {
go func() {
atomic.AddInt64(&counter, 1)
}()
c.JSON(200, gin.H{"count": atomic.LoadInt64(&counter)})
}
// 错误:死锁
var mu sync.Mutex
func handler(c *gin.Context) {
mu.Lock()
defer mu.Unlock()
// 某些条件下再次获取锁
someFunction() // 如果 someFunction 也获取 mu,会死锁
}
// 正确:避免嵌套锁或使用 RWMutex
var mu sync.RWMutex
func handler(c *gin.Context) {
mu.RLock()
defer mu.RUnlock()
// 读操作
}
// 错误:goroutine 泄漏
func handler(c *gin.Context) {
ch := make(chan int)
go func() {
ch <- doWork() // 如果没有接收者,goroutine 会一直阻塞
}()
// 如果请求提前结束,goroutine 泄漏
}
// 正确:使用带缓冲的通道或 context
func handler(c *gin.Context) {
ch := make(chan int, 1)
go func() {
ch <- doWork()
}()
select {
case result := <-ch:
c.JSON(200, result)
case <-c.Request.Context().Done():
return
}
}
Go 提供了数据竞争检测工具:
go run -race main.go
go test -race ./...
示例输出:
==================
WARNING: DATA RACE
Write at 0x000001234567 by goroutine 8:
main.handler.func1()
/app/main.go:20 +0x123
Previous read at 0x000001234567 by goroutine 7:
main.handler()
/app/main.go:22 +0x234
==================
Gin 并发安全的关键点:
Copy() 创建只读副本-race 标志检测数据竞争理解并发安全是编写可靠 Web 服务的基础。在 Gin 中,遵循"每个请求一个 Context"的原则,正确处理跨 goroutine 的数据共享,就能避免大部分并发问题。