Go语言之重要数组类型切片(slice)make,append函数解读
作者:凯歌技术控团队
切片是一个动态数组,因为数组的长度是固定的,所以操作起来很不方便,比如一个names数组,我想增加一个学生姓名都没有办法,十分不灵活。
所以在开发中数组并不常用,切片类型才是大量使用的。
切片基本操作
切片的创建有两种方式:
- 从数组或者切片上切取获得
- 直接声明切片 : var name []Type // 不同于数组, []没有数字
切片语法:
arr [start : end] 或者 slice [start : end] // start: 开始索引 end:结束索引
切片特点:
- 左闭右开 [ )
- 取出的元素数量为:结束位置 - 开始位置;
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)]获取;
- 当缺省开始位置时,表示从连续区域开头到结束位置;当缺省结束位置时,表示从开始位置到整个连续区域末尾;两者同时缺省时,与切片本身等效;
var arr = [5]int{10, 11, 12, 13, 14} var s1 = arr[1:4] fmt.Println(s1, reflect.TypeOf(s1)) // [11 12 13] []int var s2 = arr[2:5] fmt.Println(s2, reflect.TypeOf(s2)) // [12 13 14] var s3 = s2[0:2] // [12 13]
值类型和引用类型
数据类型从存储方式分为两类:值类型和引用类型!
(1) 值类型
基本数据类型(int,float,bool,string)以及数组和struct都属于值类型。
特点:变量直接存储值,内存通常在栈中分配,栈在函数调用完会被释放。值类型变量声明后,不管是否已经赋值,编译器为其分配内存,此时该值存储于栈上。
var a int //int类型默认值为 0 var b string //string类型默认值为 nil空 var c bool //bool类型默认值为false var d [2]int //数组默认值为[0 0]
当使用等号=将一个变量的值赋给另一个变量时,如 j = i ,实际上是在内存中将 i 的值进行了拷贝,可以通过 &i 获取变量 i 的内存地址。此时如果修改某个变量的值,不会影响另一个。
// 整型赋值 var a =10 b := a b = 101 fmt.Printf("a:%v,a的内存地址是%p\n",a,&a) fmt.Printf("b:%v,b的内存地址是%p\n",b,&b) //数组赋值 var c =[3]int{1,2,3} d := c d[1] = 100 fmt.Printf("c:%v,c的内存地址是%p\n",c,&c) fmt.Printf("d:%v,d的内存地址是%p\n",d,&d)
(2) 引用类型
指针、slice,map,chan,interface等都是引用类型。
特点:变量通过存储一个地址来存储最终的值。内存通常在堆上分配,通过GC回收。
引用类型必须申请内存才可以使用,new()和make()是给引用类型申请内存空间。
切片原理
切片的构造根本是对一个具体数组通过切片起始指针,切片长度以及最大容量三个参数确定下来的
type Slice struct { Data uintptr // 指针,指向底层数组中切片指定的开始位置 Len int // 长度,即切片的长度 Cap int // 最大长度(容量),也就是切片开始位置到数组的最后位置的长度 }
var arr = [5]int{10, 11, 12, 13, 14} s1 := arr[0:3] // 对数组切片 s2 := arr[2:5] s3 := s2[0:2] // 对切片切片 fmt.Println(s1) // [10, 11, 12] fmt.Println(s2) // [12, 13, 14] fmt.Println(s3) // [12, 13] // 地址是连续的 fmt.Printf("%p\n", &arr) fmt.Printf("%p\n", &arr[0]) // 相差8个字节 fmt.Printf("%p\n", &arr[1]) fmt.Printf("%p\n", &arr[2]) fmt.Printf("%p\n", &arr[3]) fmt.Printf("%p\n", &arr[4]) // 每一个切片都有一块自己的空间地址,分别存储了对于数组的引用地址,长度和容量 fmt.Printf("%p\n", &s1) // s1自己的地址 fmt.Printf("%p\n", &s1[0]) fmt.Println(len(s1), cap(s1)) fmt.Printf("%p\n", &s2) // s2自己的地址 fmt.Printf("%p\n", &s2[0]) fmt.Println(len(s2), cap(s2)) fmt.Printf("%p\n", &s3) // s3自己的地址 fmt.Printf("%p\n", &s3[0]) fmt.Println(len(s3), cap(s3))
var a = [...]int{1, 2, 3, 4, 5, 6} a1 := a[0:3] a2 := a[0:5] a3 := a[1:5] a4 := a[1:] a5 := a[:] a6 := a3[1:2] fmt.Printf("a1的长度%d,容量%d\n", len(a1), cap(a1)) fmt.Printf("a2的长度%d,容量%d\n", len(a2), cap(a2)) fmt.Printf("a3的长度%d,容量%d\n", len(a3), cap(a3)) fmt.Printf("a4的长度%d,容量%d\n", len(a4), cap(a4)) fmt.Printf("a5的长度%d,容量%d\n", len(a5), cap(a5)) fmt.Printf("a6的长度%d,容量%d\n", len(a6), cap(a6))
除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:
var name []Type // []Type是切片类型的标识
其中 name 表示切片的变量名,Type 表示切片对应的元素类型。
var names = []string{"张三","李四","王五"} fmt.Println(names,reflect.TypeOf(names)) // [张三 李四 王五 赵六 孙七] []string
直接声明切片,会针对切片构建底层数组,然后切片形成对数组的引用
练习
s1 := []int{1, 2, 3} s2 := s1[1:] s2[1] = 4 fmt.Println(s1)
var a = []int{1, 2, 3} b := a a[0] = 100 fmt.Println(b)
make函数
变量的声明我们可以通过var关键字,然后就可以在程序中使用。当我们不指定变量的默认值时,这些变量的默认值是他们的零值,比如int类型的零值是0,string类型的零值是"",引用类型的零值是nil。
对于例子中的两种类型的声明,我们可以直接使用,对其进行赋值输出。但是如果我们换成引用类型呢?
// arr := []int{} var arr [] int // 如果是 var arr [2] int arr[0] = 1 fmt.Println(arr)
从这个提示中可以看出,对于引用类型的变量,我们不光要声明它,还要为它分配内容空间。
对于值类型的声明不需要,是因为已经默认帮我们分配好了。要分配内存,就引出来今天的make函数。
make也是用于chan、map以及切片的内存创建,而且它返回的类型就是这三个类型本身。
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make([]Type, size, cap)
其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。
示例如下:
a := make([]int, 2) b := make([]int, 2, 10) fmt.Println(a, b) fmt.Println(len(a), len(b)) fmt.Println(cap(a), cap(b))
使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
a := make([]int, 5) b := a[0:3] a[0] = 100 fmt.Println(a) fmt.Println(b)
append(重点)
上面我们已经讲过,切片作为一个动态数组是可以添加元素的,添加方式为内建方法append。
(1)append的基本用法
var emps = make([]string, 3, 5) emps[0] = "张三" emps[1] = "李四" emps[2] = "王五" fmt.Println(emps) emps2 := append(emps, "rain") fmt.Println(emps2) emps3 := append(emps2, "eric") fmt.Println(emps3) // 容量不够时发生二倍扩容 emps4 := append(emps3, "yuan") fmt.Println(emps4) // 此时底层数组已经发生变化
扩容机制
1、每次 append 操作都会检查 slice 是否有足够的容量,如果足够会直接在原始数组上追加元素并返回一个新的 slice,底层数组不变,但是这种情况非常危险,极度容易产生 bug!而若容量不够,会创建一个新的容量足够的底层数组,先将之前数组的元素复制过来,再将新元素追加到后面,然后返回新的 slice,底层数组改变而这里对新数组的进行扩容
2、扩容策略:如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
arr := [4]int{10, 20, 30, 40} s1 := arr[0:2] // [10, 20] s2 := s1 // // [10, 20] s3 := append(append(append(s1, 1), 2), 3) s1[0] = 1000 fmt.Println(s1) fmt.Println(s2) fmt.Println(s3) fmt.Println(arr)
(2)append的扩展用法
var a []int a = append(a, 1) // 追加1个元素 fmt.Println(a) a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式 fmt.Println(a) a = append(a, []int{1, 2, 3}...) // 追加一个切片, 切片需要解包 fmt.Println(a)
a = append(a, 1)返回切片又重新赋值a的目的是丢弃老数组
// 案例1 a := []int{11, 22, 33} fmt.Println(len(a), cap(a)) c := append(a, 44) a[0] = 100 fmt.Println(a) fmt.Println(c) // 案例2 a := make([]int, 3, 10) fmt.Println(a) b := append(a, 11, 22) fmt.Println(a) // 小心a等于多少? fmt.Println(b) a[0] = 100 fmt.Println(a) fmt.Println(b) // 案例3 l := make([]int, 5, 10) v1 := append(l, 1) fmt.Println(v1) fmt.Printf("%p\n", &v1) v2 := append(l, 2) fmt.Println(v2) fmt.Printf("%p\n", &v2) fmt.Println(v1)
切片的插入和删除
开头添加元素
var a = []int{1,2,3} a = append([]int{0}, a...) // 在开头添加1个元素 a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
任意位置插入元素
var a []int a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。
思考这样写可以不:
var a = []int{1,2,3,4} s1:=a[:2] s2:=a[2:] fmt.Println(append(append(s1,100,),s2...))
删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
// 从切片中删除元素 a := []int{30, 31, 32, 33, 34, 35, 36, 37} // 要删除索引为2的元素 a = append(a[:2], a[3:]...) fmt.Println(a) //[30 31 33 34 35 36 37]
要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]…)
a:=[...]int{1,2,3} b:=a[:] b =append(b[:1],b[2:]...) fmt.Println(a) fmt.Println(b)
切片元素排序
a:=[]int{10,2,3,100} sort.Ints(a) fmt.Println(a) // [2 3 10 100] b:=[]string{"melon","banana","caomei","apple"} sort.Strings(b) fmt.Println(b) // [apple banana caomei melon] c:=[]float64{3.14,5.25,1.12,4,78} sort.Float64s(c) fmt.Println(c) // [1.12 3.14 4 5.25 78] // 注意:如果是一个数组,需要先转成切片再排序 [:] sort.Sort(sort.Reverse(sort.IntSlice(a))) sort.Sort(sort.Reverse(sort.Float64Slice(c))) fmt.Println(a,c)
切片拷贝
var s1 = []int{1, 2, 3, 4, 5} var s2 = make([]int, len(s1)) copy(s2, s1) fmt.Println(s2) s3 := []int{4, 5} s4 := []int{6, 7, 8, 9} copy(s4, s3) fmt.Println(s4) //[4 5 3]
练习
func 第1个_指针变量() { //取址 var x int x = 10 //x是整型变量 fmt.Println(x) //=============== var p *int p = &x //取址,p是int类型的指针变量 fmt.Println(p) //取值,v=10 fmt.Println(*p) } func 第2个_new函数() { //new 和 make 是 Go 语言中用于内存分配的原语。简单来说,new 只分配内存,make 用于初始化 slice、map 和 channel。 //之前我们学习的基本数据类型声明之后是有一个默认零值的,但是指针类型呢? var p *int //var p *int = new(int) //new函数的作用是开辟了一块儿空间,否则*p = 10就报错 // fmt.Println(p) // <nil> // fmt.Println(*p) // 报错,并没有开辟空间地址 *p = 10 // 报错 fmt.Println(*p) } func 第3个_数组() { //数组其实是和字符串一样的序列类型,不同于字符串在内存中连续存储字符,数组用[]的语法将同一类型的多个值存储在一块连续内存中。 //数组是值类型,不存地址都是值类型 var arr [3]int fmt.Println(arr) fmt.Println(&arr[0]) fmt.Printf("%p\n", &arr[0]) fmt.Println(&arr[1]) fmt.Println(&arr[2]) } func 第4个_基于数组的切片() { //切片有自己的空间 //存放了3个值,起始地址,长度,容量, //为啥说切片是对数组的引用? //切片本身不存数据!! } func 第5个_make函数() { //make返回的还是引用类型的本身,而new返回的是指向类型的指针。 //new()返回的是指针 //make()返回的是引用类型本身,切片本身 var s = make([]int, 3, 5) fmt.Print(s) } func 第6个_append函数1() { var emps = make([]string, 3, 5) emps[0] = "张三" emps[1] = "李四" emps[2] = "王五" fmt.Println(emps) emps2 := append(emps, "rain") fmt.Println(emps2) //["张三","李四","王五","rain"] emps3 := append(emps2, "eric") fmt.Println(emps3) //["张三","李四","王五","rain","eric"] //容量不够时发生二倍扩容 emps4 := append(emps3, "yuan") fmt.Println(emps4) //此时底层数组已经发生变化,[张三 李四 王五 rain eric yuan] } func 第7个_append函数2() { var emps = make([]string, 3, 5) emps[0] = "张三" emps[1] = "李四" emps[2] = "王五" fmt.Println(emps) emps2 := append(emps, "rain") fmt.Println(emps2) emps3 := append(emps, "eric") fmt.Println(emps3) //[张三 李四 王五 eric] fmt.Println(emps2) //[张三 李四 王五 eric] } func 第8个_append函数3() { var s1 = []int{1, 2, 3} s1 = append(s1, 4) fmt.Println(s1) //fmt.Println(s2) }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。