千家信息网

Golang中sync.Map的坑

发表于:2026-11-02 作者:千家信息网编辑
千家信息网最后更新 2026年11月02日,这篇文章主要介绍了Golang中sync.Map的坑,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。缘起Go 1.15 发布了,我也第一
千家信息网最后更新 2026年11月02日Golang中sync.Map的坑

这篇文章主要介绍了Golang中sync.Map的坑,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。

缘起

Go 1.15 发布了,我也第一时间更新了这个版本,毕竟对 Go 的稳定性还是有一些信心的,于是直接在公司上了生产。

结果,上线几分钟,就出现了 OOM,于是 pprof 了一下 heap,然后赶紧回滚,发现某块本应该在一次请求结束时被释放的内存,被保留了下来而且一直在增长,如图(图中的 linkBufferNode):

这次上线的变更只有 Go 版本的升级,没有任何其它变动,于是在本地开始测试,发现在本地也能百分百复现。

排查过程

看了 Go 1.15 的 Release Note,发现有俩高度疑似的东西:

  1. 去除了一些 GC Data,使得 binary size 减少了 5%;

  2. 新的内存分配算法。

于是改 runtime,关闭新的内存分配算法,切换回旧的,等等一顿操作猛如虎下来,发现问题还是没解决,现象仍然存在。

于是实在不行,祭出了GODEBUG="allocfreetrace=1大法,肉眼从 100MB+ 的日志文件里面看啊看啊看啊看啊看啊看啊看啊看啊看啊看啊……(此处省略心酸过程)

最终直觉告诉我,这个问题可能和 Go 1.15 中 sync.Map 的改动有关(别问我为啥,真的是直觉,我也说不出来)。

示例代码

为了方便讲解,我写了一个最小可复现的代码,如下:

package main

import (
"sync"
)

var sm sync.Map

func insertKeys() {
keys := make([]interface{}, 0, 10)
// Store some keys
for i := 0; i < 10; i++ {
v := make([]int, 1000)
keys = append(keys, &v)
sm.Store(keys[i], struct{}{})
}
// delete some keys, but not all keys
for i, k := range keys {
if i%2 == 0 {
continue
}
sm.Delete(k)
}
}

func shutdown() {
sm.Range(func(key, value interface{}) bool {
// do something to key
return true
})
}

func main() {
insertKeys()
// do something ...
shutdown()
}

Go 1.15 中 sync.Map 改动

在 Go 1.15 中,sync.Map 增加了一个方法LoadAndDelete,具体的 issue 在这:https://github.com/golang/go/issues/33762CL, 在这:https://go-review.googlesource.com/c/go/+/205899/。

为什么我确认是这个改动导致的呢?很简单:我在本地把这个改动 revert 掉了,问题就没了,好了关机下班……

当然没这么简单,知其然要知其所以然,于是开始看到底改了哪块……(此处省略 100000 字)

最终发现,关键代码是这段:

// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete()
}
return nil, false
}

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}

func (e *entry) delete() (value interface{}, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*interface{})(p), true
}
}
}

在这段代码中,会发现在 Delete 的时候,并没有真正删除掉 key,而是从 key 中取出了 entry,然后把 entry 设为 nil……

所以,在我们场景中,我们把一个连接作为 key 放了进去,于是和这个连接相关的比如 buffer 的内存就永远无法释放了……

那么为什么在 Go 1.14 中没有问题呢?以下是 Go 1.14 的代码:

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
e.delete()
}
}

在 Go 1.14 中,如果 key 在 dirty 中,是会被删除的;而凑巧,我们其实 "误用" 了 sync.Map,在我们的使用过程中没有读操作,导致所有的 key 其实都在 dirty 里面,所以当调用 Delete 的时候是会被真正删除的。

要注意,无论哪个版本的 Go,一旦 key 升级到了 read 中,都是永远不会被删除的。

感谢你能够认真阅读完这篇文章,希望小编分享的"Golang中sync.Map的坑"这篇文章对大家有帮助,同时也希望大家多多支持,关注行业资讯频道,更多相关知识等着你来学习!

代码 内存 篇文章 问题 版本 过程 时候 直觉 算法 还是 分配 升级 不行 凑巧 心酸 最小 知其所以然 知其然 东西 价值 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 互联网最新的科技 互联网巨头反垄断金融科技 港股互联网科技股 互联网科技媒体 深圳齐家互联网科技 互联网科技公司做什么 互联网 科技服务平台 老年人脱离互联网科技的作文 云创客互联网科技公司 科技互联网的估值 昭通云南众福互联网科技有限公司 银川新华互联网科技学校联系电话 阿里互联网科技企业 南博望互联网科技有限公司 产业互联网科技服务公司 互联网营销推广黑科技 互联网风格科技公司名称 东营紫格互联网科技 科技互联网公司两年变化 互联网金融金融科技 中国互联网科技发展 互联网科技创新企业排名 互联网时代科技改变生活 崇明区互联网教育科技诚信经营 苏州五指互联网络科技有限 互联网金融科技名词解释 云南省自由互联网科技有限公司 领航科技大厦互联网公司 上海互联网科技公司ppt 广西泛欧互联网科技有限公司
0