Go: 提案:规范:添加内置结果类型(如 Rust、OCaml)

创建于 2017-04-15  ·  79评论  ·  资料来源: golang/go

这是一个向 Go 添加Result Type的提议。 结果类型通常包含返回值或错误,并且可以提供在整个 Go 程序中无处不在的常见(value, err)模式的一流封装。

如果之前提交过类似的内容,我深表歉意,但希望这是对这个想法的相当全面的描述。

背景

关于这个想法的一些背景可以在 Go中的

也就是说,我自行应用“Go 2”标签不是因为这是一个重大变化,而是因为我预计它会引起争议,并且在某种程度上与语言的本质背道而驰。

Rust Result 类型提供了一些先例。 类似的想法可以在许多函数式语言中找到,包括 Haskell 的Each 、OCaml 的result和 Scala 的Each 。 Rust 管理错误与 Go 非常相似:错误只是值,在每个调用站点处理它们的冒泡,而不是使用非本地跳转的异常动作,可能需要一些工作转换错误类型或将错误包装到错误链中

Rust 使用 sum 类型(参见Go 2 sum types 提案)和泛型来实现结果类型,作为一种特殊情况的核心语言功能,我认为 Go 结果类型也不需要,并且可以简单地利用特殊情况编译器魔术。 这将涉及特殊的语法和特殊的 AST 节点,很像 Go 目前使用的集合类型。

目标

我相信将结果类型添加到 Go 可能会产生以下积极的结果:

  1. 减少错误处理样板:这是关于 Go 的一个非常普遍的抱怨。 if err != nil { return nil, err } “模式”(或其微小变化)在 Go 程序中随处可见。 这个样板没有任何价值,只会让程序变得更长。
  2. 允许编译器对结果进行推理:在 Rust 中,未使用的结果会发出警告。 尽管 Go 有一些 linting 工具可以完成同样的事情,但我认为作为编译器的一流功能,它会更有价值。 这也是一个相当简单的实现,不应该对编译器性能产生不利影响。
  3. 错误处理组合器(我觉得这是与语言的本质背道而驰的部分):如果有一种结果类型,它可以支持多种处理、转换和消费结果的方法。 我承认这种方法有一点学习曲线,因此对于不熟悉组合器习语的人来说,这会对程序的清晰度产生负面影响。 虽然我个人喜欢用于错误处理的组合器,但我绝对可以看到它们在文化上可能不适合 Go。

语法示例

首先是一个简短的说明:请不要让这个想法在语法上陷入困境。 语法是一件非常容易的事情,我不认为这些示例中的任何一个都可以作为 One True Syntax,这就是为什么我给出了几种替代方案。

相反,我更希望人们关注问题的一般“形状”,并且只查看这些示例以更好地理解这个想法。

结果类型签名

最简单的事情:只需在返回值元组前面添加“结果”:

func f1(arg int) result(int, error) {

更典型的是“泛型”语法,但这可能应该保留给 Go 实际添加泛型的 if/when(如果发生这种情况,可以调整结果类型功能以利用它们):

func f1(arg int) result<int, error> {

返回结果时,我们需要一种语法来将值或错误包装在结果类型中。 这可能只是一个方法调用:

return result.Ok(value)

```去
返回结果.Err(error)

If we allow "result" to be shadowed here, it should avoid breaking any code that already uses "result".

Perhaps "Go 2" could add syntax sugar similar to Rust (although it would be a breaking change, I think?):

```go
return Ok(value)

```去
返回错误(值)

### Propagating errors

Rust recently added a `?` operator for propagating errors (see [Rust RFC 243](https://github.com/rust-lang/rfcs/blob/master/text/0243-trait-based-exception-handling.md)). A similar syntax could enable replacing `if err != nil { return _, err }` boilerplate with a shorthand syntax that bubbles the error up the stack.

Here are some prospective examples. I have only done some cursory checking for syntactic ambiguity. Apologies if these are either ambiguous or breaking changes: I assume with a little work you can find a syntax for this which isn't at breaking change.

First, an example with present-day Go syntax:

```go
count, err = fd.Write(bytes)
if err != nil {
    return nil, err
}

现在有了一个新的语法,它使用结果并将错误向上冒泡。 请记住,这些示例仅用于说明目的:

count := fd.Write!(bytes)

```去
计数:= fd.Write(字节)!

```go
count := fd.Write?(bytes)

```去
计数:= fd.Write(字节)?

```go
count := try(fd.Write(bytes))

注意:Rust 以前支持后者,但通常已经远离它,因为它不可链接。

在我随后的所有示例中,我将使用这种语法,但请注意这只是一个示例,可能有歧义或有其他问题,我当然不会与它结婚:

count := fd.Write(bytes)!

向后兼容

语法建议都使用result关键字来标识类型。 我相信(但肯定不确定)可以开发阴影规则,允许使用“结果”的现有代码(例如变量名称)继续按原样运行而不会出现问题。

理想情况下,应该可以“升级”现有代码以完全无缝的方式使用结果类型。 为此,我们可以允许将结果作为 2 元组使用,即给定:

func f1(arg int) result(int, error) {

应该可以将其消耗为:

result := f1(42)

或者:

(value, err) := f1(42)

也就是说,如果编译器看到result(T, E)(T, E)的赋值,它应该自动强制。 这应该允许函数无缝切换到使用结果类型。

组合器

通常错误处理会比if err != nil { return _, err }涉及更多。 如果这是它帮助的唯一案例,那么该提案将是非常不完整的。

由于它们支持“组合器”,结果类型以函数式语言中的错误处理瑞士刀而闻名。 实际上,这些组合器只是一组方法,它们允许我们根据结果类型进行转换和选择性行为,通常与闭包“组合”。

Then() :将返回相同结果类型的函数调用链接在一起

假设我们有一些看起来像这样的代码:

resp, err := doThing(a)
if err != nil {
    return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

使用结果类型,我们可以创建一个函数,该函数将闭包作为参数,并且只有在结果成功时才调用闭包,否则短路并返回自身表示错误。 我们将调用此函数Then在 Go中的and_then )。 使用这样的函数,我们可以将上面的示例重写为:

result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })

if result.isError() {
    return result.Error()
}

或使用上面建议的语法之一(我将选择!作为魔术运算符):

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

这将原始示例中的 12 行代码减少到 3 行,并为我们留下了我们实际需要的最终值,而结果类型本身从图片中消失了。 在这种情况下,我们甚至不必为结果类型命名。

现在当然,这种情况下的闭包语法感觉有点笨拙/JavaScript 风格。 它可能会受益于更轻量级的闭包语法。 我个人喜欢这样的事情:

final_value := doThing(a).
    Then(|resp| doAnotherThing(b, resp.foo())).
    Then(|resp| FinishUp(c, resp.bar()))!

...但类似的事情可能值得单独提出建议。

Map()MapErr() :在成功和错误值之间转换

很多时候,在进行if err != nil { return nil, err }舞蹈时,您实际上需要对错误进行一些处理或将其转换为不同的类型。 像这样的东西:

resp, err := doThing(a)
if err != nil {
    return nil, myerror.Wrap(err)
}

在这种情况下,我们可以使用MapErr()完成同样的事情(我将再次使用!语法来返回错误):

resp := doThing(a).
    MapErr(func(err) { myerror.Wrap(err) })!

Map做同样的事情,只是转换成功值而不是错误。

和更多!

组合子比我在这里展示的要多得多,但我相信这些是最有趣的。 为了更好地了解功能齐全的结果类型是什么样的,我建议查看 Rust 的:

https://doc.rust-lang.org/std/result/enum.Result.html

Go2 LanguageChange NeedsInvestigation Proposal

最有用的评论

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

我认为这不是 Go 的正确方向。 ()) })! ,认真的? Go 的主要目标应该是易于学习、可读性和易用性。 这没有帮助。

所有79条评论

由于 Go 1.x 语言已被冻结(正如您所指出的,这是 Go2),因此目前在提案审查过程中并未考虑语言更改提案。 只是让您知道不要指望很快就对此做出决定。

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

我认为这不是 Go 的正确方向。 ()) })! ,认真的? Go 的主要目标应该是易于学习、可读性和易用性。 这没有帮助。

正如有人在 reddit 帖子中所说:肯定更喜欢正确的sum 类型泛型,而不是新的特殊内置函数。

也许我在帖子中不清楚:我当然更喜欢由和类型和泛型组成的结果类型。

我试图以这样一种方式来规范它,即添加两者(我个人认为极不可能)不会成为添加此功能的障碍,并且可以以这样的方式添加,如果可用,这个特性可以切换到它们(我什至举了一个例子,说明使用传统的通用语法会是什么样子,并且还与 Go sum 类型问题相关联)。

我不明白结果类型和目标之间的联系。 您关于错误传播和组合器的想法似乎与当前对多个结果参数的支持一样有效。

@ianlancetaylor你能举例说明如何定义一个对当前结果元组通用的组合器吗? 如果可能的话,我很想看看它,但我认为不是(根据这篇文章

@tarcieri那篇文章有很大的不同,因为error没有出现在Result<A>建议用法中。 与帖子不同,这个问题似乎暗示result<int, error> ,这对我来说意味着提议的组合器特别识别error 。 如果我误解了,我深表歉意。

目的不是将result耦合到error ,而是让result携带两个值,类似于 Rust 中的Result类型或Either在 Haskell 中。 在这两种语言中,按照惯例,第二个值通常是error类型(尽管不是必须)。

这个问题,不像帖子,似乎是在暗示结果

该帖子建议:

type Result<A> struct {
    // fields
}

func (r Result<A>) Value() A {…}
func (r Result<A>) Error() error {…}

...因此,相反,该帖子专门围绕error ,而该提案接受用户指定的第二个值类型。

诚然,诸如result.Err()result.MapErr()的东西都认可这个值始终是error

@tarcieri 结构有什么问题? https://play.golang.org/p/mTqtaMbgIF

@griesemer正如Go帖子中的

@tarcieri 明白了。 但是如果这(非通用性,或者可能没有和类型)是这里的问题,那么我们应该解决这些问题。 仅处理结果类型只是添加更多特殊情况。

Go 是否具有泛型与一流的结果类型是否有用是正交的。 这将使实现更接近于您自己实现的东西,但正如提案中所涵盖的那样,允许编译器以一流的方式对其进行推理,从而允许它例如警告未使用的结果。 具有单一结果类型也是使提案中的组合器可组合的原因。

您建议的@tarcieri组合也可以使用单个结果结构类型。

我不明白你为什么不使用嵌入或定义的结构类型。 为什么有专门的方法和语法来检查错误? Go 已经拥有完成所有这些的方法。 看起来这只是添加了没有定义 Go 语言的特性,它们定义了 Rust。 实施此类更改将是错误的。

我不明白你为什么不使用嵌入或定义的结构类型。 为什么有专门的方法和语法来检查错误?

再重复一遍:因为拥有泛型结果类型需要...泛型。 Go 没有泛型。 缺少 Go 获得泛型,它需要语言的特殊情况支持。

也许你在建议这样的事情?

type Result struct {
    value interface{}
    err error
}

是的,这“有效”......以类型安全为代价。 现在要使用任何结果,我们必须进行类型断言以确保interface{} -typed 值是我们期望的值。 如果没有,它现在变成了一个运行时错误(而不是像现在这样的编译时错误)。

这将是对 Go 现在所拥有的重大退步。

为了让这个特性真正有用,它需要是类型安全的。 Go 的类型系统表达能力不够,无法在没有特殊情况语言支持的情况下以类型安全的方式实现它。 它至少需要泛型,理想情况下还需要和类型。

似乎这只是添加未定义 Go 语言的功能 [...]。 实施此类更改将是错误的。

我在最初的提案中涵盖了尽可能多的内容:

“我承认这种方法有一点学习曲线,因此对于不熟悉组合器习语的人来说,这会对程序的清晰度产生负面影响。虽然我个人喜欢组合器进行错误处理,但我绝对可以看到文化上的它们可能不适合 Go。”

我觉得我已经证实了我的怀疑,并且 Go 开发人员不容易理解这样的功能,并且违背了该语言面向简单性的本质。 它利用了编程范式,很明显,Go 开发人员似乎不理解或不想要,在这种情况下似乎是一个错误。

他们定义了 Rust

结果类型不是 Rust 特有的功能。 它们可以在许多函数式语言中找到(例如 Haskell 的Either和 OCaml 的result )。 也就是说,将它们引入 Go 感觉就像一座桥太远了。

感谢您分享您的想法,但我认为上面使用的示例并不能令人信服。 对我来说,A 比 B 好:

一个
```resp, err := doThing(a)
如果错误!= nil {
返回零,错误
}
如果 resp, err = doAnotherThing(b, resp.foo()); 错误!= 零{
返回错误
}
如果 resp, err = FinishUp(c, resp.bar()); 错误!= 零{
返回错误
}


结果 := doThing(a)。
然后(func(resp){ doAnotherThing(b,resp.foo())})。
然后(func(resp) { FinishUp(c, resp.bar()) })

如果 result.isError() {
返回结果.Error()
}
``

  • A 更具可读性,大声和精神上。
  • A 不需要行格式/换行
  • 在 A 中,错误条件明确地终止执行; 不需要精神上的否定。 B 不一样。
  • 在 B 中,关键字“Then”并不表示条件因果关系。 关键字“if”确实如此,并且它已经存在于语言中。
  • 在 B 中,我不想通过将它打包到 lambda 中来减慢我最有可能的执行分支

我不认为 A 更具可读性。 事实上,这些动作根本不明显。 相反,乍一看会发现正在获取和返回一堆错误。

如果 B 被格式化为使闭包体在新行上,那将是最易读的格式。

另外,最后一点似乎有点傻。 如果函数调用性能如此重要,那么一定要使用更传统的语法。

A来自@as我认为我们正常的流程不应该缩进。

if err != nil {
    return err
}

resp, err = doAnotherThing(b, resp.foo());
if  err != nil {
    return err
}

resp, err = FinishUp(c, resp.bar());
if  err != nil {
    return err
}

来自该线程的一个有趣观察:我给出的人们不断复制和粘贴的原始示例包含一些错误(第一个if nil, err在错误时返回err )。 这些错误不是我故意的,但我认为这是一个有趣的案例研究。

虽然这种特殊的错误类别会被 Go 编译器捕获,但我认为有趣的是注意到,使用这么多句法样板,在复制和粘贴时很容易忽略这些错误。

这并没有使提案更好。 假设未能返回多个值是显式错误处理的结果。 您也可以在函数内部犯同样的错误,只是由于它们不必要的封装,您不会看到它们。

我不同意,我认为这是这种提案的一个优点。 如果程序所做的只是返回错误而不是处理它,那么它就是在浪费认知开销和代码,并使事情的可读性降低。 添加这样的功能意味着(在选择使用它的项目中)处理错误的代码实际上是在做一些值得理解的事情。

我们将不得不同意不同意。 提案中的魔法令牌写起来容易,但很难理解。 仅仅因为我们使它更短并不意味着我们使它更简单。

让事情变得不那么可读是主观的,所以这是我的意见。 我在这个提案中看到的只是更复杂和晦涩的代码,带有魔法函数和符号(很容易错过)。 他们所做的只是在情况 A 中隐藏一个非常简单易懂的代码。对我来说,他们不会增加任何价值,不会在重要的地方缩短代码或简化事情。 我认为在语言层面上对待他们没有任何价值。

该提案解决的唯一问题,我可以清楚地看到,是错误处理中的样板。 如果这是唯一的原因,那么这对我来说不值得。 关于句法样板的争论实际上与提案背道而驰。 在这方面它要复杂得多 - 所有那些很容易错过的魔法符号和括号。 示例 A 有样板文件,但不会导致逻辑错误。 在这种情况下,该提案没有任何好处,再次使其不是很有用。

让我们将 Rust 功能留给 Rust。

澄清一下,我并不热衷于添加!后缀作为快捷方式,但我确实喜欢提出一个简单的语法来简化

err = foo()
if err != nil {
  return err
}

即使该语法是关键字而不是特殊符号。 这是我对语言的最大抱怨(甚至比泛型个人更严重),我认为代码中乱七八糟的模式使它更难阅读和嘈杂。

我也很想看到能够实现@tarcieri带来的那种链接的@creker暗示的复杂性可以通过代码中更好的信噪比来平衡。

我不完全理解该提案将如何实现其既定目标。

  1. 减少错误处理样板:该提案有一些假设的 Go 代码:

    result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })
    
    if result.isError() {
    return result.Error()
    }
    

    如果不对函数文字的工作方式进行更广泛的更改,我不太确定func(resp) { expr }应该如何工作。 我认为生成的代码最终看起来更像这样:

    result := doThing(a).
    Then(func(resp T) result(T, error) { return doAnotherThing(b, resp.foo()) }).
    Then(func(resp T) result(T, error) { return FinishUp(c, resp.bar()) })
    
    if result.isError() {
    return result.Error()
    }
    

    在现实的 Go 代码中,中间表达式比这更长并且需要放在自己的行上也是很常见的。 这在今天真正的 Go 代码中很自然地发生; 根据这项提议,它将是:

    result := doThing(a).
    Then(func(resp T) result(T, error) {
        return doAnotherThing(b, resp.foo())
    }).
    Then(func(resp T) result(T, error) {
        return FinishUp(c, resp.bar())
    })
    
    if result.isError() {
    return result.Error()
    }
    

    无论哪种方式,这让我觉得还可以,但不是很好,就像提案中上面的真正 Go 代码一样。 它的“Then”组合子本质上与“return”相反。 (如果您熟悉 monad,这不会让人感到惊讶。)它删除了编写“if”语句的要求,但引入了编写函数的要求。 总的来说,它并没有好坏之分。 它是具有新拼写的相同样板逻辑。

  2. 允许编译器对结果进行推理:如果这个特性是可取的(我在这里没有表达任何意见),我看不出这个提议如何使它或多或少地变得可行。 它们让我觉得是正交的。

  3. 错误处理组合器:这个目标肯定是通过提案实现的,但在今天的 Go 语言的上下文中,并不完全清楚是否值得为实现它而进行必要的更改。 (我认为这是目前讨论中的主要争论点。)

在大多数编写良好的 Go 中,这种错误处理样板只占代码的一小部分。 在我对一些我认为写得很好的 Go 代码库的简要介绍中,这是个位数的行数百分比。 是的,它有时是合适的,但通常它表明一些重新设计是有序的。 特别是,简单地返回一个错误而不添加任何上下文,无论发生的情况比现在更频繁。 它可以被称为“反习语”。 有一个关于 Go 应该或可以做什么来阻止这种反习语的讨论,无论是在语言设计中,还是在库中,或在工具中,或纯粹的社会方面,或在这些方面的某种组合中. 无论是否通过该提案,我都同样有兴趣进行讨论。 事实上,使这种反习语更容易表达,正如我认为是本提案的目的,可能会设置错误的激励措施。

目前,该提案主要被视为品味问题。 在我看来,有什么证据表明它的采用会减少错误的总量,这将使它更具说服力。 一个好的第一步可能是转换Go 语料库的一个代表性块,以证明某些类型的错误不可能或不太可能以新风格表达——实际 Go 代码中每行x 个错误将通过使用来修复新风格。 (似乎更难证明新风格不会通过增加其他类型的错误来抵消任何改进。在那里我们可能不得不处理关于可读性和复杂性的抽象论点,就像在 Go 之前的糟糕过去一样语料库上升到突出。)

有了这样的支持证据,人们可以提出更有力的理由。

简单地返回一个错误而不添加任何上下文,无论发生什么事情都比现在更频繁。 它可以被称为“反习语”。

我想回应这种情绪。 这

if err := foo(x); err != nil {
    return err
}

不应该简化,应该劝阻,赞成例如

if err := foo(x); err != nil {
    return errors.Wrapf(err, "fooing %s", x)
}

@peterbourgon

我最大的问题不是盲目返回错误。 事实是这样的动作: foo(x) ; 不是那么明显,恕我直言,与替代的“功能性”解决方案相比,整个事情的可读性要低得多,在这种解决方案中,操作本身就是一个简单的换行返回。

即使赋值和动作与 if 语句本身分开,结果语句仍然会强调结果,而不是动作。 这是完全有效的,特别是如果结果是重要的部分。 但是如果你有一堆语句,其中每个语句都得到一个 (result, error) 元组,检查错误/返回,然后在获取新元组的同时继续做另一个动作,结果本身显然不是其中的主要字符阴谋。

@urandom我认为结果是一对 (val, error) 所以我认为检查错误/返回也是图中的主要字符。

保留字(类似于reterr )如何避免所有if err != nil { return err }

所以这

resp, err := doThing(a)
if err != nil {
    return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

会成为:

resp, _ := reterr doThing(a)
resp, _ = reterr doAnotherThing(b, resp.foo())
resp, _ = reterr FinishUp(c, resp.bar())

reterr 基本上会检查被调用函数的返回值,并在其中任何一个错误且不为零时返回(并在任何非错误返回值中返回 nil)。

听起来越来越像#18721

@tarcieri只需使用一些reflect包。 我可以模拟类似你的提议的东西。
但我认为不值得这样做。

https://play.golang.org/p/CC5txvAc0e

func main() {

    result := Do(func() (int, error) {
        return doThing(1000)
    }).Then(func(resp int) (int, error) {
        return doAnotherThing(200000, resp)
    }).Then(func(resp int) (int, error) {
        return finishUp(1000000, resp)
    })

    if result.err != nil {
        log.Fatal(result.err)
    }

    val := result.val.(int)
    fmt.Println(val)
}

@iporsut反射有两个问题,这使得它不适合解决这个特定问题,尽管它表面上似乎“解决”了问题:

  1. 没有类型安全性:通过反射,我们无法在编译时确定闭包的类型是否合适。 相反,无论类型如何,我们的程序都会编译,如果它们不匹配,我们将遇到运行时崩溃。
  2. 巨大的性能开销:您建议的方法与go-linq提供的方法相差不远。 他们声称为此目的使用反射是“慢 5-10 倍”。 现在想象一下每个呼叫站点的开销。

对我来说,这些问题中的任何一个都是 Go 已经存在的巨大倒退,同时它们完全是一个无足轻重的问题。

我喜欢 Go 及其处理错误的方式。 然而,也许它可以更简单。 以下是我关于 Go 中错误处理的一些想法。

现在的样子:

resp, err := doThing(a)
if err != nil {
    return nil, err
}

resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}

resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

A:

resp, _ := doThing(a) 
resp, _ = doAnotherThing(b, resp.foo())
resp, _ = FinishUp(c, resp.bar())
// return if error is omited, otherwise deal with it as usual (if err != nil { return err })
//However, this breaks semantics of Go and may mislead due to the usa of _ (__ or !_ could be used to avoid such misleading)

乙:

resp, err := doThing(a)?
resp, err = doAnotherThing(b, resp.foo())?
resp, err = FinishUp(c, resp.bar())?
// ? indicates that it will return in case of error (more explicit)
// or any other indication could be used
// this approach is preferred for its explicitness

C:

resp, err := doThing(a)
return if err

resp, err = doAnotherThing(b, resp.foo())
return if err

resp, err = FinishUp(c, resp.bar())
return if err
// if err return err
// or if err return (similar to javascript return)
// this one is my favorite, almost no changes to the language, very readable and less SLOC

乙:

resp, _ := return doThing(a)
resp, _ = return doAnotherThing(b, resp.foo())
resp, _ = return FinishUp(c, resp.bar())
// or 
resp = throw FinishUp(c, resp.bar())
// this one is also very readable (although maybe a litle less than option **C**) and even less SLOC than **C**
// at this point I'm not sure whether C or D is my favorite )) 

//This applies to all approaches above
// if the function that contains any of these options has no value to return, exit the function. E.g.:
func test() {
    resp, _ := return doThing(a) // or any of other approaches
    // exit function
}

func test() ([]byte, error) {
    resp, _ := return doThing(a) // or any of other approaches
    // return whatever is returned by doThing(a) (this function of course must return ([]byte, error))
}

请原谅我的英语,我不确定此类更改是否可行以及它们是否会导致性能开销。

如果您喜欢这些方法中的任何一种,请按照以下规则喜欢它们:

A = 👍
B = 😄
C = ❤️
D = 🎉

👎如果你不喜欢整个想法))

这样我们就可以有一些统计数据,避免不必要的评论,比如“+1”

精心设计我的“提案”...

// no need to explicitely define error in return statement, much like throw, try {} catch in java
func test() int {
     resp := throw doThing() // "returns" error if doThing returns (throws) an error
     return resp // yep, resp is int
}

func main() {
     resp, err := test() // the last variable is always error type
     if err != nil {
          os.Exit(0)
     }
}

同样,不确定这样的事情是否可能))

这是另一个疯狂的选择,让error这个词更神奇一点。 它可以在赋值(或简短声明)的左侧使用,并且有点像魔术函数:

res, error() := doThing()
// Shorthand for
res, err := doThing()
if err != nil {
  return 0, ..., 0, err
}

具体来说, error()的行为如下:

  1. 出于赋值的目的,它被视为具有error类型。
  2. 如果将nil分配给它,则什么也不会发生。
  3. 如果为它分配了非nil值,则封闭函数会立即返回。 除了最后一个返回值之外,所有返回值都设置为 0,它的类型必须为error并且分配给error()

如果您想对错误应用一些更改,则可以执行以下操作:

res, error(func (e error) error { return fmt.Errorf("foo: %s", error)})
  := doThing()

在这种情况下,闭包应用于函数返回之前分配的值。

这有点难看,很大程度上是由于必须处理闭包的语法膨胀。 标准库可以很好地解决这个问题,例如error(errors.Wrapper("foo"))将为您生成正确的包装器闭包。

作为替代方案,如果很可能会遗漏空error()语法,我建议将error(return)作为替代方案; 关键字的使用降低了误解的风险。 然而,它不能很好地扩展到关闭案例。

每个编写 Go 的人都遇到过错误处理样板的不幸扩散,这分散了他们代码的核心目的。 这就是Rob Pike 在 2015 年谈到这个主题的原因。 正如Martin Kühl 指出的,Rob 的简化错误处理的建议:

让我们不得不为每个我们想要处理错误的接口实现手工一次性 monad,我认为这仍然是冗长和重复的

这就是为什么今天仍然有如此多的人关注这个话题。

理想情况下,我们可以找到一个解决方案:

  1. 减少重复的错误处理样板,并最大限度地关注代码路径的主要意图。
  2. 鼓励正确的错误处理,包括在传播错误时包装错误。
  3. 遵循 Go 的清晰和简单的设计原则。
  4. 适用于尽可能广泛的错误处理情况。

我建议引入一个新的关键字catch: ,其作用如下:

而不是当前的形式:

res, err := doThing()
if err != nil {
  return 0, ..., 0, err
}

我们会写:

res, err := doThing() catch: 0, ..., 0, err

它的行为方式与上面当前的表单代码完全相同。 更具体地说,首先执行catch:左侧的函数和赋值。 然后,当且仅当返回参数之一的类型error该值非零时, catch:充当return语句,其值为正确的。 如果有零个或多个error类型从返回doThing() ,它使用一个语法错误catch: 。 如果从doThing()返回的错误值是nil ,则从catch:到语句末尾的所有内容都将被忽略并且不进行评估。

从 Nemanja Mijailovic 最近题为“ Go 中的错误处理模式”的博客文章中给出一个更复杂的例子:

func parse(r io.Reader) (*point, error) {
  var p point

  if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
    return nil, err
  }

  return &p, nil
}

这变成了:

func parse(input io.Reader) (*point, error) {
  var p point

  err := read(&p.Longitude) catch: nil, errors.Wrap(err, "Failed to read longitude")
  err = read(&p.Latitude) catch: nil, errors.Wrap(err, "Failed to read Latitude")
  err = read(&p.Distance) catch: nil, errors.Wrap(err, "Failed to read Distance")
  err = read(&p.ElevationGain) catch: nil, errors.Wrap(err, "Failed to read ElevationGain")
  err = read(&p.ElevationLoss) catch: nil, errors.Wrap(err, "Failed to read ElevationLoss")

  return &p, nil
}

好处:

  1. 接近最少的额外样板用于错误处理。
  2. 改进对代码主要意图的关注,语句左侧的错误处理包袱最少,右侧的错误处理本地化。
  3. 在许多不同的情况下工作,在多个返回值的情况下为程序员提供灵活性(例如,如果除了错误之外,您还想返回成功的项目计数的指示器)。
  4. 语法很简单,很容易被 Go 新老用户理解和采用。
  5. 通过使错误代码更加简洁,部分成功地鼓励了正确的错误处理。 可能使错误代码不太可能被复制粘贴,从而减少常见的复制粘贴错误的引入。

缺点:

  1. 这种方法在鼓励正确的错误处理方面并没有完全成功,因为它在传播错误之前没有促进包装错误。 在我的理想世界中,这种新语法要求catch:返回的错误是新错误或包装错误,但与catch:左侧的函数返回的错误不同
  2. 有些人可能会争辩说,这只是语法糖,语言中不需要。 一个相反的论点可能是 Go 中当前的错误处理是语法反式脂肪,而这个提议只是消除了它。 要被广泛采用,编程语言应该使用起来令人愉悦。 很大程度上 Go 在这方面取得了成功,但错误处理样板是一个特别多的异常。
  3. 我们是从我们调用的函数中“捕获”错误,还是向调用我们的人“抛出”错误? 在没有显式抛出的情况下使用catch:是否合适? 保留字不一定必须是catch: 。 其他人可能有更好的想法。 它甚至可以是运算符而不是保留字。

每个编写 Go 的人都遇到过错误处理样板的不幸扩散,这分散了他们代码的核心目的。

那不是真的。 我经常用 Go 编程,而且我对任何错误处理样板都没有任何问题。 编写错误处理代码占用了开发项目的一小部分时间,我几乎没有注意到它,恕我直言,它并不能证明对语言进行任何更改是合理的。

每个编写 Go 的人都遇到过错误处理样板的不幸扩散,这分散了他们代码的核心目的。

那不是真的。 我经常用 Go 编程,而且我对任何错误处理样板都没有任何问题。 编写错误处理代码占用了开发项目的一小部分时间,我几乎没有注意到它,恕我直言,它并不能证明对语言进行任何更改是合理的。

我没有说编写错误处理代码需要多少时间。 我只是说它分散了代码的核心目的。 也许我应该说“每个读过Go 的人都遇到过不幸的错误处理扩散……”。

所以, @cznic ,我想你的问题是你是否读过 Go 代码,你是否觉得有过多的错误处理样板,或者是否分散了你试图理解的代码的注意力?

没有人喜欢我的提议😅
无论如何,我们应该有一些语法,并投票选出最好的(一些投票系统)并在此处或自述文件中包含链接

也许我应该说“每个读过 Go 的人都遇到过错误处理的不幸扩散……”。

这不是真的。 我更喜欢当前错误处理技术的明确性和适当的局部性。 与我见过的任何其他提案一样,该提案使代码的可读性降低,维护起来更糟。

所以, @cznic ,我想你的问题是你是否读过 Go 代码,你是否觉得有过多的错误处理样板,或者是否分散了你试图理解的代码的注意力?

不。根据我的经验,Go 是一种非常易读的编程语言。 当然,其中一半归功于 gofmt。

我自己的经验是,当你有一堆依赖语句时,它真的开始拖了,每个依赖语句都会抛出一个错误,错误处理加起来并很快变老。 5 行代码变成了 20 行。

@cznic
根据我的经验,拥有如此多的错误处理样板会使代码的可读性大大降低。 因为错误处理本身大部分是相同的(没有可能发生的任何错误包装),它会产生一种栅栏效应,如果你快速浏览一段代码,你最终会看到大量的错误处理。 因此,最大的问题,实际代码,程序中最重要的部分,隐藏在这种视觉错觉的背后,使得真正看到一段代码的内容变得非常困难。

错误处理不应该是任何代码的主要部分。 不幸的是,结果往往就是这样。
其他语言的语句组合如此流行是有原因的。

因为错误处理本身大部分是相同的(没有任何错误
可能发生的包装),它会产生一种栅栏效应,如果
你快速浏览一段代码,你最终看到的是一堆
的错误处理。

这是一个非常主观的立场。 这就像争论 if 语句
使代码不可读,或者 K&R 风格的大括号使事情不可读。

从我的角度来看,go 错误处理的明确性很快就消失了
进入熟悉的背景,直到你注意到模式被打破;
人眼非常擅长做的事情; 缺少错误处理,
错误变量分配给 _ 等。

打字是一种负担,别搞错了。 但是 Go 并没有针对
代码作者,它明确地为读者优化。

在星期二,2017年5月16日,在5:45 PM,尤Kojouharov < [email protected]

写道:

@cznic https://github.com/cznic
根据我的经验,有这么多的错误处理样板使代码
可读性要低得多。 因为错误处理本身大部分是相同的
(没有可能发生的任何错误包装),它会产生一种围栏
效果,如果你快速浏览一段代码,你大多会结束
看到大量的错误处理。 因此最大的问题,实际
代码,程序中最重要的部分,隐藏在这个光学的后面
错觉,很难真正看到一块
代码是关于。

错误处理不应该是任何代码的主要部分。 不幸的是,相当
通常它最终就是那样。


您收到此消息是因为您订阅了此线程。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/19991#issuecomment-301702623或静音
线程
https://github.com/notifications/unsubscribe-auth/AAAcA4ydpBFiapYBOBUyUjg6du5Dnjs5ks5r6VQjgaJpZM4M-dud
.

如果你快速浏览一段代码,你最终会看到一堆
的错误处理。

这是一个非常主观的立场。

高度主观但广泛共享。

正如罗伯本人所说,

Go 程序员,尤其是刚接触 Go 语言的程序员,讨论的一个共同点是如何处理错误。 谈话经常变成哀叹的次数

if err != nil {
    return err
}

出现。

公平地说,Rob 继续说这种关于 Go 错误处理的看法是“不幸的、误导性的,并且很容易纠正”。 然而,他用那篇文章的大部分时间解释的那样,Rob 的处方本身就有问题。 除了 Martin 的批评之外,Rob 的建议还减少了@cznic所说的他在 Go 错误处理中重视的局部性。

也许问题是我们是否有能力更换

res, err := doThing()
if err != nil {
  return nil, err
}

类似于:

res, err := doThing() catch: nil, err

你会使用它,还是坚持使用四行版本? 不管你的个人偏好如何,你认为这样的替代方案会被 Go 社区广泛采用并成为惯用语吗? 考虑到任何关于较短版本对可读性产生不利影响的论点的主观性,我与程序员的经验表明他们会强烈倾向于单行版本。

实事求是:go 1 是固定的,不会改变,尤其是在这个基本方式上。

在 Go 2 实现一些 for 模板类型之前,提出某种选项类型是没有意义的。 那个时候,一切都变了。

2017 年 5 月 16 日,23:46,Billy Hinners [email protected]写道:

如果你快速浏览一段代码,你最终会看到一堆
的错误处理。

这是一个非常主观的立场。

高度主观但广泛共享。

正如罗伯本人所说,

Go 程序员,尤其是刚接触 Go 语言的程序员,讨论的一个共同点是如何处理错误。 谈话经常变成哀叹的次数

如果错误!= nil {
返回错误
}
出现。

公平地说,Rob 继续说这种关于 Go 错误处理的看法是“不幸的、误导性的,并且很容易纠正”。 然而,他用那篇文章的大部分时间来解释他推荐的纠正感知的方法。 不幸的是,正如 Martin Kühl 所解释的那样,Rob 的处方本身就有问题。 除了 Martin 的批评之外,Rob 的建议还减少了@cznic所说的他在 Go 错误处理中重视的局部性。

也许问题是我们是否有能力更换

资源,错误:= doThing()
如果错误!= nil {
返回零,错误
}
类似于:

res, err := doThing() catch: nil, err

你会使用它,还是坚持使用四行版本? 不管你的个人偏好如何,你认为这样的替代方案会被 Go 社区广泛采用并成为惯用语吗? 考虑到任何关于较短版本对可读性产生不利影响的论点的主观性,我与程序员的经验表明他们会强烈倾向于单行版本。


您收到此消息是因为您发表了评论。
直接回复此邮件,在 GitHub 上查看,或将线程静音。

在 Go 2 实现一些 for 模板类型之前,提出某种选项类型是没有意义的。 那个时候,一切都变了。

我认为我们在讨论 Go 2,正如该主题的标题所暗示的那样,并且完全相信“Go 2”不是“从不”的委婉说法。 事实上,鉴于 Go 1 是固定的,我们可能应该将更多的 Go 讨论投入到 Go 2 上。

话虽如此,我认为每个抱怨 Go 冗长的人
错误处理缺少根本点,错误的目的
Go 中的处理_不_使非错误情况变得简短且不显眼
尽可能。 相反,Go 错误处理策略的目标是强制
代码的作者在任何时候都要考虑当
功能失败,最重要的是如何清理、撤消和恢复
在返回给调用者之前。

在我看来,隐藏错误处理样板的所有策略都是
忽略这一点。

2017 年 5 月 16 日,星期二,23:51 Dave Cheney [email protected]写道:

实谈:go 1 是固定的,不会改变,尤其是在这个
根本途径。

在 Go 2 实现之前提出某种选项类型是没有意义的
一些用于模板类型。 那个时候,一切都变了。

2017 年 5 月 16 日,23:46,Billy Hinners [email protected]写道:

如果你快速浏览一段代码,你最终会看到一个
大量的
的错误处理。

这是一个非常主观的立场。

高度主观但广泛共享。

正如罗伯本人所说,

Go 程序员之间的一个共同讨论点,尤其是那些刚接触
语言,就是如何处理错误。 谈话往往变成
感叹序列的次数

如果错误!= nil {
返回错误
}

出现。

公平地说,Rob 继续说这种关于 Go 错误处理的看法是
“不幸的,误导性的,很容易纠正。” 然而,他大部分时间都花在了
文章https://blog.golang.org/errors-are-values解释他的
推荐的矫正方法。 不幸的是,罗伯的
正如所解释的,处方本身是有问题的
https://www.innoq.com/en/blog/golang-errors-monads/ Martin 写得很好
库尔。 除了 Martin 的批评,Rob 的建议还减少了
@cznic https://github.com/cznic说他在 Go 中重视的地方
错误处理。

也许问题是我们是否有能力更换

资源,错误:= doThing()
如果错误!= nil {
返回零,错误
}

类似于:

res, err := doThing() catch: nil, err

你会使用它,还是坚持使用四行版本?
无论您的个人喜好如何,您是否认为有其他选择
这会被 Go 社区广泛采用并成为惯用语吗?
鉴于任何论点的主观性,即较短的版本不利
影响可读性,我与程序员的经验表明他们会
强烈倾向于单行版本。


您收到此消息是因为您发表了评论。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/19991#issuecomment-301787215或静音
线程
https://github.com/notifications/unsubscribe-auth/AAAcAwATgoJwL5WV-0nffLjLB9L86GYOks5r6ai3gaJpZM4M-dud
.

相反,Go 错误处理策略的目标是强制
代码的作者在任何时候都要考虑当
功能失败,最重要的是如何清理、撤消和恢复
在返回给调用者之前。

那么,Go 并没有实现那个目标。 默认情况下,Go 允许您忽略返回的错误,并且在许多情况下,您甚至不会知道这一点,直到某处无法正常工作。 相反,在 Go 社区中非常讨厌的异常(这只是证明这一点的一个例子)迫使您考虑它们,否则应用程序将崩溃。 这通常会导致我们遇到捕获所有内容并忽略的问题,但这是程序员的错。

基本上,Go 中的错误处理是选择加入的。 更多的是关于应该处理每个错误的口头约定。 如果它实际上会迫使您处理错误,那么目标就会实现。 例如,编译时错误或警告。

考虑到这一点,隐藏样板不会伤害任何人。 口头约定仍然有效,程序员仍然会选择加入错误处理,就像现在一样。

Go 的错误处理策略的目标是强制
代码的作者在任何时候都要考虑当
功能失败,最重要的是如何清理、撤消和恢复
在返回给调用者之前。

这是一个无可争议的崇高目标。 但是,这是一个目标,必须与主要流程的可读性和代码的意图相平衡。

作为一名 Go 程序员,我可以告诉你,我没有发现 Go 的冗长
Go 的错误处理损害了它的可读性。 我看不出有什么
换掉,因为我不觉得不舒服_阅读_其他人写的代码
去程序员。

2017 年 5 月 17 日星期三上午 12:10,Billy Hinners通知@github.com
写道:

Go 的错误处理策略的目标是强制
代码的作者在任何时候都要考虑当
功能失败,最重要的是如何清理、撤消和恢复
在返回给调用者之前。

这是一个无可争议的崇高目标。 这是一个目标,但必须是
平衡主要流程的可读性和代码的意图。


您收到此消息是因为您发表了评论。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/19991#issuecomment-301794653或静音
线程
https://github.com/notifications/unsubscribe-auth/AAAcAzfcu5hq86xxVj85qfOquVawHh44ks5r6a5zgaJpZM4M-dud
.

@davecheney ,虽然我同意你的观点,错误处理应该是明确的,而不是推迟到以后(当然,你可以用 _ 来做),还有一种“冒泡”错误的策略,以在一个函数中处理它们,添加额外信息或删除(在将其发送给客户端之前)。 我个人的问题是我必须一遍又一遍地编写相同的 4 行代码

例如:

getNewToken(id int64)(令牌,错误){

user := &User{ID:id}

u, err := user.Get();
if err != nil {
    return Token{}, err
}

token, err := token.New(u);
if err != nil {
    return Token{}, err
}
return token, nil

}
我不在这里处理错误,我只是返回它。 而当我阅读这种代码时,我不得不跳过错误“处理”,并且很难找到代码的主要目的

上面的代码很容易被替换为类似的东西:

getNewToken(id int64)(令牌,错误){

user := &User{ID:id}

u, err := throw user.Get(); //throw should also wrap the error

token, err := throw token.New(u);

return token, nil

}
像这样的代码可读性更好,不需要(恕我直言)代码。 并且错误可以并且应该在使用此函数的函数中处理。

作为一名 Go 程序员,我可以告诉你,我没有发现 Go 错误处理的冗长会损害它的可读性。

我同意。

在一个不相关的注释上:

在我看来,“结果”类型对于提案来说有点过于具体了; 也许类型真的只是两个变量的枚举类型。 如果有枚举的概念,那么可以从树中创建一个结果或选项包,并在将其添加到语言之前进行试验,而无需添加许多无法真正重用且仅是好的额外语法或方法对于结果类型。 我不知道枚举在 Go 中是否有用,但是如果您可以争论更一般的情况,它可能也会使您的情况对更具体的结果类型更有力(我怀疑;也许我错了)。

func getNewToken(id int64)(令牌,错误){
用户 := &User{ID:id}

u, err := user.Get()
if err != nil {
    return Token{}, err
}

return token.New(u)

}

似乎是等价的。

在周三2017年5月17日12:34 AM,Kiura [email protected]写道:

@davecheney https://github.com/davecheney ,而我同意你的看法
错误处理应该是明确的,而不是推迟到以后(这
你当然可以用_),还有“冒泡”的策略
在一个功能中处理它们的错误,添加额外信息或
删除(在将其发送给客户端之前)。 我的个人问题是我有
一遍又一遍地编写相同的 4 行代码

例如:

getNewToken(id int64)(令牌,错误){

用户 := &User{ID:id}

你,错误:= user.Get();
如果错误!= nil {
返回令牌{},错误
}

令牌,错误:= token.New(u);
如果错误!= nil {
返回令牌{},错误
}
返回令牌,无

}
我不在这里处理错误,我只是返回它。 当我阅读时
这种代码,我不得不跳过错误“处理”,而且很难找到
代码的主要目的

上面的代码很容易被替换为类似的东西:

getNewToken(id int64)(令牌,错误){

用户 := &User{ID:id}

u, err := throw user.Get(); //throw 也应该包装错误

token, err := throw token.New(u);

返回令牌,无

}
像这样的代码可读性更好,不需要(恕我直言)代码。 而
错误可以并且应该在此函数所在的函数中处理
用过的。


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/19991#issuecomment-301802010或静音
线程
https://github.com/notifications/unsubscribe-auth/AAAcA9sIRXX7RSdDcUOidpe-qLTR7unNks5r6bP3gaJpZM4M-dud
.

在我看来,“结果”类型对于提案来说有点过于具体了; 也许类型真的只是两个变量的枚举类型。 如果有枚举的概念,那么可以从树中创建一个结果或选项包,并在将其添加到语言之前进行试验,而无需添加许多无法真正重用且仅是好的额外语法或方法对于结果类型。 我不知道枚举在 Go 中是否有用,但是如果您可以争论更一般的情况,它可能也会使您的情况对更具体的结果类型更有力(我怀疑;也许我错了)。

正如原始提案中所述,理想情况下,结果类型将实现为和类型(例如 enums ala Rust's),并且有一个公开的提案将它们添加到语言中。

然而,在没有额外语言支持的情况下,单独的 sum 类型不足以实现可重用的结果类型库。 他们还需要泛型。

该提案正在探索实现不依赖于泛型,而是依赖于编译器的特殊情况帮助的结果类型的想法。

我只想补充一点,现在已经发布了,我同意追求这一点的最佳方式(如果有的话)是使用语言级别的泛型支持。

@davecheney ,确实,在这种情况下几乎没有区别,但是如果您在函数中有 3-4 次调用返回错误怎么办?

PS 我并不反对 Go1 结构处理错误的方式,我只是认为它可以更好。

正如原始提案中所述,理想情况下,结果类型将实现为和类型(例如 enums ala Rust's),并且有一个公开的提案将它们添加到语言中。

对不起,我应该更清楚:我认为这个陈述:

我认为 Go 结果类型也不需要,并且可以简单地利用特殊情况编译器魔术。

对我来说感觉是个坏主意。

然而,在没有额外语言支持的情况下,单独的 sum 类型不足以实现可重用的结果类型库。 他们还需要泛型。

该提案正在探索实现不依赖于泛型,而是依赖于编译器的特殊情况帮助的结果类型的想法。

我只想补充一点,现在已经发布了,我同意追求这一点的最佳方式(如果有的话)是使用语言级别的泛型支持。

是的,足够公平; 那我同意你最后的说法。 如果我们无论如何都要等待 Go 2,我们不妨先解决更一般的问题(假设它实际上是一个问题):)

另外,Rob Pike 写了一篇关于上面提到的错误处理的文章。 虽然这种方法似乎在“修复”问题,但它引入了另一个问题:更多的代码膨胀与接口。

我认为不要将“显式错误处理”与“详细错误处理”混淆,这一点很重要。 Go 希望强制用户在每一步都考虑错误处理,而不是将其委派出去。 对于您调用的每个可能抛出错误的函数,您需要决定是否要处理错误以及如何处理。 有时意味着您忽略错误,有时意味着您重试,通常意味着您只是将其传递给调用者来处理。

Rob 的文章很棒,确实应该成为 Effective Go 2 的一部分,但它的策略只能带您到此为止。 尤其是在处理异构的被调用者时,您有很多错误处理需要管理

我不认为考虑使用语法糖或其他一些工具来帮助处理错误是不合理的。 我认为重要的是它不会破坏 Go 错误处理的基础。 例如,建立一个函数级的错误处理程序来处理所有发生的错误是不好的; 这意味着我们允许程序员做异常处理通常会做的事情:将错误的考虑从语句级问题转移到块级或函数级问题。 这绝对是违反哲学的。

@billyh关于“Go 中的错误处理模式”文章,还有其他解决方案:

@egonelbre
这些解决方案仅适用于您重复执行相同类型的操作。 通常情况并非如此。 因此,这几乎不可能在实践中应用。

@urandom请展示一个现实的例子呢?

当然我可以一个

func (conversion *PageConversion) Convert() (page *kb.Page, errs []error, fatal error)

我知道这些并不适用于任何地方,但如果没有我们想要改进的适当示例列表,就无法进行体面的讨论。

@egonelbre

https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go

免责声明:我没有使用过juju,也没有阅读过代码。 这只是我头脑中知道的“生产”产品。 我有理由确信这种类型的错误处理(在独立操作之间检查错误)在 go 世界中很普遍,而且我非常怀疑有没有人没有偶然发现这一点。

@urandom我同意。 在没有实际代码的情况下进行讨论的主要问题是人们记住了问题的“要点”,而不是实际问题——这通常会导致问题陈述过于简单。 _PS:我记得go 中的一个

例如,从这些现实世界的例子中,我们可以看到还有一些其他的事情需要考虑:

  • 好的错误信息
  • 基于错误值的恢复/备用路径
  • 后备
  • 尽最大努力执行错误
  • 快乐案例记录
  • 故障记录
  • 故障追踪
  • 返回多个错误
  • _当然,其中一些会一起使用_
  • ......可能是我错过的一些......

不仅仅是“快乐”和“失败”的道路。 我并不是说这些问题无法解决,只是需要对其进行规划和讨论。

@egonelbre这是本周 Golang Weekly 的另一个例子,在 Mario Zupan 的题为“Writing a Static Blog Generator in Go”的文章中:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    fmt.Printf("Fetching data from %s into %s...\n", from, to)
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    fmt.Print("Fetching complete.\n")
    return dirs, nil
}

注意:我并不是在暗示对马里奥代码的任何批评。 事实上,我很喜欢他的文章。
不幸的是,像这样的例子在 Go 源代码中太常见了。 Go 代码倾向于这种火车轨道模式,即一行感兴趣的行,然后是三行相同或几乎相同的样板,一遍又一遍地重复。 在可能的情况下将赋值和条件结合起来,就像马里奥所做的那样,会有一点帮助。

我不确定任何编程语言的设计主要目标是最大限度地减少代码行数,但是 a) 有意义的代码与样板的比率可能是一种(许多)编程语言质量的有效衡量标准,并且 b)因为太多的编程涉及错误处理,这种模式遍及 Go 代码,因此使这种多余样板的特殊情况变得值得精简。

如果我们能找到一个好的替代方案,我相信它会被迅速采用,并使 Go 的阅读、编写和维护变得更加愉快。

Rebecca Skinner (@cercerilla) 在她的幻灯片Monadic Error Handling in Go 中分享了一篇关于 Go 错误处理缺点的优秀文章,以及使用 monad 作为解决方案的分析。 我特别喜欢她最后的结论。

感谢@davecheney在他的文章Simplicity Debt Redux 中提到 Rebecca 的套牌,这让我能够找到它。 (还要感谢 Dave 将我对 Go 2 的玫瑰色乐观与更严峻的现实联系起来。)

Go 代码倾向于这种火车轨道模式,即一行感兴趣的行,然后是三行相同或几乎相同的样板,一遍又一遍地重复。

每个控制流控制语句都很重要。 从正确性的角度来看,错误处理线非常重要。

有意义的代码与样板的比率可能是(许多)有效衡量编程语言质量的一个

如果有人认为错误处理语句没有意义,那么祝你编码好运,我希望远离结果。

为了解决@davecheneySimplicity Debt Redux (我已经介绍过,但我认为它值得重复)中涵盖的要点之一:

下一个问题是,这种一元形式会成为处理错误的单一方式吗?

对于这样的事情要成为处理错误的“单一”方式,必须在整个标准库和每个“Go2”兼容项目中进行重大更改。 我认为这是不明智的:Python2/3 的崩溃显示了这样的分裂是如何破坏语言生态系统的。

正如本提案中提到的,如果结果类型可以自动强制转换为等效的元组形式,那么就假设的 Go2 标准库全面采用这种方法,同时仍然保持与现有代码的向后兼容性,您可以自食其力. 这将允许那些有兴趣的人利用它,但仍然希望在 Go1 上工作的库将只是开箱即用。 库作者可以有自己的选择:使用旧样式编写在 Go1 和 Go2 上都可以使用的库,或者使用 monadic 样式编写仅适用于 Go2 的库。

错误处理的“旧方式”和“新方式”可以兼容,以至于该语言的用户甚至不必考虑它,如果他们愿意,可以继续以“旧方式”做事。 虽然这缺乏一定的概念纯度,但我认为这比允许现有代码继续工作而不进行修改以及允许人们开发适用于该语言的所有版本(而不仅仅是最新版本)的库要重要得多。

这似乎令人困惑,并为 Go 2.0 的新手提供了不明确的指导,以继续支持错误接口模型和新的 monadic 类型。

它们是刹车:要么让语言保持原样,要么进化语言,增加偶然的复杂性并将以前的做事方式归咎于遗留的问题。 我真的认为这是唯一的两个选项,因为添加一个新功能来取代旧功能,无论旧功能是否已弃用但兼容或以破坏性更改的形式退出,我认为是该语言的用户将不得不学习不管。

我认为改变语言是不可能的,但让新手避免学习做事的“旧方式”和“新方式”,即使 Go2 假设完全采用这种方式。 你仍然会留下 Go1 和 Go2 的分歧,新手会想知道有什么区别,无论如何最终都不可避免地不得不学习“Go1”。

我认为向后兼容性对教授语言和代码兼容性都有帮助:所有现有的 Go 教学材料将继续有效,即使语法已经过时。 不需要通读 Go 教材的每一部分并使旧语法无效:教材可以在闲暇时添加一个新语法的通知。

我理解“有不止一种方法可以做到”通常与 Go 的简单和极简主义哲学背道而驰,但这是添加新语言功能必须付出的代价。 就其性质而言,新的语言特性将淘汰旧的方法。

我当然愿意承认,可能有一种方法可以以对 Gophers 更自然的方式解决相同的核心问题,而不是对现有方法进行如此不和谐的改变。

还有一件需要考虑的事情:虽然 Go 在保持语言易于学习方面做得堪称典范,但这并不是让人们开始使用一种语言的唯一障碍。 我认为可以肯定地说,有很多人看到 Go 错误处理的冗长性并被它推迟,有些人甚至拒绝采用该语言。

我认为值得一问的是,语言的改进是否可以吸引目前被它拖延的人,以及这如何与使语言更难学习之间取得平衡。

然而,做像 monadic 错误处理这样的事情违背了 Go 的让你思考错误的哲学。 Monadic 错误处理和 Java 风格的异常处理在语义上非常接近(尽管语法不同)。 Go 采取了一种刻意不同的哲学,期望程序员显式处理每个错误,而不是仅在您想到时添加错误处理代码。 事实上,严格来说return nil, err成语并不是最佳的,因为您可能可以添加额外的有用上下文。

我觉得任何解决 Go 错误处理的尝试都应该牢记这一点,不要轻易避免考虑错误。

@alercah我几乎不得不对你刚才所说的一切表示不同...

做像 monadic 错误处理这样的事情违背了 Go 的让你思考错误的哲学

来自 Rust,我认为 Rust(或者更确切地说,Rust 编译器)实际上让我比 Go 更考虑错误。 Rust 在其Result类型上有一个#[must_use]属性,这意味着未使用的结果会生成编译器警告。 在 Go 中并非如此(Rebecca Skinner 在她的演讲中提到了这一点):Go 编译器不会警告例如未处理的error值。

Rust 类型系统强制在您的代码中解决每个错误情况,如果没有,那就是类型错误,或者充其量是警告。

Monadic 错误处理和 Java 风格的异常处理在语义上非常接近(尽管语法不同)。

让我分解一下为什么这不是真的:

错误传播策略

  • Go:返回值,显式传播
  • Java:非本地跳转,隐式传播
  • Rust:返回值,显式传播

错误类型

  • Go:每个函数一个返回值,通常是(success, error) 2 元组
  • Java:由许多异常类型组成的已检查异常,表示从方法中可能抛出的所有异常的集合联合。 还有未声明且可能随时随地发生的未经检查的异常。
  • Rust:每个函数一个返回值,一般Result总和类型,例如Result<Success, Error>

总而言之,在错误处理方面,我觉得 Go 更接近 Rust 而不是 Java:Go 和 Rust 中的错误只是值,它们不是异常。 您必须明确选择加入传播。 您必须将不同类型的错误转换为给定函数返回的错误类型,例如通过包装。 它们最终都代表一个成功值/错误对,只是使用不同的类型系统功能(元组与通用和类型)。

在一些例外情况下,Rust 确实提供了一些抽象,可以在一个板条箱的基础上选择性地使用它们来进行隐式错误处理(或者更确切地说,显式错误转换,您仍然必须手动传播错误)。 例如, From trait 可用于自动将错误从一种类型转换为另一种类型。 我个人认为,能够定义一个完全适用于特定包的策略,让您可以自动将错误从一种显式类型转换为另一种显式类型,这是一个优势,而不是一个缺点。 Rust 的 trait 系统只允许你为你自己的 crate 中的类型定义From ,防止任何类型的幽灵般的远距离动作。

不过,这远远超出了本提案的范围,并且涉及 Go 没有协同工作的几种语言功能,所以我认为 Go 没有任何滑坡支持这些类型的隐式转换的“风险” ,至少在 Go 添加泛型和特征/类型类之前不会。

在这件事上投入我的两分钱。 我认为这种功能对于公司(例如我自己的雇主)非常有用,在这些公司中,单个应用程序与大量附属数据源对话并以直接的方式组合结果。

这是我们将拥有的一些代码流的代表性数据示例

func generateUser(userID : string) (User, error) {
      siteProperties, err := clients.GetSiteProperties()
      if err != nil {
           return nil, err
     }
     chatProperties, err := clients.GetChatProperties()
      if err != nil {
           return nil, err
     }

     followersProperties, err := clients.GetFollowersProperties()
      if err != nil {
           return nil, err
     }


// ... (repeat X5)
     return createUser(siteProperties, ChatProperties, followersProperties, ... /*other properties here */), nil
}

我理解 Go 旨在迫使用户在每个点考虑错误的很多阻力,但在绝大多数函数返回T, err代码库中,这会导致大量的、现实世界的代码膨胀并且实际上导致了生产失败,因为有人在进行额外的函数调用后忘记添加错误处理代码,并且 err 默默地未被检查。 此外,事实上,对于我们一些最健谈的服务来说,大约 20% 以上的错误处理并不少见,其中很少有人感兴趣。

此外,这种错误处理逻辑的绝大多数是相同的,而且矛盾的是,我们代码库中大量的显式错误处理使得很难找到真正有趣的异常情况的代码,因为有一点“大海捞针” ' 现象在起作用。

我完全可以理解为什么这个提议可能不是解决方案,但我相信需要有_一些_方式来减少这个样板。

还有一些无聊的想法:

Rust 的尾随?是一个很好的语法。 但是,对于 Go,鉴于错误上下文的重要性,我可能会建议以下变体:

  • 尾随?工作方式类似于 Rust,针对 Go 进行了修改。 具体来说:它只能在最后一个返回值为error的函数中使用,并且必须在最后一个返回值也是error类型的函数调用之后立即出现(注意:我们可以也允许实现error任何类型,但需要error可以防止出现 nil 接口问题,这是一个很好的奖励)。 效果是如果错误值非零, ?出现的函数从函数返回,将最后一个参数设置为错误值。 对于使用命名返回值的函数,它可以为其他值或当前存储的任何值返回零; 对于没有的函数,其他返回值始终为零。
  • 尾随.?("opening %s", file)工作方式与上述相同,不同之处在于它不是返回未修改的错误,而是通过一个组成错误的函数传递; 粗略地说, .?(str, vals...)fmt.Errorf(str + ": %s", vals..., err)
  • 可能应该有一个版本,要么是.?语法的变体,要么是不同的版本,涵盖包想要导出可分辨错误类型的情况。

与#19412(和类型)和#21161(错误处理)和#15292(泛型)相关。

有关的:

新错误处理功能的“设计草案”:
https://go.googlesource.com/proposal/+/master/design/go2draft.md

对错误设计的反馈:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

我喜欢@alercah 的建议来解决@LegoRemix正在谈论的 go-lang 的一个令人讨厌的功能,而不是创建单独的返回类型。

我只是建议更多地遵循 Rust 的 RFC 以避免猜测零值并引入catch表达式来让函数明确指定在主体返回错误的情况下返回的内容:

所以这:

func generateUser(userID string) (*User, error) {
    siteProperties, err := clients.GetSiteProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    chatProperties, err := clients.GetChatProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    followersProperties, err := clients.GetFollowersProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    return createUser(siteProperties, ChatProperties, followersProperties), nil
}

变成了这个 DRY 代码:

func generateUser(userID string) (*User, error) {
    siteProperties := clients.GetSiteProperties()?
    chatProperties := clients.GetChatProperties()?
    followersProperties := clients.GetFollowersProperties()?

    return createUser(siteProperties, ChatProperties, followersProperties), nil
} catch (err error) {
    return nil, errors.Wrapf(err, "error generating user: %s", userID)
}

并且要求使用?运算符的函数还必须定义catch

@bradfitz @peterbourgon @SamWhited也许应该有另一个问题?

@sheerun您的?运算符和catch语句看起来与新错误处理草案设计中的check运算符和handle语句非常相似(https: //go.googlesource.com/proposal/+/master/design/go2draft.md)。

它看起来更好,对于好奇的人来说,这就是我的代码与checkhandle样子:

func generateUser(userID string) (*User, error) {
    handle err { return nil, errors.Wrapf(err, "error generating user: %s", userID) }

    siteProperties := check clients.GetSiteProperties()
    chatProperties := check clients.GetChatProperties()
    followersProperties := check clients.GetFollowersProperties()

    return createUser(siteProperties, chatProperties, followersProperties), nil
}

我唯一要改变的是摆脱隐式handle并要求它在使用检查时被定义。 它将防止开发人员懒惰地使用检查并更多地考虑如何处理或包装错误。 隐式返回应该是单独的功能,可以按照之前的建议使用:

func generateUser(userID string) (*User, error) {
    handle err { return _, errors.Wrapf(err, "error generating user: %s", userID) }

    siteProperties := check clients.GetSiteProperties()
    chatProperties := check clients.GetChatProperties()
    followersProperties := check clients.GetFollowersProperties()

    return createUser(siteProperties, chatProperties, followersProperties), nil
}

作为该提案的作者,我认为值得注意的是,它实际上被 #15292 无效,并且像https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md一样工作,作为这个提案是在假设通用编程工具不可用的情况下编写的。 因此,它建议使用新语法来允许result()的特殊情况的类型多态性,如果可以通过使用例如合同来避免这种情况,我认为这个提议不再有意义。

由于看起来其中至少有一个可能会在 Go 2 中结束,我想知道是否应该关闭这个特定的提案,以及人们是否仍然对结果类型感兴趣作为handle的替代方案,假设例如合同可用,它被重写。

(请注意,我可能没有时间做这项工作,但如果其他人有兴趣看到这个想法,那就去做吧)

@sheerun提交有关 Go 2 错误处理的反馈和想法的地方是这个 wiki 页面:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

和/或 Go 2 错误处理要考虑的_Requirements 的综合列表:_
https://gist.github.com/networkimprov/961c9caa2631ad3b95413f7d44a2c98a

此页面是否有帮助?
0 / 5 - 0 等级