【读书笔记&个人心得】第7章:数组与切片
数组与切片
这章我们开始剖析 集合,它是可以包含大量条目 (item) 的数据结构,例如数组、切片和 map。从这看到 Go 明显受到 Python 的影响。
数组有特定的用处,但是却有一些呆板,所以在 Go 语言的代码里并不是特别常见。
相对的,切片确实随处可见的。它们构建在数组之上并且提供更强大的能力和便捷。
(
数组(x)
切片(√)
)
声明和初始化
数组是具有相同 唯一类型(统一类型) 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构)
数组长度
数组长度必须是一个常量表达式,并且必须是一个非负整数(必须编译时就清楚)
数组长度也是数组类型的一部分,所以 [5]int 和 [10]int 是属于不同类型的
【注意事项】 如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型(参考 第 11 章)。当使用值时我们必须先做一个类型判断(参考 第 11 章)
元素的数目(也称为长度或者数组大小)必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2GB
// var identifier [len]type
var arr1 [5]int //类型依然是最后写,当声明数组时所有的元素都会被自动初始化为默认值 0
数组的容量(容量=长度)不可变,但是值可变
索引溢出
只有有效的索引可以被使用,当使用等于或者大于 len(arr1) 的索引时:如果编译器可以检测到,会给出索引超限的提示信息;如果检测不到的话编译会通过而运行时会 panic():(参考第 13 章)
runtime error: index out of range
两种遍历方式
这两种 for 结构对于切片(slices)(参考 第 7 章)来说也同样适用
package main
func main() {
var arr1 [10]int
for i := 0; i < len(arr1); i++ {
arr1[i] = i
}
for i, _ := range arr1 {
arr1[i] = i
}
}
另一种声明方式
package main
import "fmt"
func main() {
a := [...]string{"a", "b", "c", "d"}
for i := range a {
fmt.Println("Array item", i, "is", a[i])
}
b := []string{"a", "b", "c", "d"}//注:初始化得到的实际上是切片slice
for i := range b {
fmt.Println("Array item2", i, "is", b[i])
}
}
值类型
Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建: var arr1 = new([5]int)
var arr1 = new([5]int) 和 var arr2 [5]int 的区别是什么呢?
arr1 的类型是 *[5]int,而 arr2 的类型是 [5]int
当把一个数组赋值给另一个时,需要再做一次数组内存的拷贝(值)操作
arr2 := *arr1// arr2成了arr1的副本,因为是值拷贝,改 arr2 不会对 arr1 生效
arr2[2] = 100
不管是哪种声明方式,都可以使用索引访问值
package main
import "fmt"
func main() {
a := new([3]int)// 不可以使用 a:=[10]int
var b [3]int
for i, _ := range a {
a[i] = i
b[i] = i
}
for i, _ := range a {
fmt.Println(a[i])
fmt.Println(b[i])
}
}
输出:
0
0
1
1
2
2
数组作函数参数
所以在函数中数组作为参数传入时,如 func1(arr2),会产生一次数组拷贝,func1() 方法不会修改原始的数组 arr2,要想对原数组有效,要使用 & 操作符以引用方式传过来,当然了,参数也要相应变成指针类型。
package main
import "fmt"
func f(a [3]int) { fmt.Println(a) }
func fp(a *[3]int) { fmt.Println(a) }
func main() {
var ar [3]int
f(ar) // passes a copy of ar
fp(&ar) // passes a pointer to ar
}
另一种方法就是生成数组切片并将其传递给函数(详见第 7.1.4 节)
练习1 证明当数组赋值时,发生了数组内存拷贝
package main
import "fmt"
func main() {
arr1 := new([5]int)
var arr2 [5]int
arr3 := *arr1
arr4 := arr2
arr3[0] = 100
arr4[0] = 100
fmt.Printf("arr1[0]=%d arr2[0]=%d arr3[0]=%d arr4[0]=%d", arr1[0], arr2[0], arr3[0], arr4[0])
}
输出:arr1[0]=0 arr2[0]=0 arr3[0]=100 arr4[0]=100
通过 数组常量 的方法来初始化数组
(貌似这种方式不可以使用new())
package main
import "fmt"
func main() {
// var arrAge = [5]int{18, 20, 15, 22, 16}// 常量数不得超过5
// var arrLazy = [...]int{5, 6, 7, 8, 22}// 常量数可以很多
// var arrLazy = []int{5, 6, 7, 8, 22} //注:初始化得到的实际上是切片slice
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}// 只有索引 3 和 4 被赋予实际的值,其他元素都被设置为空的字符串
// var arrKeyValue = []string{3: "Chris", 4: "Ron"} //注:初始化得到的实际上是切片slice,可以写为 []string{3: "Chris", 4: "Ron"}
for i:=0; i < len(arrKeyValue); i++ {
fmt.Printf("Person at %d is %s\n", i, arrKeyValue[i])
}
}
package main
import "fmt"
func main() {
var arr1 = [5]string{1: "hello"}
for i, _ := range arr1 {
fmt.Printf("%d:%s", i, arr1[i])
}
}
输出:0:1:hello2:3:4:
取数组常量 创建 新实例,并且是通过指针的方式
package main
import "fmt"
func fp(a *[3]int) { fmt.Println(a) } // 输出的是地址
func main() {
for i := 0; i < 3; i++ {
fp(&[3]int{i, i * i, i * i * i})
}
}
几何点(或者数学向量)是一个使用数组的经典例子。为了简化代码通常使用一个别名
type Vector3D [3]float32
var vec Vector3D
多维数组
内部数组总是长度相同的。Go 语言的多维数组是矩形式的(唯一的例外是切片的数组,参见第 7.2.5 节。
取值方式为 先行再列
package main
import "fmt"
func main() {
// Step 1: 创建数组
values := [][]int{}
// Step 2: 使用 append() 函数向空的二维数组添加两行一维数组
row1 := []int{1, 2, 3}
row2 := []int{4, 5, 6}
values = append(values, row1)
values = append(values, row2)
// Step 3: 显示两行数据
fmt.Println("Row 1")
fmt.Println(values[0])
fmt.Println("Row 2")
fmt.Println(values[1])
fmt.Println(values[0][0], values[0][1])
}
package main
import "fmt"
const (
HEIGHT = 10 // 行
WIDTH = 5 // 列
)
type pixel int
var screen [HEIGHT][WIDTH]pixel
func main() {
var i pixel
for x := 0; x < HEIGHT; x++ {
for y := 0; y < WIDTH; y++ {
screen[x][y] = i
fmt.Printf("%3d ", i)
i++
}
fmt.Printf("\n")
}
fmt.Printf("%d %d", screen[1][2], screen[2][1])
}
输出
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
25 26 27 28 29
30 31 32 33 34
35 36 37 38 39
40 41 42 43 44
45 46 47 48 49
7 11
将大数组传递给函数
把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种情况:
1.传递数组的指针
2.使用数组的切片
方式1
package main
import "fmt"
func main() {
array := [3]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
// to pass a pointer to the array
fmt.Printf("The sum of the array is: %f", x)
}
func Sum(a *[3]float64) (sum float64) {
for _, v := range a { // derefencing *a to get back to the array is not necessary!
sum += v
}
return
}
但这在 Go 中并不常用,通常使用切片(参考 第 7.2 节)
切片
切片 (slice) 是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。(对数组的连续片段引用、不包括终点)
切片是一个 长度可变的数组
len() 函数获取切片的长度,cap()函数计算容量,可以测量切片最长可以达到多少(最长能切多少),它等于切片的长度 + 数组除切片之外的长度。切片的长度永远不会超过它的容量,所以对于切片 s 来说该不等式永远成立:0 <= len(s) <= cap(s)
一个切片和相关数组的其他切片是共享存储的
因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用。
声明和初始化
声明 var identifier []type(不需要说明长度)
初始化 var slice1 []type = arr1[start:end] start:end 被称为切片表达式
一个切片在未初始化之前默认为 nil,长度为 0
[:] 前面空的是0,后面空代表len(arr)
var slice1 []type = arr1[:] 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是 arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是:slice1 = &arr1
arr1[:3] 和 arr1[0:3] 相同,包含了从第一个到第三个元素(不包括第四个)。
去掉元素
arr1[2:] 和 arr1[2:len(arr1)] 相同,都不包含数组第一二个元素。
去掉 slice1 的最后一个元素,只要 slice1 = slice1[:len(slice1)-1]
s := []int
一个由数字 1、2、3 组成的切片可以这么生成:s := [3]int{1,2,3}[:](错误,应先用 s := [3]int{1, 2, 3} 生成数组, 再使用 s[:] 转成切片)
甚至更简单的 s := []int{1,2,3} (因为数组是有长度的,通过[]内有无数字可以判断是数组还是切片)
s2 := s[:] 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组(切片只指向相关数组,切片不指向切片)
一个切片 s 可以这样扩展到它的大小上限:s = s[:cap(s)],如果再扩大的话就会导致运行时错误(参见第 7.7 节)
以下状态成立
s == s[:i] + s[i:] // i是一个整数且: 0 <= i <= len(s)
len(s) <= cap(s)
切片在内存中的组织方式
切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。
package main
import "fmt"
func main() {
var arr [6]int
var slice1 []int = arr[:]
var slice2 []int = arr[1:]
var slice3 []int = arr[:len(arr)-1]
var slice4 []int = arr[0:4]
var slice5 []int = arr[1:4]
var slice6 []int = slice4[1:3]
var slice7 []int = slice5[1:3]
var slice8 []int = slice4[0:6]
var slice9 []int = slice4[0:6]
for i := 0; i < len(arr); i++ {
arr[i] = i
}
fmt.Println(slice4[0], slice6[0])
//fmt.Println(slice4[5]) // index out of range [5] with length 4
fmt.Println(len(slice1), cap(slice1))
fmt.Println(len(slice2), cap(slice2))
fmt.Println(len(slice3), cap(slice3))
fmt.Println(len(slice4), cap(slice4))
fmt.Println(len(slice5), cap(slice5))
fmt.Println(len(slice6), cap(slice6))
fmt.Println(len(slice7), cap(slice7))
fmt.Println(len(slice8), cap(slice8))
fmt.Println(len(slice9), cap(slice9))
}
0 1 5
6 6
5 5
5 6
4 6
3 5
2 5
2 4
6 6
6 6
1.切片的指针指向 一个相关数组,且具体指向在该数组实际开始取值的位置
由于slice6是基于slice4的,因此[0]的值是叠加slice4的影响而取到相关数组的值的
2.切片长度是切片自己的,算法为:切片最大索引 - 左边起始索引
【注意】左边起始索引要就是字面索引,直接减即可
3.容量其实是相关数组的长度(数组的长度=容量,在声明时已经固定),这是切片长度最长的上限,切片的容量算法为:
容量 = 数组的最大索引 - 左边起始索引 + 1 = len(arr) - 左边起始索引
【注意】左边起始索引要根据实际确定,如slice7是基于slice5确定左边起始索引的,仍然是要注意叠加影响
根据我的实验,切片slice7虽基于slice5,但是不受slice5限制(即7的起始基于5,但是7的终止不限制于5),因为不论slice7还是slice5最终都指向同一相关数组,绝对不要用指针指向切片,切片本身已经是一个引用类型,所以它本身就是一个指针!!
切切片时,只可以往后移
如果 s2 是一个切片,你可以将 s2 向后移动一位 s2 = s2[1:],但是末尾没有移动。切片只能向后移动,s2 = s2[-1:] 会导致编译错误。切片不能被重新分片以获取数组的前一个元素
将切片传递给函数
func sum(a []int) int {
s := 0
for i := 0; i < len(a); i++ {
s += a[i]
}
return s
}
func main() {
var arr = [5]int{0, 1, 2, 3, 4}
sum(arr[:])
}
用 make() 创建一个切片
当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片,同时创建好相关数组:var slice1 []type = make([]type, len) 或 slice1 := make([]type, len)(make先创建数组,再创建切片)
func make([]T, len, cap) 接受 3 个参数,cap 是可选参数:默认len()==cap()
make([]int, 50, 100) // 相当于下面
new([100]int)[0:50]
因为字符串是纯粹不可变的字节数组,它们也可以被切分成切片
略
new() 和 make() 的区别
看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型
不可以使用a:=[10]int,而必须使用a := new([10]int)
var p *[]int = new([]int) // *p == nil; with len and cap 0
或
p := new([]int)
这只是创建一个p指针,指向一个切片,但是切片指向谁,len、cap是多少都不确定
var v []int = make([]int, 10, 50)
或
v := make([]int, 10, 50)
创建一个数组,又创建一个切片,切片指向数组,并默认设置len、cap
【译者注】
1.slice、map 以及 channel 都是 golang 内建的一种引用类型,三者在内存中存在多个组成部分, 需要对内存组成部分初始化后才能使用,而 make 就是对三者进行初始化的一种操作方式
- new 获取的是存储指定变量内存地址的一个变量(存储地址的变量,即指针),对于变量内部结构并不会执行相应的初始化操作, 所以 slice、map、channel 需要 make 进行初始化并获取对应的内存地址,而非 new 简单的获取内存地址
(我的理解是,new只申请了一个切片的内存,但是指向哪个相关数组不清楚,是空引用的指针,除非像数组一样指定内存大小[10],但是切片没办法指定,因此需要初始化,创建数组可以用new,因为只要申请内存即可)
总结:值类型用new创建,引用类型用make创建
多维切片
和数组一样,切片通常也是一维的,但是也可以由一维组合成高维。通过分片的分片(或者切片的数组),长度可以任意动态变化,所以 Go 语言的多维切片可以任意切分。而且,内层的切片必须单独分配(通过 make() 函数)
取值方式为 先行再列
package main
import "fmt"
func main() {
s := make([][]int, 2)
fmt.Println(len(s), cap(s), &s[0])
// fmt.Println( &s[0][0]) // error: index out of range [0] with length 0
s[0] = []int{1, 2, 3}
fmt.Println(len(s[0]), cap(s[0]), &s[0], &s[0][0])
s[1] = make([]int, 3, 5)
fmt.Println(len(s[1]), cap(s[1]), &s[1], &s[1][0])
for row := range s {
for column := range s[row] {
s[row][column] = 1
}
}
for row := range s {
for column := range s[row] {
fmt.Println(s[row][column])
}
}
}
bytes 包
byte你可以理解为就是ASCII字符
类型 []byte 的切片十分常见,Go 语言有一个 bytes 包专门用来提供这种类型的操作方法
bytes 包和字符串包十分类似(参见第 4.7 节)。因为一个ASCII使用一个字节。而且它还包含一个十分有用的类型 Buffer,读写长度未知的 bytes 最好使用 buffer。
Buffer 可以这样定义
var buffer bytes.Buffer
或者使用 new() 获得一个指针:
var r *bytes.Buffer = new(bytes.Buffer)
或者通过函数:func NewBuffer(buf []byte) *Buffer,创建一个 Buffer 对象并且用 buf 初始化好;NewBuffer 最好用在从 buf 读取的时候使用【读取】
通过 buffer 串联字符串
这种实现方式比使用 += 要更节省内存和 CPU,尤其是要串联的字符串数目特别多的时候
var buffer bytes.Buffer
for {
if s, ok := getNextString(); ok { //method getNextString() not shown here
buffer.WriteString(s)
} else {
break
}
}
fmt.Print(buffer.String(), "\n")
给定切片 sl,将一个 []byte 数组追加到 sl 后面
package main
import (
"fmt"
)
func main() {
s1 := []byte{'h', 'e', 'l', 'l', 'o'}
s2 := [4]byte{'w', 'o', 'r', 'd'}
for _, v := range s2 {
s1 = append(s1, v)
}
fmt.Printf("%s", s1)
}
如果是两个切片,直接用...打散
package main
import (
"fmt"
)
func main() {
s1 := []byte{'h', 'e', 'l', 'l', 'o'}
s2 := []byte{'w', 'o', 'r', 'd'}
s1 = append(s1, s2...)
fmt.Printf("%s", s1)
}
把一个缓存 buf 分片成两个切片,第一个是前 n 个 bytes,后一个是剩余的,用一行代码实现
package main
import (
"bytes"
"fmt"
)
func main() {
n := 5
var buffer bytes.Buffer
buffer.WriteString("HelloWorld")
slice1, slice2 := buffer.Bytes()[:n], buffer.Bytes()[n:] //核心一行
fmt.Printf("%s %s", slice1, slice2)
}
For-range 结构和切片
这种构建方法可以应用于数组和切片:
for ix, value := range slice1 {
...
}
第一个返回值 ix 是数组或者切片的索引,第二个是在该索引位置的值;他们都是仅在 for 循环内部可见的局部变量
package main
import "fmt"
func main() {
var slice1 []int = make([]int, 4)
slice1[0] = 1
slice1[1] = 2
slice1[2] = 3
slice1[3] = 4
for ix, value := range slice1 {
fmt.Printf("Slice at %d is: %d %d\n", ix, value, slice1[ix])
}
}
多维切片下的 for-range
range只返回index
package main
import "fmt"
func main() {
s := make([][]int, 2)
fmt.Println(len(s), cap(s), &s[0])
// fmt.Println( &s[0][0]) // error: index out of range [0] with length 0
s[0] = []int{1, 2, 3}
fmt.Println(len(s[0]), cap(s[0]), &s[0], &s[0][0])
s[1] = make([]int, 3, 5)
fmt.Println(len(s[1]), cap(s[1]), &s[1], &s[1][0])
for row := range s {
for column := range s[row] {
s[row][column] = 1
}
}
for row := range s {
for column := range s[row] {
fmt.Println(s[row][column])
}
}
}
输出:
2 2 &[]
3 3 &[1 2 3] 0xc000016018
3 5 &[0 0 0] 0xc000100030
1
1
1
1
1
1
假设我们有如下数组:items := [...]int{10, 20, 30, 40, 50},
a) 如果我们写了如下的 for 循环,那么执行完 for 循环后的 items 的值是多少?
b) 如果 a) 无法正常工作,写一个 for 循环让值可以变成自身的两倍。
a) :
package main
import "fmt"
func main() {
items := [...]int{10, 20, 30, 40, 50} // 数组
for _, item := range items {
item *= 2
}
fmt.Printf("%d", items)
}
b) :
package main
import "fmt"
func main() {
items := [...]int{10, 20, 30, 40, 50} // 数组
for ix, _ := range items {
items[ix] *= 2
}
fmt.Printf("%d", items)
}
通过使用省略号操作符 ... 来实现累加方法
package main
import (
"fmt"
)
func main() {
s1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
total := sum(s1...)
fmt.Printf("%d", total)
}
func sum(a ...int) (total int) {
for i := range a {
total += i
}
return
}
写一个 Sum() 函数,传入参数为一个 float32 数组成的数组 arrF,返回该数组的所有数字和。
如果把数组修改为切片的话代码要做怎样的修改?如果用切片形式方法实现不同长度数组的的和呢?
实践证明,数组和切片不会自动转换,因为C函数数组参数实际是一个指针,传数组会自动转换成指针
package main
import "fmt"
func main() {
var a = [4]float32{1.0, 2.0, 3.0, 4.0}
var a2 = [5]float32{1.0, 2.0, 3.0, 4.0, 5.0}
fmt.Printf("%f %f %f \n", Sum(a), Sum2(a[:]), Sum2(a2[:]))
}
func Sum(a [4]float32) (sum float32) {
for _, item := range a {
sum += item
}
return
}
func Sum2(a []float32) (sum float32) {
for _, item := range a {
sum += item
}
return
}
写一个 SumAndAverage() 方法,返回两个 int 和 float32 类型的未命名变量的和与平均值。
由于不会自动转类型的特性,每一项加减乘除的类型都要手动保持统一
package main
import (
"fmt"
"strconv"
)
func main() {
var a = []int{1.0, 2.0, 3.0, 4.0}
var b = []float32{1.0, 2.0, 3.0, 4.0}
sum, avg := SumAndAverage(a)
fmt.Println(strconv.Itoa(sum), strconv.FormatFloat(float64(avg), 'f', 2, 32))
sum2, avg2 := SumAndAverage2(b)
fmt.Println(fmt.Sprintf("%f", sum2), strconv.FormatFloat(float64(avg2), 'f', 2, 32))
}
func SumAndAverage(a []int) (int, float32) {
sum := 0
for _, item := range a {
sum += item
}
return sum, float32(sum / len(a))
}
func SumAndAverage2(a []float32) (float32, float32) {
var sum float32
for _, item := range a {
sum += item
}
return sum, float32(sum / float32(len(a)))
}
切片重组 (reslice)
说白了,就是改一些开头和结尾
slice1 := make([]type, start_length, capacity)
其中 start_length 作为切片初始长度而 capacity 作为相关数组的长度。
这么做的好处是我们的切片在达到容量上限后可以扩容。改变切片长度的过程称之为切片重组 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)
拓展1位
sl = sl[0:len(sl)+1]
反复拓展直到到达数组容量
package main
import "fmt"
func main() {
slice1 := make([]int, 0, 10)
// load the slice, cap(slice1) is 10:
for i := 0; i < cap(slice1); i++ {
slice1 = slice1[0:i+1] // 实际上,切片的终点是10,而不是9
slice1[i] = i
fmt.Printf("The length of slice is %d\n", len(slice1))
}
// print the slice:
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
}
package main
import "fmt"
func main() {
//a := [10]int // error
a := new([10]int)
slice1 := a[0:10]
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
}
package main
import "fmt"
func main() {
var a = [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice1 = a[5:7] // reference to subarray {5,6} - len(a) is 2 and cap(a) is 5
fmt.Printf("%d %d\n", len(slice1), cap(slice1))
slice1 = slice1[0:4]// 起始基于5,终止不限制于7
fmt.Printf("%d %d\n", len(slice1), cap(slice1))
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
}
2 5
4 5
Slice at 0 is 5
Slice at 1 is 6
Slice at 2 is 7
Slice at 3 is 8
切片的复制与追加
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来
copy()和append()的使用
package main
import "fmt"
func main() {
slFrom := []int{1, 2, 3}
slTo := make([]int, 10)
n := copy(slTo, slFrom)
fmt.Println(slTo)
fmt.Printf("Copied %d elements\n", n) // n == 3
sl3 := []int{1, 2, 3}
sl3 = append(sl3, 4, 5, 6)
fmt.Println(sl3)
}
返回的切片的相关数组可能不是原来的
func append(s[]T, x ...T) []T
类型都是T,即追加的元素必须和原切片的元素是同类型
如果 s 的容量不足以存储新增元素,append() 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。append() 方法总是返回成功,除非系统内存耗尽了。
切片追加切片
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
b := make([]int, 5)
c := append(a, b...)
fmt.Println(c)
}
实现一个AppendByte() ?
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
func copy(dst, src []T) int 方法将类型为 T 的切片从源地址 src 拷贝到目标地址 dst,覆盖 dst 的相关元素,并且返回拷贝的元素个数(所谓拷贝个数,就是覆盖数)
package main
import "fmt"
func main() {
a := []int{1}
b := []int{7, 8, 9, 10, 11, 12}
n := copy(a, b)
fmt.Println(n, b, a)
fmt.Println(n, a, b)
}
1 [7 8 9 10 11 12] [7]
1 [7] [7 8 9 10 11 12]
练习题
切片扩容
给定一个切片 s []int 和一个 int 类型的因子 factor,扩展 s 使其长度为 len(s) * factor
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
b := enlarge(a, 2)
fmt.Println(cap(a), cap(b))
}
// 切片扩容
// 绝对不要用指针指向切片。切片本身已经是一个引用类型,所以它本身就是一个指针
func enlarge(s []int, factor int) (s1 []int) {
s1 = make([]int, len(s), len(s)*factor)
copy(s1, s)
return
}
实现一个Filter
s 是前 10 个整型的切片。构造一个函数 Filter,第一个参数是 s,第二个参数是一个 fn func(int) bool,返回满足函数 fn 的元素切片。通过 fn 测试方法测试当整型值是偶数时的情况。
// filter_slice.go
package main
import (
"fmt"
)
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s = Filter(s, even)
fmt.Println(s)
}
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, i := range s {
if fn(i) {
//通过append实现动态长度的切片,代替动态数组
//因为容量不足以存储新增元素时,append会创建新的相关数组和切片进行扩容
p = append(p, i)
}
}
return p
}
func even(n int) bool {
if n%2 == 0 {
return true
}
return false
}
切片s1指定位置插入切片s2
写一个函数 InsertStringSlice() 将切片插入到另一个切片的指定位置。
写一个函数 RemoveStringSlice() 将从 start 到 end 索引的元素从切片中移除
package main
import "fmt"
func main() {
s1 := []byte{'0', '1', '2', '3', '4'}
s2 := []byte{'5', '6', '7', '8', '9'}
s3 := InsertStringSlice(s1, s2, 2)
s4 := RemoveStringSlice(s3, 1, 2)// 不包括结束
fmt.Printf("%s %s", s3, s4)
}
//切片s1指定位置插入切片s2,位置是0开始
func InsertStringSlice(s1 []byte, s2 []byte, i int) (s3 []byte) {
s3 = make([]byte, len(s1)+len(s2))
copy(s3, s1)
copy(s3[i:], s2)
copy(s3[i+len(s2):], s1[i:]) //i不用+1,因为i代表的下标位开始,0开始
return
}
// 指定位置删除元素
func RemoveStringSlice(slice []byte, start, end int) []byte {
result := make([]byte, len(slice)-(end-start))
at := copy(result, slice[:start])
copy(result[at:], slice[end:])
return result
}
字符串、数组和切片的应用
从字符串生成字节切片
假设 s 是一个字符串(本质上是一个字节数组),那么就可以直接通过 c := []byte(s) 来获取一个字节的切片 c 。另外,您还可以通过 copy() 函数来达到相同的目的:copy(dst []byte, src string)。
package main
import "fmt"
func main() {
s := "\u00ff\u754c"
s2 := []byte("Hello World!")
for i, c := range s {
fmt.Printf("%d:%c ", i, c)
}
for i, c := range s2 {
fmt.Printf("%d:%c ", i, c)
}
}
输出:
0:ÿ 2:界 0:H 1:e 2:l 3:l 4:o 5: 6:W 7:o 8:r 9:l 10:d 11:!
我们知道,Unicode 字符会占用 2 个字节,有些甚至需要 3 个或者 4 个字节来进行表示。如果发现错误的 UTF8 字符,则该字符会被设置为 U+FFFD 并且索引向前移动一个字节。
您同样可以使用 c := []int32(s) 语法,这样切片中的每个 int 都会包含对应的 Unicode 代码,因为字符串中的每次字符都会对应一个整数(一般unicode不会超过4个字节,因此4*8=32,用int32)。
package main
import "fmt"
func main() {
c := []int32("你好!")
for i, c := range c {
fmt.Printf("%d:%c ", i, c)
}
}
输出:
0:你 1:好 2:!
类似的,您也可以将字符串转换为元素类型为 rune 的切片:r := []rune(s)。(因为rune就是int32)
package main
import "fmt"
func main() {
c := []rune("你好!")
for i, c := range c {
fmt.Printf("%d:%c ", i, c)
}
}
获得字符串中字符的数量
可以通过代码 len([]int32(s)) 来获得字符串中字符的数量,使用 utf8.RuneCountInString(s) 效率会更高一点
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "ancdefg你好!"
c := []rune(s)
fmt.Printf("%d %d ", len(c), utf8.RuneCountInString(s))
}
将一个字符串追加到某一个字节切片的尾部
字符串本质上是一个字节数组,用...打碎后就是字符(byte)
package main
import "fmt"
func main() {
b := []byte("aaaaa")
var s string = "sssssss"
b = append(b, s...)
fmt.Printf("%s", b)
}
获取字符串的某一部分
package main
import "fmt"
func main() {
b := "aaaaa"
fmt.Printf("%s", b[0:1])
}
字符串和切片的内存结构
在内存中,一个字符串实际上是一个双字结构,即一个指向实际数据的指针和记录字符串长度的整数(见图 7.4)。因为指针对用户来说是完全不可见,因此我们可以依旧把字符串看做是一个值类型,也就是一个字符(字节)数组
修改字符串中的某个字符
Go 语言中的字符串是不可变的,因此不可以直接修改字符串中的某个字符,但是可以使用字节数组作中介
package main
import "fmt"
func main() {
s := "hello"
//s[i] = 'D' // error:cannot assign to str[i]
c := []byte(s)
c[0] = 'c'
s2 := string(c) // s2 == "cello"
fmt.Printf("%s", s2)
}
字节数组对比函数
func Compare(a, b[]byte) int {
for i:=0; i < len(a) && i < len(b); i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
// 数组的长度可能不同(若前面没return说明可能是一致,也可能是子串)
switch {
case len(a) < len(b):
return -1
case len(a) > len(b):
return 1
}
return 0 // 数组相等
}
PS:字符串对比
有 strings.Compare(s, s2) 和 == (区分大小写)
搜索及排序切片和数组
标准库提供了 sort 包来实现常见的搜索和排序操作。
int 类型的切片排序
您可以使用 sort 包中的函数 func Ints(a []int) 来实现对 int 类型的切片排序。例如 sort.Ints(arri),其中变量 arri 就是需要被升序排序的数组或切片。
TODO:Go降序怎么做?
检查是否已排序
为了检查某个数组是否已经被排序,可以通过函数 IntsAreSorted(a []int) bool 来检查,如果返回 true 则表示已经被排序
float64 和 字符串元素排序
使用函数 func Float64s(a []float64) 来排序 float64 的元素
使用函数 func Strings(a []string) 排序字符串元素
搜索 int、float64、string
函数返回对应结果的索引值
func SearchInts(a []int, n int) int
func SearchFloat64s(a []float64, x float64) int
func SearchStrings(a []string, x string) int
这就是如何使用 sort 包的方法,我们会在第 11.7 节 对它的细节进行深入,并实现一个属于我们自己的版本
append() 函数常见操作
-
将切片 b 的元素追加到切片 a 之后:a = append(a, b...)
-
复制切片 a 的元素到新的切片 b 上:
b = make([]T, len(a))
copy(b, a)
-
删除位于索引 i 的元素:a = append(a[:i], a[i+1:]...)
若切片a长度为2,a[2:2]是合法的,只是取不到元素而已 -
切除切片 a 中从索引 i 至 j 位置的元素:a = append(a[:i], a[j:]...)
-
为切片 a 扩展 j 个元素长度:a = append(a, make([]T, j)...)
-
在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]...)...)
-
在索引 i 的位置插入长度为 j 的新切片:a = append(a[:i], append(make([]T, j), a[i:]...)...)
-
在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, a[i:]...)...)
-
取出位于切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]
-
将元素 x 追加到切片 a:a = append(a, x)
因此,您可以使用切片和 append() 操作来表示任意可变长度的序列(本质就是使用append的动态长度和切片始终索引的灵活)
切片和垃圾回收
切片的底层指向一个数组,该数组的实际容量可能要大于切片所定义的容量。只有在没有任何切片指向的时候,底层的数组内存才会被释放,这种特性有时会导致程序占用多余的内存
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
问题:这段代码可以顺利运行,但返回的 []byte 指向的底层是整个文件的数据。只要该返回的切片不被释放,垃圾回收器就不能释放整个文件所占用的内存。换句话说,一点点有用的数据却占用了整个文件的内存。
解决:想要避免这个问题,可以通过拷贝我们需要的部分到一个新的切片中:
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))// 创建一个新的相关数组
copy(c, b)
return c
}
正则匹配问题:
事实上,上面这段代码只能找到第一个匹配正则表达式的数字串。要想找到所有的数字,可以尝试下面这段代码:
参考资料:https://www.jianshu.com/p/7bd8324b0870
func FindFileDigits(filename string) []byte {
fileBytes, _ := ioutil.ReadFile(filename)
b := digitRegexp.FindAll(fileBytes, len(fileBytes))// 第二个参数是 只查找前 n 个匹配项,如果 n < 0,则查找所有匹配项
c := make([]byte, 0)
for _, bytes := range b {
c = append(c, bytes...)
}
return c
}
练习
分割字符串
编写一个函数,要求其接受两个参数,原始字符串 str 和分割索引 i,然后返回两个分割后的字符串。
package main
import (
"fmt"
)
func main() {
s := "helloWorld!"
str, str2 := split_string(s, 5)
fmt.Printf("%s|%s", str, str2)
}
func split_string(s string, i int) (str string, str2 string) {
str = s[:i]
str2 = s[i:]
return
}
假设有字符串 str,那么 str[len(str)/2:] + str[:len(str)/2] 的结果是什么?
其实就是字符串头尾颠倒
package main
import (
"fmt"
)
func main() {
s := "aaaabbb"
s2 := "aaaabbbb"
fmt.Printf("%s\n", s[len(s)/2:]+s[:len(s)/2])
fmt.Printf("%s", s2[len(s2)/2:]+s2[:len(s2)/2])
}
编写一个程序,要求能够反转字符串
即将 "Google" 转换成 "elgooG"(提示:使用 []byte 类型的切片)。
解决:
一种思路是使用defer,当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)
package main
import (
"fmt"
)
func main() {
c := []byte("abcdefg")
fmt.Printf("%s", reverse(c))
}
func reverse(c []byte) (str string) {
var c1 = make([]byte, len(c))
for i, v := range c {
c1[len(c)-i-1] = v
}
str = string(c1)
return
}
如果您使用两个切片来实现反转,请再尝试使用一个切片(提示:使用交换法)。
package main
import (
"fmt"
)
func main() {
c := []byte("abcdefg")
fmt.Printf("%s", reverse(c))
}
func reverse(c []byte) (str string) {
for i := 0; i <= len(c)/2; i++ {
var temp byte
temp = c[i]
c[i] = c[len(c)-i-1]
c[len(c)-i-1] = temp
}
str = string(c)
return
}
如果您想要反转 Unicode 编码的字符串,请使用 []int32 类型(rune)的切片。
编写一个程序,要求能够遍历一个字符数组,并将当前字符和前一个字符不相同的字符拷贝至另一个数组
package main
import (
"fmt"
)
func main() {
c := []byte("abcdefg")
c2 := []byte("abcdeff")
fmt.Printf("%s %s", fn(c), fn(c2))
}
func fn(c []byte) (c1 []byte) {
c1 = make([]byte, len(c))
var temp byte
var i int
for _, v := range c {
if temp != v {
c1[i] = v
i++
}
temp = v
}
return
}
冒泡排序
编写一个程序,使用冒泡排序的方法排序一个包含整数的切片
package main
import (
"fmt"
)
func main() {
n := []int{1, 6, 2, 99, 22, 663, 24, 5, 16, 0}
fmt.Printf("%d", bubblesort(n, fn))
}
func bubblesort(n []int, fn func(int, int) bool) (n1 []int) {
var temp int
for i, _ := range n {
for i2 := 0; i2 < len(n)-i-1; i2++ {
if fn(n[i2], n[i2+1]) {
temp = n[i2]
n[i2] = n[i2+1]
n[i2+1] = temp
}
}
}
n1 = n
return
}
func fn(a int, b int) (isMatch bool) {
return a > b
}
map-function
在函数式编程语言中,一个 map-function 是指能够接受一个函数原型和一个列表,并使用列表中的值依次执行函数原型,公式为:map ( F(), (e1,e2, . . . ,en) ) = ( F(e1), F(e2), ... F(en) )。
编写一个函数 mapFunc 要求接受以下 2 个参数:
一个将整数乘以 10 的函数
一个整数列表
最后返回保存运行结果的整数列表。
TODO:由于目前没学interface,先默认实现一个int型的func
package main
import (
"fmt"
)
func main() {
n := []int{1, 6, 2, 99, 22, 663, 24, 5, 16, 0}
fmt.Printf("%d", mapFunc(fn, n))
}
func mapFunc(fn func(int) int, n []int) (n1 []int) {
n1 = make([]int, len(n))
for i, v := range n {
n1[i] = fn(v)
}
return
}
func fn(n int) (n1 int) {
n1 = n * 10
return
}