<b>1.3 找出重複行</b>
<b></b>
用于檔案複制、列印、檢索、排序、統計的程式,通常有一個相似的結構:在輸入接口上循環讀取,然後對每一個元素進行一些計算,在運作時或者在最後輸出結果。我們展示三個版本的dup程式,它受unix的uniq指令啟發來找到相鄰的重複行。這個程式使用容易适配的結構和包。
第一個版本的dup程式輸出标準輸入中出現次數大于1的行,前面是次數。這個程式引入if語句、map類型和bufio包。
像for一樣,if語句中的條件部分也從不放在圓括号裡面,但是程式體中需要用到大括号。這裡還可以有一個可選的else部分,當條件為false的時候執行。
map存儲一個鍵/值對集合,并且提供常量時間的操作來存儲、擷取或測試集合中的某個元素。鍵可以是其值能夠進行相等(==)比較的任意類型,字元串是最常見的例子;值可以是任意類型。這個例子中,鍵的類型是字元串,值是int。内置的函數make可以用來建立map,它還可以有其他用途。map将在4.3節中進行更多讨論。
每次dup從輸入讀取一行内容,這一行就作為map中的鍵,對應的值遞增1。語句counts[input.text()]++等價于下面的兩個語句:
鍵在map中不存在時也是沒有問題的。當一個新的行第一次出現時,右邊的表達式counts[line]根據值類型被推演為零值,int的零值是0。
為了輸出結果,我們使用基于range的for循環,這次在map類型的counts變量上周遊。像以前一樣,每次疊代輸出兩個結果,map裡面一個元素對應的鍵和值。map裡面的鍵的疊代順序不是固定的,通常是随機的,每次運作都不一緻。這是有意設計的,以防止程式依賴某種特定的序列,此處不對排序做任何保證。
下面讨論bufio包,使用它可以簡便和高效地處理輸入和輸出。其中一個最有用的特性是稱為掃描器(scanner)的類型,它可以讀取輸入,以行或者單詞為機關斷開,這是處理以行為機關的輸入内容的最簡單方式。
程式使用短變量的聲明方式,建立一個bufio.scanner類型input變量:
掃描器從程式的标準輸入進行讀取。每一次調用input.scan()讀取下一行,并且将結尾的換行符去掉;通過調用input.text()來擷取讀到的内容。scan函數在讀到新行的時候傳回true,在沒有更多内容的時候傳回false。
像c語言或其他語言中的printf一樣,函數fmt.printf從一個表達式清單生成格式化的輸出。它的第一個參數是格式化訓示字元串,由它指定其他參數如何格式化。每一個參數的格式是一個轉義字元、一個百分号加一個字元。例如:%d将一個整數格式化為十進制的形式,%s把參數展開為字元串變量的值。
printf函數有超過10個這樣的轉義字元,go程式員稱為verb。下表遠不完整,但是它說明有很多可以用的功能:
verb 描述
%d 十進制整數
%x,%o,%b 十六進制、八進制、二進制整數
%f,%g,%e 浮點數:如3.141593, 3.141592653589793, 3.141593e+00
%t 布爾型:true或false
%c 字元(unicode碼點)
%s 字元串
%q 帶引号字元串(如"abc")或者字元(如'c')
%v 内置格式的任何值
%t 任何值的類型
%% 百分号本身(無操作數)
程式dup1中的格式化字元串還包含一個制表符\t和一個換行符\n。字元串字面量可以包含類似轉義序列(escape sequence)來表示不可見字元。printf預設不寫換行符。按照約定,諸如log.printf和fmt.errorf之類的格式化函數以f結尾,使用和fmt.printf相同的格式化規則;而那些以ln結尾的函數(如println)則使用%v的方式來格式化參數,并在最後追加換行符。
許多程式既可以像dup一樣從标準輸入進行讀取,也可以從具體的檔案讀取。下一個版本的dup程式可以從标準輸入或一個檔案清單進行讀取,使用os.open函數來逐個打開:
函數os.open傳回兩個值。第一個是打開的檔案(*os.file),該檔案随後被scanner讀取。
第二個傳回值是一個内置的error類型的值。如果err等于特殊的内置nil值,标準檔案成功打開。檔案在被讀到結尾的時候,close函數關閉檔案,然後釋放相應的資源(記憶體等)。另一方面,如果err不是nil,說明出錯了。這時,error的值描述錯誤原因。簡單的錯誤處理是使用fprintf和%v在标準錯誤流上輸出一條消息,%v可以使用預設格式顯示任意類型的值;錯誤處理後,dup開始處理下一個檔案;continue語句讓循環進入下一個疊代。
為了保持示例代碼簡短,這裡對錯誤處理有意進行了一定程度的忽略。很明顯,必須檢查os.open傳回的錯誤;但是,我們忽略了使用input.scan讀取檔案的過程中出現機率很小的錯誤。我們将标記所跳過的錯誤檢查,5.4節将更詳細地讨論錯誤處理。
值得注意的是,對countlines的調用出現在其聲明之前。函數和其他包級别的實體可以以任意次序聲明。
map是一個使用make建立的資料結構的引用。當一個map被傳遞給一個函數時,函數接收到這個引用的副本,是以被調用函數中對于map資料結構中的改變對函數調用者使用的map引用也是可見的。在示例中,countlines函數在counts map中插入的值,在main函數中也是可見的。
這個版本的dup使用“流式”模式讀取輸入,然後按需拆分為行,這樣原理上這些程式可以處理海量的輸入。一個可選的方式是一次讀取整個輸入到大塊記憶體,一次性地分割所有行,然後處理這些行。接下去的版本dup3将以這種方式處理。這裡引入一個readfile函數(從io/ioutil包),它讀取整個命名檔案的内容,還引入一個strings.split函數,它将一個字元串分割為一個由子串組成的slice。(split是前面介紹過的strings.join的反操作。)
我們在某種程度上簡化了dup3:第一,它僅讀取指定的檔案,而非标準輸入,因為readfile需要一個檔案名作為參數;第二,我們将統計行數的工作放回main函數中,因為它目前僅在一處用到。
readfile函數傳回一個可以轉化成字元串的位元組slice,這樣它可以被strings.split分割。3.5.4節将詳細讨論字元串和位元組slice。
實際上,bufio.scanner、ioutil.readfile以及ioutil.writefile使用*os.file中的read
和write方法,但是大多數程式員很少需要直接通路底層的例程。像bufio和io/ioutil包中上層的方法更易使用。
練習1.4:修改dup2程式,輸出出現重複行的檔案的名稱。