go学习记录——第六天

For-range结构

这种构建方法可以应用于数组和切片:

1
2
3
for ix, value := range slice1 {
...
}

第一个参数ix是数组或切片的索引,第二个参数value是该索引位置的值。他们均是仅在for循环内的局部变量,value只是slice1某个索引位置的值的一个拷贝,不能用来修改slice1该索引位置的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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\n", ix, value)
}
}

/*output
Slice at 0 is: 1
Slice at 1 is: 2
Slice at 2 is: 3
Slice at 3 is: 4
*/
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
package main

import "fmt"

func main() {
seasons := []string{"Spring", "Summer", "Autumn", "Winter"}

for ix, season := range seasons {
fmt.Printf("Season %d is: %s\n", ix, season)
}

var season string
for _, season = range seasons {
fmt.Printf("%s\n", season)
}

for ix := range seasons {
fmt.Printf("%d ", ix)
}
}

/* output
Season 0 is: Spring
Season 1 is: Summer
Season 2 is: Autumn
Season 3 is: Winter
Spring
Summer
Autumn
Winter
0 1 2 3
*/

第二个例子中可以看到,使用_可以忽略索引。

如果只需要索引,也可以忽略第二个变量。

1
2
3
4
for ix := range seasons {
fmt.Printf("%d", ix)
}
// Output: 0 1 2 3

如果只需要修改seasons[ix]的值,也可以忽略第二个变量。

多维切片下的for-range

通过计算行数和列数就可以对矩阵进行操作,和python的类似,和前面提到的多维数组一样。

1
2
3
4
5
for row := range screen {
for column := range screen[row] {
screen[row][column] = 1
}
}

切片重组(reslice)

我们首先创建一个普通的切片(比相关数组小)

1
slice1 := make([]type, start_length, capacity)

start_length作为初始长度,capacity作为切片的容量。

然后我们就可以在切片达到容量上限后扩容,过程就是reslicing。做法就是slice1 = slice1[0 : end]end就是新的切片的末尾索引。

将切片扩展1位可以这样做

1
sl = sl[0:len(sl)+1]

切片可以反复扩容操作直到占据整个数组。

实例

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
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]
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])
}
}
/*output
The length of slice is 1
The length of slice is 2
The length of slice is 3
The length of slice is 4
The length of slice is 5
The length of slice is 6
The length of slice is 7
The length of slice is 8
The length of slice is 9
The length of slice is 10
Slice at 0 is 0
Slice at 1 is 1
Slice at 2 is 2
Slice at 3 is 3
Slice at 4 is 4
Slice at 5 is 5
Slice at 6 is 6
Slice at 7 is 7
Slice at 8 is 8
Slice at 9 is 9
*/

再举一个例子

1
2
var ar = [10]int{0,1,2,3,4,5,6,7,8,9}
var a = ar[5:7] // reference to subarray {5,6} - len(a) is 2 and cap(a) is 5

我们将a重新分片

1
a = a[0:4] // ref of subarray {5,6,7,8} - len(a) is now 4 but cap(a) is still 5

总结一下,我们对切片进行重组扩容,实际上就是重新创建一个切片,如果是用类似a = a[0 :end]的方式,那就是把a的引用指向了一个新的切片,原本的切片地址就被替换成了现在的切片。也就是说把原来的删了换成新的。

切片的复制与追加

如果想增加切片的容量,我们可以创建一个新的更大的切片并把原来的分片内容都拷贝过来。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
slFrom := []int{1, 2, 3}
slTo := make([]int, 10)

n := copy(slTo, slFrom)
fmt.Println(slTo) // output: [1 2 3 0 0 0 0 0 0 0]
fmt.Printf("Copied %d elements\n", n) // n == 3

sl3 := []int{1, 2, 3}
sl3 = append(sl3, 4, 5, 6)
fmt.Println(sl3) // output: [1 2 3 4 5 6]
}

这个实例中我们使用了copy来拷贝切片,用append来向切片追加新的元素。

func append(s[]T, x ...T)[]T,其中append()方法将0个或多个具有相同类型的s的元素追加到切片后面并返回新的切片;追加的元素必须和原本切片的元素类型相同。如果s的容量不足以存储新增元素,append()会分配新的切片来保证已有切片元素和新增元素的存储。在分配后切片的指针、长度和容量都会被更新。

如果向追加切片y到切片x的后面,我们只需要将第二个参数扩展为一个列表即可。

通常情况下append()是很好用的,但我们如果想掌握整个追加过程,我们就可以使用AppendByte()方法。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
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的相关元素,并返回拷贝元素的个数。源地址可能和目标地址有重叠,拷贝个数就是srcdst长度的最小值。如果src是字符串类型,那么元素类型就是byte。如果想继续使用src就可以在拷贝执行后再使用src = dst

由于这部分内容用的多,写几个练习(我自己写完后对照了一下,然后直接把答案贴上来了)。

练习1:
给定一个切片 s []int 和一个 int 类型的因子 factor,扩展 s 使其长度为 len(s) * factor

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
package main

import "fmt"

var s []int

func main() {
s = []int{1, 2, 3}
fmt.Println("The length of s before enlarging is:", len(s))
fmt.Println(s)
s = enlarge(s, 5)
fmt.Println("The length of s after enlarging is:", len(s))
fmt.Println(s)
}

func enlarge(s []int, factor int) []int {
ns := make([]int, len(s)*factor)
// fmt.Println("The length of ns is:", len(ns))
copy(ns, s)
//fmt.Println(ns)
s = ns
//fmt.Println(s)
//fmt.Println("The length of s after enlarging is:", len(s))
return s
}
/*output
The length of s before enlarging is: 3
[1 2 3]
The length of s after enlarging is: 15
[1 2 3 0 0 0 0 0 0]
*/

练习2:
用顺序函数过滤容器:s 是前 10 个整型的切片。构造一个函数 Filter,第一个参数是 s,第二个参数是一个 fn func(int) bool,返回满足函数 fn 的元素切片。通过 fn 测试方法测试当整型值是偶数时的情况。

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
// 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)
}

// Filter returns a new slice holding only
// the elements of s that satisfy f()
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, i := range s {
if fn(i) {
p = append(p, i)
}
}
return p
}

func even(n int) bool {
if n%2 == 0 {
return true
}
return false
}
/* [0 2 4 6 8] */

练习3:
写一个函数 InsertStringSlice() 将切片插入到另一个切片的指定位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// insert_slice.go
package main

import (
"fmt"
)

func main() {
s := []string{"M", "N", "O", "P", "Q", "R"}
in := []string{"A", "B", "C"}
res := InsertStringSlice(s, in, 0) // at the front
fmt.Println(res) // [A B C M N O P Q R]
res = InsertStringSlice(s, in, 3)
fmt.Println(res) // [M N O A B C P Q R]
}

func InsertStringSlice(slice, insertion []string, index int) []string {
result := make([]string, len(slice)+len(insertion))
at := copy(result, slice[:index])
at += copy(result[at:], insertion)
copy(result[at:], slice[index:])
return result
}

练习4:
写一个函数 RemoveStringSlice() 将从 startend 索引的元素从切片中移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// remove_slice.go
package main

import (
"fmt"
)

func main() {
s := []string{"M", "N", "O", "P", "Q", "R"}
res := RemoveStringSlice(s, 2, 4)
fmt.Println(res) // [M N Q R]
}

func RemoveStringSlice(slice []string, start, end int) []string {
result := make([]string, len(slice)-(end-start))
at := copy(result, slice[:start])
copy(result[at:], slice[end:])
return result
}

字符串、数组和切片的应用

从字符串生成字节切片

假设s是一个字符串(本质上是一个不可变的字节数组),那么可以直接通过c := []byte(s)来获取一个字节的切片。另外还可以通过copy()函数来达到相同的目的copy(dst []byte, src string)

同样也可以使用for-range来获取每个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
s := "\u00ff\u754c"
for i, c := range s {
fmt.Printf("%d:%c ", i, c)
}
}
/*output
0:ÿ 2:界
*/

这里涉及到了Unicode字符,Unicode字符会占两个字节甚至3个或4个字节。如果发现错误的UTF8字符,将会被设置为U+FFFD并且索引会前移一位。和字符串转换一样,可以用c := []int32(s)语法,这样切片中每个int都会包含在对应的Unicode字符中,因为字符中的每个字符都对应一个整数,所以我们也可以将字符串转换为元素类型为rune的切片 r := []rune(s)

可以通过len([]int32(s))来获取字符串字符的数量,但使用utf8.RunCountInString(s)效率会更高

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"unicode/utf8"
)

func main() {
// count number of characters:
str1 := "asSASA ddd dsjkdsjs dk"
fmt.Printf("The number of bytes in string str1 is %d\n", len(str1))
fmt.Printf("The number of characters in string str1 is %d\n", utf8.RuneCountInString(str1))
str2 := "asSASA ddd dsjkdsjsこん dk"
fmt.Printf("The number of bytes in string str2 is %d\n", len(str2))
fmt.Printf("The number of characters in string str2 is %d", utf8.RuneCountInString(str2))
}

/* Output:
The number of bytes in string str1 is 22
The number of characters in string str1 is 22
The number of bytes in string str2 is 28
The number of characters in string str2 is 24
*/

还可以将一个字符串追加到某个字节切片的后面

1
2
3
var b []byte
var s string
b = append(b, s...)

获取字符串的某一部分

使用substr := str[start : end]可以从字符串str获取从索引startend - 1的子字符串。

字符串和切片的内存结构

在内存中,一个字符串实际上是双字结构,即一个指向实际数据的指针一个记录字符串长度的整数。因为指针对用户来说不可见,所以依旧可以把字符串看作一个值类型。

7.6.1

修改字符串中的某个字符

xxxxxxxxxx26 1package main2​3import (4 “fmt”5)6​7func main() {8 // 原始映射9 original := map[string]int{10 “apple”:  1,11 “banana”: 2,12 “cherry”: 3,13 }14​15 // 创建一个新的映射来存储调换后的键值对16 swapped := make(map[int]string)17​18 // 遍历原始映射并调换键值19 for key, value := range original {20 swapped[value] = key // 将原本的值作为新键,原本的键作为新值21 }22​23 // 输出调换后的映射24 fmt.Println(“原始映射:”, original)25 fmt.Println(“调换后的映射:”, swapped)26}go

因此我们需要将字符串换成字节数组,然后再去修改数组中的元素值,最后再将数组转换为字符串。

1
2
3
4
s := "hello"
c := []byte(s)
c[0] = 'c'
s2 := string(c) // s2 == "cello"

字节数组对比函数

Compare() 函数会返回两个字节数组字典顺序的整数对比结果,即
0 if a == b, -1 if a < b, 1 if a > b

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}
}
// 数组的长度可能不同
switch {
case len(a) < len(b):
return -1
case len(a) > len(b):
return 1
}
return 0 // 数组相等
}

搜索及排序切片和数组

标准库中提供了 sort 包来实现常见的搜索和排序操作。(python中sorted万能函数🤣,leetcode只要遇到需要排序的写了就完事,根本不用自己去写排序算法)

sort/search example
func Ints(a []int) sort.Ints(arri)
func Float64s(a []float64) sort.Float64s(arrf)
func Strings(a []string) sort.Strings(arrs)
func Slice(slice interface{}, less func(i, j int) bool) sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })
func SearchInts(a []int, x int) sort.SearchInts(arr, x)
func SearchFloat64s(a []float64, x float64) sort.SearchFloat64s(arr, x)
func SearchStrings(a []string, x string) sort.SearchStrings(arr, x)

这里我们需要注意,想要搜索一个元素,数组或切片必须先进行排序(标准库中默认为二分排序)

append()函数常见操作

append()函数常见操作 example
切片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 中从索引 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)

这边书上推荐了三个能够实现更完整的操作的三个包,我也附上链接(也方便我自己后续去学)Slices, chain, lists

这里再次感谢原书作者 Ivo Balbaert 和中译本的各位老师,以及三个包的开发者 Eleanor McHugh。

切片和垃圾回收

切片的底层指向一个数组,该数组的实际容量可能要大于切片所定义的容量,只有在没有任何切片指向的时候,底层的数组内存才会被释放,这种特性会导致程序产生多余的内存消耗。

实例(函数FindDigits将一个文件加载到内存,然后搜索其中的数字并返回一个切片)

1
2
3
4
5
6
var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}

代码可以正常运行,但返回的 []byte 指向的底层是整个文件的数据。只要该返回的切片不被释放,垃圾回收就不能释放整个文件占据的内存。(这句话看到就想到了之前在不知道哪个论坛看到的一个例子,那个例子中程序运行过程中内存会被完全使用,也就是说即使只调用一个功能也会将整个内存拉取,造成大量的内存浪费,大致内容是这样。但我自己感觉和这个有点类似,都会造成内存浪费,拖慢运行速度。)

避免这个问题可以通过将需要的部分拷贝到一个新的切片再调用

1
2
3
4
5
6
7
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}

上面这个只找到了第一个匹配正则表达式的字符串,要找到全部的用下面这个

1
2
3
4
5
6
7
8
9
func FindFileDigits(filename string) []byte {
  fileBytes, _ := ioutil.ReadFile(filename)
b := digitRegexp.FindAll(fileBytes, len(fileBytes))
c := make([]byte, 0)
for _, bytes := range b {
c = append(c, bytes...)
}
return c
}

go学习记录——第六天
https://www.lx02918.ltd/2024/08/14/go-study-sixth-day/
作者
Seth
发布于
2024年8月14日
许可协议