Golang中context包的空指针引用问题

Golang中context包的空指针引用问题 这一行代码

return c.Context.Value(key)

是并发安全的吗?

我收到的错误堆栈跟踪已附上。

Screenshot 2021-05-04 at 3.57.19 PM

当对上下文值进行大量读取操作时,就会引发这个 panic。

4 回复

是的,Context.Value 是线程/goroutine安全的。从这段代码片段中我无法看出问题所在。你为什么要把堆栈跟踪中的一行信息涂掉呢?

更多关于Golang中context包的空指针引用问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


栈中包含我们组织要求不得在任何地方公开的仓库信息。这就是我必须将其编辑掉的原因。

不过,我可以提供一些信息。 对于每个请求,我们都会将其放入请求上下文中。

*r = *r.WithContext(ctx.WithValue(r.Context(), appData, &sync.Map{}))

并获取它

if data, ok := ctx.Value(appData).(*sync.Map); ok {        //这里发生了panic
		data.Range(func(key, value interface{}) bool {
			if k, ok := key.(string); ok {
				appData[k] = value
			}

			return true
		})
	}

所有这些都通过中间件发生。

Pratim_Roy:

*r = *r.WithContext(ctx.WithValue(r.Context(), appData, &sync.Map{}))

这并非在所有机器架构上都是线程安全/goroutine安全的。

编辑:这是错误的;我原以为这是在存储一个指针,但我怀疑它实际上是存储到 Request 结构体,而这绝对不是线程安全的。

遍历 data *sync.Map 不会访问无效内存,但它是不确定的,因此如果其他 goroutine 正在添加/删除元素,你将无法确定会得到什么。

Pratim_Roy:

appData[k] = value

看起来 appData 是一个(可能是全局的?)map,而你正在向其中写入数据。这不是线程安全/goroutine安全的。

根据你提供的错误堆栈信息,这是典型的并发读写map导致的panic。context.ContextValue()方法本身是并发安全的,但问题出在你存储到context中的值。

问题分析

当你在context中存储值时,如果这个值本身不是并发安全的(比如map、slice等引用类型),并且在多个goroutine中同时读写,就会引发panic。

示例代码

package main

import (
    "context"
    "fmt"
    "sync"
)

func main() {
    // 错误示例:存储非并发安全的map
    ctx := context.WithValue(context.Background(), "data", make(map[string]interface{}))
    
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            // 这里并发读写map会导致panic
            if m, ok := ctx.Value("data").(map[string]interface{}); ok {
                m[fmt.Sprintf("key%d", idx)] = idx
            }
        }(i)
    }
    wg.Wait()
}

解决方案

package main

import (
    "context"
    "fmt"
    "sync"
    "sync/atomic"
)

// 使用并发安全的map
type SafeMap struct {
    mu sync.RWMutex
    data map[string]interface{}
}

func NewSafeMap() *SafeMap {
    return &SafeMap{
        data: make(map[string]interface{}),
    }
}

func (sm *SafeMap) Set(key string, value interface{}) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.data[key] = value
}

func (sm *SafeMap) Get(key string) (interface{}, bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    val, ok := sm.data[key]
    return val, ok
}

func main() {
    // 正确示例:存储并发安全的类型
    ctx := context.WithValue(context.Background(), "data", NewSafeMap())
    
    var wg sync.WaitGroup
    var successCount int32
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            if sm, ok := ctx.Value("data").(*SafeMap); ok {
                sm.Set(fmt.Sprintf("key%d", idx), idx)
                atomic.AddInt32(&successCount, 1)
            }
        }(i)
    }
    wg.Wait()
    
    fmt.Printf("成功执行次数: %d\n", successCount)
}

替代方案:使用sync.Map

package main

import (
    "context"
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    // 使用sync.Map
    var safeMap sync.Map
    ctx := context.WithValue(context.Background(), "data", &safeMap)
    
    var wg sync.WaitGroup
    var successCount int32
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            if m, ok := ctx.Value("data").(*sync.Map); ok {
                m.Store(fmt.Sprintf("key%d", idx), idx)
                atomic.AddInt32(&successCount, 1)
            }
        }(i)
    }
    wg.Wait()
    
    fmt.Printf("成功执行次数: %d\n", successCount)
}

关键点:context.Value()方法调用本身是线程安全的,但你需要确保存储在context中的值类型是并发安全的,或者在访问时进行适当的同步控制。

回到顶部