diff options
| author | Eltrac <hi@guhub.cn> | 2025-11-25 23:50:03 +0800 |
|---|---|---|
| committer | Eltrac <hi@guhub.cn> | 2025-11-25 23:50:03 +0800 |
| commit | 9f91c20d6ea80aab0f92c7c4f3551883b21e4997 (patch) | |
| tree | 0aa4ce3875a285e6329718c1e9146e85f71fada2 | |
| parent | 57033903dd5f89255d37d67728f2927a683f2b16 (diff) | |
docs: new post
Former-commit-id: 90c136454fe4641656e3eb1a1fb6b8ceb37d6699
| -rw-r--r-- | content/posts/go-philosophy.md (renamed from content/_drafts/go-philosophy.md) | 96 |
1 files changed, 91 insertions, 5 deletions
diff --git a/content/_drafts/go-philosophy.md b/content/posts/go-philosophy.md index d5820aa7..f65b2a1c 100644 --- a/content/_drafts/go-philosophy.md +++ b/content/posts/go-philosophy.md @@ -4,8 +4,8 @@ tags: - 编程 - 计算机 - 设计 -date: 2025-11-24T08:45:00 -draft: true +date: 2025-11-25T23:49:00 +draft: false toc: true --- @@ -565,7 +565,7 @@ func doSomeReflection(i interface{}) { 这句话直接翻译过来是:错误是值。 -接下来我们正式进入到 Go 的错误处理哲学。Go 语言完全抛弃了传统的 `try ... catch` 结构,将错误简单化为一个值,将错误处理从一种特殊的控制接口降级为简单的 `if ... else`。Go 还支持多函数返回值,也就是说,你可以将错误当成一个返回值返回给调用者,而不需要像 Java 一样抛出(throw)。 +接下来我们正式进入到 Go 的错误处理哲学。Go 语言完全抛弃了传统的 `try ... catch` 结构,将错误简化为值,将错误处理从一种特殊的控制接口降级为简单的 `if ... else`。Go 还支持多函数返回值,也就是说,你可以将错误当成一个返回值返回给调用者,而不需要像 Java 一样抛出(throw)。 在 Go 的错误处理中,程序员最常写的就是这样一段代码: @@ -593,17 +593,103 @@ try { 这意味着你需要把可能会抛出错误(异常)的代码提前包裹在 `try ... catch` 块中,而且你还创建一个新的作用域(scope),如果你在 `try ... catch` 里面获取了数据,就得直接在里面处理这个数据,或者在这个作用域外面先声明函数,再在里面使用。总之是有点麻烦的。更不用说,在 Java 里面你还需要考虑不同类型的异常,光是想到这些我都不想做错误处理了。 -Go 语言将错误作为值,不仅让错误处理变得简单直观,减少了一种控制结构,还让程序变得更灵活——既然错误是值,那你就可以把错误储存起来,像普通的值一样直接写进日志里,或者等完成一大批操作之后统一反馈给用户,甚至打印出来发送给你的妈妈看——最后一条是 Rob Pike 在演讲时开的玩笑。 +Go 语言将错误作为值,不仅让错误处理变得简单直观,减少了一种控制结构,还让程序变得更灵活——既然错误是值,那你就可以把错误储存起来,像普通的值一样直接写进日志里,放进一个 map 里,缓存起来,或者等完成一大批操作之后统一反馈给用户,甚至发送给你的妈妈看——最后一条是 Rob Pike 在演讲时开的玩笑。其他语言也可以做到,但在 Go 里面会更容易,更重要的是,你可以对错误进行编程,而不会被局限在 `try ... catch` 这一种控制结构里。 总之,错误在 Go 语言里是值,你可以用它干任何事情! ## Don't just check errors, handle them gracefully. +这句话直接翻译过来是:不要只是检查错误,要优雅地处理错误。 + +所谓的「gracefully」(优雅地)其实只是「intentionally」(有意识地),也就是不理所当然地使用一种错误处理方式,不理所当然地觉得「发生错误了,我直接输出错误信息就好了」,而应该思考:我应该不应该增加更多上下文信息?应该不应该保存这个错误,做完其他事情之后再打印出来? + +以下是写在[官方文档](https://go.dev/doc/effective_go#errors)里的,处理错误的例子: + +```go +for try := 0; try < 2; try++ { + file, err = os.Create(filename) + if err == nil { + return + } + if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC { + deleteTempFiles() // Recover some space. + continue + } + return +} +``` + +`for` 循环的目的是尝试创建文件两次,如果第一次成功(`err == nil`),就 `return` 跳出循环,不执行第二次;如果第一次失败,那就对 `err` 进行类型断言(type assertion),判断这是不是一个 `PathError`,并进一步对错误进行检查,如果发现是空间不足导致的错误,就调用 `deleteTempFiles()` 函数删除临时文件恢复一些空间,然后 `continue` 进行下一次循环,再次尝试创建文件。 + +总而言之,不要把错误抛出来之后就真的抛之脑后了,想想自己能对这个错误做些什么! + +## Don't panic. + +这句话并不是 Rob Pike 在前文提及的演讲中发表的,而是写在 Go 官方文档里的[一句话](https://go.dev/wiki/CodeReviewComments#dont-panic),由于它和前面两小节讨论的错误处理相关,我就插到了这里。 + +直接翻译过来是:不要恐慌(panic)。这里的 `panic` 指的是 Go 语言中用于错误处理的一个函数,表示程序遇到了**意料之外**且无法处理的错误,必须结束程序。这句话很直接地告诉你:不要使用 `panic()` 函数。 + +文档很明确地告诉 Go 程序员,不要把 `panic()` 用来处理常规错误,任何能被预料的、有办法处理的(诊断原因并打印错误也是一种处理),都应该使用 `error` 类型的值,并将这个值作为函数的额外返回值返回给调用者,然后调用者再检查 `if err != nil` 来处理错误。原因在于,`panic()` 会直接结束程序,只有在程序真的完全没有办法继续执行,发生了无法预料的致命错误时,才应该使用 `panic()`。 + +即使是这样,`panic()` 也可以被恢复,即使程序被 `panic()` 停止,`defer` 的函数也会执行(前文有提及),程序员可以在 `defer` 函数中使用 `recover()` 函数恢复错误。以下是官方文档中的例子。 + +```go +func server(workChan <-chan *Work) { + for work := range workChan { + go safelyDo(work) + } +} + +func safelyDo(work *Work) { + defer func() { + if err := recover(); err != nil { + log.Println("work failed:", err) + } + }() + do(work) +} +``` + +简单来说,除非真的没办法处理,否则不要 `panic()`。 + ## Design the architecture, name the components, document the details. +这句话直接翻译过来是:设计架构,命名组件,用文档展开细节。 + +比起编程语言的基础知识,这条更像是软件工程的经验之谈,Rob Pike 说这是在 Go 语言中设计大型软件系统的方式,不过这对任何语言来说似乎都大有裨益。这句话总结了系统设计的三步骤: + +1. 设计架构:宏观上,完成这个软件系统,我需要哪些组件(模块)? +2. 命名组件:给每个组件起个好名字,清晰的名字会让系统更容易被理解,最好是让人看到名字之后就立马能知道各个组件的职责。 +3. 用文档展开细节:就像命名变量和函数一样,清晰直观固然能提升可读性,但也免不了要写注释。一些名字说不清楚的细节,就在文档里写清楚。 + ## Documentation is for users. -## Don't panic. +这句话直接翻译过来是:文档是给用户的。 + +> A lot of time when people write documentation, they say "this is what the function does", and they don't think about "what the function is for". +> +> 很多时候人们写文档,会写「这个函数会做这个事情」,而他们不会想「这个函数是拿来干嘛的」。 + +这条也更像是工程智慧,而非语言的基础知识。在我的理解力,「这个函数是拿来干嘛的」其实是提供了相关的上下文,即「为什么一开始要设计这个函数」「为什么用户会用到这个函数」。在上一小节的例子中,`safelyDo()` 函数封装了 `do()` 函数,用于恢复可能出现的 `panic`,如果只考虑「做什么」,文档就应该是这样: + +> `safelyDo()` 调用 `do()`,并恢复潜在的 `panic`。 + +但用户可能会感到困惑:我为什么会需要恢复潜在的 `panic`?不恢复会有什么问题吗?如果这样写,就会清晰不少: + +> 由于 `do()` 可能会触发不可恢复的 `panic` 错误,为了保证程序能继续执行,`safelyDo()` 会恢复错误并不中断程序。 + +--- + +## 我的想法 + +总结下来,我认为 Go 语言最鲜明的几条观点是: + +1. ==代码的可读性和可维护性比开发效率更重要。== 不要贪快写聪明的代码而忽略了其他人能不能读懂、几个月后的自己能不能读懂,不要因为自己的好恶影响了代码的风格,不要因为不愿意复制粘贴就引入动态的依赖,不要因为方便就滥用强大但不安全且不跨平台的 `syscall`、Cgo 或 `unsafe`,减少使用不清晰的反射。我认为这是**反技术债务**且**长期主义**的,这避免了给未来的自己和开发团队留下隐患、增加工作量。 +2. ==程序员必须要有清晰的头脑,而且不能把任何事情当成理所当然,要思考每一个决策。== 使用 `interface{}` 接收任意类型很好用,但自己真的无法确定参数的类型吗?直接打印错误看起来很常规,但自己有没有办法恢复错误,或者打印错误时能不能提供更清晰的上下文信息以供排查问题?我要实现的需求真的只能用危险的 `unsafe` 或 Cgo 实现吗,如果一定要脱离 Go 的安全区,我愿不愿意接受代码随时可能崩溃的代价?代码复用和不重复造轮子真的是任何时候都必须遵守的铁律吗,这个涉及第三方库的需求我真的不能自己写吗? +3. ==不要轻易离开 Go 的保护。== Go 有自带的垃圾回收机制、安全的内存访问和各种安全机制,使用 Cgo 和 `unsafe` 就失去了 Go 的保障。 +4. ==保持代码的灵活性。== 无论是小巧的、积木式的接口、「错误是值」的思想,还是保证零值可用的做法,在我看来都是为灵活性服务的。小接口让抽象更强大,类型系统更灵活;错误是值意味着可以对错误做任何对值能做的事情;零值可用意味着无论有没有初始化,无论变量是不是空的,方法都可以被正常地调用。 + +目前看来,Go 就是一门既安全又灵活,写代码既优雅又清晰,没有痛苦且观点鲜明的编程语言。Go 能用更少的代码写出更健壮的程序,很难令人不爱。 [^1]: C 语言里还有用信号量(Semaphore)实现互斥的方法,不过似乎已经没有什么人用了。 |
