slice 是什么
slice 类似于 Java 中的 ArrayList,是一个可以自动扩容的"数组",可以进行添加、删除、修改、查询、遍历且有自动扩容的功能,而且 slice 不是并发安全的,
本文基于 Go 1.14,如果过程中过有疑问、建议等等,欢迎在评论区或者公众号给我留言,我们一起交流学习,码字不易,希望能该你带来帮助
文中有些许汇编知识,可以优先学习 《 go 语言高级编程》--汇编章节,找不到的可以到我公众号(在文末)留言「go语言高级编程」拿到
slice 的内存结构以及寻找思路
思路
func main() {
a := make([]int, 0)
a = append(a, 1)
fmt.Println(a)
}
通过 go tool compile -S -l -N main.go
,找到底层执行的函数
"".main STEXT size=328 args=0x0 locals=0x98
...
0x0042 00066 (main.go:6) CALL runtime.makeslice(SB)
...
0x007c 00124 (main.go:7) CALL runtime.growslice(SB)
...
具体代码如下
// 这里只是表达了 slice 底部的数组地址
func makeslice(et *_type, len, cap int) unsafe.Pointer {
...
}
// 这里反映出 slice 的结构
func growslice(et *_type, old slice, cap int) slice {
...
return slice{p, old.len, newcap}
}
思路主要是创建函数的”返回“以及操作过程中函数的 “传参” 或者 “返回“,由此我们可以看到具体的结构如下
内存结构
type slice struct {
array unsafe.Pointer // 底层数组的地址
len int // slice 的 len
cap int // slice 的 cap
}
问题1: var a []int
和 []int{}
有什么区别
func main() {
var a []int
fmt.Println(a == nil) //true
b := []int{}
fmt.Println(b == nil) //false
}
首先我们来看切片的创建方式
创建方式 | 代码例子 |
---|---|
直接声明 | var a []int |
make | a := make([]int, len) a := make([]int, len, cap) |
字面表示 | a := []int{} |
截取 | a = a[i:j] a = a[i:j:k] |
new(感谢老钱的文章,文末有原地址) | a := *new([]int) |
直接声明
func main() {
var a []int
fmt.Println(a) //[]
fmt.Println(len(a)) //0
fmt.Println(cap(a)) //0
fmt.Println(a == nil) //true
}
make
方式1:make([]int, len)
func main() {
a := make([]int, 1) // 如果 make 只有2个参数,则 len 和 cap 都第2个参数的值
fmt.Println(a) // [0]
fmt.Println(len(a)) // 1
fmt.Println(cap(a)) // 1
}
方式2:make([]int, len, cap)
func main() {
a := make([]int, 1, 2) // 如果 make 有 3 个参数,则则 len 为第 2 个参数的值,cap 为第 3 个参数的值
fmt.Println(a) // [0]
fmt.Println(len(a)) // 1
fmt.Println(cap(a)) // 2
}
方式2 我们通过 go tool compile -S -l -N main.go
我们可以知道如下的情况
"".main STEXT size=557 args=0x0 locals=0xe0
...
0x002f 00047 (main.go:6) PCDATA $0, $1
0x002f 00047 (main.go:6) PCDATA $1, $0
0x002f 00047 (main.go:6) LEAQ type.int(SB), AX
0x0036 00054 (main.go:6) PCDATA $0, $0
0x0036 00054 (main.go:6) MOVQ AX, (SP)
0x003a 00058 (main.go:6) MOVQ $1, 8(SP)
0x0043 00067 (main.go:6) MOVQ $2, 16(SP)
0x004c 00076 (main.go:6) CALL runtime.makeslice(SB) // 通过这里创建 []int
0x0051 00081 (main.go:6) PCDATA $0, $1
0x0051 00081 (main.go:6) MOVQ 24(SP), AX
0x0056 00086 (main.go:6) PCDATA $1, $1
0x0056 00086 (main.go:6) MOVQ AX, "".a+120(SP)
0x005b 00091 (main.go:6) MOVQ $1, "".a+128(SP)
0x0067 00103 (main.go:6) MOVQ $2, "".a+136(SP)
...
我们通过查询 makeslice
找到对应的编译逻辑
// 位置在于 /cmd/compile/internal/gc/typecheck.go
func typecheck1(n *Node, top int) (res *Node) {
...
ok := 0
switch n.Op {
...
case OMAKE: // 这里是校验 make 语法中
ok |= ctxExpr
args := n.List.Slice() // make([]int, 1) \ make([]int, 1, 2),即 args 长度为 2 或者 3,第一是[]int ,第二个是 len ,第三个是 cap
if len(args) == 0 {
yyerror("missing argument to make")
n.Type = nil
return n
}
n.List.Set(nil)
l := args[0]
l = typecheck(l, ctxType) // 这里 l 就是 []int
t := l.Type
if t == nil {
n.Type = nil
return n
}
i := 1
switch t.Etype {
...
case TSLICE:
if i >= len(args) {
yyerror("missing len argument to make(%v)", t)
n.Type = nil
return n
}
l = args[i] //len
i++ //2
l = typecheck(l, ctxExpr)
var r *Node
if i < len(args) { // 如果 make 中有3个参数,则 r 为 cap,否则为nil
r = args[i]
i++
r = typecheck(r, ctxExpr)
}
... // 校验
if Isconst(l, CTINT) && r != nil && Isconst(r, CTINT) && l.Val().U.(*Mpint).Cmp(r.Val().U.(*Mpint)) > 0 { // 如果 len > cap 则报错
yyerror("len larger than cap in make(%v)", t)
n.Type = nil
return n
}
n.Left = l
n.Right = r
/*
在syntax.go中,有 const 为
const (
...
OMAKESLICE // make(Type, Left, Right) (type is slice)
...
)
正好印证了 n.left 代表的是第二个参数,n.Right 代表的是第三个参数
*/
n.Op = OMAKESLICE
...
}
...
}
...
}
接下来我们跳转到 OMAKESLICE的阶段
// 位置在于 cmd/compile/internal/gc/walk.go
func walkexpr(n *Node, init *Nodes) *Node {
opswitch:
switch n.Op {
...
case OMAKESLICE:
l := n.Left // len
r := n.Right // cap
if r == nil { // 加入没有 cap 这个参数,则与 len 一致
r = safeexpr(l, init)
l = r
}
t := n.Type
if n.Esc == EscNone { // EscNone 与之前 map 一样,表示不逃逸到堆上
// 这里整体看下来,就是如果不逃逸到堆上,其底层是一个数组,然后进行截取 cap ,具体可以细看代码
...
} else {
...
len, cap := l, r
fnname := "makeslice64"
argtype := types.Types[TINT64]
if (len.Type.IsKind(TIDEAL) || maxintval[len.Type.Etype].Cmp(maxintval[TUINT]) <= 0) &&
(cap.Type.IsKind(TIDEAL) || maxintval[cap.Type.Etype].Cmp(maxintval[TUINT]) <= 0) {
fnname = "makeslice"
argtype = types.Types[TINT]
}
...
fn := syslook(fnname) // 这里可以看到,这里执行 makeslice64 / makeslice
...
}
...
}
...
}
因此创建总共有两个,如下
func makeslice(typ *byte, len int, cap int) unsafe.Pointer
func makeslice64(typ *byte, len int64, cap int64) unsafe.Pointer
makeslice64 函数中,本质也是执行 makeslice
func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer {
// len 和 cap 的校验
len := int(len64)
if int64(len) != len64 {
panicmakeslicelen()
}
cap := int(cap64)
if int64(cap) != cap64 {
panicmakeslicecap()
}
return makeslice(et, len, cap)
}
// 这里我们看到返回一个指针 (unsafe.Pointer),这里表示底层数组的指针,即 slice 的 array
// 要分别判断 len 和 cap 是否内存溢出或者超过可分配的容量
func makeslice(et *_type, len, cap int) unsafe.Pointer {
//由于 mallocgc 的 size 为 0 ,则会返回 zerobase 的指针,即 mem 为 0 的时候,因此
//即当 et.size 为0 或者是 cap 为 0 即可
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 { // 当 size 为 0,则返回指向 zerobase 的指针
return unsafe.Pointer(&zerobase)
}
...
}
对于 mallocgc 中 size 为 0的情况,可以看以下代码
func main() {
e := make([]int, 0)
fmt.Println(e == nil) //false
e1 := *(*[3]int)(unsafe.Pointer(&e))
fmt.Println(e1[0]) // 824634227792,表示 array
fmt.Println(e1[1]) // 0,表示 len
fmt.Println(e1[2]) // 0,表示 cap
fmt.Println("---------------------")
// ** 这个只是来证明一下
f := make([]struct{}, 10)
fmt.Println(f == nil) //false
f1 := *(*[3]int)(unsafe.Pointer(&f))
fmt.Println(f1[0]) // 824634227792,表示 array
fmt.Println(f1[1]) // 10,表示 len
fmt.Println(f1[2]) // 10,表示 cap
}
上面代码中, e 由于 cap 为 0(不理解的可以看上文),以及 f 中 _type 的 size 为 0,因此里面的 array 的值是相同的,都是 zerobase 的地址
通过 *[3]int 强转的方式后续会在后文讲解思路
字面表示
func main() {
a := []int{1, 2, 3, 5: 100}
fmt.Println(a) // [1,2,3,0,0,100]
fmt.Println(len(a)) // 6
fmt.Println(cap(a)) // 6
b := []int{}
fmt.Println(b) // []
fmt.Println(len(b)) // 0
fmt.Println(cap(b)) // 0
fmt.Println(b == nil) // false
}
这里可以看到,我们可以直接索引值来初始化
截取
选择截取数组或slice的一部分,注意截取的范围不能超过底层数组的索引范围
方式1 : arr[i:j]
func main() {
a := make([]int, 5, 10)
for i := 0; i < len(a); i++ {
a[i] = i
}
i := 1
j := 3
b := a[i:j] // 长度索引范围是 [1:3),但是 cap 是 cap(a) - i
fmt.Println(b) // [1,2]
fmt.Println(len(b)) // 2,这里表示 b 的长度,即 j-i
fmt.Println(cap(b)) // 9,这里表示 b 的容量,即 cap(a) - i
}
方式2 : arr[i:j:k]
func main() {
a := make([]int, 5, 10)
for i := 0; i < len(a); i++ {
a[i] = i
}
i := 1
j := 3
k := 5
b := a[i:j:k] // 长度索引范围是 [1:3),但是 cap 是 k-i
fmt.Println(b) // [1,2]
fmt.Println(len(b)) // 2,这里表示 b 的长度,即 j-i
fmt.Println(cap(b)) // 4,这里表示 b 的容量,即 k - i
}
以下对于不能超过底层数组的边界有一个图解
func main() {
a := make([]int, 5, 10)
fmt.Println(a) // [0,0,0,0,0]
fmt.Println(len(a)) // 5
fmt.Println(cap(a)) // 10
b := a[2:7] // 这里已经超过了 a 的 len
fmt.Println(b) // [0,0,0,0,0]
fmt.Println(len(b)) // 5
fmt.Println(cap(b)) // 8
// 以下超过了选择范围超过了 a 的 cap 的范围
//c := a[3:11]
//fmt.Println(c)
d := a[1:2:5]
fmt.Println(d) // [0]
fmt.Println(len(d)) // 1
fmt.Println(cap(d)) // 4
//e := a[4:7:11]
//fmt.Println(e)
//fmt.Println(len(e))
//fmt.Println(cap(e))
f := b[2:8] // 这里也超过了 b 的 len
fmt.Println(f) // [0,0,0,0,0,0]
fmt.Println(len(f)) // 6
fmt.Println(cap(f)) // 6
//g := b[2:9]
//fmt.Println(g)
//fmt.Println(len(g))
//fmt.Println(cap(g))
}
具体细节如下图
注意其中的 f 和 g ,是从 b 切片取出来的,但从 f 和 g 来看,我们知道这里的边界不是按照 b ,还是按照底层的 a 的 len 和 cap 来判断
new
func main() {
a := *new([]int)
fmt.Println(a) // []
fmt.Println(len(a)) // 0
fmt.Println(cap(a)) // 0
}
空切片 和 nil切片 的概念
以上4个方式在日常使用上并没有什么不同,不过具体底层的数据是不同的,这里再次感谢老钱(原文在文末),引申出「空切片」和 「nil 切片」这两个概念,看下图
这里先来一个延伸,在代码中执行 fmt.Println(slice) 的时候,汇编中会调用 runtime.convTslice ,如下
func convTslice(val []byte) (x unsafe.Pointer) {
if (*slice)(unsafe.Pointer(&val)).array == nil { // 标识
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(unsafe.Sizeof(val), sliceType, true)
*(*[]byte)(x) = val
}
return
}
在上面代码中,我们可以将一个 slice对象 对应其本质也是 []type 来转化(「标识」 部分),下面代码可以看到
nil 指针如下
func main() {
var a []int
fmt.Println(a == nil) //true
// 这里不用 []int 是因为已知 slice 结构只有3个字段
//a1 := *(*[]int)(unsafe.Pointer(&a))
a1 := *(*[3]int)(unsafe.Pointer(&a))
fmt.Println(a1[0]) // 0,表示 array
fmt.Println(a1[1]) // 0,表示 len
fmt.Println(a1[2]) // 0,表示 cap
fmt.Println("---------------------")
b := *new([]int)
fmt.Println(b == nil) //true
b1 := *(*[3]int)(unsafe.Pointer(&b))
fmt.Println(b1[0]) // 0,表示 array
fmt.Println(b1[1]) // 0,表示 len
fmt.Println(b1[2]) // 0,表示 cap
}
我们可以看到,a 和 b 对应的 array 字段的值都是 0
空指针如下
func main() {
c := []int{}
fmt.Println(c == nil) //false
c1 := *(*[3]int)(unsafe.Pointer(&c))
fmt.Println(c1[0]) // 824634227792,表示 array
fmt.Println(c1[1]) // 0,表示 len
fmt.Println(c1[2]) // 0,表示 cap
fmt.Println("---------------------")
e := make([]int, 0)
fmt.Println(e == nil) //false
e1 := *(*[3]int)(unsafe.Pointer(&e))
fmt.Println(e1[0]) // 824634227792,表示 array
fmt.Println(e1[1]) // 0,表示 len
fmt.Println(e1[2]) // 0,表示 cap
}
可以看到 c、e 底层对应的 array 为固定值,从上文的「创建方式- make」可以知道为 zerobase 的地址
总计如下
创建方式 | 切片种类 |
---|---|
直接声明 | nil 切片 |
make | 空切片 |
new | nil 切片 |
字面表示([]int{}) | 空切片 |
要如何选择呢
情况1:
判断json格式的时候,会产生如下的情况,看场景的需要
type Out struct {
Tmp []int //这里要保证是大写开头
}
func main() {
s := Out{}
var a []int
s.Tmp = a
byt, _ := json.Marshal(s)
fmt.Println(string(byt)) //{"Tmp":null}
b := make([]int, 0)
s.Tmp = b
byt, _ = json.Marshal(s)
fmt.Println(string(byt)) //{"Tmp":[]}
}
情况2: 在其他日常使用的时候,官方推荐选择 nil 切片,可以看如下链接查看详情 github.com/golang/go/w…
问题2: 子切片 append 是否影响父切片
场景1: b 进行了 a 的切片 ,然后再 append 的时候,为啥会影响了 a 呢
func main() {
a := make([]int, 1, 10)
fmt.Println("before:", a) // before : [0]
add(a)
fmt.Println("after:", a) // after : [10] // 这里进行了变更
}
func add(src []int) {
src = append(src, 1)
src[0] = 10
fmt.Println("add:", src) //do : [10,1]
}
场景2:按 「场景1」 中的描述,使用 append 会影响老的部分,此时为啥又没有影响,原因是什么呢?
func main() {
a := make([]int, 1)
fmt.Println("before:", a) // before : [0]
add(a)
fmt.Println("after:", a) // after : [0] // 这里原来的没有变更
}
func add(src []int) {
src = append(src, 1)
src[0] = 10
fmt.Println("add:", src) //do : [10,1]
}
我们看下文
append 使用方式
通过 append 给 slice 尾部进行添加,如果底层数组的空间不够会重新生产一个 slice ,然后将数据复制过去(这里后续会进行讲解)
方式1:append(slice, elem)
func main() {
a := make([]int, 1)
fmt.Println(a) // [0]
fmt.Println(len(a)) // 1
fmt.Println(cap(a)) // 1
a = append(a, 2)
fmt.Println(a) // [0,2]
fmt.Println(len(a)) // 2
fmt.Println(cap(a)) // 2
}
方式2: append(slice, slice2...)
func main() {
a := []int{1, 2, 3, 4, 5, 6}
var b []int
//虽然方式2中有3个参数,但是 append 之后,我们还是只要其 len,而不是 cap
b = append(b, a[1:3:5]...) //
fmt.Println(b) // [2,3]
fmt.Println(len(b)) // 2
fmt.Println(cap(b)) // 2
}
问题解析
场景1如下图,虽然 b 来添加,但是 a 和 b 的底层数组是一致的,因此 b 的操作影响了 a 的数据
那场景2 为啥又不会造成影响呢,我们将 add 函数用汇编语言来看下
"".add STEXT size=440 args=0x18 locals=0x98
...
0x002f 00047 (main.go:13) MOVQ "".src+160(SP), DX // DX 表示 src 的 array
0x0037 00055 (main.go:13) MOVQ "".src+168(SP), BX // BX 表示 src 的 len
0x003f 00063 (main.go:13) PCDATA $1, $1
0x003f 00063 (main.go:13) MOVQ "".src+176(SP), SI // SI 表示 src 的 cap
0x0047 00071 (main.go:13) LEAQ 1(BX), DI // Di=BX+1,即 len+1
0x004b 00075 (main.go:13) CMPQ DI, SI // 这就是将 len+1 和cap 对比
0x004e 00078 (main.go:13) JLS 85 // 如果 len+1 不高于 cap,则跳到 85
0x0050 00080 (main.go:13) JMP 349 // 这里跳转到 349
...
0x015d 00349 (main.go:13) PCDATA $0, $1
0x015d 00349 (main.go:13) MOVQ BX, ""..autotmp_5+64(SP)
0x0162 00354 (main.go:13) PCDATA $0, $5
0x0162 00354 (main.go:13) LEAQ type.int(SB), AX // 获取 int 类型的地址
0x0169 00361 (main.go:13) PCDATA $0, $1
0x0169 00361 (main.go:13) MOVQ AX, (SP) // 表示 slice 的 type
0x016d 00365 (main.go:13) PCDATA $0, $0
0x016d 00365 (main.go:13) MOVQ DX, 8(SP) // 表示 old slice 的 array
0x0172 00370 (main.go:13) MOVQ BX, 16(SP) // 表示 old slice 的 len
0x0177 00375 (main.go:13) MOVQ SI, 24(SP) // 表示 old slice 的 cap
0x017c 00380 (main.go:13) MOVQ DI, 32(SP) // 表示传参对应的 cap
0x0181 00385 (main.go:13) CALL runtime.growslice(SB) // 这里表示新建一个 slice
0x0186 00390 (main.go:13) PCDATA $0, $1
0x0186 00390 (main.go:13) MOVQ 40(SP), DX // 这里表示 new slice 的 array
0x018b 00395 (main.go:13) MOVQ 48(SP), AX // 这里表示 new slice 的 len
0x0190 00400 (main.go:13) MOVQ 56(SP), SI // 这里表示 new slice 的 cap
...
可以看到「汇编代码的第9行」,场景 2 中由于 len >= cap,所以会新建了一个 slice ,所以此时的 slice 的底层数组地址与原来的不一致,所以不会影响老的 slice 的底层数组,具体的 growslice
相关部分如下
func growslice(et *_type, old slice, cap int) slice {
...
var p unsafe.Pointer
// 执行到这里,说明已经拿到了新的地址
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
...
} else {
p = mallocgc(capmem, et, true)
...
}
memmove(p, old.array, lenmem) // 这里将老的数组数据来进行迁移
return slice{p, old.len, newcap} //然后返回新的 slice
}
那按以上的说法,如果没有新建一个slice ,那就会有影响了,以下代码来证明
func main() {
a := make([]int, 1, 2)
fmt.Println("before:", a) // before : [0]
add(a)
fmt.Println("after:", a) // after : [10] //这里原来的受到了影响
}
func add(src []int) {
src = append(src, 1)
src[0] = 10
fmt.Println("do:", src) // do : [10,1]
}
问题3:append 对于 nil 的切片是有什么特殊处理吗
如下代码,一般对于go中对于 nil 都是报 panic 的处理,那底层有什么神奇力量呢?
func main() {
var a []int
fmt.Println(a == nil) //true
a = append(a, 1)
fmt.Println(a) //[1]
}
问题解析
通过汇编 go tool compile -S -l -N main.go
得到,看到
"".main STEXT size=152 args=0x0 locals=0x60
...
0x0030 00048 (main.go:5) LEAQ type.int(SB), AX
0x0037 00055 (main.go:5) PCDATA $0, $0
0x0037 00055 (main.go:5) MOVQ AX, (SP) // 这里表示 *_type
0x003b 00059 (main.go:5) XORPS X0, X0
0x003e 00062 (main.go:5) MOVUPS X0, 8(SP) // 这里表示 old.array
0x0043 00067 (main.go:5) MOVQ $0, 24(SP) // 这里表示 old.cap
0x004c 00076 (main.go:5) MOVQ $1, 32(SP) // 这里表示传参数对应的cap,为1
0x0055 00085 (main.go:5) CALL runtime.growslice(SB) // 这里具体看上文
0x005a 00090 (main.go:5) PCDATA $0, $1
0x005a 00090 (main.go:5) MOVQ 40(SP), AX // 这里表示新生成的slice的array
0x005f 00095 (main.go:5) MOVQ 48(SP), CX // 这里表示新生成的 slice的 len
0x0064 00100 (main.go:5) MOVQ 56(SP), DX // 这里表示新生成的slice的cap
0x0069 00105 (main.go:5) INCQ CX // 这里表示新生成的 len + 1
0x006c 00108 (main.go:5) JMP 110
0x006e 00110 (main.go:5) MOVQ $1, (AX) // 这里表示将新生成的 array [0]的位置,设置为1
...
因此分别对应上 func growslice(et *_type, old slice, cap int) slice
的部分,得到内部会创建一个 cap 为 1 的切片,可以看上面的注释,关键点在「第9行」
问题4:切片扩容的规律是什么呢,每次乘以2?
如下代码,看到每次 append 一个数字后,cap 每次扩容都是乘以 2,真的是这样吗?
func main() {
a := make([]int, 0)
a = append(a, 1)
fmt.Println(len(a)) //1
fmt.Println(cap(a)) //1
a = append(a, 1)
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //2
a = append(a, 1)
fmt.Println(len(a)) //3
fmt.Println(cap(a)) //4
}
我们来整体看下 growslice 的源码
growslice 源码解析
func growslice(et *_type, old slice, cap int) slice {
...
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
// et.size 表示切片的类型大小
if et.size == 0 { // 类型为0,则表示 struct{}类型
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap // 老的 slice 的cap
doublecap := newcap + newcap //这里表示老的 slice 的 cap*2
if cap > doublecap {// 如果这次要求 cap 大于 oldcap*2,则最终的cap为所需要的cap
newcap = cap
} else {
if old.len < 1024 { // 如果 老slice 的len 小于1024,则最终的cap 为老cap*2
newcap = doublecap
} else {
for 0 < newcap && newcap < cap { // 如果newcap小于要求的cap,则 newcap 以 5/4倍增加,直到超过cap
newcap += newcap / 4
}
if newcap <= 0 {//如果这里newcap 的数值溢出,则最终cap为cap
newcap = cap
}
}
}
...
var p unsafe.Pointer
// 执行到这里,说明已经拿到了新的地址
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
...
} else {
p = mallocgc(capmem, et, true)
...
}
memmove(p, old.array, lenmem) // 这里将老的数组数据来进行迁移
return slice{p, old.len, newcap} //然后返回新的 slice
}
规律总结
当 老slice 的len 小于 1024 的时候
问题5:Golang 中 slice 和数组有什么区别呢?
代码写的比较草率,目的达到就行
func main() {
a := []int{0, 0}
fmt.Println(a) //[0,0]
fmt.Println(len(a)) // 0
fmt.Println(cap(a)) // 0
doSlice(a)
fmt.Println(a) //[1,0]
fmt.Println("-----")
b := [2]int{}
fmt.Println(b) //[0,0]
fmt.Println(len(b)) // 2
fmt.Println(cap(b)) // 2
doArray(b)
fmt.Println(b) //[0,0]
a = append(a, 1)
//b = append(b, 1) // 数组不能使用 append
}
func doArray(src [2]int) {
src[0] = 1
}
func doSlice(src []int) {
src[0] = 1
}
不同的点:
-
数组的 len 和 cap 是固定的且一致,而切片的 len <= cap ,且两者随着 append 而改变
-
数组不会影响原来的数据,slice 在不扩容的情况下,会影响原来的数据
-
数组不能使用 append 来填充,而 slice 可以
切片也可以通过 数组截取来获得,但注意不要超过数组的底层数组的索引范围
参考资料(不分先后)
《深度解析 Go 语言中「切片」的三种特殊状态 - 老钱大佬》
---------------------------- 这是分割线 ------------------------
以下是我的公众号,欢迎调戏