Go学习——《Go语言程序设计》Chap4

集合类型

Go 语言的指针与 C/C++中的之后怎类似,无论是语法上还是语意上。但是 Go 语言的指针不支持指针运算,这样就消除了在 C/C++程序中一类潜在的 bug。Go 语言也不用 free()函数或者 delete 操作符号,因为 Go 语言有垃圾回收器,并且自动管理内存。

值、指针和引用类型

通常情况下 Go 语言的变量持有相应的值。也就是说,我们可以将一个变量想象成它所持有的值来使用。其中有些例外是对于通道、函数、方法、映射以及切片的引用变量。它们持有的都是引用,也即保存指针的变量

对于字符串,如果修改一个传入的字符串(例如,使用+=操作符),Go 语言必须创建一个新的字符串,并且复制原始的字符串,并将其加到该字符串之后,这对于大字符串来说很可能代价非常大。(实际上,如Chap3里面讲的,更多时候我们会准备一个字符切片[]string,或者用bytes.Buffer对应方法实现)

剩下的指针与 C/C++及其类似,在此不再赘述,需要特别了解的是,虽然 Go 编译器可能在内部将栈和堆的内存区分对待,但是 Go 程序员从不需要担心这些,因为 Go 语言自己会在内部处理好内存管理的事情。

我的测试

1
2
3
4
5
6
7
8
z := 37
pi := &z
ppi := &pi
fmt.Println(z, *pi, **ppi)
**ppi++
fmt.Println(z, *pi, **ppi)
fmt.Println(reflect.TypeOf(pi))
fmt.Println(reflect.TypeOf(ppi))

测试结果

1
2
3
4
37 37 37
38 38 38
*int
**int

对于 swap 等操作(需要对变量本身而非变量的副本进行操作),C/C++的常见思路是将需要操作的原始变量的指针以参数形式传入函数并进行修改,而 Go 提供了一个更人性化的方法,一般而言,对于少量这样的变量,我们仍采用按值传递的方式,然后通过多返回值的设计完成需要,或者,对于大量的,我们会传入一个切片来达到效果。

表示成功与失败,C/C++中习惯传入一个布尔类型指针,Go 语言中直接以最后一个返回值的形式返回一个布尔型的成功标志(或者最好是一个 error 值)的写法更好用。

数组和切片

数组的创建方法

1
2
3
[length]Type
[N]Type{v1,v2,v3,...,vn}
[...]Type{v1,v2,v3,...,vn}

...在这种场景下使用,Go 会自动补全,我们可以理解为定长度的(与后面切片的变长相对应)。

一般而言,Go 语言的切片比数组更加灵活、强大且方便。数组是按值传递的(即传递副本,虽然可以通过传递指针来避免,PS 这么操作的话 Go 比 C/C++还反人类)。

虽然数组和切片所保存的元素类型相同,但在实际使用中并不受此限。这是因为其类型也可以是一个接口。因此我们可以保存任意满足所声明的接口的元素(即它们定义了该接口所需的方法)。然后我们可以让一个数组或者切片为空接口 interface{},这意味着我们可以存储任意类型的元素,不过这导致我们在获取一个元素时需要使用类型断言或者类型转变,或者两者配合使用。

切片的创建方法

1
2
3
4
make([]Type, length, capacity)
make([]Type, length)
[]Type{}
[]Type{value1,value2,...,valueN}

切片创建时会创建一个隐藏的初始化为零值的数组(如果使用第四种方法则是有初始值的)。一个切片的容量即为其隐藏数组的长度,而其长度为不超过容量的任意值。

实际中使用空切片,make()创建会更实用,只需将长度设为 0,并且将容量设为一个我们估计需要保存元素的个数。

切片再切片是引用切片参数也是引用,都是对同一底层数组的引用,其中一个改变会影响到其他所有指向该相同数组的任何其他引用。

二维切片的实验:

使用长度为 3(即包含三个切片和容量为 3(默认容量为其长度)来创建一个切片的切片 grid),我们尝试让内层切片长度不一样。

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

import (
"fmt"
"os"
"strconv"
)

func main() {
// debug参数设定为 ["3","4","5"]
if len(os.Args) < 4 {
fmt.Println("This program needs three int:")
os.Exit(1)
}
grid1 := make([][]int, 3)
for index := 0; index < 3; index++ {
parint, _ := strconv.ParseInt(os.Args[index+1], 10, 0)
grid1[index] = make([]int, parint)
}
fmt.Println(grid1)
grid1[0][0], grid1[1][1], grid1[2][2] = 1, 2, 3
fmt.Println(grid1)
// [[0 0 0] [0 0 0 0] [0 0 0 0 0]]
// [[1 0 0] [0 2 0 0] [0 0 3 0 0]]
// 符合预期
}

遍历切片

for…range 循化,带循环计数器的循环,(”_“)表示丢弃该值

我的实验:

1
2
3
4
5
6
7
8
9
10
grid2 := make([]int, 5)
for _, i := range grid2 {
fmt.Println(i)
i = 2
}
fmt.Println(grid2)
for k, _ := range grid2 {
grid2[k] = 2
}
fmt.Println(grid2)

输出为

1
2
3
4
5
6
7
0
0
0
0
0
[0 0 0 0 0]
[2 2 2 2 2]

发现取到的值是值,改变并不会引起切片的改变,如果我们想要修改切片中的项,我们必须使用可以提供有效切片索引而非仅仅是元素副本的 for 循环,需要区分切片本身引用与 for 循环取值副本的差别。

切片的修改

1
2
3
4
5
6
7
8
s = []string{}
t = []string{"a","b","c","d"}
s = append(s,"h","i","j") //添加多个单一值
s = append(s, t...) //添加t整个切片中所有值
s = append(s, t[1:3]...) // 添加一个子切片所有值
b := []byte{'U','V'}
letters := "WXY"
b = append(b, letters...) //将字符串字节添加到字节且片中

...这时候很像解包,把切片拆解为底层构成元素然后统一放入。

排序和搜索切片

一些常用方法:sort.Float64s(),sort.Ints(),sort.IntsAreSorted()等等。

sort.Sort()函数能够对任意类型进行排序,只需其类型提供了sort.Int结果义的方法,**即只要这些类型根据相应的签名实现了Len(结果 Swap()`**等方法,便可以使用函数排序,下面是自己对结果

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 m结果

import (
"fmt"结果
"sort"
"strings"
)

type FoldedString []string

func (slice FoldedString) Len() int {
return len(slice)
}

func (slice FoldedString) Less(i int, j int) bool {
return strings.ToLower(slice[i]) < strings.ToLower(slice[j])
}

func (slice FoldedString) Swap(i int, j int) {
slice[i], slice[j] = slice[j], slice[i]
}

func main() {
test1 := make(FoldedString, 2)
test1 = append(test1, "who")
test1 = append(test1, "What")
test1 = append(test1, "Answer")
fmt.Println(test1)
sort.Sort(test1)
fmt.Println(test1)
}

sort.Search()提供了一个二分搜索算法的函数。该函数两个参数:长度、与元素比较的函数(必须为闭包,闭包在下一节涉及)。

映射

某些场合也成为散列映射、散列表、无序映射、字典。

映射的操作

语法 含义/结果
make[k] = v 用键 k 将值 v 赋给映射 m。如果映射中存在,抛弃原先值。
Delete(m,k) 将键 k 及其值从映射 m 删除,如果不存在则安全地不进行任何操作。
v:= m[k] 从映射 m 中取得键 k 的值赋给 v,如果不存在则赋类型 0 值
v,found := m[k] v 同上,存在则 found 为 true,否则为 false
len(m) 返回映射 m 中的项数

创建删除等过程和切片很类似(make 方式),时间复杂度直接索引要比切片慢两个数量级(非正式数据)

例子:猜测分割符

例子很简单,二维切片counts [][]int,外层是sepIndex(分割符的 index),内层是lineIndex行数,统计一下内层全相同且不为 0 的就可以了。

例子:词频统计

这个例子里面涉及到 UTF 编码文件的分割和映射反转(多值),所以详细看了下。

处理文件

  • 主函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    func main() {
    if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" {
    fmt.Printf("usage: %s <file1> [<file2> [... <fileN>]]\n",
    filepath.Base(os.Args[0]))
    os.Exit(1)
    }

    frequencyForWord := map[string]int{} // Same as: make(map[string]int)
    for _, filename := range commandLineFiles(os.Args[1:]) {
    updateFrequencies(filename, frequencyForWord)
    }
    reportByWords(frequencyForWord)
    wordsForFrequency := invertStringIntMap(frequencyForWord)
    reportByFrequency(wordsForFrequency)
    }

    主函数描述了基本流程,建立空映射,便利文件处理更新映射。得到第一个映射后就输出第一份报告reportByWords()这是以字符为键的,然后进行映射反转构建以频数为键的多值映射,并输出第二份报告reportByFrequency()

  • Windows 通配符处理函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    func commandLineFiles(files []string) []string {
    if runtime.GOOS == "windows" {
    args := make([]string, 0, len(files))
    for _, name := range files {
    if matches, err := filepath.Glob(name); err != nil {
    args = append(args, name) // Invalid pattern
    } else if matches != nil { // At least one match
    args = append(args, matches...)
    }
    }
    return args
    }
    return files
    }

    由于 cmd 不支持通配符,对传入参数(文件名)进行预处理

  • 文件处理函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func updateFrequencies(filename string, frequencyForWord map[string]int) {
    var file *os.File
    var err error
    if file, err = os.Open(filename); err != nil {
    log.Println("failed to open the file: ", err)
    return
    }
    defer file.Close()
    readAndUpdateFrequencies(bufio.NewReader(file), frequencyForWord)
    }

    打开文件,并用defer让函数返回时关闭文件句柄,并将文件作为*bufferio.Reader传给实际工作函数,实际工作函数readAndUpdateFrequencies()按行读取而不是读取字节流。

  • 实际工作函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func readAndUpdateFrequencies(reader *bufio.Reader,
    frequencyForWord map[string]int) {
    for {
    line, err := reader.ReadString('\n')
    for _, word := range SplitOnNonLetters(strings.TrimSpace(line)) {
    if len(word) > utf8.UTFMax ||
    utf8.RuneCountInString(word) > 1 {
    frequencyForWord[strings.ToLower(word)] += 1
    }
    }
    if err != nil {
    if err != io.EOF {
    log.Println("failed to finish reading the file: ", err)
    }
    break
    }
    }
    }

    一个无限循环一行一行读取文件,出现错误的时候报告并给 log,但不直接退出程序(因为还有其他文件需要处理)。

    按行读取reader.ReadString('\n'),然后内循环处理每行,对单词进行分割并忽略掉非单词的字符。一开始使用strings.TrimeSpace(line)去除行开头和结束的空白。

    为了快速检查,使用 if 的两个分句,首先检查其长度是否大于 UTF 最多需要的字节,如果多,则必为符合要求的 word(因为分割是按照非 unicode 的 letter 分割的),如果少,那么再特判断,检查 rune 的个数。

    • 分割函数

      1
      2
      3
      4
      func SplitOnNonLetters(s string) []string {
      notALetter := func(char rune) bool { return !unicode.IsLetter(char) }
      return strings.FieldsFunc(s, notALetter)
      }

      用于对行进行字符切分,应用的是strings.FieldsFunc,传入原始字符串和 bool 类型返回值的函数,进行多字符切分(字符串一节有涉及),strings.Splitstrings.FieldsFunc详细解释,可以看着一篇Medium:strings.FieldsFunc vs strings.Split

  • 统计、反映射与输出函数

    给出实现,不再分析

    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
    func reportByWords(frequencyForWord map[string]int) {
    words := make([]string, 0, len(frequencyForWord))
    wordWidth, frequencyWidth := 0, 0
    for word, frequency := range frequencyForWord {
    words = append(words, word)
    if width := utf8.RuneCountInString(word); width > wordWidth {
    wordWidth = width
    }
    if width := len(fmt.Sprint(frequency)); width > frequencyWidth {
    frequencyWidth = width
    }
    }
    sort.Strings(words)
    gap := wordWidth + frequencyWidth - len("Word") - len("Frequency")
    fmt.Printf("Word %*s%s\n", gap, " ", "Frequency")
    for _, word := range words {
    fmt.Printf("%-*s %*d\n", wordWidth, word, frequencyWidth,
    frequencyForWord[word])
    }
    }

    func reportByFrequency(wordsForFrequency map[int][]string) {
    frequencies := make([]int, 0, len(wordsForFrequency))
    for frequency := range wordsForFrequency {
    frequencies = append(frequencies, frequency)
    }
    sort.Ints(frequencies)
    width := len(fmt.Sprint(frequencies[len(frequencies)-1]))
    fmt.Println("Frequency → Words")
    for _, frequency := range frequencies {
    words := wordsForFrequency[frequency]
    sort.Strings(words)
    fmt.Printf("%*d %s\n", width, frequency, strings.Join(words, ", "))
    }
    }

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!