Golang 第一章 3.查找重复行

  • By v2ray节点

  • 2024-06-24 12:03:18

  • 评论

用于文件复制、打印、搜索、排序、计数等的程序都具有类似的结构:对输入进行循环,对每个元素进行一些计算,并在运行过程中或结束时生成输出。我们将展示一个名为 dup 的程序的三个变体;它部分受到 Unix uniq 命令的启发,该命令查找相邻的重复行。所使用的结构和包是可以轻松适应的模型。

dup 的第一个版本打印标准输入中出现多次的每一行,并在其前面加上计数。此程序引入了 if 语句、map 数据类型和 bufio 包。

gopl.io/ch1/dup1
// Dup1 打印标准输入中出现超过
// 一次的每一行的文本,并在其前面加上计数。
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 语句中的条件从不使用圆括号,但主体必须使用大括号。可以有一个可选的 else 部分,当条件为 false 时执行该部分。

map包含一组键/值对,并提供常量时间操作来存储、检索或测试集合中的项。键可以是任何类型,其值可以用 == 进行比较,最常见的例子是字符串;值可以是任何类型。在此示例中,键是字符串,值是整数。内置函数 make 创建一个新的空map;它还有其他用途。第四章.3 节将详细讨论map。

每次 dup 读取一行输入时,该行将用作map中的键,并且相应的值将递增。语句 counts[input.Text()]++ 相当于以下两个语句:

line := input.Text()
counts[line] = counts[line] + 1

如果map尚未包含该键,则不会出现问题。第一次看到新行时,右侧的表达式 counts[line] 会计算为其类型的零值,对于 int 则为 0。

为了打印结果,我们使用另一个基于范围的 for 循环,这次是在 counts map上。和以前一样,每次迭代都会产生两个结果,一个键和该键的map元素的值。map迭代的顺序没有指定,但实际上它是随机的,每次运行都会有所不同。这种设计是故意的,因为它可以防止程序依赖任何特定的顺序,因为没有特定的顺序是可以保证的。

接下来是 bufio 包,它有助于使输入和输出变得高效和方便。它最有用的功能之一是 Scanner 类型,它读取输入并将其分解为行或单词;这通常是处理自然以行出现的输入的最简单方法。

该程序使用短变量声明来创建一个引用

bufio.Scanner 的新变量输入:

input := bufio.NewScanner(os.Stdin)

扫描器从程序的标准输入读取。每次调用 input.Scan() 都会读取下一行并从末尾删除换行符;可以通过调用 input.Text() 检索结果。如果有一行,Scan 函数返回 true;如果没有更多输入,则返回 false。

函数 fmt.Printf 与 C 语言和其他语言中的 printf 类似,可从表达式列表生成格式化输出。其第一个参数是格式字符串,用于指定后续参数的格式。每个参数的格式由转换字符(百分号后面的字母)决定。例如,%d 使用十进制表示法格式化整数操作数,而 %s 则扩展为字符串操作数的值。

Printf 有十几种这样的转换,Go 程序员称之为动词。下表远非完整的规范,但说明了许多可用的功能:

%d 整数 小数   
%x, %o, %b 十六进制、八进制、二进制整数
%f, %g, %e 浮点数:3.141593 3.141592653589793 3.141593e+00
%t 布尔值:真或假
%c 字符(Unicode)
%s 字符串
%q 带引号的字符串 "abc" 或 字符 'c'
%v 任何自然格式的值
%T 任意值的类型
%% 文字百分号(无操作数)

dup1 中的格式字符串还包含制表符 \t 和换行符 \n。字符串文字可能包含此类转义序列,用于表示原本不可见的字符。默认情况下,Printf 不会写入换行符。按照惯例,名称以 f 结尾的格式化函数(例如 log.Printf 和 fmt.Errorf)使用 fmt.Printf 的格式化规则,而名称以 ln 结尾的函数遵循 Println,将其参数格式化为 %v,后跟换行符。

许多程序要么从标准输入读取(如上所述),要么从命名文件序列读取。 dup 的下一个版本可以从标准输入读取或处理文件名列表,使用 os.Open 打开每个文件:


gopl.io/ch1/dup2
// Dup2 打印输入中出现多次的行数和文本。
// 它从标准输入或命名文件列表中读取

os.Open 函数返回两个值。第一个是打开的文件 (*os.File),供扫描仪后续读取时使用。

os.Open 的第二个结果是内置错误类型的值。如果 err 等于特殊内置值 nil,则文件打开成功。读取文件,当到达输入末尾时,Close 将关闭文件并释放所有资源。另一方面,如果 err 不为 nil,则出现问题。在这种情况下,错误值描述了问题。我们简单的错误处理使用 Fprintf 和动词 %v 在标准错误流上打印一条消息,它以默认格式显示任何类型的值,然后 dup 继续处理下一个文件;continue 语句转到封闭 for 循环的下一次迭代。

为了将代码示例保持在合理的大小,我们前面的示例故意对错误处理有些漫不经心。显然,我们必须检查 os.Open 中的错误;但是,我们忽略了使用 input.Scan 读取文件时发生错误的可能性较小。我们将记录跳过错误检查的地方,并将在第五章.4 节中详细介绍错误处理。

请注意,对 countLines 的调用先于其声明。函数和其他包级实体可以按任何顺序声明。

map是对 make 创建的数据结构的引用。当将map传递给函数时,该函数会收到该引用的副本,因此被调用函数对底层数据结构所做的任何更改也将通过调用者的map引用可见。在我们的示例中,countLines 插入到 counts map中的值可供 main 看到。

上述 dup 版本以“流式”模式运行,其中输入被读取并根据需要分成几行,因此原则上这些程序可以处理任意数量的输入。另一种方法是将整个输入一次性读入内存,一次性将其分成几行,然后处理这些行。以下版本 dup3 以这种方式运行。它引入了 ReadFile 函数(来自 io/ioutil 包),该函数读取命名文件的全部内容,以及 strings.Split,该函数将字符串拆分为一组子字符串。(Split 与我们之前看到的 strings.Join 相反。)

我们对 dup3 进行了一些简化。首先,它只读取命名文件,而不是标准输入,因为 ReadFile 需要文件名参数。其次,我们将行数计数移回了 main,因为现在只有一个地方需要它。

gopl.io/ch1/dup3
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 返回一个字节切片,必须将其转换为字符串,以便可以通过 strings.Split 进行拆分。我们将在第三章.5.4 节详细讨论字符串和字节切片。

在底层,bufio.Scanner、ioutil.ReadFile 和 ioutil.WriteFile 使用 *os.File 的 Read 和 Write 方法,但大多数程序员很少需要直接访问这些低级例程。来自 bufio 和 io/ioutil 的高级函数更易于使用。

练习 1.4: 修改 dup2 以打印出现每个重复行的所有文件的名称。

v2ray节点购买