1. 特点
slice并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。
- 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
- 切片的长度可以改变,因此,切片是一个可变数组。
- 切片的遍历方式和数组一样,可以用len()求长度。表示可用元素的数量,读写操作不能超过该限制。
- cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array)。其中array是slice引用的数组。
- 切片的定义:var 变量名 []类型,比如:var str []string var arr []int。
- 如果slice等于nil,那么len,cap结果都等于0。
2. 切片源码
切片对应的数据结构定义位于runtime/slice.go
文件中,其定义如下:
type slice struct {array unsafe.Pointerlen intcap int
}
- array:指针,指向引用数组对应位置。
- len:可用元素个数。
- cap:容量,可放元素个数。
我们在进行切片赋值,传参,截断时,其实是复制一个slice结构体,只不过底层数组是同一个。这就导致了无论是在复制的切片中修改值,还是修改形参切片值,都会修改到原来的切片和引用的数组。
总的来说:切边底层有一个数组(make创建的切片,底层也会先创建一个数组),切片是数组的引用。
3. 创建切片
4. 切片初始化
package mainimport "fmt"//全局变量
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[2:8]
var slice1 []int = arr[0:6] //可以简写为arr[:6]
var slice2 []int = arr[5:10] //可以简写为arr[5:]
var slice3 []int = arr[0:len(arr)] //可以简写为arr[:]
var slice4 []int = arr[0 : len(arr)-1] //去掉最后一个元素func main() {fmt.Printf("全局变量arr %v\n", arr)fmt.Printf("全局变量slice0 %v\n", slice0)fmt.Printf("全局变量slice1 %v\n", slice1)fmt.Printf("全局变量slice2 %v\n", slice2)fmt.Printf("全局变量slice3 %v\n", slice3)fmt.Printf("全局变量slice4 %v\n", slice4)fmt.Println("--------------------------")arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}slice5 := arr2[2:8]slice6 := arr2[0:6] //可以简写为arr[:6]slice7 := arr2[5:10] //可以简写为arr[5:]slice8 := arr2[0:len(arr)] //可以简写为arr[:]slice9 := arr2[:len(arr)-1] //去掉最后一个元素fmt.Printf("局部变量arr %v\n", arr2)fmt.Printf("局部变量slice5 %v\n", slice5)fmt.Printf("局部变量slice6 %v\n", slice6)fmt.Printf("局部变量slice7 %v\n", slice7)fmt.Printf("局部变量slice8 %v\n", slice8)fmt.Printf("局部变量slice9 %v\n", slice9)
}
5. 通过make来创建切片
var slice []type = make([]type, len)
var slice []type = make([]type, len, cap)
slice := make([]type, len)
slice := make([]type, len, cap)
- 切片的内存布局
- 读写操作实际目标是底层数组,只需要注意索引号的差别。
package mainimport "fmt"func main() {data := [...]int{0, 1, 2, 3, 4, 5}s := data[2:4]s[0] += 100s[1] += 200fmt.Println(s)fmt.Println(data)
}
- 可直接创建slice对象,自动分配底层数组
package mainimport "fmt"func main() {var s1 []int = []int{0, 1, 2, 3, 8: 100} //通过初始化表达式构造,可使用索引号fmt.Println(s1, len(s1), cap(s1))s2 := make([]int, 6, 8) //使用make创建,指定len和capfmt.Println(s2, len(s2), cap(s2))s3 := make([]int, 6) //使用make创建,省略cap,相当于cap=lenfmt.Println(s3, len(s3), cap(s3))
}
- 使用make动态创建slice,避免了数组必须用常量的麻烦。还可以用指针直接访问底层数组,退化成普通数组操作。
package mainimport "fmt"func main() {s := []int{1, 2, 3, 4}p := &s[1] //获得底层数组元素指针*p += 100fmt.Println(s)
}
- 至于[][]T,是指元素类型为[]T
package mainimport "fmt"func main() {data := [][]int{[]int{1, 2, 3},[]int{10, 20, 30, 40},[]int{100, 200},}fmt.Println(data)
}
- 可以直接修改struct array/slice成员
package mainimport "fmt"func main() {data := [5]struct {x int}{}s := data[:]s[1].x = 10s[2].x = 20fmt.Println(data)fmt.Printf("%p, %p\n", &data, &data[0])
}
6. 用append内置函数操作切片(切片追加)
append:向slice尾部添加数据,返回新的slice对象。
package mainimport ("fmt"
)func main() {var s []int //s是nil,没有分配内存fmt.Println(s)s = append(s, 1) //append会先为s分配内存,再存放数据fmt.Println(s)var a []int = []int{1, 2, 3}fmt.Printf("slice a: %v\n", a)b := []int{4, 5, 6}fmt.Printf("slice b: %v\n", b)//append向slice尾部添加数据c := append(a, b...)fmt.Printf("slice c: %v\n", c)d := append(a, 10)fmt.Printf("slice d: %v\n", d)e := append(a, 100, 200, 300)fmt.Printf("slice e: %v\n", e)//append返回的新的slice对象fmt.Printf("a:%p, c:%p, d:%p, e:%p", &a, &c, &d, &e)
}
8. slice扩容
- 超出元slice.cap限制,就会重新分配底层数组,即便原数组并未填满。
从下面现象可以看出:
- 当数据没有超出slice的cap时,如果引用存在的数组变量(不是make创建的),底层数组使用的是存在的数组变量。修改切片的值,就会修改数组的值。
- 当数据超出slice的cap时,会重新分配底层数组,修改切片的值,不会修改存在数组变量的值,而是修改切片底层数组的值。
package mainimport "fmt"func main() {var data = [...]int{1, 2, 3, 4, 10: 0}s := data[:2:3]fmt.Printf("data:%p, data[0]:%p\n", &data, &data[0]) //data:0xc0000103f0, data[0]:0xc0000103f0//slice中数据保存的是数组对应下标的地址,使用索引访问,相当于对指针解引用,在取地址,实际取的是数组中的地址fmt.Printf("s:%p, s[0]:%p\n", &s, &s[0]) //s:0xc000008048, s[0]:0xc0000103f0//未扩容前修改值s[0] = 11fmt.Println(s, data)s = append(s, 100, 200)fmt.Println(s, data)fmt.Printf("s[0]:%p, data[0]:%p\n", &s[0], &data[0])//扩容后修改值s[0] = 10fmt.Println(s, data)
}
- slice中cap重新分配规律
从下面输出可以看出,slice扩容通常是以两倍原来容量重新分配底层数组。
建议一次性分配足够大的空间,以减少内存分配和数据复制的开销,或者初始化足够长的len属性,改用索引号进行操作。
及时释放不再使用的slice对象,避免持有过期数组,造成GC无法回收。
package mainimport "fmt"func main() {s := make([]int, 0, 2)c := cap(s)for i := 0; i < 50; i++ {s = append(s, i)if c < cap(s) {fmt.Printf("cap: %d->%d\n", c, cap(s))c = cap(s)}}
}
9. 切片拷贝
函数copy在两个slice间复制数据,复制长度以len小的为准。两个slice可指向同一底层数组,允许元素区间重叠(本来创建切片不同的slice也可以指向同一个底层数组)。
应及时将所需数据copy到较小的slice,以便释放超大号底层数组内存。
10. slice遍历
slice遍历实际和数组遍历一样。
package mainimport "fmt"func main() {data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}s1 := data[:]for index, value := range s1 {fmt.Printf("s1[%d] = %d\n", index, value)}fmt.Println("-----------------------------")for i := 0; i < len(s1); i++ {fmt.Printf("s1[%d] = %d\n", i, s1[i])}
}
11. 切片调整大小
由于底层数组相同,且数组空间连续。所以可以像下面赋值。
12. 数组和切片的内存分布
底层数组不一定要声明出来,比如:make的切片,或者 s := []int{1, 2, 3}
13. 字符串和切片
string底层就是一个byte的数组,因此,也可以进行切片操作。
package mainimport "fmt"func main() {str := "hello world"s := str[:5]fmt.Println(s)s1 := str[6:]fmt.Println(s1)
}
- 修改字符串
string本身是不可修改的,要修改string,需要先转成[]byte或[]rune,进行修改,再转成string。
英文字符串
package mainimport "fmt"func main() {str := "hello world"//字符串不能修改// s1 := str[:8]// s1[6] = 'G'// str[6] = 'G'//转成[]bytebt := []byte(str) //中文字符需要用[]rune(str)//修改[]bytebt[6] = 'G'bt = bt[:8]bt = append(bt, '!')str = string(bt)fmt.Println(str)
}
含中文字符串
package mainimport "fmt"func main() {str := "你好 世界 hello world"//字符串不能修改// s1 := str[:8]// s1[6] = 'G'// str[6] = 'G'rn := []rune(str)//修改[]bytern[3] = '够'rn[4] = '浪'rn = rn[:12]rn = append(rn, '好')//再转回字符串str = string(rn)fmt.Println(str)
}
- 数组或切片转字符串
strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), "", ",", -1)
golang slice data[:6:8]两个冒号的理解:
对于data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
常规slice,data[6:8],从data数组索引6到索引7,长度len为2,最大可扩充长度cap为4(slice底层数组为data,指针指向索引6位置,cap从索引6位置到data最后,即6-9,所以cap为4)。
另一种写法:data[:6:8]每一个数字前面都有冒号,slice内容为data索引从0到6,长度len为6,最大扩充项cap设置为8。
a[x:y:z]切片内容[x:y](前闭后闭),切片长度:y-x,切片容量:z-x
package mainimport "fmt"func main() {data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}s1 := data[6:8]fmt.Println(len(s1), cap(s1))//s2 := data[6: :8] 中间必须要有数字s2 := data[:6:8]fmt.Println(len(s2), cap(s2))fmt.Println(s2)
}