Golang如何减少错误处理中的重复代码

Golang如何减少错误处理中的重复代码 我相信很多人都已经看到了,这里有一个有趣的提案:

discussion: spec: reduce error handling boilerplate using ? · golang/go ·...

这是关于 Go 语言中最具争议性话题之一——错误处理——的一项修改。我唯一的担忧是:对于我们这些喜欢 Go 当前错误处理方式的人来说,这可能会把事情搞得更复杂(现在我们有了更多的处理方式),同时并没有真正解决批评者们真正想要的东西(比如在中间件等地方被自动处理的异常)。

在某些情况下,我确实看到了它的好处,比如当你有很多可能返回错误的函数时:

func DoLotsOfStuff() error {
	mightReturnError1() ?
	mightReturnError2() ?
	mightReturnError3() ?
	mightReturnError4() ?
	return nil
}

但我不太能看出下面这种情况有太多好处:

// 新方式:
r := SomeFunction() ? {
	return fmt.Errorf("something failed: %v", err)
}
// ... 对比旧的等效写法:
r, err := SomeFunction()
if err != nil {
	return fmt.Errorf("something failed: %v", err)
}

无论如何,这可能是自泛型引入以来最大的语言变化之一。大家对此有什么看法?


更多关于Golang如何减少错误处理中的重复代码的实战教程也可以访问 https://www.itying.com/category-94-b0.html

17 回复

我已经观察了一段时间。 似乎没有办法阻止那些人的热情。 我只能希望集成开发环境不会向我推荐这种方法…😢

更多关于Golang如何减少错误处理中的重复代码的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的——当 doSomething() 返回的不仅仅是错误时,将 val 的作用域限定在 else 代码块内,这是我不喜欢使用单行写法的地方。显然,这正是该提案试图解决的主要问题之一。

你说得对,之前的说法确实过于宽泛了。我会将其限定为那些仅用于缩短或高亮代码的简单视觉插件。在这种情况下,一个简单的区域折叠功能就可以将 if-err 部分折叠成单行,而无需改变语法。

在我看来,如果能够使用现有的单行错误检查,而无需为除错误值之外的其他值标注类型,那本身就已经是一种进步了,例如:

// var val type <- 避免这样做,除非要使用变量遮蔽。

if val, err := doSomething(); err != nil {
    // 处理错误或返回。
}

根据我个人的经验,在所有的生产代码中,我最终总是会回过头来,为几乎每一个 if err != nil { return err } 添加独立的错误信息。因为大多数错误都有不同的原因,需要采取不同的处理措施。

如果你不喜欢它的外观,也有编辑器插件可以隐藏或最小化 if err.... 的显示。如果能通过编辑器插件解决问题,就不应该改变语言的基本语法。

我理解你的观点。我喜欢 Go 中错误处理的工作方式,并且对这个改动感到犹豫。社区似乎也相当分裂。我很欣赏泛型以及 net/http 中路由的更新。那是一个真正改善了我日常工作的生活质量提升。

无论如何,我能理解他们为什么要这样做。正如讨论中提到的,错误处理是 Go 中被抱怨最多的事情。但这感觉就像有人学了 Rust 然后抱怨借用检查器;而借用检查器恰恰是 Rust 的核心所在。

我认为这对于原型设计来说是一个不错的功能。但在实际应用中,你很可能很少会直接返回错误。通常你会使用 fmt.Errorf() 来附加相关信息(否则,要找出是哪个调用和哪些变量导致了“未找到元素”这个错误,就得费一番功夫了)。

当前这种冗长的风格有几个好处,如果我们采用这些替代方案,就会失去这些好处——而所有这些改变,仅仅是为了让代码在视觉上或书写时稍微短一点。实际上,自动补全和编辑器折叠功能已经可以在没有这些缺点的情况下实现同样的效果。

Karl:

我反对这项改动。错误处理确实如此重要,开发者必须在每种情况下都决定如何处理。

这正是我反对的核心所在。这项提案突出了“快乐路径”,而 Go 的优势之一恰恰在于迫使开发者当场思考如何处理错误。我以 Java 和 Python 开发为生,过去几十年里,我见过不少 bug 正是因为在这两种语言中,你可以在很多情况下轻易地忽略异常。

我知道社区的压力是需要考虑的因素,我也见过语言创始人因此心力交瘁。但有些时候,语言的创造者/维护者应该立场坚定地说:“这不会发生。我们这样做是有原因的,而且这是个好理由。”

我怀念 Pike-Thompson-Griesemer 三人组。

我不喜欢像 ? 符号这样的提案。我更喜欢 Go 现有的错误处理方式,并且不觉得冗长有什么不好。人们可以简单地设置他们的 IDE 来生成模板代码。在我看来,那些不断要求一些能快速将错误抛出函数的东西的人,只是没有理解 Go 错误处理机制的强大之处。当我使用第三方库并遇到错误时,我希望得到关于错误可能在哪里发生以及为何发生的解释,而不是对所有情况都使用简单的 ErrOfMyLibrary = errors.New("some random and not useful text error")。我建议我们需要学习如何用有用的信息正确地包装错误,而不是学习如何减少编码时的击键次数。即使开发者们重新设计我们现在的错误处理方式,也会有利有弊。不可能满足所有人的愿望。

对我来说,泛型是期待已久且受欢迎的;迭代器则出乎意料,感觉一般但有用;而其他大多数变化几乎察觉不到,但仍然是生活质量的提升。

我就是不喜欢这个。Go 最初的理念是尽量减少语法糖,让事情变得明确。我希望他们不要这样做,就像他们决定不使用三元操作符 ?: 一样。_iota 对我来说已经足够“魔法”了(顺便说一句,我不理解为什么有人讨厌 iota)。

我认为 Go 团队的能量最好花在其他领域:

  • 弄清楚枚举的实现,或者,如果概念验证暴露了根本性问题,就做出最终决定反对它。
  • 为迭代器提供更好的语法(但仍然保持真正的 Go 风格,无论在这里意味着什么)。
  • 改进标准库,例如解决 time 包中的一些瑕疵。
  • 改进工具链,或者稳定 /x/ 库中的内容。

?
这种语法糖对我来说感觉是一种非常令人困惑的行为。它使用了一种类似三元运算符的语法来处理错误。
但这种形式是一种新的特殊语法,这会增加 Golang 的学习成本,而且实际代码不够直观。
就像其中一种用法,r := SomeFunction() ?,我甚至没反应过来它是什么意思!这种做法只会隐藏错误进行处理,但这不应该被提倡。

// 旧方式 :
err = Func()
if err!=nil{
    return err
}
// 新方式 :
Func() ? 将会返回
// 但是
Func() 将会继续执行

而由 ? 触发的 {} 执行逻辑也让我感到担忧,例如:

F1() ? {
		return F2() ? {
               		return F3 ...
               }
}

一个非常不直观的“伪”回调地狱!

我不希望在看到这个 ? 时,需要过多思考它会生成什么逻辑。原来的 if 逻辑非常符合通用逻辑。

我认为 Golang 的错误处理应该是显式的,这样人们可以一目了然地知道这里会发生错误,并进行相应的错误处理。

我个人认为,我们应该保持开放的心态。特别是对于那些受 Go 语言启发、但采用自己方法来解决错误样板代码问题的语言,例如 Odin。在 Odin 中,or_return 操作符通过首先检查其值是否为 nil 或 false,来返回函数调用的结束值。

例如,如果一个函数调用返回 (string, error),这里的结束值就是 error 值。如果该值不为 nil,or_return 就会将其返回。 以下是我用 Go 的风格编写的示例。 示例 1 和 2 是等价的。

示例1:

func someFunc() error {
str1, err := giveStr()
if err != nil {
 return err
 }
}

示例2:

func someFunc() error {
str1:= giveStr() or_return
}

希望我的解释能够清楚地传达这个观点。 如需更多细节或更好的解释,你可以阅读 Odin 文档中专门关于 or_return 的部分。

Overview

Overview

An overview of the Odin programming language and its features.

我反对这项改动。错误处理真的非常重要,开发者必须在每种情况下都决定如何处理。使用下划线忽略错误的自由已经存在了。😉

此外,编写一个多错误结构体来收集错误并在稍后处理是非常容易的。也许更好的想法是将其纳入标准库,这样人们就可以共享同一个多错误结构体。

我在许多项目中注意到,它们有时会因为添加并非真正需要的功能而变得过度工程化。感觉这经常发生,仅仅是因为我们人类不满足于保持现状的感觉。或者他们获得了太多资金,人们觉得需要做点什么。

我可以举一个发生这种情况的例子,那就是 React 及其路由器。如果你避开它的许多新功能,保持简单,它们用起来会非常顺手👍。不要总是使用最新、臃肿、过度工程化的东西,结果仍然可以非常快。 但你必须知道该使用什么,以及你可以直接跳过哪些 90% 的功能,并且仍然没问题。

这种过度工程化的结果是: 很多人不喜欢 React,因为它让他们感觉笨拙。他们是对的,我的意思是,你是在构建菜单,拜托。 React 更难学习。 甚至有全职的 React 开发者,他们了解所有那些并非真正需要的东西。

falco467:

根据我的个人经验,在我编写的所有生产代码中,我最终几乎都会为每一个 if err != nil { return err } 添加独立的错误信息。因为大多数错误的原因各不相同,需要采取不同的处理措施。

确实如此——在其他语言中,错误处理常常被当作“这不是我的问题!总会有某个异常处理器来处理它!”。但我通常希望根据错误类型采取特定的操作。作为主要使用 Go 的开发人员一段时间后,我对错误的看法已经改变。我们不再认为错误是异常或坏事,而是明白错误当然会发生;它们只是值,我们需要处理它们。

falco467:

如果你不喜欢 if err.... 这种写法,也有编辑器插件可以隐藏或简化它。如果能用编辑器插件解决问题,就不应该改变语言的基本语法。

反驳观点:你构建过 Flutter 项目吗?人们将小部件嵌套得如此之深,以至于围绕如何理解它构建了大量工具。但这充其量只是权宜之计。你同样可以反对在 Go 中添加模块,因为在模块出现之前我们已经有工具了。在这个具体案例中我同意你的看法,但只是想说明有时情况会更微妙一些。

adsr303: 我是一名以Java和Python为生的开发者,在很多情况下可以轻易忽略异常,这导致了过去几十年里我见过的不少bug。

我这些天主要在后端使用Go,但也会做一些.NET的工作。每当我看到人们忽略/抛出异常时,我总是想知道异常将如何以及在何处被处理。而答案有时比你想象的更复杂。写了几年Go API再回到.NET,有时感觉有点像陷入混乱。

adsr303: 但有时语言创建者/维护者应该坚定立场,说“这不会发生。我们这样做是有原因的,而且是个好理由。”

我也同意这一点。如果每种语言都实现所有功能,它们最终会变得同质化;而使得Go最初非常适合解决其目标问题的那些独特之处将会消失。

peakedshout:

// new :
Func() ? will be return
// but
Func() will be continue

在提案/讨论中,这一点被列为缺点之一。意思是,因为你没注意到函数调用后面没有?,所以更容易意外地忽略错误。

无论如何,如果你还没做,去那个GitHub issue投票吧!有很多评论线程,你可以在那里点“踩”来对功能投反对票。

很高兴见到你,Karl。

Karl: 使用 _ 来忽略错误的自由已经存在了。😉

同意。而且它总会回来困扰你!

Karl: 我在很多项目中注意到,它们有时会因为添加并非真正需要的功能而变得过度设计。感觉这常常发生,仅仅是因为我们人类不满足于停滞不前的状态。或者他们获得了太多资金,人们觉得需要做点什么。

是的——这是我思考了很多的问题。比如——当项目最近没有很多提交时,人们会认为它们“死了”。但是,对于那些你已经构建了所有你需要的功能,并且不需要任何新功能的项目呢?

工程师的天性就是喜欢过度设计。我认为 Go 语言迄今为止在缓慢添加新功能方面做得非常好。我是通过 .NET 开发成为 Gopher 的;随着诸如空安全等变化,.NET 代码与我几年前写的代码完全不同。突然间,我看到了像 required 这样的关键字,需要去查阅它们,等等。另外——如果你想更加热爱 Go 的零值,那就去发布一个 .NET 或 Dart 项目吧!

Karl: React 更难学。

我想这是我最大的担忧。Go 语言的部分吸引力在于它非常容易学习。当我构建我的第一个 Go 项目时(有一些注意事项,比如包管理,因为那是在 Go Modules 之前),我基本上在第一天就能高效工作。我不认为 ? 运算符会给新的 Gopher 增加太多认知负担,但它确实是又多了一个需要学习的东西。

如果你还没试过,可以看看 preact。它是一个无需臃肿功能的 React 直接替代品!如果你需要路由,它还附带了一个非常简单的(可选的)路由器。

关于Go错误处理中减少重复代码的问题,目前社区确实在讨论?操作符提案。这个提案的核心是在特定场景下简化错误传播的语法。

实际应用示例:

// 当前方式
func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("open failed: %w", err)
    }
    defer f.Close()
    
    data, err := io.ReadAll(f)
    if err != nil {
        return fmt.Errorf("read failed: %w", err)
    }
    
    var config Config
    err = json.Unmarshal(data, &config)
    if err != nil {
        return fmt.Errorf("parse failed: %w", err)
    }
    
    return process(config)
}

// 使用?操作符提案
func processFile(path string) error {
    f := os.Open(path) ?
        return fmt.Errorf("open failed: %v", err)
    defer f.Close()
    
    data := io.ReadAll(f) ?
        return fmt.Errorf("read failed: %v", err)
    
    var config Config
    json.Unmarshal(data, &config) ?
        return fmt.Errorf("parse failed: %v", err)
    
    return process(config)
}

链式调用场景的改进:

// 当前方式
func getServerInfo() (string, error) {
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    
    var data map[string]interface{}
    err = json.Unmarshal(body, &data)
    if err != nil {
        return "", err
    }
    
    info, ok := data["info"].(string)
    if !ok {
        return "", errors.New("invalid data format")
    }
    
    return info, nil
}

// 提案方式
func getServerInfo() (string, error) {
    resp := http.Get("https://api.example.com/data") ?
        return "", err
    defer resp.Body.Close()
    
    body := io.ReadAll(resp.Body) ?
        return "", err
    
    var data map[string]interface{}
    json.Unmarshal(body, &data) ?
        return "", err
    
    info, ok := data["info"].(string)
    if !ok {
        return "", errors.New("invalid data format")
    }
    
    return info, nil
}

错误包装的简化:

// 当前需要重复的if err != nil模式
func loadConfig() (*Config, error) {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return nil, fmt.Errorf("read config: %w", err)
    }
    
    var cfg Config
    err = json.Unmarshal(data, &cfg)
    if err != nil {
        return nil, fmt.Errorf("parse config: %w", err)
    }
    
    err = validateConfig(&cfg)
    if err != nil {
        return nil, fmt.Errorf("validate config: %w", err)
    }
    
    return &cfg, nil
}

// 提案提供的语法糖
func loadConfig() (*Config, error) {
    data := os.ReadFile("config.json") ?
        return nil, fmt.Errorf("read config: %v", err)
    
    var cfg Config
    json.Unmarshal(data, &cfg) ?
        return nil, fmt.Errorf("parse config: %v", err)
    
    validateConfig(&cfg) ?
        return nil, fmt.Errorf("validate config: %v", err)
    
    return &cfg, nil
}

这个提案确实主要针对错误传播的简化,而不是异常处理。它保持了Go显式错误处理的哲学,只是减少了if err != nil的重复书写。对于中间件等需要自动错误处理的场景,仍然需要现有的recover机制或第三方错误处理库。

回到顶部