Files
MyBlog/src/programming/backend/go/Go基础语法/03slice和map.md
2025-12-15 23:16:59 +08:00

6.2 KiB
Raw Blame History

icon, date, title, category
icon date title category
mdi:table-arrow-down 2025-05-22 Go 基础语法 - Slice 和 Map
Go
后端
编程
教程
数据结构
slice
map
扩容机制
深拷贝
浅拷贝
并发安全
sync.Map

当然可以以下是基于你当前环境Linux Mint XFCE、Go 1.22.2、项目目录 /home/liumangmang/GolandProjects)整理的 Go 语言 slice 与 map 深入实践步骤,涵盖:

  • slice 的扩容机制
  • slice 的深拷贝 vs 浅拷贝
  • map 的基本使用
  • map 的并发写风险与安全方案

我们将通过一个可运行的示例程序,在你的环境中一步步验证这些行为。


步骤 1创建新项目

cd /home/liumangmang/GolandProjects
mkdir go-slice-map && cd go-slice-map
go mod init go-slice-map

步骤 2编写演示代码main.go

创建 main.go

nano main.go

粘贴以下完整代码(含详细注释):

package main

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

func main() {
	fmt.Println("=== 1. Slice 扩容机制 ===")
	demoSliceGrowth()

	fmt.Println("\n=== 2. Slice 深拷贝 vs 浅拷贝 ===")
	demoSliceCopy()

	fmt.Println("\n=== 3. Map 基本使用 ===")
	demoMapUsage()

	fmt.Println("\n=== 4. Map 并发写风险(会 panic===")
	// ⚠️ 下面这行默认注释掉,避免程序崩溃
	// demoUnsafeConcurrentMap()

	fmt.Println("\n=== 5. 安全的并发 mapsync.Map===")
	demoSafeConcurrentMap()
}

// 1. 演示 slice 扩容机制
func demoSliceGrowth() {
	var s []int
	fmt.Printf("初始: len=%d, cap=%d, addr=%p\n", len(s), cap(s), s)

	for i := 0; i < 10; i++ {
		s = append(s, i)
		fmt.Printf("append %d: len=%d, cap=%d, addr=%p\n", i, len(s), cap(s), s)
	}
	// 观察:当容量不足时,底层数组会重新分配,地址改变
}

// 2. 深拷贝 vs 浅拷贝
func demoSliceCopy() {
	original := []int{1, 2, 3, 4, 5}
	shallow := original           // 浅拷贝:共享底层数组
	deep := make([]int, len(original))
	copy(deep, original)         // 深拷贝:独立底层数组

	fmt.Println("修改前:")
	fmt.Println("original:", original)
	fmt.Println("shallow :", shallow)
	fmt.Println("deep    :", deep)

	// 修改原 slice
	original[0] = 999

	fmt.Println("\n修改 original[0] = 999 后:")
	fmt.Println("original:", original) // [999, 2, 3, 4, 5]
	fmt.Println("shallow :", shallow)  // [999, 2, 3, 4, 5] ← 被影响!
	fmt.Println("deep    :", deep)     // [1, 2, 3, 4, 5]   ← 不受影响
}

// 3. Map 基本使用
func demoMapUsage() {
	m := make(map[string]int)
	m["apple"] = 5
	m["banana"] = 3

	fmt.Println("map 内容:", m)
	fmt.Println("apple 数量:", m["apple"])

	// 检查 key 是否存在
	if count, ok := m["orange"]; ok {
		fmt.Println("orange 存在,数量:", count)
	} else {
		fmt.Println("orange 不存在")
	}

	// 删除 key
	delete(m, "apple")
	fmt.Println("删除 apple 后:", m)
}

// 4. 不安全的并发 map 写入(会导致 panic
func demoUnsafeConcurrentMap() {
	m := make(map[int]int)
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(key int) {
			defer wg.Done()
			m[key] = key * 10 // 并发写 → fatal error: concurrent map writes
		}(i)
	}

	wg.Wait()
	fmt.Println("unsafe map done (should not reach here)")
}

// 5. 使用 sync.Map 实现安全并发
func demoSafeConcurrentMap() {
	var sm sync.Map
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(key int) {
			defer wg.Done()
			sm.Store(key, key*10)
		}(i)
	}

	wg.Wait()

	// 读取部分值验证
	sm.Range(func(key, value any) bool {
		if key.(int) < 5 { // 只打印前几个
			fmt.Printf("key=%v, value=%v\n", key, value)
		}
		return true // 继续遍历
	})
}

保存退出(Ctrl+O → Enter → Ctrl+X)。


步骤 3格式化并运行

go fmt
go run .

预期输出(节选):

=== 1. Slice 扩容机制 ===
初始: len=0, cap=0, addr=0x0
append 0: len=1, cap=1, addr=0xc0000180c0
append 1: len=2, cap=2, addr=0xc0000180e0
append 2: len=3, cap=4, addr=0xc00001c0a0   ← 容量翻倍,地址变了!
...

=== 2. Slice 深拷贝 vs 浅拷贝 ===
修改前:
original: [1 2 3 4 5]
shallow : [1 2 3 4 5]
deep    : [1 2 3 4 5]

修改 original[0] = 999 后:
original: [999 2 3 4 5]
shallow : [999 2 3 4 5]
deep    : [1 2 3 4 5]

=== 3. Map 基本使用 ===
map 内容: map[apple:5 banana:3]
apple 数量: 5
orange 不存在
删除 apple 后: map[banana:3]

=== 4. Map 并发写风险(会 panic===

=== 5. 安全的并发 mapsync.Map===
key=0, value=0
key=1, value=10
key=2, value=20
key=3, value=30
key=4, value=40

🔒 注意:demoUnsafeConcurrentMap() 默认被注释。如果你想亲眼看到 panic,可以取消注释它再运行——程序会崩溃并输出:

fatal error: concurrent map writes

📚 关键知识点总结

🔸 Slice 扩容机制Go 1.22

  • cap < 1024:扩容为 2 倍
  • cap >= 1024:扩容为 1.25 倍(实际策略可能微调,但趋势是渐进增长)
  • 扩容时会分配新底层数组,旧 slice 和新 slice 不再共享数据

🔸 深拷贝 vs 浅拷贝

方式 是否共享底层数组 是否安全独立修改
s2 := s1
copy(s2, s1)

💡 提醒:copy 要求目标 slice 已分配内存(用 make

🔸 Map 并发安全

  • 普通 map 不是并发安全的:多个 goroutine 同时写会 panic
  • 解决方案
    • 使用 sync.Mutexsync.RWMutex 保护 map
    • 或直接使用 sync.Map(适用于读多写少场景)

⚠️ sync.Map 性能不一定比加锁的 map 高,仅在特定场景推荐使用。


在 GoLand 中调试建议

  1. 打开项目 /home/liumangmang/GolandProjects/go-slice-map
  2. demoSliceGrowth() 中设置断点,观察每次 append 后地址变化
  3. 尝试取消注释 demoUnsafeConcurrentMap(),运行看 panic有助于理解并发风险

如果你希望继续学习 channel、goroutine、context、错误处理Go 与 Java 对比(如 slice vs ArrayList,欢迎随时告诉我!祝你 Go 编程愉快 🚀