Go语言实战案例:使用sync.Map构建线程安全map

在并发编程中,共享资源的访问是一个绕不开的问题。Go 中的 map 在并发读写时是不安全的,直接使用可能导致程序 panic。因此,在多协程同时访问 Map 的场景下,必须采取有效的同步措施。

本篇将通过一个实战案例,介绍 Go 的并发神器 ------ sync.Map,并展示其使用方式和应用场景。


一、为什么普通 map 不安全?

go 复制代码
m := make(map[string]int)

go func() {
    m["a"] = 1
}()

go func() {
    fmt.Println(m["a"])
}()

上述代码在运行时,极有可能 panic

arduino 复制代码
fatal error: concurrent map read and map write

这是因为 Go 的原生 map 类型并不支持并发读写。


二、解决方式有哪些?

  1. 使用 sync.Mutex + map 手动加锁;
  2. 使用 sync.RWMutex 读写分离锁;
  3. 使用 Go 官方提供的 sync.Map ------ 最简便的线程安全 map 实现!

三、什么是 sync.Map

sync.Map 是 Go 标准库提供的并发安全 map,特点包括:

  • 内置锁机制,适合高并发读写;
  • 无需手动加锁;
  • 内部采用读优化结构(读多写少场景性能更佳);
  • 提供一组特定 API(不能用下标访问,如 m["a"])。

四、实战案例:使用 sync.Map 构建并发计数器

我们实现一个并发访问的计数器,多个 goroutine 同时对多个 key 进行访问和计数。

示例代码:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var counter sync.Map
    var wg sync.WaitGroup

    keys := []string{"A", "B", "C"}

    // 每个 key 启动 10 个协程并发计数
    for _, key := range keys {
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go func(k string) {
                defer wg.Done()
                for j := 0; j < 100; j++ {
                    // 原子更新 key 的计数
                    val, _ := counter.LoadOrStore(k, 0)
                    counter.Store(k, val.(int)+1)
                }
            }(key)
        }
    }

    wg.Wait()

    // 打印结果
    counter.Range(func(k, v any) bool {
        fmt.Printf("Key: %s, Count: %d\n", k, v)
        return true
    })
}

五、输出示例:

yaml 复制代码
Key: A, Count: 1000
Key: B, Count: 1000
Key: C, Count: 1000

六、常用方法介绍:

方法 功能描述
Load(key) 获取键对应的值和是否存在
Store(key, value) 设置键值
LoadOrStore(key, value) 获取已存在值,否则存入新值
Delete(key) 删除指定键值对
Range(func(k, v any) bool) 遍历所有键值对,返回 false 可中断遍历

七、注意事项

  1. 无法使用 m["key"] 形式 ,只能通过 Load / Store

  2. LoadOrStore 非原子加法操作 :如本例中 val + 1 不是并发安全操作;

    • 更好的方式是配合原子操作 sync/atomic 或引入锁;
  3. sync.Map 更适合"读多写少"的场景;

  4. 若是写多场景,建议仍用 RWMutex + map 自行控制。


八、应用场景举例

  • 统计类服务:如 PV/UV 实时统计;
  • 缓存系统:缓存热点数据;
  • 注册中心:维护在线服务列表;
  • 连接池管理:存储连接状态;

九、总结

sync.Map 提供了一种简洁、高效的方式来处理并发情况下的共享数据。它避免了手动加锁的繁琐,让我们更专注于业务逻辑。

✅ 并发安全 ✅ 使用简单 ✅ 性能优越(适合读多写少)


下一个实战案例:我们将结合 context + sync.Map 实现带过期控制的并发缓存服务,敬请关注!


🔧 附加福利:你可以这样封装自己的线程安全结构

go 复制代码
type SafeMap struct {
    data sync.Map
}

func (sm *SafeMap) Incr(key string) {
    for {
        val, _ := sm.data.LoadOrStore(key, 0)
        newVal := val.(int) + 1
        sm.data.Store(key, newVal)
        break
    }
}

相关推荐
绝知此事3 分钟前
ELK 从入门到精通:Spring Boot 实战三部曲(一)—— 基础核心与快速上手
spring boot·后端·elk
土狗TuGou9 分钟前
SQL内功笔记 · 第5篇:SQL逻辑执行顺序
数据库·笔记·后端·sql·mysql
大家的林语冰19 分钟前
AI 遥控代码截图,录制终端动画,定制自动化批量制图流程,解放你的双手~
前端·ai编程·trae
久违 °9 小时前
【AI-Agent】TagMatrix 数据标注工具开发
人工智能·数据分析·go·agent·数据隐私
为思念酝酿的痛9 小时前
POSIX信号量
linux·运维·服务器·后端
小羊在睡觉9 小时前
力扣84. 柱状图中最大的矩形
后端·算法·leetcode·golang·go
油炸自行车9 小时前
Claude Code 错误:API Error: 400 Failed to deserialize the JSON body into the
开发语言·javascript·json·trae·claude code·api error 400
swipe10 小时前
Neo4j + Graph RAG 医疗知识图谱工程实践:患者教育问答真正需要的是“关系可追溯”
后端·langchain·llm
源码宝11 小时前
MES系统源码:Java8 + SpringBoot2.7 + MySQL8 + Redis,后端源码清爽易扩展
java·后端·源码·springboot·mes系统·源码二开·mes源码
金銀銅鐵11 小时前
[Java] 如何理解 class 文件中方法的 descriptor?
java·后端