gopl-zh.github.com/ch1/ch1-03.md
关胜 523e81ec43
Update ch1-03.md
如果在windows下默认换行可能是\r\n,如果仍然使用\n最后没有空行,其他行分割后都多\r,最后一行分割后没有\r将会计数错误
2024-05-28 19:12:37 +08:00

181 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 1.3. 查找重复的行
对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为 `dup` 的程序的三个版本;灵感来自于 Unix 的 `uniq` 命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。
`dup` 的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入 `if` 语句,`map` 数据类型以及 `bufio` 包。
<u><i>gopl.io/ch1/dup1</i></u>
```go
// Dup1 prints the text of each line that appears more than
// once in the standard input, preceded by its count.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
```
正如 `for` 循环一样,`if` 语句条件两边也不加括号,但是主体部分需要加。`if` 语句的 `else` 部分是可选的,在 `if` 的条件为 `false` 时执行。
**map** 存储了键/值key/value的集合对集合元素提供常数时间的存、取或测试操作。键可以是任意类型只要其值能用 `==` 运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数 `make` 创建空 `map`此外它还有别的作用。4.3 节讨论 `map`
(译注:从功能和实现上说,`Go` 的 `map` 类似于 `Java` 语言中的 `HashMap`Python 语言中的 `dict``Lua` 语言中的 `table`,通常使用 `hash` 实现。遗憾的是,对于该词的翻译并不统一,数学界术语为*映射*,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。)
每次 `dup` 读取一行输入,该行被当做键存入 `map`,其对应的值递增。`counts[input.Text()]++` 语句等价下面两句:
```go
line := input.Text()
counts[line] = counts[line] + 1
```
`map` 中不含某个键时不用担心,首次读到新行时,等号右边的表达式 `counts[line]` 的值将被计算为其类型的零值,对于 `int``0`
为了打印结果,我们使用了基于 `range` 的循环,并在 `counts` 这个 `map` 上迭代。跟之前类似,每次迭代得到两个结果,键和其在 `map` 中对应的值。`map` 的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。(译注:具体可以参见这里[https://stackoverflow.com/questions/11853396/google-go-lang-assignment-order](https://stackoverflow.com/questions/11853396/google-go-lang-assignment-order)
继续来看 `bufio` 包,它使处理输入和输出方便又高效。`Scanner` 类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。
程序使用短变量声明创建 `bufio.Scanner` 类型的变量 `input`
```go
input := bufio.NewScanner(os.Stdin)
```
该变量从程序的标准输入中读取内容。每次调用 `input.Scan()`,即读入下一行,并移除行末的换行符;读取的内容可以调用 `input.Text()` 得到。`Scan` 函数在读到一行时返回 `true`,不再有输入时返回 `false`
类似于 C 或其它语言里的 `printf` 函数,`fmt.Printf` 函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串指定后续参数被如何格式化。各个参数的格式取决于“转换字符”conversion character形式为百分号后跟一个字母。举个例子`%d` 表示以十进制形式打印一个整型操作数,而 `%s` 则表示把字符串型操作数的值展开。
`Printf` 有一大堆这种转换Go程序员称之为*动词verb*。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:
```text
%d 十进制整数
%x, %o, %b 十六进制,八进制,二进制整数。
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔true或false
%c 字符rune (Unicode码点)
%s 字符串
%q 带双引号的字符串"abc"或带单引号的字符'c'
%v 变量的自然形式natural format
%T 变量的类型
%% 字面上的百分号标志(无操作数)
```
`dup1` 的格式字符串中还含有制表符`\t`和换行符`\n`。字符串字面上可能含有这些代表不可见字符的**转义字符escape sequences**。默认情况下,`Printf` 不会换行。按照惯例,以字母 `f` 结尾的格式化函数,如 `log.Printf``fmt.Errorf`,都采用 `fmt.Printf` 的格式化准则。而以 `ln` 结尾的格式化函数,则遵循 `Println` 的方式,以跟 `%v` 差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀 `f``format``ln` 指 `line`。)
很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。`dup` 程序的下个版本读取标准输入或是使用 `os.Open` 打开各个具名文件,并操作它们。
<u><i>gopl.io/ch1/dup2</i></u>
```go
// Dup2 prints the count and text of lines that appear more than once
// in the input. It reads from stdin or from a list of named files.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
func countLines(f *os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
}
```
`os.Open` 函数返回两个值。第一个值是被打开的文件(`*os.File`),其后被 `Scanner` 读取。
`os.Open` 返回的第二个值是内置 `error` 类型的值。如果 `err` 等于内置值`nil`(译注:相当于其它语言里的 `NULL`),那么文件被成功打开。读取文件,直到文件结束,然后调用 `Close` 关闭该文件,并释放占用的所有资源。相反的话,如果 `err` 的值不是 `nil`,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。我们的错误处理非常简单,只是使用 `Fprintf` 与表示任意类型默认格式值的动词 `%v`,向标准错误流打印一条信息,然后 `dup` 继续处理下一个文件;`continue` 语句直接跳到 `for` 循环的下个迭代开始执行。
为了使示例代码保持合理的大小,本书开始的一些示例有意简化了错误处理,显而易见的是,应该检查 `os.Open` 返回的错误值,然而,使用 `input.Scan` 读取文件过程中不大可能出现错误因此我们忽略了错误处理。我们会在跳过错误检查的地方做说明。5.4 节中深入介绍错误处理。
注意 `countLines` 函数在其声明前被调用。函数和包级别的变量package-level entities可以任意顺序声明并不影响其被调用。译注最好还是遵循一定的规范
`map` 是一个由 `make` 函数创建的数据结构的引用。`map` 作为参数传递给某函数时该函数接收这个引用的一份拷贝copy或译为副本被调用函数对 `map` 底层数据结构的任何修改,调用者函数都可以通过持有的 `map` 引用看到。在我们的例子中,`countLines` 函数向 `counts` 插入的值,也会被 `main` 函数看到。(译注:类似于 C++ 里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)
`dup` 的前两个版本以"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,`dup3`,就是这么操作的。这个例子引入了 `ReadFile` 函数(来自于`io/ioutil`包),其读取指定文件的全部内容,`strings.Split` 函数把字符串分割成子串的切片。(`Split` 的作用与前文提到的 `strings.Join` 相反。请注意如果在Windows下测试注意换行是否为\r\n否则最后一行是否有空行将会影响结果。
我们略微简化了 `dup3`。首先,由于 `ReadFile` 函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回 `main` 函数。
<u><i>gopl.io/ch1/dup3</i></u>
```go
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
func main() {
counts := make(map[string]int)
for _, filename := range os.Args[1:] {
data, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
continue
}
for _, line := range strings.Split(string(data), "\n") {
counts[line]++
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
```
`ReadFile` 函数返回一个字节切片byte slice必须把它转换为 `string`,才能用 `strings.Split` 分割。我们会在3.5.4 节详细讲解字符串和字节切片。
实现上,`bufio.Scanner`、`ioutil.ReadFile` 和 `ioutil.WriteFile` 都使用 `*os.File``Read``Write` 方法但是大多数程序员很少需要直接调用那些低级lower-level函数。高级higher-level函数`bufio``io/ioutil` 包中所提供的那些,用起来要容易点。
-----
**练习 1.4** 修改 `dup2`,出现重复的行时打印文件名称。