silce的扩容,截取,使用规范总结
切片
什么是slice
Go中的切片,是我们经常用到的数据结构。有着比数组更灵活的用法,那么作者就去探究下什么是切片。
我们先来了解下切片的数据结构
type slice struct {
array unsafe.Pointer // 指针
len int // 长度
cap int // 容量
}
切片一共三个属性:指针,指向底层的数组;长度,表示切片可用元素的个数,也就是说使用下标
对元素进行访问的时候,下标不能超过的长度;容量,底层数组的元素个数,容量》=长度。

底层的数组是可以被多个切片同时指向的,因此对一个切片元素的操作可能会影响到其他的切片。
slice的创建使用
| 序号 | 方式 | 代码示例 |
|---|---|---|
| 1 | 直接声明 | var slice []int |
| 2 | new | slice := *new([]int) |
| 3 | 字面量 | slice := []int |
| 4 | make | slice := make([]int, 5, 10) |
| 5 | 从切片或数组截取 | 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) |
| 长度 | 0 | 0 |
| 容量 | 0 | 0 |
| 和nil比较 | true | false |
nil 切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil 切片。
- 字面量
直接初始化表达式进行创建
s1 := []int{0, 1, 2, 3,5,6}
- make
slice := make([]int, 5, 10) // 长度为5,容量为10
slice使用的一点规范
根据 Uber Go代码风格指南
nil 是一个有效的 slice
nil 是一个长度为 0 的 slice。意思是,
使用
nil来替代长度为 0 的 slice 返回Bad Good if x == "" {
return []int{}
}
if x == "" {
return nil
}
检查一个空 slice,应该使用
len(s) == 0,而不是nil。Bad Good func isEmpty(s []string) bool {
return s == nil
}
func isEmpty(s []string) bool {
return len(s) == 0
}
The zero value (a slice declared with
var) is usable immediately without
make().零值(通过
var声明的 slice)是立马可用的,并不需要make()。Bad Good nums := []int{}
// or, nums := make([]int) if add1 {
nums = append(nums, 1)
} if add2 {
nums = append(nums, 2)
}
var nums []int if add1 {
nums = append(nums, 1)
} if add2 {
nums = append(nums, 2)
}
slice和数组的区别
slice的底层是数组,slice是对数组的封装,它描述一个数组的片段。两者都可以通过下标访问单个元素。
数组是定长的,长度定义好,不能改变。在Go中数组是不常见的,因为长度是类型的一部分,限制了它的表达
能力,比如[3]int 和 [4]int 就是不同的类型。
切片可以动态的扩容,非常灵活。切片的类型和长度没有关系。
slice的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函数后不使用返回值。
append(slice, elem1, elem2)
append(slice, anotherSlice...)
上面是不能编译通过的
使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。
这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。
新slice预留buffer大小是有一定规律的。
// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if et.size == 0 {
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
// append should not create a slice with nil pointer but non-zero len.
// We assume that append doesn't need to preserve old.array in this case.
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
const ptrSize = unsafe.Sizeof((*byte)(nil))
switch et.size {
case 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > _MaxMem
newcap = int(capmem)
case ptrSize:
lenmem = uintptr(old.len) * ptrSize
newlenmem = uintptr(cap) * ptrSize
capmem = roundupsize(uintptr(newcap) * ptrSize)
overflow = uintptr(newcap) > _MaxMem/ptrSize
newcap = int(capmem / ptrSize)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
overflow = uintptr(newcap) > maxSliceCap(et.size)
newcap = int(capmem / et.size)
}
// The check of overflow (uintptr(newcap) > maxSliceCap(et.size))
// in addition to capmem > _MaxMem is needed to prevent an overflow
// which can be used to trigger a segfault on 32bit architectures
// with this example program:
//
// type T [1<<27 + 1]int64
//
// var d T
// var s []T
//
// func main() {
// s = append(s, d, d, d, d)
// print(len(s), "\n")
// }
if cap < old.cap || overflow || capmem > _MaxMem {
panic(errorString("growslice: cap out of range"))
}
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
p = mallocgc(capmem, nil, false)
memmove(p, old.array, lenmem)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if !writeBarrier.enabled {
memmove(p, old.array, lenmem)
} else {
for i := uintptr(0); i < lenmem; i += et.size {
typedmemmove(et, add(p, i), add(old.array, i))
}
}
}
return slice{p, old.len, newcap}
}
其中这一段是重点的代码,我们可以看到
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
复制Slice和Map注意事项
slice 和 map 包含指向底层数据的指针,因此复制的时候需要当心。
slice 和 map 包含指向底层数据的指针,因此复制的时候需要当心。
接收 Slice 和 Map 作为入参
需要留意的是,如果你保存了作为参数接收的 map 或 slice 的引用,可以通过引用修改它。
| Bad | Good |
|---|---|
|
|
返回 Slice 和 Map
类似的,当心 map 或者 slice 暴露的内部状态是可以被修改的。
| Bad | Good |
|---|---|
|
|
切片的截取
基于已有 slice 创建新 slice 对象,被称为 reslice。新 slice 和老 slice 共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。基于数组创建的新 slice 对象也是同样的效果:对数组或 slice 元素作的更改都会影响到彼此。
如果新截取的切片发生了扩容了,就会重新申请新的内存空间,这样就新截取的切片就不指向原来的地址空间了。
截取的例子:
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := data[2:4:6] // data[low, high, max]
max >= high >= low
或者
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := data[2:4] // data[low, high]
这两个low, high对应的都是前闭后开。
区别就是如果不设置max,那么新切片的长度cap就是原切片的长度len(data)-low
设置了max那么新切片的长度cap就是原切片的长度max-low
package main
import "fmt"
func main() {
s1 := []int{2, 3, 6, 2, 4, 5, 6, 7}
fmt.Println(cap(s1), len(s1))
s2 := s1[2:3:4]
fmt.Println("带上max")
fmt.Println(s2)
fmt.Println(cap(s2), len(s2))
fmt.Println("不带max")
s3 := s1[2:3]
fmt.Println(s3)
fmt.Println(cap(s3), len(s3))
}
输出
8 8
带上max
[6]
2 1
不带max
[6]
6 1
不发生扩容情况下修改新切片
截取了新的切片,当不发生扩容的情况下,操作新的切片是会对老切片的数据产生影响,因为他们指向的是通一地址空间。
package main
import "fmt"
func main() {
s1 := []int{2, 3, 6, 2, 4, 5, 6, 7}
fmt.Println("原切片")
fmt.Println("cap", cap(s1), "len", len(s1))
s2 := s1[6:7]
fmt.Println("新切片")
fmt.Println("cap", cap(s2), "len", len(s2))
fmt.Println(s2)
s2 = append(s2, 100)
fmt.Println("append之后的新切片")
fmt.Println("cap", cap(s2), "len", len(s2))
fmt.Println(cap(s2), len(s2))
fmt.Println(s2)
fmt.Println("老切片")
fmt.Println(s1)
}
打印下结果
原切片
cap 8 len 8
新切片
cap 2 len 1
[6]
append之后的新切片
cap 2 len 2
2 2
[6 100]
老切片
[2 3 6 2 4 5 6 100]
发生扩容情况下修改新的切片
截取的新的切片发生扩容的情况下,新切片将指向一个新的数据空间。
package main
import "fmt"
func main() {
s1 := []int{2, 3, 6, 2, 4, 5, 6, 7}
fmt.Println("原切片")
fmt.Println("cap", cap(s1), "len", len(s1))
s2 := s1[6:7]
fmt.Println("新切片")
fmt.Println("cap", cap(s2), "len", len(s2))
fmt.Println(s2)
s2 = append(s2, 100)
fmt.Println("append之后的新切片")
fmt.Println("cap", cap(s2), "len", len(s2))
fmt.Println(cap(s2), len(s2))
fmt.Println(s2)
fmt.Println("老切片")
fmt.Println(s1)
s2 = append(s2, 888)
fmt.Println("append之后的新切片,发生扩容")
fmt.Println("cap", cap(s2), "len", len(s2))
fmt.Println(cap(s2), len(s2))
fmt.Println(s2)
fmt.Println("老切片")
fmt.Println(s1)
}
打印下结果
cap 8 len 8
新切片
cap 2 len 1
[6]
append之后的新切片
cap 2 len 2
2 2
[6 100]
老切片
[2 3 6 2 4 5 6 100]
append之后的新切片,发生扩容
cap 4 len 3
4 3
[6 100 888]
老切片
[2 3 6 2 4 5 6 100]
总结
- 切片是对底层数组的一个抽象,描述了它的一个片段。
- 切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。
- 多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。
- append 函数会在切片容量不够的情况下,调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。
- 扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量 >= 原容量的 2 倍或 1.25 倍。
- 当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。
参考
- 【快速理解Go数组和切片的内部实现原理】 https://i6448038.github.io/2018/08/11/array-and-slice-principle/
- 【GO代码风格指南 Uber Go】 https://github.com/uber-go/guide
- 【深度解密Go语言之Slice】 https://mp.weixin.qq.com/s/MTZ0C9zYsNrb8wyIm2D8BA
silce的扩容,截取,使用规范总结的更多相关文章
- Java面试题大全(javaSe,HTML,CSS,js,Spring框架等)
目录 1. Java基础部分 7 1.一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 7 2.Java有没有goto? 7 3.说说&和& ...
- JAVA笔试题(全解)
目录 一. Java基础部分................................................................. 9 1.一个".java& ...
- 谈谈对hibernate的理解
它是ORM思想的一个实现,对JDBC进行了很好的封装,它通过配置使JavaBean对象和数据库表之间进行映射,并提供对增.删.改.查便利的操作方法,同时支持事务处理,它对数据库记录还提供了缓存机制,提 ...
- hibernate面试点
1.谈谈你对hibernate的认识和理解 01.全自动的ORM框架 02.子项目 03.面向对象的思想来解决操作数据库 01.hibernate是一个开放源代码的对象关系映射(ORM)框架,它对JD ...
- Ruby 读书
输出: print printf 既定格式输出 puts 自动换行 p 显示对象 sprintf 不规则字符串 pp 需要导入库 putc(字母) 转移字符和单双引号 include Math或者直 ...
- 8.5-7 mkfs、dumpe2fs、resize2fs
8.5 mkfs:创建Linux文件系统 mkfs命令用于在指定的设备(或硬盘分区等)上创建格式化并创建文件系统,fdisk和parted等分区工具相当于建房的人,把房子(硬盘),分成几居室( ...
- 阿里巴巴Java开发手册正确学习姿势是怎样的?刷新代码规范认知
很多人都知道,阿里巴巴在2017发布了<阿里巴巴Java开发手册>,前后推出了很多个版本,并在后续推出了与之配套的IDEA插件和书籍. 相信很多Java开发都或多或少看过这份手册,这份手册 ...
- Axure原型制作规范
一. 名词定义: Sitemap 导航图 Widgets 组件 Master 库 Label 控件名 Interactions 交互动作 Annotations 注释 Location and siz ...
- JavaScript语法规范
推荐的JavaScript编码规范 阅读 247 评论 0 喜欢 0 作为前端开发人员,我相信每一个人都或多或少的用到原生的JavaScript,也正是因为用的人多,导致编码风格也是多种多样的,而不规 ...
- JavaScript编码规范指南
前言 本文摘自Google JavaScript编码规范指南,截取了其中比较容易理解与遵循的点作为团队的JavaScript编码规范. JavaScript 语言规范 变量 声明变量必须加上 var ...
随机推荐
- GitLab--安装部署
配置信息 系统:centos7.8 gitlab版本:12.8.8 1 下载gitlab wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum ...
- WCF 使用动态代理精简代码架构 (WCF动态调用)
使用Castle.Core.dll实现,核心代码是使用Castle.DynamicProxy.ProxyGenerator类的CreateInterfaceProxyWithoutTarget方法动态 ...
- freeswitch xml_rpc模块
概述 freeswitch有非常多的周边模块,给我们提供各种各样的功能,有些功能在适当的场景下可以极大的方便我们的开发和应用. 今天我们介绍一个不常用的模块mod_xml_rpc. freeswitc ...
- Gradle 出现 Could not resolve gradle
Gradle 在进行 sync 的时候会出现 Caused by: org.gradle.internal.resolve.ModuleVersionResolveException: Could n ...
- 【java】 向上转型的运用
应用 :求面积 1,抽象类 Geometry . public abstract class Geometry { public abstract double getArea(); } 2,矩形 ...
- Linux-远程连接-ssh
- [转帖]oracle导出千万级数据为csv格式
当数据量小时(20万行内),plsqldev.sqlplus的spool都能比较方便进行csv导出,但是当数据量到百万千万级,这两个方法非常慢而且可能中途客户端就崩溃,需要使用其他方法. 一. sql ...
- [转帖]一文深度讲解JVM 内存分析工具 MAT及实践(建议收藏)
https://juejin.cn/post/6911624328472133646 1. 前言 熟练掌握 MAT 是 Java 高手的必备能力,但实践时大家往往需面对众多功能,眼花缭乱不知如何下手, ...
- [转帖]Django10——从db.sqlite3迁移到MySQL
https://blog.csdn.net/weixin_47197906/article/details/124889477 文章目录 1.查看Django支持的数据库 2.修改数据库配置 1.查看 ...
- Numa以及其他内存参数等对Oracle的影响
Numa以及其他内存参数等对Oracle的影响 背景知识: Numa的理解 Numa 分一致性内存访问结构 主要是对应UMA 一致性内存访问而言的. 在最初一个服务器只有一个CPU的场景下, 都是UM ...