直接用map[string]bool做IP黑名单会因并发写入触发“fatal error: concurrent map writes”崩溃,必须用sync.RWMutex包裹或改用sync.Map;推荐结合Redis SETEX实现TTL自动过期,避免本地内存管理复杂性。

为什么直接用 map[string]bool 做 IP 黑名单会出问题
因为并发写入时 map 不是线程安全的,只要有两个 goroutine 同时调用 delete 或 map[key] = true,就可能触发 panic:"fatal error: concurrent map writes"。这不是偶发,而是确定性崩溃。
常见错误写法是把黑名单 map 放全局变量里,然后在 HTTP handler 里直接读写——这在压测或真实流量下必崩。
- 必须用
sync.RWMutex包裹读写,或改用sync.Map(但注意它不支持遍历和批量删除) - 如果需要定时清理过期 IP,不能靠循环遍历原生 map,得额外维护一个带过期时间的结构,比如用
map[string]time.Time+ 定期 goroutine 扫描 - 更稳妥的做法是用带 TTL 的内存缓存,比如
github.com/go-redis/redis/v9配合SETEX,天然支持过期,且避免本地内存膨胀
如何用 sync.RWMutex 实现可读写的动态黑名单
核心是把读操作(检查 IP 是否在黑名单)和写操作(添加/删除 IP)用不同粒度锁隔离:读多写少场景下,RWMutex 能显著提升吞吐。
示例结构体:
立即学习“go语言免费学习笔记(深入)”;
type IPBlacklist struct {
mu sync.RWMutex
list map[string]struct{}
}
func (b *IPBlacklist) Contains(ip string) bool {
b.mu.RLock()
defer b.mu.RUnlock()
_, ok := b.list[ip]
return ok
}
func (b *IPBlacklist) Add(ip string) {
b.mu.Lock()
defer b.mu.Unlock()
b.list[ip] = struct{}{}
}
func (b *IPBlacklist) Remove(ip string) {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.list, ip)
}
-
Contains用RUnlock,允许多个并发检查;Add/Remove用Lock,保证写互斥 - 值类型用
struct{}而不是bool,省内存(零字节) - 初始化时别忘了
b.list = make(map[string]struct{}),否则首次写入 panic nil map
怎么让黑名单支持自动过期(TTL)
原生 Go 没有带 TTL 的 map,硬要在内存里实现,就得自己管理时间。最简方案是组合 map[string]time.Time + 单独的清理 goroutine。
关键点不是“怎么存”,而是“怎么清”:不能每次检查都遍历全量 map,也不能让清理逻辑阻塞主流程。
- 写入时记录当前时间:
b.expires[ip] = time.Now().Add(duration) - 读取时先检查是否过期:
if exp, ok := b.expires[ip]; !ok || time.Now().After(exp) { return false } - 清理 goroutine 每 30 秒跑一次,用
time.AfterFunc或time.Ticker,遍历并删除过期项——注意遍历时要加写锁,且遍历前最好先 copy key 列表,避免边遍历边删引发 panic - 更推荐跳过手动 TTL,直接用 Redis:
client.SetEX(ctx, "blacklist:"+ip, "1", 10*time.Minute),查的时候client.Exists(ctx, "blacklist:"+ip)
HTTP 中间件里怎么安全集成黑名单检查
中间件里调用 Contains 必须是非阻塞、低延迟的,否则拖慢所有请求。重点不是“怎么写中间件”,而是“怎么避免中间件变成瓶颈”。
- 不要在中间件里做任何网络 IO(比如每次查 Redis),应提前用本地缓存兜底,比如用
freecache或bigcache缓存最近 10 分钟的黑名单结果 - 如果用了 Redis,务必设置超时:
ctx, cancel := context.WithTimeout(r.Context(), 50*time.Millisecond),超时就放行,别卡住请求 - 日志里记录被拦截的 IP 时,别直接打全量
r.RemoteAddr,要先做脱敏或哈希,防止日志泄露敏感信息 - 别在中间件里调用
Add—— 封禁动作应该由独立 API 触发(如POST /admin/blacklist),否则攻击者可能通过构造请求自行封禁别人 IP
context.WithCancel 控制的话,进程退出时 goroutine 还在跑,可能造成资源泄漏或误删。


















