在Golang开发中,map是常用的数据结构,很多场景下我们会将指针作为map的value存储,比如存储结构体指针、切片指针等。这种map套指针的用法是否安全,需要从map本身的特性和指针的特性两方面来分析。

map的基础并发特性
Golang内置的map不是并发安全的数据结构,官方明确说明普通map在多个goroutine同时读写时会触发fatal error。即使map的value是普通值类型,并发读写也会导致程序崩溃,更不用说嵌套指针的场景。
下面是一段并发读写普通map的示例代码,会直接触发运行时错误:
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[int]int)
// 启动写goroutine
go func() {
for i := 0; i < 1000; i++ {
m[i] = i
}
}()
// 启动读goroutine
go func() {
for i := 0; i < 1000; i++ {
fmt.Println(m[i])
}
}()
time.Sleep(time.Second)
}
map套指针的额外风险
除了map本身的并发不安全问题,嵌套指针还会带来以下额外风险:
1. 指针悬空风险
如果map中存储的指针指向的对象已经被释放,或者指针指向的变量已经离开作用域,此时通过map获取指针并解引用,就会访问无效内存,导致程序崩溃或者数据错误。
示例代码如下:
package main
import "fmt"
func getPointer() *int {
num := 10
return &num // 返回局部变量指针,函数结束后num的内存可能被回收
}
func main() {
m := make(map[string]*int)
m["test"] = getPointer()
// 此时m["test"]指向的内存可能已经无效
fmt.Println(*m["test"])
}
2. 指针指向的数据并发修改风险
即使我们通过加锁保证了map的读写安全,如果多个goroutine拿到同一个指针并修改其指向的内容,依然会出现数据竞争问题,因为指针指向的是同一块内存区域。
package main
import (
"fmt"
"sync"
)
type User struct {
Name string
Age int
}
func main() {
m := make(map[int]*User)
m[1] = &User{Name: "Tom", Age: 18}
var wg sync.WaitGroup
wg.Add(2)
// 第一个goroutine修改Age
go func() {
defer wg.Done()
m[1].Age = 20
}()
// 第二个goroutine读取Age
go func() {
defer wg.Done()
fmt.Println(m[1].Age)
}()
wg.Wait()
}
如何安全使用map套指针
要避免上述问题,可以采取以下方案:
- 如果需要在并发场景使用map,优先使用
sync.Map,它内置了并发安全机制,适合读多写少的场景。 - 如果必须使用普通map,需要配合
sync.RWMutex对map的读写操作加锁,保证同一时间只有一个goroutine修改map结构。 - 存储指针时,确保指针指向的对象生命周期长于map的使用周期,避免返回局部变量指针,或者存储已经被释放的对象指针。
- 如果多个goroutine需要修改指针指向的内容,需要对指针指向的对象也加锁,或者使用值类型存储,避免共享内存修改冲突。
下面是使用sync.RWMutex保证map套指针并发安全的示例:
package main
import (
"fmt"
"sync"
)
type User struct {
Name string
Age int
}
func main() {
var (
m = make(map[int]*User)
mutex sync.RWMutex
wg sync.WaitGroup
)
wg.Add(2)
// 写操作加写锁
go func() {
defer wg.Done()
mutex.Lock()
m[1] = &User{Name: "Tom", Age: 18}
mutex.Unlock()
}()
// 读操作加读锁
go func() {
defer wg.Done()
mutex.RLock()
user := m[1]
mutex.RUnlock()
if user != nil {
fmt.Println(user.Name, user.Age)
}
}()
wg.Wait()
}
总结
map套指针本身不是绝对不安全,核心风险来自两方面:一是map本身的并发读写不安全特性,二是指针带来的内存管理和共享修改问题。只要我们在合适的场景下选择对应的并发控制方案,合理管理指针的生命周期,就可以安全地使用这种结构。如果不需要并发场景,且指针指向的对象生命周期可控,map套指针是完全可以正常使用的。