本文首发于公众号:Hunter后端

原文链接:Golang基础笔记三之数组和切片

这一篇笔记介绍 Golang 里的数组和切片,以下是本篇笔记目录:

  1. 数组定义和初始化
  2. 数组属性和相关操作
  3. 切片的创建
  4. 切片的长度和容量
  5. 切片的扩容
  6. 切片操作

1、数组定义与初始化

第一篇笔记的时候介绍过数组的定义与初始化,这里再介绍一下。

数组是具有固定长度的相同类型元素的序列。

这里有两个点需要注意,数组的长度是固定的,数组的元素类型是相同的,且在定义的时候就确定好的。

1. 一维数组

我们可以通过下面几种方式对数组进行定义和赋值:

    var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
fmt.Println("arr: ", arr)

也可以在定义的时候直接对其赋值:

    var arr [3]int = [3]int{1, 2, 3}
fmt.Println("arr: ", arr)

或者定义的时候不指定数量,自动获取:

    var arr = [...]int{1, 2, 3}
fmt.Println("arr: ", arr)

还可以在定义的时候,指定索引位置的值:

    var arr = [...]string{0: "Peter", 3: "Tome", 1: "Hunter"}
fmt.Println("arr: ", arr)

2. 多维数组

多维数组一般是二维数组用的较多,示例如下,表示一个两行三列的二维数组:

    var s [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
s[i][j] = 0
}
}
fmt.Println(s)
// [[0 0 0] [0 0 0]]

2、数组属性和相关操作

1. 获取数组长度和容量

获取数组长度和容量分别使用 len() 和 cap() 函数。

    arr := [...]int{2, 3, 4}
    fmt.Println("len: ", len(arr))
    fmt.Println("cap: ", cap(arr))

对于数组而言,数组的长度是固定的,所以其长度和容量都是一样的。

对于长度和容量的概念,我们在后面介绍切片的时候,再详细介绍。

2. 数组的复制

我们可以通过 copy() 函数将一个数组复制到另一个数组,其返回值是复制元素的个数:

    arr := [3]int{2, 3, 4}
    var arr2 [3]int
    numCopied := copy(arr2[:], arr[:])
    fmt.Printf("复制的元素个数:%d, arr2:%v\n", numCopied, arr2)

3. 数组的排序

我们可以引入 sort 包,使用 sort.Ints() 函数对数组进行排序:

import "sort"
func main() {
    arr := [3]int{5, 2, 4}
    sort.Ints(arr[:])
    fmt.Println(arr) // 2, 4, 5
}

3、切片的创建

切片是对数组的一个连续片段的引用,它本身不存储数据,而是指向底层数据。

切片由三部分组成:指针,长度,容量。

指针指向引用的数组的起始位置,长度则是切片中元素的数量,容量则是从切片起始位置到底层数据末尾的元素数量。

下面介绍切片创建的几种方式。

1. 引用数组创建切片

切片本身的定义就是对数组的引用,所以可以通过引用数组的方式来创建切片:

var arr = [3]int{1, 2, 3}
var slice = arr[1:]
fmt.Println(slice) // [2 3]

在这里,切片 slice 是从 arr 第二个元素开始引用,因此 slice 的内容是 [2, 3]。

注意,这里 slice 的切片是引用的 arr 数组,所以他们指向的是同一个内存空间,如果修改切片内的元素,会同步影响数组的元素,而修改数组的元素,也会影响切片内容:

    var arr = [3]int{1, 2, 3}
var slice = arr[1:]
fmt.Printf("修改前: arr:%v, slice:%v\n", arr, slice)
// 修改前: arr:[1 2 3], slice:[2 3] arr[1] = 7
fmt.Printf("修改 arr 后,arr:%v, slice:%v\n", arr, slice)
// 修改 arr 后,arr:[1 7 3], slice:[7 3] slice[1] = 10
fmt.Printf("修改 slice 后,arr:%v, slice:%v\n", arr, slice)
// 修改 slice 后,arr:[1 7 10], slice:[7 10]

2. 创建数组的方式创建切片

使用创建数组的方式不定义其长度,创建的就是一个切片:

slice := []int{1, 2, 3}

3. make 的方式创建切片

使用 make 的方式创建切片,可以指定切片的长度和容量,其格式如下:

var 切片名 []type = make([]type, len, [cap])

make 函数接受三个参数,第一个就是切片类型,第二个是切片长度,第三个是切片容量,其中切片容量是可选参数,如不填写则默认等于切片长度。

以下是一个创建切片的示例:

    slice := make([]int, 3)
fmt.Printf("slice length:%d, cap:%d\n", len(slice), cap(slice)) // 3 3

4、切片的长度和容量

切片的长度和容量分别使用 len()cap() 函数来获取。

长度的概念很好理解,就是切片的元素个数就是它的长度。

而对于容量,可以理解是这个切片预留的总长度,而如果切片是从数组中引用而来,其定义是 从切片的第一个元素到引用数组的最后一个元素的长度就是切片的容量

对于下面这个 arr,其长度是 5,两个切片分别从第三个和第五个元素开始引用:

    arr := [5]int{1,2,3,4,5}
slice1 := arr[2:4]
slice2 := arr[4:]

对于 slice1, 它的长度就是 2,因为它引用的元素个数是两个

slice2 的长度是 1,它是从第五个元素开始引用,直到数组结尾。

但是两个切片的容量因为其开始引用的下标的不同而不一致,原数组总长度为 5

slice1 是从下标为 2 开始引用,所以它的容量是 5-2=3

slice2 是从下标为 4 开始引用,它的容量是 5-4=1

    fmt.Printf("slice1 length:%d, cap:%d\n", len(slice1), cap(slice1))
// slice1 length:2, cap:3 fmt.Printf("slice2 length:%d, cap:%d\n", len(slice2), cap(slice2))
// slice2 length:1, cap:1

5、切片的扩容

当我们向一个切片添加元素,且其长度超出了定义的容量大小,这个就涉及到切片扩容的概念。

首先,我们可以创建一个切片,然后查看其长度和容量:

    slice := make([]int, 2, 2)
slice[0] = 1
slice[1] = 2
fmt.Printf("slice length:%d, cap:%d, %p\n", len(slice), cap(slice), slice)
// slice length:2, cap:2, addr:0xc0000120c0

注意,在上面创建 slice 的时候,它的长度和容量如果是一样的话,可以默认不填写 cap 参数,这里为了表示清楚,所以显式指定其长度和容量。

当我们向 slice 再添加一个元素,就已经超出了其容量大小了,因此切片会自动进行扩容,其容量会变为原来的两倍,接着可以看到切片地址已经发生了变化:

    slice = append(slice, 3)
fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), slice)
// slice length:3, cap:4, addr:0xc000020160

而如果再往其中添加两个元素,其容量又会扩大,变为原来的两倍,变成 8:

    slice = append(slice, 4)
slice = append(slice, 5)
fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), slice)
// slice length:5, cap:8, addr:0xc00001c180

切片自动扩容规律

关于 Golang 里切片的自动扩容规律,之前搜索到这样一个扩容规律:

  1. 如果新元素追加后所需要的容量超过原容量的两倍,新容量会直接设为所需的容量
  2. 当原切片长度小于 1024 时,新容量会是原来容量的两倍
  3. 当原切片的大于等于 1024 时,新容量会是原来容量 1.25 倍

但是这个规律并不完全准确,下面是基于 go1.22.6 版本做的相应的测试:

    slice := make([]int, 2)
for _ = range 1028 {
slice = append(slice, 1)
fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), &slice)
}

对于输出的结果,后面的 cap 的变化趋势是 32, 64, 128, 256, 512, 848, 1280。

可以看到,在容量为 512 之前,确实是遵循两倍扩容的规律,但是 512 之后的扩容规律则不再是两倍,而且 1024 之后的扩容也不是 1.25 倍。

因此,去查询相关资料和源代码,发现切片自动扩容的计算分为两个阶段,第一个阶段是扩容容量计算阶段,第二个阶段是内存对齐阶段。

1) 扩容容量计算

第一阶段进行扩容容量计算的源代码如下:

func nextslicecap(newLen, oldCap int) int {
    newcap := oldCap
    doublecap := newcap + newcap
    if newLen > doublecap {
        return newLen
    }
    const threshold = 256
    if oldCap < threshold {
        return doublecap
    }
    for {
        newcap += (newcap + 3*threshold) >> 2
        if uint(newcap) >= uint(newLen) {
            break
        }
    }
    if newcap <= 0 {
        return newLen
    }
    return newcap
}

其大体逻辑为:

  1. 当新切片的长度大于原容量的两倍时,直接将新容量设为所需的容量
  2. 当原容量小于 256 时,新容量为原容量的两倍
  3. 当原容量大于等于 256 时,新容量的计算规则为 新容量 = 原容量 + (原容量 + 3 * 256) / 4, 直到这个新容量大于等于新切片的长度

在这个逻辑里,当原容量的逐步增大,新容量跟原容量的关系会逐渐向 1.25 倍靠拢,这也是之前搜索到的 1.25 倍这个数字的来源。

2) 内存对齐

而在第二个内存对齐阶段,会进一步根据切片的元素的类型,使用一个函数来进行计算,由此适配到具体的需要扩容的容量大小。

比如,我们使用字符串切片来进行测试,返回的扩容容量的大小也是不一样的:

    slice := make([]string, 2)
    for _ = range 1028 {
        slice = append(slice, "hell")
        fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), &slice)
    }

因此,对于切片的自动扩容规律这个问题,我们可以如下回答:

  1. 当新切片长度大于原容量的两倍时,新容量会直接设为所需的容量
  2. 当原容量小于 256 时,新容量为原容量的两倍
  3. 当原容量大于等于 256 时,新容量的计算规则为 新容量 = 原容量 + (原容量 + 3 * 256) / 4, 直到这个新容量大于等于新切片的长度

且在上面的计算规则之后,得出的新容量会进一步根据切片元素类型的大小进行内存对齐,通过 roundupsize() 函数得到最终的容量大小。

6、切片操作

1. 增

可以使用 append() 函数对切片尾部增加元素:

    slice := []int{1, 2, 3}
fmt.Println("slice: ", slice) slice = append(slice, 4)
fmt.Println("slice: ", slice)

如果同时增加多个元素,可以如下操作:

    slice = append(slice, 5, 6)
fmt.Println("slice: ", slice)

也可以把一个切片中的全部元素添加到原切片中:

    slice2 := []int{7, 8}
slice = append(slice, slice2...)
fmt.Println("slice: ", slice)

但是如果想要把元素插入切片中的指定位置,Golang 没有提供对应的方法,所以只能通过对切片进行拆分然后拼接的方式:

    targetIndex := 2
targetValue := 3 slice := []int{1, 2, 4, 5} slice = append(slice, -1) copy(slice[targetIndex+1:], slice[targetIndex:]) slice[targetIndex] = targetValue
fmt.Println("slice: ", slice) // [1 2 3 4 5]

2. 删

同样,Golang 也没有提供直接删除某个元素,或者根据索引下标删除元素的方法,但我们还是可以通过拼接的方式来实现。

比如想要删除切片中下标为 2 的元素,可以先将该索引后的元素都往挪一个下标,然后对切片进行截断:

    targetIndex := 2

    slice := []int{1, 2, 3, 3, 4, 5}

    copy(slice[targetIndex:], slice[targetIndex+1:])

    slice = slice[:len(slice)-1]
fmt.Println("slice: ", slice) // slice: [1 2 3 4 5]

同理,如果想要删除切片中某个指定值的元素,可以先遍历一遍切片,然后不等于该值的元素组合成一个新的切片。

3. 改

如果想要更改切片中某个下标的值,可以直接通过下标的方式进行更改:

    targetIndex := 2
targetValue := 100 slice := []int{1, 2, 3, 3, 4, 5} slice[targetIndex] = targetValue
fmt.Println("slice: ", slice) // slice: [1 2 100 3 4 5]

4. 查

如果想要查找切片中是否包含某个元素,也只能通过遍历的方式来进行查找,或者如果数组是有序的,可以通过二分查找等方式自己实现对应的方法。

Golang基础笔记三之数组和切片的更多相关文章

  1. Go语言系列(三)之数组和切片

    <Go语言系列文章> Go语言系列(一)之Go的安装和使用 Go语言系列(二)之基础语法总结 1. 数组 数组用于存储若干个相同类型的变量的集合.数组中每个变量称为数组的元素,每个元素都有 ...

  2. Numpy 笔记: 多维数组的切片(slicing)和索引(indexing)【转】

    目录 切片(slicing)操作 索引(indexing) 操作 最简单的情况 获取多个元素 切片和索引的同异 切片(slicing)操作 Numpy 中多维数组的切片操作与 Python 中 lis ...

  3. 手把手golang教程【二】——数组与切片

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是golang专题的第五篇,这一篇我们将会了解golang中的数组和切片的使用. 数组与切片 golang当中数组和C++中的定义类似, ...

  4. MYSQL基础笔记(三)-表操作基础

    数据表的操作 表与字段是密不可分的. 新增数据表 Create table [if not exists] 表名( 字段名 数据类型, 字段名 数据类型, 字段n 数据类型 --最后一行不需要加逗号 ...

  5. Golang基础笔记

    <基础> Go语言中的3个关键字用于标准的错误处理流程: defer,panic,recover. 定义一个名为f 的匿名函数: Go 不支持继承和重载. Go的goroutine概念:使 ...

  6. jquery基础 笔记三

    一. 操作"DOM属性" 在jQuery中没有包装操作"DOM属性"的函数, 因为使用javascript获取和设置"DOM属性"都很简单. ...

  7. Python学习笔记三,数组list和tuple

    list也就是列表的意思,可以存储一组数据集合,比如classmates=['zhangsan','lisi','123']每个数据用单引号包裹,逗号隔开.

  8. js 基础笔记三

    词法结构: 1:区分大小写 2:特殊字符的区分,unicode转义 3:注释, //  ;  /* */ ; 4 : 标识字符和保留字 数据类型: 1原始类型 数字,字符串,布尔值.特殊的原始值(nu ...

  9. Python基础笔记(三)

    1. 循环与流程控制 (1) for myList1 = ["A", "B", "C", "D"] # 正序遍历 for ...

  10. Vue学习计划基础笔记(三)-class与style绑定,条件渲染和列表渲染

    Class与style绑定.条件渲染和列表渲染 目标: 熟练使用class与style绑定的多种方式 熟悉v-if与v-for的用法,以及v-if和v-for一起使用的注意事项 class与style ...

随机推荐

  1. 免费包白嫖最新DeepSeek-V3驱动的MCP与SemanticKernel实战教程 - 打造智能应用的终极指南

    如果您需要深入交流了解请加入我们一块交流 https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=b7co0430-d ...

  2. STM32串口缓冲区

    在嵌入式开发中,外设通信(如UART.SPI.I2C)的数据接收常面临两大挑战:不定时.不定量数据的实时处理和高频率数据流下的稳定性保障.传统的轮询方式效率低下,而中断驱动的接收逻辑又容易因处理延迟导 ...

  3. SQLAlchemy 核心概念与同步引擎配置详解

    title: SQLAlchemy 核心概念与同步引擎配置详解 date: 2025/04/14 00:28:46 updated: 2025/04/14 00:28:46 author: cmdra ...

  4. Hystrix两种隔离方式对比

    ​在微服务架构中,我们不可避免的与Hystrix打交道,最近在面试过程中,也总是被问到Hystrix两种熔断方式的区别,今天,就给大家做个小结. 首先,Hystrix熔断方式主要有两种: 线程池隔离 ...

  5. 掌握Tortoise-ORM高级异步查询技巧

    title: 掌握Tortoise-ORM高级异步查询技巧 date: 2025/04/22 12:05:33 updated: 2025/04/22 12:05:33 author: cmdrago ...

  6. Win10/win11系统如何禁用笔记本自带键盘、笔记本键盘禁用后无法恢复解决办法【靠谱】

    原文:[靠谱]Win10/win11系统如何禁用笔记本自带键盘.禁用后无法恢复解决办法 - 搜栈网 (seekstack.cn)

  7. thinkphp 命令行执行导入

    <?phpdeclare (strict_types=1);namespace app\command;use think\console\Command;use think\console\I ...

  8. Python3 queue

    1.创建一个容器 2.把1-10放入容器 3.输出的时候先判断容器是否为空 4.依次从容器中取出 用法: Queue.qsize() 返回队列的大小 Queue.empty() 如果队列为空,返回Tr ...

  9. termux安装vim

    pkg install vim 解决乱码问题 在家⽬录( ~ )下,新建 .vimrc ⽂件 vim .vimrc 添加内容如下: set fileencodings=utf-8,gb2312,gb1 ...

  10. 一个简单的struts2配置

    目录 1 需求 2 需要导入的jar包 3 项目的目录结构 3.1  demo1.jsp 3.2 success.jsp 3.3 HelloAction.java 3.4 struts.xml 3.5 ...