当前位置: 首页 > article >正文

深入 Go 语言核心:map 和 slice 的传参有什么不同

在 Go 开发中,经常会遇到需要在函数中修改 map 或 slice 的场景。虽然它们都支持动态扩容,但在函数传参时的行为却大不相同。今天,让我们通过实例深入理解这个问题。

一个困惑的开始

看这样一个例子:

func main() {
    // Map 示例
    m := map[string]int{"old": 1}
    modifyMap(m)
    fmt.Println(m) // 输出: map[new:1]

    // Slice 示例
    s := []int{1, 2, 3}
    modifySlice(s)
    fmt.Println(s) // 输出: [100 2 3],而不是 [100 2 3 200]
}

func modifyMap(m map[string]int) {
    m["new"] = 1        // 会影响原始 map
    delete(m, "old")    // 也会影响原始 map
}

func modifySlice(s []int) {
    s[0] = 100          // 会影响原始 slice
    s = append(s, 200)  // 不会影响原始 slice
}

有趣的是:

  1. map 的所有操作都会影响原始数据
  2. slice 的简单索引修改会影响原始数据,但 append 可能不会

为什么会这样?让我们从内部结构开始分析。

内部结构解析

Map 的内部结构

type hmap struct {
    count      int            // 元素个数
    flags      uint8          // 状态标志
    B          uint8          // 桶的对数 B
    buckets    unsafe.Pointer // 指向桶数组的指针
    // ... 其他字段
}

当我们声明一个 map 变量时:

m := make(map[string]int)
// 实际上 m 是 *hmap,即指向 hmap 结构的指针

Slice 的内部结构

type slice struct {
    array unsafe.Pointer  // 指向底层数组的指针
    len   int            // 当前长度
    cap   int            // 当前容量
}

当我们声明一个 slice 变量时:

s := make([]int, 0, 10)
// s 是一个完整的 slice 结构体,而不是指针

深入理解传参行为

场景一:简单修改(不涉及扩容)

func modifyBoth(m map[string]int, s []int) {
    m["key"] = 1   // 通过指针修改原始 map
    s[0] = 100     // 通过指向相同底层数组的指针修改
}

图解:

Map:
main()中的 m  -----> hmap{...}  <----- modifyBoth()中的 m
(同一个底层结构)

Slice:
main()中的 s      = slice{array: 指向数组1, len: 3, cap: 3}
                           |
                           v
                        [1 2 3]
                           ^
modifyBoth()中的 s = slice{array: 指向数组1, len: 3, cap: 3}

场景二:涉及扩容的操作

func expandBoth(m map[string]int, s []int) {
    // map 扩容
    for i := 0; i < 100; i++ {
        m[fmt.Sprintf("key%d", i)] = i
    }

    // slice 扩容
    s = append(s, 200)
}

图解:

Map 扩容过程:
Before:
main()中的 m  -----> hmap{buckets: 指向存储A}
                           ^
expandBoth()中的 m ---------|

After:
main()中的 m  -----> hmap{buckets: 指向更大的存储B}  // 同一个 hmap,只是更新了内部指针
                           ^
expandBoth()中的 m ---------|


Slice 扩容过程:
Before:
main()中的 s      = slice{array: 指向数组A, len: 3, cap: 3}
                           |
                           v
                        [1 2 3]
                           ^
expandBoth()中的 s = slice{array: 指向数组A, len: 3, cap: 3}

After append:
main()中的 s      = slice{array: 指向数组A, len: 3, cap: 3}     // 保持不变
                           |
                           v
                        [1 2 3]

expandBoth()中的 s = slice{array: 指向数组B, len: 4, cap: 6}    // 新的结构体,指向新数组
                           |
                           v
                     [1 2 3 200]

关键区别解析

  1. 传递方式不同

    • map 传递的是指针,函数内外使用的是同一个 hmap 结构
    • slice 传递的是结构体副本,函数内的修改发生在副本上
  2. 扩容行为不同

    • map 扩容时,原有的 hmap 结构保持不变,只更新内部的 buckets 指针
    • slice 扩容时,会创建新的底层数组,并返回一个指向新数组的新 slice 结构体
  3. 修改效果不同

    • map 的所有操作(包括扩容)都会反映到原始数据
    • slice 的行为分两种情况:
      • 不涉及扩容的修改会影响原始数据(因为指向同一个底层数组)
      • 涉及扩容的操作(如 append)会创建新的底层数组,修改不会影响原始数据

最佳实践

基于以上原理,在编码时应注意:

  1. 对于 map:
func modifyMap(m map[string]int) {
    m["key"] = 1    // 直接修改即可,不需要返回
}
  1. 对于 slice:
func modifySlice(s []int) []int {
    // 如果需要 append 或其他可能导致扩容的操作
    return append(s, 1)
}

// 使用时
s = modifySlice(s)

总结

理解 map 和 slice 的这些差异,关键在于:

  1. map 是指针类型,始终指向同一个 hmap 结构
  2. slice 是结构体,包含了指向底层数组的指针
  3. 扩容时 map 只更新内部指针,而 slice 需要创建新的底层数组

这种设计各有优势:

  • map 的行为更加统一和直观
  • slice 的设计提供了更多的灵活性和控制权

在实际编程中,正确理解和处理这些差异,是写出健壮 Go 代码的关键。


http://www.kler.cn/a/551160.html

相关文章:

  • DeepSeek 助力 Vue 开发:打造丝滑的颜色选择器(Color Picker)
  • MySQL的聚簇索引与非聚簇索引
  • 深入解析淘宝分类详情API接口:使用Python爬虫调用
  • 网络安全不分家 网络安全不涉及什么
  • Vue 3 中可读可写的计算属性(Computed Properties)的使用场景
  • go语言并发的最佳实践
  • 2025最新Java面试题大全(整理版)2000+ 面试题附答案详解
  • Python实战进阶 No1: RESTful API - 基于Flask的实例说明
  • Golang GORM系列:GORM分页和排序
  • SpringBoot分布式应用程序和数据库在物理位置分配上、路由上和数量上的最佳实践是什么?
  • Spring Boot最新技术特性深度解析与实战应用
  • SpringBoot 核心总结图
  • HarmonyNext上传用户相册图片到服务器
  • Spreadjs与GcExcel
  • 迅为RK3568开发板篇Openharmony配置HDF控制UART-什么是串口
  • Docker+DockerCompose+Harbor安装
  • DeepSeek R1本地部署 DeepSeek Api接口调用 java go版本
  • DFS算法篇:理解递归,熟悉递归,成为递归
  • 腿足机器人之二- 运动控制概览
  • SSH 登录到 Linux 服务器为什么没有要求输入密码