本文首发于公众号: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. Tinyhttpd 源代码初步解读

    Tinyhttpd 是很早以前的一个 web 服务器程序,由 C 语言编写,整个程序十分小巧,源码只有几百行.它一般不适合用于生产环境,因为它很简单,只实现了读取 html 以及 Get / POST ...

  2. .NET & JSON

    C# & JSON DataContractJsonSerializer // JsonHelper.cs using System; using System.Collections.Gen ...

  3. Web前端入门第 32 问:CSS background 元素渐变背景用法全解

    渐变背景在 CSS 里面就是一个颜色到另一个颜色渐渐变化的样子. 本文示例中,盒子基础样式: .box { margin: 20px; padding: 20px; border: 10px dash ...

  4. Vue的前端项目开发环境搭建

    一.本机window端:安装Node.js,其实质性功能相当于,java的maven https://nodejs.org/en/download/ 二.本机window端:检查Node.js的版本 ...

  5. java 限流

    题记 在高并发的分布式系统中,我们都需要考虑接口并发量突增时造成的严重后果,后端服务的高压力严重甚至会导致系统宕机.为避免这种问题,我们都会为接口添加限流.降级.熔断等能力,从而使接口更为健壮. 限流 ...

  6. python爬虫爬取B站视频字幕,词频统计,使用pyecharts画词云(wordcloud)

    我们使用beatifulsop爬取到B站视频的字幕:https://www.cnblogs.com/becks/p/14540355.html 然后将爬取的字幕,使用pandas处理后写到CSV文件中 ...

  7. 聊聊四种实时通信技术:长轮询、短轮询、WebSocket 和 SSE

    这篇文章,我们聊聊 四种实时通信技术:短轮询.长轮询.WebSocket 和 SSE . 1 短轮询 浏览器 定时(如每秒)向服务器发送 HTTP 请求,服务器立即返回当前数据(无论是否有更新). 优 ...

  8. K8s新手系列之K8s中的资源

    K8s中资源的概念 在kubernetes中,所有的内容都抽象为资源,用户需要通过操作资源来管理kubernetes. kubernetes的本质上就是一个集群系统,用户可以在集群中部署各种服务,所谓 ...

  9. HttpServletRequest相关

    简介 获取客户端请求头及参数 获取提交给服务器的中文数据 简介 这个对象封装了客户端提交过来的一切数据. 获取客户端请求头及参数 package com.zhujunwei.httpServletRe ...

  10. SpringBoot项目创建的三种方式

    目录 1 通过官网创建 2 通过IDEA脚手架创建 2.1 IDEA新建项目 2.2 起Group名字,选择Java版本,点击Next 2.3 选择Web依赖,选择Spring Web,确认Sprin ...