Files
gopl-zh.github.com/ch1/ch1-03.md
2016-01-29 01:40:10 +08:00

174 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`創建空映射此外它還有别的作用。4.3节讨论映射。
每次`dup`讀取一行輸入,该行被當做映射的一个键,其对应的值递增。`counts[input.Text()]++`語句等價下面兩句:
```go
line := input.Text()
counts[line] = counts[line] + 1
```
映射不含某個键時不用擔心,首次遇到新行时,等号右边的表达式`counts[line]`的值将被计算为其类型的零值对于int`即0。
为了打印结果,我们使用了基于`range`的循环,并在`counts`这个映射上迭代。跟之前类似,每次迭代得到两个结果,键和其在映射中对应的值。映射的迭代順序并不確定,從實踐來看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。
继续来看`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 16進製8進製2進製整数。
%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`差不多的方式格式化參數,并在最後添加一個換行符。
很多程序要么從標準輸入中讀取數據,如上面的例子所示,要么从一系列具名文件中讀取數據。`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返迴的第二個值是一個Go語言內置的error類型。如果這個error和內置值的nil譯註相當於其它語言里的NULL相等的話説明文件被成功的打開了。之後文件被讀取一直到文件的最後文件的Close方法關閉該文件併釋放占用的一切資源。如果err的值不是nil的話那説明在打開文件的時候出了某種錯誤。這種情況下error類型的值會描述具體的問題。我們例子里的簡單錯誤處理會在標準錯誤流中用Fprintf和%v來格式化該錯誤字符串。然後繼續處理下一個文件continue語句會直接跳過之後的語句直接開始執行下一個循環迭代。
我們在本書早期的例子中做了比較詳盡的錯誤處理當然了在實際編碼過程中像os.Open這類的函數是一定要檢査其返迴的error值的爲了減少例子程序的代碼量我們姑且簡化掉這些不太可能返迴錯誤的處理邏輯。後面的例子里我們會跳過錯誤檢査。在5.4節中我們會對錯誤處理做更詳細的闡述。
讀者可以再觀察一下上面的例子countLines函數是在其聲明之前就被調用了。在Go語言里函數和包級别的變量可以以任意的順序被聲明併不影響其被調用。譯註最好還是遵循一定的規范
再來講講map這個數據結構map是用make函數創建的數據結構的一個引用。當一個map被作爲參數傳遞給一個函數時函數接收到的是一份引用的拷貝雖然本身併不是一個東西但因爲他們指向的是同一塊數據對象譯註類似於C++里的引用傳遞實際上指針是另一個指針了但內部存的值指向同一塊內存所以你在函數里對map里的值進行脩改時原始的map內的值也會改變。在我們的例子中我們在countLines函數中插入到counts這個map里的值在主函數中也是看得到的。
上面這個版本的dup是以流的形式來處理輸入併將其打散爲行。理論上這些程序也是可以以二進製形式來處理輸入的。我們也可以一次性的把整個輸入內容全部讀到內存中然後再把其分割爲多行然後再去處理這些行內的數據。下面的dup3這個例子就是以這種形式來進行操作的。這個例子引入了一個新函數ReadFile從io/ioutil包提供這個函數會把一個指定名字的文件內容一次性調入之後我們用strings.Split函數把文件分割爲多個子字符串併存儲到slice結構中。Split函數是strings.Join的逆函數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這個slice必須被轉換爲string之後才能夠用strings.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice字節數組
在更底層一些的地方bufio.Scannerioutil.ReadFile和ioutil.WriteFile使用的都是*os.File的Read和Write方法不過一般程序員併不需要去直接了解到其底層實現細節在bufio和io/ioutil包中提供的方法已經足夠好用。
**練習 1.4** 脩改dup2使其可以分别打印重複的行出現在哪些文件。