Golang笔记——切片与数组

news/2025/1/13 18:47:23/

本文详细介绍Golang的切片与数组,包括他们的联系,区别,底层实现和使用注意事项等。

在这里插入图片描述

文章目录

    • 数组与切片的异同
      • 相同之处
      • 区别
    • 切片(Slice)源码解析
      • Go 源码中 `len()` 和 `cap()` 定义
      • 长度与容量示例
    • `append()` 函数
    • Go 切片扩容机制
      • 基本原理
      • 扩容策略(依据 Go 版本)
      • 扩容源码解析
      • 常见误区
      • 建议
    • 切片作为函数参数传递

数组与切片的异同

相同之处

  • 集合类型:数组和切片均属于集合类类型,其值均可用于存储某一种类型的元素。
  • 内存布局:在内存中,数组和切片的元素存储是连续分配的。
  • 访问方式:两者都可以通过下标来访问单个元素。

区别

  • 数组

    • 数组的长度是固定的,必须在声明时指定,且之后无法改变。
    • 数组的长度是其类型的一部分,例如 [3]int[4]int 是不同的类型。
    • 由于长度固定,数组在实际开发中使用较少。
  • 切片

    • 切片更加灵活,是数组的封装和增强。
    • 切片的长度可变,其类型字面量中只有元素类型,没有长度(可通过 make 函数初始化时指定长度和容量)。
    • 切片的长度可随着添加元素而动态增长,但不会因移除元素而减少(直到没有引用时垃圾回收机制才会释放)。

切片(Slice)源码解析

切片的长度和容量均可动态扩展,其底层结构如下:

go">type slice struct {array unsafe.Pointer // 指向切片中第一个元素的地址len   int            // 切片长度cap   int            // 切片容量
}
  • array:指向底层数组的内存地址。
  • len(长度):返回集合中的元素数量。切片中实际包含的元素数量,必须小于等于 cap
  • cap(容量):返回切片的最大长度(当重新切片时可达到的长度)。从切片第一个元素到底层数组末尾元素的最大可用空间。

Go 源码中 len()cap() 定义

len() 示例定义:

go">// The len built-in function returns the length of v, according to its type:// Array: the number of elements in v.// Pointer to array: the number of elements in *v (even if v is nil).// Slice, or map: the number of elements in v; if v is nil, len(v) is zero.// String: the number of bytes in v.// Channel: the number of elements queued (unread) in the channel buffer;//          if v is nil, len(v) is zero.// For some arguments, such as a string literal or a simple array expression, the// result can be a constant. See the Go language specification's "Length and// capacity" section for details.func len(v Type) int

描述:

  • 数组:返回数组中元素数量。
  • 指针数组:返回指向数组的元素数量。
  • 切片或map:返回元素数量(若切片为 nil,长度为 0)。
  • 字符串:返回字节长度。
  • 通道:返回缓冲区中未读取的元素数量(若通道为 nil,长度为 0)。

cap() 示例定义:

go">// The cap built-in function returns the capacity of v, according to its type:// Array: the number of elements in v (same as len(v)).// Pointer to array: the number of elements in *v (same as len(v)).// Slice: the maximum length the slice can reach when resliced;// if v is nil, cap(v) is zero.// Channel: the channel buffer capacity, in units of elements;// if v is nil, cap(v) is zero.// For some arguments, such as a simple array expression, the result can be a// constant. See the Go language specification's "Length and capacity" section for// details.func cap(v Type) int

描述:

  • 数组:返回元素数量(与 len() 一致)。
  • 指针数组:返回指向数组的元素数量。
  • 切片:返回可达的最大长度(若切片为 nil,容量为 0)。
  • 通道:返回缓冲区的最大容量。

注意:map没有cap()。

长度与容量示例

go">func main() {a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}fmt.Println(len(a), cap(a)) // 输出: 10 10b := a[3:4]fmt.Println(len(b), cap(b)) // 输出: 1 7
}

解释:

  • len(b) 为切片长度,即 4-3=1
  • cap(b) 为切片容量,即从索引 3 开始,直到底层数组末尾的元素数,即 10-3=7

append() 函数

  • append 函数的原型

    go">func append(slice []Type, elems ...Type) []Type
    
    • 支持可变参数,可追加多个值。
    • 可使用 ... 直接追加另一个 slice。
  • 调用规则

    • append 函数返回新的 slice,必须使用返回值,未使用返回值的调用会编译失败。一般会使用原变量赋值。
  • append() 用于向切片添加元素或合并两个切片,行为如下:

    1. 容量足够
      • 直接在原底层数组后追加元素,返回的切片与原切片共享底层数组。
      • 此时,原切片的值会发生变化。
    2. 容量不足
      • 创建一个新的底层数组,将原切片的数据复制到新数组,再追加元素。
      • 此时,原切片不会变化。

面试考点:

  • 如果多个切片共享一个底层数组,append 超出容量时,切片迁移到新内存,其他切片仍指向旧底层数组。
  • 示例:
    go">s := []int{1, 2, 3}
    x := append(s, 4) // 底层数组有剩余空间,不迁移
    y := append(s, 5) // 底层数组已满,迁移到新内存
    fmt.Println(s, x, y) // 输出: [1 2 3] [1 2 3 4] [1 2 3 5]
    

Go 切片扩容机制

基本原理

  • 使用 append() 向切片追加元素时,如果底层数组容量不足,切片会迁移到新的内存位置。
  • 扩容过程
    1. 在底层数组追加元素。
    2. 若容量不足,创建新数组并迁移原数据。
    3. 新切片预留一定容量 buffer,以降低未来迁移成本。

那么这个buffer会预留多少呢?

扩容策略(依据 Go 版本)

  • Go 1.18 之前

    1. 新容量 > 旧容量的 2 倍:直接将新容量作为扩容后的容量。
    2. 旧容量 < 1024:扩容后容量为旧容量的 2 倍。
    3. 旧容量 ≥ 1024:每次增加旧容量的 1/4,直到满足 newcap >= cap
    4. 溢出检查:若容量cap计算值溢出,最终容量直接设置为新申请容量。
  • Go 1.18 及之后

    1. 原容量 < 256:新容量为原容量的 2 倍。
    2. 原容量 ≥ 256
      go">newcap = oldcap + (oldcap + 3*256)/4
      
    3. 内存对齐:最终容量经过内存对齐处理(如 8 字节倍数),可能略大于计算值。例如:
      go">  s := make([]int, 2, 2)s = append(s, 4, 5, 6)fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // 输出: len=5, cap=6
      

扩容源码解析

  • 函数调用append 会调用 growslice 完成扩容。
  • 内存分配
    • 计算新容量 newcap
    • 调用 roundupsize 函数完成内存对齐。
    • 分配新内存,将旧数据复制到新数组,追加新元素。
  • 扩容后的特性
    • 长度(len:仅增加到实际元素数量。
    • 容量(cap:扩容后值变大,满足未来可能的 append 操作。

常见误区

  1. 未使用 append 返回值

    • 忘记更新切片引用会导致数据未正确扩容。
    go">s := []int{1, 2}
    append(s, 3) // 错误,未保存返回值
    fmt.Println(s) // 输出: [1 2]
    
  2. 忽略内存对齐影响

    • 假设容量完全等于理论计算值,未考虑内存对齐可能导致实际值略大。
  3. 未意识到 append 返回新切片

    • 原切片数据可能未被更新。

建议

为了避免意外修改原切片数据,可以通过切片的第三个索引限制容量,从而强制触发新底层数组的创建。例如:

go">a := []int{1, 2, 3, 4}
b := a[:2:2] // 限制长度和容量相等
b = append(b, 5) // 生成新的切片,原切片 a 不受影响

切片作为函数参数传递

  • 多个切片共享底层数组,所以作为函数参数传递需要特别注意:不同的切片可能同时指向同一个底层数组,因此对其中一个切片的操作可能影响到其他切片。

  • 作为函数参数

    • 切片本质是一个结构体。当切片作为函数参数传递时,切片本身是按值传递的:
      1. 直接传切片:按值传递切片结构体
        当切片作为函数参数传递时,传递的是切片结构体的值(包括指向底层数组的指针)。
        • 对切片本身(结构体字段如 lencap)的修改不会影响调用者的切片,因为传递的是结构体的副本。
        • 对切片底层数组的修改会影响调用者的切片,因为底层数组是共享的。
      2. 传递切片指针:按值传递切片结构体的指针
        当切片的指针传递到函数时,函数可以直接修改调用者的切片结构体本身(例如修改 lencap),并且仍然可以修改底层数组。
    • 函数参数无论是直接传递切片还是传递切片的指针,底层数组的值都可能会被改变,因为底层数组是通过指针访问的。是否改变底层数组,应该看容量是否足够,和append() 函数与原数组变化问题一样。

    示例

    go">func modifySlice(s []int) {s[0] = 42 // 修改底层数组
    }
    func main() {nums := []int{1, 2, 3}modifySlice(nums)fmt.Println(nums) // 输出: [42, 2, 3]
    }
    
  • Go 的参数传递

    • Go 语言中只有值传递,没有引用传递。即使传递切片,也是将切片的结构体副本传入。
    • 通过切片的 array 字段(底层数组指针),可以操作底层数组的值,从而间接修改原始数据。

http://www.ppmy.cn/news/1562865.html

相关文章

一文通透OpenVLA及其源码剖析——基于Prismatic VLM(SigLIP、DinoV2、Llama 2)及离散化动作预测

前言 当对机器人动作策略的预测越来越成熟稳定之后(比如ACT、比如扩散策略diffusion policy)&#xff0c;为了让机器人可以拥有更好的泛化能力&#xff0c;比较典型的途径之一便是基于预训练过的大语言模型中的广泛知识&#xff0c;然后加一个policy head(当然&#xff0c;一开…

C#,图论与图算法,任意一对节点之间最短距离的弗洛伊德·沃肖尔(Floyd Warshall)算法与源程序

一、弗洛伊德沃肖尔算法 Floyd-Warshall算法是图的最短路径算法。与Bellman-Ford算法或Dijkstra算法一样&#xff0c;它计算图中的最短路径。然而&#xff0c;Bellman Ford和Dijkstra都是单源最短路径算法。这意味着他们只计算来自单个源的最短路径。另一方面&#xff0c;Floy…

IOS网络协议HTTP

1、网络层基础知识 1.1、HTTP 协议层级连接性可靠性应用场景TCP传输层面向连接高文件传输、网页浏览UDP传输层无连接低实时通信、流媒体HTTP应用层基于TCP由TCP保证网页浏览、API通信 HTTP通过过程 ④⑤ 是应用层通信&#xff0c;①②③⑥⑦⑧⑨是运输层通信①②③是三次握手…

【Rust】函数

目录 思维导图 1. 函数的基本概念 1.1 函数的定义 2. 参数的使用 2.1 单个参数的示例 2.2 多个参数的示例 3. 语句与表达式 3.1 语句与表达式的区别 3.2 示例 4. 带返回值的函数 4.1 返回值的示例 4.2 返回值与表达式 5. 错误处理 5.1 错误示例 思维导图 1. 函数…

Go 中的单引号 (‘)、双引号 (“) 和反引号 (`)

在 Go 中&#xff0c;单引号 ()、双引号 (") 和反引号 () 都有不同的用途和含义&#xff0c;具体如下&#xff1a; 1. 单引号 () 单引号用于表示 字符字面量&#xff08;单个字符&#xff09;。在 Go 中&#xff0c;字符是一个单独的 Unicode 字符&#xff0c;并且它的类…

转运机器人在物流仓储行业的优势特点

在智能制造与智慧物流的浪潮中&#xff0c;一款革命性的产品正悄然改变着行业的面貌——富唯智能转运机器人&#xff0c;它以卓越的智能科技与创新的设计理念&#xff0c;引领着物流领域步入一个全新的高效、智能、无人的时代。 一、解放双手&#xff0c;重塑物流生态 富唯智能…

前端拿到zip中所有文件并下载为新的zip文件

问题原因&#xff1a;后端返回了一个zip格式文件供前端下载&#xff0c;然后下载后&#xff0c;形成了zip套zip的形式&#xff0c;当后端不愿处理时&#xff0c;前端不能坐以待毙 PS&#xff1a;当压缩包文件量过大&#xff0c;前端可能会出问题&#xff08;脑测&#xff0c;未…

天天 AI-250110:今日热点-字节豆包Web端反超百度文心一言,DeepSeek也发力了|量子位智库月报

2AGI.NET&#xff1a;天天AI-20250109 人工智能&#xff08;AI&#xff09;和硬件技术继续以惊人的速度发展&#xff0c;不断刷新我们对技术边界的认知。从英伟达的RTX 50系列显卡到清华团队的数学推理突破&#xff0c;再到AI算力的多个利好&#xff0c;这些技术的发展正在推动…