Go的切片:长度和容量
虽然说 Go 的语法在很大程度上和 PHP 很像,但 PHP 中却是没有“切片”这个概念的,在学习的过程中也遇到了一些困惑,遂做此笔记。
困惑1:使用 append 函数为切片追加元素后,切片的容量时变时不变,其扩容机制是什么?
困惑2:更改切片的元素会修改其底层数组中对应的元素。为什么有些情况下更改了切片元素,其底层数组元素没有更改?
一、切片的声明
切片可以看成是数组的引用。在 Go 中,每个数组的大小是固定的,不能随意改变大小,切片可以为数组提供动态增长和缩小的需求,但其本身并不存储任何数据。
/*
* 这是一个数组的声明
*/
var a [5]int //只指定长度,元素初始化为默认值0
var a [5]int{1,2,3,4,5}
/*
* 这是一个切片的声明:即声明一个没有长度的数组
*/
// 数组未创建
// 方法1:直接初始化
var s []int //声明一个长度和容量为 0 的 nil 切片
var s []int{1,2,3,4,5} // 同时创建一个长度为5的数组
// 方法2:用make()函数来创建切片:var 变量名 = make([]变量类型,长度,容量)
var s = make([]int, 0, 5)
// 数组已创建
// 切分数组:var 变量名 []变量类型 = arr[low, high],low和high为数组的索引。
var arr = [5]int{1,2,3,4,5}
var slice []int = arr[1:4] // [2,3,4]
二、切片的长度和容量
切片的长度是它所包含的元素个数。
切片的容量是从它的第一个元素到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s)
和 cap(s)
来获取。
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // [0 1 2 3 4 5 6 7 8 9] len=10,cap=10
s1 := s[0:5] // [0 1 2 3 4] len=5,cap=10
s2 := s[5:] // [5 6 7 8 9] len=5,cap=5
三、切片追加元素后长度和容量的变化
1.append 函数
Go 提供了内建的 append 函数,为切片追加新的元素。
func append(s []T, vs ...T) []T
append 的结果是一个包含原切片所有元素加上新添加元素的切片。
下面分两种情况描述了向切片追加新元素后切片长度和容量的变化。
Example 1:
package main
import "fmt"
func main() {
arr := [5]int{1,2,3,4,5} // [1 2 3 4 5]
fmt.Println(arr)
s1 := arr[0:3] // [1 2 3]
printSlice(s1)
s1 = append(s1, 6)
printSlice(s1)
fmt.Println(arr)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s)
}
执行结果如下:
[1 2 3 4 5]
len=3 cap=5 0xc000082030 [1 2 3]
len=4 cap=5 0xc000082030 [1 2 3 6]
[1 2 3 6 5]
可以看到切片在追加元素后,其容量和指针地址没有变化,但底层数组发生了变化,下标 3 对应的 4 变成了 6。
Example 2:
package main
import "fmt"
func main() {
arr := [5]int{1,2,3,4} // [1 2 3 4 0]
fmt.Println(arr)
s2 := arr[2:] // [3 4 0]
printSlice(s2)
s2 = append(s2, 5)
printSlice(s2)
fmt.Println(arr)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s)
}
执行结果如下:
[1 2 3 4 0]
len=3 cap=3 0xc00001c130 [3 4 0]
len=4 cap=6 0xc00001c180 [3 4 0 5]
[1 2 3 4 0]
而这个切片在追加元素后,其容量和指针地址发生了变化,但底层数组未变。
当切片的底层数组不足以容纳所有给定值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
2.切片的源代码学习
Go 中切片的数据结构可以在源码下的 src/runtime/slice.go
查看。
// go 1.3.16 src/runtime/slice.go:13
type slice struct {
array unsafe.Pointer
len int
cap int
}
可以看到,切片作为数组的引用,有三个属性字段:长度、容量和指向数组的指针。
向 slice 追加元素的时候,若容量不够,会调用 growslice 函数,
// go 1.3.16 src/runtime/slice.go:76
func growslice(et *_type, old slice, cap int) slice {
//...code
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
switch {
// ...code
}
// ...code...
// 将旧切片的数据搬到新切片开辟的地址中
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
从上面的源码,在对 slice 进行 append 等操作时,可能会造成 slice 的自动扩容。其扩容时的大小增长规则是:
- 如果切片的容量小于 1024,则扩容时其容量大小乘以2;一旦容量大小超过 1024,则增长因子变成 1.25,即每次增加原来容量的四分之一。
- 如果扩容之后,还没有触及原数组的容量,则切片中的指针指向的还是原数组,如果扩容后超过了原数组的容量,则开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
上面的两个例子中,切片的容量均小于 1024 个元素,所以扩容的时候增长因子为 2,每增加一个元素,其容量翻番。
Example2 中,因为切片的底层数组没有足够的可用容量,append() 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值,所以原数组没有变化,不是我想象中的[1 2 3 4 5],
3.切片扩容的内部实现
扩容1:切片扩容后其容量不变
slice := []int{1,2,3,4,5}
// 创建新的切片,其长度为 2 个元素,容量为 4 个元素
mySlice := slice[1:3]
// 使用原有的容量来分配一个新元素,将新元素赋值为 40
mySlice = append(mySlice, 40)
执行上面代码后的底层数据结构如下图所示:
扩容2:切片扩容后其容量变化
// 创建一个长度和容量都为 5 的切片
mySlice := []int{1,2,3,4,5}
// 向切片追加一个新元素,将新元素赋值为 6
mySlice = append(mySlice, 6)
执行上面代码后的底层数据结构如下图所示:
四、小结
- 切片是一个结构体,保存着切片的容量,长度以及指向数组的指针(数组的地址)。
- 尽量对切片设置初始容量值,以避免 append 调用 growslice,因为新的切片容量比旧的大,会开辟新的地址,拷贝数据,降低性能。
Go的切片:长度和容量的更多相关文章
- Go语言 切片长度和容量
package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) // Sl ...
- Go指南_切片的长度与容量
源地址 https://tour.go-zh.org/moretypes/11 一.描述 切片拥有 长度 和 容量. 切片的长度就是它所包含的元素个数. 切片的容量是从它的第一个元素开始数,到其底层数 ...
- Go语言数组和切片的原理
目录 数组 创建 访问和赋值 切片 结构 初始化 访问 追加 拷贝 总结 数组和切片是 Go 语言中常见的数据结构,很多刚刚使用 Go 的开发者往往会混淆这两个概念,数组作为最常见的集合在编程语言中是 ...
- [Golang学习笔记] 07 数组和切片
01-06回顾: Go语言开发环境配置, 常用源码文件写法, 程序实体(尤其是变量)及其相关各种概念和编程技巧: 类型推断,变量重声明,可重名变量,类型推断,类型转换,别名类型和潜在类型 数组: 数组 ...
- golang 切片小记
1 切片初始化 func printSlice(s []int) { fmt.Printf("len=%d cap=%d underlying array:%p, %v\n", l ...
- go内建容器-切片
1.基础定义 看到'切片'二字,满脸懵逼.切的啥?用的什么刀法切?得到的切片有什么特点?可以对切片进行什么操作? 先看怎么得到切片,也就是前两个问题.切片的底层是数组,所以切片切的是数组:切的时候采用 ...
- 换个语言学一下 Golang (6)——数组,切片和字典
在上面的章节里面,我们讲过Go内置的基本数据类型.现在我们来看一下Go内置的高级数据类型,数组,切片和字典. 数组(Array) 数组是一个具有相同数据类型的元素组成的固定长度的有序集合.比如下面的例 ...
- 学习Golang语言(6):类型--切片
学习Golang语言(1): Hello World 学习Golang语言(2): 变量 学习Golang语言(3):类型--布尔型和数值类型 学习Golang语言(4):类型--字符串 学习Gola ...
- 『GoLang』数组与切片
数组 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列(这是一种同构的数据结构):这种类型可以是任意的原始类型例如整型.字符串或者自定义类型. 数组长度必须是一个常量表达式,并且必须是一个非负 ...
随机推荐
- 一个vue管理系统的初步搭建总结
ps:目前这个项目只是有一个大致的框架,并没有做完 前期准备工作 前端构建工具:Visual Studio Code后端构建工具:IDEA数据库和服务器构建工具:WampServer (使用的是2.4 ...
- MyBatis整合Spring MVC(易百教程)
MyBatis是ibatis的升级版,作为hibernate的老对手,它是一个可以自定义SQL.存储过程和高级映射的持久层框架.与Hibernate 的主要区别就是 Mybatis 是半自动化的,而 ...
- MyBatis学习与使用(一)
写在前面—— 用 MyBatis 也做过几个项目了,但是一直没有很深入的去理解这个框架,最近决定从头开始学习和整理MyBatis. 之前开发的项目并不是我先创建的,等我介入的时候发现他们已经稍稍封装了 ...
- 基于ambassador实现K8S灰度发布
为什么需要灰度发布 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式.在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对 ...
- $POJ1015\ Jury\ Compromise\ Dp$/背包
洛谷传送门 $Sol$ 这是一道具有多个“体积维度”的$0/1$背包问题. 把$N$个候选人看做$N$个物品,那么每个物品有如下三种体积: 1.“人数”,每个候选人的“人数”都是$1$,最终要填满容积 ...
- win服务器管理工具,服务器vps管理
win系列服务器,vps桌面如何管理?用这个工具: IIS7远程桌面批量管理,同时管理上千台vps,服务器,3389远程端口.
- 洛谷P1147 连续自然数和 题解 枚举
题目链接:https://www.luogu.com.cn/problem/P1147 题目大意: 给你一个数 \(M\) ,求有多少对连续自然数对之和为 \(M\),输出这列连续自然数对的首项和末项 ...
- 组件的props属性和state状态
props属性: 我使用代码来说明React中props属性: // Profile.jsx import React from 'react' ; export default Class Prof ...
- Spring Security 实战干货: 简单的认识 OAuth2.0 协议
1.前言 欢迎阅读 Spring Security 实战干货 系列文章 .OAuth2.0 是近几年比较流行的授权机制,对于普通用户来说可能每天你都在用它,我们经常使用的第三方登录大都基于 OAuth ...
- 27.python中excel处理库openpyxl使用详解
openpyxl是一个第三方库,可以处理xlsx格式的Excel文件.pip install openpyxl安装. 读取Excel文件 需要导入相关函数 ? 1 2 3 from openpyxl ...