1. 从数组说起
数组是具有固定长度具有零个或者多个相同数据类型元素的序列。
由于数组长度固定,在Go里很少直接使用。
1.1 定义数组
// 3种方式,声明,初始,省略号
// 变量arr1类型为[5]int
var arr1 [5]int
// 变量arr2类型为[3]int,同时初始化赋值
var arr2 [5]int = [5]int{1,2,3}
// 让编译器自己数,结果为[3]int
arr3 := [...]int{1,2,3}
// 错误例子,因为[3]int和[4]int是两种类型
arr3 := [4]int{1,2,3,4}
- 数组长度是数组类型的一部分,
[3]int
和[5]int
就是两种类型。 - 数组长度必须是常量表达式,即这个表达式的值在编译阶段就可以确定。
1.2 查看数组基本信息
- Go内置函数len,可以返回数组中元素个数。
arr1 := [...]{1,2,3}
lenth := len(arr1)
1.3 数组遍历
- 数组中的每个元素是通过index来访问的。
arr1 := [3]{1,2,3}
for i,val := range arr1 {
}
1.4 数组作为函数参数
-
数组是值类型,值类型意味着拷贝。
-
注意!!!在其他语言中,数组是隐式的使用引用传递;Golang传参时,传入的参数会创建一个副本,使用这种方式传递大的数组会变的很低效。
-
可以显式的给函数传递一个数组的指针。
// 1. 遍历数组,将数组中元素清零
fun zero (ptr *[32]byte) {
for i := range ptr {
ptr[i] = 0
}
}
// 2. 利用数组指针,将数组元素清零
func zero (ptr *[32]byte) {
*ptr = [32]byte{}
}
1.5 数组比较
- 如果一个数组中的元素是可比较的,那么数组也是可比较的
- 可以直接使用==和!=比较两个数组
a := [2]int{1,2} // [2]int类型
b := [...]int{1,2} // [2]int类型
c := [2]int{1,3} // [2]int类型
fmt.Println(a==b,a==c,b==c) // true,false,false
d := [3]int{1,2} // [3]int类型
fmt.Println(a == d) // 编译错误:不能比较[2]int和[3]int
2. 切片
切片用于表示一个拥有相同类型元素的可变长的序列,看上去像是没有长度的数组类型。
2.1 定义Slice
通过数组定义
// 定义一个数组
arr := [...]int{0,1,2,3,4,5,6,7}
// 根据数组,定义该数组的view,即slice
s := arr[2:6] // s={2,3,4,5},左开右闭
// s就是一个切片,它是数组arr的一个视图
// 总结来说,一个数组取它的slice,使用slice操作符arr[:]即可,看的是底层数组的全部景象
直接定义切片
var s1 []int
s2 := []int{0,1,2,3,4}
s3 := make([]int,6) // 默认len == cap == 6
s4 := make([]int,10,32) // len == 10 ,cap == 32
2.2 查看切片基本信息
- Go内置函数len和cap,用来返回slice的长度和容量。
// 定义一个数组
arr := [...]int{0,1,2,3,4,5,6,7}
// 根据数组,定义该数组的view,即slice
s := arr[2:6] // s={2,3,4,5},左开右闭
fmt.Println(len(s)) // len(s) == 4 (2-5)
fmt.Println(cap(s)) // cap(s) == 6 (2-7)
- 判断slice是否为空。
// slice 不能使用 == 操作符来比较两个切片元素是否相等
// 唯一允许的比较操作是和nil作比较
// 但是,slice为nil的情况有很多种
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int{nil} // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s!=nil
// 所以一般情况不用 s == nil来判断slice是否为空,而是使用len(s) == 0来判断
2.3 切片相关操作
- 切片遍历在语法上和数组是相似的
for _,s := range s1 {
}
- append()函数追加元素
// 使用append追加元素可以超过capacity,它会触发slice底层数组的扩容机制
// 扩容就是重新分配长度更大的底层数组,原来的数组就会进行垃圾回收
// 由于值传递的关系,必须要使用一个新的slice接收append的返回值
// append的时候,ptr,len,cap有可能都会发生变化,必须接收新的ptr,len和cap
s := []int{0,1,2,3,4,5}
s = append(s,val)
// 下面的一种操作,可以达到删除slice中元素的目的
// ...表示变长参数列表,追加s[4:]切片后所有元素
s = append(s[:3],s[4:]...)
// append是从切片的len,向后进行扩展的操作
var ints []int = make([]int,2,5) // len=2,cap=5
// 底层数组:0 0 0 0 0
ints = append(ints,1) // len=3,cap=5
// 底层数组:0 0 1 0 0
2.4 切片作为函数参数
- 所谓切片是引用类型,是指切片作为函数的参数,操作的是同一个底层数组。(由于引用类型数据存放在堆上,值类型数据存放在栈上,因此切片数据存放在堆上,栈上保存堆在内存中的地址)
切片拷贝:
- 对于切片直接使用
=
拷贝,实际上是浅拷贝,只拷贝了切片在堆上的内存地址;sliceA = sliceB
。- 对于切片深拷贝的需求,可以使用
copy
内置函数完成;copy(sliceA,sliceB)
。
func nonempty(strings []string) []string {
i := 0
for _, s := range strings {
if s != "" {
strings[i] = s
i++
}
}
return strings[:i]
}
- golang中函数传递都是值传递,map、channel、slice都是引用传递,会传递指针值。参数传递时,切片进行浅拷贝(指针、cap、len),在函数内部发生append扩容等操作时,会改变原切片的值。https://zhuanlan.zhihu.com/p/111796041
//定义一个函数,给切片添加一个元素
func addOne(s []int) {
s[0] = 4 // 可以改变原切片值
s = append(s, 1) // 扩容后分配了新的地址,原切片将不再受影响
s[0] = 8
}
var s1 = []int{2} // 初始化一个切片
addOne(s1) // 调用函数添加一个切片
fmt.Println(s1) // 输出一个值 [4]
3. 面试
3.1. 数组和切片
3.1.1. 数组
- Go中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份
- 如果Go中的数组作为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针。
3.1.2. 切片
- 切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。
- 切片可能会在堆上分配内存,本身不是动态数组或者数组指针,内部是通过指针引用底层数组,切片本身是一个只读对象,本身没有数据,底层数组才有数据,类似于数组指针的一种封装,是引用类型
- 当使用for range 遍历slice的时候,拿到的value其实是切片的值拷贝,每次打印出来的value地址不变
- slice扩容时,当cap小于1024的时候,每次扩容都会变成原来容量的2倍;当大于1024的时候,每次变为之前的1.25倍
3.1.3. 切片扩容
- 预估扩容后的newCap
// 预估规则:
if oldCap * 2 < newCap
直接分配内存
else
if oldLen < 1024 newCap = oldCap * 2
if oldLen > 1024 newCap = oldCap *1.25
- newCap个元素需要多大的内存?
// 预估到的newCap只是扩容后元素的个数,具体分配多大的内存呢?
// newCap * sizeof(T)吗?
// 事实上,许多编程语言中,申请分配内存,并不是直接和操作系统交涉,而是和语言自身实现的内存管理模块。内存管理模块会提前申请一批常用的内存,管理起来,需要申请 内存时内存管理模块会帮我们匹配到最接近的规格
- 匹配到合适的内存规格
3.2 切片为什么叫切片?与Java中动态数组有区别吗?
TODO: 以java 中StringBuffer和ArrayList为例作比较