来自于《go语言中文文档》的学习及自我分析
数组和切片的区别
golang中有两个很相似的数据结构:数组(Array)和slice。数组和slice实际有各自的优缺点和区别,这里列出最主要的区别
功能点 | 数组 | slice |
---|---|---|
概念 | 是同一种数据类型的固定长度的序列 | 切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递 |
数组长度 | 数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变 | 切片的长度可以改变,因此,切片是一个可变的数组 |
访问 | 数组可以通过下标进行访问 | 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制 |
访问越界 | 如果下标在数组合法范围之外,则触发访问越界,会panic | 访问越界也会panic |
值类型or引用 | 赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值 | 切片可以看做对数组的一个引用 |
len返回什么 | 数组长度 | 切片长度 |
cap返回什么 | 数组长度 | cap可以求出slice最大扩张容量,不能超出数组限制 |
其他注意事项 | 多维数组只有最外层允许不指定长度使用…来判断 |
数组:
实际上,对于数组的定义or初始化,已经有很多示例了,例如:
var arr0 [5]int = [5]int{1, 2, 3} // 正常定义,不足的部分默认为0var arr1 = [5]int{1, 2, 3, 4, 5}var arr2 = [...]int{1, 2, 3, 4, 5, 6} // 根据值的数量得到数组长度var str = [5]string{3: "hello world", 4: "tom"} // 定义某些key上的值,剩余的补默认值,string补""
数组的定义就是这么简单,注意到上文提过数组是值传递,意思是将数组赋值或传参,修改了赋值或传参后的数据,原数组本身不会被改变。给出分别对应的示例:
- 赋值给另一个变量,修改另一个变量的值:
func main() {a := [2]int{} // 定义一个长度为2的数组,值为[0, 0]b := ab[1] = 3000fmt.Printf("res A: %v\n", a) // 打印a,虽然a赋值给b,且b的值有变化,但a不变fmt.Printf("res B: %v\n", b) // 打印b, b的值已经重新赋值了fmt.Printf("addr A: %p\n", &a) // a的地址fmt.Printf("addr B: %p\n", &b) // b的地址,会发现与a并不相同
}
运行结果为:
res A: [0 0]
res B: [0 3000]
addr A: 0xc000096080
addr B: 0xc000096090
- 传参给另一个function,修改值:
func test(x [2]int) {fmt.Printf("x: %p\n", &x)x[1] = 1000
}func main() {a := [2]int{} // 定义一个长度为2的数组,值为[0, 0]test(a)fmt.Printf("res A: %v\n", a) // 打印a,虽然传参给test,但实际上不修改a的值
}
以上代码结果为:
x: 0xc00000a0d0
res A: [0 0]
func test(x *[2]int) {fmt.Printf("x: %p\n", &x)x[1] = 1000
}func main() {a := [2]int{} // 定义一个长度为2的数组,值为[0, 0]test(&a)fmt.Printf("res A: %v\n", a) // 打印a,因为test实际传的是a的引用,在test中修改a也会对a本身产生影响
}
以上代码运行结果为:
x: 0xc00005c028
res A: [0 1000]
练习题:
题目描述:第一行输入一个int类型的n,表示将要计算的数组和,接下来的每行输入数组的元素,以空格分隔,当什么都不输入直接回车表示结束。计算每一行的数组中,有哪两个key的和为第一行给出的n,例如数组[1,3,5,8,7],找出两个元
素之和等于8的下标分别是(0,4)和(1,2)
以下是我的代码:
package mainimport ("bufio""fmt""os""strconv""strings"
)func getAddSumNum(sliceArr []int, sum int) (res [][]int) {for i := 0; i < len(sliceArr); i++ {for j := i + 1; j < len(sliceArr); j++ {if sliceArr[i]+sliceArr[j] == sum {res = append(res, []int{i, j})}}}return res
}/*
*
找出数组中和为给定值的两个元素的下标
第一行输入要找到和为n的值
每行输入数组的元素,以空格分隔
输入0代表结束
计算和为n的下标有哪些并输出
*/
func main() {scanner := bufio.NewScanner(os.Stdin)if scanner.Scan() {sum := scanner.Text()for {if scanner.Scan() {input := scanner.Text()inputArr := strings.Split(input, " ")if len(inputArr) == 0 && inputArr[0] == "" {break}var inputIntSlice []intfor _, v := range inputArr {vInt, _ := strconv.Atoi(v)inputIntSlice = append(inputIntSlice, vInt)}sumInt, _ := strconv.Atoi(sum)res := getAddSumNum(inputIntSlice, sumInt)fmt.Printf("origin: %#v, res %#v\n", inputIntSlice, res)}}}
}
slice_150">slice
slice可以看做数组的引用,但slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案
创建切片的方式:
var s1 []int // 第一种方式,直接定义slices2 := []int{1, 2} // 第二种方式,定义slice的值var s3 []int = make([]int, 0) // 通过makevar s4 []int = make([]int, 0, 0) // 初始化赋值arr := [5]int{1, 2, 3, 4, 5}s5 := arr[2:4] //直接从数组切片,注意是左开右闭fmt.Printf("res:%v\n", s1)fmt.Printf("res:%v\n", s2)fmt.Printf("res:%v\n", s3)fmt.Printf("res:%v\n", s4)fmt.Printf("res:%v\n", s5)
slice[m:n]:切片,从slice[m]一直到slice[n-1]都在这个切片中
slice[:]表示完整的slice
slice[m:n:q],表示从m-n的切片,依然是左开右闭,但是cap为q-m
通过make来创建slice:
make([]int, 0, 0):
var slice []type = make([]type, len)slice := make([]type, len) // 不传cap则cap=lenslice := make([]type, len, cap)
注意若使用make定义slice的时候传了cap参数,那么定义slice的值的个数不允许超过cap的长度,例如以下代码会报错:
var sliceTest []int = make([]int, 2, 3)sliceTest[0] = 3fmt.Printf("%d %v\n", 0, sliceTest)sliceTest[1] = 4fmt.Printf("%d %v\n", 1, sliceTest)sliceTest[2] = 5fmt.Printf("%d %v\n", 2, sliceTest)sliceTest[3] = 6fmt.Printf("%d %v\n", 3, sliceTest)sliceTest[4] = 7fmt.Printf("%d %v\n", 4, sliceTest)sliceTest[5] = 8fmt.Printf("%d %v\n", 5, sliceTest)
//执行结果为:
0 [3 0]
1 [3 4]
panic: runtime error: index out of range [2] with length 2// 但是使用append是不会报错的
var sliceTest []int = make([]int, 2, 3)sliceTest[0] = 3fmt.Printf("%d %v\n", 0, sliceTest)sliceTest = append(sliceTest, 4)fmt.Printf("%d %v\n", 1, sliceTest)sliceTest = append(sliceTest, 5)fmt.Printf("%d %v\n", 2, sliceTest)sliceTest = append(sliceTest, 6)fmt.Printf("%d %v\n", 3, sliceTest)sliceTest = append(sliceTest, 7)fmt.Printf("%d %v\n", 4, sliceTest)sliceTest = append(sliceTest, 8)fmt.Printf("%d %v\n", 5, sliceTest)
注意对切片的操作实际上是操作的底层数组,示例:
arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}slice := arr[2:4]slice[0] += 100slice[1] += 100fmt.Printf("slice is: %#v", slice)fmt.Printf("arr is: %#v", arr)
// 结果为:
// slice is: []int{103, 104}arr is: [9]int{1, 2, 103, 104, 5, 6, 7, 8, 9}
超出cap限制时,cap自动扩容:
超出原 slice.cap 限制,就会重新分配底层数组,即便原
数组并未填满。依旧以上文的append为示例:
var sliceTest []int = make([]int, 2, 3)sliceTest[0] = 3fmt.Printf("%d %v, len: %d, cap:%d, \n", 0, sliceTest, len(sliceTest), cap(sliceTest))sliceTest = append(sliceTest, 4)fmt.Printf("%d %v len: %d, cap:%d,\n", 1, sliceTest, len(sliceTest), cap(sliceTest))sliceTest = append(sliceTest, 5)fmt.Printf("%d %v len: %d, cap:%d,\n", 2, sliceTest, len(sliceTest), cap(sliceTest))sliceTest = append(sliceTest, 6)fmt.Printf("%d %v len: %d, cap:%d,\n", 3, sliceTest, len(sliceTest), cap(sliceTest))sliceTest = append(sliceTest, 7)fmt.Printf("%d %v len: %d, cap:%d,\n", 4, sliceTest, len(sliceTest), cap(sliceTest))sliceTest = append(sliceTest, 8)fmt.Printf("%d %v len: %d, cap:%d,\n", 5, sliceTest, len(sliceTest), cap(sliceTest))
// cap会自动扩容,输出结果如下:
0 [3 0], len: 2, cap:3,
1 [3 0 4] len: 3, cap:3,
2 [3 0 4 5] len: 4, cap:6,
3 [3 0 4 5 6] len: 5, cap:6,
4 [3 0 4 5 6 7] len: 6, cap:6,
5 [3 0 4 5 6 7 8] len: 7, cap:12,
cap每次扩容都是上次cap的2倍
切片的拷贝:copy
func copy(dst, src []T) int
// dst:目标切片,数据将被拷贝到这里
// src:源切片(数据来源)
// 返回值:实际复制的元素个数(取 dst 和 src 长度的较小值)
copy的几个功能/注意事项
(1)copy只会copy dst 和 src 长度的较小值,也就是说如果源切片比目标切片长,那么目标切片不会自动扩容;反之目标切片只有部分值被覆盖,例如:
// 目标切片比源切片短时:
src := []int{1, 2, 3, 4}
dst := make([]int, 2)
n := copy(dst, src) // n=2,dst=[1,2]// 目标切片比源切片长时:dst := []int{1, 2, 3, 4, 5}src := []int{10, 11}copy(dst, src)fmt.Printf("dst res is: %v\n", dst)// dst res is: [10 11 3 4 5]fmt.Printf("src res is: %v\n", src)
(2)copy时源切片和目标切片的类型必须匹配,否则会编译错误
(3)如果目标切片长度不够,需要显式扩容后再复制,例如:
src := []int{1, 2, 3}
dst := make([]int, 2)
// 扩展 dst 长度
dst = append(dst, make([]int, len(src)-len(dst))...)
copy(dst, src) // dst=[1,2,3]
(4)copy是深拷贝,修改拷贝后的切片不影响原切片
slice_293">slice遍历:
使用for range即可
切片resize(调整大小)
var a = []int{1, 3, 4, 5}fmt.Printf("slice a : %v , len(a): %v, cap(a) %v: \n", a, len(a), cap(a))b := a[1:2] // b的cap=原始数组长度-起始索引fmt.Printf("slice b : %v , len(b): %v, cap(b) %v:\n", b, len(b), cap(b))c := b[0:3] // b的容量为3,虽然b没有用到后几位,但是可以拿到fmt.Printf("slice c : %v , len(c): %v, cap(c) %v:\n", c, len(c), cap(c))
// 以上代码输出结果为:
/**
slice a : [1 3 4 5] , len(a): 4, cap(a) 4:
slice b : [3] , len(b): 1, cap(b) 3:
slice c : [3 4 5] , len(c): 3, cap(c) 3:
*/
这里有个注意事项,因为slice b的长度只有1,因此直接读取b[2]会报错:panic: runtime error: index out of range [2] with length 1
,但是获取切片不会报错:c := b[0:3]
数组和切片的内存布局
可以看出,数组实际上是存储在连续的内存地址中的,而切片实际上只是指向开始位置的指针+len+cap构成的结构
字符串和切片
string底层就是一个byte的数组,因此,也可以进行切片操作,例如;
str := "hello world"fmt.Printf("str[0:5] is: %v\n", str[0:5]) // str[0:5] is: hellofmt.Printf("str[6:] is: %v\n", str[6:]) // str[6:] is: world
不能直接修改字符串中的某个字符:
s := "hello"// 下面这行代码会报错,因为不能直接修改字符串的某个字符// s[0] = 'H' // 如果要改变,可以重新赋值s = "Hello"fmt.Println(s)// 要改变某个位置的值,可以现改为[]byte()或[]rune()(中文用这个),然后修改sByte := []byte(s)sByte[6] = 'G'sByte = sByte[:8]sByte = append(sByte, '!')s = string(sByte)fmt.Printf("res of sByte's s: %s\n", s) // res of sByte's s: Hello Go!
切片中两个冒号的理解
切片中可能出现1个冒号,两个冒号的情况,分别列举:
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}d0 := slice[:] // 实际就是和原数组大小相等,cap相等的fmt.Printf("d0: %v, len(d0): %d, cap(d0):%d\n", d0, len(d0), cap(d0)) // [0 1 2 3 4 5 6 7 8 9] 10 10d1 := slice[6:] //从第6个开始,截取到末尾fmt.Printf("d1: %v, len(d1): %d, cap(d1):%d\n", d1, len(d1), cap(d1)) // [6 7 8 9] 4 4d2 := slice[:3] // 从第0个开始,截取到3-1个fmt.Printf("d2: %v, len(d2): %d, cap(d2):%d\n", d2, len(d2), cap(d2)) // [0 1 2] 3 10d3 := slice[6:8] // 从第6个截取到第8-1个fmt.Printf("d3: %v, len(d3): %d, cap(d3):%d\n", d3, len(d3), cap(d3)) // [6 7] 2 4d4 := slice[:6:8] // 从第0个开始,截取到第6-1个,cap为8fmt.Printf("d4: %v, len(d4): %d, cap(d4):%d\n", d4, len(d4), cap(d4)) // [0 1 2 3 4 5] 6 8
常规slice : data[6:8],从第6位到第8位(返回6, 7),长度len为2, 最大可扩充长度cap为4(6-9)
另一种写法: data[:6:8] 每个数字前都有个冒号, slice内容为data从0到第6位,长度len为6,最大扩充项cap设置为8
a[x:y:z] 切片内容 [x:y] 切片长度: y-x 切片容量:z-x;
在书中提到了一个快速将slice转化为逗号分隔的字符串的方法:
strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1)
,可以分解为:(1)fmt.Sprint得到字符串,对于数组就是形如[1 2 3]的结果
(2)然后使用strings.Trim去除收尾的字符"[“以及”]"
(3)使用strings.Replace替换空格为逗号,注意最后一个参数-1表示所有都要替换