Go语言找出重复行
用于文件复制、打印、检索、排序、统计的程序通常有一个相似的结构:在输入接口上循环读取,然后对每一个元素进行一些计算,在运行时或者在最后输出结果,下面我们通过三个版本的 dup 程序,来演示如何使用Go语言来找出输入内容中重复的行,它受 UNIX 的 uniq 命令启发来找到相邻的重复行。
第一个版本的 dup1 程序输出标准输入中出现次数大于 1 的行,前面是次数,这个程序引入 if 语句、map 类型和 bufio 包。
map 存储一个键/值对集合,并且提供常量时间的操作来存储、获取或测试集合中的某个元素,键可以是其值能够进行相等(==)比较的任意类型,字符串是最常见的例子,值可以是任意类型,这个例子中,键的类型是字符串,值是 int,内置的函数 make 可以用来新建 map。
每次 dup 从输入读取一行内容,这一行就作为 map 中的键,对应的值递增 1。语句
为了输出结果,我们使用基于 range 的 for 循环,这次在 map 类型的 counts 变量上遍历,每次迭代输出两个结果分别是 map 里面一个元素对应的键和值,map 里面键的迭代顺序通常是随机的,每次运行都不一致,这是有意设计的,以防止程序依赖某种特定的序列,此处不对排序做任何保证。
下面讨论 bufio 包,使用它可以简便、高效地处理输入和输出,其中一个最有用的特性是称为扫描器(Scanner)的类型,它可以读取输入,以行或者单词为单位断开,这是处理以行为单位输入内容的最简单方式。
程序使用短变量的声明方式,新建一个 bufio.Scanner 类型 input 变量:
像C语言或其他语言中的 printf 一样,函数 fmt.Printf 从一个表达式列表生成格式化的输出,它的第一个参数是格式化指示字符串,由它指定其他参数如何格式化,每一个参数的格式是一个转义字符、一个百分号加一个字符,例如:%d 将一个整数格式化为十进制的形式,%s 把参数展开为字符串变量的值。
Printf 函数有超过 10 个这样的转义字符,Go 程序员称为 verb,下表列出了一些常用的转义字符:
程序 dup1 中的格式化字符串还包含一个制表符 \t 和一个换行符 \n,字符串字面量可以包含类似转义序列(escape sequence)来表示不可见字符,Printf 默认不换行。
按照约定,诸如 log.Printf 和 fmt.Errorf 之类的格式化函数以 f 结尾,使用和 fmt.Printf 相同的格式化规则,而那些以 ln 结尾的函数(如 Println)则使用 %v 的方式来格式化参数,并在最后追加换行符。
许多程序既可以像 dup 一样从标准输入进行读取,也可以从具体的文件读取,下一个版本的 dup 程序可以从标准输入或一个文件列表进行读取,使用 os.Open 函数来逐个打开:
第二个返回值是一个内置的 error 类型的值,如果 err 等于 nil,则表示文件成功打开,文件在被读到结尾的时候,使用 Close 函数关闭文件,然后释放相应的资源(内存等)。
另一方面,如果 err 不等于 nil,说明出错了,这时,error 的值为描述错误原因,简单的错误处理是使用 Fprintf 和 %v 在标准错误流上输出一条消息,%v 可以使用默认格式显示任意类型的值;错误处理后,dup 开始处理下一个文件;continue 语句让循环进入下一个迭代。
为了保持示例代码简短,这里对错误处理有意进行了一定程度的忽略。很明显,必须检查 os.Open 返回的错误;但是,我们忽略了使用 input.Scan 读取文件的过程中出现概率很小的错误。我们将标记所跳过的错误检查。
值得注意的是,对 countLines 的调用出现在其声明之前。函数和其他包级别的实体可以以任意次序声明。
map 是一个使用 make 创建的数据结构的引用。当一个 map 被传递给一个函数时,函数接收到这个引用的副本,所以被调用函数中对于 map 数据结构中的改变对函数调用者使用的 map 引用也是可见的。在示例中,countLines 函数在 counts map 中插入的值,在 main 函数中也是可见的。
这个版本的 dup 使用“流式”模式读取输入,然后按需拆分为行,这样原理上这些程序可以处理海量的输入。一个可选的方式是一次读取整个输入到大块内存,一次性地分割所有行,然后处理这些行。接下去的版本 dup3 将以这种方式处理。这里引入一个 ReadFile 函数(从 io/ioutil 包),它读取整个命名文件的内容,还引入一个 strings.Split 函数,它将一个字符串分割为一个由子串组成的 slice。(Split 是前面介绍过的 strings.Join 的反操作。)
我们在某种程度上简化了 dup3:第一,它仅读取指定的文件,而非标准输入,因为 ReadFile 需要一个文件名作为参数;第二,我们将统计行数的工作放回 main 函数中,因为它当前仅在一处用到。
实际上,bufio.Scanner、ioutil.ReadFile 以及 ioutil.WriteFile 使用 *os.File 中的 Read 和 Write 方法,但是大多数程序员很少需要直接访问底层的例程。像 bufio 和 io/ioutil 包中上层的方法更易使用。
第一个版本的 dup1 程序输出标准输入中出现次数大于 1 的行,前面是次数,这个程序引入 if 语句、map 类型和 bufio 包。
// dup1 输出标准输入中出现次数大于1的行,前面是次数 package main import ( "bufio" "fmt" "os" ) func main() { counts := make(map[string]int) input := bufio.NewScanner(os.Stdin) for input.Scan() { counts[input.Text()]++ } //注意:忽略 input.Err() 中可能的错误 for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } }像 for 一样,if 语句中的条件部分也从不放在小括号里面,但是程序体中需要用到大括号,这里还可以有一个可选的 else 部分,当条件为 false 的时候执行。
map 存储一个键/值对集合,并且提供常量时间的操作来存储、获取或测试集合中的某个元素,键可以是其值能够进行相等(==)比较的任意类型,字符串是最常见的例子,值可以是任意类型,这个例子中,键的类型是字符串,值是 int,内置的函数 make 可以用来新建 map。
每次 dup 从输入读取一行内容,这一行就作为 map 中的键,对应的值递增 1。语句
counts[input.Text()]++
等价于下面的两个语句:
line := input.Text()
counts[line] = counts[line] + 1
为了输出结果,我们使用基于 range 的 for 循环,这次在 map 类型的 counts 变量上遍历,每次迭代输出两个结果分别是 map 里面一个元素对应的键和值,map 里面键的迭代顺序通常是随机的,每次运行都不一致,这是有意设计的,以防止程序依赖某种特定的序列,此处不对排序做任何保证。
下面讨论 bufio 包,使用它可以简便、高效地处理输入和输出,其中一个最有用的特性是称为扫描器(Scanner)的类型,它可以读取输入,以行或者单词为单位断开,这是处理以行为单位输入内容的最简单方式。
程序使用短变量的声明方式,新建一个 bufio.Scanner 类型 input 变量:
input := bufio.NewScarmer(os.Stdin)
扫描器从程序的标准输入进行读取。每一次调用 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 函数来逐个打开:
// dup2 打印输入中多次出现的行的个数和文本 //它从 stdin 或指定的文件列表读取 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()]++ } //注意:忽略 input.Err() 中可能的错误 }函数 os.Open 返回两个值,第一个是打开的文件(*os.File),该文件随后被 Scanner 读取。
第二个返回值是一个内置的 error 类型的值,如果 err 等于 nil,则表示文件成功打开,文件在被读到结尾的时候,使用 Close 函数关闭文件,然后释放相应的资源(内存等)。
另一方面,如果 err 不等于 nil,说明出错了,这时,error 的值为描述错误原因,简单的错误处理是使用 Fprintf 和 %v 在标准错误流上输出一条消息,%v 可以使用默认格式显示任意类型的值;错误处理后,dup 开始处理下一个文件;continue 语句让循环进入下一个迭代。
为了保持示例代码简短,这里对错误处理有意进行了一定程度的忽略。很明显,必须检查 os.Open 返回的错误;但是,我们忽略了使用 input.Scan 读取文件的过程中出现概率很小的错误。我们将标记所跳过的错误检查。
值得注意的是,对 countLines 的调用出现在其声明之前。函数和其他包级别的实体可以以任意次序声明。
map 是一个使用 make 创建的数据结构的引用。当一个 map 被传递给一个函数时,函数接收到这个引用的副本,所以被调用函数中对于 map 数据结构中的改变对函数调用者使用的 map 引用也是可见的。在示例中,countLines 函数在 counts map 中插入的值,在 main 函数中也是可见的。
这个版本的 dup 使用“流式”模式读取输入,然后按需拆分为行,这样原理上这些程序可以处理海量的输入。一个可选的方式是一次读取整个输入到大块内存,一次性地分割所有行,然后处理这些行。接下去的版本 dup3 将以这种方式处理。这里引入一个 ReadFile 函数(从 io/ioutil 包),它读取整个命名文件的内容,还引入一个 strings.Split 函数,它将一个字符串分割为一个由子串组成的 slice。(Split 是前面介绍过的 strings.Join 的反操作。)
我们在某种程度上简化了 dup3:第一,它仅读取指定的文件,而非标准输入,因为 ReadFile 需要一个文件名作为参数;第二,我们将统计行数的工作放回 main 函数中,因为它当前仅在一处用到。
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), "\r") { counts[line]++ } } for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } }ReadFile 函数返回一个可以转化成字符串的字节 slice,这样它可以被 strings.Split 分割。
实际上,bufio.Scanner、ioutil.ReadFile 以及 ioutil.WriteFile 使用 *os.File 中的 Read 和 Write 方法,但是大多数程序员很少需要直接访问底层的例程。像 bufio 和 io/ioutil 包中上层的方法更易使用。
所有教程
- C语言入门
- C语言编译器
- C语言项目案例
- 数据结构
- C++
- STL
- C++11
- socket
- GCC
- GDB
- Makefile
- OpenCV
- Qt教程
- Unity 3D
- UE4
- 游戏引擎
- Python
- Python并发编程
- TensorFlow
- Django
- NumPy
- Linux
- Shell
- Java教程
- 设计模式
- Java Swing
- Servlet
- JSP教程
- Struts2
- Maven
- Spring
- Spring MVC
- Spring Boot
- Spring Cloud
- Hibernate
- Mybatis
- MySQL教程
- MySQL函数
- NoSQL
- Redis
- MongoDB
- HBase
- Go语言
- C#
- MATLAB
- JavaScript
- Bootstrap
- HTML
- CSS教程
- PHP
- 汇编语言
- TCP/IP
- vi命令
- Android教程
- 区块链
- Docker
- 大数据
- 云计算