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