Go 语言 slice(切片) 的使用
序言
在许多开发语言中,动态数组是必不可少的一个组成部分。在实际的开发中很少会使用到数组,因为对于数组的大小大多数情况下我们是不能事先就确定好的,所以他不够灵活。动态数组通过提供自动扩容的机制,极大地提升了开发效率。这篇文章将介绍 Go 语言中的动态数组 — slice(切片)
。
1. 数据结构
切片的组成如下, 每一个字段的含义如下:
Data
:指向存储元素数组的指针;Len
:该数组中元素的个数Cap
:该数组的容量大小
type SliceHeader struct {Data uintptrLen intCap int
}
如果你之前了解过 C++ 中的 vector 你会发现其实他们的思路是一样的。一个实际存储元素的切片如下:
2. 切片的初始化
a. 声明但不初始化
在 Go 语言中,如果你声明一个切片但不初始化它,它的默认值是 nil
。这意味着该切片没有指向任何底层数组,长度和容量都为 0:
func main() {var slice []intsliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))dataPtr := unsafe.Pointer(sliceHeader.Data)fmt.Printf("data = %v, len = %d, cap = %d\n", dataPtr, len(slice), cap(slice))
}
这里程序的输出是:
data = , len = 0, cap = 0
b. 带初始值初始化
比起第一种方式,这个会在声明的时候带上字面值来初始化一个切片:
slice := []int{1, 2, 3}
fmt.Printf("data = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
此时,该切片的 len
和 cap
会和元素数量保持一致,程序输出:
data = [1 2 3], len = 3, cap = 3
c. 使用 make 初始化
使用 make
来初始化一个切片也有两者方式,首先是第一种:
slice := make([]int, 5)
fmt.Printf("data = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
这代表创建一个切片,并且切片的 len
和 cap
都是 5,切片的元素的值采用该类型的默认值:
data = [0 0 0 0 0], len = 5, cap = 5
第二种是将 len
和 cap
分别赋值:
slice := make([]int, 2, 4)
fmt.Printf("data = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
这代表创建一个切片,并且切片的len
是 2,cap
是 4:
data = [0 0], len = 2, cap = 4
这也是最常用的方式,使用 make
来预先分配内存大小可以避免后续添加元素时频繁进行扩容操作!
d. 下标索引初始化
Go 支持指定一个索引范围来初始化一个切片,这是 C++ 的 vector 所不具备的能力,举个例子:
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4]
fmt.Println(slice) // [2 3 4]
这里有一个长度为 5 的数组,现在使用索引范围 [1, 4) 「左闭右开」
来初始化一个切片,甚至还可以这样表达:
slice = arr[:4] // 等价于 [0:4]
slice = arr[1:] // 等价于 [1:len(arr) - 1]
现在有一个问题,使用索引初始化的切片和原数组是什么关系呢?换句话说这里是否涉及到了深拷贝呢?上代码:
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // [2, 3, 4]
slice[0] = 0 // 修改值
fmt.Printf(“arr=%v\n”, arr)
fmt.Printf(“slice=%v\n”, slice)
输出结果是:
arr=[1 0 3 4 5]
slice=[0 3 4]
上文中我们了解到了一个 slice
的结构是怎么样的,结合输出的结果,不难推断出 data
指针指向了该数组的第二个位置,如下:
这里 cap
的大小为什么是 4 怎么得到的呢 — cap = cap(arr) - 1
。
3. 切片的追加和扩容
a. 元素追加
我们可以通过 append
操作来在切片最后追加元素,追加方式也有多种,举个栗子:
slice := []int{1, 2}
slice = append(slice, 3) // 追加一个元素
slice = append(slice, 4, 5, 6) // 追加多个元素
slice = append(slice, []int{7, 8}...) // 追加一个切片,...表示解包,不能省略
对于追加的操作,大家是否存在疑惑的点呢?我刚开始就不理解为什么在追加操作后对 slice
进行赋值的操作。这是因为 append
函数有一个重要的特性需要特别注意:它可能会返回一个新的底层数组(取决于是否进行扩容操作)。如果没有进行赋值操作,那么 slice
还是指向原来的数组,举个栗子:
b. 切片扩容
当切片的 len
等于 cap
时,在下一次 append
操作前就会进行一次扩容操作,扩容的逻辑如下:
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 0 < newcap && newcap < cap {newcap += newcap / 4}if newcap <= 0 {newcap = cap}}}...
}
扩容的策略总结如下:
可以看到 Go 语言增长容量的策略还是比较缓和的。
4. 切片易踩的坑
a. 参数传递类型傻傻分不清
首先,我们先聊聊 C++
当中的值传递和引用传递,就比如:
int main() {vector<int> vec = { 1, 2, 3, 4, 5 }funcJustForRead(vec)return 0;
}void funcJustForRead(vector<int> &vec) {...
}
对于某些只读的场景,我们一般会传引用,这样就大大减少了拷贝带来的开销。在 Go 语言中好像并没有 引用
的概念?但是仔细思考一下,Go 真的需要吗:
func main() {slice := []int{ 1, 2, 3, 4, 5 }funcJustForRead(slice)
}func funcJustForRead(slice []int) {...
}
形参是实参的拷贝,slice
中指向元素的是 data
指针,即使形参和实参的 data
不一样,但是两者是指向的同一个数组,所以不需要引用。
现在,这里有一个函数会对切片进行追加操作,我依然是值传递是否还是可行呢?举个栗子(假设这里不涉及扩容操作):
func main() {slice := []int{1, 2}fmt.Println(slice)funcForAppend(slice)fmt.Println(slice)
}func funcForAppend(slice []int) {slice = append(slice, 3)
}
输出结果是:
[1 2]
[1 2]
并没有预想的新增一个值,为什么?上面我们介绍了,append
会返回一个新的切片,我们在 main
中使用的还是原来的切片。怎么解决呢?传递指针:
func main() {slice := make([]int, 0, 2)fmt.Println(slice)funcForAppend(&slice)fmt.Println(slice)
}func funcForAppend(slice *[]int) {*slice = append(*slice, 3)
}
b. len 和 cap 傻傻分不清
之前我们谈到过,可以预先分配好空间,可以避免后续的频繁扩容操作,但是是否会有以下的误解呢:
func main() {slice := make([]int, 5)slice = append(slice, 1)slice = append(slice, 1)slice = append(slice, 1)slice = append(slice, 1)fmt.Println(slice) // [0 0 0 0 0 1 1 1 1]
}
这里代表预先分配好 5 个空间,并且每一个空间使用该类型的默认值填充,当我们新加入元素时,是在已有的基础上往后添加而不是从前开始覆盖。正确的姿势应该是这样子的:
func main() {slice := make([]int, 0, 5)slice = append(slice, 1)slice = append(slice, 1)slice = append(slice, 1)slice = append(slice, 1)fmt.Println(slice) // [1 1 1 1]
}
5. 总结
不仅只是会使用,并且知其所以然。我自认为这是非常重要的,这不仅能够很大程度上减小我们在开发中犯错的概念,还能够有效提升代码的质量。所以通过这篇 silce
带我们走入 Go 的世界吧。
相关文章:

Go 语言 slice(切片) 的使用
序言 在许多开发语言中,动态数组是必不可少的一个组成部分。在实际的开发中很少会使用到数组,因为对于数组的大小大多数情况下我们是不能事先就确定好的,所以他不够灵活。动态数组通过提供自动扩容的机制,极大地提升了开发效率。这…...
Android Exoplayer 实现多个音视频文件混合播放以及音轨切换
在之前的文章ExoPlayer中常见MediaSource子类的区别和使用场景中介绍了Exoplayer中各种子MediaSource的使用场景,这篇我们着重详细介绍下实现多路流混合播放的用法。常见的使用场景有:视频文件电影字幕、正片视频广告视频、背景视频背景音乐等。 初始化…...
深入浅出:Java 中的动态类加载与编译技术
1. 引言 Java 的动态性是其强大功能之一,允许开发者在运行时加载和编译类,从而构建灵活、可扩展的应用程序。动态类加载和编译在许多高级场景中至关重要,例如插件系统、动态代理、框架开发(如 Spring)和代码生成工具。Java 提供了两大核心机制来实现这一目标: 自定义 Cl…...

js常用的数组遍历方式
以下是一个完整的示例,将包含图片、文字和数字的数组渲染到 HTML 页面,使用 多种遍历方式 实现不同的渲染效果: 1. 准备数据(数组) const items [{ id: 1, name: "苹果", price: 5.99, image: "h…...

【网络编程】五、三次握手 四次挥手
文章目录 Ⅰ. 三次握手Ⅱ. 建立连接后的通信Ⅲ. 四次挥手 Ⅰ. 三次握手 1、首先双方都是处于未通信的状态,也就是关闭状态 CLOSE。 2、因为服务端是为了服务客户端的,所以它会提前调用 listen() 函数进行对客户端请求的监听。 3、接着客户端就…...
【类拷贝文件的运用】
常用示例 当我们面临将文本文件分成最大大小块的时,我们可能会尝试编写如下代码: public class TestSplit {private static final long maxFileSizeBytes 10 * 1024 * 1024; // 默认10MBpublic void split(Path inputFile, Path outputDir) throws IOException {…...

从 AGI 到具身智能体:解构 AI 核心概念与演化路径全景20250509
🤖 从 AGI 到具身智能体:解构 AI 核心概念与演化路径全景 作者:AI 应用实践者 在过去的几年中,AI 领域飞速发展,从简单的文本生成模型演进为今天具备复杂推理、感知能力的“智能体”系统。本文将从核心概念出发&#x…...

Docker Compose 的历史和发展
这张图表展示了Docker Compose从V1到V2的演变过程,并解释了不同版本的Compose文件格式及其支持情况。以下是对图表的详细讲解: Compose V1 No longer supported: Compose V1已经不再支持。Compose file format 3.x: 使用了版本3.x的Compose文件格式。 …...
ARMV8 RK3399 u-boot TPL启动流程分析 --crt0.S
上一篇介绍到start.S 最后一个指令是跳转到_main, 接下来分析 __main 都做了什么 arch/arm/lib/crt0.S __main 注释写的很详细,主要分为5步 1. 准备board_init_f的运行环境 2. 跳转到board_init_f 3. 设置broad_init_f 申请的stack 和 GD 4. 完整u-boot 执行re…...

从 JIT 即时编译一直讲到CGI|FastGGI|WSGI|ASGI四种协议的实现细节
背景 我一度理解错了这个东西,之前没有AI的时候,也没深究过,还觉得PHP8支持了常驻内存的运行的错误理解,时至今日再来看这个就很清晰了。 另外,早几年对以上4个协议,我也没搞懂,时至今日&…...
Vue.js 页面切换空白与刷新 404 问题深度解析
在使用 Vue.js 开发单页应用 (SPA) 的过程中,开发者经常会遇到两个常见问题:页面切换时出现短暂的空白屏幕,以及刷新页面时返回 404 错误。这两个问题不仅影响用户体验,还可能阻碍项目的正常上线。本文将深入探讨这两个问题的成因…...

CSS3 遮罩
在网页设计中,我们经常需要实现一些特殊的视觉效果来增强用户体验。CSS3 遮罩(mask)允许我们通过控制元素的可见区域来创建各种精美的视觉效果。本文将带你全面了解 CSS3 遮罩的功能和应用。 什么是 CSS3 遮罩? CSS3 遮罩是一种…...

ResNet残差神经网络的模型结构定义(pytorch实现)
ResNet残差神经网络的模型结构定义(pytorch实现) ResNet‑34 ResNet‑34的实现思路。核心在于: 定义残差块(BasicBlock)用 _make_layer 方法堆叠多个残差块按照 ResNet‑34 的通道和层数配置来搭建网络 import torch…...

uniapp|商品列表加入购物车实现抛物线动画效果、上下左右抛入、多端兼容(H5、APP、微信小程序)
以uniapp框架为基础,详细解析商品列表加入购物车抛物线动画的实现方案。通过动态获取商品点击位置与购物车坐标,结合CSS过渡动画模拟抛物线轨迹,实现从商品图到购物车图标的动态效果。 目录 核心实现原理坐标动态计算抛物线轨迹模拟动画元素控制代码实现详解模板层设计脚本…...

谈AI/OT 的融合
过去的十几年间,工业界讨论最多的话题之一就是IT/OT 融合,现在,我们不仅要实现IT/OT 的融合,更要面向AI/OT 的融合。看起来不太靠谱,却留给我们无限的想象空间。OT 领域的专家们不要再当“九斤老太”,指责这…...

USB传输模式
USB有四种传输模式: 控制传输, 中断传输, 同步传输, 批量传输 1. 中断传输 中断传输一般用于小批量, 非连续的传输. 对实时性要求较高. 常见的使用此传输模式的设备有: 鼠标, 键盘等. 要注意的是, 这里的 “中断” 和我们常见的中断概念有差异. Linux中的中断是设备主动发起的…...
Tomcat的`context.xml`配置详解!
全文目录: 开篇语前言一、context.xml 文件的基本结构二、常见的 context.xml 配置项1. **数据源(DataSource)配置**示例: 2. **日志配置**示例: 3. **设置环境变量(Environment Variables)**示…...
MapReduce 的工作原理
MapReduce 是一种分布式计算框架,用于处理和生成大规模数据集。它将任务分为两个主要阶段:Map 阶段和 Reduce 阶段。开发人员可以使用存储在 HDFS 中的数据,编写 Hadoop 的 MapReduce 任务,从而实现并行处理1。 MapReduce 的工作…...

.NET10 - 尝试一下Open Api的一些新特性
1.简单介绍 .NET9中Open Api有了很大的变化,在默认的Asp.NET Core Web Api项目中,已经移除了Swashbuckle.AspNetCore package,同时progrom中也变更为 builder.Servers.AddOpenApi() builder.Services.MapOpenApi() 2025年微软将发布…...

RabbitMQ 工作模式
RabbitMQ 一共有 7 中工作模式,可以先去官网上了解一下(一下截图均来自官网):RabbitMQ 官网 Simple P:生产者,要发送消息的程序;C:消费者,消息的接受者;hell…...

基于C++的多线程网络爬虫设计与实现(CURL + 线程池)
在当今大数据时代,网络爬虫作为数据采集的重要工具,其性能直接决定了数据获取的效率。传统的单线程爬虫在面对海量网页时往往力不从心,而多线程技术可以充分利用现代多核CPU的计算能力,显著提升爬取效率。本文将详细介绍如何使用C…...
Android11.0 framework第三方无源码APP读写断电后数据丢失问题解决
1.前言 在11.0中rom定制化开发中,在某些产品开发中,在某些情况下在App用FileOutputStream读写完毕后,突然断电 会出现写完的数据丢失的问题,接下来就需要分析下关于使用FileOutputStream读写数据的相关流程,来实现相关 功能 2.framework第三方无源码APP读写断电后数据丢…...
国产大模型「五强争霸」:决战AGI,谁主沉浮?
引言 中国AI大模型市场正经历一场史无前例的洗牌!曾经“百模混战”的局面已落幕,字节、阿里、阶跃星辰、智谱和DeepSeek五大巨头强势崛起,形成“基模五强”新格局。这场竞争不仅是技术实力的较量,更是资源、人才与生态的全面博弈。…...
【Python 基础语法】
Python 基础语法是编程的基石,以下从核心要素到实用技巧进行系统梳理: 一、代码结构规范 缩进规则 使用4个空格缩进(PEP 8标准)缩进定义代码块(如函数、循环、条件语句) def greet(name):if name: # 正确缩…...

【日撸 Java 三百行】Day 11(顺序表(一))
目录 Day 11:顺序表(一) 一、关于顺序表 二、关于面向对象 三、代码模块分析 1. 顺序表的属性 2. 顺序表的方法 四、代码及测试 拓展: 小结 Day 11:顺序表(一) Task: 在《数…...
path环境变量满了如何处理,分割 PATH 到 Path1 和 Path2
要正确设置 Path1 的值,你需要将现有的 PATH 环境变量 中的部分路径复制到 Path1 和 Path2 中。以下是详细步骤: 步骤 1:获取当前 PATH 的值 打开环境变量窗口: 按 Win R,输入 sysdm.cpl,点击 确定。在 系…...

软考 系统架构设计师系列知识点之杂项集萃(55)
接前一篇文章:软考 系统架构设计师系列知识点之杂项集萃(54) 第89题 某软件公司欲开发一个Windows平台上的公告板系统。在明确用户需求后,该公司的架构师决定采用Command模式实现该系统的界面显示部分,并设计UML类图如…...

保持Word中插入图片的清晰度
大家有没有遇到这个问题,原本绘制的高清晰度图片,插入word后就变模糊了。先说原因,word默认启动了自动压缩图片功能,分享一下如何关闭这项功能,保持Word中插入图片的清晰度。 ①在Word文档中,点击左上角的…...
Web应用开发指南
一、引言 随着互联网的迅猛发展,Web应用已深度融入日常生活的各个方面。为满足用户对性能、交互与可维护性的日益增长的需求,开发者需要一整套高效、系统化的解决方案。在此背景下,前端框架应运而生。不同于仅提供UI组件的工具库,…...
贝叶斯算法
贝叶斯算法是一类基于贝叶斯定理的机器学习算法,它们在分类任务中表现出色,尤其在处理具有不确定性和 probabilistic 关系的数据时具有独特优势。本文将深入探讨贝叶斯算法的核心原理、主要类型以及实际应用案例,带你领略贝叶斯算法在概率推理…...