gopl-zh.github.com/ch1/ch1-03.md

176 lines
9.7 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`的迭代顺序并不确定,從實踐來看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。
继续来看`bufio`包,它使處理輸入和輸出方便又高效。`Scanner`類型是该包最有用的特性之一,它读取输入并将其拆成行或單詞;通常是處理行形式的輸入最簡單的方法。
程序使用短變量聲明創建`bufio.Scanner`类型的变量`input`。
```
input := bufio.NewScanner(os.Stdin)
```
该变量從程序的標準輸入中讀取內容。每次調用`input.Scanner`,即读入下一行,并移除行末的換行符;读取的内容可以调用`input.Text()`得到。`Scan`函数在读到一行时返迴`true`,在无输入时返迴`false`。
类似于C或其它語言里的`printf`函數,`fmt.Printf`函数对一些表达式产生格式化输出。该函数的首個參數是个格式字符串指定后续参数被如何格式化。各個參數的格式取決於“轉換字符”conversion character形式为百分号后跟一個字母。举个例子`%d`表示以十进制形式打印一個整型操作数,而`%s`則表示把字符串型操作数的值展开。
`Printf`有一大堆這種轉換Go程序員称之为*动词verb*。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:
```
%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`。字符串字面上可能含有這些代表不可見字符的**轉義字符escap sequences**。默認情况下,`Printf`不會換行。按照慣例,以字母`f`結尾的格式化函數,如`log.Printf`和`fmt.Errorf`,都采用`fmt.Printf`的格式化准则。而以`ln`結尾的格式化函數,则遵循`Println`的方式,以跟`%v`差不多的方式格式化參數,并在最後添加一個換行符。(译注:后缀`f`指`fomart``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`相反。)
我們略微簡化了`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`、`outil.ReadFile`和`ioutil.WriteFile`都使用`*os.File`的`Read`和`Write`方法但是大多数程序員很少需要直接调用那些低级lower-level函数。高级higher-level函数像`bufio`和`io/ioutil`包中所提供的那些,用起来要容易点。
**練習 1.4** 脩改`dup2`,出现重複的行时打印文件名称。