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 ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 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`,出現重複的行時打印文件名稱。