首页 > 其他分享 >Golang笔记——切片与数组

Golang笔记——切片与数组

时间:2025-01-10 14:59:20浏览次数:3  
标签:切片 容量 cap 笔记 Golang len 数组 底层

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

在这里插入图片描述

文章目录

数组与切片的异同

相同之处

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

区别

  • 数组

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

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

切片(Slice)源码解析

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

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

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

len() 示例定义:

// 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() 示例定义:

// 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()。

长度与容量示例

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

    b := a[3:4]
    fmt.Println(len(b), cap(b)) // 输出: 1 7
}

解释:

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

append() 函数

  • append 函数的原型

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

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

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

面试考点:

  • 如果多个切片共享一个底层数组,append 超出容量时,切片迁移到新内存,其他切片仍指向旧底层数组。
  • 示例:
    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
      newcap = oldcap + (oldcap + 3*256)/4
      
    3. 内存对齐:最终容量经过内存对齐处理(如 8 字节倍数),可能略大于计算值。例如:
        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 返回值

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

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

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

建议

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

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

切片作为函数参数传递

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

  • 作为函数参数

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

    示例

    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 字段(底层数组指针),可以操作底层数组的值,从而间接修改原始数据。

标签:切片,容量,cap,笔记,Golang,len,数组,底层
From: https://blog.csdn.net/qq_42410605/article/details/144413126

相关文章

  • Golang笔记——hashmap
    本文详细介绍golang的哈希表的底层实现、扩容机制、插入查询过程以及并发安全性。文章目录定义Key无序性Key唯一性Key可比性基本使用底层实现哈希表实现hmapbucket数据结构bmap链地址法哈希冲突负载因子扩容增量扩容等量扩容查找过程插入过程删除流程非并发安全......
  • 【C#学习笔记】C#中委托
    概述C#的委托是一种类型安全的函数指针,用于引用方法,委托允许方法作为参数传递,或者将方法赋值给委托变量,并通过委托调用方法。委托类型:委托定义了方法的的签名([方法的参数类型和返回值]),所以,委托只能引用符合签名的方法。委托实例:委托是一个引用类型,可以实例化并指向一个或......
  • 2025-1-6 / 2025-1-7 做题笔记
    2025-1-6/2025-1-7做题笔记持续更新中……目录2025-1-6/2025-1-7做题笔记P11365[Ynoi2024]新本格魔法少女りすかCF1693D-DecincDividingATUTPC2023G-GraphWeightingABC269Ex-AntichainP11365[Ynoi2024]新本格魔法少女りすかケロシの代码namespaceIO{ ......
  • 学习笔记(五十一):onAreaChange 组件区域变化监听
    onAreaChange(event:(oldValue:Area,newValue:Area)=>void):T 组件区域变化时触发该回调。仅会响应由布局变化所导致的组件大小、位置发生变化时的回调。由绘制变化所导致的渲染属性变化不会响应回调,如translate、offset。若组件自身位置由绘制变化决定也不会响应回......
  • 抽象代数学习笔记
    【基础定义】笛卡尔积。\(A\timesB=\{(a,b)|a\inA,b\inB\}\)。\(A\)和\(B\)各是一个集合。运算。一般研究二元运算(加减乘除等)。运算是一种映射。从两个集合\(A,B\)到一个集合\(C\)的映射,满足\(A\timesB=C\)(笛卡尔积)。群。群是集合\(G\)和运算\(*\)的......
  • HTML基础知识笔记
    参考视频:【狂神说Java】HTML5完整教学通俗易懂_哔哩哔哩_bilibili一、基本结构二、基本标签 <h1>:一级标题,通常用于页面的主标题,字体较大且醒目。<h2>:二级标题,用于副标题或主要章节标题,字体稍小于 <h1>。<h3>:三级标题,可用于子章节标题,以此类推,还有 <h4>、<h5>、<h6>......
  • Win32汇编学习笔记09.SEH和反调试
    Win32汇编学习笔记09.SEH和反调试-C/C++基础-断点社区-专业的老牌游戏安全技术交流社区-BpSend.netSEH-structedexceptionhandler结构化异常处理跟筛选一样都是用来处理异常的,但不同的是筛选器是整个进程最终处理异常的函数,但无法做到比较精细的去处理异常(例如处理某......
  • 【学习笔记】AC自动机
    期末考试前学了下这个东西,感觉很简单,不像某mp。然而期末Day1考完就忘了,所以还是写篇笔记吧。前置知识:字典树先来看一下洛谷上的AC自动机模版题。P5357【模板】AC自动机给你一个文本串\(S\)和\(n\)个模式串\(T_{1\simn}\),请你分别求出每个模式串\(T_i\)在\(......
  • mysql-笔记
    如果要添加多列一起的唯一约束,使用第二种创建方式,查看主外键,唯一,约束的语句都是相同的。默认情况下唯一约束不起名的话,以列名为约束名。外连接left|right确定谁为逻辑主表,会显示所有逻辑主表中的内容,从表没有则为空,外连接一定要设置主外键相等(与内连接不同)。any是多......
  • [数据结构学习笔记10] 哈希表(Hashtable)
    哈希表也叫Hashmap或者Dictionary,它存储和检索都非常快,所以常用于缓存数据供后续快速访问。哈希函数,是这样的一个函数,你提供一个input,它会返回一个唯一的值(hashcode)。只要你的input是相同的,这个哈希函数会返回同样的output。从哈希函数到哈希表哈希表底层是一个数组结构,这意味......