跳转至

错误处理、切片、函数、正则,以及两个 Unix 工具的 Go 重写

这一章讲一组「每天都在用」的机制,加一个实战转向。 前半回答四个问题:Go 的程序出错了怎么办(错误是值,不是异常)? 切片和数组到底是什么关系(视图,共享内存)?函数能当什么用(一等公民)? 正则怎么干活?后半回答:怎么写正经的命令行工具——最后亲手用 Go 重写 两个你天天在用的 Unix 命令:which 和 basename。 读完你会拿到全书的第一批实战手感:从「语言机制」走到「系统工具」。

1. 这一章在原书里的位置

本章是原书第 1 章(Essential Go)的后半段。前半(01 号拆解)解决了「Go 是什么」, 这一段解决「拿它干活时长什么样」。书里在这里立了一个贯穿全书的模式: 每讲一组机制,就用它重写一个经典 Unix 工具——本章是 which(1) 和 basename(1), 后面还会有 wc(1)、dd(1)。读完原文的人会获得一种直觉:这些用了几十年的小工具, 每一个都不过几十行 Go。

2. 顶层全景

  前半:语言机制(§3–§6)
    错误处理   → error 是返回值,不是异常;panic/recover 是逃生舱
    数组与切片 → 切片 = (指向数组的指针, 长度, 容量);append 超容量才搬家
    函数       → 多返回值 / 变参 / 高阶 / 惰性迭代器(Go 1.23)
    正则       → 编译一次,匹配/提取/替换/校验

  后半:命令行实战(§7–§10)
    os.Args + flag → 原始参数和结构化参数两档
    cobra + viper  → 专业 CLI 的骨架(docker、hugo 同款)
    which(1)       → 主线例子:搜 PATH、查可执行位
    basename(1)    → 主线例子:截路径、剥后缀、处理边角

图说:一条从「机制」滑向「工具」的线。主线例子放在 which(1) 上走全程,basename(1) 走另一条边角线。

3. 错误处理:没有 try/catch,错误是返回值

Go 没有异常机制(没有 try/catch)。错误的载体就是普通的值:语言内置一个 error 接口, 任何实现了 Error() string 方法的类型都是一个 error。因为 Go 函数可以返回多个值, 惯例是「最后一个返回值是 error」:调用方拿到结果后第一件事就是查它1。

  func divide(a, b float64) (float64, error) {
      if b == 0 {
          return 0, &DivideError{A: a, B: b}   // 出问题时:结果位填零值,error 位填具体错误
      }
      return a / b, nil                        // 正常时:error 位是 nil
  }

  result, err := divide(10, 0)
  if err != nil { /* 处理它 */ }                // 调用方:看到 err 不是 nil 就处理

图说:divide(10, 0) 的输出是 Error: cannot divide 10.00 by 0.00,后面还能用 errors.As() 把具体类型(*DivideError)挖出来,取出 A=10.00、B=0.00 两个字段2。

这套设计的三个配套工具,每个都常用:

  • errors.Is(err, target):这个错误是不是(或是不是包着)那个已知错误? 书里用它查「文件不存在」这个哨兵错误(sentinel error)os.ErrNotExist3。
  • errors.As(err, &target):把错误链里某个具体类型的错误提取出来, 好读取它携带的字段(上面的 DivideError 就是这么被挖出 A、B 的)。
  • 错误包装(wrapping):Go 1.13 起用 fmt.Errorf("...: %w", err) 给错误包上一层下文, 同时保留里面的原始错误供 Is/As 穿透4。

逃生舱:panic、recover、defer

错误处理是日常机制;panic 是「程序没法继续了」的紧急制动——触发后正常执行中断, 一路向上撞,直到被 recover 接住,否则整个进程崩掉。配套的两个关键字:

  • defer:函数退出时(无论正常还是 panic)必定执行,用来释放资源,如 defer f.Close();
  • recover:只在 defer 的函数里有用,接住正在传播的 panic,让程序活下来。

书里的例子:开一个不存在的文件,本来该返回 error,但用 panic(fmt.Errorf("critical failure...: %w", err)) 升级为紧急状态;外层 defer 里 recover 接住,程序打印 Recovered from panic: 后继续跑5。 作者的态度(也是 Go 社区的惯例):panic/recover 留给真正的灾难,业务错误一律走 error 返回值。

判断(我们的,不是书里的): 「错误是值」是 Go 被批评最多、也是维护收益最大的设计。 批评点:每个调用后面跟三行 if err != nil,啰嗦。收益点:错误的处理路径全部显式写在代码里, 不存在「这个异常在哪一层被谁吞了」的考古问题——大型系统里后者才是要命的。 如果错,会错在: 如果工具链层面(编译器或 linter)哪天开始强制「error 必须被处理或显式丢弃」, 啰嗦的批评就会消失,这条判断的语境就不存在了。

4. 主线例子(一):切片和它的底层数组——共享,直到不共享

切片(slice)是 Go 里最重要的数据结构,也是隐性 bug 的头号产地。先立三个概念:

  • 数组(array):定长、连续内存、值类型——赋值给另一个数组会把全部元素拷一份; 而且长度是类型的一部分,[4]int 和 [8]int 是两个不兼容的类型6。
  • 切片:不定长的「视图」。它自己不存数据,内部是三个部件:指向底层数组的指针、 长度(len,现在能访问几个)、容量(cap,从起点到数组末尾还有几个位置)7。
  • 切片表达式 a[1:4]:左闭右开,取从下标 1 到 3 的元素,不复制数据, 得到的新切片和原数组指同一块内存。

现在拿书上的例子从头走一遍。起点是数组 arr := [5]int{10, 20, 30, 40, 50}, 切出两个切片:slice1 := arr[1:4](值 [20 30 40])和 slice2 := arr[2:](值 [30 40 50])。

第一步,改 slice1,所有人跟着变。 执行 slice1[1] = 999:

  arr:     [10 | 20 | 999 | 40 | 50]
                   ▲     ▲     ▲
  slice1:     [20 | 999 | 40]      len=3, cap=4(从数组下标 1 起,到末尾还有 4 个位置)
  slice2:         [999 | 40 | 50]  len=3, cap=3(从数组下标 2 起)

图说:999 写进的是同一块内存,arr 变成 [10 20 999 40 50],slice2 变成 [999 40 50]—— 三个名字,一份数据8。这就是「切片是视图」的字面意思。

第二步,append 没超容量,还是一家人。 slice1 = append(slice1, 888): 容量还有富余,直接把 888 写进底层数组的第 4 个位置。arr 变成 [10 20 999 40 888]—— 原数组被改了9。

第三步,append 超过容量,悄悄搬家,从此两家。 再 slice1 = append(slice1, 777): cap=4 已经满了,Go 的做法是:申请一块新数组、把现有元素全部拷过去、再追加 777, slice1 改为指向新数组。此刻起改 slice1 跟 arr 再无关系:

  slice1[0] = 12345 之后:
  slice1: [12345 999 40 888 777]   ← 住新房子
  arr:    [10 20 999 40 888]       ← 老房子,纹丝不动

图说:书上原话——「not understanding this fact could create some nasty bugs」10。 append 的返回值必须接回来(slice1 = append(slice1, 888)),因为它可能指向新数组; 只写 append(slice1, 888) 不赋值,是新手标志性 bug。

容量怎么来的?用 make([]int, 3, 5) 可以指定初始长度 3、容量 5; 不指定的话,每次长度要超过容量时,容量翻倍11。

5. 函数:多返回值、变参、高阶,外加 Go 1.23 的惰性迭代器

函数在 Go 里是一等公民:可以赋给变量、当参数传、当返回值返回12。 本章出现五种形态,前四种看一眼就够:

形态 书里长什么样 一句话
多返回值 func divide(a, b float64) (float64, error) 结果和错误一起回来(§3 的惯例)
命名返回值 func rectangleStats(l, w float64) (area, perimeter float64) 返回值在签名里起了名,函数体里直接给它们赋值,return 后面可以空着
变参 func joinStrings(sep string, words ...string) 末尾的 ...string 表示「再来多少个 string 都行」,函数内它是个切片
高阶函数 func applyTwice(fn func(int) int, value int) int 参数是一个函数;applyTwice(func(x int) int { return x * 2 }, 5) 得 20

惰性迭代器(Go 1.23):一个值得单独记住的新东西

前四种是旧账。第五种是 Go 1.23(2024 年 8 月发布)才进语言的:迭代器函数。 解决的问题:想产出一串值,老办法是先全算出来装进切片再交出去——数据量大时 「装切片」这一步本身就吃掉大量内存。迭代器把顺序倒过来:消费者每要一个,生产者才算一个。

书上的例子是斐波那契数列。核心是一个约定:迭代器是一个函数,它接收一个叫 yield 的回调函数,每产出一个值就调用一次 yield(值);yield 返回 false 表示 「消费者不要了」,迭代器就此收工13:

  func fibonacci(limit int) func(yield func(int) bool) {
      return func(yield func(int) bool) {
          a, b := 0, 1
          for i := 0; i < limit; i++ {
              if !yield(a) { return }   // 消费者喊停,立刻收工
              a, b = b, a+b
          }
      }
  }

  for num := range fibonacci(10) {      // range 直接迭在函数上,是 Go 1.23 的新语法
      fmt.Print(num, " ")
  }
  → 0 1 1 2 3 5 8 13 21 34

图说:10 个数一个切片都没建,每个数在用它的那一刻才算出来。yield 只是约定俗成的参数名, 不是关键字14。这条机制到第 10 章解释器里还会再见到(遍历语法树)。

6. 正则表达式:编译一次,之后匹配/提取/替换/校验

正则表达式(regular expression,行内常写 regex)是用一小段字符写的「搜索模式」: 不是搜一个固定的词,而是搜「长成这样的东西」——比如「大写字母开头、后面跟小写字母的词」。 Go 的标准库 regexp 包直接用,典型流程是四步,书上各给了一个例子15:

  1. 写模式并用反引号包起来。 如 `[A-Z][a-z]+`(一个大写字母 + 一个以上小写字母)。 反引号是 Go 的原始字符串:反斜杠不转义——写正则必须用它,否则 \. 会先被字符串层吃掉一层16。
  2. 编译。 re := regexp.MustCompile(pattern)。模式写错时 MustCompile 直接 panic—— 只对「模式是程序里写死的常量」安全;模式来自用户输入就得用会返回 error 的 Compile()。
  3. 干活,四种常用法:
  4. re.MatchString(text) —— 有没有匹配?返回 true/false;
  5. re.FindAllString(text, -1) —— 找出全部匹配,-1 表示不限个数。 对 "Go Rust Python Zig C C++" 用 [A-Z][a-z]+,得 [Go Rust Python Zig]—— C 和 C++ 因没有小写字母尾巴而被排除17;
  6. re.FindStringSubmatch(email) —— 抓分组。模式 (\w+)@(\w+\.\w+) 把邮箱劈成 用户名和域名两段括号,返回的切片第 0 个是整串匹配、之后依次是各组: 对 "support@example.com" 得 [support@example.com support example.com]18;
  7. re.ReplaceAllString(text, "XXXX") —— 全部替换。把订单号 "1234, 5678, 91011" 抹成 "Order IDs: XXXX, XXXX, XXXX"——脱敏就是这么做的19。
  8. 校验输入:模式 ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ 查邮箱格式—— ^ 和 $ 把匹配钉死在整串的头尾,\.[a-zA-Z]{2,} 要求结尾是「点 + 至少两个字母」20。

判断(我们的,不是书里的): 书把正则当「文本处理的瑞士军刀」直接教,但没讲它的代价: 正则是出了名的「看着对、 edge case 全错」的工具,邮箱那个模式就放行不了 a@b 这类合法但罕见的地址,也挡不住故意的假地址。它是过滤器,不是验证器。 如果错,会错在: Go 的 regexp 用的是 RE2 引擎,天生没有「灾难性回溯」问题 (恶意输入把正则卡死),比很多语言安全——在这个引擎上,「代价」主要是可维护性而不是可用性。

7. 命令行参数:os.Args 是毛坯,flag 是精装

命令行程序拿到的参数,Go 里分两档处理。

毛坯档:os.Args。 就是一个字符串切片:os.Args[0] 是程序自己的名字, os.Args[1:] 是用户给的参数21。几行就能打印出来,但没有类型、没有默认值、没有帮助信息。

精装档:flag 包。 声明式地定义参数:

port := flag.Int("port", 8080, "Port to listen on")   // 名字叫 port,默认 8080
host := flag.String("host", "localhost", "Hostname or IP")
debug := flag.Bool("debug", false, "Enable debug mode")
flag.Parse()                                           // 真正去解析 os.Args

图说:flag.Parse() 之后,*port 就是用户给的值(没给则是 8080)。 它自动认 -port 和 --port 两种写法、自动生成 -h 帮助、解析失败会报错退出; 剩下没被认领的位置参数用 flag.Args() 取22。 注意 flag.Int 返回的是指针,取值要加 *——这是 Go 里指针的第一个实战露面。

再往上:cobra 与 viper

工具大到 docker、hugo 那个量级时,flag 也嫌薄。生态里有两件套:

  • cobra:命令的脚手架。每个命令是一个 *cobra.Command 结构体,天然支持子命令 (如 git add、git commit 那种层级)、自动生成帮助和使用说明23。
  • viper:配置的集中处。从 JSON/TOML/YAML 配置文件、环境变量、命令行 flag 多处读配置,按优先级合并,内部维护一张键值表24。

两者咬合:cobra 管「命令长什么样」,viper 管「配置从哪来」。 原书在这里只给概念,到第 14 章写索引服务的客户端时会回来真用一次。

8. 主线例子(二):亲手实现 which(1)

which 是你 shell 里天天在用的命令:which go 回答「敲 go 的时候实际执行的是哪个文件」。 它的全部逻辑:沿 PATH 环境变量列出的目录,从左到右找同名的可执行文件,找到就打印完整路径。 书里的 Go 实现约 30 行,我们跟着走一遍25:

func which(command string) (string, error) {
    pathEnv := os.Getenv("PATH")                                  // ① 取出 PATH,形如 /usr/bin:/bin:/opt/go/bin
    paths := strings.Split(pathEnv, string(os.PathListSeparator)) // ② 按分隔符劈开(Unix 是冒号,Windows 是分号)
    for _, dir := range paths {                                   // ③ 逐个目录找
        fullPath := filepath.Join(dir, command)                   //    拼出候选完整路径
        fileInfo, err := os.Stat(fullPath)                        // ④ 问操作系统:这个文件存在吗
        if err != nil {
            if os.IsNotExist(err) { continue }                    //    不存在 → 下一个目录
            return "", err                                        //    别的错(权限等)→ 报错收工
        }
        if fileInfo.Mode().Perm()&0111 != 0 {                     // ⑤ 可执行吗?0111 = 三个执行位
            return fullPath, nil                                  //    找到,返回完整路径
        }
    }
    return "", fmt.Errorf("%s not found in PATH", command)        // ⑥ 全部目录都没有
}

每一步对应的机制:①② 环境变量(os.Getenv)和按平台取分隔符(os.PathListSeparator—— 同一行代码 Unix 和 Windows 都正确);③④ 文件存在性检查(os.Stat + os.IsNotExist); ⑤ 可执行位检查,Mode().Perm()&0111 != 0 是按位与:0111(八进制)的三个 1 分别对应 所有者、同组、其他人的执行权限,任一个成立就算可执行——这一步就是 which 和「找到同名文件」 的区别;⑥ 找不到也返回 error,由 main 决定怎么收场。

main 函数里还有两个 Unix 工具的美德:报错写到标准错误(fmt.Fprintf(os.Stderr, ...)) 而不是标准输出(输出要留给管道下游用),退出码错误时返回 126。

判断(我们的,不是书里的): which(1) 这个例子选得极好,因为它把本章前半的机制全用上了: 环境变量、字符串劈分、文件 stat、(结果, error) 双返回、defer——一个例子回收整章。 这也是为什么这本书适合「读完复述」:它的例子不是演示片段,是成品。 如果错,会错在: 真实系统的 which 还要处理别名、shell 内建命令、符号链接, 书里这版是教学简化——拿它当「which 的全部真相」会漏掉这三类。

9. 边角例子:basename(1)——路径的最后一段

basename 做一件事:给 /usr/local/bin/go,返回 go;给 /opt/homebrew/bin/go 加后缀参数, 还可以把 .go 剥掉。书里的实现把边角处理讲得比正事还细,这恰恰是 Unix 工具的精神:

func basename(path string, suffixes ...string) string {
    if path == "/" { return "/" }                  // ① 根目录的 basename 就是 "/"
    path = strings.TrimRight(path, "/")            // ② 剥尾部斜杠:/usr/local/bin/ → /usr/local/bin
    if path == "" { return "/" }                   // ③ 输入全是斜杠("////")时,语义上还是根
    lastSlash := strings.LastIndex(path, "/")      // ④ 找最后一个斜杠
    if lastSlash >= 0 { path = path[lastSlash+1:] }//    只留它后面的部分
    if len(suffixes) > 0 {                          // ⑤ 有后缀参数就剥(命中第一个就停)
        for _, s := range suffixes {
            if strings.HasSuffix(path, s) { path = strings.TrimSuffix(path, s); break }
        }
    }
    return path
}

实际输出:basename.go /opt/homebrew/bin/go → go;basename.go ///opt//// → opt27。 注意 ①③ 两步:「/」和「全斜杠」都归一到根目录——处理用户输入的程序,大半代码在处理边角, 这行经验比 basename 本身值钱。

10. 作者的判断与证据

判断 性质 依据
错误该显式处理,panic 只留给灾难 Go 社区的通行惯例,作者全盘接受 全书代码无一次用 panic 做业务控制流
append 超容量会换底层数组,不记住会出 nasty bug 语言事实 + 作者警告 slicing.go 完整输出 walkthrough10
MustCompile 只对写死的模式安全 正确且实用 与 Compile 的对比是原文明写的16
cobra+viper 是专业 CLI 的事实标准 生态共识 docker、hugo 为例,书的两处提及一致

11. 边界与局限

  • 切片共享内存的危险,书里只演示了「数组↔切片」一个方向。 切片之间互相 append 产生的数据竞争(两个切片共享底层数组、一个 append 触发搬家),在本章无警告,要到第 3 章 race condition 才接上。
  • 错误处理的「啰嗦」没有缓解技巧。 书中所有例子都是逐行 if err != nil; 聚集错误(errors.Join)、包装的最佳实践一笔带过。
  • 正则一节没提 RE2 的独有性质(线性时间匹配、不支持回溯引用), 从别的语言迁过来的人会以为语法全集一致。
  • viper/cobra 只给概念无代码,真要上手得等第 14 章或官方文档。

12. 可带走的

  1. Go 没有异常:错误是最后一个返回值,业务错误永不 panic。
  2. errors.Is 比「错误是不是这个」,errors.As 挖具体类型,%w 包装时保留链。
  3. defer 释放资源、recover 接 panic——都只在函数边界处使用。
  4. 切片 = (指针, 长度, 容量);append 超容量才搬家,搬家前改谁都互相可见。 s = append(s, x) 的赋值不能省。
  5. 函数是一等公民;...T 变参;Go 1.23 起 range 可以直接迭在一个「调 yield 的函数」上。
  6. 正则用反引号原始字符串包;MustCompile 只适合编译期确定的死模式。
  7. flag 返回指针,取值加 *;大工具用 cobra(命令树)+ viper(配置源)。
  8. Unix 工具的两条纪律:错误走标准错误、退出码 0/1 分成功失败;用户输入的大半代码在处理边角。

13. 原文地图

主题 原书章 原文位置
error 是值、多返回、errors.Is/As、panic/recover/defer Error handling text/17-fm-error-handling.txt:37(搜「explicit, simple, and predictable」) · :72(搜「divide(a, b float64)」) · :94(搜「riskyFileOperation」) · :14(搜「Recovered from panic」)
数组是值类型、长度进类型 Arrays and slices text/18-fm-arrays-and-slices.txt:42(搜「fixed-size」) · :52(搜「carry their size」)
切片三部件、len/cap、append 扩容翻倍 Arrays and slices text/18-fm-arrays-and-slices.txt:58(搜「dynamic, flexible views」) · :76(搜「capacity of the slice doubles」)
共享内存到搬家完整走一遍 Arrays and slices text/18-fm-arrays-and-slices.txt:129(搜「slice1 := arr[1:4]」) · :168(搜「Original array」) · :189(搜「Initially, we start with」)
函数五种形态 Functions text/19-fm-functions.txt:17(搜「first-class citizens」) · :50(搜「named return values」) · :56(搜「variadic」)
惰性迭代器与 yield 协议 Functions text/19-fm-functions.txt:117(搜「Introduced in Go version 1.23」) · :133(搜「func(yield func(int) bool)」) · :13(搜「0 1 1 2 3 5 8 13 21 34」)
正则四步与特殊字符 Learning how to use regular expressions text/20-fm-learning-how-to-use-regular-expressions.txt:58(搜「MustCompile」) · :107(搜「word character」) · :99(搜「submatches」) · :152(搜「Validating Input」) · :14(搜「is a valid email」)
os.Args 与 flag 包 Working with command-line arguments text/21-fm-working-with-command-line-arguments.txt:43(搜「os.Args」) · :92(搜「Using the flag Package」) · :112(搜「flag.Int」)
cobra 与 viper The viper and cobra packages text/22-fm-the-viper-and-cobra-packages.txt:36(搜「configuration management」) · text/23-fm-implementing-which-1.txt:11(搜「widely used Go package」)
which(1) 实现与可执行位检查 Implementing which(1) text/23-fm-implementing-which-1.txt:72(搜「func which」) · :87(搜「0111」) · :14(搜「not found in PATH」)
basename(1) 的边角处理 Implementing basename(1) text/24-fm-implementing-basename-1.txt:34(搜「Handle root directory」) · :40(搜「TrimRight」) · :26(搜「///opt////」)


  1. 出处:「Error handling」第 36–48 段(text/17-fm-error-handling.txt:37,搜「explicit, simple」;:43,搜「additional result」)。原文:「Go uses error values to signal failure… when they fail, they typically return an error as an additional result, and it is the caller function's responsibility to inspect and handle it」。 ↩

  2. 出处:「Error handling」(text/17-fm-error-handling.txt:63,搜「DivideError struct」;:14,搜「cannot divide 10.00 by 0.00」)。输出含 Info: A=10.00, B=0.00,证明 errors.As 挖字段成功。 ↩

  3. 出处:「Error handling」(text/17-fm-error-handling.txt:147,搜「os.ErrNotExist」)。errors.Is(err, os.ErrNotExist) 判「文件不存在」是原文示例。 ↩

  4. 出处:「Error handling」第 49–50 段(text/17-fm-error-handling.txt:49,搜「error wrapping」)。「With Go 1.13, error wrapping became idiomatic」;%w 动词见第 106 行(fmt.Errorf("critical failure opening %q: %w", ...))。 ↩

  5. 出处:「Error handling」(text/17-fm-error-handling.txt:94,搜「riskyFileOperation」;:96,搜「recover」;:23,搜「Recovered from panic」)。defer + recover + panic 三件套在同一段代码里齐活。 ↩

  6. 出处:「Arrays and slices」第 42–55 段(text/18-fm-arrays-and-slices.txt:42,搜「fixed-size」;:51,搜「value types」;:52,搜「carry their size」)。 ↩

  7. 出处:「Arrays and slices」第 58–70 段(text/18-fm-arrays-and-slices.txt:58,搜「dynamic, flexible views」)。「a pointer to the first element, a length… and a capacity」。 ↩

  8. 出处:「Arrays and slices」(text/18-fm-arrays-and-slices.txt:172,搜「Modifying slice1」;:175,搜「Array after modification」)。输出:Array after modification: [10 20 999 40 50],Slice2 变 [999 40 50]。 ↩

  9. 出处:「Arrays and slices」(text/18-fm-arrays-and-slices.txt:181,搜「Array after append」)。输出 Array after append: [10 20 999 40 888]——append 写进了原数组。 ↩

  10. 出处:「Arrays and slices」(text/18-fm-arrays-and-slices.txt:164,搜「nasty bugs」;:186,搜「Array remains unchanged」)。搬家后 slice1[0] = 12345,arr 停在 [10 20 999 40 888]。 ↩↩

  11. 出处:「Arrays and slices」第 76–81 段(text/18-fm-arrays-and-slices.txt:77,搜「capacity of the slice doubles」)。make 三参数示例 numbers := make([]int, 3, 5) 在同段。 ↩

  12. 出处:「Functions」第 17–29 段(text/19-fm-functions.txt:17,搜「first-class citizens」)。多返回、无默认参数无重载、匿名函数闭包、变参、方法,一段里全点齐。 ↩

  13. 出处:「Functions」第 116–126 段(text/19-fm-functions.txt:117,搜「Introduced in Go version 1.23」;:133,搜「func(yield func(int) bool)」)。yield 协议与「返回 false 就提前终止」是原文明写的。 ↩

  14. 出处:「Functions」(text/20-fm-learning-how-to-use-regular-expressions.txt:2,搜「yield is a conventional function name」)。原文在正则章开头收束上一章:「Notice that yield is a conventional function name, not a Go keyword」。 ↩

  15. 出处:「Learning how to use regular expressions」第 27–33 段(text/20-fm-learning-how-to-use-regular-expressions.txt:32,搜「defines a search pattern」)。「a sequence of characters that defines a search pattern」。 ↩

  16. 出处:「Learning how to use regular expressions」第 51–60 段(text/20-fm-learning-how-to-use-regular-expressions.txt:54,搜「raw string literals」)。「unlike interpreted string literals… raw string literals do not process escape sequences」;MustCompile 与 Compile 的区别在同段。 ↩↩

  17. 出处:「Learning how to use regular expressions」(text/20-fm-learning-how-to-use-regular-expressions.txt:68,搜「Finding All Matches」;:18,搜「Capitalized words」)。输出 [Go Rust Python Zig],排除 C 和 C++ 的解释在同段。 ↩

  18. 出处:「Learning how to use regular expressions」(text/20-fm-learning-how-to-use-regular-expressions.txt:99,搜「submatches」;:19,搜「support@example.com」)。分组 (\w+)@(\w+\.\w+) 的语义是原文逐组解释的。 ↩

  19. 出处:「Learning how to use regular expressions」(text/20-fm-learning-how-to-use-regular-expressions.txt:137,搜「ReplaceAllString」;:24,搜「Replaced IDs」)。原文:「can be used not only for matching and extracting patterns but also for sanitizing or masking sensitive data」。 ↩

  20. 出处:「Learning how to use regular expressions」(text/20-fm-learning-how-to-use-regular-expressions.txt:152,搜「Validating Input」;:1,搜「^[a-zA-Z0-9._%+-]」)。{2,} 要求至少两个字母;^/$ 锚点见 107–116 行的 Note。 ↩

  21. 出处:「Working with command-line arguments」第 43–47 段(text/21-fm-working-with-command-line-arguments.txt:43,搜「os.Args」)。「a slice of strings where os.Args[0] holds the program name」。 ↩

  22. 出处:「Working with command-line arguments」(text/21-fm-working-with-command-line-arguments.txt:92,搜「Using the flag Package」;:98,搜「flag.Parse」;:104,搜「flag.Args」)。-flag/--flag 双写法和内建 -h 帮助是原文点到的。 ↩

  23. 出处:「The viper and cobra packages」(text/23-fm-implementing-which-1.txt:11,搜「widely used Go package」)。「Each command in cobra is represented as a *cobra.Command struct… via a Run() function」。 ↩

  24. 出处:「The viper and cobra packages」(text/22-fm-the-viper-and-cobra-packages.txt:36,搜「configuration management」)。viper 支持 JSON/TOML/YAML/HCL/INI、环境变量、命令行 flag,键值表见第 5–7 行。 ↩

  25. 出处:「Implementing which(1)」(text/23-fm-implementing-which-1.txt:72,搜「func which」)。完整实现同文件第 63–109 行;步骤解释见第 111–119 行。 ↩

  26. 出处:「Implementing which(1)」(text/23-fm-implementing-which-1.txt:97,搜「os.Stderr」;:13,搜「exit codes」)。「returning appropriate exit codes: 1 for errors and 0 (implicit) for success」。 ↩

  27. 出处:「Implementing basename(1)」(text/25-fm-summary.txt:26,搜「/opt/homebrew/bin/go」;:29,搜「///opt////」)。两个输出一行一个。 ↩