Golang中处理语法错误引发的Panic问题探讨

Golang中处理语法错误引发的Panic问题探讨 来源:Go by Example: Reading Files

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {
    dat, err := os.ReadFile("/tmp/dat")
    check(err)

我认为,如果 Go 语言能提供一种更简短的方式来“在遇到错误时触发 panic”,那将会非常棒。

考虑一下这种语法如何?

func main() {
    dat, .. := os.ReadFile("/tmp/dat")

如果你有充足的时间(并且你的雇主有充足的资金),那么详细处理每一个错误是没问题的。

但如果你正在创建一个最小可行产品(MVP),只是想测试你的假设,那么如果错误不为 nil 时,能有一个简单的方法触发 panic 会很好。

我思考这个问题已经好几天了,认为上述语法易于阅读,并且能简化 Go 语言。

你怎么看?


更多关于Golang中处理语法错误引发的Panic问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

10 回复

编写一个 MustReadFile 辅助函数来处理 panic 会更符合惯用法。

更多关于Golang中处理语法错误引发的Panic问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我正在尝试使用 http.ListenAndServe(),它似乎能够处理 HTTP 处理程序中的 panic。即使某个 HTTP 请求导致了 panic,服务器也会继续运行。

作为一名Go应用开发者,我可以调用数千个函数。

其中许多函数将err作为最后一个参数返回。我认为将函数数量翻倍,并为每个函数创建一个MustFoo()函数是没有意义的。

记住哪些函数有Must...()版本,哪些没有,是一种负担。

我原以为Go是为了减轻认知负荷。

而这正是隐式恐慌不会发生的原因。

隐式行为总是意味着额外的认知负担。

恐慌通常根本不应该发生。它们并非用于控制程序流程。

如果在 HTTP 请求期间读取文件时出现错误,你肯定不希望该请求只是将错误信息杂乱地记录到日志中,并给用户一个通用的 50x 响应。你希望以一种用户能够理解的方式,告诉他们发生了什么,并且说明这不是他们的错。

dat, .. := os.ReadFile("/tmp/dat")

我仍然认为使用实用的 .. 在遇到错误时引发恐慌是有道理的。

当然,如果你在编写一个可重用的库,这没有意义。在那里它确实没有意义。

但是,如果你在编写一个 HTTP 请求处理器,那么我认为这是可以的,因为 HTTP 服务器会处理恐慌。由 .. 引发的恐慌不会终止服务器,它只会终止当前的 HTTP 请求。

我仍然反对在这里使用 panic。数据库无法访问可能是一个临时问题;网络并非100%可靠,但如果你使用 panic,那将导致整个应用程序崩溃,而不仅仅是处理请求的 goroutine,除非你或许在中间件的某个地方延迟调用了 recover。如果你的应用程序在尝试检查这些权限时,有任何其他 goroutine 正在执行与数据库无关的工作,panic 将使整个进程崩溃,并使其他工作 goroutine 的数据处于未知状态。

也许我使用 Go 语言太久了,现在已经被洗脑得只看到它的优点了。

不,你没有 🙂

对于 @guettli 的最小可行产品(MVP)场景,泛型的 must() 方法似乎非常完美。

否则,引发恐慌(panicking)应该仍然是绝对的例外情况,仅用于在发生错误后没有合理的继续执行方式时。

显式的错误处理确实需要一些时间来适应,但清晰看到正常情况和错误情况下的执行路径所带来的好处,超过了代码冗长带来的不便。

func main() {
    fmt.Println("hello world")
}

否则,panic 应作为绝对的例外情况保留,仅用于在发生错误后无法以合理方式继续执行的场景。

这取决于具体的用例。想象一下应用开发的场景:你正在处理 HTTP 请求。身份验证已经完成,现在我检查请求是否具有相应的权限(通过 SQL),然后我从 SQL 数据库中获取一些数据。如果数据库无法连接,这两个步骤实际上什么都做不了。如果数据库宕机,返回 500 “内部服务器错误” 是完全合适的。我认为在这种情况下 “panic on err” 是可以接受的。

如果是编写库,我同意你的观点。在那里使用 “panic on err” 没有意义。但对于应用开发,我认为 “panic on err” 是可以的。

我认为这个改动不太可能被添加到语言中,主要有两个原因:

  1. 既然 Go 已经有了泛型,你几乎可以通过这样的函数获得所有功能:

    func must[T any](value T, err error) T {
        if err != nil {
            panic(err)
        }
        return value
    }
    
    func main() {
        dat := must(os.ReadFile("/tmp/dat"))
    

    需要注意的是,对于返回两个值的函数,你需要编写一个 must2 版本的函数,依此类推。

  2. Go 的核心在于显式的错误处理。语言设计者并不认为所有的 if err != nil 检查是语言设计的缺陷,反而认为这是一个特性。就像你可以对“正常”的返回值进行操作一样,你也可以对返回的错误进行操作(或者明确选择操作)。抛出异常/恐慌(panic)被视为与 goto 类似,因为它使得控制流难以追踪,而 Go 追求的是清晰明了、甚至有些“枯燥”、易于阅读甚至浏览的代码。

    另外请考虑,如果你最终需要将 main 函数中的逻辑移到一个单独的包中,你会希望将所有 panic 改为返回错误,这样你的包的使用者就可以检查错误,而不是用类似 defer func() { v := recover(); ... }() 这样的结构来保护每一次对包的调用。如果你使用你提议的 , .. := 语法或类似 must 的函数,你将不得不遍历代码并将其全部改为检查和返回错误(或者你必须编写一个程序来遍历抽象语法树并为你重写代码)。

也许我用 Go 太久了,现在已经被“洗脑”得只看到它的优点了(斯德哥尔摩综合症? 😄)但我想说的是,在原型设计阶段保留错误处理通常是有益的。有时,当我在构建某个东西时,我会问自己:“哦,对了,但我从哪里获取这个上下文信息呢?”或者“当这个出错时应该发生什么?”等等。

关于你提出的语法错误处理简化方案,我认为从Go语言设计哲学来看存在几个问题:

  1. Go明确区分错误和异常:panic设计用于不可恢复的程序错误,而非普通的错误处理。你的提案会模糊这个重要界限。

  2. 现有更简洁的方案:实际上已经有更符合Go习惯的简洁写法:

// 方案1:使用匿名函数立即执行
func main() {
    dat := func() []byte {
        data, err := os.ReadFile("/tmp/dat")
        if err != nil {
            panic(err)
        }
        return data
    }()
    
    // 使用dat...
}

// 方案2:创建辅助函数(推荐)
func mustReadFile(filename string) []byte {
    data, err := os.ReadFile(filename)
    if err != nil {
        panic(err)
    }
    return data
}

func main() {
    dat := mustReadFile("/tmp/dat")
    // 使用dat...
}

// 方案3:标准库中的must模式
import "text/template"

func main() {
    // 标准库中已有类似模式
    t := template.Must(template.New("name").Parse("text"))
}
  1. MVP开发的实际方案:对于快速原型开发,可以这样处理:
func check(err error, msgs ...interface{}) {
    if err != nil {
        if len(msgs) > 0 {
            panic(fmt.Sprint(msgs...))
        }
        panic(err)
    }
}

func main() {
    // 快速开发时使用
    dat, err := os.ReadFile("/tmp/dat")
    check(err, "读取文件失败")
    
    // 或者更简洁的
    check(os.WriteFile("/tmp/output", dat, 0644))
}
  1. 语法冲突问题:你提议的..语法在Go中已有明确含义(变长参数和切片展开),会造成语法歧义。

Go社区更倾向于保持错误处理的显式性,这是经过深思熟虑的设计选择。对于MVP开发,现有的check函数模式已经足够简洁,同时保持了代码的清晰性和可维护性。

回到顶部