目录

go语言圣经1.3

【go语言圣经1.3】

概念

  1. map 数据类型
    • map 存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型,只要其值能用 == 运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数 make 创建空 map ,此外,它还有别的作用。4.3 节讨论 map
    • 从功能和实现上说, Gomap 类似于 Java 语言中的 HashMap ,Python 语言中的 dictLua 语言中的 table ,通常使用 hash 实现

语言特性

  1. if 语句:条件部分不需要括号,但主体必须用大括号包围。

  2. Go 中只有 for 一种循环形式,可以写成传统 for 循环、while 循环形式或无限循环。

  3. 通过 range 遍历 map 或切片时,返回的顺序是不确定的,这要求程序不能依赖遍历顺序。

  4. 标准输入与文件输入都通过类似方式处理:使用 bufio.Scannerioutil.ReadFile 。不同之处在于:前者逐行读取适合大数据流,后者适合一次性加载整个文件

  5. map 是一种引用类型(这里的“引用”类似于指针,但语法上更加简单)

    • 当你使用 make(map[string]int) 创建一个 map 时,实际上 Go 分配了一块内存来存储这个 map 的数据,并返回了一个引用,这个引用指向那块内存。你可以把这个引用看作是一个“地址”,它告诉程序实际数据存储在哪里。
    • 当你将 map 作为参数传递给函数时,传递的是这个引用的一个副本(浅拷贝),而不是整个 map 数据结构的拷贝。副本只是另一个指向同一块内存的引用。
    • 因为传入函数的 map 引用(副本)和原始的 map 引用都指向同一块内存,所以在函数内部对 map 的修改(如增加、删除或更新键值对)会直接影响到这块内存。结果就是,无论是在函数内部还是外部,你看到的都是同一份数据。
    • 再次强调:对于 map,使用 赋值语句 或者 传递参数 时,实际上只是把 这个 map 的引用 复制了一份(浅拷贝)。新变量和原变量都指向同一块内存区域。
    original := map[string]int{"age": 20}
    copyRef := original // 复制的是引用,而非数据内容的独立副本
    
    copyMap := deepCopy(original) // 相当于手动复制了 map 的每个键值对

要点(案例)

通过 dup 程序的三个版本,我们可以看到 Go 语言在文本处理上的多种方法和技巧:

  • dup1 采用流式读取和 map 统计的方式,适合处理标准输入;
  • dup2 在此基础上扩展到处理具名文件,并引入了错误处理;
  • dup3 则展示了如何一次性读取整个文件,再用字符串分割方法处理数据。

在 dup 程序中,核心思想是扫描输入中的每一行,然后利用数据结构统计每一行出现的次数,最后只输出出现多次的行。

这个思路在许多文本处理工具中都非常常见,比如 Unix 中的 uniq 命令(寻找相邻的重复行)

// Dup1 prints the text of each line that appears more than
// once in the standard input, preceded by its count.
package main

import (
    "bufio"
    "fmt"
    "os"
)

int 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)
	}
}
  • bufio :提供高效的缓冲输入,主要用于按行读取文本。
  • os :提供与操作系统交互的能力,比如读取标准输入
  • 主要数据结构——map
    • 使用 make(map[string]int) 创建了一个空的 map,该 map 的键为字符串(每一行的内容),值为整型(该行出现的次数)。
    • 当某一行首次出现时,由于 map 默认值为该类型的零值(对 int 来说为 0),直接进行自增操作 counts[input.Text()]++ 即可。
  • bufio.Scanner
    • Scanner 类型是 bufio 包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法
    • bufio.NewScanner(os.Stdin) 创建了一个扫描器,用于逐行读取标准输入。
    • 调用 input.Scan() 读取下一行, 并移除行末的换行符(当没有更多输入时返回 false)
    • 调用 input.Text() 可以获取当前扫描到的行(已经去掉了换行符)
// 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.
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()]++
	}
	// NOTE: ignoring potential errors from input.Err()
}
  • 扩展读取具名函数的能力
  • 将按行读取并计数的逻辑提取到独立的函数 countLines
    • 由于 map 是引用类型,将其作为参数传递后,函数内部对 map 的修改会反映到外部。
// 使用一种不同的方法:一次性将整个文件内容读入内存,再进行分割处理

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)
        }
    }
}
  • ioutil.ReadFile 函数用于一次性读取整个文件内容,返回的是 字节切片。
  • 需要将 字节切片 转换为 字符串string(data) ,才能使用 strings.Split 按换行符分割成多个行。
  • 适用场景
    • 当输入数据较小或对内存要求不高时,一次性读取所有内容简化了处理过程;
    • 对于大文件或内存敏感的应用,前两种基于流式处理的方式(dup1 和 dup2)更合适。

总结与补充

  1. 标准化输出 format
    • 格式化动词

      %d          十进制整数
      %x, %o, %b  十六进制八进制二进制整数
      %f, %g, %e  浮点数 3.141593 3.141592653589793 3.141593e+00
      %t          布尔true或false
      %c          字符rune (Unicode码点)
      %s          字符串
      %q          带双引号的字符串"abc"或带单引号的字符'c'
      %v          变量的自然形式natural format
      %T          变量的类型
      %%          字面上的百分号标志无操作数
    • 转义字符

  2. strings包的常用函数
    • strings.Split(s, sep string) []string 将字符串s按照分隔符sep分割成子串,并返回字符串切片
    • strings.Join(a []string, sep string) string 将字符串切片a中的各个元素以分隔符sep连接成一个单一的字符串