错误处理、切片、函数、正则,以及两个 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 再无关系:
图说:书上原话——「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:
- 写模式并用反引号包起来。 如
`[A-Z][a-z]+`(一个大写字母 + 一个以上小写字母)。 反引号是 Go 的原始字符串:反斜杠不转义——写正则必须用它,否则\.会先被字符串层吃掉一层16。 - 编译。
re := regexp.MustCompile(pattern)。模式写错时 MustCompile 直接 panic—— 只对「模式是程序里写死的常量」安全;模式来自用户输入就得用会返回 error 的Compile()。 - 干活,四种常用法:
re.MatchString(text)—— 有没有匹配?返回 true/false;re.FindAllString(text, -1)—— 找出全部匹配,-1表示不限个数。 对 "Go Rust Python Zig C C++" 用[A-Z][a-z]+,得 [Go Rust Python Zig]—— C 和 C++ 因没有小写字母尾巴而被排除17;re.FindStringSubmatch(email)—— 抓分组。模式(\w+)@(\w+\.\w+)把邮箱劈成 用户名和域名两段括号,返回的切片第 0 个是整串匹配、之后依次是各组: 对 "support@example.com" 得 [support@example.com support example.com]18;re.ReplaceAllString(text, "XXXX")—— 全部替换。把订单号 "1234, 5678, 91011" 抹成 "Order IDs: XXXX, XXXX, XXXX"——脱敏就是这么做的19。- 校验输入:模式
^[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. 可带走的¶
- Go 没有异常:错误是最后一个返回值,业务错误永不 panic。
errors.Is比「错误是不是这个」,errors.As挖具体类型,%w包装时保留链。defer释放资源、recover接 panic——都只在函数边界处使用。- 切片 = (指针, 长度, 容量);append 超容量才搬家,搬家前改谁都互相可见。
s = append(s, x)的赋值不能省。 - 函数是一等公民;
...T变参;Go 1.23 起range可以直接迭在一个「调 yield 的函数」上。 - 正则用反引号原始字符串包;MustCompile 只适合编译期确定的死模式。
flag返回指针,取值加*;大工具用 cobra(命令树)+ viper(配置源)。- 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////」) |
-
出处:「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」。 ↩ -
出处:「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 挖字段成功。 ↩ -
出处:「Error handling」(
text/17-fm-error-handling.txt:147,搜「os.ErrNotExist」)。errors.Is(err, os.ErrNotExist)判「文件不存在」是原文示例。 ↩ -
出处:「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", ...))。 ↩ -
出处:「Error handling」(
text/17-fm-error-handling.txt:94,搜「riskyFileOperation」;:96,搜「recover」;:23,搜「Recovered from panic」)。defer + recover + panic 三件套在同一段代码里齐活。 ↩ -
出处:「Arrays and slices」第 42–55 段(
text/18-fm-arrays-and-slices.txt:42,搜「fixed-size」;:51,搜「value types」;:52,搜「carry their size」)。 ↩ -
出处:「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」。 ↩ -
出处:「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]。 ↩ -
出处:「Arrays and slices」(
text/18-fm-arrays-and-slices.txt:181,搜「Array after append」)。输出Array after append: [10 20 999 40 888]——append 写进了原数组。 ↩ -
出处:「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]。 ↩↩ -
出处:「Arrays and slices」第 76–81 段(
text/18-fm-arrays-and-slices.txt:77,搜「capacity of the slice doubles」)。make 三参数示例numbers := make([]int, 3, 5)在同段。 ↩ -
出处:「Functions」第 17–29 段(
text/19-fm-functions.txt:17,搜「first-class citizens」)。多返回、无默认参数无重载、匿名函数闭包、变参、方法,一段里全点齐。 ↩ -
出处:「Functions」第 116–126 段(
text/19-fm-functions.txt:117,搜「Introduced in Go version 1.23」;:133,搜「func(yield func(int) bool)」)。yield 协议与「返回 false 就提前终止」是原文明写的。 ↩ -
出处:「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」。 ↩ -
出处:「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」。 ↩ -
出处:「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 的区别在同段。 ↩↩ -
出处:「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++ 的解释在同段。 ↩ -
出处:「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+)的语义是原文逐组解释的。 ↩ -
出处:「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」。 ↩ -
出处:「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。 ↩ -
出处:「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」。 ↩ -
出处:「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帮助是原文点到的。 ↩ -
出处:「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」。 ↩ -
出处:「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 行。 ↩ -
出处:「Implementing which(1)」(
text/23-fm-implementing-which-1.txt:72,搜「func which」)。完整实现同文件第 63–109 行;步骤解释见第 111–119 行。 ↩ -
出处:「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」。 ↩ -
出处:「Implementing basename(1)」(
text/25-fm-summary.txt:26,搜「/opt/homebrew/bin/go」;:29,搜「///opt////」)。两个输出一行一个。 ↩