Go map 默认是并发不安全的,同时对 map 进行并发读写的时,程序会 panic,原因如下:Go 官方经过长时间的讨论,认为 map 适配的场景应该是简单的(不需要从多个 gorountine 中进行安全访问的),而不是为了小部分情况(并发访问),导致大部分程序付出锁的代价,因此决定了不支持。

并发读写可能引发的问题

func runWithPanic() {
    s := make(map[int]int)
    n := 100
    for i := 0; i < n; i++ {
        go func(i int) {
            s[i] = i
        }(i)
    }
    for i := 0; i <= n; i++ {
        go func(i int) {
            fmt.Printf("第 %d 个元素是 %v", i, s[i])
        }(i)
    }
    time.Sleep(time.Second)
    // fatal error: concurrent map writes
}
sync.RWMutex
func runWithSyncRWMutex() {
    var lock sync.RWMutex
    s := make(map[int]int)
    n := 100
    for i := 0; i < n; i++ {
        go func(i int) {
            lock.Lock()
            s[i] = i
            lock.Unlock()
        }(i)
    }
    for i := 0; i <= n; i++ {
        go func(i int) {
            lock.RLock()
            fmt.Printf("第 %d 个元素是%v;", i, s[i])
            lock.RUnlock()
        }(i)
    }
    time.Sleep(time.Second)
}

使用 sync.Map 解决并发读写的问题

func RunWithSyncMap() {
    s := sync.Map{}
    n := 100
    for i := 0; i < n; i++ {
        go func(i int) {
            s.Store(i, i)
        }(i)
    }
    for i := 0; i <= n; i++ {
        go func(i int) {
            v, ok := s.Load(i)
            if ok {
                fmt.Printf("第 %d 个元素是%v;", i, v)
            }
        }(i)
    }
    time.Sleep(time.Second)
}
sync.Mapsync.Map
type Map struct {
    mu Mutex

    // read contains the portion of the map's contents that are safe for
    // concurrent access (with or without mu held).
    //
    // The read field itself is always safe to load, but must only be stored with
    // mu held.
    //
    // Entries stored in read may be updated concurrently without mu, but updating
    // a previously-expunged entry requires that the entry be copied to the dirty
    // map and unexpunged with mu held.
    read atomic.Value // readOnly

    // dirty contains the portion of the map's contents that require mu to be
    // held. To ensure that the dirty map can be promoted to the read map quickly,
    // it also includes all of the non-expunged entries in the read map.
    //
    // Expunged entries are not stored in the dirty map. An expunged entry in the
    // clean map must be unexpunged and added to the dirty map before a new value
    // can be stored to it.
    //
    // If the dirty map is nil, the next write to the map will initialize it by
    // making a shallow copy of the clean map, omitting stale entries.
    dirty map[any]*entry

    // misses counts the number of loads since the read map was last updated that
    // needed to lock mu to determine whether the key was present.
    //
    // Once enough misses have occurred to cover the cost of copying the dirty
    // map, the dirty map will be promoted to the read map (in the unamended
    // state) and the next store to the map will make a new dirty copy.
    misses int
}

最后

就像官方考虑的那样,我们在使用中,应尽量避免对 Map 进行并发读写,尝试通过其他方式解决问题,如数据解耦、分布执行、动态规划等,真的需要并发读写时,为避免产生并发读写的问题,请使用锁的机制进行控制