zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Go程序设计语言1.3 找出重复行

2023-03-09 22:23:09 时间

1.3 找出重复行


用于文件复制、打印、检索、排序、统计的程序,通常有一个相似的结构:在输入接口上循环读取,然后对每一个元素进行一些计算,在运行时或者在最后输出结果。我们展示三个版本的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程序,输出出现重复行的文件的名称。