后期回顾
后面的一章次要和大家分享了 GO 语言的函数的定义,以及 GO 语言中的指针的简略用法,那么本章,老猫就和大家一起来学习一下 GO 语言中的容器。
数组
数组的定义
说到容器,大家有编程教训的必定第一个想到的就是数组了,当然也有编程教训的小伙伴会感觉数组并不是容器。然而无论如何,说到数组其实它就是存储和组织数据的一种形式而已,大家就不要太过纠结叫法了。
咱们间接上数组定义的例子,具体如下:
var arr1 [5]int // 定义一个长度为 5 的默认类型
arr2:=[3]int{1,2,3} // 定义一个数组,并且指定长度为 3
arr3:=[...]int{1,2,3,4,5,6} // 定义一个数组,具体的长度交给编译器来计算
var grid [4][5] bool // 定义一个四行五列的二维数组
fmt.Println(arr1,arr2,arr3,grid)
下面的例子输入的后果如下
[0 0 0 0 0] [1 2 3] [1 2 3 4 5 6] [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]
大家能够总结一下,其实数组有这么几个特点
- 在写法上,其实也是和其余编程语言是相同的,其定义的数组的长度写在变量类型的后面
- 数组中所存储的内容必然是同一类型的
数组的遍历
那么咱们如何遍历获取数组中的数据呢?其实看过老猫之前文章的小伙伴应该知道能够用 for 循环来遍历获取,其中一种大家比拟容易想到的形式如下(咱们以遍历下面的 arr3 为例)
for i:=0;i<len(arr3);i++ {fmt.Println(arr3[i])
}
这种形式呢,咱们当然是能够获取的。接下来老猫其实还想和大家分享另外一种形式,采纳 range 关键字的形式
// i 示意的是数据在数组中的地位下标,v 示意理论的值
for i,v :=range arr3 {fmt.Println(i,v)
}
// 那么如果咱们只想要 value 值呢, 回顾一下老猫之前所说的就能够知道,咱们能够用_的形式进行对 i 省略
for _,v :=range arr3 {fmt.Println(v)
}
// 如果咱们只有地位下标,那么咱们如上来写即可
for i:=range arr3 {fmt.Println(i)
}
大家感觉上述两种形式哪种形式会比拟优雅?不言而喻是后者了,意义明确而且好看。
go 语言中数组是值传递的
另外和大家同步一点是数组作为参数也是值传递。还是沿用之前的咱们从新定义一个新的函数如下:
func printArray(arr [5]int){
for i,v:=range arr {println(i,v)
}
}
那么咱们在 main 函数中进行相干调用(为了演示编译谬误,老猫这里用图片)
大家依据下面的图能够很清晰的看到调用 printArray(arr2)的时候报了编译谬误,其实这就是说明,在 go 语言中,即便同一个类型的数组,如果不同长度,那么编译器还是认为他们是不同类型的。
那么咱们这个时候再对传入的数组进行值的变更呢,具体如下代码
func main() {arr3:=[...]int{1,2,3,4,5} // 定义一个数组,并且长度可变
printArray(arr3)
for i,v:=range arr3 {println(i,v)
}
}
func printArray(arr [5]int){arr[0] = 300
for i,v:=range arr {println(i,v)
}
}
大家能够看到,老猫在这里操作了两次打印,第一次打印是间接在函数中打印,此时曾经更改了第一个值,其函数外部打印的后果为
0 300
1 2
2 3
3 4
4 5
显然外部的值是变更了,然而咱们再看一下里面的函数的打印的值,如下
0 1
1 2
2 3
3 4
4 5
其实并没有产生变更,这其实阐明了什么呢,这其实阐明了在调用 printArray 的时候其实是间接将数组拷贝一份传入函数的,里面的数组并未被更新,这也间接阐明了 GO 语言是值传递的参数传递形式。
大家在应用这个数组的时候肯定要留神好了,说不准就被坑了。大家可能会感觉这个数组真难用,其实能够通知大家一个好消息,在 GO 语言中,个别其实不会间接去应用数组的,咱们用的比拟多的还是“切片”
切片
说到切片的话,咱们其实最好是基于下面数组的根底下来了解切片。咱们先来看一个例子
func main() {arr := [...]int{1,2,3,4,5,6,7}
fmt.Println("arr[2:6]",arr[2:6])
fmt.Println("arr[:6]",arr[:6])
fmt.Println("arr[2:]",arr[2:])
fmt.Println("arr[:]",arr[:])
}
其实像相似于 '[]’ 这种定义咱们就称说其为切片,英文成为 slice,它示意领有雷同类型元素的可变长度的序列。咱们来看一下后果:
arr[2:6] [3 4 5 6]
arr[:6] [1 2 3 4 5 6]
arr[2:] [3 4 5 6 7]
arr[:] [1 2 3 4 5 6 7]
其实这么说会比拟好了解,slice 咱们能够将其看作为视图,就拿 arr[2:6]来说,咱们其实在原来数组的根底上抽取了从第二个地位到第六个地位的元素作为值从新展示进去,当然咱们的取值为左闭右开区间的。
slice 其实是视图概念
下面咱们说了 slice 相当于是数组的视图,那么接下来的例子,咱们来证实上述的说法,具体看上面的例子
func main() {arr := [...]int{1,2,3,4,5,6,7}
fmt.Println("arr[2:6]",arr[2:6])
updateSlice(arr[2:6])
fmt.Println("arr[2:6]",arr[2:6])
fmt.Println(arr)
}
func updateSlice(arr []int){arr[0] = 100
}
老猫写了个函数,次要是更新 slice 第一个地位的值,大家能够先思考一下执行前后所失去的后果是什么,而后再看上面的答案。
其实最终执行的后果为:
arr[2:6] [3 4 5 6]
arr[2:6] [100 4 5 6]
[1 2 100 4 5 6 7]
那么为什么是这样的?其实 arr[2:6]很容易了解是下面的 3456,第二个也比拟容易了解,当咱们 slice 的第一个值被更新成了 100,所以编程了第二种,那么原始的数据为什么也会变成 100 呢?这外面其实是须要好好品一下,因为咱们之前说 slice 是对原数组的视图,当咱们第二种看到 slice 其实曾经产生了更新变成了 100,那么底层的数据必定也产生了变更,变成了 100 了。(这里要留神的是,并没有谁说视图的操作不会副作用于原数组)。这里还是比拟重要的,心愿大家细品一下。
reslice 以及扩大
说到 reslice,说白了就是对原先的 slice 再做一次 slice 取值,那么咱们看上面的例子。
func main() {arr := [...]int{1,2,3,4,5,6,7}
s1 := arr[:]
s2 := s1[2:4]
fmt.Println(s2)
}
以上例子可见 s1 是对数组的全量切片,而后咱们对 s1 又进行了一次切片解决,很容易地能够推算出来咱们第二次所失去的后果为[3,4],像这种行为咱们就称为 reslice,这个还是比拟好了解的。
接下来咱们在这个根底上加深一下难度,咱们在 S2 的根底上再次进行 resilce,具体如下:
func main() {arr := [...]int{1,2,3,4,5,6,7}
s1 := arr[:]
s2 := s1[2:4]
s3 := s2[1:3]
fmt.Println(s3)
}
咱们都晓得 s2 所失去的值为 [3,4],当咱们在次对其进行 reslice 的时候,因为取的是[1:3],那么此时咱们发现是从第一个地位到第三个地位,第一个地位还是比拟好推算出来的,基于[3,4] 的话,那么其第一个地位应该是 4,那么前面呢?后果又是什么呢?这里将后果间接通知大家吧,其实老猫运行之后所失去的后果是
[4 5]
那么为什么会有这样的一个后果?5 又是从哪里来的呢?
咱们来看一下老猫上面整顿的一幅示意图。
- arr 的一个数组,并且其长度为 7,并且外面存储了七个数。
- 接下来 s1 对其去齐全切片,所以咱们失去的也是一个残缺的 7 个数。
- 须要留神的是,这时候咱们用的是下标示意,当 s2 对 s1 在此切片的时候,咱们发现其本质是对数组的第二个元素开始进行取值,因为是视图的概念,其实 s2 还会视图 arr 空幻出另外两个地位,也就是咱们示意的灰色的 3 以及 4 下标。
- 同样的咱们将 s3 示意进去,由此咱们 s3 是在 s2 的根底上再次切片,实践上有三个下标值,别离是 0、1、2 下标取值,然而咱们发现 s2 的 3 号地位批示空幻进去的地位, 并未真正存在值与之对应,因而,咱们取交加之后与数组 arr 对应只能取出两个,也就是最终的[4,5]。
此处还是比拟难了解,心愿大家好好了解一下,而后写代码本人推演一下,其实这个知识点就是 slice 的扩大,咱们再来看一下上面的 slice 的底层实现。
其实 slice 个别蕴含三个概念,slice 的底层其实是空数组构造,ptr 为指向数组第一个地位的指针,Len 示意具体的 slice 的可用长度,而 cap 示意有能力扩大的长度。
其实对于 len 以及 cap 咱们都有函数间接能够调用获取,咱们看一下下面的例子,而后打印一下其长度以及扩大 cap 大家就分明了。具体打印的代码如下。
func main() {arr := [...]int{1,2,3,4,5,6,7}
s1 := arr[:]
s2 := s1[2:4]
s3 := s2[1:3]
fmt.Printf("arr=%v\n",arr)
fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n",s1,len(s1),cap(s1))
fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",s2,len(s2),cap(s2))
fmt.Printf("s3=%v,len(s3)=%d,cap(s3)=%d\n",s3,len(s3),cap(s3))
}
上述代码输入的后果为
arr=[1 2 3 4 5 6 7]
s1=[1 2 3 4 5 6 7],len(s1)=7,cap(s1)=7
s2=[3 4],len(s2)=2,cap(s2)=5
s3=[4 5],len(s3)=2,cap(s3)=4
当咱们的取值超过 cap 的时候就会报错,例如当初 s2 为 s2:=[2:4],当初咱们发现其 cap 为 5,如果咱们超过 5,那么此时 s2 能够写成 s2:=[2:8], 那么此时就会报以下异样
panic: runtime error: slice bounds out of range [:8] with capacity 7
goroutine 1 [running]:
main.main()
E:/project/godemo/part6-slice.go:8 +0x7f
再者如果咱们这么取值
fmt.Printf("s3=%v",s3[4])
此时 s3 曾经超过了 len 长度,那么也会报错,报错如下
panic: runtime error: index out of range [4] with length 2
goroutine 1 [running]:
main.main()
E:/project/godemo/part6-slice.go:14 +0x49f
综上例子,咱们其实能够失去这么几个论断。
- slice 能够向后扩大,不能够向前扩大。
- s[i]不能够超过 len(s),向后扩大不能够超过底层数组 cap(s)
以上对 slice 的扩大其实还是比拟让人头疼的,比拟难了解,不过真正弄清外面的算法倒是也还好,心愿大家也能了解上述的阐释,老猫曾经尽最大致力了,如果还有不太分明的,也欢送大家私聊老猫。
切片的操作
向 slice 增加元素,如何增加呢?看一下老猫的代码,如下:
func main() {arr :=[...]int{0,1,2,3,4,5,6,7}
s1 :=arr[2:6]
s2 :=s1[3:5]
s3 := append(s2,10) //[5,6,10]
s4 := append(s3,11) //[5,6,10,11]
s5 := append(s4,12)
fmt.Printf("arr=%v\n",arr)
fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",s2,len(s2),cap(s2))
fmt.Printf("s2=%v\n",s2)
fmt.Printf("s3=%v\n",s3)
fmt.Printf("s4=%v\n",s4)
fmt.Printf("s5=%v\n",s5)
}
如上述所示,咱们往切片中增加操作的时候采纳的是 append 函数,大家能够先不看老猫上面的理论后果本人推算一下最终的输入后果是什么。联合之前老猫所述的切片操作。后果如下:
arr=[0 1 2 3 4 5 6 10]
s2=[5 6],len(s2)=2,cap(s2)=3
s2=[5 6]
s3=[5 6 10]
s4=[5 6 10 11]
s5=[5 6 10 11 12]
上述咱们会发现 append 操作的话会有这样的一个论断
- 增加元素的时候如果超过 cap,零碎会重新分配更大的底层数组
- 因为值传递的关系,必须接管 append 的返回值
slice 的创立、拷贝
之前老猫和大家分享的 slice 看起来都是基于 arr 的,其实 slice 的底层也的确是基于 arry 的,那么咱们是不是每次在创立 slice 的时候都须要去新建一个数组呢?其实不是的,咱们 slice 的创立形式有很多种,咱们来看一下上面的创立形式
func main() {var s []int //1、空 slice 的创立形式,其实底层是基于 Nil 值的数组创立而来
for i := 0;i<100;i++ {s = append(s,2*i+1)
}
fmt.Println(s)
s1 :=[]int {2,4,5,6} //2、创立一个带有初始化值得 slice
s2 :=make([]int ,16) //3、采纳 make 内建函数创立一个长度为 16 的切片
s3 :=make([]int,10,32) //4、采纳 make 内建函数创立一个长度为 10 的切片,然而 cap 为 32
//slice 的拷贝也是相当简略的也是间接用内建函数即可,如下
copy(s2,s1) // 这里次要示意的是将 s1 拷贝给 s2, 这里须要留神的是不要搞反了
}
slice 元素的删除操作
为什么要把删除操作独自拎进去分享,次要是因为上述这些操作都有比拟便捷的内建函数来应用,然而删除操作就没有了。咱们只能通过切片的个性来求值。如下例子
func main() {s1 :=[] int{2,3,4,5,6}
s2 :=append(s1[:2],s1[3:]...)
fmt.Println(s2)
}
上述有一个 2 到 6 的切片,如果咱们要移除其中的 4 元素,那么咱们就得用这种切片组合的形式去移除外面的元素,置信大家能够看懂,至于“s1[3:]…”这种模式,其实是 go 语言的一种写法,示意取从 3 号地位剩下的所有的元素。
最终咱们失去的后果就失去了
[2 3 5 6]
以上就是对 slice 的所有的常识分享了,花了老猫不少工夫整理出来的,老猫也尽量把本人的一些了解说分明,slice 在语言中还是比拟重要的。
写在最初
回顾一下下面的 GO 语言容器,其实重点和大家分享是 slice(切片)的相干定义,操作以及底层的一些原理。弄清楚的话还是比拟容易上手的。当然 go 语言的容器可不止这些,因为篇幅的限度,老猫就不分享其余的容器了,置信在写下去就没有急躁看了。前面的容器次要会和大家分享 map 以及字符和字符串的解决。
我是老猫,更多内容,欢送大家搜寻关注老猫的公众号“程序员老猫”。