1. 为什么需要Map练习题
在Go语言开发中,map是最常用的数据结构之一。它提供了高效的键值对存储和检索能力,是处理各种业务逻辑的利器。但很多初学者在使用map时常常会遇到一些"坑",比如:
- 未初始化的map导致panic
- 并发读写引发的竞态问题
- 遍历顺序的不确定性
- 值类型的陷阱
这些问题在实际项目中一旦出现,往往需要花费大量时间排查。因此,通过系统的map练习题来掌握这些细节,对于Go开发者来说至关重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Map基础操作练习
2.1 创建和初始化
在Go中,map的声明和初始化有多种方式:
go复制// 方式1:声明后make
var m1 map[string]int
m1 = make(map[string]int)
// 方式2:声明时make
m2 := make(map[string]float64)
// 方式3:声明并初始化
m3 := map[string]bool{
"apple": true,
"orange": false,
}
注意:未初始化的map是nil,直接操作会导致panic。比如下面的代码会报错:
go复制var m map[string]int m["key"] = 1 // panic: assignment to entry in nil map
2.2 增删改查操作
go复制m := make(map[string]int)
// 添加/修改
m["apple"] = 5
m["banana"] = 7
// 查询
count := m["apple"] // 5
// 删除
delete(m, "banana")
// 检查键是否存在
if val, exists := m["orange"]; exists {
fmt.Println(val)
} else {
fmt.Println("orange not found")
}
2.3 遍历map
Go中map的遍历顺序是不确定的,这是有意为之的设计:
go复制m := map[string]int{
"apple": 5,
"banana": 7,
"orange": 3,
}
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
每次运行这段代码,输出的顺序可能都不一样。如果需要固定顺序,可以:
go复制keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
3. Map进阶练习题
3.1 并发安全处理
Go的map不是并发安全的,下面的代码会导致竞态条件:
go复制m := make(map[int]int)
// 并发写入
go func() {
for i := 0; i < 1000; i++ {
m[i] = i
}
}()
go func() {
for i := 0; i < 1000; i++ {
fmt.Println(m[i])
}
}()
解决方案有几种:
- 使用sync.Mutex
go复制var m = make(map[int]int)
var mu sync.Mutex
// 写入时
mu.Lock()
m[key] = value
mu.Unlock()
// 读取时
mu.Lock()
val := m[key]
mu.Unlock()
- 使用sync.Map(适合读多写少场景)
go复制var m sync.Map
// 存储
m.Store("key", "value")
// 加载
if val, ok := m.Load("key"); ok {
fmt.Println(val)
}
3.2 值类型陷阱
当map的值是结构体时,直接修改需要特别注意:
go复制type person struct {
name string
age int
}
m := make(map[string]person)
m["john"] = person{name: "John", age: 30}
// 这样修改不会生效
p := m["john"]
p.age = 31
fmt.Println(m["john"].age) // 仍然是30
// 正确做法1:整体替换
p.age = 31
m["john"] = p
// 正确做法2:使用指针
m2 := make(map[string]*person)
m2["john"] = &person{name: "John", age: 30}
m2["john"].age = 31 // 可以直接修改
3.3 实现Set类型
Go没有内置Set类型,可以用map来模拟:
go复制set := make(map[string]bool)
// 添加元素
set["apple"] = true
set["banana"] = true
// 检查存在
if set["apple"] {
fmt.Println("apple exists")
}
// 删除元素
delete(set, "banana")
// 获取大小
size := len(set)
更通用的实现可以使用空结构体(占用0字节):
go复制set := make(map[string]struct{})
// 添加
set["apple"] = struct{}{}
// 检查
if _, exists := set["apple"]; exists {
fmt.Println("apple exists")
}
4. 实战练习题
4.1 统计单词频率
go复制func wordCount(s string) map[string]int {
words := strings.Fields(s)
count := make(map[string]int)
for _, word := range words {
count[word]++
}
return count
}
text := "hello world hello go hello world"
fmt.Println(wordCount(text))
// 输出:map[go:1 hello:3 world:2]
4.2 实现LRU缓存
go复制type LRUCache struct {
capacity int
cache map[int]*list.Element
list *list.List
}
type entry struct {
key int
value int
}
func Constructor(capacity int) LRUCache {
return LRUCache{
capacity: capacity,
cache: make(map[int]*list.Element),
list: list.New(),
}
}
func (l *LRUCache) Get(key int) int {
if elem, ok := l.cache[key]; ok {
l.list.MoveToFront(elem)
return elem.Value.(*entry).value
}
return -1
}
func (l *LRUCache) Put(key int, value int) {
if elem, ok := l.cache[key]; ok {
elem.Value.(*entry).value = value
l.list.MoveToFront(elem)
return
}
if len(l.cache) >= l.capacity {
// 移除最久未使用的
back := l.list.Back()
delete(l.cache, back.Value.(*entry).key)
l.list.Remove(back)
}
elem := l.list.PushFront(&entry{key, value})
l.cache[key] = elem
}
4.3 实现图数据结构
go复制type Graph struct {
nodes map[string][]string
}
func NewGraph() *Graph {
return &Graph{nodes: make(map[string][]string)}
}
func (g *Graph) AddEdge(from, to string) {
g.nodes[from] = append(g.nodes[from], to)
}
func (g *Graph) BFS(start string) []string {
visited := make(map[string]bool)
queue := []string{start}
result := []string{}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
if !visited[node] {
visited[node] = true
result = append(result, node)
queue = append(queue, g.nodes[node]...)
}
}
return result
}
5. 性能优化技巧
5.1 预分配空间
当你知道map的大致大小时,可以预分配:
go复制// 不好的做法
m := make(map[int]string)
for i := 0; i < 1000; i++ {
m[i] = fmt.Sprintf("value%d", i)
}
// 更好的做法
m := make(map[int]string, 1000) // 预分配空间
for i := 0; i < 1000; i++ {
m[i] = fmt.Sprintf("value%d", i)
}
预分配可以避免多次扩容带来的性能损耗。
5.2 使用int作为键
当可能时,使用int作为键比string更高效:
go复制// 较慢
m1 := make(map[string]int)
m1["12345"] = 1
// 更快
m2 := make(map[int]int)
m2[12345] = 1
5.3 减少map访问次数
go复制// 不好的做法
if _, ok := m["key"]; ok {
val := m["key"]
// 使用val
}
// 更好的做法
if val, ok := m["key"]; ok {
// 使用val
}
每次map访问都有开销,应该尽量减少不必要的访问。
6. 常见错误与调试
6.1 并发读写panic
go复制m := make(map[int]int)
// 并发写入
go func() {
for i := 0; i < 100; i++ {
m[i] = i
}
}()
// 并发读取
go func() {
for i := 0; i < 100; i++ {
_ = m[i]
}
}()
运行这段代码可能会报错:"fatal error: concurrent map read and map write"
解决方法前文已经提到,可以使用sync.Mutex或sync.Map。
6.2 修改nil map
go复制var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
总是记得用make初始化map。
6.3 遍历时修改
go复制m := map[int]int{1: 1, 2: 2, 3: 3}
for k := range m {
if k == 2 {
delete(m, k) // 安全
}
m[k+10] = k // 可能panic
}
在Go中,遍历时删除当前键是安全的,但添加新键可能导致panic。
7. 实际项目中的应用
7.1 配置管理
go复制type Config struct {
Port int
Timeout time.Duration
Database map[string]string
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
7.2 中间件路由
go复制type Middleware func(http.Handler) http.Handler
var middlewareMap = map[string]Middleware{
"logger": loggingMiddleware,
"recovery": recoveryMiddleware,
"auth": authMiddleware,
}
func ApplyMiddleware(h http.Handler, names ...string) http.Handler {
for _, name := range names {
if mw, ok := middlewareMap[name]; ok {
h = mw(h)
}
}
return h
}
7.3 缓存实现
go复制type Cache struct {
data map[string]cacheEntry
mutex sync.RWMutex
ttl time.Duration
}
type cacheEntry struct {
value interface{}
expires time.Time
}
func NewCache(ttl time.Duration) *Cache {
return &Cache{
data: make(map[string]cacheEntry),
ttl: ttl,
}
}
func (c *Cache) Set(key string, value interface{}) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.data[key] = cacheEntry{
value: value,
expires: time.Now().Add(c.ttl),
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mutex.RLock()
entry, exists := c.data[key]
c.mutex.RUnlock()
if !exists {
return nil, false
}
if time.Now().After(entry.expires) {
c.mutex.Lock()
delete(c.data, key)
c.mutex.Unlock()
return nil, false
}
return entry.value, true
}
通过以上练习题和示例,你应该对Go中的map有了更深入的理解。记住,map是Go中非常强大且常用的数据结构,掌握它的各种用法和注意事项,对写出高质量的Go代码至关重要。
