值类型与引用类型
值类型:基本数据类型,比如int,float,bool,string,以及数组和struct这些基本类型,变量直接存储的是一个值,内存通常只在栈中分配,栈在函数调用完会被释放
引用类型:slice,map,chan。它的内部结构有地址以及一些其他值(如长度,容量),因此它不等于一个指针,可以通过取地址操作&获得该引用类型的地址。
切片
切片是拥有相同元素类型的可变长度的序列,支持自动扩容,而且是一个引用类型,它的内部结构有地址,长度和容量,一般用于快速操作一块数据集合。
切片的实质是一个引用类型指向了一个底层数组即一块连续的内存,并且支持扩容
切片的长度就是它元素的个数,切片的容量为切片指向的底层数组的第一个到最后一个元素(包括它们自身)的元素的数量
它包含了三个信息:==底层数组的指针、切片的长度(len)和切片的容量(cap)。==长度是能够访问的上限,容量是底层实际开辟的数组的上限
因为切片是引用类型所以不能直接比较,只能和nil
比较,nil
就是相当于空指针,没有指向任何底层数组,所以此时的len和cap都是0
长度和容量都为0的切片不一定为nil
,对声明后的切片赋空值,或者使用make建立长和容量都为0的切片都是分配了内存了,所以不为nil
切片后面加上…表示将切片里的元素拆开一个个放入使用,而不是直接传入整个切片。注意与函数的参数中…区分,函数中加…表示有多个参数。
- 切片的声明与基本信息
package main
import "fmt"
//切片是相同数据类型,自动扩容的引用类型,定义格式为 var 名称 []数据类型
func main() {
//定义切片
var s1 []int //不赋值则默认为空,等于nil,理解为空指针
var s2 []string
fmt.Println(s1, s2)
fmt.Println(s1 == nil)
//初始化
s1 = []int{1, 2, 3, 4, 5}
s2 = []string{"沙河", "清水河", "嘉陵江"}
//同样可以用简写方式声明切片
s := []int{1, 9, 9, 7}
fmt.Println(s1, s2, s)
fmt.Println(s1 == nil) //因为切片指向的区域有值了,所以不为空了
//长度和容量
//因为直接声明的方式得到的切片实际上就是在底层上生成一个相应大小的数组,然后包装为切片返回这个引用类型,所以长度和容量一样大
fmt.Printf("len(s1)=%d,cap(s1)=%d\n", len(s1), cap(s1))
fmt.Printf("len(s2)=%d,cap(s2)=%d\n", len(s2), cap(s2))
//数组得到切片,实际上就是声明一个引用切片指向它,并且可以规定指向的范围,设置容量是为了说明右边界,防止越界报错
a1 := [...]int{1, 3, 5, 7, 9, 11, 13}
s3 := a1[0:4] //1,3,5,7;[0,4)对数组进行左闭右开的切割得到切片,即[起始序号,结束序号)
s4 := a1[1:3] //3,5;[1,3)
s5 := a1[2:] //5,7,9,11,13;[2,len(a1))
s6 := a1[:] //1,3,5,7,9,11,13;[0,len(a1))
fmt.Println(s3, s4, s5, s6)
//长度是包含的元素的个数,容量是切片对应的第一个元素到底层数组的最后一元素包含的所有元素(包括起始和终止元素)的个数
fmt.Printf("len(s4)=%d,cap(s4)=%d\n", len(s4), cap(s4)) //len=2,cap=7=1~6共6个
fmt.Printf("len(s5)=%d,cap(s5)=%d\n", len(s5), cap(s5)) //len=5,cap=5=2~6共5个
//切片再切片,上限边界是容量而不是长度。即起始位置为当前切片的第一个元素为下界,而终止位置是指向的底层数组的最后一个元素
//切片再切片本质上就是对以前的切片的范围进行修改得到新切片,但是还是指向同一个底层数组
s7 := s4[3:6] //9,11,13,虽然s4长为2,但是容量为6,所以切割上限为6,起始只要比上限低就行
fmt.Printf("len(s7)=%d,cap(s7)=%d\n", len(s7), cap(s7)) //len=3,cap=3=3~5共3个
//直接对底层数组或对指向同一个数组的切片修改,因为切片是引用类型,所以其他以该底层数组为基础的切片里的值都会发生改变
fmt.Println(s3)
a1[1] = 200
s6[0] = 100
fmt.Println(s3)
var s8 []int //len(s1)=0;cap(s1)=0;s1==nil
s9 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil,因为初始化后分配内存了
s10 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil,make函数会分配内存
}
-
切片的划分
可以通过对数组或者切片使用切片表达式得到切片,如果是数组的话,则划分的下标low和high的关系是0<=low<=high<=len,划分的范围是[low,high) (左闭右开),而如果对已经已经切片的切片再进行划分,则low和high 的关系是0<=low<=high<=cap。func main() { a := []int{1, 2, 3, 4, 5, 6} a1 := a[0:3] fmt.Printf("a1:v:%v,len:%d,cap:%d\n", a1, len(a1), cap(a1)) //a1:v:[1 2 3],len:3,cap:6 //切片再切片的上限是cap不是len a2 := a1[4:6] fmt.Printf("a2:v:%v,len:%d,cap:%d\n", a2, len(a2), cap(a2)) //a2:v:[5 6],len:2,cap:2 }
-
make()函数及赋值遍历
package main import "fmt" //make()函数创建切片,可以自定义类型,长度,容量 //make([]T, size, cap),其中T为数据类型,size为切片元素的数量,cap是切片的容量,cap可以不写则默认和size一样 func main() { //使用make函数会为切片开辟有size个元素最大为cap的数组 s1 := make([]int, 5, 10) //默认全为0 fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d \n", s1, len(s1), cap(s1)) s2 := make([]int, 0) //长度为0则为空数组 fmt.Printf("s2=%v len(s2)=%d cap(s1)=%d \n", s2, len(s2), cap(s2)) //切片的复制拷贝 s3 := make([]int, 3) //[0 0 0] s4 := s3 //将s3直接赋值给s4,s3和s4共用一个底层数组 s4[0] = 100 fmt.Println(s3) //[100 0 0] fmt.Println(s4) //[100 0 0] //切片的遍历 //索引遍历 for i := 0; i < len(s3); i++ { fmt.Println(s3[i]) } //for range for _, v := range s3 { fmt.Println(v) } }
-
append()
append()
用于扩充切片的大小,它的本质是为一个切片里的内容和需要新增的元素或者切片内容重新形成切片,如果新的切片所需要的容量小于(删除操作)第一个切片参数的容量,则用原来的内存,如果大于则需要开辟一块新的内存,然后返回这块新的区域的地址,扩容策略如下首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。由于切片没有删除操作,所以需要用append将原来切片里需要留下来的元素重新拼起来
package main import "fmt" //append扩容切片 func main() { //append格式为 需要扩容的切片名 = append (需要扩容的切片名,扩容元素),对原来的扩容就需要返回原来的切片名 //append的实质就是将为原来需要扩容的切片里的内容再加上扩容的元素开辟一个新的内存空间存储,然后返回一个地址给一个切片名 //append可以理解为将一个切片的数据加上后面的元素或新的切片进行拼接到一块新的内存然后返回地址 //append返回给任何一个同类型的切片就行,因为切片是应用类型,不用在不它现在指向的区域有没有值,都可以接受 s1 := []string{"北京", "上海", "深圳"} //零值切片可以直接用无需初始化 var s2 []string var s3 []string var s4 []string var n []int fmt.Printf("s1=%v,len=%d,cap=%d\n", s1, len(s1), cap(s1)) //新申请的容量小于原来的二倍,所以容量是原来的二倍,重新开辟新的内存空间 s2 = append(s1, "广州") fmt.Printf("s1=%v,len=%d,cap=%d\n,ptr=%p\n", s1, len(s1), cap(s1), &s1[0]) fmt.Printf("s2=%v,len=%d,cap=%d\n,ptr=%p\n", s2, len(s2), cap(s2), &s2[0]) //append支持一次性扩容多个元素 //新申请的容量大于原来的二倍,所以就是新申请的容量 s3 = append(s1, "广州", "杭州", "成都", "长沙") fmt.Printf("s1=%v,len=%d,cap=%d\n", s1, len(s1), cap(s1)) fmt.Printf("s3=%v,len=%d,cap=%d\n", s3, len(s3), cap(s3)) //append可以直接添加切片,但需要在切片后加...,...表示拆开的意思 s := []string{"桂林", "西藏", "柳州"} s4 = append(s1, s...) fmt.Printf("s4=%v,len=%d,cap=%d\n", s4, len(s4), cap(s4)) //为零值切片赋值 n = append(n, 1, 2, 3, 4, 5) fmt.Println(n) // 从切片中删除元素 x := [...]int{30, 31, 32, 33, 34, 35, 36, 37} a := x[:] //利用数组生产切片 // 要删除索引为2的元素,加...表示拆开 //使用这种方法删除实际上就是为其中不需要删除的元素重新排在一起,并且由于剩下的元素个数小于原来的容量,所以不需要重新开辟内存 //所以相当于就是将需要留下来的元素重新拍好位置,然后放在原来的数组的前面 a = append(a[:2], a[3:]...) fmt.Printf("a=%v,len=%d,cap=%d,ptr=%p\n", a, len(a), cap(a), &a[0]) //[30 31 33 34 35 36 37] cap=8因为还是用的原来的内存 fmt.Printf("x=%v,len=%d,cap=%d,ptr=%p\n", x, len(x), cap(x), &x[0]) //[30,31,33,34,35,36,37,37]就是将需要留下的数拍好序后放在数组前,a和x的地址一样的,占用的同样的内存 }
-
copy()
copy
可以将一个切片的里的值赋值到另外一个切片空间中,但是目的切片必须要有空间,copy本身不会为目标切片开辟空间package main import "fmt" //copy的格式为copy(目标切片, 源切片) func main() { a1 := []int{1, 3, 5} a2 := a1 a3 := make([]int, 3) copy(a3, a1) a1[0] = 100 //因为是把值复制给了a3,所以修改a1影响不到a3 fmt.Println(a1, a2, a3) }
-
练习
package main import ( "fmt" "sort" ) //练习 //在初始化为0的切片后使用append //使用sort函数 func main() { //在五个0后面添加0到10 var a = make([]int, 5, 10) for i := 0; i < 10; i++ { a = append(a, i) } fmt.Println(a) //在五个空字符后添加字符串0到10 var b = make([]string, 5, 10) for i := 0; i < 10; i++ { b = append(b, fmt.Sprintf("%v", i)) //sprintf将数字转为字符串 } fmt.Println(b) //排序函数 a1 := []int{3, 9, 1, 5, 7} sort.Ints(a1[:]) //sort只能用于切片,所以需要转换 fmt.Println(a1) }