在Golang中实现Rust的unwrap()功能,求反馈
在Golang中实现Rust的unwrap()功能,求反馈
几个月前我开始用Go语言编程,现在逐渐熟悉这门语言后,我整理了一些实用函数并上传到了Github。其中一个是类似Rust中unwrap()的重新实现,旨在提供更便捷的错误处理。以下是包含所有解释和示例的文档:g package - github.com/n-mou/yagul/g - Go Packages,但长话短说:
import (
"os"
"github.com/n-mou/yagul/itertools"
)
// 这样写:
fileInfo := g.Unwrap(os.Stat("."))
// 等价于这样写:
fileInfo, err := os.Stat(".")
if err != nil {
panic(err)
}
我希望能得到Gopher们的反馈。我知道不应该每次函数返回错误时都触发panic。通常,如果我写的是库,我会传播错误;如果我写的是程序,我会单独处理错误。但有时触发panic是合适的做法(或者在处理了合理的预期错误后触发panic)。对于这些情况,Rust为Result类型实现了unwrap()函数,但在Go中没有现成的解决方案,你需要重复写那三行代码(if err != nil { panic(err) })直到筋疲力尽。这是我在像Go这样缺乏宏的语言中能写出的所有语法糖了。
我是唯一觉得这有用的人吗?如果不是,Unwrap是最好的名字吗?在Rust中它有意义,因为它解包了一个Result,但在Go中,“解包”这个术语用于错误处理(而且errors包中有一个名为Unwrap的函数),所以应该避免使用,但我想不出更好的名字了(除了Force,但同一个模块中的其他函数已经使用了这个名字)。你们怎么看?
提前感谢
更多关于在Golang中实现Rust的unwrap()功能,求反馈的实战教程也可以访问 https://www.itying.com/category-94-b0.html
func main() {
fmt.Println("hello world")
}
更多关于在Golang中实现Rust的unwrap()功能,求反馈的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你说得对。这个函数的初衷是作为原型设计时的语法糖。我怀疑在生产代码中这会有多大用处(如果是应用程序,我会尽量避免使用 panic;如果是库,则绝不会使用 panic)。在那个环境中,可读性应该优先于开发者的便利性。
一个需要注意的地方:实际上,执行 panic() 会在从函数返回之前运行所有延迟函数。否则,defer(func(){ recover()...}() 就无法像现在这样正常工作。
defer(func(){ recover()...}()
我不知道你在寻找什么,我看了你的实现代码,我想我明白你的想法了。 我只会在测试代码中使用这种类型的代码,而且正如我之前所说,在出现错误的情况下我不会使用 panic,所以我不会在正式代码中使用这样的代码。 在 panic 的情况下,直接写比再包装一层要直观得多。
哦,原来区别在这里。我之前不知道 panic 不会触发清理操作。我肯定会把它改成 log.Fatal()。我选择 panic 而不是 log.Fatal() 是因为它还会打印堆栈跟踪。在调用 log.Fatal() 之前,我该如何打印堆栈跟踪呢?
嗯,有意思。我也会包装我的错误,但我不能指望我使用的每一个第三方库都这么做,这就是为什么我想打印堆栈跟踪。所以,为了确保在退出前所有清理代码都已运行,我不能在 main() 函数之外的任何其他函数中退出程序……感谢所有建议,我会再考虑一下。
是的,在大多数情况下,我使用它来进行更快速的原型设计(毕竟,在一个成熟的应用中,几乎所有的错误都应该被处理)。这只是我在 Rust(一种以类似方式处理错误的语言)中看到的一种语法糖,在实验这门语言时,我想到将其引入 Go。我只是想知道其他在 Go 方面更有经验的人对此有何看法,看看是只有我一个人这样想,还是其他人也可能觉得它有用。感谢您的反馈。
最简单的区别在于,工具库不应引发恐慌,而应用程序(区别在于是否会被外部引用)和初始化过程(例如 init)可以适当地引发恐慌(例如,你的程序业务是写入文件,但文件无法打开,作为应用程序工具,那么引发恐慌是可以接受的)。
所有逻辑都为业务服务,这是我的理念。如果它影响了主要业务,那么我会毫不犹豫地停止,以避免造成无意义的工作(例如,Web 服务甚至无法监听套接字)(但如果只是路由异常,则不应停止整个服务)。
log.Fatal* 也不会触发延迟函数。如果你有任何延迟函数,它们将被跳过。我不使用追踪,因为我在错误出现的地方用扩展描述包装了错误,并且可以毫无问题地找到代码中的位置。如果你想要有追踪功能,可以尝试自定义错误类型,它可以保存额外的信息,例如:
type myError struct {
trace string
err error
}
func (m *myError) Error() string {
return fmt.Sprintf("%s\n%s\n", m.trace, m.err)
}
我明白了。我们都同意在有限的情况下,panic 是可以接受的,但问题在于我实现了一个会为用户 panic 的库。如果那个函数本应被用于另一个不同的目的,并且作为一个副作用它 panic 了而不是返回错误,我同意这是不可接受的。但在这种情况下,这个函数的唯一用例就是 panic,并且在文档和 README 中明确说明了这个函数旨在用作 panic 的语法糖。
然而,我理解之前的表述方式有误导性,我将在文档中添加一个警告,说明这个函数被认为是辅助原型设计的,并且在生产代码中 panic 几乎从来不是一个好的选择。感谢大家的反馈。
您还可以通过将错误处理移到其他地方来减少主函数中的错误,例如:
func main() {
// 设置和无错误处理的部分。
if err := run(); err != nil {
// 主函数中没有defer语句,清理工作已经完成,
// 因此可以直接使程序崩溃。
log.Fatalln(err)
}
// 如果没有错误,执行其他操作。
}
func run() error {
defer cleanup() // 在这里进行清理。
// 主逻辑放在这里...
if err := doSomething(); err != nil {
return fmt.Errorf("error in doSomething: %w", err)
}
// ...
return nil
}
嗯,这更多是关于个人偏好、习惯以及每个人偏好的工作流程方式。如果我在 main 函数中执行某些可能返回错误的操作,我会使用 log.Fatal*,它在底层会调用 Exit(1)。但当我添加了用于清理的 defer 语句或其他与优雅关闭相关的代码时,这种方法就会失效。如果你触发 panic 或直接退出,defer 将不会被执行。因此,我更倾向于控制我的运行时。你的例子更多是关于处理你作用域之外的错误。如果简单的 Mkdir 返回一个错误,这个错误来自底层操作系统,即使在这种情况下,我也看不出有任何理由要 panic,而不是像处理其他错误一样来处理它。
Rust 和 Go 在错误处理方面没有相似的方法。它们的基本相似之处在于将错误视为值,仅此而已。Go 社区内部有很多关于如何摆脱 err != nil 的讨论,但这是另一个需要讨论的话题。在这里,我认为,如果第三方库引发恐慌(panic),那它就不是一个好的库。我同意,我能看到其使用场景的地方可能只有测试,而且可能性很小。例如,我使用 Go 处理的所有工作在任何情况下都需要优雅地关闭,这就是为什么即使是 std 也会返回错误以便进行适当处理,而不是在运行时因恐慌而崩溃。相信我,编写 recover 恢复代码比就地执行 nil 检查更令人疲惫。
func main() {
fmt.Println("hello world")
}
我已经在我的帖子中说过,我并不总是使用它。我知道错误必须单独处理或传播到其他地方处理,但有时一个函数返回的错误是应用程序不应该处理的(例如:当内存不足、用户权限不足或硬件错误时,os.Mkdir 会返回错误,这些情况都不是用户空间应用程序应该处理的)。这并不是说每次遇到错误都要系统性地退出应用程序,我确实会单独处理错误,但有时应用程序并不打算处理某些类型的错误,如果收到这些错误就应该停止(就像我上面提到的例子)。
我并不反对将错误作为返回值,这与 Rust 的 Result 概念类似,处理方式也相似(在 Rust 中,你可以使用 unwrap_or_else() 和一个通过模式匹配错误类型来处理特定情况的代码块,但也有 unwrap() 函数,如果返回错误就会触发 panic,这不是为了系统性地忽略每个错误,而是为了处理这些特定情况)。
问题的核心在于,哪种写法更容易编写和阅读。
-
x := g.Must(foo())- 只提供通用的错误信息
- 可能无法意识到这段代码会引发恐慌
- 必须阅读
g.Must的源代码才能确定其工作原理 - 引入了来源不明/小众的额外依赖(存在安全隐患)
-
x, err := foo() if err != nil { panic(fmt.Errf("could not foo: %w', err)) }- 需要多写62个字符/3行代码(但其中大部分可以由IDE自动生成,并且可以通过IDE插件使其不那么显眼)
- 错误信息明确
- 任何Go开发者都能一眼看出发生了什么
我个人认为更详细的第二种形式更容易理解。既然代码被阅读的次数远多于被编写的次数,我几乎总是会选择“易于阅读”而不是“易于编写”。
另一方面,也许我遗漏了什么。所以,能否请你告诉我,在我实际应用中使用 panic 的具体案例?
这是一个用于备份的文件管理工具(一个程序,而非供他人使用的库)。在创建目录时,我必须检查目录是否已存在(如果存在,os.MkdirAll 不会返回错误),如果不存在则创建它。在研究 MkdirAll 可能返回哪些类型的错误时,我发现了这些情况(权限不足、内存存储问题以及较低级别的外部或网络驱动器问题)。这个工具本不应该处理这些错误,所以我使用了 panic,因为在我看来,panic 与打印错误信息及堆栈跟踪并以代码 1 退出之间并没有太大区别。
是否存在一个我忽略的理由,使得这样做:
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
比这样做更可取:
if err != nil {
panic(err)
}
感谢你的反馈。
这不是一个好的处理方式,实际上你只是把错误塞进了上下文里,并没有解决异常被捕获的情况。(你是想在遇到错误时直接 panic 吗?)如果是这样,我建议你不要那样处理。
看起来你只想要结果(xxx:=),而不是(xxx, err:=)。
我遇到过不止一个人这样想,实际上在我学习了 Go 语言之后再看其他语言,比如 Kotlin 或其他使用 try-catch 来处理错误的语言,那有点类似于 Go 的 panic-recover。
但事实上,这两者并不相同,如果你用这种方式处理 Go 语言的错误,那么我建议你不要这样做。
对于这类错误处理,Go 语言这种简单的逻辑可能看起来有些繁琐,但正因为如此,开发者能够掌握每一个产生错误的情况,并且更加直观,这是我非常喜欢的一点。
处理错误的正确做法是,不要忽略它(除非你知道自己在做什么)。如果能使用 error,就不要 panic。panic 的影响级别非常高,尽管有 defer 来处理后续步骤,但这并不是良好的开发实践。
如果你真的讨厌这种方式:1. 使用其他开发语言;2. 使用 IDE 的点号代码补全功能。
当然,我们绝不应该在第三方库中使用 panic,这个实验也并非提倡这样做。我的意思是,这就像是把 if err != nil { panic(err) } 写在一行里,并且只应在你原本就会使用 panic() 的极少数有限场景中使用。
关于 Go 和 Rust 的错误处理,我知道它们并不完全相同,我说的是相似。我所说的相似,是指它们都将错误作为返回值,而不是通过 try/catch 块来中断程序流程。我理解 Rust 的设计晚于 Go,其设计者可以调整方案(例如:可能失败(fallible)的函数不返回一个值和一个错误,而是返回一个类型联合体,以强制用户处理错误,因为在 Go 中确实存在错误被忽略而非被妥善处理的情况)。然而,尽管 Rust 被设计为必须处理结果而不能忽略错误,它也提供了 unwrap 函数。因此,当遇到程序确实应该 panic 的罕见情况时,有一种更符合人体工程学的方法来实现。
我也了解关于错误处理的讨论,并理解其繁琐之处(Go 被设计成一种简单的语言,具有最少的必要抽象和语法特性,这与 Rust 相反,尽管随着泛型和迭代器的加入,它已略微偏离了最初的愿景……)。我个人不会对它做太多改变,我认为将错误作为值返回远比 try/catch 块要好(我来自 Java 和 Python 背景)。不过,如果能有一个用于 panic 的单行语句,以及另一个用于返回错误的单行语句(某种“传播”关键字,当在函数中执行时,如果栈帧中最后一个类型为 error 的变量不为 nil,则停止函数执行并返回该错误以及函数返回类型的零值),那将会方便得多。如果 Go 支持元编程,我会写一个宏来实现同样的功能(一行代码等价于 if err != nil { return zeroVal, err })。但是,重申一下,Go 的设计初衷就是简单,而元编程与简单背道而驰。
尽管有以上所有观点,我知道我对这门语言还是新手,在参与语言设计的讨论之前,我还有很多东西需要学习。我喜欢 Go,它拥有巨大的优势,其中许多优势都源于它的简单性。因此,尽管存在这些轻微的不便,我仍然使用它。这个实验只是为了测试我是否能使其使用稍微顺畅一些。
在Go中实现类似Rust的unwrap()功能确实是个有趣的想法。从你的实现来看,g.Unwrap()函数通过panic来处理错误,这在特定场景下是合理的。以下是一些专业反馈:
实现分析
你的实现核心是:
func Unwrap[T any](val T, err error) T {
if err != nil {
panic(err)
}
return val
}
这种模式在某些场景下确实能减少样板代码,特别是在原型开发、测试或确定错误应该导致程序终止的情况下。
命名建议
关于命名,确实需要考虑Go的惯例:
Must前缀:Go标准库中常见模式,如template.Must()、regexp.MustCompile()PanicOnErr:更明确地表达行为AssertNoErr:强调这是断言式的检查
示例:
// 使用Must前缀
fileInfo := g.Must(os.Stat("."))
// 或更明确的命名
fileInfo := g.PanicIfErr(os.Stat("."))
使用场景示例
package main
import (
"encoding/json"
"fmt"
"github.com/n-mou/yagul/g"
)
func main() {
// 1. 配置加载(失败应终止程序)
config := g.Unwrap(loadConfig("config.json"))
// 2. 初始化必须成功的资源
db := g.Unwrap(connectDB(config.DSN))
// 3. 测试代码中的简化
result := g.Unwrap(processData(testInput))
fmt.Printf("Config: %+v\n", config)
}
func loadConfig(path string) (Config, error) {
data := g.Unwrap(os.ReadFile(path))
var cfg Config
err := json.Unmarshal(data, &cfg)
return cfg, err
}
type Config struct {
DSN string `json:"dsn"`
}
注意事项
- 性能影响:panic/defer/recover比正常错误处理开销大
- 可读性:对于不熟悉Rust的Go开发者,
Unwrap可能不够直观 - 错误上下文:panic会丢失调用栈的某些信息
替代实现考虑
可以考虑提供带上下文的版本:
func UnwrapWith[T any](val T, err error, msg string) T {
if err != nil {
panic(fmt.Errorf("%s: %w", msg, err))
}
return val
}
// 使用
fileInfo := g.UnwrapWith(os.Stat("."), "failed to stat current directory")
结论
你的Unwrap()实现为Go的错误处理提供了一个有用的工具,特别是在快速原型开发或确定错误应导致程序终止的场景。建议考虑使用Must前缀以符合Go的命名惯例,这样对其他Go开发者会更直观。


