Golang-常见数据结构Slice
Slice
slice 翻译成中文就是切片,它和数组(array)很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。
了解 slice 的本质, 最简单的方法就是看它的源码:
// runtime/slice.go
type slice struct {array unsafe.Pointer // 元素指针len int // 长度 cap int // 容量
}
slice 共有三个属性:
- 指针 指向底层数组
- 长度 表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度
- 容量 底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。

注意: 底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
slice 创建
| 方式 | 代码示例 |
|---|---|
| 直接声明 | var slice []int |
| new | slice := *new([]int) |
| 字面量 | slice := []int{1,2,3,4} |
| make | slice := make(int[], 5, 10) |
| 从切片或者数组"截取" | slice := array[1:5] 或 slice := sourceSlice[1:5] |
直接声明
第一种创建出来的 slice 其实是一个 nil slice。它的长度和容量都为0。和nil比较的结果为true。
这里比较混淆的是empty slice,它的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0。空切片和 nil 比较的结果为false。

| 创建方式 | nil切片 | 空切片 |
|---|---|---|
| 方式一 | var s1 []int | var s2 = []int{} |
| 方式二 | var s4 = *new([]int) | var s3 = make([]int, 0) |
| len | 0 | 0 |
| cap | 0 | 0 |
| 和 nil 比较 | true | false |
nil 切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil 切片。
关于nil slice和empty slice的探索可以参考 - 深度解析 Go 语言中「切片」的三种特殊状态
字面量
比较简单,直接用初始化表达式创建。
package mainimport "fmt"func main() {s1 := []int{0, 1, 2, 3, 8: 100}fmt.Println(s1, len(s1), cap(s1))
}
运行结果:
[0 1 2 3 0 0 0 0 100] 9 9
唯一值得注意的是上面的代码例子中使用了索引号,直接赋值,这样,其他未注明的元素则默认 0 值
make
make 函数需要传入三个参数:切片类型,长度,容量。当然,容量可以不传,默认和长度相等。
使用 make 关键字创建 slice:
packge main
import "fmt"func main(){// 长度为 5, 容量为 10slice := make(int[], 5, 10)// 索引为 2 的元素赋值为 2slice[2] = 2 fmt.Println(slice)
}
截取
截取也是比较常见的一种创建 slice 的方法,可以从数组或者 slice 直接截取,当然需要指定起止索引位置。
基于已有 slice 创建新 slice 对象,被称为 reslice。新 slice 和老 slice 共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。
基于数组创建的新 slice 对象也是同样的效果:对数组或 slice 元素作的更改都会影响到彼此。
值得注意的是,新老 slice 或者新 slice 老数组互相影响的前提是两者共用底层数组,如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组。
截取操作采用如下方式:
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// data[low, high, max]
slice := data[2:4:6]
其他易错点
slice 和数组的区别在哪
slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。
数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。
而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。
append 到底做了什么
先来看看 append 函数的原型:
func append(slice []Type, elems ...Type) []Type
append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 … 传入 slice,直接追加一个切片。
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
注: append函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。
slice 扩容
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
大多数的文章都是这样描述的:
当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。
其实结论不太准确的
为了说明上面的规律是错误的,如下代码:
package mainimport "fmt"func main() {s := make([]int, 0)oldCap := cap(s)for i := 0; i < 2048; i++ {s = append(s, i)newCap := cap(s)if newCap != oldCap {fmt.Printf("[%d -> %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap)oldCap = newCap}}
}
运行结果:
[0 -> -1] cap = 0 | after append 0 cap = 1
[0 -> 0] cap = 1 | after append 1 cap = 2
[0 -> 1] cap = 2 | after append 2 cap = 4
[0 -> 3] cap = 4 | after append 4 cap = 8
[0 -> 7] cap = 8 | after append 8 cap = 16
[0 -> 15] cap = 16 | after append 16 cap = 32
[0 -> 31] cap = 32 | after append 32 cap = 64
[0 -> 63] cap = 64 | after append 64 cap = 128
[0 -> 127] cap = 128 | after append 128 cap = 256
[0 -> 255] cap = 256 | after append 256 cap = 512
[0 -> 511] cap = 512 | after append 512 cap = 1024
[0 -> 1023] cap = 1024 | after append 1024 cap = 1280
[0 -> 1279] cap = 1280 | after append 1280 cap = 1696
[0 -> 1695] cap = 1696 | after append 1696 cap = 2304
在老 slice 容量小于1024的时候,新 slice 的容量的确是老 slice 的2倍。目前还算正确。
但是,当老 slice 容量大于等于 1024 的时候,情况就有变化了。当向 slice 中添加元素 1280 的时候,老 slice 的容量为 1280,之后变成了 1696,两者并不是 1.25 倍的关系(1696/1280=1.325)。添加完 1696 后,新的容量 2304 当然也不是 1696 的 1.25 倍。
可见,现在网上各种文章中的扩容策略并不正确。我们直接搬出源码:源码面前,了无秘密。
从前面汇编代码我们也看到了,向 slice 追加元素的时候,若容量不够,会调用 growslice 函数,所以我们直接看它的代码。
// go 1.9.5 src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {// ……newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {if old.len < 1024 {newcap = doublecap} else {for newcap < cap {newcap += newcap / 4}}}// ……capmem = roundupsize(uintptr(newcap) * ptrSize)newcap = int(capmem / ptrSize)
}
看到了吗?如果只看前半部分,现在网上各种文章里说的 newcap 的规律是对的。现实是,后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。
之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。
为什么 nil slice 可以直接 append
其实 nil slice 或者 empty slice 都是可以通过调用 append 函数来获得底层数组的扩容。最终都是调用 mallocgc 来向 Go 的内存管理器申请到一块内存,然后再赋给原来的nil slice 或 empty slice,然后摇身一变,成为“真正”的 slice 了。
总结
- 切片是对底层数组的一个抽象,描述了它的一个片段。
- 切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。
- 多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。
- append 函数会在切片容量不够的情况下,调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。
- 扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量 >= 原容量的 2 倍或 1.25 倍。
- 当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。
参考
深度解密Go语言之Slice
相关文章:
Golang-常见数据结构Slice
Slice slice 翻译成中文就是切片,它和数组(array)很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。 了解 slice 的本质, 最简单的方法就是…...
操作系统——设备管理
0.关注博主有更多知识 操作系统入门知识合集 目录 1.设备管理概念 2.SPOOLing技术 1.设备管理概念 在计算中,除CPU、内存以外的所有设备统称为外设,即外部设备,例如鼠标、键盘、打印机、摄像头、磁盘、硬盘......那么这些只需要连接到计…...
图片分类:精细化分类,(Fine-Grained Categorization) 基于人的行为的精细化分类
文字大纲 简介数据集常用数据集方法1 : 强监督方法2 : 弱监督传统 ResNet EfficientNet 等Two Level Attention Model双线性网络 Bilinear CNN model参考文献和学习路径简介 细粒度图像识别 (fine-grained image recognition),即 精细化分类。 细粒度图像分类(Fine-Graine…...
Matlab2012a的图像处理工具箱的imshow函数
在处理图片文件时,除了使用matlab自带的image函数,还可以考虑用matlab的图像处理工具箱。这个工具箱提供了imshow和imtool两个函数,可实现图片的显示。 这两个函数都支持Handle Graphics体系结构,它们可创建图像对象,…...
Linux_红帽8学习笔记分享_10(SELinux管理与防火墙)
Linux_红帽8学习笔记分享_10(SELinux管理与防火墙) 文章目录 Linux_红帽8学习笔记分享_10(**SELinux管理与防火墙**)1.Linux系统的安全机制1.1 Filesystem1.2 Service1.3 Firewall1.4 SELinux 2.SElinux理论概述3.SElinux的配置文件3.1 SELINUX参数3.2 SELINUXTYPE参数 4.查看和…...
【资料分享】
文章目录 前言《408统考,真题&解析(2009-2021)》《Android 移动应用基础教程(Android Studio)(第2版)》黑马程序员 源代码《Hackers & Painters》--Paul Graham《数字电路与逻辑设计(第3版…...
NewBing 还无法访问的几个问题
大部分的AI自媒体都在说,Bing new已经向全世界开放了,我也凑一下这个热闹,用Edge浏览器打开,访问https://www.bing.com/new?ccus 想体验一下Bing new的效果,结果如下: 相信很多人都碰到了这个问题 此体验…...
将 Segment Anything 扩展到医学图像领域
文章目录 前言技术交流SAM 拆解分析从医学角度理解 SAM 的效用MedSAM实验总结 前言 SAM 是一种在自然图像分割方面取得成功的模型,但在医学图像分割方面表现不佳。MedSAM 首次尝试将 SAM 的成功扩展到医学图像,并成为用于分割各种医学图像的通用工具。为…...
毕业5年,技术越来越好,混的却越来越差...
别人都是越来越好,而我是越来越差! 17年,从一个普通的本科毕业,那个时候的我,很迷茫,简历上的求职岗位都不知道写什么,因为家里是农村的,朴实的父母也帮不上什么忙,关于…...
C#实现把txt文本数据快速读取到excel中
主要介绍了C#实现把txt文本数据快速读取到excel中,本文直接给出示例代码,需要的朋友可以参考下 今天预实现一功能,将txt中的数据转到excel表中,做为matlab的数据源。搜集一些c#操作excel的程序。步骤如下: 下载一个Microsoft.Office.Interop.Excel.dll 在项目中引用。 编…...
Office转换需要用到的SDK(建议)
PDF相关SDK1. PDFBox:一个基于Java的开源库,可以读取、创建和操作PDF文件。2. iText:一个用于Java和.NET的开源库,可以创建、编辑和操作PDF文件。3. MuPDF:一个轻量级的开源PDF阅读器和渲染器,可用于创建和…...
python语法入门到面向过程编程(二)
类型总结 一:整形基本使用(int) 1 int用途:年龄 号码 等级… 2 定义方式 age10 ageint(10) xint(‘1111’) int只能将纯数字的字符串转成十进制的整型 print(type(x)) 3 整形常用操作内置的方法 算数运算(、—、*、/࿰…...
Java常用类
基本数据类型包装类 Java是一种纯面向对象语言,但是java中有8种基本数据类型,破坏了java为纯面向对象的特征。为了承诺在java中一切皆对象,java又给每种基本数据类型分别匹配了一个类,这个类我们称之为包装类/封装类。每个基本数据…...
4.30下周美联储携非农来袭黄金多空该如何布局?
近期有哪些消息面影响黄金走势?下周黄金多空该如何研判? 黄金消息面解析:周五(4月28日)当周金价维持震荡交投,金价基本持稳于2000美元下方。支撑和打压金价的因素参半。经济衰退的担忧,以及避险情绪支持金价&#x…...
利用python查找指定目录下大于300M的文件
直接上代码,欢迎小伙伴们交流 import os def getBigFile(path, filesize): # 遍历指定目录及其子目录 for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: target_file os.path.join(dirpath, filename…...
浅尝ChatGPT使用之Python字典嵌套排序
一、背景 所负责的项目从v1.0升级到v2.0之后,发送到kafka的Json数据字段顺序和内容有所改变, v1.0版本推送数据样例: {"name": "小王子","author": "安托万德圣-埃克苏佩里(1900-1944&#…...
最大网络流算法之dinic算法详解
1、题目描述 On the Internet, machines (nodes) are richly interconnected, and many paths may exist between a given pair of nodes. The total message-carrying capacity (bandwidth) between two given nodes is the maximal amount of data per unit time that can b…...
051、面试必刷TOP101--链表(230503)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言链表1、BM1 反转链表2、BM2 链表内指定区间反转3、BM3 链表中的节点每k个一组翻转4、BM4 合并两个排序的链表5、BM5 合并k个已排序的链表6、BM6 判断链表中是否…...
开源中国面试准备
dockerFile常见命令 1、FROM 设置要制作的镜像基于哪个镜像,FROM指令必须是整个Dockerfile的第一个指令,如果指定的镜像不存在默认会自动从Docker Hub上下载 2、MAINTAINER 镜像作者的信息,比如名字或邮箱地址 语法:MAINTAINER n…...
基于J2EE的B2C电子商务系统开发与实现
摘要 当今社会,科学技术突飞猛进,知识经济初见端倪。电子商务作为一种新型的贸易方式,极大地促进了全球经济贸易的发展,同时也正在改变人们的生活方式和思想观念。电子商务是指整个贸易活动实现电子化,交易各方以电子交易方式而进行的商业交易。世界贸易组织电子商务专题报告定…...
K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)
考察一般的三次多项式,以r为参数: p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]; 此多项式的根为: 尽管看起来这个多项式是特殊的,其实一般的三次多项式都是可以通过线性变换化为这个形式…...
华为OD机考-机房布局
import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...
【网络安全】开源系统getshell漏洞挖掘
审计过程: 在入口文件admin/index.php中: 用户可以通过m,c,a等参数控制加载的文件和方法,在app/system/entrance.php中存在重点代码: 当M_TYPE system并且M_MODULE include时,会设置常量PATH_OWN_FILE为PATH_APP.M_T…...
