summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
authorEltrac <hi@guhub.cn>2025-11-25 21:01:32 +0800
committerEltrac <hi@guhub.cn>2025-11-25 21:01:32 +0800
commit57033903dd5f89255d37d67728f2927a683f2b16 (patch)
tree194b6a39bab588dd47f8b73d66dcc318c540b353 /content
parentc931a96ac0c367e56afcebd83865ac4fb062fe69 (diff)
docs: update draft
Former-commit-id: 77a1103255814a1d9e1bc3cca02701828c3b5973
Diffstat (limited to 'content')
-rw-r--r--content/_drafts/go-philosophy.md159
1 files changed, 134 insertions, 25 deletions
diff --git a/content/_drafts/go-philosophy.md b/content/_drafts/go-philosophy.md
index 0cd17c87..d5820aa7 100644
--- a/content/_drafts/go-philosophy.md
+++ b/content/_drafts/go-philosophy.md
@@ -9,9 +9,9 @@ draft: true
toc: true
---
-从这个月 10 号,水逆期开始,我就开始减少出门,宅在家里读一读书,捡一捡法语学习,看看音乐剧,还找了些新东西来学,Go 语言就是其中一个。一开始只是抱着好奇心探索,并没有什么功利心,不过越学越发觉,Go 真的很适合用来写高并发高性能的 Web 应用(而且越学越讨厌 Java)。
+从这个月 10 号,水逆期开始,我就开始减少出门,宅在家里读一读书,捡一捡法语学习,看看音乐剧,还找了些新东西来学,Go 语言就是其中一个。一开始只是抱着好奇心探索,并没有什么功利心,不过越学越发觉,Go 真的很适合用来写高并发高性能的 Web 应用(而且越学越讨厌 Java),也完全能胜任系统开发,就业岗位貌似不少,竞争也相对更小。更重要的是,我非常喜欢这种「观点鲜明」的编程语言,我想我会一直用下去的。
-学任何一门编程语言我都更喜欢「不自量力」地直接上手写代码,一边写一边学基础语法,遇到不知道怎么实现的需求时再查阅文档。不过,学 Go 的一开始我发现了 [Go Proverbs](https://go-proverbs.github.io/) 这个网站,便在实战之外还花了不少时间研读这里的「名言警句」。这些 Go 语言编程相关的谚语大多来自 Rob Pike 在 2015 年的[一场演讲](https://www.youtube.com/watch?v=PAAkCSZUG1c),每一条都很有启发性,这篇文章就来逐条阐述我对它们的理解,其中有不少也可以用在 Go 语言以外的编程中。<!--more-->
+学任何一门编程语言我都更喜欢「不自量力」地直接上手写代码,一边写一边学基础语法,遇到不知道怎么实现的需求再查阅文档。不过,学 Go 的一开始我发现了 [Go Proverbs](https://go-proverbs.github.io/) 这个网站,便在实战之外还花了不少时间研读这里的「名言警句」。这些 Go 语言编程相关的谚语大多来自 Rob Pike 在 2015 年的[一场演讲](https://www.youtube.com/watch?v=PAAkCSZUG1c),每一条都很有启发性,这篇文章就来逐条阐述我对它们的理解,其中有不少也可以用在 Go 语言以外的编程中,希望能引发读者的一些思考。<!--more-->
## Don't communicate by sharing memory, share memory by communicating.
@@ -19,7 +19,7 @@ toc: true
这需要一些上下文来理解。Go 是一门可以通过非常优雅的方式实现并发的编程语言,异步编程若是要在多个线程之间同步信息,往往需要加锁,这是为了避免线程同时操作内存,导致冲突、数据准确性(integrity)被破坏等无法预测的问题。加锁一般指的是互斥锁(mutual exclusion object,简写作 `mutex`),线程操作数据时需要先获取对应的锁,如果锁被其他线程持有,则需要等待,获取到锁的线程才能执行操作,这保证数据操作是互斥的。
-加锁是异步编程中的常见做法[^1],但 Go 程序员认为「锁是痛苦的」,不仅要创建锁、管理锁,还要想办法避免死锁等运行时错误,这类错误没法被编译器察觉,程序员排查起来很困难。所以,为了保证程序的安全性和可读性,应当避免使用锁。使用互斥锁保证数据通信安全的方法,就是这句话的前半句「communicate by sharing memory」,因为共享了内存,所以必须加锁,不然就会出现问题,而锁又会带来更多难题。
+加锁是异步编程中的常见做法[^1],但「锁是痛苦的」,不仅要创建锁、管理锁,还要想办法避免死锁等运行时错误,这类错误没法被编译器察觉,程序员排查起来很困难。所以,为了保证程序的健壮性和可读性,应当避免使用锁。线程之间之所以要用锁来实现互斥,是因为共享了同一块内存区域,而共享内存是为了传递数据,也就是这句话的前半句「communicate by sharing memory」。共享了内存就必须加锁,不然就会出现问题,而锁又会带来更多难题。
Go 语言鼓励的「share memory by communicating」,要用到 Go 语言的一个特性—— `channel`。你可以把 `channel` 理解为在不同线程之间传递数据的「通道」。以下是一个整数类型的 `channel` 例子。
@@ -41,11 +41,11 @@ Go 语言实现并发的方式非常优雅,而 `channel` 的存在又简化了
这句话直接翻译过来是:并发不是并行。
-这其实是计算机科学的基础知识,并发(concurrency)和并行(parallelism)本来就不是一回事。并发指的是程序在处理器上交替执行,在单核处理器上,一个进程完全占用 CPU 资源,完成计算或者用完时间片之后,其运行的上下文会被保存,然后 CPU 进行上下文切换(context switch),换上下一个要执行的进程的上下文,然后运行下一个进程。每个进程执行时都完全占用一个处理器内核,时间到了之后就让下一个进程执行,只不过进程在 CPU 上切换的速度非常快,宏观上让人感觉是在同步执行。并行指的是真正的同步执行,在多核处理器上,不同的进程在不同的处理器内核上执行,而不需要交替执行。
+这其实是计算机科学的基础知识,并发(concurrency)和并行(parallelism)本来就不是一回事。一般来说,并发指的是程序在处理器上交替执行,在单核处理器上,一个进程完全占用 CPU 资源,完成计算或者用完时间片之后,其运行的上下文会被保存,然后 CPU 进行上下文切换(context switch),换上下一个要执行的进程的上下文,然后运行下一个进程。每个进程执行时都完全占用一个处理器内核,时间到了之后就让下一个进程执行,只不过进程在 CPU 上切换的速度非常快,宏观上让人感觉是在同步执行。并行指的是真正的同步执行,在多核处理器上,不同的进程在不同的处理器内核上执行,而不需要交替执行。
-高级语言编程一般是跨平台的,不需要考虑硬件底层,也就是说,编程语言不关心执行程序的计算机究竟有多少个处理器核心。Go 语言强调并发并非并行的原因是,Go 把并发当作一种「编程结构」,即组织程序的方式,而并行是否能实现要取决于硬件和调度器,编程语言无法决定,也就是说,在编程语言层面无法实现并行。程序员在进行多线程编程时,应该聚焦于程序结构而非硬件底层,而且也不能理所应当地认为自己用了并发的结构,程序就一定是并行的。
+Go 语言是跨平台的,不需要考虑硬件底层,也就是说,编程语言不关心执行程序的计算机究竟有多少个处理器核心。Go 语言强调并发并非并行的原因是,Go 把并发当作一种「编程结构」,即组织程序的方式。程序员在进行多线程编程时,应该聚焦于程序结构而非硬件底层,如果关注程序结构,就能写出更清晰、可读、可维护的多线程程序代码;而且,也不能理所应当地认为自己用了并发的结构,程序就一定是并行的。
-Go 语言里实现并发非常简单,假设我们有一个 `FetchData()` 函数,而这个函数需要进行大量 IO 操作或者需要发送 HTTP 请求,如果同步执行会长时间阻塞主线程,要把这个函数变成并发的,只需要在调用函数时,在前面加一个关键词 `go`。
+Go 语言里实现并发非常简单,假设我们有一个 `FetchData()` 函数,而这个函数需要进行大量 IO 操作或者需要发送 HTTP 请求,如果同步执行会长时间阻塞主线程,要把这个函数变成并发的,只需要在调用函数时,在前面加一个关键词 `go`。这有点像 JavaScript 里的 `async`,但并不相同。
```go
go FetchData() // 这样就创建了一个线程执行 FetchData()
@@ -73,15 +73,15 @@ go func() {
stat = NewStatistics(<-fmlChan, <-caskChan, pkgName, <-rvsChan)
```
-总结一下,关注程序结构,写出更清晰的代码,这是并发;至于并行,那是硬件和操作系统要考虑的事情。
+总结一下,关注程序结构,写出更清晰的代码,在逻辑结构上让代码异步执行,这是并发;至于并行,那与硬件底层和操作系统有关,不能认为按并发结构编写的代码就一定是并行的。
## Channels orchestrate; mutexes serialize.
这句话直接翻译过来是:Channels 编排;互斥锁串行。
-尽管 Go 提供了在大部分时候都更好用的 `channel`,也更鼓励使用 `channel`,但互斥锁并不是被禁止的,也没有从这门语言里消失,因为有些时候必须要用到锁。这句话简单回答了「什么时候该用 channel?」「什么时候该用锁?」这个问题,也能帮助 Go 程序员更好地理解 `channel` 的用途。
+尽管 Go 提供了在大部分时候都更好用的 `channel`,也更鼓励使用 `channel`,但互斥锁并不是被禁止的,也没有从这门语言里消失,因为有些时候必须要用到锁。这句话简单回答了「什么时候该用 channel?」「什么时候该用锁?」这两个问题,也能帮助 Go 程序员更好地理解 `channel` 的用途。
-编排这个词很有意思,它的原文是 orchestration,又名 choreography,意思分别是「编配管弦乐曲」和「编舞」。如果你理解概念,就会觉得这个描述非常生动形象。这个词在不同的语境下的意思有一些区别,但一般来说,是指自动化的协调管理、统一掌控和分工协作。在管弦乐(orchestration)中,各个演奏者各司其职,但由指挥家统一主导;在舞蹈(choreography)中,舞者之间需要相互沟通,分工协作。这引申到编程中,对应了不同的编程结构,下面用简单的例子来解释。
+编排这个词很有意思,它的原文是 orchestration,又名 choreography,意思分别是「编配管弦乐曲」和「编舞」。如果你理解概念,就会觉得这个描述非常生动形象。这个词在不同的语境下的意思有一些区别,但一般指代一种自动化的工作流,可以统一调度、掌控多个作业,高效处理大量用户请求,或者让多个程序之间相互协作等等。在管弦乐(orchestration)中,各个演奏者各司其职,但由指挥家统一主导;在舞蹈(choreography)中,舞者之间需要相互沟通,分工协作。这引申到编程中,对应了不同的编程结构,下面用简单的例子来解释。
假设有一项操作需要大量计算,这些计算被分散开,启动多个 `goroutines` 执行,最后由主线程将计算的结果整合起来,Go 语言代码可以这样写:
@@ -91,21 +91,22 @@ stat = NewStatistics(<-fmlChan, <-caskChan, pkgName, <-rvsChan)
jobs := make(chan int, 100)
results := make(chan int, 100)
-// 启动 10 个 Worker 执行 100 个作业
-for _ := range 10 {
- go Worker(jobs, results)
+// 启动 10 个 Worker 执行这些作业
+for w := range 10 {
+ go Worker(w, jobs, results)
}
// 传入 100 个作业给 jobs channel
// worker 接收到之后就会执行
+// 执行完一个作业之后会继续接收 jobs channel 里的剩余作业
for j := range 100 {
jobs <- j
}
+close(jobs)
// 收集 100 个作业结果
for _ := range 100 {
- result := <- results
- doSomethingWith(result)
+ doSomethingWith(<- results)
}
```
@@ -195,7 +196,7 @@ var ops atomic.Uint64
ops.Add(1)
```
-总而言之,`channel` 用于高效地编排程序,而 `mutex`(互斥锁)用于严格地保证串行。
+总而言之,`channel` 用于高效、灵活地编排程序,而 `mutex`(互斥锁)用于安全、严格地保证串行。
## The bigger the interface, the weaker the abstraction.
@@ -294,7 +295,7 @@ func (*fw FileWriter) Write() { ... }
Zero value 指的是变量被声明但尚未初始化(赋值),类型系统自动为变量填充的值。对字符串来说,这个值是 `""`,整数是 `0`,浮点数是 `0.0`,布尔值是 `false`,指针是 `nil`。结构体被声明时,其中的变量也会被填充零值。
-Go 语言鼓励程序员好好利用这个零值,让类型即时没有初始化也能正常使用。标准库里有很多例子,前面提到的 `sync.Mutex` 就是一个。让我们回看这个例子:
+Go 语言鼓励程序员好好利用这个零值,让类型即使没有初始化也能正常使用。标准库里有很多例子,前面提到的 `sync.Mutex` 就是一个。让我们回看这个例子:
```go
// 例子来源:https://gobyexample.com/mutexes
@@ -323,7 +324,7 @@ io.Copy(os.Stdout, &b)
字节缓冲区没有初始化,仅仅是声明过后就可以直接调用 `Write()` 方法使用。如果是在 Java 语言里,你大概需要 `new` 一个类的实例才行,或者用到别的构造方法,总之做不到声明即用。
-Go 语言之所以强调「让零值可用」「声明即可用」,很大的一个原因是在 Go 语言里,空指针(`nil`)可以用来调用违背初始化的类型的方法。假设你有一个名为 `Config` 的结构体类型,你为它编写了 `Path()` 方法,你声明一个 `Config` 之后并没有指定路径,甚至 `Config` 本身就是空的,`Path()` 也可以返回一个默认值。
+Go 语言之所以强调「让零值可用」「声明即可用」,除了让代码更简单易读、编写程序更简单,另一个原因是在 Go 语言里,空指针(`nil`)可以用来调用未被初始化的类型的方法。假设你有一个名为 `Config` 的结构体类型,你为它编写了 `Path()` 方法,你声明一个 `Config` 之后并没有指定路径,甚至 `Config` 本身就是空的,`Path()` 也可以返回一个默认值。
```go
func (c *Config) Path() string {
@@ -345,10 +346,12 @@ func (c *Config) Path() string {
`interface{}` 常常被用来接收不确定类型的值,很多时候这种操作是必要的,但 Rob Pike 指出,很多人滥用了空接口类型,他说:
> You might as well write in Python.
+>
+> 你还不如去写 Python 呢。[^2]
-将代码保持在 Go 严格的类型系统中,有助于保持程序的安全性。除此之外,空接口最大的问题是「不够清晰」——清晰的代码可读性是 Go 语言的核心之一。程序员在如果调用了一个函数,结果发现要传入的参数是一个空接口,即任意类型都可以,那他就要多花些时间思考,就算去读函数实现也要费些功夫。简单来说,使用空接口类型让函数的输入和行为都变得不可预测,代码更难阅读,而且更难发现 Bug。
+将代码保持在 Go 严格的类型系统中,有助于保持程序的安全性。除此之外,空接口最大的问题是「不够清晰」——清晰的代码可读性是 Go 语言的核心之一。使用空接口类型让函数的输入和行为都变得不可预测,代码更难阅读,而且更难发现 Bug。
-在 Rob Pike 这场演讲之后发布的 Go 1.18 中,Go 语言添加了 `any` 类型,是 `interface{}` 的别名。添加这个别名的主要原因是让代码变得更可读,人们看到 `any` 就会知道在这里传入任何类型的值都可以,而不熟悉 Go 的人看到 `interface{}` 可能会觉得困惑。即使是 `any`,在使用前也要想清楚自己是不是真的没有办法预测数据类型,还是只是为了图省事而牺牲了安全性和可读性。
+在 Rob Pike 这场演讲的几年之后,Go 发布了 1.18 版本,其中添加了 `any` 类型,是 `interface{}` 的别名。添加这个别名的主要原因是让代码变得更可读,人们看到 `any` 就会知道在这里传入任何类型的值都可以,而不熟悉 Go 的人看到 `interface{}` 可能会觉得困惑。即使是 `any`,在使用前也要想清楚自己是不是真的没有办法预测数据类型,还是只是为了图省事而牺牲了安全性和可读性。
合理使用 `any` / `interface{}` 的例子是标准库里的 `fmt.Println()` 方法,程序员的确无法预知要打印的是字符串、数字还是别的什么类型。
@@ -366,22 +369,24 @@ func (c *Config) Path() string {
这句话直接翻译过来是:一点点复制比一点点依赖更好。
-大部分程序员都在强调代码复用性(reusability),排斥重复造轮子,追捧 DRY(Don't Repeat Yourself)的理念,这导致许多程序员在编写简单的需求时也会使用大量的第三方依赖库,即时他们只调用其中的一两个方法,即时这些依赖库完全可以自己用三五行代码封装来代替。
+大部分程序员都在强调代码复用性(reusability),排斥重复造轮子,追捧 DRY(Don't Repeat Yourself)的理念,这导致许多程序员在编写简单的需求时也会使用大量的第三方依赖库,即使他们只调用其中的一两个方法,即使自己完全可以封装一个三五行代码的函数来实现相同的需求。
Rob Pike 强调:要保持一个小巧干净的依赖树。维护依赖是痛苦的,因为依赖是**动态**的,如果依赖库有更新、出了问题,使用了这个依赖库的代码往往也需要跟进。这个教训,已经被 JavaScript 程序员吃尽了,JavaScript 庞大的生态意味着几乎没有一个 JavaScript 项目不大量使用依赖库,而一个 JavaScript 项目要是有几个月的时间没有更新,代码很可能就过时甚至用不了了。
+所以:
+
1. 在大多数情况下都尽量依赖标准库;
2. 需要更复杂的工具时,优先考虑封装在自己的代码库里;
3. 如果几行代码就能解决,就不要引入依赖;
-4. 如果考虑周全,得出结论认为一定要使用第三方库,这时候引入依赖才是最好的做法。
+4. 如果考虑周全,得出结论认为一定要使用第三方库,这时候才引入依赖。
-总而言之,头脑清晰的「C-V 工程师」比喜欢尝试各种库的「时髦工程师」写出来的代码更稳定!
+总而言之,**头脑清晰**的「C-V 工程师」比喜欢尝试各种库的「时髦工程师」写出来的代码更稳定!
## Syscall must always be guarded with build tags.
这句话直接翻译过来是:Syscall 必须总是被构建标签守护。
-`syscall` 即系统调用,让程序从用户态访问内核态的操作,像操作系统发出请求,完成较高权限的读写。系统调用很强大,很多情况下也是必要的,尤其是在系统级开发下几乎必不可少,但系统调用的问题是:它不跨平台。
+`syscall` 即系统调用,允许程序从用户态访问内核态,像操作系统发出请求,完成需要系统级权限的操作。系统调用很强大,很多情况下也是必要的,尤其是在系统级开发下几乎必不可少,但系统调用的问题是:它不跨平台。
Windows、macOS 以及各种千奇百怪的 Linux 发行版,各自的 `syscall` 都有很大差异,几乎不能写出通用的代码。要保证全平台通用,就应该用 `os` 库;但如果必须用 `syscall`,就要用 Go 编译器的「构建标签」(build tags)。
@@ -404,7 +409,7 @@ go build -tags pro
这句话直接翻译过来是:Cgo 必须总是被构建标签守护。
-Cgo(读作 Seagull,即海鸥)指的是 Go 语言标准库里的 `C` 库,这个库允许程序员在 Go 语言中调用 C 标准库,还可以在注释里写 C 代码并使用在 Go 代码中。
+Cgo(读音类似 Seagull)指的是 Go 语言标准库里的 `C` 包,这个包提供的方法允许程序员在 Go 语言中调用 C 标准库,还可以在注释里写 C 代码并使用在 Go 代码中。
```go
// 例子来源:https://go.dev/wiki/cgo
@@ -434,7 +439,7 @@ func Example() {
这句话直接翻译过来是:Cgo 不是 Go。
-这句话强调的是,Go 语言提供性能良好的垃圾回收机制、安全的类型系统、安全的操作内存的方式,编写纯粹的 Go 代码可以保证准确性、稳定性、安全性以及代码可读性。相反,C 语言本身并不是一门安全的语言,需要程序员手动 `free()` 释放内存。如果使用 Cgo,就会失去 Go 语言自带的各种安全机制,很容易出现难以排查的问题。
+这句话强调的是,Go 语言提供性能良好的垃圾回收机制、安全的类型系统、安全的操作内存的方式,编写纯粹的 Go 代码可以保证准确性、稳定性、安全性以及代码可读性。相反,C 语言本身并不是一门安全的语言,需要程序员手动 `free()` 释放内存,容易出现段错误等等。如果使用 Cgo,就会失去 Go 语言自带的各种安全机制,很容易出现难以排查的问题。
Rob Pike 建议,尽量避免使用 Cgo,你多半不需要用到它。
@@ -488,10 +493,110 @@ if isAnimal {
}
```
+还有那些过度复杂的条件表达式,看起来很聪明,但可读性非常差。
+
+```go
+// ❌
+
+if thisVar <= thatVar || (isLikeThis && fetchStatus() != status.OK) {
+ return
+}
+
+// ✅
+
+if thisVar <= thatVar {
+ return
+}
+
+statusOK := (fetchStatus() != status.OK)
+if isLikeThis && !statusOK {
+ return
+}
+```
+
+更直接、简单、清晰的代码不仅提高可读性,让维护代码变得更容易(这对自己和团队成员都是好事),也变相提高了代码稳定性,因为清晰的代码更容易排查问题。
+
+执拗地写聪明但不清晰的代码有点像某种大男子主义的攀比心,像是「我昨天熬夜到凌晨四点把工作做完了!」,许多人都会为这种刻苦但实际上是耍帅的精神鼓掌,然而,人的认知能力在凌晨四点到六点比喝醉了酒还要差[^3],而且熬夜对身体的伤害不必多说。
+
## Reflection is never clear.
+这句话直接翻译过来是:反射永远不清晰。
+
+反射(Reflection)是很多高级语言里都有的机制,用于在程序**运行时**检视和操作对象。有的信息在程序运行之前无法预知,要在程序中使用这些不可预知的值,就可以用到反射;比如前面提到的空接口,如果函数接收一个空接口类型(任意类型)的值,但需要根据值的类型执行操作,就可以利用反射来获取这个值的类型。
+
+反射也带点 Meta(元)的意味,经常被称作 Metaprogramming(元编程),因为它可以被用来检视程序自身的结构,比如 Java 语言中就可以利用反射机制获取某个类的注解、构造函数、成员变量等等,并把它们当作普通的对象来使用。
+
+在 Go 语言提供的 `reflect` 包里,大致有以下几种方法:
+
+```go
+// 接收一个 interface{} 类型的值,即任意类型的值
+func doSomeReflection(i interface{}) {
+ // 获取 i 的类型
+ // 一个定义在 main 包里、名为 User 的结构体会返回 main.User
+ t := reflect.TypeOf(i)
+
+ // 获取 i 的底层类型
+ // 同样一个名为 User 的结构体,会返回 struct
+ k := reflect.KindOf(i)
+
+ // 获取 i 的值
+ v := reflect.ValueOf(i)
+ // 如果要获取值的类型,就可以这样写
+ vk := reflect.ValueOf(i).Kind()
+
+ // 如果 i 是一个结构体,可以用这个方法获取它的字段数量
+ nf := reflect.NumField(i)
+
+ // Field() 方法用于获取字段,配合上面的方法可以遍历结构体
+ for n := 0; n < numberOfFields; n++ {
+ // 这里会输出该字段的类型和值
+ fmt.Printf("Type:%T, Value:%#v", value.Field(n), value.Field(n))
+ // 这里输出的是该字段的底层类型
+ fmt.Println("Kind is ", value.Field(i).Kind())
+ }
+}
+```
+
+至于 Rob Pike 为什么说「反射不够清晰」,我想读完上面的代码已经不言自明了,反射就是很难读。我认为最根本的原因是,在代码中引入反射,迫使程序员用另一个完全不同的视角(元视角)来审视代码,这种视角的转换是很困难的,而且往往要同时使用常规的编程逻辑和元编程逻辑才能读懂反射。
+
+这并不是说反射不好,有的需求必须要用反射实现。只不过,清晰永远比聪明更好,如果有更清晰的、不涉及反射的方法,那就不要用反射。
+
## Errors are values.
+这句话直接翻译过来是:错误是值。
+
+接下来我们正式进入到 Go 的错误处理哲学。Go 语言完全抛弃了传统的 `try ... catch` 结构,将错误简单化为一个值,将错误处理从一种特殊的控制接口降级为简单的 `if ... else`。Go 还支持多函数返回值,也就是说,你可以将错误当成一个返回值返回给调用者,而不需要像 Java 一样抛出(throw)。
+
+在 Go 的错误处理中,程序员最常写的就是这样一段代码:
+
+```go
+result, err := GetSomeData()
+if err != nil {
+ // 在这里处理错误
+}
+```
+
+如果是 Java 等依赖 `try ... catch` 进行错误(异常)处理的语言,你需要这样写。
+
+```java
+try {
+ // 其他相关的代码
+ ResultType result = GetSomeData()
+ // 继续其他相关的代码
+ // ...
+} catch (IOException e) {
+ // 处理 IO 异常
+} catch (Exception e) {
+ // 处理其他类型的异常
+}
+```
+
+这意味着你需要把可能会抛出错误(异常)的代码提前包裹在 `try ... catch` 块中,而且你还创建一个新的作用域(scope),如果你在 `try ... catch` 里面获取了数据,就得直接在里面处理这个数据,或者在这个作用域外面先声明函数,再在里面使用。总之是有点麻烦的。更不用说,在 Java 里面你还需要考虑不同类型的异常,光是想到这些我都不想做错误处理了。
+
+Go 语言将错误作为值,不仅让错误处理变得简单直观,减少了一种控制结构,还让程序变得更灵活——既然错误是值,那你就可以把错误储存起来,像普通的值一样直接写进日志里,或者等完成一大批操作之后统一反馈给用户,甚至打印出来发送给你的妈妈看——最后一条是 Rob Pike 在演讲时开的玩笑。
+
+总之,错误在 Go 语言里是值,你可以用它干任何事情!
+
## Don't just check errors, handle them gracefully.
## Design the architecture, name the components, document the details.
@@ -501,3 +606,7 @@ if isAnimal {
## Don't panic.
[^1]: C 语言里还有用信号量(Semaphore)实现互斥的方法,不过似乎已经没有什么人用了。
+
+[^2]: 此处并非嘲讽 Python 语言和 Python 程序员,这么说是因为 Python 是一门非常灵活的**动态类型语言**。Rob Pike 的意思应该是:滥用 `interface{}` 就像是抛弃了类型系统,把 Go 当作动态类型语言来说,如果这么做的话,用真的动态类型语言会更好。
+
+[^3]: 来源:《[绝佳时间](/library/2025/绝佳时间/)》