网站更新和维护怎么做,pc网站同步手机网站,合肥网络推广培训学校,做网站代理工作安全吗目录
概述
一、什么是slice
二、slice的声明
三、slice的初始化、创建
make方式创建
创建一个包含指定长度的切片
创建一个指定长度和容量的切片
创建一个空切片
创建一个长度和容量都为 0 的切片
new方式创建
短声明初始化切片
通过一个数组来创建切片
声明一个 …目录
概述
一、什么是slice
二、slice的声明
三、slice的初始化、创建
make方式创建
创建一个包含指定长度的切片
创建一个指定长度和容量的切片
创建一个空切片
创建一个长度和容量都为 0 的切片
new方式创建
短声明初始化切片
通过一个数组来创建切片
声明一个 nil 切片
四、nil切片和空切片的区别
空切片Empty Slice
nil 切片nil Slice
共同点
注意事项和易错点
五、常见导致panic的slice操作
索引越界
切片越界
小结
六、slice的相关操作
slice底层数组指针的获取方式
方式1、unsafe.SliceData
方式2、%p
方式3、reflect
slice的长度
slice的容量
slice的遍历
基于索引的遍历
使用range实现遍历
slice的截取
slice的追加
内置append函数
append不扩容的情况
append扩容的情况
append的返回值很重要
slice的拷贝
浅拷贝
深拷贝
内置copy函数
内置copy函数实现深拷贝
内置append函数实现深拷贝
七、slice在函数调用时是header传递
总结 概述 本文主要是对slice的总结从slice的基本概念、创建方式、常用操作自己经历过的坑点易错点以及append的机制slice的header传递尽可能用最简单的代码说明问题本文的代码是基于golang 1.22.1版本验证。
一、什么是slice 切片是用来描述底层数组的连续片段的数据结构本身并不保存数组数据而只是保存数组连续片段的描述信息通过结构体里的array段指针引用底层数组长度和容量属性限制底层数组的读写片段。
type slice struct { array unsafe.Pointer len int cap int
}
描述切片的数据结构由指向数组的指针、长度和容量三部分组成 array 指针指向通过slice访问底层数组的的的第一个元素的指针这里的被指位置不一定是数组的第一个元素在对slice进行切片操作时会移动这个指针。len 长度代表切片当前包含的元素数量len的大小不能超过cap的大小。cap 容量表示底层数组从切片开始位置到数组末尾的元素个数,即当前切片的容量cap总是大于或者等于len。 为什么指针的类型是 unsafe.Pointer 呢 这是因为在底层实现中Go 语言的切片并不直接指向底层数组的数据而是通过指针间接引用。unsafe.Pointer 类型是 Go 语言中用于处理底层指针的一种特殊类型它可以指向任意类型的数据包括未导出的结构体和数组。这种设计的目的是为了支持切片的动态扩展和收缩。当切片需要扩容时底层的数组可能会被重新分配内存新的数组可能位于内存的不同位置。使用指针可以更灵活地引用底层数组而不受具体类型的限制。需要注意的是由于 unsafe之所以叫unsafe包是因为这里面的大部分操作绕开里golang的data type system的约束直接操作内存单元是比较危险容易引起程序运行安全的操作unsafe.Pointer 类型可以指向任意类型的数据因此在使用时需要特别小心避免造成内存访问越界或者类型不匹配的问题。通常情况下开发者应该尽量避免直接使用 unsafe.Pointer除非是在必要的情况下进行底层操作。 二、slice的声明
var slicename []T //slicevar slicename []T //slice
其中slicename是切片的变量名T 是切片中元素的类型 ,注意中括号里并没有数值n。
区分数组的声明
var arrayname [n]T //array
三、slice的初始化、创建
make方式创建
golang提供了内置的函数make创建
func make([]T, len, cap) []T T 表示切片中元素的类型。len 表示切片的长度即切片中包含的元素个数。cap 表示切片的容量即底层数组的长度,这个参数可选 make函数调用后它其实分配了一个T类型的数组 并返回一个slice指向该数组
创建一个包含指定长度的切片 当cap参数未指定时cap的值与len的值相同
s : make([]int, 5) // 创建一个包含 5 个整数的切片初始值为对应类型的零值
//[0 0 0 0 0] cap(s) 5 len(s) 5
创建一个指定长度和容量的切片
s : make([]int, 5, 10) // 创建一个包含 5 个整数的切片并且底层数组的长度为 10
//[0 0 0 0 0] cap(s) 10 len(s) 5
创建一个空切片
s :make([]int, 0) // 创建一个空切片长度为 0
//[] cap(s) 0 len(s) 0
创建一个长度和容量都为 0 的切片
s : make([]int, 0, 0) // 创建一个长度和容量都为 0 的切片
//[] cap(s) 0 len(s) 0
new方式创建 golang的内置函数new是用来分配内存并返回指向该类型的零值的指针而slice本身是一个包含指针、长度和容量的复合类型使用new来创建slice不是很合适这种方式还会涉及到unsafe包的使用它绕过了Go的类型安全并且需要手动管理内存这很容易引发错误和内存泄漏。因此在实际开发中最好避免使用这种方法而是使用make函数来创建slice对于new不展开介绍
短声明初始化切片
s : []int{1, 2, 3, 4, 5} //[1 2 3 4 5] cap(s) 5 len(s) 5
通过一个数组来创建切片
arr : [5]int{1, 2, 3, 4, 5}
s : arr[:]
//[1 2 3 4 5] cap(s) 5 len(s) 5s1 : arr[:]
s2 : arr[0:]
s3 : arr[:5]
// 以上 s s1 s2 s3都是相等的 都是[1 2 3 4 5]s4 : arr[:0]
//而s4是创建一个空的slice
声明一个 nil 切片
var s []int // 这是一个 nil 切片 //[] cap(s) 0 len(s) 0
四、nil切片和空切片的区别 在 Go 中空切片empty slice和 nil 切片nil slice是两种不同的概念
它们有着共同点和某些不同的含义和用途
空切片Empty Slice 空切片是一个长度为 0 的切片但其底层数组已经被分配了。可以通过 make 函数创建一个空切片也可以通过切片字面量 []T{} 创建。空切片可以被使用可以进行追加元素、遍历等操作但不会引发 panic因为底层数组已经被分配。通常用于表示一个空的集合或者没有元素的情况。encoding/json编码时空切片会被编码为 JSON 数组 []表示一个空的数组。当你有一个空的切片时你期望它在 JSON 中被表示为一个空数组这与空的集合或序列的语义一致。 示例
// 使用 make 函数创建空切片
emptySlice : make([]int, 0)// 使用切片字面量创建空切片
emptySlice2 : []int{}fmt.Println(emptySlice , emptySlice, address of underlying array:, unsafe.SliceData(emptySlice), \nlen, len(emptySlice), \ncap, cap(emptySlice))
fmt.Println(emptySlice2 , emptySlice2, address of underlying array:, unsafe.SliceData(emptySlice2), \nlen, len(emptySlice2), \ncap, cap(emptySlice2)) 运行结果 nil 切片nil Slice nil 切片是一个指向 nil 的切片引用即没有指向任何底层数组。一个 nil 切片的长度和容量都是 0并且它的指针为 nil。可以将一个切片赋值为 nil或者声明一个没有初始化的切片它们都会被视为 nil 切片。encoding/json编码时nil 切片会被编码为 JSON 的 null 值表示不存在的值。当你有一个 nil 切片时JSON 编码将把它解释为一个缺失值这在某些情况下可能会引起问题特别是在期望一个数组而不是 null 值的情况下 示例
// 声明一个未初始化的切片会被初始化为 nil 切片
var nilSlice []int
fmt.Println(nilSlice , nilSlice, address of underlying array:, unsafe.SliceData(nilSlice), \nlen, len(nilSlice), \ncap, cap(nilSlice))
运行结果 共同点 nil切片和空切片在进行如下操作len(), cap(), append(), and for .. range loop时的行为和结果都是一致,都不会引发panic都表示空的集合或序列有的地方说对nil进行这些操作会引发panic应该是跟go的版本不同但是对nil slice的索引还是会抛出panic的多一嘴对nil map进行append操作会引发panic
注意事项和易错点 使用空切片和 nil 切片的场景不同需要根据实际情况进行选择。如果需要表示一个空的集合或者没有元素的情况应该使用空切片如果需要表示一个未初始化或者未指定的切片可以使用 nil 切片。在函数返回值中通常使用 nil 切片来表示某些特定条件下的空值。当需要传递一个切片作为函数参数并且可能为空时建议使用 nil 切片而不是空切片以便明确表明该切片是未初始化的。在判断切片是否为空时应该使用 len(slice) 0 来判断而不是 slice nil因为后者只能用于判断切片是否为 nil。 对于 JSON 编码需要注意的是:当你希望表示一个空的切片时使用空切片 [];当你希望表示一个不存在的切片时使用nil切片nil。 五、常见导致panic的slice操作
索引越界 当访问slice的元素时如果使用的索引超出了slice的有效范围即小于0或大于等于slice的长度,程序会触发运行时panic常见的是因为忽略slice或者array的起始索引是0最后一个是len(slice)-1导致的。
这种panic提示一般是这样的 panic: runtime error: index out of range ** with length ** fruits : []string{apple, orange, grape}
fmt.Println(my favorite fruit is:, fruits[len(fruits)])
// panic: runtime error: index out of range [3] with length 3
注意索引index是小于等于len(slice)-1而不是容量cap(slice)-1
numbers : make([]int, 3, 5)
numbers[3] 5
//panic: runtime error: index out of range [3] with length 3
切片越界 在通过切片操作slice[low:high]创建新的slice时如果low或high超出了原slice的界限也会触发panic。
这样panic提示一般是这样的 panic: runtime error: slice bounds out of range ** slice : []int{1, 2, 3}
newSlice : slice[4:]
// low超出原slice的界限会触发panic
小结 在某些情况下对slice的底层数组进行不安全的操作比如直接修改slice的结构体字段或使用unsafe包进行底层操作可能会导致slice处于不一致的状态进而引发panic。这种情况较为罕见通常发生在底层编程或处理复杂数据结构的场景中Go语言本身对slice操作有严格的类型检查和边界检查因此大多数常见的错误在编译时就能被捕获。然而由于运行时动态分配内存和扩展slice的特性上述提到的几种情况仍可能导致运行时panic。为了避免这些问题需要仔细检查slice的索引和切片操作确保它们始终在有效范围内。
六、slice的相关操作
slice底层数组指针的获取方式 为了调试方面、以及更好地说明slice的操作机制有时候需要看到slice底层数组的位置
这里有三种方式可提供
方式1、unsafe.SliceData
golang的标准库src\unsafe\unsafe.go里提供了一个很好的获取slice底层数组地址的函数 s0 : make([]int, 2, 3)
fmt.Println(\n\n, s0, \n address :, unsafe.SliceData(s0), \n len:, len(s0), \n cap:, cap(s0))
输出结果 方式2、%p slice是个包含底层数组指针len,cap的值是个值和其他变量一样获取slice的地址可以用同时%p在fmt文档中介绍也有着特殊的作用参考fmt的the documentation可以看到 %p可以打印slice底层数组的第一个元素的地址也就是slice底层数组的地址
示例
s0 : make([]int, 2, 3)fmt.Printf(the address of the 0th element of the slice s0 :%p\n, s0)
输出结果 方式3、reflect
示例
s0 : make([]int, 2, 3)fmt.Printf(underlying array of slice addr%#v\n, *((*reflect.SliceHeader)(unsafe.Pointer(s0))))
输出结果 slice的长度
内置的len函数
示例
s0 : []int{10, 20, 30, 40, 50}
fmt.Println(length of s0:, len(s0))
运行结果 slice的容量
内置的cap函数
示例
s0 : []int{10, 20, 30, 40, 50}
fmt.Println(capcity of s0:, cap(s0))
运行结果 slice的遍历
基于索引的遍历
s0 : []int{10, 20, 30, 40, 50}for i : 0; i len(s0); i {fmt.Println(i, s0[i])
}使用range实现遍历
s0 : []int{10, 20, 30, 40, 50}for index, value : range s0 {fmt.Println(index, value)
}
slice的截取 如图通过对slice的截取运算并不会重新分配内存片段而是通过指针的移动实现新旧切片指向底层数组的指针仍然指向同一个内存片段
示例
var ptr *int // 声明一个int类型的指针
fmt.Printf(\n pointer occupy size: %d bytes\n, unsafe.Sizeof(ptr)) s0 : []int{10, 20, 30, 40, 50}
fmt.Println(\n\n, s0, \n address :, unsafe.SliceData(s0), \n len:, len(s0), \n cap:, cap(s0)) s1 : s0[2:4]
fmt.Println(\n\n, s1, \n address :, unsafe.SliceData(s1), \n len:, len(s1), \n cap:, cap(s1))
运行结果 slice的追加
内置append函数
func append(slice []Type, elems ...Type) []Type slice 是Type类型的目标切片elems 是要追加到切片末尾的元素也是Type类型append() 函数返回一个新的切片依然是Type类型其中包含了原始切片的所有元素以及追加的元素 append不扩容的情况
示例
s0 : make([]int, 3, 5)fmt.Println(before append:s0 slice:, s0, address of of the underlying array:, unsafe.SliceData(s0), len(s0):, len(s0), cap(s0):, cap(s0))s0 append(s0, 1, 2)fmt.Println(after append:s0 slice:, s0, address of of the underlying array:, unsafe.SliceData(s0), len(s0):, len(s0), cap(s0):, cap(s0))
运行结果 追加前后underlying array没有变化还是指向同一个底层数组
append扩容的情况
示例
s0 : make([]int, 3, 5)fmt.Println(before append:s0 slice:, s0, address of of the underlying array:, unsafe.SliceData(s0), len(s0), len(s0), cap(s0), cap(s0))s0 append(s0, 1, 2, 3)fmt.Println(after append:s0 slice:, s0, address of of the underlying array:, unsafe.SliceData(s0), len(s0), len(s0), cap(s0), cap(s0))
运行结果 append的返回值很重要 一个slice传递给append函数append会接受这个slice的一个副本然后append内部对这个副本操作注意这个副本包含了跟原始slice实参同一个底层数组的引用 如果不扩容append返回这个副本的值依然指向同原始slice实参同一个底层数组容量没变如果只是更改值长度不变如果是追加元素不扩容则长度会变化容量不变如果需要扩容append函数内部会先算出一个合理的新的容量值新建一个这个容量值的数组然后将老元素拷贝到这个数组里再将新元素追加到这个数组的后面然后将计算后新的长度和新的容量作为新slice的len和cap然后将这个新数组的地址作为slice的指针段然后返回新的slice之前的副本的slice里的指针被GC收回注意如果append内部在需要扩容的情况下不返回这个新的sliceappend的操作可能就被阉割隐匿了是无效的如果返回了需要注意的是这个新的切片是否重新赋值给原始的切片变量如果赋值给一个新的切片变量值则是两个变量如下面的情况 slice : make([]init,3,5)slice append(slice,1,2,3)
//返回给调用者的同一个变量sliceNew : append(slice,1,2,3)
//返回给新的切片变量//上面两种情况是两个不同的切片 append扩容依据及实现步骤 检测切片容量切片的扩容与不扩容的依据主要取决于当前切片的长度len和容量cap以及要追加的元素数量当向切片追加元素时如果追加后的长度len(slice) 追加元素数量超过了当前容量cap(slice)切片就会发生扩容如果追加后的长度不超过当前容量切片就不会发生扩容而是直接将新元素添加到切片的末尾然后更新切片长度。如果容量不够 如果切片的容量不足以容纳新元素append 将会执行扩容操作选择新容量Go 将根据一定的策略选择一个新的容量。一般来说新容量会比当前容量大并且通常会选择一个相对较大的值以减少后续的扩容次数现有的选择新容量的依据是原 slice 的容量小于 1024则新 slice 的容量将扩大为原来的 2 倍如果原 slice 的容量大于 1024则新的 slice 的容量将扩大为原来的 1.25 倍但不同版本可能会有不同的扩容策略具体可参考src\runtime\slice.go里的growslice函数分配新内存一旦确定了新的容量Go 将分配一个新的数组其长度为新容量然后将当前切片的所有元素复制到新的数组中将新元素追加到新数组的末尾。更新切片最后Go 更新原始切片的指向新数组的指针并更新切片的长度返回更新后的切片。 注 由于底层数组的更改append 操作可能会导致原始切片的引用失效因此通常情况下我们会将结果分配给原始切片变量以确保更新后的切片引用得到正确的处理。 slice append(slice,10) 直接调用slice append(slice,10)的时候也许会记得将结果返回给原始变量但是append在某个如change函数内部调用而调用change函数时往往忘记返回而造成错误如 func addItemToSlice(s []int) {s append(s, 10)
}slice的拷贝
浅拷贝 简单地将一个切片赋值给另一个变量并不会创建一个新的、独立的副本。相反两个变量引用同一个底层数组这意味着一个切片中的更改会反映在另一个切片中
如果只是将src slice新赋值给一个新的dst变量并不会创建一个新的独立的副本尽管两个变量都有这个各自的slice header但是这两个值保存的内容是一样的所以两个slice的array pointer是一样的指向了一个底层数组这种方式修改一个 slice 的元素会影响到另一个 slice。 示例
src : []int{10, 20, 30, 40, 50}
dst : srcfmt.Println(src slice:, src, address of of the underlying array:, unsafe.SliceData(src), len(src):, len(src), cap(src):, cap(src))
fmt.Println(dst slice:, dst, address of of the underlying array:, unsafe.SliceData(dst), len(dst):, len(dst), cap(dst):, cap(dst)
运行结果 深拷贝 深拷贝后源切片和目的切片的指针部分指向了独立的两个underlying array修改其中一个切片不会对另一个切片产生影响 内置copy函数 func copy(dst, src []Type) int dst 是目标 slice src 是源 slice Type 是 slice 中元素的类型。 int是返回的实际复制的元素 这个函数copy多少元素取决于源切片和目的切片长度的较小的值min(len(dst), len(src))
如果目标切片为空或nil则不会进行任何复制需要注意的反而是src为nil的情况。
示例
var src []int
dst : make([]int, len(src))copy(dst, src)
fmt.Println(src nil, dst nil) // true falsefmt.Println(src slice:, src, address of of the underlying array:, unsafe.SliceData(src), len(src):, len(src), cap(src):, cap(src))
fmt.Println(dst slice:, dst, address of of the underlying array:, unsafe.SliceData(dst), len(dst):, len(dst), cap(dst):, cap(dst))
运行结果 想实现srcnil的时候拷贝后dstnil
则可以加个判断
var src []int
dst : make([]int, len(src))if src nil {dst nil
} else {copy(dst, src)
}
fmt.Println(src nil, dst nil) // true false
内置copy函数实现深拷贝 我们想用内置copy函数实现深拷贝的话需要新建目的切片的时候要用dst : make([]int, len(src))这种方式。
示例
src : []int{10, 20, 30, 40, 50}
dst : make([]int, len(src))copy(dst, src)fmt.Println(src slice:, src, address of of the underlying array:, unsafe.SliceData(src), len(src):, len(src), cap(src):, cap(src))
fmt.Println(dst slice:, dst, address of of the underlying array:, unsafe.SliceData(dst), len(dst):, len(dst), cap(dst):, cap(dst))
运行结果 内置append函数实现深拷贝
示例
src : []int{10, 20, 30, 40, 50}
dst : append([]int{}, src...)fmt.Println(src slice:, src, address of of the underlying array:, unsafe.SliceData(src), len(src):, len(src), cap(src):, cap(src))
fmt.Println(dst slice:, dst, address of of the underlying array:, unsafe.SliceData(dst), len(dst):, len(dst), cap(dst):, cap(dst))
}运行结果 七、slice在函数调用时是header传递 我们平时在开发中遇到的值传递和引用传递的特点是 值传递的特点是当使用值传递时函数会接收参数的副本而不是参数本身意味着函数内部对参数的修改不会影响到原始实参的值引用传递的特点是函数会接收参数的引用即内存地址而不是参数的副本意味着在函数内部对参数的修改会影响到原始数据 严格意义上将golang的函数传递是值传递参数在传递给函数时会复制一份副本传递给函数只不过有的是普通的变量的副本有的是包含了指针的变量的副本而golang语言内部的、自动的、隐式的、引用或者解引用的对调用者不可见往往给调用者造成困扰。
在我们给slice的函数传递方式定义为值传递或者引用传递之前不妨先看两个示例
change示例
func change(s []int) {s[0] 1fmt.Println(in change , s)
}func main() {s0 : []int{10, 20, 30, 40, 50}fmt.Println(before change , s0)change(s0)fmt.Println(after chang , s0)}
运行结果 上述的change示例似乎slice是引用传递的结果然后再看一个示例
addItemToSlice示例
func addItemToSlice(s []int) {s append(s, 10)}func main() {s0 : []int{10, 20, 30, 40, 50}fmt.Println(before change , s0)addItemToSlice(s0)fmt.Println(after chang , s0)}
运行结果 上述addItemToSlice示例似乎是值传递的结果
分析
slice的传递机制是这样的前面说过slice是一个特殊的结构体
type slice struct {array unsafe.Pointerlen intcap int
} change函数接收了slice s0的一个副本这个change内部对副本里指向的底层数组里元素更改然后长度和容量没变化函数返回副本回收原始实参s0的底层数组与副本指向是一样的所以更改有效了。 addItemToSlice函数接收了slice s0的一个副本这个addItemToSlice内部调用了append函数并append函数对s0进行了扩容,新建了一个更大容量的slice,但是这个append并没有将这个新的slice f返回而传递给addItemToSlice的slice副本在函数调用完也就被GC回收了所以更改是无效的。具体解释也可以参考本文章的append返回值很重要这一节对append函数的解释
总结 slice的函数传递时究竟是值传递还是引用传递都是受限制从语义上很容易误导不能只从语义上简单归类于是值传递或者引用传递否则会造成值传递不更改Slice引用传递更改slice的错误结论关键的是append函数是否将新的slice作为返回值返回给调用者并且注意是否将这个新的切片返回给原来的切片变量还是返回给了一个新的切片变量。 规避这种误导的最好方式slice在函数传递时是header传递或者是slice header传递在channel的传递时亦是如此。