惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
S
Security Affairs
阮一峰的网络日志
阮一峰的网络日志
L
LangChain Blog
Microsoft Azure Blog
Microsoft Azure Blog
雷峰网
雷峰网
Recent Announcements
Recent Announcements
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
博客园_首页
The Cloudflare Blog
M
MIT News - Artificial intelligence
博客园 - 【当耐特】
MyScale Blog
MyScale Blog
S
SegmentFault 最新的问题
P
Proofpoint News Feed
Y
Y Combinator Blog
Jina AI
Jina AI
博客园 - 聂微东
A
About on SuperTechFans
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
F
Full Disclosure
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Microsoft Security Blog
Microsoft Security Blog
爱范儿
爱范儿
T
Tailwind CSS Blog
J
Java Code Geeks
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Blog of Author Tim Ferriss
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 三生石上(FineUI控件)
W
WeLiveSecurity
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
宝玉的分享
宝玉的分享
IT之家
IT之家
Hacker News: Ask HN
Hacker News: Ask HN
The Register - Security
The Register - Security
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs

咸糖 - 自律者自由

2025 年终总结:降噪、重构与长期主义 在新加坡和新山吃过最好的食物(持续更新) 写给还在迷茫的你:我的三本大学回忆 2024 年终总结 Neovim: No Crash Incremental Selection 2022 年终总结 使用 neovim 作为 PDE(个性化开发环境) shell 是一个不错的生产力工具 使用二八法则省力地学习 awk 肉身翻墙新加坡安顿指南 使用 Docker Compose 建立你自己的开发环境 关于编写可维护的代码的一些实践与想法 我为什么使用双向链接做笔记? 关于焦虑和拖延症 Golang: 如何处理日渐膨胀的 interface 使用番茄工作法来更好的利用你的时间 Unix 如何杀死一个进程和它的子孙进程? Golang: 让你的零值更有用 使用 Mock 和 Interface 进行 Golang 单测 总结一些计算机常用的原则 重新学习英语语法 上班族近期小半年入门投资基金组合的学习与实践经历 疫情期间的肉身翻墙新加坡指南 About me 软技能:大厂底层员工打工指南 软技能:我是如何获取知识与信息的? 分布式的令牌桶算法的实现 实现一个AtomicInteger GC root 在哪里? 什么是 Minor GC/Major GC 漏桶算法的设计与实现 剑指offer 单例模式 TCP 针对面试学习 Actor 如何处理阻塞消息 Akka 源码解析 How to learn scala AES 需要限制 SEED 长度 Java 如何区分==与.equals()方法 2018年年度总结 Java 集合扩容
关于 Golang Slice 的一些细节
xiantang · 2021-12-21 · via 咸糖 - 自律者自由

文章目录

【注意】最后更新于 December 29, 2025,文中内容可能已过时,请谨慎使用。

在 Golang 中,有两种数据类型:

一种是限定长度的数组,叫做 Array,另外一种是不限定长度的数组,叫做 Slice。

区分 Array 和 Slice

Array 和 Slice 的区别在于:

Array 是限定长度的,并且 Array 的长度是类型的一部分,因此 Array 的长度不能改变,而 Slice 可以改变长度。

Slice 是不限定长度的,可以使用 make 函数来创建。

foo = make([]int, 5) 并且 Slice 只是一个数据结构,内部有一个指针,指向数组的首地址,可以使用 len 函数来获取 Slice 的长度,也可以使用 cap 函数来获取 Slice 的容量。

下面会详细的介绍 Slice 的一些实现细节和特性。

Slice 的实现与特性

上文已经提到 Slice 其实是一个数据结构,内部有一个指针,指向数组的首地址。 让我们来先简单看看 Slice 的实现吧:

我们先给出一个简单数据结构,用来演示 Slice 的实现:

1
2
3
4
5
type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
}

我们可以看到 Slice 的实现是个结构体,其中包含了三个字段: 第一个字段是指向底层数组的指针,第二个字段是 Slice 的长度,第三个字段是 Slice 的容量。 当你初始化一个长度为 5 的 Slice 的时候,他是这样的:

foo = make([]int, 5)

foo = make([]int, 3, 5)

slice

当你初始化一个为 nil 的 Slice 的时候,他是这样的: var foo []int

1
2
3
4
5
sliceHeader{
    Length:        0,
    Capacity:      0,
    ZerothElement: nil,
}

上文的数据结构中可以看到,Slice 并不是一个真正的数组,而是一个数据结构,它的实现是个结构体,所以当我们在函数间传输 Slice 的时候,其实只是传输了一个 Slice 的 header。所以对于老练的 Gopher 来说,他们在函数间传输和 Channel 间传输的时候经常会提及 slice header。

我们可以讨论一下当 Slice 作为参数传递的时候会发生什么。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"fmt"
)

func main() {
 slice := []string{"a", "a"}

 func(slice []string) {
  slice = append(slice, "a")
  fmt.Print(slice)
 }(slice)
 fmt.Print(slice)
}

可以发现他运行的输出是:

1
2
[a a a][a a]
Program exited.

可以发现 Slice 作为参数被传递的时候,实际上和传递一个结构体一样,当你使用 append 之后赋给 slice 变量的时候只是把函数拷贝的值改了一下。 从这个例子可以看出,Golang 其实是 copy by value,而不是 copy by reference。当你传入一个结构体的时候,Golang 其实是把这个结构体拷贝了一份。

举一个另外一个例子,来体现 append 的效果以及:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
 x := make([]string, 0, 6)

 func() {
  y := append(x, "hello", "world")
  fmt.Print(y)
 }()
 func() {
  z := append(x, "goodbye", "bob")
  fmt.Print(z)
 }()
}
1
2
[hello world][goodbye bob]
Program exited.

可以看出当你在 append 的时候,其实是会修改底层数组。但是我们发现其实如果对数组进行 append 了之后,其实不会对 x 进行修改,因为 x 其实并没有被修改。记住他只是 slice header,他的内容只取决于他的 len,cap 和 array 指针。

Slice 的一些坑

切片的坑

Slice append 的时候,如果超出 cap 的长度,会去尝试 allocate 内存了,是尝试去当前容量两倍的内存,所以操作是非常昂贵的。。其实这个也不是很大的问题,因为 append 结束底层的数组就会被 GC 回收,但是如果有另外的 Slice 引用这个底层数组,就容易出问题。

1
2
3
4
a := make([]int, 32)
b := a[1:16]
a = append(a, 1)
a[2] = 42

Note:顺便说一下,append 只在 1024 以内通过加倍容量来增长分片,之后它将使用所谓的内存大小类来保证增长不超过~ 12.5%。为 32 字节的数组申请 64 字节是可以的,但是如果你的分片是 4GB,为增加一个元素再分配 4GB 是相当昂贵的,所以这是有道理的。

于是我们可以引出: 当你从一个非常大的数组里面尝试读取 3 个字符,会发现其实原始数据仍然是在内存里面

1
2
3
4
5
6
var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

笋干爆炸💥!!!

string 竟然是 slice?

在 Golang 中,string 只是一串只读的 byte slice,所以你可以直接操作它,但是你不能修改它。

1
2
3
4
5
6
7
8
9
func main() {
    const placeOfInterest = `中文`
    fmt.Printf("%v\n",len(placeOfInterest))
    
    for i := 0; i < len(placeOfInterest); i++ {
        fmt.Printf("%x ", placeOfInterest[i])
    }
    fmt.Printf("\n")
}

输出:

1
2
3
4
6
e4 b8 ad e6 96 87 

Program exited.

可以发现 string 的长度并不是 2,而是 6,因为 string 包含的是对应的 UTF-8 编码 (因为 Golang 代码是 UTF-8 编码的),同时 string 是一串 byte slice。因为中文每个字符在 unicode 中都对应一个码位,一个码位占用 3 个 byte,所以 string 的长度是 6。

总结

以上就是一些常见的 slice 的使用。

相关阅读

文章作者 xiantang

上次更新 2025-12-29 (4c152d04)

赞赏支持

微信打赏 支付宝打赏