Go: 提案:一个内置的 Go 错误检查功能,“try”

创建于 2019-06-05  ·  808评论  ·  资料来源: golang/go

建议:内置 Go 错误检查功能, try

此提案已结束

在发表评论之前,请阅读详细的设计文档并查看截至 6 月 6 日的讨论摘要、截至 6月 10日的摘要,以及_最重要的是保持专注的建议_。 您的问题或建议可能已经得到回答或提出。 谢谢。

我们提出了一个名为try的新内置函数,专门设计用于消除 Go 中通常与错误处理相关的样板if语句。 不建议更改其他语言。 我们提倡使用现有的defer语句和标准库函数来帮助增加或包装错误。 这种最小化的方法解决了最常见的场景,同时给语言增加了非常少的复杂性。 内置的try易于解释,易于实现,与其他语言结构正交,并且完全向后兼容。 如果我们将来希望这样做,它还为扩展该机制开辟了道路。

[下面的文字已经过编辑,以更准确地反映设计文档。]

try内置函数将单个表达式作为参数。 表达式必须计算为 n+1 个值(其中 n 可能为零),其中最后一个值必须是error类型。 如果(最终)错误参数为 nil,则返回前 n 个值(如果有),否则从包含该错误的封闭函数返回。 例如,代码如

f, err := os.Open(filename)
if err != nil {
    return …, err  // zero values for other results, if any
}

可以简化为

f := try(os.Open(filename))

try只能在本身返回error结果的函数中使用,并且该结果必须是封闭函数的最后一个结果参数。

该提案将去年 GopherCon 上提出的原始设计草案简化为本质。 如果需要错误扩充或包装,有两种方法:坚持使用经过验证的if语句,或者,使用defer语句“声明”错误处理程序:

defer func() {
    if err != nil { // no error may have occurred - check for it
        err = … // wrap/augment error
    }
}()

这里, err是封闭函数的错误结果的名称。 在实践中,合适的辅助函数会将错误处理程序的声明简化为单行。 例如

defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

(其中fmt.HandleErrorf装饰*err )读起来很好,不需要新的语言特性就可以实现。

这种方法的主要缺点是需要命名错误结果参数,这可能导致 API 不太美观。 归根结底,这是一个风格问题,我们相信我们会适应期待新的风格,就像我们适应没有分号一样。

总而言之, try开始可能看起来不寻常,但它只是为一项特定任务量身定制的语法糖,用更少的样板进行错误处理,并且足够好地处理该任务。 因此,它非常适合 Go 的哲学。 try并非旨在解决_所有_错误处理情况; 它旨在很好地处理_最常见的_案例,以保持设计简单明了。

学分

该提案受到我们迄今为止收到的反馈的强烈影响。 具体来说,它借鉴了以下思想:

详细设计文档

https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

tryhard工具,用于探索try的影响

https://github.com/griesemer/tryhard

Go2 LanguageChange Proposal error-handling

最有用的评论

大家好,

我们提出此类提案的目标是在整个社区范围内就影响、权衡以及如何进行进行讨论,然后利用该讨论来帮助决定前进的道路。

基于压倒性的社区反应和广泛的讨论,我们标记此提案提前被拒绝。

就技术反馈而言,本次讨论有助于确定我们错过的一些重要考虑因素,最值得注意的是添加调试打印和分析代码覆盖率的含义。

更重要的是,我们清楚地听到了许多人认为该提案不是针对一个有价值的问题。 我们仍然认为 Go 中的错误处理并不完美,可以进行有意义的改进,但很明显,作为一个社区,我们需要更多地讨论错误处理的哪些具体方面是我们应该解决的问题。

至于讨论要解决的问题,我们在去年 8 月的“ Go 2 错误处理问题概述”中试图提出我们对问题的看法,但回想起来我们没有引起足够的重视,也没有给予足够的鼓励讨论具体问题是否正确。 try提案可能是解决那里概述的问题的好方法,但对你们中的许多人来说,这根本不是要解决的问题。 未来,我们需要更好地引起人们对这些早期问题陈述的关注,并确保就需要解决的问题达成广泛共识。

(也有可能因为在同一天发布了一个泛型设计草案,错误处理问题陈述被完全抢了风头。)

关于如何改进 Go 错误处理的更广泛主题,我们很高兴看到有关 Go 错误处理的哪些方面在您自己的代码库和工作环境中对您来说最有问题的经验报告,以及一个好的解决方案会产生多大的影响有在自己的发展。 如果您确实撰写了此类报告,请在Go2ErrorHandlingFeedback 页面上发布链接。

感谢在这里和其他地方参与此讨论的所有人。 正如 Russ Cox 之前指出的那样,像这样的社区范围内的讨论是开源的。 我们非常感谢大家帮助检查这个特定的提案,以及更广泛地讨论改善 Go 中错误处理状态的最佳方法。

Robert Griesemer,提案审查委员会。

所有808条评论

我同意这是最好的方法:用简单的设计解决最常见的问题。

我不想骑自行车(请随意推迟这个对话),但 Rust 去了那里并最终解决了?后缀运算符而不是内置函数,以提高可读性。

gophercon 提案在考虑的想法中引用了?并给出了它被丢弃的三个原因:第一个(“控制流传输作为一般规则伴随关键字”)和第三个(“处理程序更自然地定义带有关键字,因此检查也应该“)不再适用。 第二个是文体:它说,即使后缀运算符更适合链接,它在某些情况下仍然可能读起来更糟,例如:

check io.Copy(w, check newReader(foo))

而不是:

io.Copy(w, newReader(foo)?)?

但现在我们将拥有:

try(io.Copy(w, try(newReader(foo))))

我认为这显然是三者中最糟糕的,因为它不再明显是被调用的主要函数。

所以我评论的要点是 gophercon 提案中提到的不使用?的所有三个原因都不适用于这个try提案; ?简洁,可读性强,不会混淆语句结构(具有内部函数调用层次结构),并且是可链接的。 它从视图中消除了更多的混乱,同时不会比建议的try()已经做的更模糊控制流。

澄清:

func f() (n int, err error) {
  n = 7
  try(errors.New("x"))
  // ...
}

返回(0,“x”)还是(7,“x”)? 我假设后者。

在没有装饰或处理的情况下(如在内部辅助函数中)是否必须命名错误返回? 我假设不是。

您的示例返回7, errors.New("x") 。 这应该在即将提交的完整文档中明确(https://golang.org/cl/180557)。

不需要命名错误结果参数即可使用try 。 仅当函数需要在延迟函数或其他地方引用它时才需要命名。

我对影响调用者控制流的内置函数非常不满意。 我很欣赏在 Go 1 中添加新关键字是不可能的,但是用魔法内置函数解决这个问题对我来说似乎是错误的。 其他内置插件的阴影不会产生像控制流变化那样不可预测的结果。

我不喜欢后缀?的样子,但我认为它仍然胜过try()

编辑:好吧,我设法完全忘记了恐慌的存在并且不是关键字。

详细的建议现在在这里(等待格式改进,很快就会出现),希望能回答很多问题。

@dominikh详细的提案详细讨论了这一点,但请注意panicrecover也是影响控制流的两个内置函数。

一项澄清/改进建议:

if the last argument supplied to try, of type error, is not nil, the enclosing function’s error result variable (...) is set to that non-nil error value before the enclosing function returns

这可以改为is set to that non-nil error value and the enclosing function returns吗? (s/之前/和)

初读时, before the enclosing function returns似乎会_最终_在函数返回之前的某个时间点设置错误值——可能在后面的一行。 正确的解释是 try 可能会导致当前函数返回。 对于当前语言来说,这是一个令人惊讶的行为,因此欢迎使用更清晰的文本。

我认为这只是糖,有少数声音反对者嘲笑 golang 重复使用键入if err != nil ...并且有人认真对待。 我不认为这是一个问题。 唯一缺少的是这两个内置函数:

https://github.com/purpleidea/mgmt/blob/a235b760dc3047a0d66bb0b9d63c25bc746ed274/util/errwrap/errwrap.go#L26

不知道为什么有人会写这样的函数,但设想的输出是什么

try(foobar())

如果foobar返回(error, error)

我收回了我之前对控制流的担忧,我不再建议使用? 。 对于下意识的回应,我深表歉意(尽管我想指出,如果问题是在完整的提案可用后提交的,则不会发生这种情况)。

我不同意简化错误处理的必要性,但我确信这是一场失败的战斗。 提案中列出的try似乎是最不坏的方法。

@webermaster只有最后一个error结果对于传递给try的表达式是特殊的,如提案文档中所述。

@dominikh一样,我也不同意简化错误处理的必要性。

它将垂直复杂性转变为水平复杂性,这很少是一个好主意。

但是,如果我必须在简化错误处理建议之间做出选择,这将是我的首选建议。

如果这可以伴随(在某个被接受的阶段)通过一个工具来转换 Go 代码以在错误返回函数的某些子集中使用try会很有帮助,这样的转换可以很容易地执行而无需改变语义。 我想到了三个好处:

  • 在评估这个提议时,它可以让人们快速了解如何在他们的代码库中使用try
  • 如果try出现在 Go 的未来版本中,人们可能会想要更改他们的代码以使用它。 拥有一个自动化简单案例的工具将有很大帮助。
  • 有一种方法可以快速转换大型代码库以使用try将可以轻松地检查大规模实现的效果。 (例如,正确性、性能和代码大小。)但是,实现可能足够简单,可以忽略不计。

我只是想表达我认为一个简单的try(foo())实际上从调用函数中跳出来带走了我们的视觉提示,即函数流可能会根据结果而改变。

我觉得我可以使用try ,只要有足够的习惯,但我也觉得我们需要额外的 IDE 支持(或类似的支持)来突出显示try以有效识别代码审查中的隐式流程/调试会话

我最关心的是需要命名返回值,这样 defer 语句才会满意。

我认为社区抱怨的整体错误处理问题是if err != nil样板和向错误添加上下文的组合。 常见问题解答清楚地指出后者是故意作为一个单独的问题排除在外的,但我觉得这成为一个不完整的解决方案,但在考虑以下两件事后,我愿意给它一个机会:

  1. 在函数的开头声明err
    这行得通吗? 我记得延迟和未命名结果的问题。 如果不是,该提案需要考虑这一点。
func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}
  1. 像过去一样分配值,但使用具有if err != nil样板的帮助器wrapf函数。
func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

如果任何一个工作,我可以处理它。

func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}

这行不通。 defer 会更新与返回值无关的本地err变量。

func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

那应该行得通。 不过,即使出现 nil 错误,它也会调用 wrapf。
这也将(继续)起作用,并且 IMO 更加清晰:

func sample() (string, error) {
  s, err := f()
  if err != nil {
      return "", wrap(err)
  }
  return s, nil
}

没有人会让你使用try

不知道为什么有人会写这样的函数,但设想的输出是什么

try(foobar())

如果foobar返回(error, error)

为什么你会从一个函数返回多个错误? 如果您从函数返回多个错误,则函数可能首先应拆分为两个单独的错误,每个仅返回一个错误。

你能用一个例子详细说明吗?

@cespare :有人应该可以编写一个go fix来重写适用于try的现有代码,以便它使用try 。 了解如何简化现有代码可能很有用。 我们预计代码大小或性能不会有任何显着变化,因为try只是语法糖,用更短的源代码替换常见模式,生成基本相同的输出代码。 另请注意,使用try的代码将绑定使用至少是引入try的版本的 Go 版本。

@lestrrat :同意必须了解try可以更改控制流。 我们怀疑 IDE 可以很容易地突出显示这一点。

@Goodwine :正如@randall77已经指出的那样,你的第一个建议是行不通的。 我们考虑过的一个选项(但未在文档中讨论)是有一些预先声明的变量来表示error结果(​​如果首先存在的话)的可能性。 这将消除命名该结果的需要,以便它可以在defer中使用。 但这会更神奇。 这似乎不合理。 命名返回结果的问题本质上是装饰性的,最重要的是go doc和朋友提供的自动生成的 API。 在这些工具中很容易解决这个问题(另请参阅有关此主题的详细设计文档的常见问题解答)。

@nictuku :关于您的澄清建议(s/before/and/):我认为您所指的段落之前的代码清楚地说明了究竟发生了什么,但我明白您的意思,s/before/and/ may使散文更清晰。 我会做出改变。

参见CL 180637

我真的很喜欢这个提议。 但是,我确实有一个批评。 Go 中函数的退出点总是用return标记。 恐慌也是退出点,但这些是通常不会遇到的灾难性错误。

为不是return的函数创建一个退出点,并且这意味着它是司空见惯的,可能会导致代码的可读性大大降低。 我在一次演讲中听说过这个,很难看出这段代码的结构之美:

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if _, err := io.Copy(w, r); err != nil {
        w.Close()
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if err := w.Close(); err != nil {
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
}

这段代码可能看起来很乱,并且是错误处理草案的_意思是_,但让我们将它与try进行比较。

func CopyFile(src, dst string) error {
    defer func() {
        err = fmt.Errorf("copy %s %s: %v", src, dst, err)
    }()
    r, err := try(os.Open(src))
    defer r.Close()

    w, err := try(os.Create(dst))

    defer w.Close()
    defer os.Remove(dst)
    try(io.Copy(w, r))
    try(w.Close())

    return nil
}

乍一看,你可能会觉得它看起来更好,因为重复的代码少了很多。 但是,很容易发现函数在第一个示例中返回的所有点。 它们都是缩进的,并以return开头,后跟一个空格。 这是因为所有条件返回_必须_在条件块内,因此按gofmt标准缩进。 如前所述, return也是离开函数而不说发生灾难性错误的唯一方法。 在第二个示例中,只有一个return ,因此看起来函数 _ever_ 应该返回的唯一内容是nil 。 最后两个try调用很容易看到,但前两个调用有点难,如果它们嵌套在某个地方会更难,例如proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1]))))

从函数返回似乎是一件“神圣”的事情,这就是为什么我个人认为函数的所有退出点都应该用return标记。

5年前已经有人实施了。 如果你有兴趣,你可以
试试这个功能

https://news.ycombinator.com/item?id=20101417

五年前,我在 Go 中使用 AST 预处理器实现了 try(),并在实际项目中使用它,非常好: https ://github.com/lunixbochs/og

以下是我在错误检查繁重的功能中使用它的一些示例: https ://github.com/lunixbochs/poxd/blob/master/tls.go#L13

我很感激为此付出的努力。 我认为这是迄今为止我见过的最简单的解决方案。 但我认为它在调试时引入了一堆工作。 每次调试时打开尝试并添加一个 if 块,完成后重新包装它是乏味的。 而且我对我需要考虑的神奇 err 变量也有些畏缩。 我从来没有被明确的错误检查所困扰,所以也许我是问错人了。 它总是让我觉得“准备调试”。

@griesemer
我建议使用 defer 作为处理错误包装的方法的问题是,我展示的代码段中的行为(在下面重复)不是很常见的 AFAICT,因为它非常罕见,所以我可以想象人们写这个认为它有效当它没有的时候。

就像..初学者不会知道这一点,如果他们因此有错误,他们不会去“当然,我需要一个命名的回报”,他们会感到压力很大,因为它应该工作而它没有。

var err error
defer fmt.HandleErrorf(err);

try已经太神奇了,所以你不妨一路走下去,加上那个隐含的错误值。 考虑初学者,而不是了解围棋所有细微差别的人。 如果不够清楚,我认为这不是正确的解决方案。

或者......不要建议像这样使用 defer ,尝试另一种更安全但仍然可读的方式。

@deanveloper确实,该提案(以及就此而言,任何尝试尝试相同事情的提案)将从源代码中删除明确可见的return语句-毕竟这就是提案的重点,不是吗? 删除相同的if语句和returns的样板。 如果要保留return ,请不要使用try

我们习惯于立即识别return语句(和panic的),因为这就是这种控制流在 Go(和许多其他语言)中的表达方式。 就像我们对return所做的那样,在一些人习惯了它之后,我们也会将try识别为改变控制流,这似乎并不牵强。 我毫不怀疑良好的 IDE 支持也会对此有所帮助。

我有两个担忧:

  • 命名返回非常令人困惑,这鼓励他们使用新的重要用例
  • 这将阻止向错误添加上下文

根据我的经验,在每个调用站点之后立即为错误添加上下文对于拥有易于调试的代码至关重要。 在某些时候,我认识的几乎所有 Go 开发人员都对命名返回造成了困惑。

一个更次要的文体问题是,不幸的是,现在有多少行代码将被包装在try(actualThing())中。 我可以想象看到包含在try()中的代码库中的大多数行。 这感觉很不幸。

我认为这些问题可以通过调整来解决:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check()的行为很像try() ,但会放弃一般传递函数返回值的行为,而是提供添加上下文的能力。 它仍然会触发返回。

这将保留try()的许多优点:

  • 这是一个内置的
  • 它遵循现有的控制流 WRT 来推迟
  • 它与为错误添加上下文的现有做法很好地保持一致
  • 它与当前的错误包装建议和库保持一致,例如errors.Wrap(err, "context message")
  • 它会产生一个干净的调用站点: a, b, err := myFunc()行上没有样板
  • 仍然可以使用defer fmt.HandleError(&err, "msg")描述错误,但不需要鼓励。
  • check的签名稍微简单一些,因为它不需要从它包装的函数返回任意数量的参数。

@s4n-gt 感谢您提供此链接。 我没有意识到这一点。

采取@Goodwine点。 设计文档中详细讨论了不提供更直接的错误处理支持的原因。 这也是一个事实,在一年左右的时间里(自去年 Gophercon 上发布的设计草案以来)没有出现令人满意的显式错误处理解决方案。 这就是为什么该提案故意将其排除在外(而是建议使用defer )。 该提议仍然为未来在这方面的改进打开了大门。

该提案提到更改包测试以允许测试和基准测试返回错误。 虽然这不是“一个适度的库更改”,但我们也可以考虑接受func main() error 。 它会使编写小脚本变得更好。 语义相当于:

func main() {
  if err := newmain(); err != nil {
    println(err.Error())
    os.Exit(1)
  }
}

最后一点批评。 并不是对提案本身的批评,而是对对“功能控制流程”反驳的共同回应的批评。

对“我不喜欢函数控制流程”的回应是“ panic也控制程序的流程!”。 但是,有几个原因更适合panic这样做不适用于try

  1. panic对初学者程序员很友好,因为它的作用很直观,它会继续展开堆栈。 甚至不必查看panic的工作原理即可了解它的作用。 初学者甚至不需要担心recover ,因为初学者通常不会建立恐慌恢复机制,特别是因为它们几乎总是不如一开始就避免恐慌。

  2. panic是一个很容易看到的名字。 它带来了担忧,而且它需要。 如果有人在代码库中看到panic ,他们应该立即考虑如何_避免_恐慌,即使它是微不足道的。

  3. 捎带最后一点, panic不能嵌套在调用中,使其更容易查看。

恐慌控制程序的流程是可以的,因为它非常容易发现,而且它的作用很直观。

try函数不满足这些点。

  1. 如果不查找文档,就无法猜测try做了什么。 许多语言以不同的方式使用关键字,因此很难理解它在 Go 中的含义。

  2. try没有引起我的注意,尤其是当它是一个函数时。 _特别是_当语法高亮将它作为一个函数高亮时。 _特别是在使用 Java 等语言进行开发后,其中try被视为不必要的样板文件(因为已检查的异常)。

  3. try可以在函数调用的参数中使用,就像我在之前的评论proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1]))))中的示例一样。 这使得它更难被发现。

我的眼睛忽略了try功能,即使我专门寻找它们。 我的眼睛会看到它们,但会立即跳到os.FindProcessstrconv.Atoi调用。 try是有条件的回报。 在 Go 中,控制流和返回都被固定在基座上。 函数内的所有控制流都是缩进的,所有返回都以return开头。 将这两个概念混合在一起形成一个容易错过的函数调用,感觉有点不对劲。


不过,这条评论和我的最后一条评论是我对这个想法的唯一真正批评。 我想我可能不喜欢这个提议,但我仍然认为这对 Go 来说是一个整体的胜利。 这个解决方案仍然比其他解决方案更像 Go。 如果添加了这个我会很高兴,但是我认为它仍然可以改进,我只是不确定如何。

@buchanae很有趣。 但是,正如所写的那样,它将 fmt 样式的格式从一个包移到了语言本身,这打开了一个蠕虫罐。

但是,正如所写的那样,它将 fmt 样式的格式从一个包移到了语言本身,这打开了一个蠕虫罐。

好点子。 一个更简单的例子:

a, b, err := myFunc()
check(err, "calling myFunc")

@buchanae我们考虑过使显式错误处理与try更直接相关 - 请参阅详细的设计文档,特别是有关设计迭代的部分。 如果我理解正确,您对check的具体建议只会允许通过类似fmt.Errorf之类的 API(作为check的一部分)来增加错误。 一般来说,人们可能想做各种有错误的事情,而不仅仅是创建一个通过错误字符串引用原始错误的新错误。

同样,这个提议并没有试图解决所有的错误处理情况。 我怀疑在大多数情况下try对于现在看起来基本上像这样的代码是有意义的:

a, b, c, ... err := try(someFunctionCall())
if err != nil {
   return ..., err
}

有很多看起来像这样的代码。 并不是每一段看起来像这样的代码都需要更多的错误处理。 在defer不正确的地方,仍然可以使用if语句。

我不遵循这条线:

defer fmt.HandleErrorf(&err, “foobar”)

它将入站错误丢弃在地板上,这是不寻常的。 是否打算使用更像这样的东西?

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

err 的重复有点口吃。 这并不是真正直接适合该提案,只是对文档的附带评论。

我分享了@buchanae提出的两个问题,即:命名返回和上下文错误。

我发现命名返回有点麻烦。 我认为它们仅作为文档才真正有益。 更严重地依赖它们是一种担忧。 不过,很抱歉这么含糊。 我会更多地考虑这一点,并提供一些更具体的想法。

我确实认为人们会努力构建他们的代码以便可以使用try ,从而避免在错误中添加上下文,这是一个真正的担忧。 这是一个特别奇怪的时间来介绍这一点,因为我们刚刚提供了更好的方法来通过官方错误包装功能为错误添加上下文。

我确实认为所提议的try使一些代码变得更好。 这是我从当前项目的代码库中或多或少随机选择的一个函数,其中一些名称已更改。 在分配给结构字段时, try的工作方式给我留下了特别深刻的印象。 (这是假设我对提案的阅读是正确的,并且这有效?)

现有代码:

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations)
        if err != nil {
                return nil, err
        }
        t := &Thing{
                thingy: thingy,
        }
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil {
                return nil, err
        }
        t.initOtherThing()
        return t, nil
}

使用try

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

不损失可读性,除了newScanner可能会失败的情况可能不太明显。 但是在一个有try的世界里,Go 程序员会对它的存在更加敏感。

@josharian关于main返回error :在我看来,您的小助手功能就是获得相同效果所需的全部。 我不确定更改main的签名是否合理。

关于“foobar”的例子:这只是一个坏例子。 我可能应该改变它。 谢谢你提出来。

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

实际上,这是不对的,因为err将被过早地评估。 有几种方法可以解决这个问题,但没有一个像原始的(我认为有缺陷的)HandleErrorf 那样干净。 我认为最好有一个或两个辅助函数的更现实的工作示例。

编辑:这个早期评估错误出现在一个例子中
在文档末尾附近:

defer fmt.HandleErrorf(&err, "copy %s %s: %v", src, dst, err)

@adg是的, try可以在您的示例中使用它。 我让你的评论重新:命名的回报保持原样。

人们可能想用错误来做各种各样的事情,而不仅仅是创建一个通过错误字符串引用原始错误的新错误。

try不会尝试处理人们想要处理错误的所有类型的事情,只会处理那些我们可以找到一种实用的方法来显着简化的事情。 我相信我的check示例也是如此。

根据我的经验,最常见的错误处理代码形式是本质上添加堆栈跟踪的代码,有时还会添加上下文。 我发现堆栈跟踪对于调试非常重要,我通过代码跟踪错误消息。

但是,也许其他提案会为所有错误添加堆栈跟踪? 我迷路了。

@adg给出的示例中,有两个潜在的失败,但没有上下文。 如果newScannerRunMigrations本身不提供提示您哪个出错的消息,那么您只能猜测。

@adg给出的示例中,有两个潜在的失败,但没有上下文。 如果 newScanner 和 RunMigrations 本身不提供提示您出错的消息,那么您只能猜测。

没错,这就是我们在这段特定代码中所做的设计选择。 我们确实在代码的其他部分包含了很多错误。

我与@deanveloper和其他人一样担心它可能会使调试变得更加困难。 确实可以选择不使用,但是第三方依赖的样式不是我们能控制的。
如果重复较少的if err := ... { return err }是主要观点,我想知道“有条件的回报”是否就足够了,就像建议的https://github.com/golang/go/issues/27794一样。

        return nil, err if f, err := os.Open(...)
        return nil, err if _, err := os.Write(...)

我认为?会比try更合适,并且总是不得不追逐defer的错误也很棘手。

这也为永远使用try/catch的异常关闭了大门。

这也为永远使用 try/catch 的异常关闭了大门。

我_more_对此感到满意。

我同意上面提出的一些关于为错误添加上下文的担忧。 我正在慢慢尝试从仅返回错误转变为始终用上下文装饰它然后返回它。 有了这个提议,我将不得不完全改变我的函数以使用命名返回参数(我觉得这很奇怪,因为我几乎不使用裸返回)。

正如@griesemer所说:

同样,这个提议并没有试图解决所有的错误处理情况。 我怀疑在大多数情况下 try 对于现在看起来基本上像这样的代码是有意义的:
a, b, c, ... 错误 := try(someFunctionCall())
如果错误!= nil {
返回...,错误
}
有很多看起来像这样的代码。 并不是每一段看起来像这样的代码都需要更多的错误处理。 在 defer 不正确的地方,仍然可以使用 if 语句。

是的,但不应该是好的,惯用的代码总是包装/装饰他们的错误吗? 我相信这就是我们引入改进的错误处理机制以在 stdlib 中添加上下文/包装错误的原因。 如我所见,这个提议似乎只考虑了最基本的用例。

此外,该提案仅解决了在一个_single place_处包装/装饰多个可能的错误返回站点的情况,使用带有延迟调用的命名参数。

但是对于需要在单个函数中为不同的错误添加不同的上下文的情况,它没有任何作用。 例如,装饰数据库错误以获取有关它们来自何处的更多信息非常重要(假设没有堆栈跟踪)

这是我拥有的真实代码的示例-

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    if err != nil {
        return err
    }
    var res int64
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table: %w", err)
    }

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table2: %w", err)
    }
    return tx.Commit()
}

根据提案:

如果需要错误增加或包装,有两种方法:坚持使用久经考验的 if 语句,或者,使用 defer 语句“声明”错误处理程序:

我认为这将属于“坚持久经考验的 if 语句”的范畴。 我希望该提案也可以改进以解决此问题。

我强烈建议 Go 团队优先考虑泛型,因为这是 Go 听到最多批评的地方,并等待错误处理。 今天的技术并不那么痛苦(尽管go fmt应该让它坐在一条线上)。

try()概念具有来自检查/句柄的check的所有问题:

  1. 它不像 Go 那样读。 人们想要赋值语法,而不需要随后的 nil 测试,因为它看起来像 Go。 对检查/处理的13 个单独的答复表明了这一点; 在此处查看_Recurring Themes_:
    https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

    f, #      := os.Open(...) // return on error
    f, #panic := os.Open(...) // panic on error
    f, #hname := os.Open(...) // invoke named handler on error
    // # is any available symbol or unambiguous pair
    
  2. 嵌套返回错误的函数调用会混淆操作顺序,并妨碍调试。 发生错误时的状态以及调用顺序应该很清楚,但这里不是:
    try(step4(try(step1()), try(step3(try(step2())))))
    现在回想一下语言禁止:
    f(t ? a : b)f(a++)

  3. 在没有上下文的情况下返回错误是微不足道的。 检查/处理的一个关键原理是鼓励情境化。

  4. 它与类型error和最后一个返回值相关联。 如果我们需要检查其他返回值/类型的异常状态,我们回到: if errno := f(); errno != 0 { ... }

  5. 它不提供多种途径。 调用存储或网络 API 的代码处理此类错误的方式不同于处理错误输入或意外内部状态的错误。 我的代码比return err更频繁地执行其中之一:

    • log.Fatal()
    • panic() 用于永远不会出现的错误
    • 记录消息并重试

@gopherbot添加 Go2,LanguageChange

如何仅使用?来解开结果,就像rust

我们对调用 try() 持怀疑态度的原因可能是两个隐式绑定。 我们看不到 try() 的返回值错误和参数的绑定。 对于 try(),我们可以制定一个规则,我们必须使用带有返回值错误的参数函数的 try()。 但是绑定到返回值不是。 所以我认为用户需要更多的表达来理解这段代码在做什么。

func doSomething() (int, %error) {
  f := try(foo())
  ...
}
  • 如果 doSomething 在返回值中没有%error ,我们就不能使用 try()。
  • 如果 foo() 在最后一个返回值中没有错误,我们不能使用 try()。

很难向现有语法添加新的要求/功能。

老实说,我认为 foo() 也应该有 %error。

再添加 1 条规则

  • %error 在函数的返回值列表中只能是一个。

在详细的设计文档中,我注意到在早期的迭代中,建议将错误处理程序传递给 try 内置函数。 像这样:

handler := func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
}

f := try(os.Open(filename), handler)  

甚至更好,像这样:

f := try(os.Open(filename), func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
})  

虽然,正如文档所述,这引发了几个问题,但我认为,如果它保留了这种可能性,可以选择性地指定这样的错误处理函数或闭包,我认为这个提议会更加可取和有用。

其次,我不介意内置函数会导致函数返回,但是,对于自行车车棚来说,“try”这个名字太短了,不能暗示它会导致返回。 所以一个更长的名字,比如attempt对我来说似乎更好。

编辑:第三,理想情况下,go 语言应该首先获得泛型,其中一个重要的用例是将这个 try 函数实现为泛型的能力,因此自行车脱落可以结束,每个人都可以获得他们自己喜欢的错误处理。

黑客新闻有一定的意义: try的行为不像普通函数(它可以返回),所以给它类似函数的语法是不好的。 returndefer语法会更合适:

func CopyFile(src, dst string) (err error) {
        r := try os.Open(src)
        defer r.Close()

        w := try os.Create(dst)
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try io.Copy(w, r)
        try w.Close()
        return nil
}

@sheerun 对此的常见反驳是panic也是一个控制流更改内置函数。 我个人不同意,但它是正确的。

  1. 上面@deanveloper以及其他人的类似评论相呼应,我担心我们低估了添加一个新的、有点微妙的成本,而且——尤其是在其他函数调用中内联时——管理调用堆栈控制的容易被忽视的关键字流动。 panic(...)是一个相对明确的例外(双关语不是有意的), return是函数的唯一出路。 我认为我们不应该以它的存在为理由来添加第三个。
  2. 该提案将返回一个未包装的错误规范为默认行为,并将包装错误归为您必须选择加入的东西,并带有额外的仪式。 但是,根据我的经验,这恰恰是对良好实践的倒退。 我希望这个领域的提案能够更容易,或者至少不会更难,在错误站点向错误添加上下文信息。

也许我们可以添加一个带有可选扩充功能的变体,例如tryf ,具有以下语义:

func tryf(t1 T1, t1 T2, … tn Tn, te error, fn func(error) error) (T1, T2, … Tn)

翻译这个

x1, x2, … xn = tryf(f(), func(err error) { return fmt.Errorf("foobar: %q", err) })

进入这个

t1, … tn, te := f()
if te != nil {
    if fn != nil {
        te = fn(te)
    }
    err = te
    return
}

由于这是一个明确的选择(而不是使用try ),我们可以在此设计的早期版本中找到合理的答案。 例如,如果扩充函数为 nil,则不执行任何操作,只返回原始错误。

我担心try会取代传统的错误处理,结果会使注释错误路径变得更加困难。

通过记录消息和更新遥测计数器来处理错误的代码将被期望try一切的 linter 和开发人员视为有缺陷或不正确。

a, b, err := doWork()
if err != nil {
  updateCounters()
  writeLogs()
  return err
}

Go 是一种非常社交的语言,具有由工具(fmt、lint 等)强制执行的常见习语。 请记住这个想法的社会影响 - 会有一种想要在任何地方使用它的趋势。

@politician ,抱歉,您要查找的词不是 _social_ 而是 _opinionated_。 Go 是一种自以为是的编程语言。 其余的我大多同意你的观点。

@beoran社区工具(如 Godep 和各种 linter)表明 Go 既固执己见又具有社交性,许多使用该语言的戏剧都源于这种组合。 希望我们都能同意try不应该成为下一部戏剧。

@politician感谢您的澄清,我没有那样理解。 我当然同意我们应该尽量避免戏剧化。

我对此感到困惑。

来自博客:错误是价值,从我的角度来看,它被设计为被重视而不是被忽视。

而且我确实相信 Rop Pike 所说的,“值可以被编程,因为错误就是值,所以错误可以被编程。”。

我们不应该将error视为exception ,这就像导入复杂性不仅是为了思考,也是为了编码。

“使用语言来简化你的错误处理。” ——罗布·派克

还有更多,我们可以查看这张幻灯片

image

我发现通过if进行错误检查特别尴尬的一种情况是关闭文件时(例如在 NFS 上)。 我想,如果.Close()可能返回错误,目前我们打算编写以下内容?

r, err := os.Open(src)
if err != nil {
    return err
}
defer func() {
    // maybe check whether a previous error occured?
    return r.Close()
}()

defer try(r.Close())能否成为一种处理此类错误的可管理语法的好方法? 至少,以某种方式调整提案中的CopyFile()示例是有意义的,而不是忽略来自r.Close()w.Close()的错误。

@seehuhn您的示例无法编译,因为延迟函数没有返回类型。

func doWork() (err error) {
  r, err := os.Open(src)
  if err != nil {
    return err
  }
  defer func() {
    err = r.Close()  // overwrite the return value
  }()
}

会像你期望的那样工作。 键是命名的返回值。

我喜欢这个提议,但我认为@seehuhn的例子也应该得到解决:

defer try(w.Close())

只有在尚未设置错误时才会从 Close() 返回错误。
这种模式经常被使用...

我同意有关为错误添加上下文的担忧。 我认为它是保持错误消息非常友好(和清晰)并使调试过程更容易的最佳实践之一。

我想到的第一件事是用tryf函数替换fmt.HandleErrorf $ 函数,该函数在错误前面加上额外的上下文。

func tryf(t1 T1, t1 T2, … tn Tn, te error, ts string) (T1, T2, … Tn)

例如(来自我拥有的真实代码):

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil {
        return nil, errors.WithMessage(err, "load config dir")
    }
    b := bytes.NewBuffer(nil)
    if err = templates.ExecuteTemplate(b, "main", c); err != nil {
        return nil, errors.WithMessage(err, "execute main template")
    }
    buf, err := format.Source(b.Bytes())
    if err != nil {
        return nil, errors.WithMessage(err, "format main template")
    }
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    if err := ioutil.WriteFile(target, buf, 0644); err != nil {
        return nil, errors.WithMessagef(err, "write file %s", target)
    }
    // ...
}

可以改为:

func (c *Config) Build() error {
    pkgPath := tryf(c.load(), "load config dir")
    b := bytes.NewBuffer(nil)
    tryf(emplates.ExecuteTemplate(b, "main", c), "execute main template")
    buf := tryf(format.Source(b.Bytes()), "format main template")
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    tryf(ioutil.WriteFile(target, buf, 0644), fmt.Sprintf("write file %s", target))
    // ...
}

或者,如果我以@agnivade为例:

func (p *pgStore) DoWork() (err error) {
    tx := tryf(p.handle.Begin(), "begin transaction")
        defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()
    var res int64
    tryf(tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res), "insert table")
    _, = tryf(tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res), "insert table2")
    return tryf(tx.Commit(), "commit transaction")
}

然而, @josharian提出了一个很好的观点,让我对这个解决方案犹豫不决:

但是,正如所写的那样,它将 fmt 样式的格式从一个包移到了语言本身,这打开了一个蠕虫罐。

我完全同意这个提议,并且可以从许多例子中看到它的好处。

我对该提案的唯一担忧是try的命名,我觉得它与其他语言的含义可能会扭曲开发者对其来自其他语言的目的的看法。 Java 来这里找。

对我来说,我更喜欢将内置函数称为pass 。 我觉得这可以更好地代表正在发生的事情。 毕竟,您没有处理错误 - 而是将其传回以由调用者处理。 try给人的印象是错误已被处理。

这对我来说是一个大拇指,主要是因为它旨在解决的问题(“典型的与错误处理相关的样板 if 语句”)对我来说根本不是问题。 如果所有错误检查都只是if err != nil { return err }那么我可以看到为此添加语法糖的一些价值(尽管 Go 是一种相对无糖的语言)。

事实上,在发生非零错误时我想要做的事情因一种情况而异。 也许我想t.Fatal(err) 。 也许我想添加一条装饰信息return fmt.Sprintf("oh no: %v", err) 。 也许我只是记录错误并继续。 也许我在我的 SafeWriter 对象上设置了一个错误标志并继续,在某些操作序列结束时检查该标志。 也许我需要采取一些其他措施。 这些都不能用try自动化。 因此,如果try的论点是它将消除所有if err != nil块,则该论点不成立。

它会消除其中的_一些_吗? 当然。 这对我来说是一个有吸引力的提议吗? 嗯。 我真的不担心。 对我来说, if err != nil只是 Go 的一部分,就像花括号或defer一样。 我知道对于刚接触 Go 的人来说,这看起来很冗长和重复,但由于种种原因,刚接触 Go 的人并不适合对语言进行重大更改。

传统上,对 Go 进行重大更改的标准是,提议的更改必须解决一个 (A) 重要的问题,(B) 影响很多人,并且 (C) 提议很好地解决了问题。 我不相信这三个标准中的任何一个。 我对 Go 的错误处理非常满意。

@peterbourgon@deanveloper 相呼应,我最喜欢Go 的一点是代码流清晰,并且panic()不像Python 中那样被视为标准流控制机制。

关于 panic 的争论,panic() 几乎总是单独出现在一条线上,因为它没有任何价值。 你不能fmt.Println(panic("oops")) 。 这极大地提高了它的知名度,使其与try()的可比性远低于人们所说的。

如果要为函数提供另一个流控制结构,我_far_ 更喜欢它是一个保证是一行中最左边的项目的语句。

提案中的一个例子为我指出了这个问题:

func printSum(a, b string) error {
        fmt.Println(
                "result:",
                try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
        )
        return nil
}

控制流确实变得不那么明显并且非常模糊。

这也违背了 Rob Pike 的初衷,即所有错误都需要明确处理。

虽然对此的反应可能是“然后不要使用它”,但问题是——其他库将使用它,并且调试它们、阅读它们并使用它们变得更加成问题。 这将激励我的公司永远不要采用 go 2,并开始只使用不使用try的库。 如果我并不孤单,它可能会导致 a-la python 2/3 的分裂。

此外, try的命名将自动暗示最终catch将出现在语法中,我们将回到 Java。

因此,由于这一切,我_强烈_反对这个提议。

我不喜欢try这个名字。 它意味着_尝试_做一些失败风险很高的事情(我可能对 _try_ 有文化偏见,因为我不是以英语为母语的人),而try将用于我们预计罕见失败的情况(希望减少错误处理的冗长的动机)并且是乐观的。 此外,此提案中的try实际上_捕获_了一个错误以提前返回它。 我喜欢@HiImJC 的pass建议。

除了名称之外,我发现现在隐藏在表达式中间的类似return的语句很尴尬。 这打破了 Go flow 风格。 这将使代码审查变得更加困难。

总的来说,我发现这个提议只会让懒惰的程序员受益,他们现在拥有更短代码的武器,甚至更少的理由去包装错误。 由于它也会使审查变得更加困难(在表达式中间返回),我认为这个提议违背了 Go 的“大规模编程”目标。

我在描述 Go 语言时通常会说的关于 Go 的最喜欢的事情之一是,对于大多数事情来说,只有一种方法可以做事。 这个提议通过提供多种方法来做同样的事情,有点违背这个原则。 我个人认为这不是必需的,它会带走,而不是增加语言的简单性和可读性。

我总体上喜欢这个提议。 与defer的交互似乎足以提供一种符合人体工程学的返回错误的方式,同时还添加了额外的上下文。 尽管解决@josharian指出的关于如何在包装的错误消息中包含原始错误的问题会很好。

缺少的是与桌面上的错误检查建议交互的符合人体工程学的方式。 我认为 API 应该非常慎重地考虑它们返回的错误类型,默认值可能应该是“返回的错误无法以任何方式检查”。 然后应该很容易进入可以以精确方式检查错误的状态,如函数签名所记录的那样(“它在情况 A 中报告类型 X 的错误,在情况 B 中报告类型 Y 的错误”)。

不幸的是,到目前为止,这个提议使最符合人体工程学的选择成为最不受欢迎的(对我来说); 盲目地通过任意错误类型。 我认为这是不可取的,因为它鼓励不要考虑您返回的错误类型以及您的 API 用户将如何使用它们。 这个提议增加的便利性当然很好,但我担心它会鼓励不良行为,因为感知到的便利性将超过仔细考虑您提供(或泄漏)哪些错误信息的感知价值。

如果try返回的错误被转换为非“不可打包”的错误,那么创可贴就是。 不幸的是,这也有相当严重的缺点,因为它使得任何defer都无法检查错误本身。 此外,它还可以防止try实际上会返回所需类型的错误(即小心使用try而不是粗心使用的用例)。

另一种解决方案是重新利用(废弃的)想法,即为try提供可选的第二个参数,用于定义/白名单可能从该站点返回的错误种类。 这有点麻烦,因为我们有两种不同的方式来定义“错误类型”,或者按值( io.EOF等)或按类型( *os.PathError*exec.ExitError )。 指定作为函数参数的错误类型很容易,但指定类型更难。 不知道如何处理,但把这个想法扔在那里。

@josharian指出的问题可以通过延迟对 err 的评估来避免:

defer func() { fmt.HandleErrorf(&err, "oops: %v", err) }()

看起来不太好,但应该可以。 但是,如果可以通过为错误指针或一般的指针添加新的格式化动词/标志来解决此问题,我更愿意将其打印为普通%v的取消引用值。 出于示例的目的,我们称它为%*v

defer fmt.HandleErrorf(&err, "oops: %*v", &err)

撇开问题不谈,我认为这个提议看起来很有希望,但保持为错误添加上下文的人体工程学似乎至关重要。

编辑:

另一种方法是将错误指针包装在实现Stringer的结构中:

type wraperr struct{ err *error }
func (w wraperr) String() string { return (*w.err).Error() }

...

defer handleErrorf(&err, "oops: %v", wraperr{&err})

从我的角度来看有几件事。 为什么我们如此关心节省几行代码? 我认为这与Small functions 被认为是有害的相同。

此外,我发现这样的提议会将正确处理错误的责任转移到一些“魔法”上,我担心这些“魔法”会被滥用,并鼓励懒惰,从而导致代码质量差和错误。

如上所述的提案也有许多不明确的行为,所以这已经比_explicit_额外的~3行更清楚了。

我们目前在内部很少使用延迟模式。 当我们写这篇文章时,这里有一篇文章也有类似的混合接收 - https://bet365techblog.com/better-error-handling-in-go

然而,我们对它的使用是为了期待check / handle提案的进展。

Check/handle 是一种更全面的方法,可以使错误处理更加简洁。 它的handle块保留了与定义它的函数范围相同的函数范围,而任何defer语句都是新的上下文,无论多少开销。 这似乎更符合 go 的习惯用法,因为如果您想要“仅在错误发生时返回错误”的行为,您可以将其明确声明为handle { return err }

Defer 显然也依赖于维护的 err 引用,但是我们已经看到使用块范围的 var 隐藏错误引用会出现问题。 因此,它不足以被认为是处理 go 错误的标准方法。

在这种情况下, try似乎并没有解决太多问题,我和其他人一样担心它只会导致惰性实现,或者过度使用延迟模式的实现。

如果基于延迟的错误处理将成为一件事,那么可能应该将类似这样的内容添加到错误包中:

        f := try(os.Create(filename))
        defer errors.Deferred(&err, f.Close)

忽略延迟关闭语句的错误是一个很常见的问题。 应该有一个标准工具来帮助它。

返回的内置函数比具有相同功能的关键字更难卖。
如果它是 Zig[1] 中的关键字,我会更喜欢它。

  1. https://ziglang.org/documentation/master/#try

内置函数,其类型签名无法使用语言的类型系统来表达,其行为混淆了函数的正常含义,就像一个可以重复使用以避免实际语言演变的逃生舱。

我们习惯于立即识别返回语句(和恐慌),因为这就是这种控制流在 Go(和许多其他语言)中的表达方式。 在一些习惯之后,我们也将尝试识别为更改控制流,这似乎并不牵强,就像我们对返回所做的那样。 我毫不怀疑良好的 IDE 支持也会对此有所帮助。

我认为这是相当牵强的。 在 gofmt'ed 代码中,return 总是匹配/^\t*return / - 这是一个非常简单的模式,无需任何帮助即可通过肉眼发现。 另一方面, try可以出现在代码中的任何位置,可以任意嵌套在函数调用的深处。 再多的培训也不会让我们能够在没有工具帮助的情况下立即发现函数中的所有控制流。

此外,依赖于“良好的 IDE 支持”的功能在所有没有良好 IDE 支持的环境中都会处于劣势。 代码审查工具立即浮现在脑海中——Gerrit 会为我突出显示所有的尝试吗? 出于各种原因选择不使用 IDE 或花哨的代码突出显示的人呢? acme 会开始突出显示try吗?

语言特性本身应该很容易理解,而不是依赖于编辑器的支持。

@kungfusheep我喜欢那篇文章。 在没有try的情况下,单独处理 defer 包装已经大大提高了可读性。

我在阵营中并不认为 Go 中的错误真的是一个问题。 即便如此, if err != nil { return err }在某些功能上还是很卡顿的。 我编写了几乎在每条语句之后都需要进行错误检查的函数,除了换行和返回之外,没有其他函数需要任何特殊处理。 有时只是没有任何聪明的 Buffer 结构可以让事情变得更好。 有时这只是一个又一个不同的关键步骤,如果出现问题,您只需简单地短路即可。

尽管try在完全向后兼容的同时肯定会使代码更容易阅读,但我同意try不是关键的必备功能,所以如果人们太害怕也许最好不要拥有它。

不过,语义非常明确。 每当您看到try时,它要么遵循快乐的道路,要么返回。 我真的不能比这更简单了。

这看起来像一个特殊的大小写宏。

@dominikh try总是匹配/try\(/所以我不知道你的意思是什么。 它同样可搜索,而且我听说过的每个编辑器都有搜索功能。

@qrpnxz我认为他试图说明的重点不是你不能以编程方式搜索它,而是用你的眼睛更难搜索。 正则表达式只是一个类比,重点放在/^\t*上,表示所有返回值都位于行首(忽略前导空格)。

想多了,应该有几个常用的辅助函数。 也许它们应该放在一个名为“deferred”的包中。

使用格式解决check的建议以避免命名返回,您可以使用检查 nil 的函数来执行此操作,如下所示

func Format(err error, message string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf(...)
}

这可以在没有命名返回的情况下使用,如下所示:

func foo(s string) (int, error) {
    n, err := strconv.Atoi(s)
    try(deferred.Format(err, "bad string %q", s))
    return n, nil
}

建议的 fmt.HandleError 可以放入 deferred 包中,而我的errors.Defer 辅助函数可以称为deferred.Exec并且只有在错误为非 nil 时才能执行程序的条件 exec。

把它放在一起,你会得到类似的东西

func CopyFile(src, dst string) (err error) {
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer deferred.Exec(&err, r.Close)

    w := try(os.Create(dst))
    defer deferred.Exec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    try(io.Copy(w, r))

    return nil
}

另一个例子:

func (p *pgStore) DoWork() (err error) {
    tx := try(p.handle.Begin())

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(deferred.Format(err, "insert table")

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(deferred.Format(err, "insert table2"))

    return tx.Commit()
}

这个提议使我们从到处都有if err != nil ,到到处都有try 。 它改变了提出的问题并且没有解决它。

虽然,我认为当前的错误处理机制从一开始就不是问题。 我们只需要改进工具并围绕它进行审查

此外,我认为if err != nil实际上比try更具可读性,因为它不会弄乱业务逻辑语言的行,而是位于它的正下方:

file := try(os.OpenFile("thing")) // less readable than, 

file, err := os.OpenFile("thing")
if err != nil {

}

如果 Go 要在错误处理方面更加神奇,为什么不完全拥有它。 例如,如果用户没有分配错误,Go 可以隐式调用内置try 。 例如:

func getString() (string, error) { ... }

func caller() {
  defer func() {
    if err != nil { ... } // whether `err` must be defined or not is not shown in this example. 
  }

  // would call try internally, because a user is not 
  // assigning an error value. Also, it can add a compile error
  // for "defined and not used err value" if the user does not 
  // handle the error. 
  str := getString()
}

对我来说,这实际上会以魔法和潜在的可读性为代价来解决冗余问题。

因此,我建议我们要么真正解决上面示例中的“问题”,要么保留当前的错误处理,而不是改变语言来解决冗余和包装,我们不改变语言,但我们改进工具审查代码以使体验更好。

例如,在 VSCode 中有一个名为iferr的代码段,如果您键入它并按 Enter 键,它会扩展为完整的错误处理语句……因此,写它对我来说永远不会感到厌烦,以后再读会更好.

@josharian

虽然这不是“一个适度的库更改”,但我们也可以考虑接受 func main() 错误。

问题在于,并非所有平台都对这意味着什么有明确的语义。 您的重写在运行在完整操作系统上的“传统”Go 程序中运行良好 - 但是一旦您编写微控制器固件甚至只是 WebAssembly,就不清楚os.Exit(1)的含义。 目前, os.Exit是一个库调用,所以 Go 实现是免费的,只是不提供它。 main的形状是一个语言问题。


关于提案的一个问题可能最好由“nope”回答: try如何与可变参数交互? 这是最后一个参数中没有它的 variadic-nes 的可变参数 (ish) 函数的第一种情况。 这是否允许:

var e []error
try(e...)

撇开你为什么会这样做。 我怀疑答案是“否”(否则后续是“如果扩展切片的长度为 0 会怎样)。只是提出这个问题,以便在最终制定规范时牢记这一点。

  • Go 中的几个最伟大的特性是当前的内置确保清晰的控制流,明确和鼓励错误处理,并且强烈劝阻开发人员不要编写“神奇”的代码。 try提议与这些基本原则不一致,因为它会以牺牲控制流的可读性为代价来促进速记。
  • 如果这个提议被采纳,那么也许考虑使try内置语句而不是函数。 然后它与if等其他控制流语句更一致。 此外,删除嵌套括号略微提高了可读性。
  • 同样,如果该提案被采纳,那么可能不使用defer或类似的方式来实施它。 它已经无法在纯 go 中实现(正如其他人所指出的那样),因此它也可以在后台使用更有效的实现。

我看到了两个问题:

  1. 它将大量代码嵌套在函数中。 这增加了很多额外的认知负担,试图在你的脑海中解析代码。
  1. 它为我们提供了代码可以从语句中间退出的地方。

2号我认为更糟糕。 这里的所有示例都是返回错误的简单调用,但更阴险的是:

func doit(abc string) error {
    a := fmt.Sprintf("value of something: %s\n", try(getValue(abc)))
    log.Println(a)
    return nil
}

这段代码可以在 sprintf 的中间退出,而且很容易错过这个事实。

我的投票是否定的。 这不会让 Go 代码变得更好。 它不会使它更容易阅读。 它不会使它更健壮。

我之前说过,这个提议就是例证——我觉得 90% 的关于 Go 的抱怨都是“我不想写 if 语句或循环”。 这删除了一些非常简单的 if 语句,但增加了认知负担,并且很容易错过函数的退出点。

我只想指出,你不能在 main 中使用它,它可能会让新用户或教学时感到困惑。 显然,这适用于任何不返回错误的函数,但我认为 main 是特殊的,因为它出现在许多示例中。

func main() {
    f := try(os.Open("foo.txt"))
    defer f.Close()
}

我不确定在 main 中尝试恐慌是否可以接受。

此外,它在测试( func TestFoo(t* testing.T) )中并不是特别有用,这很不幸:(

我遇到的问题是它假设你总是想在错误发生时返回它。 当您可能想为错误添加上下文并将其返回时,或者您可能只想在发生错误时采取不同的行为。 也许这取决于返回的错误类型。

我更喜欢类似于 try/catch 的东西,它可能看起来像

假设foo()定义为

func foo() (int, error) {}

然后你可以做

n := try(foo()) {
    case FirstError:
        // do something based on FirstError
    case OtherError:
        // do something based on OtherError
    default:
        // default behavior for any other error
}

翻译成

n, err := foo()
if errors.Is(err, FirstError) {
    // do something based on FirstError
if errors.Is(err, OtherError) {
    // do something based on OtherError
} else {
    // default behavior for any other error
}

对我来说,错误处理是代码库中最重要的部分之一。
已经有太多的 go 代码是if err != nil { return err } ,在不添加额外上下文的情况下从堆栈深处返回错误,或者甚至(可能)更糟糕地通过使用fmt.Errorf包装来掩盖底层错误来添加上下文。

提供一个新的关键字,它是一种魔法,除了替换if err != nil { return err }之外什么都不做,这似乎是一条危险的道路。
现在所有代码都将被包装在一个调用中以进行尝试。 对于仅处理包内错误的代码,这有点好(尽管可读性很差),例如:

func foo() error {
  /// stuff
  try(bar())
  // more stuff
}

但我认为给定的示例确实有点可怕,基本上让调用者试图理解堆栈中非常深的错误,就像异常处理一样。
当然,这完全取决于开发人员在这里做正确的事情,但它为开发人员提供了一种很好的方式来不关心他们的错误,也许是“我们稍后会解决这个问题”(我们都知道这是怎么回事)。

我希望我们从不同的角度来看待这个问题,而不是*“我们如何减少重复”以及“我们如何使(正确的)错误处理更简单,开发人员更有效率”。
我们应该考虑这将如何影响运行的生产代码。

*注意:这实际上并没有减少重复,只是改变了重复的内容,同时降低了代码的可读性,因为所有内容都包含在try()中。

最后一点:起初阅读提案看起来不错,然后你开始了解所有的陷阱(至少列出的那些),就像“好吧,这太多了”。


我意识到这在很大程度上是主观的,但这是我关心的事情。 这些语义非常重要。
我想看到的是一种使编写和维护生产级代码更简单的方法,这样即使对于 POC/演示级代码,您也可以“正确”地出错。

由于错误上下文似乎是一个反复出现的主题......

假设:大多数 Go 函数返回(T, error)而不是(T1, T2, T3, error)

如果不是将try定义为try(T1, T2, T3, error) (T1, T2, T3)而是将其定义为
try(func (args) (T1, T2, T3, error))(T1, T2, T3) ? (这是一个近似值)

也就是说, try调用的句法结构始终是第一个参数,它是一个返回多个值的表达式,最后一个是错误。

然后,很像make ,这为调用的 2 参数形式打开了大门,其中第二个参数是 try 的上下文(例如,固定字符串,带有%v的字符串

这仍然允许链接(T, error)案例,但您不能再链接通常不需要 IMO 的多个退货。

@cpuguy83如果您阅读该提案,您会发现没有什么可以阻止您包装错误。 事实上,在使用try的同时有多种方法可以做到这一点。 不过,许多人似乎出于某种原因认为是这样的。

if err != nil { return err }try一样“我们稍后会解决”,只是在原型设计时更烦人。

我不知道一对括号内的东西比每四行样板文件中的函数步骤的可读性差。

如果您指出其中一些困扰您的特定“陷阱”,那就太好了,因为这就是主题。

可读性似乎是一个问题,但是 go fmt 呈现 try() 以使其脱颖而出,例如:

f := try(
    os.Open("file.txt")
)

@MrTravisB

我遇到的问题是它假设你总是想在错误发生时返回它。

我不同意。 它假设您想要这样做的频率足以保证为此使用速记。 如果你不这样做,它就不会妨碍简单地处理错误。

当您可能想为错误添加上下文并将其返回时,或者您可能只想在发生错误时采取不同的行为。

该提案描述了一种向错误添加块范围上下文的模式。 @josharian指出示例中存在错误,但目前尚不清楚避免它的最佳方法是什么。 我已经写了几个处理它的方法的例子。

同样,对于更具体的错误上下文, try会做一件事,如果您不想要那件事,请不要使用try

@boomlinde正是我的观点。 这个提议试图解决一个单一的用例,而不是提供一个工具来解决更大的错误处理问题。 我认为基本问题是否正是您所指出的。

它假设您想要这样做的频率足以保证为此使用速记。

根据我的观点和经验,这个用例是少数,不保证速记语法。

此外,使用defer处理错误的方法存在问题,因为它假设您希望以相同的方式处理所有可能的错误。 defer语句无法取消。

defer fmt.HandleErrorf(&err, “foobar”)

n := try(foo())

x : try(foo2())

如果我想要对可能从foo()foo2()返回的错误进行不同的错误处理怎么办?

@MrTravisB

如果我想要对可能从 foo() 和 foo2() 返回的错误进行不同的错误处理怎么办?

然后你用别的东西。 这就是@boomlinde的目的。

也许您个人并不经常看到这个用例,但很多人会看到,并且添加try并不会真正影响您。 事实上,用例对您来说越少,添加try对您的影响就越小。

@qrpnxz

f := try(os.Open("/foo"))
data := try(ioutil.ReadAll(f))
try(send(data))

(是的,我知道有ReadFile并且这个特定的示例不是在某处复制数据的最佳方式,而不是重点)

这需要花费更多的精力来阅读,因为您必须解析出 try 的内联。 应用程序逻辑包含在另一个调用中。
我还争辩说,这里的defer错误处理程序并不好,只是用一条新消息包装错误......这很好,但处理错误比让处理更容易人类阅读发生了什么。

在 rust 中,至少操作符是一个后缀( ?添加到调用末尾),它不会增加挖掘实际逻辑的额外负担。

基于表达式的流控制

panic可能是另一个流控制函数,但它不返回值,因此它实际上是一个语句。 将此与try进行比较,后者是一个表达式,可以出现在任何地方。

recover确实有一个值并影响流控制,但必须出现在defer语句中。 这些defer通常是函数文字, recover只被调用一次,因此recover也有效地作为语句出现。 同样,将此与可以在任何地方发生的try进行比较。

我认为这些观点意味着try使得以我们以前没有过的方式遵循控制流变得更加困难,正如之前所指出的,但我没有看到语句和表达式之间的区别指出。


另一个提议

允许这样的语句

if err != nil {
    return nil, 0, err
}

当块仅包含return语句并且该语句不包含换行符时,将在一行上格式化gofmt 。 例如:

if err != nil { return nil, 0, err }

基本原理

  • 它不需要更改语言
  • 格式化规则简单明了
  • 该规则可以设计为选择在gofmt保留换行符的位置(如果它们已经存在)(如结构文字)。 选择加入还允许作者强调一些错误处理
  • 如果不选择加入,代码可以通过调用gofmt自动移植到新样式
  • 它仅适用于return语句,因此不会被不必要地滥用到高尔夫球代码中
  • 与描述为什么会发生一些错误以及为什么它们被返回的评论很好地交互。 使用许多嵌套的try表达式处理得不好
  • 它将错误处理的垂直空间减少了 66%
  • 没有基于表达式的控制流
  • 阅读代码的频率远高于编写代码的频率,因此应该针对读者进行优化。 占用较少空间的重复代码对读者有帮助,其中try更倾向于作者
  • 人们已经提议在多条线路上存在try 。 例如这条评论这条评论引入了类似的风格
f, err := os.Open(file)
try(maybeWrap(err))
  • “try on its own line”样式消除了关于返回的err值的任何歧义。 因此,我怀疑这种形式会被普遍使用。 允许一个内衬 if 块几乎是一回事,除了它也明确了返回值是什么
  • 它不提倡使用命名返回或不明确的基于defer的包装。 两者都增加了包装错误的障碍,前者可能需要godoc更改
  • 无需讨论何时使用try与使用传统错误处理
  • 不排除将来做try或其他事情。 即使try被接受,变化也可能是积极的
  • testing库或main函数没有负面交互。 事实上,如果提案允许任何单行语句而不是仅仅返回,它可能会减少基于断言的库的使用。 考虑
value, err := something()
if err != nil { t.Fatal(err) }
  • 与检查特定错误没有负面互动。 考虑
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

总而言之,这个提议成本低,可以设计为可选,不排除任何进一步的变化,因为它只是风格,并且减少了阅读冗长的错误处理代码的痛苦,同时保持一切明确。 我认为在全力以赴之前,至少应该将其视为第一步try


移植的一些示例

来自https://github.com/golang/go/issues/32437#issuecomment -498941435

尝试

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

有了这个

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations))
        if err != nil { return nil, fmt.Errorf("running migrations: %v", err) }

        t := &Thing{thingy: thingy}
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil { return nil, fmt.Errorf("creating scanner: %v", err) }

        t.initOtherThing()
        return t, nil
}

它在空间使用方面具有竞争力,同时仍允许为错误添加上下文。

来自https://github.com/golang/go/issues/32437#issuecomment -499007288

尝试

func (c *Config) Build() error {
    pkgPath := try(c.load())
    b := bytes.NewBuffer(nil)
    try(emplates.ExecuteTemplate(b, "main", c))
    buf := try(format.Source(b.Bytes()))
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644))
    // ...
}

有了这个

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

原始评论使用假设的tryf来附加格式,该格式已被删除。 目前还不清楚添加所有不同上下文的最佳方法,也许try甚至都不适用。

@cpuguy83
对我来说,使用try更具可读性。 在这个例子中,我读到“打开一个文件,读取所有字节,发送数据”。 通过常规错误处理,我会阅读“打开文件,检查是否有错误,错误处理会执行此操作,然后读取所有字节,现在检查是否发生了某些事情......”我知道你可以扫描err != nil s,但对我来说try更容易,因为当我看到它时,我立即知道行为:如果 err != nil 则返回。 如果你有一个分支,我必须看看它的作用。 它可以做任何事情。

我还认为,这里的延迟错误处理程序并不好,只是用新消息包装错误

我确定您可以在延迟中做其他事情,但无论如何, try无论如何都是针对简单的一般情况的。 任何时候你想做更多的事情,总是有好的 ol' Go 错误处理。 那不会消失。

@zeebo是的,我很喜欢。
@kungfusheep的文章使用了这样的单行错误检查,我退出尝试。 然后,我一保存,gofmt 就将其扩展为三行,这很可悲。 stdlib 中的许多函数都是这样定义在一行中的,所以 gofmt 会扩展它让我感到惊讶。

@qrpnxz

我碰巧读了很多go 代码。 该语言最好的事情之一是大多数代码遵循特定风格(感谢 gofmt)带来的易用性。
不想阅读包装在try(f())中的一堆代码。
这意味着代码风格/实践会出现分歧,或者像“哦,你应该在这里使用try() ”这样的短句(我也不喜欢,这是我和其他人评论的重点关于这个提议)。

客观上它并不比if err != nil { return err }好,只是打字少。


最后一件事:

如果您阅读该提案,您会发现没有什么可以阻止您

我们可以不要使用这种语言吗? 当然,我阅读了提案。 碰巧我昨晚读了它,然后在今天早上思考后发表了评论,并没有解释我想要表达的细节。
这是一种令人难以置信的敌对语气。

@cpuguy83
我的坏cpu家伙。 我不是那个意思。

而且我想你必须指出使用try的代码看起来与不使用的代码完全不同,所以我可以想象这会影响解析该代码的体验,但我不能完全同意不同在这种情况下意味着更糟,尽管我知道你个人不喜欢它,就像我个人喜欢它一样。 Go 中的很多东西都是这样的。 至于 linter 告诉你做什么完全是另一回事,我认为。

当然,客观上并没有更好。 我表示这样对我来说更具可读性。 我小心翼翼地这么说。

再次,抱歉听起来那样。 虽然这是一个论点,但我并不是要对抗你。

https://github.com/golang/go/issues/32437#issuecomment -498908380

没有人会让你尝试使用。

忽略油嘴滑舌,我认为这是一种非常随意的方式来驳斥设计批评。

当然,不必使用它。 但是任何与我一起编写代码的人都可以使用它并强迫我尝试破译try(try(try(to()).parse().this)).easily()) 。 这就像说

没有人会让你使用空接口{}。

无论如何,Go 对简单性非常严格: gofmt使所有代码看起来都一样。 幸福的道路向左走,任何可能昂贵或令人惊讶的事情都是明确的。 所提议的try是一个180 度的转弯。 简单!=简洁。

至少try应该是带有左值的关键字。

它_objectively_并不比if err != nil { return err }好,只是打字少。

两者之间有一个客观区别: try(Foo())是一个表达式。 对于某些人来说,这种差异是不利的( try(strconv.Atoi(x))+try(strconv.Atoi(y))批评)。 对于其他人来说,出于同样的原因,这种差异是一个好处。 仍然没有客观上更好或更坏 - 但我也不认为应该将差异扫在地毯下,并声称它“只是少打字”并不能使提案公正。

@elagergren-spideroak 很难说try一口气看到很烦人,然后说接下来就不明确了。 你得选一个。

通常会看到函数参数首先放入临时变量中。 我相信它会更常见

this := try(to()).parse().this
that := try(this.easily())

比你的例子。

try什么都不做是幸福的道路,所以看起来和预期的一样。 在不愉快的道路上,它所做的只是返回。 看到有try就足以收集该信息。 从函数返回也没有什么昂贵的,所以从那个描述我不认为try正在做 180

@josharian关于您在https://github.com/golang/go/issues/32437#issuecomment -498941854 中的评论,我认为这里没有早期评估错误。

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

err的未修改值传递给HandleErrorf ,并传递指向err的指针。 我们检查err是否为nil (使用指针)。 如果不是,我们使用err的未修改值格式化字符串。 然后我们使用指针将err设置为格式化的错误值。

@Merovius该提案实际上只是一个语法糖宏,所以它最终会成为人们认为看起来更好或会造成最少麻烦的东西。 如果您不认为,请向我解释。 这就是我个人支持它的原因。 从我的角度来看,这是一个很好的补充,无需添加任何关键字。

@ianlancetaylor ,我认为@josharian是正确的: err的“未修改”值是defer被推入堆栈时的值,而不是err的(可能是预期的)值try $ 设置。

我对try的另一个问题是,它使人们更容易将更多和逻辑转储到一行中。 这是我对大多数其他语言的主要问题,它们使将 5 个表达式放在一行中变得非常容易,而且我不希望这样。

this := try(to()).parse().this
that := try(this.easily())

^^ 即使这也非常糟糕。 第一行,我必须在脑海中来回跳来做paren匹配。 即使是实际上非常简单的第二行......也很难阅读。
嵌套函数很难阅读。

parser, err := to()
if err != nil {
    return err
}
this := parser.parse().this
that, err := this.easily()
if err != nil {
    return err
}

^^ 这比 IMO 更容易和更好。 它超级简单明了。 是的,这是更多的代码行,我不在乎。 这很明显。

@bcmills @josharian啊,当然,谢谢。 所以它必须是

defer func() { fmt.HandleErrorf(&err, “foobar: %v”, err) }()

不太好。 也许fmt.HandleErrorf毕竟应该隐式地将错误值作为最后一个参数传递。

这个问题很快就得到了很多评论,在我看来,他们中的许多人似乎在重复已经发表的评论。 当然,请随意发表评论,但我想温和地建议,如果您想重申已经提出的观点,请使用 GitHub 的表情符号来做到这一点,而不是重复这一点。 谢谢。

@ianlancetaylor如果fmt.HandleErrorf发送 err 作为 format 之后的第一个参数,那么实现会更好,用户将能够始终通过%[1]v引用它。

@natefinch完全同意。

我想知道生锈风格的方法是否更可口?
请注意,这不是一个只是考虑一下的提议......

this := to()?.parse().this
that := this.easily()?

最后我认为这更好,但是(也可以使用!或其他东西......),但仍然不能很好地解决处理错误的问题。


当然 rust 也有try()几乎就像这样,但是......另一种 rust 风格。

它_objectively_并不比if err != nil { return err }好,只是打字少。

两者之间有一个客观区别: try(Foo())是一个表达式。 对于某些人来说,这种差异是不利的( try(strconv.Atoi(x))+try(strconv.Atoi(y))批评)。 对于其他人来说,出于同样的原因,这种差异是一个好处。 仍然没有客观上更好或更坏 - 但我也不认为应该将差异扫在地毯下,并声称它“只是少打字”并不能使提案公正。

这是我喜欢这种语法的最大原因之一; 它让我可以将错误返回函数用作更大表达式的一部分,而无需命名所有中间结果。 在某些情况下命名它们很容易,但在其他情况下,没有特别有意义或非冗余的名称可以给它们,在这种情况下,我宁愿根本不给它们命名。

@MrTravisB

正是我的观点。 这个提议试图解决一个单一的用例,而不是提供一个工具来解决更大的错误处理问题。 我认为基本问题是否正是您所指出的。

我具体说了什么,这正是你的意思? 在我看来,如果您认为我们同意,您从根本上误解了我的观点。

根据我的观点和经验,这个用例是少数,不保证速记语法。

在 Go 源代码中,即使无法为错误添加上下文,也有成千上万的案例可以由try开箱即用地处理。 如果轻微,它仍然是投诉的常见原因。

此外,使用 defer 处理错误的方法存在问题,因为它假定您希望以相同的方式处理所有可能的错误。 defer 语句不能被取消。

类似地,使用 + 处理算术的方法假设您不想减法,所以如果您不想减法,您就不要减法。 有趣的问题是块范围的错误上下文是否至少代表了一种常见的模式。

如果我想要对可能从 foo() 与 foo2() 返回的错误进行不同的错误处理怎么办

同样,您不要使用try 。 然后,您不会从try中获得任何收益,但您也不会失去任何东西。

@cpuguy83

我想知道生锈风格的方法是否更可口?

该提案提出了反对这一点的论据。

在这一点上,我认为try{}catch{}更具可读性:upside_down_face:

  1. 使用命名导入来绕过defer极端情况不仅对于像 godoc 这样的东西来说很糟糕,而且最重要的是它很容易出错。 我不在乎我可以用另一个func()来解决这个问题,这只是我需要记住的更多事情,我认为它会鼓励“不良做法”。
  2. 没有人会让你尝试使用。

    这并不意味着它是一个好的解决方案,我要指出当前的想法在设计中存在缺陷,我要求以一种不易出错的方式解决它。

  3. 我认为像try(try(try(to()).parse().this)).easily())这样的例子是不切实际的,这已经可以用其他函数来完成,我认为那些审查代码的人要求将其拆分是公平的。
  4. 如果我有 3 个可能出错的地方并且我想分别包装每个地方怎么办? try()让这变得非常困难,事实上try()考虑到它的难度已经在阻止包装错误,但这里有一个例子说明我的意思:

    func before() error {
      x, err := foo()
      if err != nil {
        wrap(err, "error on foo")
      }
      y, err := bar(x)
      if err != nil {
        wrapf(err, "error on bar with x=%v", x)
      }
      fmt.Println(y)
      return nil
    }
    
    func after() (err error) {
      defer fmt.HandleErrorf(&err, "something failed but I don't know where: %v", err)
      x := try(foo())
      y := try(bar(x))
      fmt.Println(y)
      return nil
    }
    
  5. 同样,您不要使用try 。 然后,您不会从try中获得任何收益,但您也不会失去任何东西。

    假设用有用的上下文包装错误是一种很好的做法, try()将被认为是一种不好的做法,因为它没有添加任何上下文。 这意味着try()是一个没人愿意使用的功能,并成为一个很少使用的功能,以至于它可能不存在。

    与其只是说“好吧,如果你不喜欢它,不要使用它并闭嘴”(这就是它的阅读方式),我认为最好尝试解决许多用户正在考虑的缺陷在设计中。 我们可以讨论一下可以从提议的设计中修改哪些内容,以便以更好的方式处理我们的问题吗?

@boomlinde我们同意的一点是,这个提议试图解决一个小用例,而“如果你不需要,就不要使用它”这一事实是它进一步推动这一点的主要论点。 正如@elagergren-spideroak 所说,这个论点是行不通的,因为即使我不想使用它,其他人也会迫使我使用它。 根据你的论点的逻辑,Go 也应该有一个三元语句。 如果您不喜欢三元语句,请不要使用它们。

免责声明 - 我确实认为 Go 应该有一个三元声明,但考虑到 Go 语言特性的方法是不引入可能使代码更难阅读的特性,而不是它不应该引入的特性。

另一件事发生在我身上:我看到很多批评基于这样的想法,即拥有try可能会鼓励开发人员粗心地处理错误。 但在我看来,如果有的话,这更适用于当前的语言。 错误处理样板文件很烦人,它鼓励人们吞下或忽略一些错误来避免它。 例如,我写过几次这样的东西:

func exists(filename string) bool {
  _, err := os.Stat(filename)
  return err == nil
}

为了能够编写if exists(...) { ... } ,即使此代码默默地忽略了一些可能的错误。 如果我有try ,我可能不会费心去做,只是返回(bool, error)

在这里混乱,我将提出添加第二个名为catch的内置函数的想法,它将接收一个接受错误并返回覆盖错误的函数,然后如果后续的catch调用它会覆盖处理程序。 例如:

func catch(handler func(err error) error) {
  // .. impl ..
}

现在,这个内置函数也将是一个类似宏的函数,它将处理try返回的下一个错误,如下所示:

func wrapf(format string, ...values interface{}) func(err error) error {
  // user defined
  return func(err error) error {
    return fmt.Errorf(format + ": %v", ...append(values, err))
  }
}
func sample() {
  catch(wrapf("something failed in foo"))
  try(foo()) // "something failed in foo: <error>"
  x := try(foo2()) // "something failed in foo: <error>"
  // Subsequent calls for catch overwrite the handler
  catch(wrapf("something failed in bar with x=%v", x))
  try(bar(x)) // "something failed in bar with x=-1: <error>"
}

这很好,因为我可以在没有defer的情况下包装错误,这很容易出错,除非我们使用命名返回值或用另一个 func 包装,这也很好,因为defer会添加相同的错误处理程序即使我想以不同的方式处理其中两个错误,也会出现所有错误。 您也可以根据需要使用它,例如:

func foo(a, b string) (int64, error) {
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func withContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, b: %s, err: %v", a, b, err)
  })
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func moreExplicitContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, err: %v", a, err)
  })
  x := try(strconv.Atoi(a))
  catch(func (err error) error {
    return fmt.Errorf("can't parse b: %s, err: %v", b, err)
  })
  y := try(strconv.Atoi(b))
  return x + y
}
func withHelperWrapf(a, b string) (int64, error) {
  catch(wrapf("can't parse a: %s", a))
  x := try(strconv.Atoi(a))
  catch(wrapf("can't parse b: %s", b))
  y := try(strconv.Atoi(b))
  return x + y
}
func before(a, b string) (int64, error) {
  x, err := strconv.Atoi(a)
  if err != nil {
    return 0,  fmt.Errorf("can't parse a: %s, err: %v", a, err)
  }
  y, err := strconv.Atoi(b)
  if err != nil {
    return 0,  fmt.Errorf("can't parse b: %s, err: %v", b, err)
  }
  return x + y
}

并且仍然处于混乱的情绪中(以帮助您同情)如果您不喜欢catch ,则不必使用它。

现在......我并不是说最后一句话,但它确实感觉它对讨论没有帮助,非常激进的IMO。
不过,如果我们走这条路,我认为我们不妨改用try{}catch(error err){} :stuck_out_tongue:

另请参阅 #27519 - #id/catch 错误模型

没有人会让你尝试使用。

忽略油嘴滑舌,我认为这是一种非常随意的方式来驳斥设计批评。

抱歉,油嘴滑舌不是我的本意。

我想说的是, try并不是一个 100% 的解决方案。 try无法很好地处理各种错误处理范例。 例如,如果您需要将调用站点相关的上下文添加到错误中。 你总是可以回退到使用if err != nil {来处理那些更复杂的情况。

对于 X 的各种实例, try不能处理 X 无疑是一个有效的论点。但通常处理案例 X 意味着使机制更加复杂。 这里有一个权衡,一方面处理 X,但使其他一切的机制复杂化。 我们所做的一切都取决于 X 有多常见,以及处理 X 需要多少复杂性。

所以“没有人会让你使用 try”,我的意思是我认为有问题的例子是 10%,而不是 90%。 这种说法肯定有待商榷,我很高兴听到反驳。 但最终我们将不得不在某处划清界限并说“是的, try不会处理这种情况。你必须使用旧式错误处理。对不起。”。

问题不是“尝试无法处理这种特定的错误处理情况”,而是“尝试鼓励您不要包装错误”。 check-handle的想法迫使您编写返回语句,因此编写错误包装非常简单。

在这个提议下,您需要使用带有defer的命名返回,这不直观而且看起来很 hacky。

check-handle的想法迫使您编写返回语句,因此编写错误包装非常简单。

这不是真的 - 在设计草案中,每个返回错误的函数都有一个默认处理程序,它只返回错误。

基于@Goodwine的顽皮点,如果你有一个单一的桥接函数,你真的不需要像HandleErrorf这样的单独函数

func handler(err *error, handle func(error) error) {
  // nil handle is treated as the identity
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

你会用

defer handler(&err, func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

您可以使handler本身成为像try这样的半魔法内置函数。

如果它很神奇,它可以隐式地接受它的第一个参数——即使在没有命名它们的error回报的函数中也可以使用它,淘汰当前提案中不太幸运的一个方面,同时减少它挑剔和错误容易出现装饰错误。 当然,这并不能大大减少前面的示例:

defer handler(func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

如果它以这种方式很神奇,那么如果它在任何地方使用,除了作为defer的参数之外,它必须是一个编译时错误。 你可以更进一步,让它隐式延迟,但是defer handler读起来非常好。

由于它使用defer它可以在返回非零错误时调用其handle函数,即使没有try也可以使用它,因为您可以添加

defer handler(wrapErrWithPackageName)

在顶部到fmt.Errorf("mypkg: %w", err)一切。

这为您提供了很多较旧的check / handle提案,但它自然地(并且明确地)与 defer 一起工作,同时在大多数情况下摆脱了显式命名err的需要try一样,它是一个相对简单的宏,(我想)可以完全在前端实现。

这不是真的 - 在设计草案中,每个返回错误的函数都有一个默认处理程序,它只返回错误。

我的错,你是对的。

我的意思是我认为有问题的例子是 10%,而不是 90%。 这种说法肯定有待商榷,我很高兴听到反驳。 但最终我们将不得不在某处划清界限并说“是的,try 不会处理这种情况。你将不得不使用旧式错误处理。对不起。”。

同意,我的观点是,在检查 EOF 或类似内容时应该画这条线,而不是在包装时画。 但也许如果错误有更多的上下文,这将不再是一个问题。

try()可以用有用的上下文自动包装错误以进行调试吗? 例如,如果xerrors变成errors ,错误应该有一个看起来像try()可以添加的堆栈跟踪的东西,不是吗? 如果是这样也许就足够了🤔

如果目标是(阅读 https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md):

  • 消除样板
  • 最小的语言变化
  • 涵盖“最常见的场景”
  • 给语言增加很少的复杂性

我会接受这个建议,给它一个角度,并允许对所有数十亿行代码进行“小步骤”代码迁移。

而不是建议的:

func printSum(a, b string) error {
        defer fmt.HandleErrorf(&err, "sum %s %s: %v", a,b, err) 
        x := try(strconv.Atoi(a))
        y := try(strconv.Atoi(b))
        fmt.Println("result:", x + y)
        return nil
}

我们可以:

func printSum(a, b string) error {
        var err ErrHandler{HandleFunc : twoStringsErr("printSum",a,b)} 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

我们会得到什么?
twoStringsErr 可以内联到 printSum,或知道如何捕获错误的通用处理程序(在本例中使用 2 个字符串参数) - 所以如果我在许多函数中使用相同的重复 func 签名,我不需要重写每个处理程序时间
以同样的方式,我可以通过以下方式扩展 ErrHandler 类型:

type ioErrHandler ErrHandler
func (i ErrHandler) Handle() ...{

}

要么

type parseErrHandler ErrHandler
func (p parseErrHandler) Handle() ...{

}

要么

type str2IntErrHandler ErrHandler
func (s str2IntErrHandler) Handle() ...{

}

并在我的代码周围使用它:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

因此,实际需要是在 err.Error 设置为 not nil 时开发一个触发器
使用这种方法,我们还可以:

func (s str2IntErrHandler) Handle() bool{
   **return false**
}

这会告诉调用函数继续而不是返回

并在同一函数中使用不同的错误处理程序:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        var oErr overflowError 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        totalAsByte,oErr := sumBytes(x,y)
        sunAsByte,oErr := subtractBytes(x,y)
        return nil
}

等等。

再次超越目标

  • 消除样板 - 完成
  • 最少的语言更改 - 完成
  • 涵盖“最常见的情况” - 超过建议的 IMO
  • 给语言增加很少的复杂性 - sone
    另外 - 更容易的代码迁移
x, err := strconv.Atoi(a)

x, err.Error := strconv.Atoi(a)

实际上 - 更好的可读性(IMO,再次)

@guybrand你是这个反复出现的主题的最新追随者(我喜欢)。

https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

@guybrand这似乎是一个完全不同的提议; 我认为您应该将其作为自己的问题提交,以便该问题可以专注于讨论@griesemer的提案。

@natefinch同意。 我认为这更适合在编写 Go 时改善体验,而不是针对阅读进行优化。 我想知道 IDE 宏或片段是否可以在不成为语言功能的情况下解决这个问题。

@古德温

假设用有用的上下文包装错误是一种很好的做法, try()将被认为是一种不好的做法,因为它没有添加任何上下文。 这意味着try()是一个没人愿意使用的功能,并成为一个很少使用的功能,以至于它可能不存在。

如提案中所述(并通过示例显示), try不会从根本上阻止您添加上下文。 我想说的是,它提出的方式,为错误添加上下文是完全正交的。 提案的常见问题解答中专门解决了这一问题。

我认识到try将没有用。 但是,我也相信HandleErrorf的一般内容涵盖了很大的使用范围,因为仅向错误添加函数范围的上下文并不罕见。

与其只是说“好吧,如果你不喜欢它,不要使用它并闭嘴”(这就是它的阅读方式),我认为最好尝试解决许多用户正在考虑的缺陷在设计中。

如果是这样的话,我很抱歉。 我的意思不是说如果你不喜欢它就应该假装它不存在。 很明显,在某些情况下try将毫无用处,并且您不应该在这种情况下使用它,对于这个提议,我认为这在 KISS 和通用实用程序之间取得了很好的平衡。 我不认为我在这一点上不清楚。

感谢大家迄今为止的多产反馈; 这是非常有用的。
这是我对初步总结的尝试,以便更好地了解反馈。 为我错过或误传的任何人提前道歉; 我希望我理解了它的整体要点。

0) 在积极的一面, @rasky@adg@eandre 、@ dpinela和其他人明确表示对try提供的代码简化感到高兴。

1) 最重要的问题似乎是try不鼓励良好的错误处理风格,而是促进“快速退出”。 (@ agnivade 、@ peterbourgon 、@politician、@ a8m@eandre @ prologic 、@ kungfusheep 、@ cpuguy和其他人对此表示担忧。)

2) 许多人不喜欢内置的想法,或者它附带的函数语法,因为它隐藏了return 。 最好使用关键字。 (@ sheerun@Redundancy 、@ dolmen、@komuw、@RobertGrantEllis @ elagergren-spideroak)。 try也可能很容易被忽略(@peterbourgon),特别是因为它可以出现在可能任意嵌套的表达式中。 @natefinch担心try使得“在一行中倾倒太多东西太容易了”,这是我们在 Go 中通常会尽量避免的事情。 此外,强调try的 IDE 支持可能还不够(@dominikh); try需要“独立存在”。

3) 对于某些人来说,显式if语句的现状不是问题,他们对此很满意(@bitfield、@ marwan -at-work、@natefinch)。 最好只有一种方式做事(@gbbr); 和显式if语句优于隐式return@DavexPro@hmage@prologic 、@natefinch)。
同样, @mattn担心错误结果与try的“隐式绑定”——该连接在代码中不显式可见。

4) 使用try会使调试代码变得更加困难; 例如,可能需要将try表达式重写回if语句,以便可以插入调试语句( @deanveloper@typeless@networkimprov等)。

5) 使用命名返回 ( @buchanae , @adg) 存在一些问题。

有几个人提供了改进或修改提案的建议:

6) 有些人接受了可选错误处理程序 (@beoran) 或提供给try (@ unexge@a8m 、@ eandre 、@gotwarlost) 的格式字符串的想法,以鼓励良好的错误处理。

7) @pierrec建议gofmt可以适当地格式化try表达式以使它们更加可见。
或者,可以通过允许gofmt格式化if语句检查一行上的错误(@zeebo)来使现有代码更紧凑。

8) @marwan-at-work 认为try只是将错误处理从if语句转移到try表达式。 相反,如果我们想真正解决问题,Go 应该通过使其真正隐式来“拥有”错误处理。 目标应该是使(正确的)错误处理更简单,开发人员更有效率(@cpuguy)。

9) 最后,有些人不喜欢try $(@ beoran 、@ HiImJC 、@dolmen)这个名字,或者更喜欢?@twisted1919 、@ leaxoy等)这样的符号.

对此反馈的一些评论(相应编号):

0) 感谢您的积极反馈! :-)

1)了解更多有关此问题的信息会很好。 当前使用if语句来测试错误的编码风格几乎是尽可能明确的。 逐个(针对每个if )向错误添加附加信息非常容易。 通常,以统一的方式处理函数中检测到的所有错误是有意义的,这可以通过defer来完成——这现在已经成为可能。 事实上,我们已经拥有了语言中用于良好错误处理的所有工具,以及处理程序构造与defer不正交的问题,这导致我们放弃了一种仅用于增加错误的新机制.

2)当然可以使用关键字或特殊语法代替内置语法。 新关键字不会向后兼容。 一个新的运营商可能会,但似乎更不明显。 详细的提案详细讨论了各种利弊。 但也许我们误判了这一点。

3) 这个提议的原因是错误处理(特别是相关的样板代码)被 Go 社区提到为 Go 中的一个重要问题(仅次于缺乏泛型)。 该提案直接解决了样板问题。 它只解决了最基本的案例,因为任何更复杂的案例都可以更好地处理我们已有的案例。 因此,虽然很多人对现状感到满意,但也有(可能)同样庞大的人会喜欢更简化的方法,例如try ,他们知道这是“公正的”语法糖。

4)调试点是一个有效的关注点。 如果需要在检测错误和return之间添加代码,则必须将try表达式重写为if语句可能会很烦人。

5) 命名返回值:详细文档详细讨论了这一点。 如果这是对该提案的主要关注点,那么我认为我们处于一个很好的位置。

6) try的可选处理程序参数:详细文档也讨论了这一点。 请参阅设计迭代部分。

7) 使用gofmt格式化try表达式,使它们更加可见当然是一种选择。 但是在表达式中使用它会失去try的一些好处。

8) 我们考虑从错误处理 ( handle ) 的角度而不是从错误测试 ( try ) 的角度来看问题。 具体来说,我们简要地考虑了只引入错误处理程序的概念(类似于去年 Gophercon 上提出的原始设计草案)。 当时的想法是,如果(且仅当)声明了一个处理程序,在最后一个值为error类型的多值赋值中,该值可以简单地留在赋值中。 编译器将隐式检查它是否为非零,如果是则分支到处理程序。 这将使显式错误处理完全消失,并鼓励每个人改为编写处理程序。 这似乎是一种极端的方法,因为它完全是隐含的——检查发生的事实是不可见的。

9)我可以建议我们现在不要骑自行车这个名字。 一旦所有其他问题得到解决,就是微调名称的更好时机。

这并不是说这些担忧是无效的——上面的回复只是陈述了我们目前的想法。 展望未来,最好对新的担忧(或支持这些担忧的新证据)发表评论——只是重申已经说过的话并不能为我们提供更多信息。

最后,似乎不是每个评论这个问题的人都阅读了详细的文档。 请在评论之前这样做,以避免重复已经说过的话。 谢谢。

这不是对提案的评论,而是错字报告。 自从完整的提案发布以来,它还没有修复,所以我想我会提到它:

func try(t1 T1, t1 T2, … tn Tn, te error) (T1, T2, … Tn)

应该:

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

是否值得为错误检查语句分析公开可用的 Go 代码,以尝试找出大多数错误检查是否真正重复,或者在大多数情况下,同一函数中的多个检查是否会添加不同的上下文信息? 该提议对前一种情况很有意义,但对后者无济于事。 在后一种情况下,人们要么继续使用if err != nil ,要么放弃添加额外的上下文,使用try()并诉诸于为每个函数添加常见的错误上下文,这对 IMO 是有害的。 随着即将推出的错误值功能,我认为我们希望人们更频繁地用更多信息包装错误。 可能我误解了这个提议,但是 AFAIU,这有助于减少样板文件,只有当来自单个函数的所有错误必须以完全一种方式包装并且如果一个函数处理可能需要以不同方式包装的五个错误时无济于事。 不确定这种情况在野外有多常见(在我的大多数项目中都很常见),但我担心try()可能会鼓励人们对每个函数使用通用包装器,即使包装不同的错误是有意义的不同。

只是一个带有小样本集数据支持的快速评论:

我们提出了一个名为 try 的新内置函数,专门设计用于消除 Go 中通常与错误处理相关的样板 if 语句

如果这是该提案要解决的核心问题,我发现这个“样板”仅占我在数十个公开可用的开源项目中代码的 1.4%,总计约 60k SLOC。

好奇是否有其他人有类似的统计数据?

在像 Go 本身这样一个更大的代码库上,总共约 160 万条 SLOC,这相当于代码库的约 0.5% 具有像if err != nil这样的行。

这真的是用 Go 2 解决的最有影响力的问题吗?

非常感谢@griesemer花时间了解每个人的想法并明确提供想法。 我认为这确实有助于人们在此过程中听到社区的声音。

  1. @pierrec建议 gofmt 可以适当地格式化 try 表达式以使它们更加可见。
    或者,可以通过允许 gofmt 格式化 if 语句在一行上检查错误(@zeebo)来使现有代码更紧凑。
  1. 使用gofmt来格式化try表达式,使其更加可见当然是一种选择。 但是当在表达式中使用时,它会失去try的一些好处。

这些是关于要求gofmt格式化try的有价值的想法,但我很感兴趣是否有任何特别的想法在gofmt允许if语句检查错误是一行。 该提案与try的格式混为一谈,但我认为这是一个完全正交的事情。 谢谢。

@griesemer感谢您完成所有评论并回答大部分(如果不是全部)反馈的令人难以置信的工作🎉

您的反馈中没有解决的一件事是使用 Go 语言的工具/审查部分来改善错误处理体验的想法,而不是更新 Go 语法。

例如,随着新 LSP ( gopls ) 的登陆,它似乎是分析函数签名并为开发人员处理错误处理样板的完美场所,同时也进行了适当的包装和审查。

@griesemer我确信这没有经过深思熟虑,但我试图将您的建议修改为更接近我在这里会感到舒服的东西: https ://www.reddit.com/r/golang/comments/bwvyhe

@zeebo在一行上制作gofmt格式if err != nil { return ...., err }很容易。 大概它只适用于这种特定类型的if模式,而不是所有“短” if语句?

同样,有人担心try是不可见的,因为它与业务逻辑在同一条线上。 我们有所有这些选择:

当前风格:

a, b, c, ... err := BusinessLogic(...)
if err != nil {
   return ..., err
}

一行if

a, b, c, ... err := BusinessLogic(...)
if err != nil { return ..., err }

try在单独的行 (!):

a, b, c, ... err := BusinessLogic(...)
try(err)

try建议:

a, b, c := try(BusinessLogic(...))

第一行和最后一行似乎是最清楚的(对我来说),尤其是当有人习惯于识别try时。 在最后一行中,明确检查了一个错误,但由于它(通常)不是主要操作,所以它在后台更多一些。

@marwan-at-work 我不确定您建议这些工具为您做什么。 您是否建议他们以某种方式隐藏错误处理?

@dpinela

@guybrand这似乎是一个完全不同的提议; 我认为您应该将其作为自己的问题提交,以便该问题可以专注于讨论@griesemer的提案。

IMO 我的建议仅在语法上有所不同,意思是:

  • 目标在内容和优先级上是相似的。
  • 在自己的行中捕获每个错误并相应地(如果不是 nil)在通过处理程序函数时退出函数的想法是相似的(伪 asm - 它是“jnz”和“调用”)。
  • 这甚至意味着函数体中的行数(没有延迟)和流看起来完全一样(因此 AST 可能也会变得相同)

所以主要的区别是我们是否使用 try(func()) 包装原始函数调用,它总是会分析最后一个 var 以 jnz 调用或使用实际返回值来做到这一点。

我知道它看起来不同,但实际上在概念上非常相似。
另一方面 - 如果你采取通常的尝试....catch 很多类似 c 的语言 - 那将是一个非常不同的实现,不同的可读性等。

然而,我确实认真考虑写一个提案,谢谢你的想法。

@griesemer

我不确定您建议这些工具为您做什么。 您是否建议他们以某种方式隐藏错误处理?

恰恰相反:我建议gopls可以选择为您编写错误处理样板。

正如您在上一条评论中提到的:

这个提议的原因是 Go 社区提到错误处理(特别是相关的样板代码)是 Go 中的一个重要问题(在缺乏泛型旁边)

所以问题的核心是程序员最终编写了很多样板代码。 所以问题是关于写作,而不是阅读。 因此,我的建议是:让计算机(工具/gopls)通过分析函数签名并放置适当的错误处理子句来为程序员编写代码。

例如:

// user begins to write this function: 
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  return bts, nil
}

然后用户触发该工具,可能只需保存文件(类似于 gofmt/goimports 通常的工作方式), gopls将查看此函数,分析其返回签名并将代码扩充为:

// user has triggered the tool (by saving the file, or code action)
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  return bts, nil
}

这样,我们得到了两全其美:我们获得了当前错误处理系统的可读性/明确性,并且程序员没有编写任何错误处理样板。 更好的是,用户可以在以后继续修改错误处理块以执行不同的行为: gopls可以理解该块存在,并且不会修改它。

该工具如何知道我打算稍后在函数中处理err而不是提前返回? 尽管很少见,但我仍然编写了代码。

如果以前有人提出过这个问题,我深表歉意,但我找不到任何提及。

try(DoSomething())对我来说很好读,而且很有意义:代码正在尝试做某事。 try(err) ,OTOH,从语义上讲,感觉有点不对劲:如何尝试错误? 在我看来,可以_test_ 或_check_ 一个错误,但_trying_ 似乎不对。

我确实意识到允许try(err)出于一致性的原因很重要:我想如果try(DoSomething())有效,但err := DoSomething(); try(err)没有,那会很奇怪。 不过,感觉try(err)在页面上看起来有点尴尬。 我想不出任何其他内置函数可以如此容易地看起来如此奇怪。

我对此事没有任何具体的建议,但我仍然想发表这一看法。

@griesemer谢谢。 实际上,该提案仅适用于return ,但我怀疑允许任何单个语句成为单行会很好。 例如,在测试中,无需更改测试库,就可以拥有

if err != nil { t.Fatal(err) }

第一行和最后一行似乎是最清楚的(对我来说),尤其是当有人习惯于识别 try 是什么时。 在最后一行中,明确检查了一个错误,但由于它(通常)不是主要操作,所以它在后台更多一些。

最后一行隐藏了一些成本。 如果您想注释错误,我相信社区已经口头表示这是理想的最佳实践并且应该受到鼓励,您将不得不更改函数签名以命名参数并希望单个defer应用于函数体中的每一个出口,否则try没有价值; 由于它的易用性,甚至可能是负面的。

我没有更多要补充的,我相信还没有说过。


我没有看到如何从设计文档中回答这个问题。 这段代码有什么作用:

func foo() (err error) {
    src := try(getReader())
    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }
        try(err)
        println(n)
    }
    return nil
}

我的理解是它会脱糖成

func foo() (err error) {
    tsrc, te := getReader()
    if err != nil {
        err = te
        return
    }
    src := tsrc

    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }

        terr := err
        if terr != nil {
            err = terr
            return
        }

        println(n)
    }
    return nil
}

编译失败,因为err在裸返回期间被遮蔽。 这不会编译吗? 如果是这样,那是一个非常微妙的失败,而且似乎不太可能发生。 如果没有,那么除了一些糖之外,还有更多的事情发生。

@marwan-at-work

正如您在上一条评论中提到的:

这个提议的原因是 Go 社区提到错误处理(特别是相关的样板代码)是 Go 中的一个重要问题(在缺乏泛型旁边)

所以问题的核心是程序员最终编写了很多样板代码。 所以问题是关于写作,而不是阅读。

我认为实际上是相反的——对我来说,当前错误处理样板的最大烦恼不是必须输入它,而是它如何将函数的快乐路径垂直分散在屏幕上,使其更难理解一眼。 这种效果在 I/O 繁重的代码中尤其明显,其中通常每两个操作之间都有一个样板块。 即使CopyFile的简化版本也需要大约 20 行代码,尽管它实际上只执行五个步骤:开源、延迟关闭源、打开目标、复制源 -> 目标、关闭目标。

当前语法的另一个问题是,正如我之前提到的,如果您有一个操作链,每个操作都可能返回错误,那么当前语法会强制您为所有中间结果命名,即使您更愿意留下一些匿名的。 当这种情况发生时,它也会损害可读性,因为您必须花费大脑周期来解析这些名称,即使它们不是很有信息量。

我喜欢try在单独的行上。
我希望它可以独立指定handler func。

func try(error, optional func(error)error)
func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    try(err)

    handle := func(err error) error {
        tx.Rollback()
        return err
    }

    var res int64
    _, err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(err, handle)

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(err, handle)

    return tx.Commit()
}

@zeebo :我给出的例子是 1:1 翻译。 第一个(传统的if )没有处理错误,其他的也没有。 如果第一个处理了错误,并且如果这是在函数中检查错误的唯一位置,那么第一个示例(使用if )可能是编写代码的合适选择。 如果有多个错误检查,所有这些都使用相同的错误处理(包装),比如说因为它们都添加了有关当前函数的信息,那么可以使用defer语句在一个地方处理所有错误。 或者,可以将if重写为try (或不理会它们)。 如果要检查多个错误,并且它们都以不同的方式处理错误(这可能表明该函数的关注范围太广并且可能需要拆分),则使用if是要走的路。 是的,有不止一种方法可以做同样的事情,正确的选择取决于代码以及个人品味。 虽然我们在 Go 中确实为“做一件事的一种方式”而努力,但这当然已经不是这种情况了,尤其是对于常见的构造。 例如,当if - else - if序列变得太长时,有时switch可能更合适。 有时,变量声明var x intx := 0更能表达意图,依此类推(尽管不是每个人都对此感到高兴)。

关于您关于“重写”的问题:不,不会出现编译错误。 请注意,重写发生在内部(并且可能比代码模式建议的更有效),编译器无需抱怨隐藏返回。 在您的示例中,您在嵌套范围内声明了一个本地err变量。 当然, try仍然可以直接访问结果err变量。 重写可能看起来更像这样

[编辑] PS:更好的答案是: try不是赤裸裸的回报(即使重写看起来像它)。 毕竟,明确地给try一个参数,该参数包含(或是)如果不是nil则返回的错误。 裸返回的影子错误是源错误(不是源的底层翻译。编译器不需要该错误。

如果总体函数的最终返回类型不是错误类型,我们会恐慌吗?

它将使内置功能更加通用(例如满足我在 #32219 中的关注)

@pjebs这已被考虑并决定反对。 请阅读详细的设计文档(明确提及您在此主题上的问题)。

我还想指出 try() 被视为表达式,即使它用作 return 语句。 是的,我知道 try 是内置宏,但我猜大多数用户会像函数式编程一样使用它。

func doSomething() (error, error, error, error, error) {
   ...
}
try(try(try(try(try(doSomething)))))

该设计表明您使用panic进行了探索,而不是返回错误。

我强调一个细微的区别:

完全按照您当前的提案所述执行,除了删除总体函数必须具有error类型的最终返回类型的限制。

如果它没有error的最终返回类型 => 恐慌
如果使用 try 进行包级变量声明 => 恐慌(消除对MustXXX( )约定的需要)

对于单元测试,适度的语言变化。

@mattn ,我非常怀疑有多少人会编写这样的代码。

@pjebs ,这种语义——如果当前函数中没有错误导致恐慌——正是设计文档在https://github.com/golang/proposal/blob/master/design/32437-try-builtin 中讨论的内容。 md#讨论。

此外,为了使 try 不仅仅在有错误结果的函数内部有用,try 的语义取决于上下文:如果在包级别使用 try,或者如果在没有错误结果的函数内部调用它, try 在遇到错误时会恐慌。 (顺便说一句,由于该属性,在该提案中,内置函数被称为 must 而不是 try。) try(或 must)以这种上下文敏感的方式运行似乎很自然,也非常有用:它允许消除当前在包级变量初始化表达式中使用的许多用户定义的必须辅助函数。 它还将打开通过测试包在单元测试中使用 try 的可能性。

然而,try 的上下文敏感性被认为是令人担忧的:例如,如果从签名中添加或删除错误结果,则包含 try 调用的函数的行为可能会静默变化(从可能恐慌到不恐慌,反之亦然)。 这似乎太危险的财产。 显而易见的解决方案是将 try 的功能拆分为两个单独的函数,must 和 try(非常类似于 issue #31442 的建议)。 但这需要两个新的内置函数,只有 try 直接连接到对更好的错误处理支持的迫切需要。

@pjebs这正是我们在先前提案中考虑的内容(参见详细文档,设计迭代部分,第 4 段):

此外,为了使 try 不仅仅在有错误结果的函数内部有用,try 的语义取决于上下文:如果在包级别使用 try,或者如果在没有错误结果的函数内部调用它, try 在遇到错误时会恐慌。 (顺便说一句,由于该属性,内置函数被称为 must 而不是在该提案中尝试。)

(Go 团队内部)的共识是,如果try依赖于上下文并采取如此不同的行为方式,将会令人困惑。 例如,将错误结果添加到函数(或删除它)可以默默地将函数的行为从恐慌更改为不恐慌(反之亦然)。

@griesemer感谢您对重写的澄清。 我很高兴它会编译。

我知道这些示例是没有注释错误的翻译。 我试图争辩说try使得在常见情况下对错误进行良好的注释变得更加困难,并且错误注释对社区非常重要。 到目前为止,大部分评论都在探索为try添加更好的注释支持的方法。

关于必须以不同的方式处理错误,我不同意这表明该功能的关注范围太广。 我一直在翻译评论中声称的真实代码的一些示例,并将它们放在我原始评论底部的下拉列表中,以及https://github.com/golang/go/issues/32437#issuecomment中的示例 - 499007288 我认为很好地展示了一个常见的案例:

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

该函数的目的是将某些数据的模板执行到文件中。 我不认为它需要被拆分,如果所有这些错误都刚刚获得了它们从延迟中创建的界限,那将是不幸的。 这对开发人员来说可能没问题,但对用户来说用处要小得多。

我认为这也是一个信号,即defer wrap(&err, "message: %v", err)错误是多么微妙,即使是经验丰富的 Go 程序员,它们也是如何绊倒的。


总结一下我的论点:我认为错误注释比基于表达式的错误检查更重要,我们可以通过允许基于语句的错误检查为一行而不是三行来减少噪音。 谢谢。

@griesemer抱歉,我阅读了另一个讨论恐慌的部分,但没有看到对危险的讨论。

@zeebo感谢这个例子。 在这种情况下,使用if语句似乎是正确的选择。 但是要指出的是,将 if 格式化为单行可能会简化这一点。

我想再次提出处理程序作为try的第二个参数的想法,但另外处理程序参数是_required_,但可以为零。 这使得处理错误成为默认值,而不是异常。 如果您确实希望将错误原封不动地向上传递,只需向处理程序提供一个 nil 值, try的行为将与原始提案中一样,但 nil 参数将充当视觉提示,即错误没有被处理。 在代码审查期间会更容易捕获。

file := try(os.Open("my_file.txt"), nil)

如果提供了处理程序但为 nil 会发生什么? 应该尝试恐慌还是将其视为缺失的错误处理程序?

如上所述, try将按照最初的提议行事。 不会有诸如缺少错误处理程序之类的东西,只有一个 nil 。

如果调用处理程序时出现非 nil 错误,然后返回 nil 结果怎么办? 这是否意味着错误已“取消”? 或者封闭函数是否应该返回一个 nil 错误?

我相信封闭函数会返回 nil 错误。 如果try有时即使在收到非 nil 错误值后仍能继续执行,这可能会非常令人困惑。 这将允许处理程序在某些情况下“处理”错误。 例如,此行为在“获取或创建”样式函数中可能很有用。

func getOrCreateObject(obj *object) error {
    defaultObjectHandler := func(err error) error {
        if err == ObjectDoesNotExistErr {
            *obj = object{}
            return nil
        }
        return fmt.Errorf("getting or creating object: %v", err)
    }

    *obj = try(db.getObject(), defaultObjectHandler)
}

也不清楚允许可选的错误处理程序是否会导致程序员完全忽略正确的错误处理。 在任何地方进行适当的错误处理也很容易,但会错过一次尝试。 等等。

我相信通过使处理程序成为必需的、可为零的论点,可以缓解这两个问题。 它要求程序员做出一个有意识的、明确的决定,即他们不会处理他们的错误。

作为奖励,我认为需要错误处理程序也会阻止深度嵌套的try s,因为它们不那么简短。 有些人可能认为这是一个缺点,但我认为这是一个好处。

@velovix我喜欢这个想法,但为什么必须需要错误处理程序? 不能默认是nil吗? 为什么我们需要“视觉线索”?

@griesemer如果采用了@velovix想法,但builtin包含一个将 err 转换为 panic 的预定义函数,并且我们删除了过度架构函数具有错误返回值的要求怎么办?

这个想法是,如果总体函数不返回错误,则使用没有错误处理程序的try是编译时错误。

错误处理程序还可用于在错误位置使用各种库等包装即将返回的错误,而不是顶部的defer修改命名的返回错误。

@pjebs

为什么必须需要错误处理程序? 不能默认为 nil 吗? 为什么我们需要“视觉线索”?

这是为了解决人们的担忧

  1. 现在的try提案可能会阻止人们为他们的错误提供上下文,因为这样做并不是那么简单。

首先有一个处理程序可以更容易地提供上下文,并且将处理程序作为一个必需的参数会发送一条消息:常见的推荐情况是以某种方式处理或上下文化错误,而不是简单地将其传递到堆栈上。 这符合 Go 社区的一般建议。

  1. 原始提案文件中的一个问题。 我在第一条评论中引用了它:

也不清楚允许可选的错误处理程序是否会导致程序员完全忽略正确的错误处理。 在任何地方进行适当的错误处理也很容易,但会错过一次尝试。 等等。

必须传递明确的nil使得忘记正确处理错误变得更加困难。 您必须明确决定不处理错误,而不是通过省略参数来隐式处理。

进一步考虑在https://github.com/golang/go/issues/32437#issuecomment -498947603 中简要提到的条件返回。
它似乎
return if f, err := os.Open("/my/file/path"); err != nil
会更符合 Go 现有的if的外观。

如果我们为return if语句添加一条规则,
当最后一个条件表达式(如err != nil )不存在时,return if语句中声明的最后一个变量是error类型,那么最后一个变量的值将自动与nil作为隐式条件进行比较。

那么return if语句可以简写为:
return if f, err := os.Open("my/file/path")

这非常接近try提供的信噪比。
如果我们将return if更改为try ,它将变为
try f, err := os.Open("my/file/path")
它再次变得类似于此线程中try的其他提议变体,至少在语法上如此。
就个人而言,在这种情况下,我仍然更喜欢return if而不是try ,因为它使函数的退出点非常明确。 例如,在调试时,我经常在编辑器中突出显示关键字return以识别大型函数的所有退出点。

不幸的是,对于插入调试日志的不便,它似乎也没有足够的帮助。
除非我们还允许body块用于return if ,例如
原版的:

        return if f, err := os.Open("my/path") 

调试时:

-       return if f, err := os.Open("my/path") 
+       return if f, err := os.Open("my/path") {
+               fmt.Printf("DEBUG: os.Open: %s\n", err)
+       }

我假设return if的主体块的含义是显而易见的。 它将在defer之前执行并返回。

也就是说,我对 Go 中现有的错误处理方法没有任何抱怨。
我更担心添加新的错误处理将如何影响 Go 目前的优点。

@velovix我们非常喜欢带有显式处理函数作为第二个参数的try的想法。 但正如设计文档所述,有太多问题没有明显的答案。 您以您认为合理的方式回答了其中一些问题。 很有可能(这是我们在 Go 团队中的经验),其他人认为正确的答案是完全不同的。 例如,您说应该始终提供 handler 参数,但它可以是nil ,为了明确表示我们不关心处理错误。 现在,如果提供一个函数值(不是nil文字),而该函数值(存储在变量中)恰好为 nil,会发生什么? 通过与显式nil值类比,不需要处理。 但其他人可能会争辩说这是代码中的错误。 或者,另一种可能是允许零值处理程序参数,但是在某些情况下,一个函数可能会不一致地处理错误,而在其他情况下则不是,而且从代码中不一定很明显,因为它看起来好像总是存在处理程序. 另一个论点是最好有一个错误处理程序的顶级声明,因为这很清楚该函数确实处理错误。 因此defer 。 可能还有更多。

了解更多有关此问题的信息会很好。 当前使用 if 语句来测试错误的编码风格是尽可能明确的。 逐个(针对每个 if)向错误添加附加信息非常容易。 通常,以统一的方式处理函数中检测到的所有错误是有意义的,这可以通过 defer 来完成——这现在已经成为可能。 事实上,我们已经拥有了语言中用于良好错误处理的所有工具,以及处理程序构造与 defer 不正交的问题,这导致我们放弃了一种仅用于增加错误的新机制。

@griesemer -IIUC,您是说对于与调用站点相关的错误上下文,当前的 if 语句很好。 然而,这个新的try函数对于在一个地方处理多个错误很有用的情况很有用。

我相信担心的是,虽然在某些情况下简单地执行if err != nil { return err}可能没问题,但通常建议在返回之前修饰错误。 而这个提议似乎解决了前者,对后者没有多大作用。 这实质上意味着人们将被鼓励使用easy-return模式。

@agnivade你是对的,这个建议对错误装饰没有任何帮助(但建议使用defer )。 一个原因是为此的语言机制已经存在。 一旦需要错误修饰,尤其是在单个错误的基础上,修饰代码的额外源文本量使得if相比之下不那么繁琐。 在不需要装饰的情况下,或者装饰总是相同的情况下,样板变成了可见的麻烦,然后减损了重要的代码。

人们已经被鼓励使用易于返回的模式, try或不使用try ,只是写得更少了。 想一想,_鼓励错误修饰的唯一方法是强制执行_,因为无论有什么语言支持,修饰错误都需要更多的工作。

使交易更甜蜜的一种方法是只允许像try (或任何类似的快捷符号) _if_ 在某处提供显式(可能为空)处理程序(请注意,原始设计草案没有这样的要求,要么)。

我不确定我们想走这么远。 让我重申一下,很多完美的代码,比如库的内部,不需要到处修饰错误。 例如,在它们离开 API 入口点之前向上传播并修饰它们是很好的。 (事实上​​,到处装饰它们只会导致过度装饰的错误,在隐藏真正的罪魁祸首的情况下,更难找到重要的错误;就像过于冗长的日志记录会让人很难看到真正发生的事情)。

我认为我们还可以添加一个catch函数,这将是一个不错的组合,所以:

func a() int {
  x := randInt()
  // let's assume that this is what recruiters should "fix" for us
  // or this happens in 3rd-party package.
  if x % 1337 != 0 {
    panic("not l33t enough")
  }
  return x
}

func b() error {
  // if a() panics, then x = 0, err = error{"not l33t enough"}
  x, err := catch(a())
  if err != nil {
    return err
  }
  sendSomewhereElse(x)
  return nil
}

// which could be simplified even further

func c() error {
  x := try(catch(a()))
  sendSomewhereElse(x)
  return nil
}

在此示例中, catch()recover()恐慌和return ..., panicValue
当然,我们有一个明显的极端情况,其中我们有一个函数,它也返回一个错误。 在这种情况下,我认为只传递错误值会很方便。

所以,基本上,你可以使用 catch() 来实际恢复()恐慌并将它们变成错误。
这对我来说看起来很有趣,因为 Go 实际上没有异常,但在这种情况下,我们有非常简洁的 try()-catch() 模式,它也不应该用 Java ( catch(Throwable)在 Main + throws LiterallyAnything )。 您可以轻松处理某人的恐慌,就像那些通常的错误一样。 在我当前的项目中,我目前在 Go 中有大约 600 万+ LoC,我认为这至少对我来说会简化事情。

@griesemer感谢您对讨论的回顾。

我注意到其中遗漏了一点:有些人认为我们应该等待这个特性,直到我们有了泛型,这有望让我们以更优雅的方式解决这个问题。

此外,我也喜欢@velovix的建议,虽然我很欣赏这引发了规范中描述的一些问题,但我认为这些可以很容易地以合理的方式得到回答,就像@velovix已经做过的那样。

例如:

  • 如果提供一个函数值(不是 nil 文字),而该函数值(存储在变量中)恰好为 nil,会发生什么? => 不要处理错误,句号。 这在错误处理取决于上下文并且根据是否需要错误处理来设置处理程序变量的情况下很有用。 这不是一个错误,而是一个功能。 :)

  • 另一个论点是最好有一个错误处理程序的顶级声明,因为这很清楚该函数确实处理错误。 => 所以将函数顶部的错误处理程序定义为命名的闭包函数并使用它,所以也很清楚应该处理错误。 这不是一个严重的问题,更多的是风格要求。

还有哪些其他担忧? 我很确定他们都可以以合理的方式得到类似的回答。

最后,正如您所说,“如果某处提供了明确的(可能为空的)处理程序,则一种使交易更甜蜜的方法是只允许尝试(或任何类似的快捷方式符号)之类的东西”。 我认为如果我们要继续这个提议,我们实际上应该“走这么远”,以鼓励正确的、“显式优于隐式”的错误处理。

@griesemer

现在,如果提供一个函数值(不是 nil 文字),而该函数值(存储在变量中)恰好为 nil,会发生什么? 通过与显式 nil 值类比,不需要处理。 但其他人可能会争辩说这是代码中的错误。

从理论上讲,这似乎确实是一个潜在的陷阱,尽管我很难概念化一个合理的情况,即处理程序最终会意外地为零。 我想处理程序最常见的要么来自其他地方定义的实用函数,要么来自函数本身中定义的闭包。 这些都不可能意外地变为 nil。 从理论上讲,您可能会遇到处理函数作为参数传递给其他函数的场景,但在我看来,这似乎有些牵强。 也许有一种我不知道的模式。

另一个论点是最好有一个错误处理程序的顶级声明,因为这很清楚该函数确实处理错误。 因此defer

正如@beoran 所提到的,将处理程序定义为函数顶部附近的闭包在风格上看起来非常相似,这就是我个人期望人们最常使用处理程序的方式。 虽然我很欣赏所有处理错误的函数都将使用defer这一事实所带来的清晰性,但当一个函数需要在函数中途调整其错误处理策略时,它可能变得不太清楚。 然后,将有两个defer可供查看,读者将不得不推理它们将如何相互交互。 在这种情况下,我相信处理程序的论点会更加清晰和符合人体工程学,而且我确实认为这将是一个_相对_常见的场景。

是否可以在没有括号的情况下使其工作?

即类似的东西:
a := try func(some)

@Cyber​​ax - 如上所述,在发布之前仔细阅读设计文档非常重要。 由于这是一个高流量的问题,有很多人订阅。

该文档详细讨论了运算符与函数。

我喜欢这个比我喜欢八月的版本要多得多。

我认为大部分负面反馈,并不完全反对没有return关键字的退货,可以总结为两点:

  1. 人们不喜欢命名结果参数,这在大多数情况下都是必需的
  2. 它不鼓励为错误添加详细的上下文

参见例如:

对这两个反对意见的反驳分别是:

  1. “我们认为 [命名结果参数] 没问题”
  2. “没有人会让你使用try ”/它不适用于 100% 的情况

关于 1,我真的没有什么要说的(我对此感觉不强烈)。 但是关于2,我注意到八月的提案没有这个问题,大多数反提案也没有这个问题。

特别是tryf反提案(在此线程中独立发布了两次)和try(X, handlefn)反提案(这是设计迭代的一部分)都没有这个问题。

我认为很难说try会促使人们远离使用相关上下文装饰错误,而转向单个通用的每个函数错误装饰。

由于这些原因,我认为值得尝试解决这个问题,并且我想提出一个可能的解决方案:

  1. 目前defer的参数只能是函数或方法调用。 允许defer也有函数名或函数字面量,即
defer func(...) {...}
defer packageName.functionName
  1. 当 panic 或 deferreturn 遇到这种类型的 defer 时,它们会调用函数,为它们的所有参数传递零值

  2. 允许try有多个参数

  3. try遇到新类型的 defer 时,它会调用函数,将指向错误值的指针作为第一个参数传递,然后是try自己的所有参数,除了第一个参数。

例如,给定:

func errorfn() error {
    return errors.New("an error")
}


func f(fail bool) {
    defer func(err *error, a, b, c int) {
        fmt.Printf("a=%d b=%d c=%d\n", a, b, c)
    }
    if fail {
        try(errorfn, 1, 2, 3)
    }
}

将发生以下情况:

f(false)        // prints "a=0 b=0 c=0"
f(true)         // prints "a=1 b=2 c=3"

@zeebo 在https://github.com/golang/go/issues/32437#issuecomment -499309304中的代码可以重写为:

func (c *Config) Build() error {
    defer func(err *error, msg string, args ...interface{}) {
        if *err == nil || msg == "" {
            return
        }
        *err = errors.WithMessagef(err, msg, args...)
    }
    pkgPath := try(c.load(), "load config dir")

    b := bytes.NewBuffer(nil)
    try(templates.ExecuteTemplate(b, "main", c), "execute main template")

    buf := try(format.Source(b.Bytes()), "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
    // ...
}

并将 ErrorHandlef 定义为:

func HandleErrorf(err *error, format string, args ...interface{}) {
        if *err != nil && format != "" {
                *err = fmt.Errorf(format + ": %v", append(args, *err)...)
        }
}

将免费为每个人提供备受追捧的tryf ,而无需将fmt样式的格式字符串拉入核心语言。

此功能向后兼容,因为defer不允许函数表达式作为其参数。 它不会引入新的关键字。
除了https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md中概述的更改之外,实现它需要进行的更改是:

  1. 教解析器关于新的延迟
  2. 更改类型检查器以检查函数内部所有具有函数作为参数(而不是调用)的延迟也具有相同的签名
  3. 更改类型检查器以检查传递给try的参数是否与传递给defer的函数的签名匹配
  4. 更改后端 (?) 以生成适当的 deferproc 调用
  5. 更改try的实现以在遇到由新类型的 defer 延迟的调用时将其参数复制到延迟调用的参数中。

check/handle草案设计的复杂性之后,我很惊喜地看到这个更简单实用的提案落地,尽管我对它遭到如此多的反对感到失望。

诚然,很多回击来自那些对目前的冗长(一个完全合理的立场)非常满意并且可能不会真正欢迎任何减轻它的建议的人。 对于我们其他人来说,我认为这个建议达到了简单和类似 Go 的最佳点,不要尝试做太多事情,并且与现有的错误处理技术相吻合,如果try并没有完全按照您的意愿行事。

关于一些具体点:

  1. 我唯一不喜欢该提案的是在使用defer时需要有一个命名的错误返回参数,但是,话虽如此,我想不出任何其他不会与之冲突的解决方案其他语言的工作方式。 所以我认为,如果提案获得通过,我们将不得不接受这一点。

  2. 遗憾的是try不能很好地与不返回错误值的函数的测试包配合使用。 我自己的首选解决方案是拥有第二个内置函数(可能是ptrymust ),它总是在遇到非零错误时恐慌而不是返回,因此可能是与上述功能一起使用(包括main )。 虽然这个想法在当前的提案迭代中被拒绝了,但我给人的印象是它是一个“近距离电话”,因此它可能有资格重新考虑。

  3. 我认为人们很难理解go try(f)defer try(f)正在做什么,因此最好完全禁止它们。

  4. 我同意那些认为如果go fmt不重写单行if语句,现有的错误处理技术看起来不那么冗长的观点。 就个人而言,我更喜欢一个简单的规则,无论是否涉及错误处理,这都将被允许用于 _any_ 单个语句if 。 事实上,我一直无法理解为什么在编写单行函数时当前不允许这样做,其中主体放置在与允许声明相同的行上。

在装饰错误的情况下

func myfunc()( err error){
try(thing())
defer func(){
err = errors.Wrap(err,"more context")
}()
}

这感觉比现有的范式更加冗长和痛苦,并且不像检查/处理那样简洁。 非包装的 try() 变体更简洁,但感觉就像人们最终会混合使用 try,并返回简单的错误。 我不确定我是否喜欢混合尝试和简单错误返回的想法,但我完全被装饰错误所吸引(并期待 Is/As)。 让我觉得虽然这在语法上很简洁,但我不确定我是否想要实际使用它。 检查/处理感觉我会更彻底地接受。

我真的很喜欢这种简单性和“做好一件事”的方法。 在我的GoAWK解释器中,这将非常有帮助——我有大约 100 个if err != nil { return nil }结构,它可以简化和整理,而且这是在一个相当小的代码库中。

我已经阅读了将其设为内置而不是关键字的提案的理由,归结为不必调整解析器。 但这对于编译器和工具编写者来说不是一个相对较小的痛苦吗,而拥有额外的括号和 this-looks-like-a-function-but-isn't 的可读性问题将是所有 Go 编码器和代码的问题-读者不得不忍受。 在我看来,“但是panic()确实控制流”的论点(借口?:-)并没有削减它,因为恐慌和恢复本质上是例外的,而try()会是正常的错误处理和控制流。

即使按原样进行,我也会非常感激,但我强烈希望正常控制流清晰,即通过关键字完成。

我赞成这个提议。 它避免了我对先前提案的最大保留: handle相对于defer的非正交性。

我想提两个我认为上面没有强调的方面。

首先,虽然这个提议并不容易将特定于上下文的错误文本添加到错误中,但它_确实_使得向错误添加堆栈帧错误跟踪信息变得容易:https: //play.golang.org/p /YL1MoqR08E6

其次, try可以说是解决https://github.com/golang/go/issues/19642 中大多数问题的公平解决方案。 以该问题为例,您可以使用try来避免每次都写出所有返回值。 当返回具有长名称的按值结构类型时,这也可能很有用。

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    i := f.cached.locations[xx+0]
    j := f.cached.locations[xx+1]
    if j < i {
        try(errInvalidGlyphDataLength)
    }
    if j-i > maxGlyphDataLength {
        try(errUnsupportedGlyphDataLength)
    }
    buf, err = b.view(&f.src, int(i), int(j-i))
    return buf, i, j - i, err
}

我也喜欢这个提议。

我有一个要求。

make一样,我们可以允许try接受可变数量的参数吗

  • 尝试(f):
    如上。
    返回错误值是强制性的(作为最后一个返回参数)。
    最常用的模型
  • 尝试(f,doPanic bool):
    如上,但如果 doPanic,则 panic(err) 而不是返回。
    在这种模式下,不需要返回错误值。
  • 尝试(f,fn):
    如上所述,但在返回之前调用 fn(err)。
    在这种模式下,不需要返回错误值。

这样,它是一个可以处理所有用例的内置程序,同时仍然是明确的。 它的优点:

  • 总是显式的——不需要推断是恐慌还是设置错误并返回
  • 支持特定于上下文的处理程序(但没有处理程序链)
  • 支持没有错误返回变量的用例
  • 支持 must(...) 语义

虽然重复的if err !=nil { return ... err }肯定是一个丑陋的口吃,但我同意那些
谁认为 try() 提案的可读性非常低并且有些含糊。
命名返回的使用也是有问题的。

如果需要这种整理,为什么不将try(err)作为语法糖
if err !=nil { return err }

file, err := os.Open("file.go")
try(err)

为了

file, err := os.Open("file.go")
if err != nil {
   return err
}

如果有多个返回值, try(err)可能是return t1, ... tn, err
其中 t1, ... tn 是其他返回值的零值。

这个建议可以避免命名返回值的需要,并且,
在我看来,更容易理解和更具可读性。

更好的是,我认为应该是:

file, try(err) := os.Open("file.go")

甚至

file, err? := os.Open("file.go")

最后一个是向后兼容的(标识符中目前不允许使用?)。

(此建议与 https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes 相关。但重复出现的主题示例似乎有所不同,因为那是在仍在讨论显式句柄而不是离开的阶段推迟。)

感谢 go 团队提出的这个细心、有趣的提议。

@rogpeppe评论如果try自动添加堆栈框架,而不是我,我可以不鼓励添加上下文。

@aarzilli - 所以根据你的建议,每次我们向tryf提供额外参数时,都必须使用 defer 子句吗?

如果我这样做会发生什么

try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)

并且不编写延迟函数?

@agnivade

如果我这样做 (...) 并且不编写延迟函数会发生什么?

类型检查错误。

在我看来,使用try来避免写出所有返回值实际上只是对它的又一次打击。

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    //...

我完全理解避免写出return nil, 0, 0, ErrNotFound的愿望,但我更愿意以其他方式解决这个问题。

try这个词并不意味着“返回”。 这就是它在这里的使用方式。 我实际上更希望提案更改,以便try不能直接获取error值,因为我不希望任何人编写这样的代码 ^^ 。 它读错了。 如果您向新手展示该代码,他们将不知道该尝试在做什么。

如果我们想要一种方法来轻松地返回默认值和错误值,让我们单独解决。 也许另一个内置的

return default(ErrNotFound)

至少这符合某种逻辑。

但是,我们不要滥用try来解决其他问题。

@natefinch如果try内置函数在原始提案中被命名为check ,那么它将是check(err) ,它读起来要好得多,imo。

撇开这一点不谈,我不知道写try(err)是否真的是一种滥用。 它完全超出了定义。 但是,另一方面,这也意味着这是合法的:

a, b := try(1, f(), err)

我想我对try的主要问题是它实际上只是一个panic只上升一个级别......除了不像恐慌,它是一个表达式,而不是一个语句,所以你可以隐藏它在某处的语句中间。 这几乎比恐慌更糟糕

@natefinch如果您将其概念化为一种恐慌,它会上升一个级别然后做其他事情,这似乎很混乱。 但是,我对它的概念不同。 Go 中返回错误的函数实际上是返回一个 Result, 粗略地借用 Rust 的术语。 try是一个实用程序,它解包结果,如果 error != nil 则返回“错误结果”,如果error != nil error == nil解包结果的 T 部分。

当然,在 Go 中我们实际上没有结果对象,但它实际上是相同的模式, try似乎是该模式的自然编码。 我相信这个问题的任何解决方案都必须对错误处理的某些方面进行编码,并且try对我来说似乎是合理的。 我和其他人建议稍微扩展try的功能以更好地适应现有的 Go 错误处理模式,但基本概念保持不变。

@ugorji您提出的try(f, bool)变体听起来像 #32219 中的must

@ugorji您提出的try(f, bool)变体听起来像 #32219 中的must

是的。 我只是觉得这三个案例都可以用一个单一的内置函数来处理,并优雅地满足所有用例。

由于try()已经很神奇,并且知道错误返回值,是否可以扩展它以在以空值(零参数)形式调用时也返回指向该值的指针? 这将消除命名返回的需要,而且我相信,这有助于在视觉上关联延迟语句中预期错误的来源。 例如:

func foo() error {
  defer fmt.HandleErrorf(try(), "important foo context info")
  try(bar())
  try(baz())
  try(etc())
}

@ugorji
我认为try(f, bool)上的布尔值会使其难以阅读且容易错过。 我喜欢你的提议,但对于恐慌的情况,我认为用户可以在第三个子弹的处理程序中编写它,例如try(f(), func(err error) { panic('at the disco'); }) ,这对于用户来说比隐藏的try(f(), true)更明确

@ugorji
我认为try(f, bool)上的布尔值会使其难以阅读且容易错过。 我喜欢你的提议,但对于恐慌的情况,我认为用户可以在第三个子弹的处理程序中编写它,例如try(f(), func(err error) { panic('at the disco'); }) ,这对于用户来说比隐藏的try(f(), true)更明确

进一步思考,我倾向于同意你的立场和推理,它仍然看起来像单线一样优雅。

@patrick-nyt 仍然是 _assignment syntax_ 触发 nil 测试的另一个支持者,在https://github.com/golang/go/issues/32437#issuecomment -499533464

这个概念出现在对检查/处理提案的 13 个单独的响应中
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

f, ?return := os.Open(...)
f, ?panic  := os.Open(...)

为什么? 因为它读起来像 Go 1,而try()check不是。

try的一个反对意见似乎是它是一个表达式。 假设有一个一元后缀语句?表示如果不是 nil 则返回。 这是标准代码示例(假设添加了我建议的延迟包):

func CopyFile(src, dst string) error {
    var err error // Don't need a named return because err is explicitly named
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r, err := os.Open(src)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    w, err := os.Create(dst)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    _, err = io.Copy(w, r)

    return err
}

pgStore 示例:

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    err?

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    // tricky bit: this would not change the value of err 
    // but the deferred.Cond would still be triggered by err being set before
    deferred.Format(err, "insert table")?

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    deferred.Format(err, "insert table2")?

    return tx.Commit()
}

我喜欢@jargv 的这个

由于 try() 已经很神奇,并且知道错误返回值,是否可以扩展它以在以空值(零参数)形式调用时也返回指向该值的指针? 这将消除命名回报的需要

但不是根据参数的数量重载名称try ,我认为可能有另一个内置的魔法,比如reterr或其他东西。

我已经简要介绍了一些经常使用的包,寻找“遭受”错误处理但在编写之前必须经过深思熟虑的 go 代码,试图弄清楚提议的 try() 会做什么“魔法”。
目前,除非我误解了该提议,否则其中许多(例如,不是超级基本错误处理)不会获得太多收益,或者必须保持“旧”错误处理风格。
来自 net/http/request.go 的示例:

func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
`

trace := httptrace.ContextClientTrace(r.Context())
if trace != nil && trace.WroteRequest != nil {
    defer func() {
        trace.WroteRequest(httptrace.WroteRequestInfo{
            Err: err,
        })
    }()
}

// Find the target host. Prefer the Host: header, but if that
// is not given, use the host from the request URL.
//
// Clean the host, in case it arrives with unexpected stuff in it.
host := cleanHost(r.Host)
if host == "" {
    if r.URL == nil {
        return errMissingHost
    }
    host = cleanHost(r.URL.Host)
}

// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
host = removeZone(host)

ruri := r.URL.RequestURI()
if usingProxy && r.URL.Scheme != "" && r.URL.Opaque == "" {
    ruri = r.URL.Scheme + "://" + host + ruri
} else if r.Method == "CONNECT" && r.URL.Path == "" {
    // CONNECT requests normally give just the host and port, not a full URL.
    ruri = host
    if r.URL.Opaque != "" {
        ruri = r.URL.Opaque
    }
}
if stringContainsCTLByte(ruri) {
    return errors.New("net/http: can't write control character in Request.URL")
}
// TODO: validate r.Method too? At least it's less likely to
// come from an attacker (more likely to be a constant in
// code).

// Wrap the writer in a bufio Writer if it's not already buffered.
// Don't always call NewWriter, as that forces a bytes.Buffer
// and other small bufio Writers to have a minimum 4k buffer
// size.
var bw *bufio.Writer
if _, ok := w.(io.ByteWriter); !ok {
    bw = bufio.NewWriter(w)
    w = bw
}

_, err = fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(r.Method, "GET"), ruri)
if err != nil {
    return err
}

// Header lines
_, err = fmt.Fprintf(w, "Host: %s\r\n", host)
if err != nil {
    return err
}
if trace != nil && trace.WroteHeaderField != nil {
    trace.WroteHeaderField("Host", []string{host})
}

// Use the defaultUserAgent unless the Header contains one, which
// may be blank to not send the header.
userAgent := defaultUserAgent
if r.Header.has("User-Agent") {
    userAgent = r.Header.Get("User-Agent")
}
if userAgent != "" {
    _, err = fmt.Fprintf(w, "User-Agent: %s\r\n", userAgent)
    if err != nil {
        return err
    }
    if trace != nil && trace.WroteHeaderField != nil {
        trace.WroteHeaderField("User-Agent", []string{userAgent})
    }
}

// Process Body,ContentLength,Close,Trailer
tw, err := newTransferWriter(r)
if err != nil {
    return err
}
err = tw.writeHeader(w, trace)
if err != nil {
    return err
}

err = r.Header.writeSubset(w, reqWriteExcludeHeader, trace)
if err != nil {
    return err
}

if extraHeaders != nil {
    err = extraHeaders.write(w, trace)
    if err != nil {
        return err
    }
}

_, err = io.WriteString(w, "\r\n")
if err != nil {
    return err
}

if trace != nil && trace.WroteHeaders != nil {
    trace.WroteHeaders()
}

// Flush and wait for 100-continue if expected.
if waitForContinue != nil {
    if bw, ok := w.(*bufio.Writer); ok {
        err = bw.Flush()
        if err != nil {
            return err
        }
    }
    if trace != nil && trace.Wait100Continue != nil {
        trace.Wait100Continue()
    }
    if !waitForContinue() {
        r.closeBody()
        return nil
    }
}

if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
    if err := bw.Flush(); err != nil {
        return err
    }
}

// Write body and trailer
err = tw.writeBody(w)
if err != nil {
    if tw.bodyReadError == err {
        err = requestBodyReadError{err}
    }
    return err
}

if bw != nil {
    return bw.Flush()
}
return nil

}
`

或用于彻底的测试,例如 pprof/profile/profile_test.go:
`
func checkAggregation(prof *Profile, a *aggTest) 错误 {
// 检查是否保留了行的样本总数。
总计 := int64(0)

samples := make(map[string]bool)
for _, sample := range prof.Sample {
    tb := locationHash(sample)
    samples[tb] = true
    total += sample.Value[0]
}

if total != totalSamples {
    return fmt.Errorf("sample total %d, want %d", total, totalSamples)
}

// Check the number of unique sample locations
if a.rows != len(samples) {
    return fmt.Errorf("number of samples %d, want %d", len(samples), a.rows)
}

// Check that all mappings have the right detail flags.
for _, m := range prof.Mapping {
    if m.HasFunctions != a.function {
        return fmt.Errorf("unexpected mapping.HasFunctions %v, want %v", m.HasFunctions, a.function)
    }
    if m.HasFilenames != a.fileline {
        return fmt.Errorf("unexpected mapping.HasFilenames %v, want %v", m.HasFilenames, a.fileline)
    }
    if m.HasLineNumbers != a.fileline {
        return fmt.Errorf("unexpected mapping.HasLineNumbers %v, want %v", m.HasLineNumbers, a.fileline)
    }
    if m.HasInlineFrames != a.inlineFrame {
        return fmt.Errorf("unexpected mapping.HasInlineFrames %v, want %v", m.HasInlineFrames, a.inlineFrame)
    }
}

// Check that aggregation has removed finer resolution data.
for _, l := range prof.Location {
    if !a.inlineFrame && len(l.Line) > 1 {
        return fmt.Errorf("found %d lines on location %d, want 1", len(l.Line), l.ID)
    }

    for _, ln := range l.Line {
        if !a.fileline && (ln.Function.Filename != "" || ln.Line != 0) {
            return fmt.Errorf("found line %s:%d on location %d, want :0",
                ln.Function.Filename, ln.Line, l.ID)
        }
        if !a.function && (ln.Function.Name != "") {
            return fmt.Errorf(`found file %s location %d, want ""`,
                ln.Function.Name, l.ID)
        }
    }
}

return nil

}
`
这是我能想到的两个例子,其中一个会说:“我想要一个更好的错误处理选项”

有人可以演示如何使用 try() 改进这些吗?

我主要赞成这个提议。

我与许多评论者分享的主要关注点是命名结果参数。 当前的提案当然鼓励更多地使用命名结果参数,我认为这是一个错误。 我不认为这只是提案所述的风格问题:命名结果是该语言的一个微妙特征,在许多情况下,它会使代码更容易出错或不太清晰。 在阅读和编写 Go 代码约 8 年之后,我真的只将命名结果参数用于两个目的:

  • 结果参数的文档
  • 在 defer 中操作结果值(通常是error

为了从一个新的方向来解决这个问题,我认为这个想法与设计文档或这个问题评论线程中讨论的任何内容都不太一致。 让我们称之为“错误延迟”:

允许使用 defer 调用带有隐式错误参数的函数。

所以如果你有一个功能

func f(err error, t1 T1, t2 T2, ..., tn Tn) error

然后,在最后一个结果参数类型error的函数g中(即,我使用try的任何函数),调用f可以延期如下:

func g() (R0, R0, ..., error) {
    defer f(t0, t1, ..., tn) // err is implicit
}

error-defer 的语义是:

  1. f的最后一个结果参数作为g f第一个输入参数来调用
  2. f仅在错误不为零时调用
  3. f的结果赋值给g的最后一个结果参数

因此,要使用旧错误处理设计文档中的示例,使用错误延迟和尝试,我们可以这样做

func printSum(a, b string) error {
    defer func(err error) error {
        return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
    }()
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x+y)
    return nil
}

HandleErrorf 的工作原理如下:

func printSum(a, b string) error {
    defer handleErrorf("printSum(%q + %q)", a, b)
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x+y)
    return nil
}

func handleErrorf(err error, format string, args ...interface{}) error {
    return fmt.Errorf(format+": %v", append(args, err)...)
}

需要解决的一个极端情况是如何处理我们使用哪种形式的延迟不明确的情况。 我认为这只发生在(非常不寻常的)具有这样签名的函数中:

func(error, ...error) error

可以合理地说这种情况是以非错误延迟方式处理的(并且这保持了向后兼容性)。


最近几天想了想这个想法,有点神奇,但是避免命名结果参数是对其有利的一大优势。 由于try鼓励更多地使用defer进行错误操作,因此可以扩展defer以更好地适应该目的是有道理的。 此外, try和 error-defer 之间存在一定的对称性。

最后,即使没有尝试,error-defers 在今天也很有用,因为它们取代了使用命名结果参数来处理错误返回。 例如,这里是一些真实代码的编辑版本:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) (_ []*File, err error) {
    files := make([]*file, len(keys))

    defer func() {
        if err != nil {
            // Return any successfully retrieved files.
            for _, f := range files {
                if f != nil {
                    c.put(f)
                }
            }
        }
    }()

    // ...
}

使用错误延迟,这变成:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) ([]*File, error) {
    files := make([]*file, len(keys))

    defer func(err error) error {
        // Return any successfully retrieved files.
        for _, f := range files {
            if f != nil {
                c.put(f)
            }
        }
        return err
    }()

    // ...
}

@beoran关于您的评论,我们应该等待泛型。 泛型在这里无济于事 - 请阅读常见问题解答

关于您对@velovix的 2 参数try的默认行为的建议:正如我之前所说,您对什么是明显合理的选择的想法是别人的噩梦。

我是否可以建议,一旦广泛的共识演变成带有显式错误处理程序的try比当前最小的try更好的主意,我们是否可以继续讨论。 在这一点上,讨论这种设计的细节是有意义的。

(就此而言,我确实喜欢有一个处理程序。这是我们早期的提议之一。如果我们按原样采用try ,我们仍然可以在前锋中使用一个处理程序的try - 兼容的方式 - 至少如果处理程序是可选的。但让我们一次迈出一步。)

@aarzilli感谢您的建议

只要装饰错误是可选的,人们就会倾向于不这样做(毕竟这是额外的工作)。 另请参阅我评论。

所以,我不认为建议的try _discourages_ 人从装饰错误(由于上述原因,即使使用if ,他们也已经气馁了); 是try没有_鼓励_它。

(鼓励它的一种方法是将其绑定到try :如果一个人也修饰错误,或者明确选择退出,则只能使用try 。)

但回到你的建议:我认为你在这里引入了更多的机器。 改变defer的语义只是为了让它更好地为try工作不是我们想要考虑的事情,除非这些defer更改以更一般的方式有益。 此外,您的建议将defertry $ 联系在一起,从而使这两种机制不那么正交; 我们想要避免的事情。

但更重要的是,我怀疑你会想强迫每个人都写一个defer以便他们可以使用try 。 但如果不这样做,我们又回到了第一个问题:人们会倾向于不修饰错误。

(就此而言,我确实喜欢有一个处理程序。这是我们早期的提议之一。如果我们按原样采用 try ,我们仍然可以以向前兼容的方式使用处理程序进行尝试 - 至少如果处理程序是可选的。但让我们一步一步来。)

当然,也许多步骤的方法是要走的路。 如果我们将来添加一个可选的处理程序参数,则可以创建工具来警告编写者未处理的try ,其精神与errcheck工具相同。 无论如何,我感谢您的反馈!

@alanfo感谢您的积极反馈

关于你提出的观点:

1)如果try的唯一问题是必须命名错误返回,以便我们可以通过defer修饰错误,我认为我们很好。 如果命名结果是一个真正的问题,我们可以解决它。 我能想到的一个简单机制是预先声明的变量,它是错误结果的别名(将其视为包含触发最近try的错误)。 可能会有更好的想法。 我们没有提出这个,因为语言中已经有一种机制,就是给结果命名。
2) try和测试:可以解决这个问题并使其发挥作用。 请参阅详细文档。
3)这在详细文档中明确解决。
4) 承认。

@benhoyt感谢您的积极反馈

如果反对这个提议的主要论点是try是内置的,那么我们就处于一个很好的位置。 使用内置只是向后兼容问题的实用解决方案(它恰好不会导致解析器和工具等的额外工作 - 但这只是一个很好的附带好处,而不是主要原因)。 必须写括号也有一些好处,这在设计文档中详细讨论(关于提议设计的属性部分)。

综上所述,如果使用内置插件,我们应该考虑try关键字。 它不会与现有代码向后兼容,因为关键字可能与现有标识符冲突。

(为了完整起见,还可以选择诸如?之类的运算符,这将是向后兼容的。不过,我认为它并不是 Go 等语言的最佳选择。但同样,如果这就是使try可口所需的全部,我们也许应该考虑一下。)

@ugorji感谢您的积极反馈

try可以扩展为采用额外的参数。 我们的偏好是只采用带有签名func (error) error的函数。 如果你想恐慌,很容易提供一个辅助函数:

func doPanic(err error) error { panic(err) }

最好保持try的设计简单。

@patrick-nyt 你的建议是什么:

file, err := os.Open("file.go")
try(err)

目前的提案将是可能的。

@dpinela@ugorji 另请阅读有关musttry主题的设计文档。 最好让try尽可能简单。 must是初始化表达式中的常见“模式”,但没有迫切需要“修复”它。

@jargv感谢您的建议。 这是一个有趣的想法(另请参阅我在主题上的评论)。 总结一下:

  • try(x)按建议运行
  • try()返回指向错误结果的*error

这确实是另一种无需命名即可获得结果的方法。

@cespare @jargv建议对我来说看起来比你提出的要简单得多。 它解决了访问结果错误的相同问题。 你怎么看?

根据https://github.com/golang/go/issues/32437#issuecomment -499320588:

func doPanic(错误错误)错误{恐慌(错误)}

我预计这个功能会很常见。 这可以在“内置”(或标准包中的其他地方,例如errors )中预定义吗?

太糟糕了,你没有预料到泛型足够强大来实现
试试看,我实际上希望有可能这样做。

是的,这个提议可能是第一步,虽然我看不出有多大用处
它自己现在的样子。

诚然,这个问题可能过于关注详细的替代方案,
但这表明许多参与者并不完全满意
它。 似乎缺乏的是对该提案的广泛共识......

操作 vr 6 月 7 日。 2019 01:04 schreef pj [email protected]

Asper #32437(评论)
https://github.com/golang/go/issues/32437#issuecomment-499320588

func doPanic(错误错误)错误{恐慌(错误)}

我预计这个功能会很常见。 这可以预定义吗
在“内置”中?


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAARM6OOOLLYO5ZCE6VVL2TPZGJWRA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXEMYZY#issuecomment-499698791
或使线程静音
https://github.com/notifications/unsubscribe-auth/AAARM6K5AOR2DES4QDTNLSTPZGJWRANCNFSM4HTGCZ7Q
.

@pjebs ,我已经写了几十次等效函数。 我通常称之为“orDie”或“check”。 它是如此简单,没有真正需要将其作为标准库的一部分。 另外,不同的人可能想要在终止之前进行日志记录或其他任何事情。

@beoran也许您可以扩展泛型和错误处理之间的联系。 当我想到它们时,它们似乎是两种不同的东西。 泛型并不是一个可以解决语言所有问题的包罗万象的方法。 它是编写可以对多种类型进行操作的单个函数的能力。

这个特定的错误处理提议试图通过引入一个预先声明的函数try来减少样板文件,该函数在某些情况下会更改流控制。 泛型永远不会改变控制流。 所以我真的看不出这种关系。

我对此的最初反应是👎,因为我想象在一个函数中处理几个容易出错的调用会使defer错误处理令人困惑。 在阅读了整个提案之后,我将我的反应转变为 ❤️ 和 👍,因为我了解到这仍然可以以相对较低的复杂性来实现

@carlmjohnson是的,这很简单,但是...

我已经写了几十次等效的函数。

预声明函数的优点是:

  1. 我们可以单行
  2. 我们不需要在我们使用的每个包中重新声明 err => panic 函数,或者为它维护一个公共位置。 由于可能对 Go 社区中的每个人都很常见,因此“标准包”是它_ _ 通用位置。

@griesemer使用原始 try 提议的错误处理程序变体,现在不再需要返回错误的总体功能要求。

当我第一次询问 err => panic 时,有人指出该提案考虑了它,但认为它太危险了(有充分的理由)。 但是,如果我们在总体函数不返回错误的情况下使用try()而没有错误处理程序,则将其变为编译时错误可以缓解提案中讨论的问题

@pjebs原始设计中不需要返回错误的总体功能要求_if_ 提供了错误处理程序。 但这只是try的另一个并发症。 _much_ 最好保持简单。 相反,有一个单独的must函数会更清楚,它总是在出错时恐慌(但其他方面就像try )。 然后很明显代码中发生了什么,并且不必查看上下文。

拥有这样一个must的主要吸引力在于它可以与单元测试一起使用; 特别是如果testing包被适当地调整以从must引起的恐慌中恢复并以一种很好的方式将它们报告为测试失败。 但是,当我们可以调整测试包以接受TestXxx(t *testing.T) error形式的测试功能时,为什么还要添加另一种新的语言机制呢? 如果他们返回一个错误,这似乎很自然(也许我们应该从一开始就这样做),那么try就可以正常工作。 本地测试需要更多的工作,但它可能是可行的。

must的另一个相对常见的用途是全局初始化表达式( must(regexp.Compile...等)。 如果将是“很高兴拥有”,但这并不一定会将其提高到新语言功能所需的水平。

@griesemer鉴于musttry模糊相关,并且考虑到try得到实施的势头,您认为考虑must不是很好吗

很有可能,如果在这一轮中没有讨论它,它就不会得到实施/认真考虑,至少在 3 年以上(或者可能永远)。 讨论中的重叠也将是好的,而不是从头开始和循环讨论。

许多人说must try

@pjebs现在似乎没有任何“实现try的势头”...... - 而且我们也只是在两天前发布了这个。 也没有任何决定。 让我们给这个时间。

我们并没有忘记musttry很好地吻合,但这与使其成为语言的一部分不同。 我们只是开始与更广泛的人群一起探索这个空间。 我们真的不知道会出现什么支持或反对它。 谢谢。

在花了几个小时阅读了所有的评论和详细的设计文档之后,我想在这个提案中添加我的观点。

我将尽我所能尊重@ianlancetaylor要求,不仅要重申以前的观点,还要在讨论中添加新的评论。 但是,我认为如果不参考先前的评论,我就无法发表新的评论。

关注点

不幸的延迟重载

defer的明显而直接的性质重载的偏好令人震惊。 如果我写的defer closeFile(f)对我来说是直截了当的,发生了什么以及为什么; 在将被调用的函数的末尾。 虽然将defer用于panic()recover()不太明显,但我很少使用它,并且在阅读其他人的代码时几乎从未见过它。

欺骗以重载defer以处理错误并不明显且令人困惑。 为什么是关键字deferdefer不是表示_“以后做”_而不是_“也许以后做?”_

Go 团队还提到了对defer性能的担忧。 鉴于此, defer被考虑用于_“热路径”_ 代码流似乎是双重不幸的。

没有验证重要用例的统计数据

正如@prologic 所提到的,这个try()提案是基于将使用此用例的大部分代码,还是基于试图安抚那些抱怨 Go 错误处理的人?

我希望我知道如何从我的代码库中为您提供统计信息,而无需详尽地查看每个文件并做笔记; 我不知道@prologic是如何做到的,尽管他很高兴。

但有趣的是,如果try()解决了我 5% 的用例,我会感到惊讶,并且怀疑它会解决不到 1% 的用例。 您确定其他人的结果大不相同吗? 您是否采用了标准库的一个子集并尝试了解如何应用它?

因为没有已知的统计数据表明这适用于大量的代码,所以我不得不问,这种新的复杂的语言变化是否需要每个人都学习新概念,真的解决了许多令人信服的用例?

使开发人员更容易忽略错误

这是对其他人的评论的完全重复,但基本上提供try()在许多方面类似于简单地将以下内容作为惯用代码,这是永远不会进入任何代码的代码- 尊重开发者船只:

f, _ := os.Open(filename)

我知道我可以在自己的代码中做得更好,但我也知道我们中的许多人依赖于发布一些非常有用的包的其他 Go 开发人员的慷慨,但是从我在_“其他人的代码(tm)”中看到的内容来看_错误处理的最佳实践经常被忽略。

说真的,我们真的想让开发人员更容易忽略错误并允许他们用非健壮的包污染 GitHub 吗?

可以(大部分)已经在用户空间中实现try()

除非我误解了这个提议——我可能会这样做——这里是在 userland 中实现的 Go Playground 中的try() ,尽管只有一 (1) 个返回值并返回一个接口而不是预期的类型:

package main

import (
    "errors"
    "fmt"
    "strings"
)
func main() {
    defer func() {
        r := recover()
        if r != nil && strings.HasPrefix(r.(string),"TRY:") {
            fmt.Printf("Ouch! %s",strings.TrimPrefix(r.(string),"TRY: "))
        }
    }()
    n := try(badjuju()).(int)
    fmt.Printf("Just chillin %dx!",n)   
}
func badjuju() (int,error) {
    return 10, errors.New("this is a really bad error")
}
func try(args ...interface{}) interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0]
}

因此,用户可以添加try2()try3()等等,具体取决于他们需要返回多少返回值。

但是 Go 只需要一 (1) 个简单但通用的语言功能,以允许想要try()的用户推出自己的支持,尽管仍然需要显式类型断言。 为 Go func添加 _(完全向后兼容)_ 功能以返回可变数量的返回值,例如:

func try(args ...interface{}) ...interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0:len(args)-2]
}

如果你首先解决泛型,那么类型断言甚至都不需要_(尽管我认为应该通过添加内置函数来解决泛型的用例而不是添加泛型的混淆语义和语法沙拉来减少泛型的用例来自 Java 等)_

缺乏明显性

在研究提案的代码时,我发现这种行为并不明显,而且有点难以推理。

当我看到try()包装了一个表达式时,如果返回错误会发生什么?

错误会被忽略吗? 或者它会跳转到第一个或最近的defer ,如果是这样,它会自动在闭包内设置一个名为err的变量,还是将它作为参数传递_(I没有看到参数?)_。 如果不是自动错误名称,我该如何命名? 这是否意味着我不能在我的函数中声明自己的err变量以避免冲突?

它会调用所有defer吗? 逆序还是正序?

或者它会从闭包和返回错误的func中返回吗? _(如果我没有在这里读到暗示这一点的话,我永远不会考虑的事情。)_

在阅读了提案和迄今为止的所有评论之后,老实说,我仍然不知道上述问题的答案。 这是我们想要添加到倡导者拥护_“Captain Obvious”的语言的那种功能吗?

缺乏控制

使用defer ,似乎开发人员唯一能获得的控制权就是分支到 _(最新的?)_ defer 。 但根据我对任何方法的经验,除了琐碎的func之外,它通常比这更复杂。

我经常发现在func内共享错误处理方面很有帮助——甚至在package内共享——但也可以在一个或多个其他包之间共享更具体的处理。

例如,我可以调用五 (5) func调用,它们从另一个func中返回一个error() #$ ; 让我们将它们标记为A()B()C()D()E() 。 我可能需要C()有自己的错误处理, A()B()D()E()来共享一些错误处理,和B()E()进行特定处理。

但我不相信通过这个提议可以做到这一点。 至少不容易。

然而具有讽刺意味的是,Go 已经具有允许高度灵活性的语言特性,而不必局限于一小部分用例。 func和闭包。 所以我的反问是:

_ “为什么我们不能对现有语言进行轻微的改进来解决这些用例,而不需要添加新的内置函数或接受令人困惑的语义?” _

这是一个反问,因为我计划提交一个提案作为替代方案,这是我在研究该提案并考虑其所有缺点时构想的。

但我离题了,这将在稍后进行,此评论是关于为什么需要重新考虑当前提案。

缺乏对break的明确支持

这可能感觉它来自左侧字段,因为大多数人使用早期返回来进行错误处理,但我发现最好使用break进行错误处理,在return之前包装大部分或全部 func

我已经使用这种方法一段时间了,它在简化重构方面的好处使得它比早期return更可取,但它还有其他几个好处,包括单个退出点和能够提前终止一段 func 但仍然可以能够运行清理_(这可能是我很少使用defer的原因,我发现在程序流程方面更难推理。)_

要使用break而不是提前返回,请使用for range "1" {...}循环创建一个块,以便从 _ 退出中断(我实际上创建了一个名为only的包,它只包含一个常量名为Once ,值为"1" ):_

func (me *Config) WriteFile() (err error) {
    for range only.Once {
        var j []byte
        j, err = json.MarshalIndent(me, "", "    ")
        if err != nil {
            err = fmt.Errorf("unable to marshal config; %s", 
                err.Error(),
            )
            break
        }
        err = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to make directory'%s'; %s", 
                me.GetDir(), 
                err.Error(),
            )
            break
        }
        err = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to write to config file '%s'; %s", 
                me.GetFilepath(), 
                err.Error(),
            )
            break
        }
    }
    return err
}

我计划在不久的将来详细介绍这种模式,并讨论为什么我发现它比早期回报更有效的几个原因。

但我离题了。 我在这里提出它的原因是我必须让 Go 实现错误处理,假设早期return s 并忽略使用break进行错误处理

我的意见err == nil是有问题的

作为进一步的题外话,我想提出我对 Go 中惯用错误处理的担忧。 虽然我非常相信 Go 的哲学,即在错误发生时处理错误与使用异常处理,但我觉得使用nil表示没有错误是有问题的,因为我经常发现我想从一个例程——用于 API 响应——不仅在出现错误时返回非零值。

所以对于 Go 2,我真的很想看到 Go 考虑添加一个新的内置类型status和三个内置函数iserror()iswarning()issuccess()status可以实现error — 允许向后兼容,并且传递给issuccess()nil值将返回true — 但status将有一个额外的错误级别内部状态,因此错误级别的测试将始终使用内置函数之一完成,理想情况下永远不要使用nil检查。 这将允许采用以下方法:

func (me *Config) WriteFile() (sts status) {
    for range only.Once {
        var j []byte
        j, sts = json.MarshalIndent(me, "", "    ")
        if iserror(sts) {
            sts.AddMessage("unable to marshal config")
            break
        }
        sts = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to make directory'%s'", me.GetDir())
            break
        }
        sts = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to write to config file '%s'", 
                me.GetFilepath(), 
            )
            break
        }
        sts = fmt.Status("config file written")
    }
    return sts
}

我已经在测试前级别的当前内部使用包中使用了用户态方法,该包类似于上面的错误处理。 坦率地说,与我尝试遵循惯用的 Go 错误处理时相比,在使用这种方法时考虑如何构建代码的时间要少得多。

如果您认为有任何机会将惯用的 Go 代码演变为这种方法,请在实施错误处理时将其考虑在内,包括在考虑此try()提案时。

_“不适合所有人”_理由

Go 团队的主要回应之一是 _“同样,该提案并未尝试解决所有错误处理情况。”_
从治理的角度来看,这可能是最令人担忧的问题。

这种需要每个人都学习新概念的语言的新复杂变化真的解决了大量令人信服的用例吗?

这难道不是核心团队成员拒绝社区众多功能请求的同一理由吗? 以下是 Go 团队成员在对大约 2 年前提交的功能请求的原型回复中发表的评论的直接引用 _(我没有命名此人或特定功能请求,因为此讨论不应该能够人,而不是语言):_

_“一种新的语言特性需要引人注目的用例。所有语言特性都是有用的,否则没有人会提出它们;问题是:它们是否有用到足以证明使语言复杂化并要求每个人都学习新概念是合理的?有什么引人注目的用途这里的案例?人们将如何使用这些?例如,人们是否希望能够......如果是这样,他们将如何做到这一点?这个提议是否不仅仅让你......?
— Go 团队的核心成员

坦率地说,当我看到这些回复时,我有两种感觉之一:

  1. 如果这是我同意的功能,则表示愤慨,或者
  2. 如果这是我不同意的功能,我会很高兴。

但在任何一种情况下,我的感受都是/无关紧要的; 我理解并同意 Go 是我们许多人选择开发的语言的部分原因是出于对语言纯度的嫉妒保护。

这就是为什么这个提议让我如此困扰的原因,因为 Go 核心团队似乎正在深入研究这个提议,就像一个教条式地想要一个 Go 社区绝对不会容忍的深奥功能的人一样。

_(我真的希望团队不会向信使开枪,并将其视为来自希望看到 Go 继续成为我们所有人最好的人的建设性批评,因为我将不得不被视为“不受欢迎的人”核心团队。)_

如果需要一组令人信服的真实世界用例是所有社区生成的功能提案的标准,那么它不应该也是_所有_ 功能提案的标准吗?

try() 的嵌套

这也被一些人所覆盖,但我想在try()和对三元运算符的持续请求之间进行比较。 引用大约 18 个月前另一位 Go 团队成员的评论:

_“当“大型编程”(大型团队长时间使用大型代码库)时,代码的读取频率比编写代码的频率高,因此我们优化可读性,而不是可写性。”_

不添加三元运算符的主要原因之一是嵌套时它们难以阅读和/或容易误读。 然而,像try(try(try(to()).parse().this)).easily())这样的嵌套try()语句也是如此。

反对三元运算符的其他原因是它们是_“表达式”_,其中嵌套表达式会增加复杂性。 但是try()不也创建一个可嵌套的表达式吗?

现在这里有人说 _“我认为像 [nested try() s] 这样的例子是不切实际的”_ 并且该声明没有受到挑战。

但是,如果人们接受开发人员不会嵌套try()的假设,那么当人们说_“我认为深度嵌套的三元运算符不现实?”时,为什么不尊重三元运算符?

对于这一点,我认为如果反对三元运算符的论点真的有效,那么它们也应该被视为反对这个try()提案的有效论点。

总之

在撰写本文时, 58%反对票对42%赞成票。 我认为仅此一项就足以表明,这是一个足以引起分歧的提案,是时候在这个问题上重新制定计划了。

关注

PS 说得更诙谐,我认为我们应该遵循尤达的意译智慧:

_"没有try() 。只有do() 。"_

@ianlancetaylor

@beoran也许您可以扩展泛型和错误处理之间的联系。

不代表@beoran ,但在我几分钟前的评论中,您会看到如果我们有泛型_(加上可变参数返回参数)_,那么我们可以构建自己的try()

然而——我将在这里重复我上面所说的关于泛型的内容,这样更容易看到:

_" 我认为应该通过添加内置函数来解决泛型的用例来减少泛型的用例,而不是添加来自 Java 等的泛型的令人困惑的语义和语法沙拉)"_

@ianlancetaylor

在尝试为您的问题制定答案时,我尝试按原样在 Go 中实现try函数,令我高兴的是,实际上已经可以模拟非常相似的东西:

func try(v interface{}, err error) interface{} {
   if err != nil { 
     panic(err)
   }
   return v
}

在这里查看如何使用它:https: //play.golang.org/p/Kq9Q0hZHlXL

这种方法的缺点是:

  1. 需要延迟救援,但在此提案中使用try ,如果我们想要进行适当的错误处理,还需要延迟处理程序。 所以我觉得这不是一个严重的缺点。 如果 Go 有某种super(arg1, ..., argn)内置函数会更好如果你愿意的话。
  2. 我实现的这个try只能与返回单个结果和错误的函数一起使用。
  3. 您必须键入 assert 返回的 emtpy 接口结果。

足够强大的泛型可以解决问题 2 和 3,只剩下 1,这可以通过添加super()来解决。 有了这两个功能,我们可以得到类似的东西:

func (T ... interface{})try(T, err error) super {
   if err != nil { 
      super(err)
   }
  super(T...)
}

这样就不再需要延迟的救援了。 即使没有向 Go 添加泛型,这种好处也将可用。

实际上,这个 super() 内置函数的想法是如此强大和有趣,我可能会单独发布一个提案。

@beoran很高兴看到我们在用户空间中独立实现try()时遇到了完全相同的限制,除了我没有包括的超级部分,因为我想在替代提案中讨论类似的东西。 :-)

我喜欢这个提议,但事实上你必须明确指定defer try(...)go try(...)是不允许的,这让我觉得有些事情不太正确......正交性是一个很好的设计指南。 关于进一步阅读和看到类似的东西
x = try(foo(...)) y = try(bar(...))
我想知道是否可能是try需要是一个上下文! 考虑:
try ( x = foo(...) y = bar(...) )
这里foo()bar()返回两个值,第二个是error 。 尝试语义只对try块内的调用很重要,其中返回的错误值被省略(没有接收者)而不是被忽略(接收者是_ )。 您甚至可以处理foobar调用之间的一些错误。

概括:
a) 由于语法原因, godefer的不允许try的问题消失了。
b) 可以排除多个功能的错误处理。
c) 它的神奇本质用特殊语法比用函数调用更好地表达。

如果 try 是一个上下文,那么我们刚刚制作了我们特别试图避免的 try/catch 块(并且有充分的理由)

没有问题。 将生成与当前提案完全相同的代码
x = try(foo(...)) y = try(bar(...))
这只是不同的语法,而不是语义。
````

我想我对它做了一些我不应该做的假设,尽管它仍然存在一些缺点。

如果 foo 或 bar 不返回错误怎么办,它们也可以放在 try 上下文中吗? 如果不是,那么在错误函数和非错误函数之间切换似乎有点难看,如果可以,那么我们回到旧语言中的 try 块问题。

第二件事是,通常keyword ( ... )语法意味着您在每行添加关键字前缀。 因此对于 import、var、const 等:每一行都以关键字开头。 对该规则做出例外似乎不是一个好的决定

使用特殊标识符而不是使用函数会更惯用吗?

我们已经有了忽略值的空白标识符_
我们可以有类似#的东西,它只能用在最后一个返回值为 error 的函数中。

func foo() (error) {
    f, # := os.Open()
    defer f.Close()
    _, # = f.WriteString("foo")
    return nil
}

当错误分配给#时,函数会立即返回收到的错误。 至于其他变量,它们的值将是:

  • 如果它们没有被命名为零值
  • 分配给命名变量的值,否则

@deanvelopertry块语义仅对返回错误值且未分配错误值的函数很重要。 所以本提案的最后一个例子也可以写成
try(x = foo(...)) try(y = bar(...))
将两个语句放在同一个块中类似于我们对重复的importconstvar语句所做的操作。

现在,如果你有,例如
try( x = foo(...)) go zee(...) defer fum() y = bar(...) )
这相当于写
try(x = foo(...)) go zee(...) defer fum() try(y = bar(...))
在一个 try 块中考虑所有这些可以减少它的忙碌。

考虑
try(x = foo())
如果 foo() 没有返回错误值,这相当于
x = foo()

考虑
try(f, _ := os.open(filename))
由于返回的错误值被忽略,这相当于只是
f, _ := os.open(filename)

考虑
try(f, err := os.open(filename))
由于返回的错误值没有被忽略,这相当于
f, err := os.open(filename) if err != nil { return ..., err }
正如提案中目前规定的那样。

而且它也很好地整理了嵌套尝试!

这是我上面提到的替代提案的链接:

它要求添加两 (2) 个小型但通用的语言功能,以解决与try()相同的用例

  1. 能够在赋值语句中调用func /closure。
  2. 能够breakcontinuereturn超过一级。

有了这两个特性,它们就没有“魔法”了,我相信它们的使用会产生更容易理解的 Go 代码,并且更符合我们都熟悉的惯用 Go 代码。

我已经阅读了该提案,并且非常喜欢尝试的发展方向。

考虑到尝试的流行程度,我想知道将其设置为更默认的行为是否会使其更易于处理。

考虑地图。 这是有效的:

v := m[key]

就像这样:

v, ok := m[key]

如果我们完全按照 try 建议的方式处理错误,但删除内置函数会怎样。 所以如果我们开始:

v, err := fn()

而不是写:

v := try(fn())

我们可以改为:

v := fn()

当 err 值未被捕获时,它的处理方式与 try 完全相同。 需要一点时间来适应,但感觉与v, ok := m[key]v, ok := x.(string)非常相似。 基本上,任何未处理的错误都会导致函数返回并设置错误值。

回到设计文档的结论和实现要求:

• 保留语言语法,不引入新关键字
• 它仍然是像try 一样的语法糖,希望很容易解释。
• 不需要新语法
• 它应该完全向后兼容。

我想这将具有与 try 几乎相同的实现要求,因为主要区别不是内置触发语法糖,现在是缺少 err 字段。

因此,使用提案中的CopyFile示例以及defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) ,我们得到:

func CopyFile(src, dst string) (err error) {
        defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

        r := os.Open(src)
        defer r.Close()

        w := os.Create(dst)
        defer func() {
                err := w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        io.Copy(w, r)
        w.Close()
        return nil
}

@savaki我喜欢这个并且正在考虑如何通过默认情况下始终处理错误并让程序员指定何时不这样做(通过将错误捕获到变量中)来使 Go 翻转错误处理,但完全没有任何标识符会使代码难以遵循,因为无法看到所有返回点。 可能是命名可能以不同方式返回错误的函数的约定(例如大写公共标识符)。 可能是如果函数返回错误,它必须始终以?结尾。 然后 Go 总是可以隐式处理错误并像 try 一样自动将其返回给调用函数。 这使得它与一些建议使用?标识符而不是 try 的建议非常相似,但重要的区别是这里?将是函数名称的一部分,而不是附加标识符。 实际上,如果没有后缀? ,返回error作为最后一个返回值的函数甚至不会编译。 当然?是任意的,可以用任何其他使意图更明确的东西来代替。 operation?()将等效于包装try(someFunc())?将是函数名称的一部分,其唯一目的是表明该函数可以像大写一样返回错误变量的第一个字母。

这表面上最终与其他要求将try替换为?的提案非常相似,但一个关键的区别是它使错误处理隐式(自动),而不是使忽略(或包装)错误显式无论如何,这是一种最佳实践。 当然,最明显的问题是它不向后兼容,我相信还有更多。

也就是说,我很想看看 Go 如何通过自动化处理默认/隐式情况的错误,并让程序员编写一些额外的代码来忽略/覆盖处理。 我认为的挑战是如何在这种情况下使所有返回点变得明显,因为如果没有它,错误将变得更像异常,因为它们可能来自任何地方,因为程序的流程不会使其变得明显。 可以说,用视觉指示器隐含错误与实现try并使errcheck编译器失败是一样的。

我们可以用装饰器为旧函数做一些类似 c++ 异常的事情吗?

func some_old_test() (int, error){
    return 0, errors.New("err1")
}
func some_new_test() (int){
        if true {
             return 1
        }
    throw errors.New("err2")
}
func throw_res(int, e error) int {
    if e != nil {
        throw e
    }
    return int
}
func main() {
    fmt.Println("Hello, playground")
    try{
        i := throw_res(some_old_test())
        fmt.Println("i=", i + some_new_test())
    } catch(err io.Error) {
        return err
    } catch(err error) {
        fmt.Println("unknown err", err)
    }
}

@owais我在想语义将与 try 完全相同,因此至少您需要声明 err 类型。 所以如果我们开始:

func foo() error {
  _, err := fn() 
  if err != nil {
    return err
  }

  return nil
} 

如果我理解 try 建议,只需执行以下操作:

func foo() error {
  _  := fn() 
  return nil
} 

不会编译。 一个不错的好处是它使编译有机会告诉用户缺少什么。 大意是使用隐式错误处理需要将错误返回类型命名为 err。

那么,这将起作用:

func foo() (err error) {
  _  := fn() 
  return nil
} 

为什么不只处理未分配给变量的错误的情况。

  • 消除对命名返回的需求,编译器可以自己完成这一切。
  • 允许添加上下文。
  • 处理常见的用例。
  • 向后兼容
  • 不会与延迟、循环或开关进行奇怪的交互。

if err != nil 情况下的隐式返回,如果程序员无法访问,编译器可以为返回生成局部变量名称。
从代码可读性的角度来看,我个人不喜欢这种特殊情况

f := os.Open("foo.txt")

更喜欢显式返回,遵循代码比书面口头禅

f := os.Open("foo.txt") else return

有趣的是,我们可以接受这两种形式,并让 gofmt 自动添加 else 返回。

添加上下文,也是变量的本地命名。 return 变得明确,因为我们想添加上下文。

f := os.Open("foo.txt") else err {
  return errors.Wrap(err, "some context")
}

添加具有多个返回值的上下文

f := os.Open("foo.txt") else err {
  return i, j, errors.Wrap(err, "some context")
}

嵌套函数要求外部函数以相同的顺序处理所有结果
减去最终误差。

bits := ioutil.ReadAll(os.Open("foo")) else err {
  // either error ends up here.
  return i, j, errors.Wrap(err, "some context")
}

由于函数中缺少错误返回值,编译器拒绝编译

func foo(s string) int {
   i := strconv.Atoi(s) // cannot implicitly return error due to missing error return value for foo.
   return i * 2
}

愉快地编译,因为错误被明确忽略。

func foo(s string) int {
   i, _ := strconv.Atoi(s)
   return i * 2
} 

编译器很高兴。 它会像当前一样忽略错误,因为没有分配或后缀发生。

func foo() error {
  return errors.New("whoops")
}

func bar() {
  foo()
}

在循环中,您可以使用 continue。

for _, s := range []string{"1","2","3","4","5","6"} {
  i := strconv.Atoi(s) else continue
}

编辑:将;替换为else

@savaki我想我理解了你的原始评论,我喜欢 Go 默认处理错误的想法,但我认为如果不添加一些额外的语法更改是不可行的,一旦我们这样做,它就会与当前的提议惊人地相似。

您提出的最大缺点是它没有公开函数可以返回的所有点,这与当前的if err != nil {return err}或此提案中引入的 try 函数不同。 即使它在引擎盖下的功能完全相同,但在视觉上代码看起来会非常不同。 阅读代码时,无法知道哪些函数调用可能会返回错误。 这最终会比 IMO 例外情况更糟糕。

如果编译器强制对可能返回错误的函数进行一些语义约定,则错误处理可能会被隐式处理。 就像他们必须以某个短语或字符开头或结尾一样。 这将使所有返回点都非常明显,我认为它比手动错误处理要好,但不确定考虑到已经有 lint 检查在发现错误被忽略时会发出负载,这会好得多。 看看编译器是否可以根据函数是否返回可能的错误来强制以某种方式命名函数,这将是非常有趣的。

这种方法的主要缺点是需要命名错误结果参数,这可能导致 API 不太美观(但请参阅有关此主题的常见问题解答)。 我们相信,一旦这种风格建立起来,我们就会习惯它。

不确定以前是否有人建议过这样的事情,在这里或提案中找不到。 您是否考虑过另一个返回指向当前函数错误返回值的指针的内置函数?
例如:

func example() error {
        var err *error = funcerror() // always return a non-nil pointer
        fmt.Print(*err) // always nil if the return parameters are not named and not in a defer

        defer func() {
                err := funcerror()
                fmt.Print(*err) // "x"
        }

        return errors.New("x")
}
func exampleNamed() (err error) {
        funcErr := funcerror()
        fmt.Print(*funcErr) // "nil"

        err = errors.New("x")
        fmt.Print(*funcErr) // "x", named return parameter is reflected even before return is called

        *funcErr = errors.New("y")
        fmt.Print(err) // "y", unfortunate side effect?

        defer func() {
                funcErr := funcerror()
                fmt.Print(*funcErr) // "z"
                fmt.Print(err) // "z"
        }

        return errors.New("z")
}

尝试使用:

func CopyFile(src, dst string) (error) {
        defer func() {
                err := funcerror()
                if *err != nil {
                        *err = fmt.Errorf("copy %s %s: %v", src, dst, err)
                }
        }()
        // one liner alternative
        // defer fmt.HandleErrorf(funcerror(), "copy %s %s", src, dst)

        r := try(os.Open(src))
        defer r.Close()

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                err := funcerror()
                if *err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try(io.Copy(w, r))
        try(w.Close())
        return nil
}

或者,如果没有在 defer 中调用 funcerror (名称是一个正在进行的工作 :D )可能会返回 nil。

另一种选择是 funcerror 返回一个“Errorer”接口以使其只读:

type interface Errorer() {
        Error() error
}

@savaki我实际上确实喜欢您的建议,即省略try()并让它更像是测试地图或类型断言。 这感觉更像_“Go-like。”_

但是,我仍然看到一个明显的问题,那就是您的建议假定使用这种方法的所有错误都会触发return并离开该功能。 它没有考虑的是从当前的for中发出break #$ 或为当前的for continue

早期的return是一把大锤,很多时候手术刀是更好的选择。

所以我断言breakcontinue应该被允许是有效的错误处理策略,目前你的提议只假定returntry()假定或调用错误处理程序本身只能return ,而不是breakcontinue

看起来 savaki 和我有类似的想法,如果需要,我只是添加了块语义来处理错误。 例如添加 comtext,用于要短路的循环等

@mikeschinkel看到我的扩展,他和我有类似的想法,我只是用一个可选的块语句扩展它

@詹姆斯-劳伦斯

@mikesckinkel看到我的扩展,他和我有相似的想法,我只是用一个可选的块语句扩展它

以你为例:

f := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}

与我们今天所做的相比:

f,err := os.Open("foo.txt"); 
if err != nil {
  return errors.Wrap(err, "some context")
}

绝对比我更可取。 除了它有几个问题:

  1. err似乎是_“神奇地”_声明的。 魔法应该被最小化,不是吗? 所以让我们声明它:
f, err := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}
  1. 但这仍然行不通,因为 Go 不会将nil值解释为false也不会将指针值解释为true ,所以它需要是:
f, err := os.Open("foo.txt"); err != nil {
  return errors.Wrap(err, "some context")
}

什么是有效的,它开始感觉就像一行一样多的工作和大量的语法,所以为了清晰起见,我可能会继续使用旧方法。

但是如果 Go 添加了两 (2) 个内置函数呢? iserror()error() ? 然后我们可以这样做,这对我来说并没有那么糟糕:

f := os.Open("foo.txt"); iserror() {
  return errors.Wrap(error(), "some context")
}

或者更好_(类似):_

f := os.Open("foo.txt"); iserror() {
  return error().Extend("some context")
}

你和其他人怎么看?

顺便说一句,检查我的用户名拼写。 如果我不注意的话,我就不会收到你提到的通知......

@mikeschinkel对我在手机上的名字感到抱歉,而 github 没有自动建议。

err 似乎是“神奇地”声明的。 魔法应该被最小化,不是吗? 所以让我们声明它:

嗯,自动插入返回的整个想法很神奇。 这几乎不是整个提案中最神奇的事情。 另外,我认为错误已被宣布; 就在作用域块上下文的最后,防止它污染父作用域,同时仍然保持我们使用 if 语句通常得到的所有好东西。

对于即将添加到错误包中的 go 错误处理,我通常非常满意。 我认为该提案中的任何内容都不是超级有用的。 如果我们下定决心,我只是试图为 golang 提供最自然的匹配。

_“自动插入返回的整个想法很神奇。”_

你不会从我那里得到任何争论。

_“这几乎不是整个提案中最神奇的事情。”_

我想我试图争辩说_“所有魔法都是有问题的。”_

_“另外我认为错误已被声明;就在作用域块的上下文中的最后......”_

因此,如果我想将其命名为err2这也可以吗?

f := os.Open("foo.txt"); err2 {
  return errors.Wrap(err, "some context")
}

因此,我假设您还建议对分号后的err / err2进行特殊处理,即假定它是nil或不是nil而不是像查看地图时的bool

if _,ok := m[a]; !ok {
   print("there is no 'a' in 'm'")
}

对于即将添加到错误包中的 go 错误处理,我通常非常满意。

当与breakcontinue _( 但不是return结合使用时,我也对错误处理感到满意。)_

事实上,我认为这个try()提案有害多于有用,并且宁愿看到这个实施方案。 #jmtcw。

@beoran @mikeschinkel早些时候我建议我们不能使用泛型实现这个版本的try ,因为它改变了控制流。 如果我没看错,你们都建议我们可以通过调用panic来使用泛型来实现try #$ 。 但是这个版本的try非常明确地没有panic 。 所以我们不能使用泛型来实现这个版本的try

是的,我们可以使用泛型(泛型版本比 https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md 中的设计草案更强大)来编写函数对错误感到恐慌。 但是对错误感到恐慌并不是 Go 程序员今天写的那种错误处理,而且对我来说这似乎不是一个好主意。

@mikeschinkel特殊处理是该块仅在出现错误时执行。
```
f := os.Open('foo'); err { return err } // err 在这里总是非零。

@ianlancetaylor

_“是的,我们可以使用泛型......但是对错误感到恐慌并不是 Go 程序员今天编写的那种错误处理,对我来说这似乎不是一个好主意。”_

我实际上非常同意你的观点,因此看来你可能误解了我评论的意图。 我根本没有建议 Go 团队使用panic()来实现错误处理——当然不是。

相反,我试图从您过去对其他问题的许多评论中实际效仿您,并建议我们避免对 Go 进行任何并非绝对必要的更改,因为它们在 userland 中是可能的。 所以 _if_ 泛型被解决了 _then_ 想要try()的人实际上可以自己实现它,尽管通过利用panic() 。 这将是团队需要为 Go 添加和记录的一项功能。

没有做的——也许这不是很清楚——提倡人们实际上使用panic()来实现try() ,只要他们真的愿意,他们就可以这样做,并且他们具有以下特性仿制药。

这说明清楚了吗?

对我来说,调用panictry的提案完全不同。 因此,尽管我认为我理解您在说什么,但我不同意它们是等效的。 即使我们有足够强大的泛型来实现一个恐慌的try版本,我认为对于这个提案中提出的try版本仍然有合理的需求。

@ianlancetaylor 已确认。 同样,我在寻找不需要添加try()的原因,而不是找到添加它的方法。 正如我上面所说,我宁愿没有任何新的错误处理,也不愿有这里建议的try()

我个人更喜欢早期的check提案,基于纯粹的视觉方面; checktry() #$ 具有相同的功能,但bar(check foo())对我来说比bar(try(foo()))更具可读性(我只需要一秒钟来计算括号!)。

更重要的是,我对handle / check的主要抱怨是它不允许以不同的方式包装个人支票——现在这个try()提案有同样的缺陷,同时调用延迟和命名返回的棘手的很少使用的新手混淆功能。 至少有了handle ,我们可以选择使用作用域来定义句柄块,而使用defer甚至这是不可能的。

就我而言,这个提案在各个方面都输给了之前的handle / check提案。

这是使用 defers 进行错误处理的另一个问题。

try是函数的受控/有意退出。 defers 始终运行,包括函数的不受控制/意外退出。 这种不匹配可能会导致混乱。 这是一个想象的场景:

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

回想一下 net/http 从恐慌中恢复,并想象围绕恐慌调试生产问题。 您会查看您的仪器,并从recordMetric调用中看到数据库调用失败的峰值。 这可能掩盖了真正的问题,即下一行中的恐慌。

我不确定这在实践中有多严重,但(遗憾的是)可能是另一个理由认为 defer 不是错误处理的理想机制。

以下是可能有助于解决所提出的一些问题的修改:将try视为goto而不是return 。 听我说。 :)

try将改为语法糖:

t1, … tn, te := f()  // t1, … tn, te are local (invisible) temporaries
if te != nil {
        err = te   // assign te to the error result parameter
        goto error // goto "error" label
}
x1, … xn = t1, … tn  // assignment only if there was no error

好处:

  • defer不需要修饰错误。 (不过,仍然需要命名返回。)
  • error:标签的存在是函数中某处存在try的视觉线索。

这也提供了一种添加处理程序的机制,可以避免处理程序作为函数的问题:使用标签作为处理程序。 try(fn(), wrap)goto wrap而不是goto error 。 编译器可以确认函数中存在wrap: 。 请注意,拥有处理程序也有助于调试:您可以添加/更改处理程序以提供调试路径。

示例代码:

func CopyFile(src, dst string) (err error) {
    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “try” fails
        }
    }()

    try(io.Copy(w, r), copyfail)
    try(w.Close())
    return nil

error:
    return fmt.Errorf("copy %s %s: %v", src, dst, err)

copyfail:
    recordMetric("copy failure") // count incidents of this failure
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

其他的建议:

  • 我们可能希望要求用作try目标的任何标签前面都有一个终止语句。 在实践中,这将迫使他们结束函数,并可能阻止一些意大利面条代码。 另一方面,它可能会阻止一些合理、有用的用途。
  • try可用于创建循环。 我认为这属于“如果疼就不要做”的旗帜,但我不确定。
  • 这需要修复https://github.com/golang/go/issues/26058。

信用:我相信这个想法的一个变体是@griesemer 去年在GopherCon亲自提出的。

@josharian考虑与panic的交互在这里很重要,我很高兴你提出来,但你的例子对我来说似乎很奇怪。 在下面的代码中,延迟总是记录"db call failed"指标对我来说没有意义。 如果someHTTPHandlerGuts成功并返回nil ,这将是一个错误的指标。 defer在所有退出情况下运行,而不仅仅是错误或恐慌情况,因此即使没有恐慌,代码也似乎是错误的。

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

@josharian是的,这或多或少正是我们去年讨论的版本(除了我们使用check而不是try )。 我认为,一旦我们处于error标签处,就不能“跳回”到函数体的其余部分,这一点至关重要。 这将确保goto有点“结构化”(不可能有意大利面条代码)。 提出的一个问题是错误处理程序( error: )标签总是会出现在函数的末尾(否则必须以某种方式绕过它)。 就个人而言,我喜欢错误处理代码(最后),但其他人认为它应该在一开始就可见。

@mikeshenkel我认为从循环返回是一个加号而不是负面的。 我的猜测是,这将鼓励开发人员要么使用单独的函数来处理循环的内容,要么像我们目前所做的那样显式使用 err。 这两个对我来说似乎都是不错的结果。

从我的 POV 来看,我不觉得这种 try 语法必须处理每个用例,就像我觉得我不需要使用

V,好的:= m[键]

从地图读取形成

您可以通过以简化形式恢复handle / check提议来避免 goto 标签强制处理程序到函数末尾。 如果我们使用handle err { ... }语法但只是不让处理程序链接,而是只使用最后一个会怎样。 它大大简化了该提议,并且与 goto 的想法非常相似,除了它使处理更接近使用点。

func CopyFile(src, dst string) (err error) {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “check” fails
        }
    }()

    {
        // handlers are scoped, after this scope the original handle is used again.
        // as an alternative, we could have repeated the first handle after the io.Copy,
        // or come up with a syntax to name the handlers, though that's often not useful.
        handle err {
            recordMetric("copy failure") // count incidents of this failure
            return fmt.Errorf("copy %s %s: %v", src, dst, err)
        }
        check io.Copy(w, r)
    }
    check w.Close()
    return nil
}

作为奖励,这有一条让处理程序链接的未来路径,因为所有现有的使用都会有回报。

@josharian @griesemer如果您引入命名处理程序(许多对检查/处理请求的响应,请参阅重复主题),有语法选项比try(f(), err)更可取:

try.err f()
try?err f()
try#err f()

?err    f() // because 'try' is redundant
?return f() // no handler
?panic  f() // no handler

(?err f()).method()

f?err() // lead with function name, instead of error handling
f?err().method()

file, ?err := os.Open(...) // many check/handle responses also requested this style

我最喜欢 Go 的一件事是它的语法相对没有标点符号,并且可以大声朗读而不会出现大问题。 我真的很讨厌 Go 最终成为$#@!perl

对我来说,将“尝试”作为内置功能并启用链有两个问题:

  • 它与 go 中的其余控制流不一致(例如,for/if/return/etc 关键字)。
  • 它使代码的可读性降低。

我宁愿让它成为一个不带括号的声明。 提案中的示例需要多行,但会变得更具可读性(即,单个“尝试”实例将更难遗漏)。 是的,它会破坏外部解析器,但我更喜欢保持一致性。

三元运算符是另一个地方,go 没有东西,需要更多的击键,但同时提高了可读性/可维护性。 以这种更受限制的形式添加“尝试”将更好地平衡表达性与可读性,IMO。

FWIW, panic影响控制流并且有括号,但是godefer也影响流并且不影响流。 我倾向于认为trydefer更相似,因为它是一种不寻常的流程操作,并且使try (try os.Open(file)).Read(buf)更难执行是好的,因为我们想阻止单行无论如何,但无论如何。 哪一个都好。

建议每个人都会讨厌最终错误返回变量的隐式名称: $err 。 它比try() IMO 更好。 :-)

@griesemer

_“就个人而言,我喜欢错误处理代码(最后)”_

对此+1!

我发现错误发生_before_执行的错误处理比错误发生_after_执行的错误处理更难推理。 不得不在精神上跳回并强制遵循逻辑流程,感觉就像我回到了 1980 年,用 GOTO 编写 Basic。

让我再次以CopyFile()为例提出另一种处理错误的潜在方法:

func CopyFile(src, dst string) (err error) {

    r := os.Open(src)
    defer r.Close()

    w := os.Create(dst)
    defer w.Close()

    io.Copy(w, r)
    w.Close()

    for err := error {
        switch err.Source() {
        case w.Close:
            os.Remove(dst) // only if a “try” fails
            fallthrough
        case os.Open, os.Create, io.Copy:
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        default:
            err = fmt.Errorf("an unexpected error occurred")
        }
    }

    return err
}

所需的语言更改是:

  1. 允许for error{}构造,类似于for range{}但仅在错误时输入且仅执行一次。

  2. 允许省略捕获实现<object>.Error() string的返回值,但仅当同一func中存在for error{}构造时。

  3. func在其最后一个返回值中返回 _"error"_ 时,导致程序控制流跳转for error{}构造的第一行。

  4. 当返回一个 _"error"_ 时,Go 会添加一个对返回错误的函数的引用,该错误应该可以通过<error>.Source()检索

什么是_“错误”_?

目前 _"error"_ 被定义为任何实现Error() string的对象,当然不是nil

然而,通常需要扩展错误_even on success_以允许返回 RESTful API 的成功结果所需的值。 所以我会要求 Go 团队不要自动假设err!=nil意味着 _"error"_ 而是检查错误对象是否实现IsError()以及IsError()是否返回true之前假设任何非nil值是一个_“错误”。

_(我不一定是在谈论标准库中的代码,但主要是如果您选择控制流在_“错误”_上进行分支。如果您只看err!=nil ,我们将非常有限可以根据我们函数中的返回值来做。)_

顺便说一句,让每个人都可以通过添加一个新的内置iserror()函数来以同样的方式测试 _"error"_ :

type ErrorIser interface {
    IsError() bool
}
func iserror(err error) bool {
    if err == nil { 
        return false
    }
    if _,ok := err.(ErrorIser); !ok {
        return true
    }
    return err.IsError()
}

允许不捕获_“错误”_的一个附带好处

请注意,允许从func调用中不捕获最后一个 _"error"_ 将允许以后的重构从最初不需要返回错误的func中返回错误。 并且它将允许这种重构而不会破坏任何使用这种形式的错误恢复和调用的现有代码func s。

对我来说,“我应该返回错误还是放弃错误处理以简化调用?”的决定是我在编写 Go 代码时最大的困惑之一。 允许不捕获上述_“错误”_几乎可以消除这种困境。

大约半年前,我实际上尝试将这个想法作为 Go 翻译器来实现。 我没有强烈的意见是否应该将此功能添加为 Go 内置,但让我分享一下经验(虽然我不确定它是否有用)。

https://github.com/rhysd/trygo

我调用了扩展语言 TryGo 并实现了 TryGo 到 Go 的翻译器。

使用翻译器,代码

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd := try(os.Getwd())

    try(os.Mkdir(filepath.Join(cwd, subdir)))

    p := filepath.Join(cwd, subdir, filename)
    f := try(os.Create(p))
    defer f.Close()

    try(f.Write(content))

    fmt.Println("Created:", p)
    return nil
}

可以翻译成

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd, _err0 := os.Getwd()
    if _err0 != nil {
        return _err0
    }

    if _err1 := os.Mkdir(filepath.Join(cwd, subdir)); _err1 != nil {
        return _err1
    }

    p := filepath.Join(cwd, subdir, filename)
    f, _err2 := os.Create(p)
    if _err2 != nil {
        return _err2
    }
    defer f.Close()

    if _, _err3 := f.Write(content); _err3 != nil {
        return _err3
    }

    fmt.Println("Created:", p)
    return nil
}

由于语言的限制,我无法实现通用的try()调用。 它仅限于

  • RHS 定义声明
  • 赋值语句的 RHS
  • 通话声明

但我可以用我的小项目试试这个。 我的经验是

  • 它基本上工作正常并节省了几行
  • 命名返回值实际上对于err是不可用的,因为它的函数的返回值是由赋值和try()特殊函数决定的。 很混乱
  • 如上所述,此try()函数缺少“包装错误”功能。

_“这两个对我来说似乎都是好的结果。”_

我们将不得不同意在这里不同意。

_“这种尝试语法(不必)处理每个用例”_

那个模因可能是最令人不安的。 至少考虑到 Go 团队/社区对过去不广泛适用的任何变化的抵制程度。

如果我们在这里允许这种理由,为什么我们不能重新审视过去因为不广泛适用而被拒绝的提案?

我们现在是否愿意争论对选定的边缘情况有用的 Go 变化?

在我看来,树立这个先例不会长期产生好的结果......

_“@mikeshenkel”_

PS因为拼写错误,我一开始没有看到你的消息。 _(这不会冒犯我,只是当我的用户名拼写错误时我不会收到通知......)_

我很欣赏对向后兼容性的承诺,这促使您将try设为内置函数,而不是关键字,但在与拥有可以更改控制流的常用函数( panicrecover非常罕见),我想知道:有没有人对try作为开源代码库中的标识符的频率进行过大规模分析? 我很好奇和怀疑,所以我对以下内容进行了初步搜索:

在这些存储库中的 11,108,770 条重要的 Go 行中,只有63try实例被用作标识符。 当然,我意识到这些代码库(虽然很大,被广泛使用,并且本身很重要)只代表了 Go 代码的一小部分,此外,我们无法直接分析私有代码库,但它肯定是一个有趣的结果。

此外,因为try和任何关键字一样是小写的,所以您永远不会在包的公共 API 中找到它。 关键字添加只会影响包内部。

这是我想加入的一些想法的前言,这些想法将从try作为关键字中受益。

我建议以下结构。

1) 没有处理程序

// The existing proposal, but as a keyword rather than builtin.  When an error is 
// "caught", the function returns all zero values plus the error.  Nothing 
// particularly new here.
func doSomething() (int, error) {
    try SomeFunc()
    a, b := try AnotherFunc()

    // ...

    return 123, nil
}

2) 处理程序

请注意,错误处理程序是简单的代码块,旨在内联,而不是函数。 更多关于这下面。

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

建议的限制:

  • 你只能try一个函数调用。 没有try err
  • 如果不指定处理程序,则只能在函数中使用try ,该函数将错误作为最右边的返回值返回。 try基于其上下文的行为方式没有变化。 它从不恐慌(正如线程中前面所讨论的)。
  • 没有任何类型的“处理程序链”。 处理程序只是可内联的代码块。

好处:

  • try / else语法可以简单地脱糖到现有的“复合 if”中:
    go a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "error in doSomething:") }
    变成
    go if a, b, err := SomeFunc(); err != nil { return 0, errors.Wrap(err, "error in doSomething:") }
    在我看来,复合 if 似乎总是比帮助更令人困惑,原因很简单:条件通常发生在操作之后,并且与处理其结果有关。 如果操作被嵌入到条件语句中,那么它的发生就不太明显了。 眼睛被分散了。 此外,已定义变量的范围并不像它们在一行中最左边时那么明显。
  • 错误处理程序有意不被定义为函数(也没有任何类似函数语义的东西)。 这为我们做了几件事:

    • 编译器可以简单地内联一个命名的处理程序,无论它被引用到哪里。 它更像是一个简单的宏/代码生成模板,而不是函数调用。 运行时甚至不需要知道处理程序的存在。

    • 我们在处理程序内部可以做的事情不受限制。 我们绕过了对check / handle的批评,即“这个错误处理框架只适用于纾困”。 我们还绕过了“处理程序链”的批评。 任何任意代码都可以放置在这些处理程序之一中,并且不暗示其他控制流。

    • 我们不必在处理程序中劫持return来表示super return 。 劫持关键字非常令人困惑。 return只是意味着return ,并没有真正需要super return

    • defer不需要将月光作为错误处理机制。 我们可以继续认为它主要是清理资源等的一种方式。

  • 关于为错误添加上下文:

    • 使用处理程序添加上下文非常简单,看起来与现有的if err != nil块非常相似

    • 尽管“try without handler”结构并不直接鼓励添加上下文,但重构为处理程序表单非常简单。 它的预期用途主要是在开发过程中,编写go vet检查以突出未处理的错误将非常简单。

如果这些想法与其他提议非常相似,我深表歉意——我试图跟上他们的步伐,但可能错过了很多。

@brynbellomy感谢您的关键字分析 - 这是非常有用的信息。 似乎try作为关键字可能没问题。 (您说 API 不受影响 - 这是真的,但try可能仍会显示为参数名称或类似名称 - 因此文档可能必须更改。但我同意这不会影响这些包的客户端。)

关于您的建议:即使没有指定的处理程序,它也可以正常运行,不是吗? (这将在不损失功率的情况下简化提案。可以简单地从内联处理程序调用本地函数。)

关于您的建议:即使没有指定的处理程序,它也可以正常运行,不是吗? (这将在不损失功率的情况下简化提案。可以简单地从内联处理程序调用本地函数。)

@griesemer确实——我对包含这些内容感到很冷淡。 如果没有,肯定会更 Go-ish。

另一方面,人们似乎确实希望能够进行单行错误处理,包括return的单行错误处理。 一个典型的例子是日志,然后是return 。 如果我们在else子句中使用本地函数,我们可能会丢失:

a, b := try SomeFunc() else err {
    someLocalFunc(err)
    return 0, err
}

(不过,我仍然更喜欢用这个来复合 if)

但是,您仍然可以通过实现线程前面讨论的简单gofmt调整来获得添加错误上下文的单行返回:

a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

上述提案中是否需要 new 关键字? 为什么不:

SomeFunc() else return
a, b := SomeOtherFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

@griesemer如果处理程序重新回到桌面上,我建议您提出一个新问题来讨论 try/handle 或 try/_label_。 这个提议特别省略了处理程序,并且有无数种方法来定义和调用它们。

任何建议处理程序的人都应该首先阅读检查/处理反馈维基。 很有可能你的梦想已经在那里描述了:-)
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

@smonkewitz不,在该版本中不需要新的关键字,因为它绑定到赋值语句,到目前为止,在各种语法糖中已经多次提及。

https://github.com/golang/go/issues/32437#issuecomment -499808741
https://github.com/golang/go/issues/32437#issuecomment -499852124
https://github.com/golang/go/issues/32437#issuecomment -500095505

@ianlancetaylor go 团队是否考虑过这种特殊的错误处理方式? 它不像建议的 try builtin 那样容易实现,但感觉更惯用。 ~不必要的声明,对不起。~

我想重复一下@deanveloper和其他一些人说过的话,但我自己强调。 在https://github.com/golang/go/issues/32437#issuecomment -498939499 @deanveloper说:

try是有条件的回报。 在 Go 中,控制流和返回都被固定在基座上。 函数中的所有控制流都是缩进的,所有返回都以return开头。 将这两个概念混合在一起形成一个容易错过的函数调用,感觉有点不对劲。

此外,在这个提议中, try是一个返回值的函数,因此它可以用作更大表达式的一部分。

有人认为panic已经为改变控制流的内置函数开创了先例,但我认为panic根本不同,原因有两个:

  1. 恐慌不是有条件的; 它总是中止调用函数。
  2. Panic 不返回任何值,因此只能作为独立语句出现,这增加了它的可见性。

另一方面尝试:

  1. 是有条件的; 它可能会或可能不会从调用函数返回。
  2. 返回值并且可以出现在复合表达式中,可能多次出现在一行上,可能超过我的编辑器窗口的右边距。

由于这些原因,我认为try感觉不仅仅是“有点偏离”,我认为它从根本上损害了代码的可读性。

今天,当我们第一次遇到一些 Go 代码时,我们可以快速浏览它以找到可能的退出点和控制流点。 我相信这是 Go 代码的一个非常有价值的属性。 使用try编写缺少该属性的代码变得太容易了。

我承认,重视代码可读性的 Go 开发人员很可能会集中在try的使用习惯上,以避免这些可读性陷阱。 我希望这会发生,因为代码可读性似乎是许多 Go 开发人员的核心价值。 但对我来说, try对现有代码习语增加了足够的价值来承担向语言添加新概念以供每个人学习的重量,这很容易损害可读性,这对我来说并不明显。

````
如果它!=“打破”{
不要修复(它)
}

@ChrisHines对于您的观点(在此线程的其他地方有所回应),让我们添加另一个限制:

  • 任何try语句(即使是那些没有处理程序的语句)都必须出现在它自己的行上。

您仍然会从视觉噪音的大幅减少中受益。 然后,您保证了由return注释的返回和由try注释的条件返回,并且这些关键字始终位于行首(或者最坏的情况,直接在变量赋值之后)。

所以,没有这种废话:

try EmitEvent(try (try DecodeMsg(m)).SaveToDB())

而是这个:

dm := try DecodeMsg(m)
um := try dm.SaveToDB()
try EmitEvent(um)

这仍然比这更清楚:

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

我喜欢这种设计的一件事是,在不注释可能发生的错误的情况下,不可能默默地忽略错误。 而现在,你有时会看到x, _ := SomeFunc() (什么是忽略的返回值?一个错误?别的东西?),现在你必须清楚地注释:

x := try SomeFunc() else err {}

自从我上一篇支持该提案的帖子以来,我已经看到@jagv (无参数try返回*error )和@josharian (标记为错误处理程序)发布的两个想法,我相信稍加修改的形式将大大加强该提案。

将这些想法与我自己的另一个想法放在一起,我们将有四个版本的try

  1. 尝试()
  2. 尝试(参数)
  3. 尝试(参数,标签)
  4. 尝试(参数,恐慌)

1 将简单地返回一个指向错误返回参数 (ERP) 的指针,如果没有返回参数 (ERP),则返回 nil (仅 #4)。 这将提供命名 ERP 的替代方案,而无需添加进一步的插件。

2 将完全按照目前的设想工作。 将立即返回一个非零错误,但可以用defer语句修饰。

3 将按照@josharian的建议工作,即在出现非零错误时,代码将分支到标签。 但是,不会有默认的错误处理程序标签,因为这种情况现在会退化为 #2。

在我看来,与defer相比,这通常是修饰错误(或在本地处理它们然后返回 nil)的更好方法,因为它更简单、更快捷。 任何不喜欢它的人仍然可以使用#2。

最好将错误处理标签/代码放在函数末尾附近,而不是跳回函数体的其余部分。 但是,我认为编译器也不应该强制执行,因为它们可能会在某些奇怪的情况下有用,并且在任何情况下执行都可能很困难。

因此,正常的标签和goto行为会将主题(如@josharian所说)应用于首先修复的#26058,但我认为无论如何都应该修复它。

标签的名称不能是panic ,因为这会与 #4 冲突。

4 将立即panic而不是返回或分支。 因此,如果这是在特定功能中使用的唯一版本的try ,则不需要 ERP。

我已经添加了这个,所以测试包可以像现在一样工作,而不需要进一步的内置或其他更改。 但是,它在其他 _fatal_ 场景中也可能有用。

这需要是try的单独版本,而不是分支到错误处理程序,然后从那里惊慌失措仍然需要 ERP。

对最初提案最强烈的反应之一是担心失去对函数返回的正常流程的轻松可见性。

例如, @deanveloperhttps://github.com/golang/go/issues/32437#issuecomment -498932961 中很好地表达了这种担忧,我认为这是这里获得最高评价的评论。

@dominikhhttps://github.com/golang/go/issues/32437#issuecomment -499067357 中写道:

在 gofmt'ed 代码中,return 始终匹配 /^\t*return / - 这是一个非常简单的模式,无需任何帮助即可通过肉眼发现。 另一方面,try 可以出现在代码中的任何地方,嵌套在函数调用的任意深处。 再多的培训也不会让我们能够在没有工具帮助的情况下立即发现函数中的所有控制流。

为了解决这个问题, @brynbellomy昨天建议:

任何 try 语句(即使是那些没有处理程序的)都必须出现在它自己的行上。

更进一步, try可能需要作为行的开头,即使对于分配也是如此。

所以它可能是:

try dm := DecodeMsg(m)
try um := dm.SaveToDB()
try EmitEvent(um)

而不是以下内容(来自@brynbellomy的示例):

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

即使没有任何编辑器或 IDE 帮助,它似乎也会保留相当多的可见性,同时仍然减少样板文件。

这可以与当前提出的依赖于命名结果参数的基于延迟的方法一起使用,或者它可以与指定正常的处理函数一起使用。 (在我看来,在不需要命名返回值的情况下指定处理函数似乎比要求命名返回值更好,但这是一个单独的点)。

该提案包括以下示例:

info := try(try(os.Open(file)).Stat())    // proposed try built-in

那可能是:

try f := os.Open(file)
try info := f.Stat()

与今天有人可能写的相比,这仍然是样板代码的减少,即使不像建议的语法那么短。 也许那会足够短?

@elagergren-spideroak 提供了这个例子:

try(try(try(to()).parse().this)).easily())

我认为这有不匹配的括号,这可能是一个深思熟虑的观点或一点点幽默,所以我不确定该示例是否打算使用 2 try或 3 try 。 无论如何,最好将其分布在以try开头的 2-3 行中。

@thepudds ,这就是我在之前的评论中得到的。 除了给定的

try f := os.Open(file)
try info := f.Stat()

一件显而易见的事情是将try视为一个try 块,其中可以将多个句子放在括号内。 所以上面可以变成

try (
    f := os.Open(file)
    into := f.Stat()
)

如果编译器知道如何处理这个问题,同样的事情也适用于嵌套。 所以现在上面可以变成

try info := os.Open(file).Stat()

从函数签名中,编译器知道 Open 可以返回一个错误值,并且由于它在 try 块中,它需要生成错误处理,然后在主要返回值上调用 Stat() 等等。

接下来是允许没有生成错误值或在本地处理的语句。 所以你现在可以说

try (
    f := os.Open(file)
    debug("f: %v\n", f) // debug returns nothing
    into := f.Stat()
)

这允许在不重新排列 try 块的情况下进化代码。 但是出于某种奇怪的原因,人们似乎认为必须明确说明错误处理! 他们要

try(try(try(to()).parse()).this)).easily())

虽然我很好

try to().parse().this().easily()

即使在这两种情况下都可以生成完全相同的错误检查代码。 我的观点是,如果需要,您始终可以编写特殊代码来处理错误。 try (或任何你喜欢的名字)只是简单地整理默认的错误处理(即把它扔给调用者)。

另一个好处是,如果编译器生成默认错误处理,它可以添加更多识别信息,以便您知道上面四个函数中的哪个函数失败了。

我有点担心try出现在其他表达式中的程序的可读性。 所以我在标准库上运行grep "return .*err$"并开始随机读取块。 有 7214 个结果,我只看了几百个。

首先要注意的是,在try应用的地方,它使几乎所有这些块都更具可读性。

第二件事是,其中很少有人(不到十分之一)会将try放入另一个表达式中。 典型情况是x := try(...)^try(...)$形式的语句。

下面是一些示例,其中try会出现在另一个表达式中:

文本/模板

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

变成:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        return !try(le(arg1, arg2)), nil
}

文本/模板

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

变成

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                if x := v.MapIndex(try(prepareArg(index, v.Type().Key()))); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

(这是我看到的最有问题的例子)

正则表达式/语法:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

变成

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        c, t = try(nextRune(t))
        p.literal(c)
        ...
}

这不是在另一个表达式中尝试的例子,但我想把它叫出来,因为它提高了可读性。 在这里更容易看出ct的值超出了 if 语句的范围。

网络/http

网络/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

变成:

        req.Header = Header(try(tp.ReadMIMEHeader())

数据库/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

变成

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

数据库/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

变成

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }

网络/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

变成

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                return try(p.t.dialclientconn(addr, singleuse))
        }

网络/http

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

变成

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

(这个我真的很喜欢。)

网络/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

变成

        hc = try(fr.ReadFrame()).(*http2ContinuationFrame)// guaranteed by checkFrameOrder

}

(也不错。)

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

变成

        if ctrlFn != nil {
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), try(newRawConn(fd))))
        }

也许这太多了,而应该是:

        if ctrlFn != nil {
                c := try(newRawConn(fd))
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), c))
        }

总的来说,我非常喜欢try对我阅读的标准库代码的影响。

最后一点:看到try应用于阅读提案中的几个示例之外的代码是很有启发性的。 我认为值得考虑编写一个工具来自动转换代码以使用try (它不会改变程序的语义)。 阅读一个针对 github 上的流行包产生的差异样本会很有趣,看看我在标准库中找到的内容是否成立。 这样一个程序的输出可以为提案的效果提供额外的洞察力。

@crawshaw感谢您这样做,很高兴看到它在行动。 但是看到它的实际效果让我更加认真地对待我之前一直在驳斥的反对内联错误处理的论点。

由于这与@thepuddstry设为语句的有趣建议非常接近,因此我使用该语法重写了所有示例,发现它比表达式 - try或现状,不需要太多额外的行:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}
func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}
        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)
        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

如果有多个字段必须是try -ed,那么这个表达式可能会更好 - try ,但我仍然更喜欢这种权衡的平衡

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

这基本上是最坏的情况,看起来不错:

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

我与自己争论if try是否合法或应该合法,但我无法给出合理的解释,为什么它不应该合法,并且在这里效果很好:

func (f *http2Framer) endWrite() error {
        ...
        if try n := f.w.Write(f.wbuf); n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}
        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

浏览@crawshaw的示例只会让我更加确定控制流通常会变得足够神秘,以至于对设计更加谨慎。 即使是少量的复杂性也变得难以阅读并且容易出错。 我很高兴看到考虑的选项,但是在这种受保护的语言中使控制流复杂化似乎异常不合时宜。

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

此外, try不是“尝试”任何东西。 它是一个“保护继电器”。 如果提案的基本语义不正确,那么生成的代码也存在问题我并不感到惊讶。

func (f *http2Framer) endWrite() error {
        ...
        relay n := f.w.Write(f.wbuf)
        return checkShortWrite(n, len(f.wbuf))
}

如果你做一个 try 语句,你可以使用一个标志来指示哪个返回值,以及什么操作:

try c, @      := newRawConn(fd) // return
try c, <strong i="6">@panic</strong> := newRawConn(fd) // panic
try c, <strong i="7">@hname</strong> := newRawConn(fd) // invoke named handler
try c, <strong i="8">@_</strong>     := newRawConn(fd) // ignore, or invoke "ignored" handler if defined

你仍然需要一个子表达式语法(Russ 已经声明这是一个要求),至少对于恐慌和忽略操作。

首先,我赞扬@crawshaw花时间查看大约 200 个真实示例并花时间在上面进行深思熟虑的文章。

其次, @jimmyfrasche ,关于您在此处对http2Framer示例的回复:


我与自己争论if try是否合法或应该合法,但我无法给出合理的解释,为什么它不应该合法,而且在这里效果很好:

```
func (f *http2Framer) endWrite() 错误 {
...
如果尝试 n := fwWrite(f.wbuf); n!= len(f.wbuf) {
返回 io.ErrShortWrite
}
返回零
}

At least under what I was suggesting above in https://github.com/golang/go/issues/32437#issuecomment-500213884, under that proposal variation I would suggest `if try` is not allowed.

That `http2Framer` example could instead be:

func (f *http2Framer) endWrite() 错误 {
...
尝试 n := fwWrite(f.wbuf)
如果 n != len(f.wbuf) {
返回 io.ErrShortWrite
}
返回零
}
`` That is one line longer, but hopefully still "light on the page". Personally, I think that (arguably) reads more cleanly, but more importantly it is easier to see the试试`。

@deanveloper上面在https://github.com/golang/go/issues/32437#issuecomment -498932961 中写道:

从函数返回似乎是一件“神圣”的事情

那个特定的http2Framer示例最终没有尽可能短。 但是,如果try必须是一行中的第一件事,则它保留从更“神圣”的函数返回。

@crawshaw提到:

第二件事是,其中很少有(不到十分之一)会将 try 放在另一个表达式中。 典型的情况是 x := try(...) 或 ^try(...)$ 形式的语句。

也许只用更受限制的形式try来部分帮助 10 个示例中的 1 个是可以的,特别是如果这些示例中的典型案例最终具有相同的行数,即使try是需要成为一条线上的第一件事吗?

@jimmyfrasche

@crawshaw感谢您这样做,很高兴看到它在行动。 但是看到它的实际效果让我更加认真地对待我之前一直在驳斥的反对内联错误处理的论点。

由于这与@thepuddstry设为语句的有趣建议非常接近,因此我使用该语法重写了所有示例,发现它比表达式 - try或现状,不需要太多额外的行:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

您的第一个示例很好地说明了为什么我强烈喜欢表达式 - try 。 在您的版本中,我必须将调用le的结果放在一个变量中,但该变量没有le术语尚未暗示的语义含义。 所以我不能给它起一个既无意义(如x )或多余(如lessOrEqual )的名字。 使用表达式- try ,不需要中间变量,所以这个问题甚至不会出现。

我宁愿不必花费脑力为最好保持匿名的事物发明名称。

我很高兴在try (关键字)已移至行首的最后几篇文章中支持我。 它确实应该与return共享相同的视觉空间。

回复: @jimmyfrasche的建议允许在复合if语句中使用try #$,这正是我认为这里的许多人试图避免的事情,原因如下:

  • 它将两种非常不同的控制流机制合并为一条线
  • try表达式实际上是首先计算的,并且可以导致函数返回,但它出现在if之后
  • 他们返回完全不同的错误,其中一个我们在代码中实际上没有看到,而我们看到了一个
  • 这使得try实际上未处理变得不那么明显,因为该块看起来很像一个处理程序块(即使它正在处理一个完全不同的问题)

人们可以从一个稍微不同的角度来处理这种情况,这有利于推动人们处理try s。 如何允许try / else语法包含后续条件(这是许多 I/O 函数的常见模式,它们同时返回errn ,其中任何一个都可能表明有问题):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf) else err {
                return errors.Wrap(err, "error writing:")
        } else if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

在您不处理.Write返回的错误的情况下,您仍然有一个明确的注释.Write可能会出错(正如@thepudds 所指出的那样):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

我第二个@daved的回应。 在我看来, @crawshaw突出显示的每个示例都因为try而变得不那么清晰并且更容易出错。

我很高兴在try (关键字)已移至行首的最后几篇文章中支持我。 它确实应该与return共享相同的视觉空间。

鉴于这一点有两个选项,并假设选择了一个,因此为未来的潜在功能设置了一些先例:

一种。)

try f := os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

乙)

f := try os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

两者中哪一个为未来的新关键字使用提供了更大的灵活性? _(我不知道这个问题的答案,因为我还没有掌握编写编译器的黑暗艺术。)_ 一种方法会比另一种更具限制性吗?

@davecheney @daved @crawshaw
在这一点上,我倾向于同意 Daves:在@crawshaw的示例中,有很多try语句嵌入到行中,这些语句还有很多其他内容。 真的很难发现退出点。 此外,在某些示例中, try括号似乎使事情变得非常混乱。

看到一堆像这样转换的 stdlib 代码非常有用,所以我采用了相同的示例,但根据替代提案重写了它们,这更具限制性:

  • try作为关键字
  • 每行只有一个try
  • try必须在行首

希望这将有助于我们进行比较。 就个人而言,我发现这些示例看起来比它们的原始示例要简洁得多,但不会模糊控制流。 try在任何使用它的地方都非常明显。

文本/模板

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

变成:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

文本/模板

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

变成

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

正则表达式/语法:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

变成

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}

网络/http

网络/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

变成:

        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)

数据库/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

变成

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

数据库/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

变成

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

网络/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

变成

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

网络/http
这个实际上并没有为我们节省任何行,但我发现它更清晰,因为if err == nil是一种相对不常见的结构。

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

变成

func (f *http2Framer) endWrite() error {
        ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

网络/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

变成

        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}

网:

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

变成

        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

@james-lawrence 回复https://github.com/golang/go/issues/32437#issuecomment -500116099 :我不记得像可选的, err这样的想法正在认真考虑,不。 我个人认为这是一个坏主意,因为这意味着如果函数更改为添加尾随error参数,现有代码将继续编译,但行为会非常不同。

使用 defer 处理错误很有意义,但它导致需要命名错误和一种新的if err != nil样板。

外部处理程序需要这样做:

func handler(err *error) {
  if *err != nil {
    *err = handle(*err)
  }
} 

像这样使用

defer handler(&err)

外部处理程序只需要编写一次,但许多错误处理函数需要有两个版本:一个是要延迟的,一个是要以常规方式使用的。

内部处理程序需要这样做:

defer func() {
  if err != nil {
    err = handle(err)
  }
}()

在这两种情况下,必须命名外部函数的错误才能访问。

正如我前面在线程中提到的,这可以抽象为一个函数:

func catch(err *error, handle func(error) error) {
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

这与@griesemernil处理函数的模糊性的担忧相冲突,并且除了必须命名err之外,它还有自己的deferfunc(err error) error样板err在外部函数中。

如果try最终成为关键字,那么有一个catch关键字也是有意义的,如下所述。

从语法上讲,它很像handle

catch err {
  return handleThe(err)
}

从语义上讲,这将是上面内部处理程序代码的糖:

defer func() {
  if err != nil {
    err = handleThe(err)
  }
}()

由于它有点神奇,它可以抓取外部函数的错误,即使它没有被命名。 ( err之后的 $#$14$ catch更像是catch块的参数名称)。

catch将具有与try相同的限制,即它必须位于具有最终错误返回的函数中,因为它们都是依赖于此的糖。

这远没有最初的handle提案那么强大,但它可以避免为处理它而命名错误的要求,并且它会删除上面讨论的内部处理程序的新样板,同时让它很容易不需要外部处理程序的单独版本的函数。

复杂的错误处理可能需要不使用catch ,因为它可能需要不使用try

由于这些都是糖,因此无需将catchtry一起使用。 只要函数返回非nil错误,就会运行catch处理程序,例如,允许坚持一些快速日志记录:

catch err {
  log.Print(err)
  return err
}

或者只是包装所有返回的错误:

catch err {
  return fmt.Errorf("foo: %w", err)
}

@ianlancetaylor

_" 我认为这是个坏主意,因为这意味着如果一个函数更改为添加一个尾随error参数,现有代码将继续编译,但行为会非常不同。"_

这可能是看待它的正确方法,如果您能够控制上游和下游代码,那么如果您需要更改函数签名以便也返回错误,那么您可以这样做。

但是我想请你考虑一下当有人不控制他们自己的包的上游或下游时会发生什么? 还要考虑可能添加错误的用例,如果需要添加错误但您不能强制下游代码更改会发生什么?

你能想到一个例子,有人会更改签名以添加返回值吗? 对我来说,他们通常属于 _“我没有意识到会发生错误”_ 或 _“我感觉很懒,不想努力,因为错误可能不会发生。” _

在这两种情况下,我可能会添加一个错误返回,因为很明显需要处理一个错误。 发生这种情况时,如果我因为不想破坏使用我的包的其他开发人员的兼容性而无法更改签名,该怎么办? 我的猜测是,绝大多数情况下都会发生错误,并且调用不返回错误的函数的代码的行为会非常不同,_anyway._

实际上,我很少做后者,但经常做前者。 但是我注意到第 3 方包经常忽略在它们应该出现的地方捕获错误,我知道这一点是因为当我在 GoLand 中以亮橙色标记每个实例的代码时。 我希望能够提交拉取请求以向我经常使用的包添加错误处理,但如果我这样做,大多数人不会接受它们,因为我会破坏他们的代码签名。

由于不提供向后兼容的方式来添加要由函数返回的错误,因此分发代码并关心不为用户破坏事物的开发人员将无法改进他们的包以包含应有的错误处理。


也许与其考虑代码会表现不同的问题,不如将问题视为一个工程挑战,即如何最小化没有主动捕获错误的方法的缺点? 这将具有更广泛和更长期的价值。

例如,考虑添加一个必须设置的包错误处理程序,然后才能忽略错误?


坦率地说,Go 除了常规返回值之外还返回错误的习惯用法是它更好的创新之一。 但是,当您改进事物时,经常会出现其他弱点,我认为 Go 的错误处理没有足够的创新。

我们 Gophers 已经变得沉迷于返回错误而不是抛出异常,所以我的问题是 _“为什么我们不应该从每个函数中返回错误?”_ 我们并不总是这样做,因为编写没有错误处理的代码是比用它编码更方便。 因此,当我们认为可以摆脱错误处理时,我们会忽略它。 但我们经常猜错。

所以真的,如果有可能弄清楚如何使代码优雅和可读,我认为返回值和错误确实应该分开处理,并且_every_函数应该有能力返回错误,而不管它过去的函数签名。 让现有代码优雅地处理现在产生错误的代码将是一项值得努力的工作。

我没有提出任何建议,因为我无法设想一种可行的语法,但如果我们想对自己诚实,那么这个线程中的所有内容以及与 Go 的错误处理相关的内容通常都不是关于错误处理和程序逻辑是奇怪的伙伴,所以理想情况下错误最好以某种方式带外处理?

try作为关键字当然有助于提高可读性(与函数调用相比)并且看起来不那么复杂。 @brynbellomy @crawshaw感谢您花时间写出示例。

我想我的一般想法是try做的太多了。 它解决了:调用函数,分配变量,检查错误,如果存在则返回错误。 我建议我们改为削减范围并仅解决条件返回:“如果最后一个 arg 不是 nil 则返回”。

这可能不是一个新想法...但是在浏览了错误反馈wiki中的建议后,我没有找到它(并不意味着它不存在)

有条件回报的迷你提案

摘抄:

err, thing := newThing(name)
refuse nil, err

我也在“另类想法”下将其添加到 wiki

什么都不做似乎也是一个非常合理的选择。

@alexhornbake这给了我一个稍微不同的想法,这会更有用

assert(nil, err)
assert(len(a), len(b))
assert(true, condition)
assert(expected, given)

这样,它不仅适用于错误检查,而且适用于许多类型的逻辑错误。

给定的将被包装在错误中并返回。

@alexhornbake

正如try实际上并没有尝试一样, refuse实际上也没有“拒绝”。 这里的共同意图是我们正在设置一个“保护继电器”( relay是简短的、准确的,并且是return的首字母),当其中一个接线值满足条件时“跳闸” (即非零错误)。 它是一种断路器,我相信,如果它的设计仅限于无趣的案例以简单地减少一些最低悬的样板,它可以增加价值。 任何远程复杂的东西都应该依赖于简单的 Go 代码。

我也赞扬 cranshaw 浏览标准库的工作,但我得出了一个非常不同的结论……我认为这几乎使所有这些代码片段更难阅读并且更容易产生误解。

        req.Header = Header(try(tp.ReadMIMEHeader())

我经常会错过这可能会出错。 快速阅读让我“好的,将标题设置为事物的 ReadMimeHeader 的标题”。

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

这一个,我的眼睛只是交叉试图解析该 OpenDB 行。 那里的密度太大了...这显示了所有嵌套函数调用的主要问题,因为您必须从内到外阅读,并且必须在头脑中对其进行解析以找出最里面的部分在哪里.

另请注意,这可以从同一行中的两个不同位置返回。..您将要进行调试,并且会说从该行返回了一个错误,每个人要做的第一件事就是尝试弄清楚为什么 OpenDB 会因为这个奇怪的错误而失败,而实际上是 OpenConnector 失败(反之亦然)。

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }   

这是一个代码可能失败的地方,而以前这是不可能的。 没有try ,结构字面量构造就不会失败。 我的眼睛会掠过它,就像“好的,构建一个 driverStmt ......继续......”实际上很容易错过它,这可能会导致你的函数出错。 以前唯一可能的方法是如果 ctxDriverPrepare 惊慌失措……我们都知道,1.) 基本上永远不会发生,2.) 如果发生,则意味着某些事情严重错误。

使用 try 关键字和语句解决了我的很多问题。 我知道这不是向后兼容的,但我不认为使用更糟糕的版本是向后兼容问题的解决方案。

@daved我不确定我是否遵循。 你不喜欢这个名字,还是不喜欢这个想法?

无论如何,我在这里发布了这个作为替代方案......如果有合法的兴趣,我可以打开一个新问题进行讨论,不想污染这个线程(也许为时已晚?)对原始想法的竖起大拇指会给出我们有一种感觉...当然对替代名称持开放态度。 重要的部分是“有条件返回而不尝试处理分配”。

虽然我喜欢@jimmyfraschecatch 提议,但我想提出一个替代方案:
go handler fmt.HandleErrorf("copy %s %s", src, dst)
相当于:
go defer func(){ if(err != nil){ fmt.HandleErrorf(&err,"copy %s %s", src, dst) } }()
其中 err 是最后命名的返回值,类型为错误。 但是,也可以在未命名返回值时使用处理程序。 也允许更一般的情况:
go handler func(err *error){ *err = fmt.Errorf("foo: %w", *err) }() `
使用命名返回值(catch 无法解决)的主要问题是 err 是多余的。 当推迟对像fmt.HandleErrorf这样的处理程序的调用时,除了指向错误返回值的指针之外没有合理的第一个参数,为什么要让用户选择犯错呢?

与 catch 相比,主要区别在于 handler 使调用预定义的处理程序更容易一些,但代价是在适当的位置定义它们变得更加冗长。 我不确定这是否理想,但我认为它更符合最初的提议。

@yiyus catch ,正如我定义的那样,它不需要在包含catch err

catch err {中, errcatch错误的名称。 它就像一个函数参数名称。

有了它,就不需要像fmt.HandleErrorf这样的东西了,因为你可以使用常规的fmt.Errorf

func f() error {
  catch err {
    return fmt.Errorf("foo: %w", err)
  }
  return errors.New("bar")
}

它返回一个打印为foo: bar的错误。

我不喜欢这种方法,因为:

  • try()函数调用会中断父函数中的代码执行。
  • 没有return关键字,但代码实际上返回。

提出了很多处理处理程序的方法,但我认为它们经常错过两个关键要求:

  1. 它必须明显不同并且比if x, err := thingie(); err != nil { handle(err) }更好。 我认为try x := thingie else err { handle(err) }的建议不符合那个标准。 为什么不直接说if

  2. 它应该与defer的现有功能正交。 也就是说,它应该足够不同,很明显,所提出的处理机制本身是需要的,而不会在句柄和延迟交互时产生奇怪的极端情况。

在我们讨论try /handle 的替代机制时,请牢记这些需求。

@carlmjohnson我喜欢@jimmyfraschecatch关于您的第 2 点的想法 - 它只是defer的语法糖,它可以节省 2 行并让您避免命名错误返回值(在如果您还没有的话,turn 还需要您命名所有其他人)。 它不会引发defer的正交性问题,因为它defer

呼应@ubombi所说的:

try() 函数调用中断父函数中的代码执行。 没有 return 关键字,但代码实际返回。

在 Ruby 中,procs 和 lambdas 是try所做的一个示例...... proc 是一个代码块,它的 return 语句不是从块本身返回,而是从调用者返回。

这正是try所做的……它只是一个预定义的 Ruby 过程。

我想如果我们要走那条路,也许我们实际上可以通过引入proc functions来让用户定义自己的try函数

我仍然更喜欢if err != nil ,因为它更具可读性,但我认为try如果用户定义自己的过程会更有益:

proc try(err *error, msg string) {
  if *err != nil {
    *err = fmt.Errorf("%v: %w", msg, *err)
    return
  }
}

然后你可以调用它:

func someFunc() (string, error) {
  err := doSomething()
  try(&err, "someFunc failed")
}

这样做的好处是您可以用自己的方式定义错误处理。 您还可以将proc设为公开的、私有的或内部的。

它也比原始 Go2 提案中的handle {}子句更好,因为您只能为整个代码库定义一次,而不是在每个函数中定义它。

可读性的一个考虑因素是 func() 和 proc() 的调用方式可能不同,例如func()proc!()以便程序员知道 proc 调用实际上可能从调用函数。

@marwan-at-work,在您的示例中try(err, "someFunc failed")不应该是try(&err, "someFunc failed")吗?

@dpinela感谢您的更正,更新了代码:)

我们在这里试图覆盖的常见做法是在许多语言中展开的标准堆栈在异常中所暗示的内容,(因此选择了“尝试”这个词......)。
但是,如果我们只能允许一个函数(...try() 或其他)在跟踪中跳回两级,那么

try := handler(err error) {     //which corelates with - try := func(err error) 
   if err !=nil{
       //do what ever you want to do when there's an error... log/print etc
       return2   //2 levels
   }
} 

然后像这样的代码
f := try(os.Open(文件名))
可以完全按照提案的建议做,但是作为一个函数(或实际上是一个“处理函数”),开发人员将更多地控制函数的作用,在不同情况下如何格式化错误,使用类似的处理程序处理(比如说)os.Open 的代码,而不是每次都写 fmt.Errorf("error opening file %s ....")。
这也会强制错误处理,就好像没有定义“try”一样——它是一个编译时错误。

@guybrand拥有这样的两级回报return2 (或 Smalltalk 中称为一般概念的“非本地回报”)将是一个很好的通用机制( @mikeschinkel在#32473 中也建议) . 但是您的建议似乎仍然需要try ,所以我看不出return2的原因 - try可以只做return . 如果也可以在本地编写try会更有趣,但这对于任意签名是不可能的。

@griesemer

_“所以我看不出return2的原因 - try可以做return 。”_

一个原因 - 正如我在 #32473 _(感谢您的参考)_ 中指出的那样 - 除了return之外,还允许breakcontinue的多个级别。

再次感谢大家的所有新评论; 跟上讨论并撰写大量反馈是一项重要的时间投资。 甚至更好的是,尽管有时争论不休,但到目前为止,这一直是一个相当平民的话题。 谢谢!

这是另一个快速摘要,这次更简洁; 向那些我没有提及、忘记或误解的人道歉。 在这一点上,我认为一些更大的主题正在出现:

1)一般来说,使用try功能的内置函数被认为是一个糟糕的选择:鉴于它会影响控制流,它应该_至少_一个关键字( @carloslenz “更喜欢使它成为一个没有插入语”); try作为表达式似乎不是一个好主意,它会损害可读性( @ChrisHines ,@jimmyfrasche),它们是“没有return的返回”。 @brynbellomy对用作标识符的try进行了实际分析; 似乎很少有百分比,因此可能会在不影响太多代码的情况下使用关键字路线。

2) @crawshaw花了一些时间分析了标准库中的几百个用例,并得出结论,所提议的try几乎总是能提高可读性。 @jimmyfrasche得出了相反的结论。

3) 另一个主题是使用defer进行错误修饰并不理想。 @josharian指出defer总是在函数返回时运行,但如果它们在这里是为了错误修饰,我们只关心它们的主体是否有错误,这可能是混乱的根源。

4) 许多人写了改进提案的建议。 @zeebo和 @patrick-nyt 支持gofmt在一行上格式化简单的if语句(并对现状感到满意)。 @jargv建议try() (不带参数)可以返回指向当前“待处理”错误的指针,这将消除命名错误结果的需要,以便人们可以在defer中访问它@masterada建议改用errorfunc()@velovix恢复了 2 参数try的想法,其中第二个参数是错误处理程序。

@klaidliadon@networkimprov支持特殊的“赋值运算符”,例如f, # := os.Open()而不是try@networkimprov提交了一份更全面的替代提案来调查此类方法(请参阅问题 #32500)。 @mikeschinkel还提交了一个替代提案,建议引入两个新的通用语言功能,这些功能也可用于错误处理,而不是特定于错误的try (参见问题 #32473)。 @josharian恢复了我们去年在 GopherCon 讨论的一种可能性,其中try不会在错误时返回,而是跳转(使用goto )到名为error的标签(或者, try可能采用目标标签的名称)。

5)关于try作为关键词的话题,出现了两行思路。 @brynbellomy建议了一个可以替代地指定处理程序的版本:

a, b := try f()
a, b := try f() else err { /* handle error */ }

@thepudds更进一步,在行首建议try ,使tryreturn具有相同的可见性:

try a, b := f()

这两者都可以与defer一起使用。

@griesemer

感谢@mikeschinkel #32473 的参考,它确实有很多共同点。

关于

但是您的建议似乎仍然需要尝试
虽然我的建议可以用“任何”处理程序而不是保留的“内置/关键字/表达式”来实现,但我不认为“try()”是一个坏主意(因此没有否决它),我正在尝试“扩大范围”——因此它会显示出更多的优势,许多人预计“一旦推出 2.0”

我认为这也可能是您在上次总结中报告的“混合情绪”的来源——它不是“try() 不能改进错误处理”——确实如此,它是“等待 Go 3.0 解决其他一些重大错误处理痛苦”上面所说的人,看起来太长了:)

我正在进行一项关于“错误处理痛苦”的调查(并且听起来有些痛苦仅仅是“我不使用好的做法”,而有些痛苦我什至没有想到人们(主要来自其他语言)想做 - 从很酷到WTF)。

希望我能尽快分享一些有趣的结果。

最后-感谢您的出色工作和耐心!

简单地看一下当前提议的语法的长度与现在可用的语法相比,只需要返回错误而不处理它或修饰它的情况是获得最大便利的情况。 到目前为止我最喜欢的语法示例:

try a, b := f() else err { return fmt.Errorf("Decorated: %s", err); }
if a,b, err :=f;  err != nil { return fmt.Errorf("Decorated: %s", err); }
try a, b := f()
if a,b, err :=f;  err != nil { return err; }

所以,与我之前的想法不同,也许改变 go fmt 就足够了,至少对于装饰/处理的错误情况。 虽然只是传递错误案例,但对于这个非常常见的用例,像 try 这样的东西可能仍然是可取的语法糖。

关于try else ,我认为在初始注释中像fmt.HandleErrorf (编辑:我假设它在输入为 nil 时返回 nil )这样的条件错误函数可以正常工作,因此添加else是不必要的。

a, b, err := doSomething()
try fmt.HandleErrorf(&err, "Decorated "...)

像这里的许多其他人一样,我更喜欢try作为语句而不是表达式,主要是因为改变控制流的表达式与 Go 完全不同。 也因为这不是一个表达式,它应该在行首。

我也同意@daved这个名字不合适。 毕竟,我们在这里试图实现的是一个受保护的赋值,那么为什么不像在 Swift 中那样使用guard并让else子句成为可选的呢? 就像是

GuardStmt = "guard" ( Assignment | ShortVarDecl | Expression ) [  "else" Identifier Block ] .

其中Identifier是绑定在以下Block中的错误变量名称。 如果没有else子句,只需从当前函数返回(并在需要时使用延迟处理程序来修饰错误)。

我最初不喜欢else子句,因为它只是围绕通常赋值的语法糖,然后是if err != nil ,但是在看到一些示例之后,它才有意义:使用guard使意图更清晰。

编辑:有些人建议使用诸如catch之类的东西来以某种方式指定不同的错误处理程序。 我发现else在语义上同样可行,而且它已经在语言中了。

虽然我喜欢 try-else 语句,但这个语法怎么样?

a, b, (err) := func() else { return err }

表达式try - else是一个三元运算符。

a, b := try f() else err { ... }
fmt.Println(try g() else err { ... })`

语句try - else是一个if语句。

try a, b := f() else err { ... }
// (modulo scope of err) same as
a, b, err := f()
if err != nil { ... }

带有可选处理程序的内置try可以使用辅助函数(如下)或不使用try (未图示,我们都知道它的样子)来实现。

a, b := try(f(), decorate)
// same as
a, b := try(g())
// where g is
func g() (whatever, error) {
  x, err := f()
  if err != nil {
    try(decorate(err))
  }
  return x, nil
}

所有这三个都减少了样板文件并帮助包含错误范围。

它为内置try提供了最大的节省,但这存在设计文档中提到的问题。

对于语句try - else ,它提供了优于使用if而不是try的优势。 但是优势是如此微不足道,以至于我很难看到它证明自己是合理的,尽管我确实喜欢它。

三者都假设需要对个别错误进行特殊错误处理是很常见的。

可以在defer中平等地处理所有错误。 如果在每个else块中进行相同的错误处理,那有点重复:

func printSum(a, b string) error {
  try x := strconv.Atoi(a) else err {
    return fmt.Errorf("printSum: %w", err)
  }
  try y := strconv.Atoi(b) else err {
    return fmt.Errorf("printSum: %w", err)
  }
  fmt.Println("result:", x + y)
  return nil
}

我当然知道有时某个错误需要特殊处理。 这些是我记忆中突出的例子。 但是,如果这种情况只发生在 100 次中的 1 次,那么保持try简单并且在这些情况下不使用try不是更好吗? 另一方面,如果它更像是 10 次中的 1 次,则添加else /handler 似乎更合理。

有趣的是,看看没有else /handler 的try与有else /handler 的 $#$23$# try的实际分布频率会很有用,不过这不是容易收集的数据。

我想扩展@jimmyfrasche最近的评论。

该提案的目标是减少样板

    a, b, err := f()
    if err != nil {
        return nil, err
    }

这段代码很容易阅读。 只有当我们可以显着减少样板时,才值得扩展语言。 当我看到类似的东西

    try a, b := f() else err { return nil, err }

我不禁觉得我们并没有节省那么多。 我们节省了三行,这很好,但据我计算,我们正在将 56 个字符减少到 46 个字符。 那不是很多。 相比于

    a, b := try(f())

从 56 个字符减少到 18 个字符,显着减少。 虽然try语句使控制流的潜在变化更加清晰,但总体而言,我认为该语句没有更具可读性。 虽然从好的方面来说, try语句更容易注释错误。

无论如何,我的观点是:如果我们要改变某些东西,它应该显着减少样板文件或者应该显着提高可读性。 后者相当困难,所以任何改变都需要对前者真正起作用。 如果我们只对样板进行少量减少,那么在我看来这是不值得做的。

和其他人一样,我要感谢@crawshaw提供的示例。

在阅读这些示例时,我鼓励人们尝试采用一种思维方式,在这种思维方式中,由于try函数,您不必担心控制流。 我相信,也许是错误的,这种控制流将很快成为懂语言的人的第二天性。 在正常情况下,我相信人们将不再担心错误情况下会发生什么。 尝试阅读这些示例,同时对try上釉,就像您已经对if err != nil { return err }上釉一样。

在阅读完这里的所有内容并进一步思考之后,我不确定我是否认为 try 甚至是值得添加的声明。

  1. 它的基本原理似乎是减少错误处理样板代码。 恕我直言,它“整理”了代码,但并没有真正消除复杂性; 它只是掩盖了它。 这似乎没有足够的理由。 “去” 语法精美地捕获了启动并发线程。我在这里没有那种“啊哈!”的感觉。感觉不对。成本/收益比不够大。

  2. 它的名字并不反映它的功能。 以最简单的形式,它的作用是:“如果函数返回错误,则从调用者返回错误”,但这太长了 :-) 至少需要一个不同的名称。

  3. 使用 try 的隐式错误返回,感觉就像 Go 有点不情愿地支持异常处理。 也就是说,如果 A 在 try 守卫中调用 be,B 在 try 守卫中调用 C,C 在 try 守卫中调用 D,如果 D 返回错误,则实际上您已导致非本地 goto。 感觉太“神奇”了。

  4. 但我相信可能会有更好的方法。 现在选择尝试将关闭该选项。

@ianlancetaylor
如果我正确理解“尝试其他”建议,似乎else块是可选的,并保留给用户提供的处理。 在您的示例try a, b := f() else err { return nil, err }else子句实际上是多余的,整个表达式可以简单地写成try a, b := f()

我同意@ianlancetaylor
可读性和样板是两个主要问题,也许是驱动力
go 2.0 错误处理(尽管我可以添加一些其他重要的问题)

此外,当前

a, b, err := f()
if err != nil {
    return nil, err
}

具有高度可读性。
既然我相信

if a, b, err := f(); err != nil {
    return nil, err
}

几乎一样可读,但它的范围“问题”,也许是

ifErr a, b, err := f() {
    return nil, err
}

那只会 ; err != nil 部分,并且不会创建范围,或者

相似地

尝试 a, b, err := f() {
返回零,错误
}

保留额外的两行,但仍然可读。

2019 年 6 月 11 日星期二 20:19 Dmitriy Matrenichev, notifications @github.com
写道:

@ianlancetaylor https://github.com/ianlancetaylor
如果我正确理解“尝试其他”的建议,似乎其他阻止
是可选的,并为用户提供的处理保留。 在你的例子中
try a, b := f() else err { return nil, err } else 子句实际上是
多余的,整个表达式可以简单地写成 try a, b :=
F()


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4XPURMASWKZKOBPBVDPZ7NALA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXN3VDA#issuecomment-500939404
或使线程静音
https://github.com/notifications/unsubscribe-auth/ABNEY4SAFK4M5NLABF3NZO3PZ7NALANCNFSM4HTGCZ7Q
.

@ianlancetaylor

无论如何,我的观点是:如果我们要改变某些东西,它应该显着减少样板文件或者应该显着提高可读性。 后者相当困难,所以任何改变都需要对前者真正起作用。 如果我们只对样板进行少量减少,那么在我看来这是不值得做的。

同意,并且考虑到else只是语法糖(语法很奇怪!),很可能很少使用,我不太关心它。 不过,我仍然希望try成为一个声明。

@ianlancetaylor@DmitriyMV相呼应, else块将是可选的。 让我举一个例子来说明两者(就实际代码中已处理与未处理的try块的相对比例而言,这似乎并不太离谱):

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    headRef, err := r.Head()
    if err != nil {
        return err
    }

    parentObjOne, err := headRef.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentCommitOne, err := parentObjOne.AsCommit()
    if err != nil {
        return err
    }

    parentCommitTwo, err := parentObjTwo.AsCommit()
    if err != nil {
        return err
    }

    treeOid, err := index.WriteTree()
    if err != nil {
        return err
    }

    tree, err := r.LookupTree(treeOid)
    if err != nil {
        return err
    }

    remoteBranchName, err := remoteBranch.Name()
    if err != nil {
        return err
    }

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    if err != nil {
        return err
    }
    return nil
}
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()
    try userName, userEmail := r.UserIdentityFromConfig() else err {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    try r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return nil
}

虽然try / else模式并没有比复合if节省很多字符,但它确实:

  • 将错误处理语法与未处理的try统一起来
  • 一目了然地表明条件块正在处理错误条件
  • 让我们有机会减少复合if遭受的范围界定怪异

不过,未处理的try可能是最常见的。

@ianlancetaylor

尝试阅读这些示例,同时进行尝试,就像您已经对 if err != nil { return err } 进行检查一样。

我不认为这是可能的/平等的。 缺少尝试存在于拥挤的行中,或者它确切地包装了什么,或者一行中有多个实例......这些与轻松/快速标记返回点而不担心其中的细节不同。

@ianlancetaylor

当我看到一个停车标志时,我通过形状和颜色来识别它,而不是通过阅读印在上面的文字并思考其更深层次的含义。

我的眼睛可能会在if err != nil { return err }上呆滞,但同时它仍然会记录下来——清晰而迅速。

我喜欢try -statement 变体的原因是它减少了样板,但以一种既容易上釉又难以错过的方式。

这可能意味着在这里或那里多出一行,但这仍然比现状少。

@brynbellomy

  1. 您如何处理返回多个值的函数,例如:
    func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) (hash , error) {
  2. 您将如何跟踪返回错误的正确行
  3. 放弃范围问题(可以通过其他方式解决),我不确定
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {

    if headRef, err := r.Head(); err != nil {
        return err
    } else if parentObjOne, err := headRef.Peel(git.ObjectCommit); err != nil {
        return err
    } else parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit); err != nil {
        return err
    } ...

在可读性方面并没有那么不同,但是(或 fmt.Errorf("error with getting head : %s", err.Error() )允许您轻松修改和提供额外数据。

仍然是一个唠叨的是

  1. 必须重新检查; 错误!= 无
  2. 如果我们不想提供额外信息,则按原样返回错误 - 在某些情况下这不是一个好习惯,因为您依赖于您调用的函数来反映“好”错误,该错误将提示“出了什么问题”,在 file.Open 、 close 、 Remove 、 Db 函数等的情况下,许多函数调用都可以返回相同的错误(我们可以争论这是否意味着编写错误的开发人员做得很好......但它确实发生了) - 然后 - 你有一个错误,可能从调用的函数中记录它
    “ createMergeCommit”,但无法将其跟踪到它发生的确切行。

对不起,如果有人已经发布了这样的东西(有很多好主意:P)这个替代语法怎么样:

fail := func(err error) error {
  log.Print("unexpected error", err)
  return err
}

a, b, err := f1()          // normal
c, d := f2() -> throw      // calls throw(err)
e, f := f3() -> panic      // calls panic(err)
g, h := f4() -> t.Error    // calls t.Error(err)
i, j := f5() -> fail       // calls fail(err)

也就是说,您在函数调用的右侧有一个-> handler ,如果返回的 err != nil 则调用该函数调用。 处理程序是任何接受错误作为单个参数并可选择返回错误的函数(即func(error)func(error) error )。 如果处理程序返回 nil 错误,则函数继续,否则返回错误。

所以a := b() -> handler相当于:

a, err := b()
if err != nil {
  if herr := handler(err); herr != nil {
    return herr
  }
}

现在,作为快捷方式,您可以支持try内置(或关键字或?=运算符或其他),它是a := b() -> throw的缩写,因此您可以编写如下内容:

func() error {
  a, b := try(f1())
  c, d := try(f2())
  e, f := try(f3())
  ...
  return nil
}() -> panic // or throw/fail/whatever

就我个人而言,我发现?=运算符比 try 关键字/内置运算符更容易阅读:

func() error {
  a, b ?= f1()
  c, d ?= f2()
  e, f ?= f3()
  ...
  return nil
}() -> panic

注意:这里我使用 throw 作为内置函数的占位符,它将错误返回给调用者。

到目前为止,我还没有对错误处理提案发表评论,因为我普遍赞成,而且我喜欢他们前进的方式。 提案中定义的 try 函数和@thepudds提出的 try 语句似乎都是对语言的合理补充。 我相信 Go 团队想出的任何东西都会是好的。

我想提出我认为提案中定义 try 的方式的一个小问题,以及它可能如何影响未来的扩展。

Try 被定义为一个接受可变数量参数的函数。

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

由于 Go 中多个返回值的工作方式,将函数调用的结果传递给try就像在try(f())中一样隐式地工作。

通过我对提案的阅读,以下片段既有效又在语义上等效。

a, b = try(f())
//
u, v, err := f()
a, b = try(u, v, err)

该提案还提出了使用额外参数扩展try的可能性。

如果我们确定具有某种形式的显式提供的错误处理函数或任何其他附加参数是一个好主意,那么很可能将该附加参数传递给 try 调用。

假设我们要添加一个处理程序参数。 它可以位于参数列表的开头或结尾。

var h handler
a, b = try(h, f())
// or
a, b = try(f(), h)

把它放在开头是行不通的,因为(鉴于上面的语义) try将无法区分显式处理程序参数和返回处理程序的函数。

func f() (handler, error) { ... }
func g() (error) { ... }
try(f())
try(h, g())

把它放在最后可能会起作用,但是 try 在语言中是唯一的,因为它是唯一在参数列表开头带有可变参数的函数。

这两个问题都不是什么大问题,但它们确实让try感觉与语言的其余部分不一致,所以我不确定try将来是否容易扩展,因为提案状态。

@神奇

拥有一个处理程序很强大,也许:
我你已经声明了 h,

你可以

var h handler
a, b, h = f()

要么

a, b, h.err = f()

如果它是类似函数的:

h:= handler(err error){
 log(...)
 return ....
} 

然后有人建议

a, b, h(err) = f()

所有人都可以调用处理程序
您还可以按照一些建议“选择”返回或仅捕获错误(conitnue/break/return)的处理程序。

因此可变参数问题消失了。

@brynbellomy 的else建议的一种替代方法:

a, b := try f() else err { /* handle error */ }

可能是在 else 之后立即支持装饰功能:

decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }

try a, b := f() else decorate
try c, d := g() else decorate

也许还有一些实用功能类似于:

decorate := fmt.DecorateErrorf("foo failed")

装饰函数可以具有签名func(error) error ,并在出现错误时由 try 调用,就在 try 从被尝试的关联函数返回之前。

这在精神上类似于提案文件中较早的“设计迭代”之一:

f := try(os.Open(filename), handler)              // handler will be called in error case

如果有人想要更复杂的东西或语句块,他们可以改用if (就像他们今天一样)。

也就是说,@brynbellomy 在https://github.com/golang/go/issues/32437#issuecomment -500949780 中的示例中显示的try的视觉对齐有一些好处。

如果选择该方法用于统一错误修饰,所有这些仍然可以与defer一起使用(或者甚至在理论上可能存在注册修饰函数的替代形式,但这是一个单独的点)。

无论如何,我不确定这里最好的是什么,但想明确另一个选项。

这是@brynbellomytry函数重写的示例,使用var块来保留@thepuddshttps://github.com/golang/go/issues中指出的良好对齐

package main

import (
    "fmt"
    "time"
)

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    var (
        headRef          = try(r.Head())
        parentObjOne     = try(headRef.Peel(git.ObjectCommit))
        parentObjTwo     = try(remoteBranch.Reference.Peel(git.ObjectCommit))
        parentCommitOne  = try(parentObjOne.AsCommit())
        parentCommitTwo  = try(parentObjTwo.AsCommit())
        treeOid          = try(index.WriteTree())
        tree             = try(r.LookupTree(treeOid))
        remoteBranchName = try(remoteBranch.Name())
    )

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return err
}

它与try -statement 版本一样简洁,我认为它同样具有可读性。 由于try是一个表达式,因此可以消除其中一些中间变量,但会牺牲一些可读性,但这似乎更像是一种风格问题。

不过,它确实提出了try如何在var块中工作的问题。 我假设var的每一行都算作一个单独的语句,而不是整个块都是一个语句,就何时分配的顺序而言。

如果“尝试”提案明确指出使用简单语句计数来近似测试覆盖率统计的工具(例如 cmd/cover)的后果,那将是很好的。 我担心不可见的错误控制流可能会导致计数不足。

@thepudds

尝试 a, b := f() 否则装饰

也许是我的脑细胞烧得太深了,但这对我的打击太大了

try a, b := f() ;catch(decorate)

和一个滑坡到

a, b := f()
catch(decorate)

我想你可以看到它的领先地位,对我来说比较

    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()

    try ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    )

(甚至是最后的捕获)
第二个更具可读性,但强调了下面的函数返回 2 个 vars 的事实,我们神奇地丢弃了一个,将它收集到 "magic returned err" 中。

    try(err) ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    ); err!=nil {
      //handle the err
    }

至少明确设置要返回的变量,并让我在函数中随时处理它。

只是插入一个特定的评论,因为我没有看到其他人明确提到它,特别是关于更改gofmt以支持以下单行格式或任何变体:

if f() { return nil, err }

请不。 如果我们想要单行if那么请制作单行if ,例如:

if f() then return nil, err

但是请,请,请不要接受删除换行符的语法沙拉,因为这些换行符会使使用大括号的代码更容易阅读。

我想强调几件在激烈的讨论中可能被遗忘的事情:

1) 这个提议的重点是让常见的错误处理淡入后台——错误处理不应该支配代码。 但仍应明确。 任何使错误处理更加突出的替代建议都没有抓住重点。 正如@ianlancetaylor已经说过的,如果这些替代建议不能显着减少样板文件的数量,我们可以继续使用if语句。 (减少样板的请求​​来自 Go 社区。)

2)对当前提案的抱怨之一是需要命名错误结果才能访问它。 除非替代方案引入了额外的语法,即更多样板文件(例如... else err { ... }等)来显式命名该变量,否则任何替代方案都会有同样的问题。 但有趣的是:如果我们不关心修饰错误并且不命名结果参数,但仍然需要显式return因为有一个显式处理程序,那么return语句将不得不枚举所有(通常为零)结果值,因为在这种情况下不允许裸返回。 特别是如果一个函数在没有修饰错误的情况下执行大量错误返回,则那些显式返回( return nil, err等)会添加到样板文件中。 当前的提案以及任何不需要明确的return的替代方案都取消了这一点。 另一方面,如果确实想要修饰错误,当前提案_要求_一个人命名错误结果(以及所有其他结果)以访问错误值。 这有一个很好的副作用,即在显式处理程序中可以使用裸返回,而不必重复所有其他结果值。 (我知道对裸返回有一些强烈的感觉,但现实是,当我们只关心错误结果时,必须枚举所有其他(通常为零)结果值是一件真正令人讨厌的事情 - 它没有增加任何对代码的理解)。 换句话说,必须命名错误结果以便对其进行修饰可以进一步减少样板。

@magical感谢您指出这一点。 我在发布提案后不久就注意到了同样的情况(但没有提出来以免进一步引起混淆)。 您是正确的, try无法扩展。 幸运的是,修复很容易。 (碰巧我们早期的内部提案没有这个问题 - 当我重写我们的最终版本以进行发布并尝试简化try以更紧密地匹配现有的参数传递规则时,它被引入了。这看起来不错- 但事实证明,有缺陷,而且大多无用 - 能够编写try(a, b, c, handle)的好处。)

try的早期版本大致定义如下: try(expr, handler)将一个(或可能两个)表达式作为参数,其中第一个表达式可能是多值的(仅当表达式为函数调用)。 该(可能是多值)表达式的最后一个值必须是error类型,并且该值针对 nil 进行测试。 (等等 - 你可以想象的其余部分)。

无论如何,关键是try语法上只接受一个,或者可能是两个表达式。 (但是描述try的语义有点困难。)结果将是这样的代码:

a, b := try(u, v, err)

将不再被允许。 但是首先没有理由让这项工作:在大多数情况下(除非ab被命名为结果)这个代码 - 如果出于某种原因很重要 - 可以很容易地重写为

a, b := u, v  // we don't care if the assignment happens in case of an error
try(err)

(或根据需要使用if语句)。 但是,这似乎并不重要。

该 return 语句将必须枚举所有(通常为零)结果值,因为在这种情况下不允许裸返回

不允许赤裸裸的退货,但可以尝试。 我喜欢 try(作为函数或语句)的一件事是,当返回错误时,我不再需要考虑如何设置非错误值,我只会使用 try。

@griesemer感谢您的解释。 这也是我得出的结论。

try作为声明的简短评论:我认为可以在https://github.com/golang/go/issues/32437#issuecomment -501035322 中的示例中看到, try埋葬了lede。 代码变成了一系列try语句,它掩盖了代码实际在做什么。

现有代码可能会在if err != nil块之后重用新声明的错误变量。 隐藏变量会破坏这一点,并且将命名的返回变量添加到函数签名中并不总是可以修复它。

也许最好保留错误声明/赋值,并找到一个单行错误处理 stmt。

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, hname err             // handler invocation without parens
on err, ignore err            // optional ignore handler logs error if defined

if err, return err            // alternatively, use if

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err }
   fmt.Println(clr.name, err)
}

try子表达式可能会出现恐慌,这意味着永远不会出现错误。 它的一个变体可以忽略任何错误。

f(try g()) // panic on error
f(try_ g()) // ignore any error

这个提议的重点是让常见的错误处理淡入后台——错误处理不应该支配代码。 但仍应明确。

我喜欢将try列为声明的评论的想法。 它很明确,仍然很容易掩盖(因为它是固定长度的),但不容易掩盖(因为它总是在同一个地方)它们可以隐藏在拥挤的线上。 如前所述,它也可以与defer fmt.HandleErrorf(...)结合使用,但它确实存在滥用命名参数以包装错误的陷阱(这对我来说仍然是一个聪明的 hack。聪明的 hack 是不好的。)

我不喜欢将try作为表达式的原因之一是它要么太容易掩盖,要么不容易掩盖。 举以下两个例子:

尝试作为表达

// Too hidden, it's in a crowded function with many symbols that complicate the function.

f, err := os.OpenFile(try(FileName()), os.O_APPEND|os.O_WRONLY, 0600)

// Not easy enough, the word "try" already increases horizontal complexity, and it
// being an expression only encourages more horizontal complexity.
// If this code weren't collapsed to multiple lines, it would be extremely
// hard to read and unclear as to what's executing when.

fullContents := try(io.CopyN(
    os.Stdout,
    try(net.Dial("tcp", "localhost:"+try(buf.ReadString("\n"))),
    try(strconv.Atoi(try(buf.ReadString("\n")))),
))

尝试作为陈述

// easy to see while still not being too verbose

try name := FileName()
os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)

// does not allow for clever one-liners, code remains much more readable.
// also, the code reads in sequence from top-to-bottom.

try port := r.ReadString("\n")
try lengthStr := r.ReadString("\n")
try length := strconv.Atoi(lengthStr)

try con := net.Dial("tcp", "localhost:"+port)
try io.CopyN(os.Stdout, con, length)

外卖

我承认,这段代码绝对是人为的。 但我要说的是,一般来说, try作为表达式在以下情况下效果不佳:

  1. 不需要太多错误检查的拥挤表达式的中间
  2. 需要大量错误检查的相对简单的多行语句

但是,我同意@ianlancetaylor的观点,即以try开头的每一行似乎确实妨碍了每个语句的重要部分(正在定义的变量或正在执行的函数)。 但是我认为因为它位于同一位置并且是固定宽度,所以更容易掩盖,同时仍然注意到它。 不过,每个人的眼光都不一样。

我还认为,在代码中鼓励聪明的单行代码通常只是一个坏主意。 我很惊讶我能像我的第一个例子那样制作出如此强大的单行代码,它是一个值得拥有自己的全部功能的片段,因为它做了很多事情——但如果我没有折叠它,它就可以放在一行上为便于阅读,改为多个。 一站式服务:

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

它从*bufio.Reader读取端口,启动 TCP 连接,并将相同*bufio.Reader指定的字节数复制到stdout 。 全部带有错误处理。 对于具有如此严格的编码约定的语言,我认为这甚至不应该被允许。 不过,我想gofmt可以帮助解决这个问题。

对于具有如此严格的编码约定的语言,我认为这甚至不应该被允许。

用 Go 编写可恶的代码是可能的。 甚至可以将其格式化得非常糟糕; 只有强有力的规范和工具反对它。 Go 甚至有goto

在代码审查期间,我有时会要求人们将复杂的表达式分解为多个语句,并使用有用的中间名。 出于同样的原因,我会为深度嵌套的try做类似的事情。

这就是说:让我们不要太努力地取缔糟糕的代码,代价是扭曲语言。 我们还有其他保持代码整洁的机制,这些机制更适合那些从根本上涉及人类判断的事情。

用 Go 编写可恶的代码是可能的。 甚至可以将其格式化得非常糟糕; 只有强有力的规范和工具反对它。 Go 甚至有 goto。

在代码审查期间,我有时会要求人们将复杂的表达式分解为多个语句,并使用有用的中间名。 出于同样的原因,我会为深度嵌套的尝试做类似的事情。

这就是说:让我们不要太努力地取缔糟糕的代码,代价是扭曲语言。 我们还有其他保持代码整洁的机制,这些机制更适合那些从根本上涉及人类判断的事情。

这是个好的观点。 我们不应该仅仅因为一个好主意可以用来制作糟糕的代码就将其取缔。 但是,我认为如果我们有一个可以促进更好代码的替代方案,那可能是一个好主意。 在@ianlancetaylor发表评论之前,我真的没有看到太多谈论_反对_ try背后的原始想法作为声明(没有所有else { ... }垃圾),但我可能只是错过了它。

此外,并不是每个人都有代码审查员,有些人(尤其是在遥远的将来)将不得不维护未经审查的 Go 代码。 Go 作为一门语言通常可以很好地确保几乎所有编写的代码都可以很好地维护(至少在go fmt之后),这是一个不容忽视的壮举。

话虽如此,当它真的不可怕时,我对这个想法非常批评。

如果我们允许它像之前提议的那样在表达式块上工作,即使不允许 else 块或错误处理程序,try as 语句确实显着减少了样板文件,而且不仅仅是表达式。 使用它,deandeveloper 的示例变为:

try (
    name := FileName()
    file := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
    port := r.ReadString("\n")
    lengthStr := r.ReadString("\n")
    length := strconv.Atoi(lengthStr)
    con := net.Dial("tcp", "localhost:"+port)
    io.CopyN(os.Stdout, con, length)
)

如果目标是减少if err!= nil {return err}样板,那么我认为允许获取代码块的语句 try 最有可能做到这一点,而不会变得不清楚。

@beoran那时,为什么要尝试呢? 只需允许缺少最后一个错误值的赋值,并使其表现得就像它是一个 try 语句(或函数调用)。 并不是我提出它,但它会更多地减少样板。

我认为这些 var 块会有效地减少样板文件,但我担心这可能会导致大量代码缩进一个额外的级别,这将是不幸的。

@deanveloper

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

我必须承认对我来说不可读,我可能会觉得我必须:

fullContents := try(io.CopyN(os.Stdout, 
                               try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))),
                                     try(strconv.Atoi(try(r.ReadString("\n"))))))

或类似的,为了可读性,然后我们在每行的开头都有一个“尝试”,并带有缩进。

好吧,我认为我们仍然需要尝试向后兼容,并且还需要明确说明可能在块中发生的返回。 但请注意,我只是遵循减少样板的逻辑,然后看看它会把我们引向何方。 减少样板文件和清晰度之间总是存在紧张关系。 我认为这个问题的主要问题是我们似乎都不同意平衡应该在哪里。

至于缩进,这就是 go fmt 的用途,所以我个人觉得这不是什么大问题。

我想加入战斗来提及另外两种可能性,每一种都是独立的,所以我将它们放在单独的帖子中。

我认为可以定义try() (不带参数)以返回指向错误返回变量的指针的建议是一个有趣的建议,但我并不热衷于这种双关语 - 它带有函数重载的味道,Go 避免的东西。

但是,我喜欢引用本地错误值的预定义标识符的一般概念。

那么,如何将err标识符本身预定义为错误返回变量的别名呢? 所以这将是有效的:

func foo() error {
    defer handleError(&err, etc)
    try(something())
}

它在功能上等同于:

func foo() (err error) {
    defer handleError(&err, etc)
    try(something())
}

err标识符将在 Universe 范围内定义,即使它充当函数本地别名,因此err的任何包级别定义或函数本地定义都会覆盖它。 这可能看起来很危险,但我在Go 语料库中扫描了 22m 行的 Go,这是非常罕见的。 只有 4 个不同的实例err用作全局(全部作为变量,而不是类型或常量) - 这是vet可以警告的事情。

范围内可能有两个函数错误返回变量; 在这种情况下,我认为编译器最好会抱怨存在歧义并要求用户明确命名正确的返回变量。 所以这将是无效的:

func foo() error {
    f := func() error {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

但你总是可以这样写:

func foo() error {
    f := func() (err error) {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

关于try作为预定义标识符而不是运算符的主题,
在写出括号时反复弄错括号后,我发现自己倾向于后者:

try(try(os.Create(filename)).Write(data))

在“我们为什么不能使用 ? like Rust”下,FAQ 说:

到目前为止,我们已经避免了语言中的隐晦缩写或符号,包括不常见的运算符,例如?,它们具有模棱两可或不明显的含义。

我不完全确定这是真的。 在您了解 Go 之前, .()运算符是不常见的,通道运算符也是如此。 如果我们添加一个?运算符,我相信它很快就会变得无处不在,不会成为一个重大障碍。

不过,Rust ?运算符添加在函数调用的右括号之后,这意味着当参数列表很长时很容易错过。

添加?()作为呼叫操作员怎么样:

所以而不是:

x := try(foo(a, b))

你会这样做:

x := foo?(a, b)

?()的语义将与建议的内置try的语义非常相似。 除了被调用的函数或方法必须返回错误作为其最后一个参数外,它的行为类似于函数调用。 与try一样,如果错误不为零,则?()语句将返回它。

似乎讨论已经足够集中,以至于我们现在围绕着一系列明确定义和讨论的权衡取舍。 这令人振奋,至少对我而言,因为妥协非常符合这种语言的精神。

@ianlancetaylor我绝对承认我们最终会得到几十行以try前缀的行。 但是,我不认为这比由两到四行条件表达式明确说明相同的return表达式后缀的数十行更糟糕。 实际上, try (带有else子句)可以更容易地发现错误处理程序何时执行特殊/非默认操作。 另外,切线地,重新:条件if表达式,我认为它们比建议的try -as-a-statement 更埋葬了lede:函数调用与条件语句位于同一行,条件本身在已经很拥挤的行的最后结束,并且变量赋值的范围是块(如果在块之后需要这些变量,则需要使用不同的语法)。

@josharian我最近有过这样的想法。 Go 力求实用主义,而不是完美主义,它的发展往往似乎是数据驱动的,而不是原则驱动的。 你可以写出糟糕的 Go,但通常比写出体面的 Go 更难(这对大多数人来说已经足够好了)。 同样值得指出的是——我们有很多工具来对抗糟糕的代码:不仅仅是gofmtgo vet ,还有我们的同事,以及这个社区(非常小心地)精心打造的文化来引导自己。 我不想仅仅因为某个地方的某个人可能会自己开枪而避开有助于一般情况的改进。

@beoran这很优雅,当您考虑它时,它实际上在语义上与其他语言的try块不同,因为它只有一个可能的结果:从函数返回时出现未处理的错误。 但是: 1)这可能会让使用其他语言的新 Go 编码人员感到困惑(老实说,这不是我最关心的问题;我相信程序员的智慧),2)这将导致大量代码在许多代码中缩进代码库。 就我的代码而言,出于这个原因,我什至倾向于避免现有的type / const / var块。 此外,目前允许这样的块的唯一关键字是定义,而不是控制语句。

@yiyus我不同意删除关键字,因为明确性(在我看来)是 Go 的优点之一。 但我同意缩进大量代码以利用try表达式是一个坏主意。 所以也许根本没有try块?

@rogpeppe我认为这种微妙的运算符只对不应该返回错误的调用是合理的,如果他们这样做了就会恐慌。 或者调用你总是忽略错误的地方。 但两者似乎都很少见。 如果您愿意接受新的运营商,请参阅#32500。

我建议f(try g())应该在https://github.com/golang/go/issues/32437#issuecomment -501074836 中恐慌,以及 1 行处理 stmt:
on err, return ...

我认为try ... else { ... }中的可选else $ 会将代码向右推太多,可能会使其模糊。 我希望错误块大部分时间至少需要 25 个字符。 此外,到目前为止,块并没有被go fmt保留在同一行,我希望这种行为将保留try else 。 因此,我们应该讨论和比较else块位于单独一行的示例。 但即便如此,我也不确定行尾else {的可读性。

@yiyus https://github.com/golang/go/issues/32437#issuecomment -501139662

@beoran那时,为什么要尝试呢? 只需允许缺少最后一个错误值的赋值,并使其表现得就像它是一个 try 语句(或函数调用)。 并不是我提出它,但它会更多地减少样板。

这是无法做到的,因为 Go1 已经允许将func foo() error称为foo() 。 将, error添加到调用者的返回值会改变该函数内现有代码的行为。 见https://github.com/golang/go/issues/32437#issuecomment -500289410

@rogpeppe在您关于使用嵌套try的括号正确的评论中:您对try的优先级有任何意见吗? 另请参阅有关此主题的详细设计文档

@griesemer由于那里指出的原因,我确实并不热衷于将try作为一元前缀运算符。 我想到另一种方法是允许try作为函数返回元组的伪方法:

 f := os.Open(path).try()

我认为这解决了优先级问题,但它并不是真的很像 Go。

@rogpeppe

很有意思! . 你可能真的在这里。

那么我们如何扩展这个想法呢?

for _,fp := range filepaths {
    f := os.Open(path).try(func(err error)bool{
        fmt.Printf( "Cannot open file %s\n", fp );
        continue;
    });
}

顺便说一句,与try()相比,我可能更喜欢不同的名称,例如guard() ,但在其他人讨论架构之前,我不应该使用这个名称。

与:

for _,fp := range filepaths {
    if f,err := os.Open(path);err!=nil{
        fmt.Printf( "Cannot open file %s\n", fp )
    }
}

?

我喜欢try a,b := foo()而不是if err!=nil {return err}因为它替换了非常简单的案例的样板。 但是对于添加上下文的其他所有内容,我们真的需要if err!=nil {...}之外的其他东西(很难找到更好的东西)?

如果装饰/包装通常需要额外的一行,那么我们就为它“分配”一行。

f, err := os.Open(path)  // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)

@networkimprov我想我也可以这样。 推动一个我已经提出的更具头韵和描述性的术语......

f, err := os.Open(path)
relay err { nil, fmt.Errorf("Cannot open %s, due to %v", path, err) }

// marginally shorter, doesn't trigger vertical formatting unless excessively wide
// enclosed expression restricted to a list of values that match the return args

要么

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

// somewhere between statement and func, prob more pleasing to type w/out completion
// trailing expression restricted to a list of values that match the return args
// maybe excessive width triggers linting noise - with a reformatter available
// providing a reformatter would make swapping old (narrow enough) code easy

@daved很高兴你喜欢它! on err, ...将允许任何单 stmt 处理程序:

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, continue              // retry in a loop
on err, hname err             // named handler invocation without parens
on err, ignore err            // logs error if handle ignore() defined

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err } // non-local return
   fmt.Println(clr, err)
}

编辑: on从 Javascript 借用。 我不想超载if
逗号不是必需的,但我不喜欢那里的分号。 也许是结肠?

我不太关注relay ; 这意味着返回错误?

当满足某些条件时,保护继电器会​​跳闸。 在这种情况下,当错误值不为零时,继电器会更改控制流以使用后续值返回。

*我不想在这种情况下重载, ,并且不喜欢on这个术语,但我喜欢代码结构的前提和整体外观。

对于@josharian之前的观点,我觉得关于匹配括号的讨论的很大一部分主要是假设的并且使用人为的例子。 我不了解你,但我发现自己在日常编程中编写函数调用并不困难。 如果我到达一个表达式难以阅读或理解的地步,我会使用中间变量将它分成多个表达式。 我不明白为什么在实践中使用函数调用语法的try()在这方面会有所不同。

@eandre通常,函数没有这样的动态定义。 该提案的许多形式都会降低控制流通信的安全性,这很麻烦。

@networkimprov @daved我不喜欢这两个想法,但是与简单地允许单行if err != nil { ... }语句来保证语言更改相比,它们感觉还不够。 此外,在您只是返回错误的情况下,它是否可以减少重复的样板文件? 还是您总是必须写出return的想法?

@brynbellomy在我的示例中,没有returnrelay是一个保护继电器,定义为“如果这个err不为nil,则返回以下内容”。

使用我之前的第二个示例:

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

也可能是这样的:

f, err := os.Open(path)
relay(err)

随着使继电器跳闸的错误与其他返回值的零值一起返回(或为命名返回值设置的任何值)。 另一种可能有用的形式:

wrap := func(err error, msg string) error {
    if err != nil {
        fmt.Errorf("%s: %s", msg, err)
    }
    return nil
}

// ...

f, err := os.Open(path)
relay(err, wrap(err, fmt.Sprintf("Cannot open %s", path)))

除非第一个继电器 arg 使继电器跳闸,否则不会调用第二个继电器 arg。 可选的第二个中继错误 arg 将是返回的值。

_go fmt_ 应该允许单行if但不允许case, for, else, var ()吗? 我想要他们所有人,拜托;-)

Go 团队已经拒绝了许多单行错误检查的请求。

on err, return err语句可能是重复的,但它们是明确的、简洁的和清晰的。

@magical您的反馈已在详细提案的更新版本中得到解决。

一件小事,但如果try是一个关键字,它可以被识别为一个终止语句,所以而不是

func f() error {
  try(g())
  return nil
}

你可以做

func f() error {
  try g()
}

try -statement 免费获得, try $ -operator 需要特殊处理,我意识到上面不是一个很好的例子:但它是最小的)

@jimmyfrasche try可以被识别为终止语句,即使它不是关键字 - 我们已经使用panic做到了这一点,除了我们已经做的之外,不需要额外的特殊处理。 但除此之外, try并不是一个终止语句,试图人为地使它成为一个似乎很奇怪。

所有有效积分。 我想它只能被认为是一个终止语句,如果它是一个只返回错误的函数的最后一行,比如详细提案中的CopyFile ,或者它被用作try(err)if中,众所周知err != nil 。 似乎不值得。

由于这条线索变得冗长且难以遵循(并且开始在一定程度上重复自身),我认为我们都同意我们需要就“任何提案提供的一些好处”做出妥协。

当我们不断喜欢或不喜欢上面提出的代码排列时,我们并没有帮助自己真正了解“这是比另一个/已经提供的更明智的妥协”吗?

我认为我们需要一些客观的标准来评估我们的“尝试”变化和替代建议。

  • 它会减少样板吗?
  • 可读性
  • 增加了语言的复杂性
  • 错误标准化
  • Go-ish
    ...
    ...
  • 实施工作和风险
    ...

当然,我们也可以为禁止使用设置一些基本规则(没有向后兼容性是一个),并为“它看起来是否吸引人/直觉等”留下一个灰色区域(上面的“硬”标准也可以商榷.. .)。

如果我们针对此列表测试任何提案,并对每个点进行评分(样板 5 分,可读性 4 分等),那么我认为我们可以对齐:
我们的选项可能是 A、B 和 C,此外,希望添加新提案的人可以(在一定程度上)测试他的提案是否符合标准。

如果这是有道理的,请点,我们可以尝试重新审视最初的提议
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

也许其他一些建议嵌入评论或链接,也许我们会学到一些东西,甚至想出一个评分更高的组合。

标准 += 跨包和函数内重用错误处理代码

感谢大家对此提案的持续反馈。

讨论偏离了核心问题。 它也已经被十几个贡献者(你知道你是谁)主导,讨论什么相当于替代提案。

因此,让我提出一个友好的提醒,这个问题是关于一个_具体_的提案。 这_不是_为错误处理征求新颖的句法思想(这是一件好事,但不是_这个_问题)。

让我们再次让讨论更加集中并回到正轨。

如果反馈有助于识别我们错过的技术事实,例如“这个提议在这种情况下不起作用”或“它会产生我们没有意识到的这种暗示”,那么反馈是最有成效的。

例如, @magical指出,所写的提案并不像声称的那样可扩展(原始文本可能无法添加未来的第二个论点)。 幸运的是,这是一个小问题,只需对提案进行小幅调整即可轻松解决。 他的意见直接帮助使提案变得更好。

@crawshaw花时间分析了 std 库中的几百个用例,并表明try很少在另一个表达式中结束,因此直接驳斥了try可能被掩埋和不可见的担忧。 这是非常有用的基于事实的反馈,在这种情况下验证了设计。

相比之下,个人的_审美_判断不是很有帮助。 我们可以注册该反馈,但我们无法对其采取行动(除了提出另一个提案之外)。

关于提出替代方案:目前的方案是大量工作的成果,从去年的设计草案开始。 我们已经对该设计进行了多次迭代,并征求了许多人的反馈,然后我们才觉得可以发布它并建议将其推进到实际的实验阶段,但我们还没有完成实验。 如果实验失败,或者如果反馈提前告诉我们它显然会失败,那么回到绘图板确实是有意义的。 如果我们根据第一印象即时重新设计,我们只是在浪费每个人的时间,更糟糕的是,在此过程中什么也学不到。

话虽如此,许多人对这个提议表达的最重要的担忧是,除了我们已经可以在语言中做的事情之外,它并没有明确鼓励错误修饰。 谢谢,我们已经记录了该反馈。 在发布此提案之前,我们在内部收到了完全相同的反馈。 但是我们考虑过的任何替代方案都没有比我们现在拥有的更好(而且我们已经深入研究了很多)。 相反,我们决定提出一个最小的想法,它很好地解决了错误处理的一部分,如果需要,可以扩展它,正是为了解决这个问题(该提案详细讨论了这个问题)。

谢谢。

(我注意到一些提倡替代提案的人已经开始了他们自己的单独问题。这是一件好事,有助于保持各自问题的重点。谢谢。)

@griesemer
我完全同意我们应该集中精力,这正是我写作的原因:

如果这是有道理的,请点,我们可以尝试重新审视最初的提议
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

两个问题:

  1. 如果我们标记优点(样板减少、可读性等)与缺点(没有明确的错误修饰/错误线源的较低可追溯性),您是否同意,我们实际上可以声明:这个提议高度旨在解决 a、b,在某种程度上帮助 c,不旨在解决 d,e
    这样一来,“但它不d”,“它怎么能e”的所有混乱都消失了,并更多地解决了@magical指出的技术问题
    并且不鼓励评论“但解决方案 XXX 解决了 d,e 更好。
  2. 许多内联帖子是“对提案进行细微更改的建议” - 我知道这是一条很好的路线,但我认为保留这些确实有意义。

LMKWYT。

是否使用try()零参数(或不同的内置)仍在考虑中或已被排除。

在对提案进行更改之后,我仍然担心它如何使命名返回值的使用更加“普遍”。 但是我没有数据支持:upside_down_ face:。
如果try()零参数(或不同的内置函数)被添加到提案中,是否可以更新提案中的示例以使用try() (或不同的内置函数)以避免命名返回?

@guybrand赞成和反对是表达_sentiment_ 的好东西- 仅此而已。 那里没有更多信息。 我们不会根据投票数(即仅凭情绪)做出决定。 当然,如果每个人(比如 90% 以上)都讨厌某个提案,那可能是一个不好的迹象,我们应该在继续之前三思而后行。 但这里的情况似乎并非如此。 很多人似乎对尝试一些事情感到满意,并且已经转向其他事情(并且不要费心评论这个帖子)。

正如我在上面试图表达的那样,提案的这个阶段的情绪不是基于对该功能的任何实际经验; 这是一种感觉。 感觉往往会随着时间而改变,尤其是当一个人有机会真正体验到这种感觉所涉及的主题时...... :-)

@Goodwine没有人排除try()得到错误值; 虽然 _if_ 需要这样的东西,但最好像@rogpeppe建议的那样预先声明一个err变量(我认为)。

同样,该提案并未排除任何这种可能性。 如果我们发现有必要,我们就去那里。

@griesemer
我想你完全误解了我的意思。
我不是在考虑对这个或任何提案进行投票/否决,我只是在寻找一种方法来很好地了解“我们认为根据硬标准做出决定而不是‘我喜欢 x '或者'你看起来不好看'"

从你写的 - 这正是你的想法......所以请投票支持我的评论,如下所示:

“我认为我们应该列出该提案旨在改进的内容,并在此基础上我们可以
A. 决定这是否足够有意义
B. 决定提案是否真的解决了它想要解决的问题
C.(正如你所补充的)做出额外的努力来看看它是否可行......

@guybrand他们显然确信值得在预发布 1.14(?)中进行原型设计并收集动手用户的反馈。 IOW 已做出决定。

此外,提交 #32611 以讨论on err, <statement>

@guybrand我很抱歉。 是的,我同意我们需要查看提案的各种属性,例如减少样板文件,它是否解决了手头的问题等等。但是提案不仅仅是其各个部分的总和 - 归根结底,我们需要看大局。 这是工程,工程是混乱的:设计中有很多因素,即使客观地(基于硬标准)设计的一部分不令人满意,它可能仍然是整体上“正确”的设计。 因此,我毫不犹豫地支持基于对提案各个方面的某种“独立”评级的决定。

(希望这能更好地解决你的意思。)

但关于相关标准,我相信这个提案清楚地说明了它试图解决的问题。 也就是说,您所指的列表已经存在:

...,我们的目标是通过减少专门用于错误检查的源代码量来使错误处理更加轻量级。 我们还希望更方便地编写错误处理代码,以提高程序员花时间去做的可能性。 同时,我们确实希望保持错误处理代码在程序文本中明确可见。

碰巧的是,对于错误装饰,我们建议使用defer和命名的结果参数(或 ye olde if语句),因为这不需要更改语言 - 这是一件很棒的事情因为语言变化有巨大的隐性成本。 我们确实得到很多评论者认为这部分设计“完全糟糕”。 尽管如此,在这一点上,就我们所知,总体而言,我们认为这可能已经足够好了。 另一方面,我们需要更改语言 - 语言支持,而不是 - 以摆脱样板文件,而try几乎是我们能想到的最小更改。 显然,代码中的一切仍然是明确的。

我想说,有这么多反应和这么多小建议的原因是,这是一个几乎每个人都同意 Go 语言确实需要做一些事情来减少错误处理的样板,但我们真的没有就如何做达成一致。

这个提议本质上归结为一个内置的“宏”,用于一个非常常见但特定的样板案例,很像内置的append()函数。 因此,虽然它对于特定的id err!=nil { return err }特定用例很有用,但它也仅此而已。 由于它在其他情况下不是很有帮助,也不是真正普遍适用,所以我会说它令人印象深刻。 我感觉大多数 Go 程序员都期待更多,因此该线程中的讨论仍在继续。

作为一个函数,它是反直觉的。 因为在 Go 中不可能有具有这种参数顺序的函数func(... interface{}, error)
先输入然后可变数量的任何模式在 Go 模块中无处不在。

我想得越多,我就喜欢目前的提议。

如果我们需要错误处理,我们总是有 if 语句。

大家好。 感谢您迄今为止进行的平静、尊重和建设性的讨论。 我花了一些时间做笔记,最终感到非常沮丧,以至于我构建了一个程序来帮助我维护这个评论线程的不同视图,它应该比 GitHub 显示的更易于导航和完整。 (它的加载速度也更快!)请参阅https://swtch.com/try.html。 我会保持更新,但分批更新,而不是每分钟更新一次。 (这是一个需要仔细思考的讨论,并且没有“互联网时间”的帮助。)

我有一些想法要补充,但这可能要等到星期一。 再次感谢。

@mishak87我们在详细的提案中解决了这个问题。 请注意,我们还有其他“不规则”的内置插件( trymakeunsafe.Offsetof等)——这就是内置插件的用途。

@rsc ,超级有用! 如果您仍在修改它,也许链接 #id 问题参考? 还有字体风格的无衬线字体?

这可能已经被覆盖过,所以我很抱歉添加了更多的噪音,但只是想说明一下 try builtin 与 try ... else 的想法。

我认为尝试内置函数在开发过程中可能会有点令人沮丧。 我们可能偶尔想在返回之前添加调试符号或添加更多特定于错误的上下文。 一个人将不得不重新写一行

user := try(getUser(userID))

user, err := getUser(userID)
if err != nil {  
    // inspect error here
    return err
}

添加 defer 语句会有所帮助,但当函数抛出多个错误时,它仍然不是最佳体验,因为它会在每次 try() 调用时触发。

在同一个函数中重写多个嵌套的 try() 调用会更烦人。

另一方面,将上下文或检查代码添加到

user := try getUser(userID)

就像在末尾添加一个 catch 语句,然后是代码一样简单

user := try getUser(userID) catch {
   // inspect error here
}

删除或暂时禁用处理程序就像在捕获之前断行并将其注释掉一样简单。

try()if err != nil之间切换感觉更烦人。

这也适用于添加或删除错误上下文。 可以在快速制作原型的同时编写try func() ,然后在程序成熟时根据需要为特定错误添加上下文,而不是像try()作为内置程序,必须重新编写在调试期间添加上下文或添加额外检查代码的行。

我确信try ... catch () 会很有用,但正如我想象在我的日常工作中使用它一样,我不禁想象当我' d 需要添加/删除特定于某些错误的额外代码。


另外,我觉得添加try()然后建议使用if err != nil添加上下文与make() vs new() vs :=非常相似var 。 这些功能在不同的场景中很有用,但如果我们有更少的方法甚至是单一的方法来初始化变量,那不是很好吗? 当然,没有人强迫任何人使用 try 并且人们可以继续使用 if err != nil 但我觉得这会拆分 Go 中的错误处理,就像分配新变量的多种方式一样。 我认为添加到语言中的任何方法也应该提供一种轻松添加/删除错误处理程序的方法,而不是强迫人们重写整行来添加/删除处理程序。 这对我来说并不是一个好的结果。

再次为噪音感到抱歉,但想指出以防有人想为try ... else的想法写一个单独的详细提案。

//cc @brynbellomy

感谢@owais再次提出这个问题 - 这是一个公平的观点(之前确实提到了调试问题)。 try确实为扩展打开了大门,例如第二个参数,它可能是一个处理函数。 但是try函数确实不会使调试更容易 - 与try - catchtry相比,可能需要重写代码。 else

@owais

添加 defer 语句会有所帮助,但当函数抛出多个错误时,它仍然不是最佳体验,因为它会在每次 try() 调用时触发。

你总是可以在延迟函数中包含一个类型开关,它会在返回之前以适当的方式处理(或不处理)不同类型的错误。

鉴于迄今为止的讨论——特别是 Go 团队的回应——我得到了强烈的印象,该团队计划推进摆在桌面上的提案。 如果是,则发表评论和请求:

  1. IMO 的原样提案将导致公开可用的存储库中代码质量的显着降低。 我的期望是许多开发人员会走阻力最小的道路,有效地使用异常处理技术,并选择使用try()而不是在错误发生时处理它们。 但是鉴于该线程上的普遍情绪,我意识到现在任何哗众取宠都只会打一场失败的战斗,所以我只是为后代表达我的反对意见。

  2. 假设团队确实按照当前编写的提案继续前进,您能否添加一个编译器开关,该开关将禁用try()对于那些不想要任何以这种方式忽略错误的代码并禁止他们雇用的程序员的人从使用它? _(当然是通过 CI。)_ 提前感谢您的考虑。

你能添加一个编译器开关来禁用 try()

这必须在 linting 工具上,而不是在编译器 IMO 上,但我同意

这必须在 linting 工具上,而不是在编译器 IMO 上,但我同意

我明确要求编译器选项而不是 linting 工具,因为不允许编译选项。 否则在本地开发过程中很容易“忘记” lint。

@mikeschinkel在那种情况下忘记打开编译器选项不是很容易吗?

编译器标志不应更改语言规范。 这更适合兽医/皮棉

在那种情况下忘记打开编译器选项不是很容易吗?

在使用 GoLand 之类的工具时,无法强制在编译前运行 lint。

编译器标志不应更改语言规范。

-nolocalimports更改规范, -s发出警告。

编译器标志不应更改语言规范。

-nolocalimports更改规范, -s发出警告。

不,它不会改变规格。 不仅语言的语法继续保持不变,而且规范特别指出:

ImportPath 的解释取决于实现,但它通常是已编译包的完整文件名的子字符串,并且可能与已安装包的存储库相关。

在使用 GoLand 之类的工具时,无法强制在编译前运行 lint。

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

@deanveloper

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

当然存在,但是您正在将苹果与器官进行比较。 您展示的是一个文件观察器,它在文件更改时运行,并且由于 GoLand 自动保存文件,这意味着它会不断运行,产生的噪音远大于信号。

lint 始终不会也不能(AFAIK)配置为运行编译器的先决条件:

image

不,它不会改变规格。 不仅语言的语法继续保持不变,而且规范特别指出:

你在这里玩语义而不是关注结果。 所以我也会这样做。

我要求添加一个编译器选项,该选项将禁止使用try()编译代码。 这不是更改语言规范的请求,它只是请求编译器在这种特殊情况下停止。

如果它有帮助,语言规范可以更新为:

try()的解释是依赖于实现的,但它通常是在最后一个参数是错误时触发返回的解释,但是它可以实现为不允许。

要求编译器切换或审查检查的时间是在try()原型登陆 1.14(?) 提示之后。 那时你会为它提交一个新问题(是的,我认为这是一个好主意)。 我们被要求将此处的评论限制为有关当前设计文档的事实输入。

您好,只是为了在开发过程中添加调试语句等来解决整个问题。
我认为第二个参数的想法对于try()函数来说很好,但另一个想法只是把它扔在那里,添加一个emit子句作为try()的第二部分

例如,我相信在开发时可能会出现这样的情况,即我想在这一瞬间调用fmt来打印错误。 所以我可以从这个开始:

func writeStuff(filename string) (io.ReadCloser, error) {
    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

可以重写为类似这样的调试语句或一般处理或返回之前的错误。

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        fmt.Printf("something happened [%v]\n", err.Error())
        return nil, err
    }

    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

所以在这里我最终提出了一个新关键字emit的提案,它可以是一个声明或一个用于立即返回的语句,就像最初的try()功能一样:

emit return nil, err

如果try()被不等于 nil 的错误触发,则 emit 本质上只是一个子句,您可以在其中放入任何您希望的逻辑。 emit关键字的另一个功能是,如果您在关键字之后添加一个变量名,就像我在第一个使用它的示例中所做的那样,您就可以在那里访问错误。

这个提议确实try()函数造成了一些冗长,但我认为它至少更清楚地说明了错误发生的情况。 这样,您也可以修饰错误,而不会将其全部卡在一行中,并且您可以在阅读函数时立即看到错误是如何处理的。

这是对@mikeschinkel的回复,我将我的回复放在一个详细信息块中,这样我就不会过多地混淆讨论。 无论哪种方式, @networkimprov都是正确的,应该在此提案实施之前提交此讨论(如果确实如此)。

有关禁用尝试的标志的详细信息
@mikeschinkel

lint 始终不会也不能(AFAIK)配置为运行编译器的先决条件:

重新安装 GoLand 只是为了测试这一点。 这似乎工作得很好,唯一的区别是如果 lint 找到了它不喜欢的东西,它不会使编译失败。 不过,这可以通过自定义脚本轻松修复,该脚本运行golint并在有任何输出时以非零退出代码失败。
image

(编辑:我修复了它试图在底部告诉我的错误。即使存在错误,它也运行良好,但是将“运行种类”更改为目录删除了错误并且工作正常)

这也是为什么它不应该是编译器标志的另一个原因 - 所有 Go 代码都是从源代码编译的。 这包括图书馆。 这意味着,如果您想通过编译器关闭try ,您也将关闭您正在使用的每个库的try 。 让它成为编译器标志只是一个坏主意。

你在这里玩语义而不是关注结果。

不我不是。 编译器标志不应更改语言规范。 规范的布局非常好,为了使某些东西成为“Go”,它需要遵循规范。 您提到的编译器标志确实改变了语言的行为,但无论如何,它们确保语言仍然遵循规范。 这是 Go 的一个重要方面。 只要你遵循 Go 规范,你的代码应该可以在任何 Go 编译器上编译。

我要求添加一个编译器选项,该选项将禁止使用 try() 编译代码。 这不是更改语言规范的请求,它只是请求编译器在这种特殊情况下停止。

这是更改规格的请求。 该提案本身就是更改规范的请求。 内置函数非常具体地包含在规范中。 . 因此,要求有一个删除try内置的编译器标志将是一个编译器标志,它将改变正在编译的语言的规范。

话虽如此,我认为ImportPath应该在规范中标准化。 我可以为此提出一个建议。

如果有帮助,可以更新语言规范以说出类似 [...]

虽然这是真的,但您不希望try的实现依赖于实现。 它已成为语言错误处理的重要组成部分,这在每个 Go 编译器中都需要相同。

@deanveloper

_“无论哪种方式, @networkimprov都是正确的,应该在此提案实施之前提交此讨论(如果确实如此)。”_

那你为什么决定忽略这个建议并在这个线程中发布而不是等待以后呢? 你在这里争论你的观点,同时断言我不应该挑战你的观点。 实践你所宣扬的...

给你选择,我也会选择回应,也在细节块中

这里:

_“这可以很容易地用一个自定义脚本来修复,它运行 golint 并且如果有任何输出则以非零退出代码失败。”_

是的,只要有足够的编码,_any_ 问题就可以解决。 但我们都从经验中知道,解决方案越复杂,想要使用它的人就越少,最终会使用它。

所以我在这里明确要求一个简单的解决方案,而不是一个自己动手的解决方案。

_“你也会关闭你正在使用的每一个库的尝试。”_

就是我提出要求的_明确_原因。 因为我想确保所有使用这个麻烦的_“特性”_的代码都不会进入我们分发的可执行文件。

_“这是更改规范的请求。该提案本身就是更改规范的请求。_”

绝对不是对规范的更改。 这是请求切换以更改build命令的_behavior_,而不是更改语言规范。

如果有人要求go命令有一个开关来以普通话显示其终端输出,这并不是对语言规范的更改。

同样,如果go build看到这个开关,那么它会简单地发出一条错误消息并在遇到try()时停止。 无需更改语言规范。

_“它已成为语言错误处理的重要组成部分,这在每个 Go 编译器中都需要相同。”_

这将是该语言错误处理的一个有问题的部分,并且使其成为可选将允许那些想要避免其问题的人能够这样做。

如果没有开关,大多数人可能只会将其视为一项新功能并接受它,而不会问自己是否真的应该使用它。

_使用 switch_——以及解释提到 switch 的新特性的文章——许多人会理解它有潜在的问题,因此 Go 团队可以通过查看有多少公共代码避免使用它来研究它是否是一个好的包含与公共代码如何使用它。 这可以为 Go 3 的设计提供信息。

_“不,我不是。编译器标志不应该改变语言的规范。”_

说你没有玩语义并不意味着你没有玩语义。

美好的。 然后我改为请求一个名为 _(something like)_ build-guard的新顶级命令,用于在编译期间禁止有问题的功能,从禁止try()开始。

当然,最好的结果是,如果try()功能计划在未来重新考虑以不同的方式解决问题,这是绝大多数人都同意的方式。 但我担心这艘船已经以try()航行,所以我希望将其不利因素降到最低。


所以现在,如果您真的同意@networkimprov ,那么请按照他们的建议,稍后再回复。

抱歉打断,但我有事实要报告:-)

我确信 Go 团队已经对 defer 进行了基准测试,但我还没有看到任何数字......

$ go test -bench=.
goos: linux
goarch: amd64
BenchmarkAlways2-2      20000000                72.3 ns/op
BenchmarkAlways4-2      20000000                68.1 ns/op
BenchmarkAlways6-2      20000000                68.0 ns/op

BenchmarkNever2-2       100000000               16.5 ns/op
BenchmarkNever4-2       100000000               13.1 ns/op
BenchmarkNever6-2       100000000               13.5 ns/op

资源

package deferbench

import (
   "fmt"
   "errors"
   "testing"
)

func Always(iM, iN int) (err error) {
   defer func() {
      if err != nil {
         err = fmt.Errorf("d: %v", err)
      }
   }()
   if iN % iM == 0 {
      return errors.New("e")
   }
   return nil
}

func Never(iM, iN int) (err error) {
   if iN % iM == 0 {
      return fmt.Errorf("r: %v", errors.New("e"))
   }
   return nil
}

func BenchmarkAlways2(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e2, a) }}
func BenchmarkAlways4(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e4, a) }}
func BenchmarkAlways6(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e6, a) }}

func BenchmarkNever2(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e2, a) }}
func BenchmarkNever4(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e4, a) }}
func BenchmarkNever6(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e6, a) }}

@networkimprov

来自https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#efficiency -of-defer (我的重点是粗体)

独立地,Go 运行时和编译器团队一直在讨论替代实现选项,我们相信我们可以使典型的 defer 用于错误处理,与现有的“手动”代码一样高效。 我们希望在 Go 1.14 中提供这种更快的延迟实现(另请参阅* CL 171758 *这是朝这个方向迈出的第一步)。

ie defer 现在对于 go1.13 的常用性能提高了 30%,并且应该比 go 1.14 中的 non-defer 模式更快、更高效

也许有人可以发布 1.13 和 1.14 CL 的数字?

优化并不总能在与敌人的接触中幸存下来……呃,生态系统。

1.13 的延迟将快 30% 左右:

name     old time/op  new time/op  delta
Defer-4  52.2ns ± 5%  36.2ns ± 3%  -30.70%  (p=0.000 n=10+10)

这是我在上面的@networkimprov测试中得到的(1.12.5 到小费):

name       old time/op  new time/op  delta
Always2-4  59.8ns ± 1%  47.5ns ± 1%  -20.57%  (p=0.008 n=5+5)
Always4-4  57.9ns ± 2%  43.5ns ± 1%  -24.96%  (p=0.008 n=5+5)
Always6-4  57.6ns ± 2%  44.1ns ± 1%  -23.43%  (p=0.008 n=5+5)
Never2-4   13.7ns ± 8%   3.8ns ± 4%  -72.27%  (p=0.008 n=5+5)
Never4-4   10.5ns ± 6%   1.3ns ± 2%  -87.76%  (p=0.008 n=5+5)
Never6-4   10.8ns ± 6%   1.2ns ± 1%  -88.46%  (p=0.008 n=5+5)

(我不确定为什么 Never 的速度要快得多。也许内联更改?)

1.14 的延迟优化还没有实现,所以我们不知道性能会如何。 但我们认为我们应该接近常规函数调用的性能。

那你为什么决定忽略这个建议并在这个线程中发布而不是等待以后呢?

在我阅读了@networkimprov的评论后,稍后编辑了详细信息块。 我很抱歉让它看起来好像我已经理解了他所说的并忽略了它。 在此声明之后我将结束讨论,我想解释一下自己,因为你问我为什么发表评论。


关于延迟的优化,我为他们感到兴奋。 他们帮助了这个提议一点点,使defer HandleErrorf(...)不那么沉重。 不过,我仍然不喜欢滥用命名参数以使这个技巧起作用的想法。 预计 1.14 会加速多少? 他们应该以相似的速度运行吗?

@griesemer一个可能值得进一步扩展的领域是过渡如何在具有try的世界中工作,可能包括:

  • 在错误装饰样式之间转换的成本。
  • 在样式之间转换时可能导致的错误类别。
  • (a) 编译器错误会立即捕获哪些错误类别,而 (b) 会被vetstaticcheck或类似错误捕获,而 (c) 可能会导致错误可能不会被注意到或需要通过测试来发现。
  • 在样式之间转换时,工具可以降低成本和出错机会的程度,特别是gopls (或其他实用程序)是否可以或应该在自动化常见装饰样式转换中发挥作用。

错误修饰阶段

这并不详尽,但一组具有代表性的阶段可能类似于:

0.没有错误修饰(例如,使用try没有任何修饰)。
1.统一错误修饰(例如,使用try + defer进行统一修饰)。
2. N-1 个出口点具有统一的错误修饰,但1 个出口点具有不同的修饰(例如,可能是仅在一个位置的永久详细错误修饰,或者可能是临时调试日志等)。
3.所有出口点都有独特的错误装饰,或接近独特的东西。

任何给定的功能都不会在这些阶段有严格的进展,所以“阶段”可能是错误的词,但有些功能会从一种装饰风格过渡到另一种装饰风格,更明确地说明这些过渡可能是有用的就像它们何时或是否发生。

阶段 0 和阶段 1 似乎是当前提案的最佳点,而且恰好也是相当常见的用例。 阶段 0->1 的过渡似乎很简单。 如果您在第 0 阶段使用没有任何装饰的try ,则可以添加类似defer fmt.HandleErrorf(&err, "foo failed with %s", arg1)的内容。 那时您可能还需要在最初编写的提案下引入命名返回参数。 但是,如果提案采用了预定义内置变量的建议之一,该变量是最终错误结果参数的别名,那么这里的错误成本和风险可能很小?

另一方面,如果阶段 1 是带有defer的统一错误装饰,则阶段 1->2 的过渡似乎很尴尬(或像其他人所说的那样“令人讨厌”)。 要在一个出口点添加一个特定的装饰,首先您需要删除defer (以避免双重装饰),然后似乎需要访问所有返回点以对try脱糖if语句,其中 N-1 个错误以相同方式修饰,1 个错误以不同方式修饰。

如果手动完成,阶段 1->3 的过渡也似乎很尴尬。

装修风格转换时的误区

作为手动脱糖过程的一部分可能发生的一些错误包括意外隐藏变量,或更改命名返回参数的影响方式等。例如,如果您查看“示例”部分中的第一个也是最大的示例试试建议, CopyFile函数有 4 个try用途,包括在本节中:

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

如果有人对w := try(os.Create(dst))进行了“明显”的手动脱糖,则该行可以扩展为:

        w, err := os.Create(dst)
        if err != nil {
            // do something here
            return err
        }

乍一看,这看起来不错,但根据更改所在的块,这也可能意外地隐藏命名的返回参数err并破坏后续defer中的错误处理。

装饰风格之间的自动转换

为了帮助减少时间成本和出错的风险,也许gopls (或其他实用程序)可以有某种类型的命令来对特定的try进行脱糖,或者使用命令对try的所有用途进行脱糖gopls命令只专注于删除和替换try ,但也许一个不同的命令可以使try的所有用途脱糖,同时也至少可以改变一些常见的情况就像函数顶部的defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)到之前每个try位置的等效代码(这在从阶段 1->2 或阶段 1->3 转换时会有所帮助)。 这不是一个完全成熟的想法,但可能值得更多思考什么是可能的或可取的,或者用当前的想法更新提案。

惯用的结果?

一个相关的评论是,一个try的程序化无错误转换最终看起来像普通的惯用 ​​Go 代码的频率并不是很明显。 改编提案中的一个示例,例如,如果您想脱糖:

x1, x2, x3 = try(f())

在某些情况下,保留行为的程序化转换可能会以如下方式结束:

t1, t2, t3, te := f()  // visible temporaries
if te != nil {
        return x1, x2, x3, te
}
x1, x2, x3 = t1, t2, t3

这种确切的形式可能很少见,似乎编辑器或 IDE 进行程序化脱糖的结果通常最终看起来更惯用,但听到这有多真实会很有趣,包括面对命名的返回参数可能变成更常见,并考虑到阴影, :== ,同一函数中err的其他用途等。

该提案讨论了iftry之间由于命名结果参数可能存在的行为差异,但在该特定部分中,它似乎主要讨论从iftry的转换部分_“虽然这是一个细微的区别,但我们认为这样的情况很少见。如果预期当前行为,请保留 if 语句。”_)。 相比之下,在从try转换回if时可能存在不同的可能错误值得详细说明,同时保留相同的行为。


无论如何,对于冗长的评论感到抱歉,但似乎担心样式之间的高转换成本是此处发布的其他一些评论中表达的一些担忧的基础,因此建议更明确地说明这些转换成本和潜在的缓解措施。

@thepudds我爱你正在强调与语言功能如何对重构产生积极或消极影响相关的成本和潜在错误。 这不是我经常讨论的话题,但可以产生很大的下游影响。

如果阶段 1 是带有延迟的统一错误装饰,那么阶段 1->2 的过渡似乎很尴尬。 要在一个退出点添加一个特定的装饰位,首先您需要删除延迟(以避免双重装饰),然后似乎需要访问所有返回点以将 try 用于 if 语句中去糖,使用 N -1 的错误以相同的方式装饰,1 的错误以不同的方式装饰。

这就是在 1.12 中使用break而不是return的地方。 在for range once { ... }块中使用它,其中once = "1"来划分您可能想要退出的代码序列,然后如果您只需要装饰一个错误,您可以在break处执行此操作return之前进行。

它之所以成为如此好的模式,是因为它能够适应不断变化的需求; 你很少需要破坏工作代码来实现新的需求。 这是一种更清晰、更明显的方法 IMO,而不是在跳出方法之前跳回方法的开头。

关注

@randall77我的基准测试结果显示 1.12 和小费的每次调用开销为 40+ns。 这意味着 defer 可以抑制优化,在某些情况下渲染改进以延迟没有实际意义。

@networkimprov Defer 目前确实禁止优化,这是我们想要修复的部分内容。 例如,内联 defer 函数的主体就像我们内联常规调用一样。

我看不出我们所做的任何改进是没有意义的。 这种说法从何而来?

这种说法从何而来?

具有延迟包装错误的函数的每次调用开销 40+ns 没有改变。

1.13 中的更改是优化延迟的一部分。 还计划进行其他改进。 这包含在设计文档中,以及在上面某个点引用的设计文档的一部分中。

重新 swtch.com/try.html 和https://github.com/golang/go/issues/32437#issuecomment -502192315:

@rsc ,超级有用! 如果您仍在修改它,也许链接 #id 问题参考? 还有字体风格的无衬线字体?

该页面是关于内容的。 不要专注于渲染细节。 我在未更改的输入降价上使用 blackfriday 的输出(因此没有 GitHub 特定的#id 链接),我对 serif 字体感到满意。

重新禁用/审查尝试

很抱歉,不会有编译器选项来禁用特定的 Go 功能,也不会有兽医检查说不要使用这些功能。 如果该功能严重到无法禁用或审核,我们不会将其放入。相反,如果该功能存在,则可以使用。 有一种 Go 语言,而不是每个开发人员根据他们选择的编译器标志选择不同的语言。

@mikeschinkel ,在这个问题上,您已经两次将 try 的使用描述为 _ignoring_ 错误。
6 月 7 日,您在“使开发人员更容易忽略错误”的标题下写道:

这是对其他人的评论的完全重复,但基本上提供try()在许多方面类似于简单地将以下内容作为惯用代码,这是永远不会进入任何代码的代码- 尊重开发者船只:

f, _ := os.Open(filename)

我知道我可以在自己的代码中做得更好,但我也知道我们中的许多人依赖于发布一些非常有用的包的其他 Go 开发人员的慷慨,但是从我在_“其他人的代码(tm)”中看到的内容来看_错误处理的最佳实践经常被忽略。

说真的,我们真的想让开发人员更容易忽略错误并允许他们用非健壮的包污染 GitHub 吗?

然后在6 月 14 日,您再次将使用 try 称为“以这种方式忽略错误的代码”。

如果不是代码片段f, _ := os.Open(filename) ,我认为您只是通过将“检查错误并返回它”描述为“忽略”错误来夸大其词。 但是代码片段以及提案文档或语言规范中已经回答的许多问题让我想知道我们到底在谈论相同的语义。 因此,为了清楚并回答您的问题:

在研究提案的代码时,我发现这种行为并不明显,而且有点难以推理。

当我看到try()包装了一个表达式时,如果返回错误会发生什么?

当您看到try(f())时,如果f()返回错误,则try将停止执行代码并从函数体中返回该错误try出现。

错误会被忽略吗?

不,错误永远不会被忽略。 它被返回,与使用 return 语句相同。 喜欢:

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

还是会跳转到第一个或最近的defer

语义与使用 return 语句相同。

延迟函数以“它们被延迟的相反顺序”运行。

如果是这样,它会在闭包内自动设置一个名为err的变量,还是将它作为参数传递_(我没有看到参数?)_。

语义与使用 return 语句相同。

如果你需要在一个延迟函数体中引用一个结果参数,你可以给它一个名字。 请参阅https://golang.org/ref/spec#Defer_statements中的result示例。

如果不是自动错误名称,我该如何命名? 这是否意味着我不能在我的函数中声明自己的err变量以避免冲突?

语义与使用 return 语句相同。

return 语句总是分配给实际的函数结果,即使结果未命名,即使结果已命名但被隐藏。

它会调用所有defer吗? 逆序还是正序?

语义与使用 return 语句相同。

延迟函数以“它们被延迟的相反顺序”运行。 (倒序为正序。)

或者它会从闭包和返回错误的func中返回吗? _(如果我没有在这里读到暗示这一点的话,我永远不会考虑的事情。)_

我不知道这意味着什么,但可能答案是否定的。 我鼓励关注提案文本和规范,而不是在此关于该文本可能或可能不意味着什么的其他评论。

在阅读了提案和迄今为止的所有评论之后,老实说,我仍然不知道上述问题的答案。 这是我们想要添加到倡导者拥护_“Captain Obvious”的语言的那种功能吗?

一般来说,我们的目标是使用一种简单易懂的语言。 很抱歉你有这么多问题。 但是这个提议实际上是尽可能多地重用现有语言(特别是延迟),所以应该很少有额外的细节需要学习。 一旦你知道了

x, y := try(f())

方法

tmp1, tmp2, tmpE := f()
if tmpE != nil {
   return ..., tmpE
}
x, y := tmp1, tmp2

几乎所有其他内容都应从该定义的含义中得出。

这不是“忽略”错误。 忽略错误是当您编写时:

c, _ := net.Dial("tcp", "127.0.0.1:1234")
io.Copy(os.Stdout, c)

并且代码恐慌,因为 net.Dial 失败并且错误被忽略,c 为 nil,以及 io.Copy 对 c.Read 错误的调用。 相反,此代码检查并返回错误:

 c := try(net.Dial("tcp", "127.0.0.1:1234"))
 io.Copy(os.Stdout, c)

回答您关于我们是否要鼓励后者而不是前者的问题:是的。

@damienfamed75提议的emit声明看起来与设计草案中的handle声明基本相同。 放弃handle声明的主要原因是它与defer重叠。 我不清楚为什么不能只使用defer来获得与emit相同的效果。

@dominikh

acme 会开始高亮尝试吗?

关于试用提案的很多内容尚未决定,悬而未决,未知。

但这个问题我可以肯定地回答:不。

@rsc

谢谢您的答复。

_"在这个问题上,您已经两次将 try 的使用描述为忽略错误。"_

是的,我是在用我的观点发表评论,但在技术上并不正确。

我的意思是_“允许错误在不被修饰的情况下传递。”_对我来说,这就是_“忽略”_——就像人们如何使用异常处理_“忽略”_错误——但我当然可以看到其他人会如何认为我的措辞在技术上不正确。

_"当您看到try(f())时,如果f()返回错误,则 try 将停止执行代码并从 try 出现在其主体中的函数返回该错误。"_

这是我不久前评论中一个问题的答案,但现在我已经弄清楚了。

它最终做了两件让我伤心的事情。 原因:

  1. 它将成为避免修饰错误的阻力最小的路径——鼓励许多开发人员这样做——并且许多开发人员将发布该代码供其他人使用,从而导致更多质量较低的公开可用代码和不那么健壮的错误处理/错误报告.

  2. 对于像我这样使用breakcontinue而不是return $ 进行错误处理的人——一种更能适应不断变化的需求的模式——我们甚至无法使用try() ,即使确实没有理由注释错误。

_“或者它会从闭包和返回错误的函数中返回吗?(如果我没有在这里读到暗示这一点的话,我永远不会考虑这一点。)”_

_“我不知道这意味着什么,但答案可能是否定的。我鼓励关注提案文本和规范,而不是这里关于该文本可能或可能不意味着什么的其他评论。”_

同样,这个问题是一个多星期前的问题,所以我现在有了更好的理解。

为了后代澄清, defer有一个闭包,对吧? 如果你从那个闭包返回——除非我误解了——它不仅会从闭包返回,还会从发生错误的func返回,对吗? _(如果是,则无需回复。)_

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

顺便说一句,我的理解是try()的原因是因为开发人员抱怨样板。 我也觉得这很可悲,因为我认为接受导致此样板文件的返回错误的要求有助于使 Go 应用程序比许多其他语言更健壮。

我个人更愿意看到你让不修饰错误更难,而不是更容易忽略修饰它们。 但我确实承认,在这方面我似乎是少数。


顺便说一句,有些人提出了类似以下之一的语法_(我添加了一个假设的.Extend()以保持我的示例简洁):_

f := try os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err   
}

要么

try f := os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

然后其他人声称它并没有真正节省任何字符:

f,err := os.Open(filename)
if err != nil {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

但是缺少批评的一件事是它从 5 行变为 4 行,减少了垂直空间,这似乎很重要,尤其是当您在func中需要许多这样的构造时。

更好的是这样的东西,它可以消除 40% 的垂直空间_(尽管考虑到关于关键字的评论我怀疑这会被考虑):_

try f := os.Open(filename) 
    else err().Extend("Cannot open file %s",filename)
    end //break, continue or return err    

#fwiw


不管怎样,就像我之前说的,我猜船已经航行了,所以我会学会接受它。

目标

这里的一些评论质疑我们试图对提案做什么。 提醒一下,我们去年 8 月发布的错误处理问题陈述“目标”部分中说:

“对于 Go 2,我们希望错误检查更加轻量,减少专门用于错误检查的 Go 程序文本的数量。 我们还想让编写错误处理更方便,增加程序员花时间去做的可能性。

错误检查和错误处理都必须保持明确,即在程序文本中可见。 我们不想重复异常处理的陷阱。

现有代码必须继续工作并保持与今天一样有效。 任何更改都必须与现有代码互操作。”

有关“异常处理的陷阱”的更多信息,请参阅较长的“问题”部分中的讨论。 特别是,错误检查必须清楚地附加到正在检查的内容上。

@mikeschinkel

为了后人澄清, defer有一个闭包,对吧? 如果你从那个闭包返回——除非我误解了——它不仅会从闭包返回,还会从发生错误的func返回,对吗? _(如果是,则无需回复。)_

不,这不是关于错误处理,而是关于延迟函数。 它们并不总是关闭。 例如,一个常见的模式是:

func (d *Data) Op() int {
    d.mu.Lock()
    defer d.mu.Unlock()

     ... code to implement Op ...
}

d.Op 的任何返回都会在 return 语句之后但在代码传输到 d.Op 的调用者之前运行延迟解锁调用。 在 d.mu.Unlock 中所做的任何事情都不会影响 d.Op 的返回值。 d.mu.Unlock 中的 return 语句从 Unlock 返回。 它本身不会从 d.Op 返回。 当然,一旦 d.mu.Unlock 返回,d.Op 也会返回,但不是直接因为 d.mu.Unlock。 这是一个微妙的点,但很重要。

获取您的示例:

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

至少按照书面说明,这是一个无效程序。 我不想在这里迂腐——细节很重要。 这是一个有效的程序:

func example() (err error) {
    defer func() {
        if err != nil {
            println("FAILED:", err.Error())
        }
    }()

    try(funcReturningError())
    return nil
}

延迟函数调用的任何结果在执行调用时都会被丢弃,因此在延迟调用闭包的情况下,编写闭包以返回值根本没有意义。 所以如果你在闭包体内写return err ,编译器会告诉你"too many arguments to return"

所以,不,写return err不会从任何真正意义上的延迟函数和外部函数返回,并且在常规用法中甚至不可能编写看起来这样做的代码。

许多针对此问题的反对建议表明其他更强大的错误处理结构与现有语言结构重复,例如 if 语句。 (或者它们与“使错误检查更轻量级,减少 Go 程序文本的数量以进行错误检查”的目标相冲突。或者两者兼而有之。)

一般来说,Go 已经有一个完美的错误处理结构:整个语言,尤其是 if 语句。 @DavexPro正确地引用了 Go 博客条目Errors are values 。 我们不需要设计一个完全独立的与错误有关的子语言,我们也不应该。 我认为过去半年左右的主要见解是从“检查/处理”提案中删除“处理”,以支持重用我们已有的语言,包括在适当的情况下回退到 if 语句。 这种关于尽可能少做的观察消除了大多数关于进一步参数化新结构的想法。

感谢@brynbellomy的许多好评,我将使用他的 try-else 作为说明性示例。 是的,我们可以这样写:

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

但所有事情都认为这可能不是使用现有语言结构的重大改进:

func doSomething() (int, error) {
    a, b, err := SomeFunc()
    if err != nil {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    logAndContinue := func(err error) {
        log.Errorf("non-critical error: %v", err)
    }
    annotate:= func(err error) (int, error) {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d, err := SomeFunc()
    if err != nil {
        logAndContinue(err)
    }
    e, f, err := SomeFunc()
    if err != nil {
        return annotate(err)
    }

    // ...

    return 123, nil
}

也就是说,继续依赖现有语言编写错误处理逻辑似乎比创建新语句更可取,无论是 try-else、try-goto、try-arrow 还是其他任何语句。

这就是为什么try仅限于简单语义if err != nil { return ..., err }仅此而已:缩短一种常见模式,但不要尝试重新发明所有可能的控制流。 当 if 语句或辅助函数适用时,我们完全希望人们继续使用它们。

@rsc感谢您的澄清。

对了,我没搞清楚细节。 我想我不经常使用defer来记住它的语法。

_(FWIW 我发现将defer用于比关闭文件句柄更复杂的事情,因为在返回之前会在func中向后跳转,所以总是把代码放在末尾func之后的for range once{...}我的错误处理代码break出来了。)_

将每次尝试调用 gofm 到多行的建议直接与“使错误检查更轻量级,减少 Go 程序文本用于错误检查的数量”的目标相冲突。

在单行中执行错误测试 if 语句的建议也与此目标直接冲突。 通过删除内部换行符,错误检查不会变得更轻量级,也不会减少数量。 如果有的话,它们变得更难浏览。

try 的主要好处是对最常见的情况有一个清晰的缩写,使不寻常的情况更加突出,值得仔细阅读。

从 gofmt 备份到通用工具,专注于编写错误检查而不是语言更改的工具的建议同样有问题。 正如 Abelson 和 Sussman 所说,“必须编写程序供人们阅读,并且只是偶然地供机器执行。” 如果机器工具是_需要_来处理语言的,那么语言就没有发挥作用。 可读性绝不能仅限于使用特定工具的人。

一些人的逻辑相反:人们可以编写复杂的表达式,所以他们不可避免地会,所以你需要 IDE 或其他工具支持才能找到 try 表达式,所以 try 是个坏主意。 不过,这里有一些不受支持的飞跃。 主要的观点是,因为它是_可能_编写复杂的、不可读的代码,所以这样的代码将变得无处不在。 正如@josharian所说,“用 Go 编写可恶的代码已经成为可能” 。 这并不常见,因为开发人员有规范试图找到最易读的方式来编写特定的代码。 因此,毫无疑问,在读取涉及 try 的程序时需要 IDE 支持。 在少数情况下,人们编写了真正糟糕的代码滥用尝试,IDE 支持不太可能有多大用处。 这种反对意见——人们可以使用新特性编写非常糟糕的代码——几乎在每一种语言的每一种新语言特性的每次讨论中都会提出。 这不是很有帮助。 一个更有帮助的反对意见是“人们会编写一开始看起来不错的代码,但由于这个意想不到的原因而变得不太好”,就像在讨论调试打印时一样。

再次重申:可读性绝不能仅限于使用特定工具的人。
(我仍然在纸上打印和阅读程序,尽管人们经常给我奇怪的表情。)

感谢@rsc提供您关于允许将if语句作为单行进行 gofmt 的想法。

在单行中执行错误测试 if 语句的建议也与此目标直接冲突。 通过删除内部换行符,错误检查不会变得更轻量级,也不会减少数量。 如果有的话,它们变得更难浏览。

我对这些断言的估计不同。

我发现将行数从 3 减少到 1 会更加轻量级。 gofmt 要求 if 语句包含例如 9 个(甚至 5 个)换行符而不是 3 个换行符不是更重量级吗? 这是减少/扩展的相同因素(数量)。 我认为 struct 文字具有这种精确的折衷,并且加上try ,将允许控制流与if语句一样多。

其次,我发现他们变得更加难以略读以同样适用于try的论点,如果不是更多的话。 至少if语句必须在它自己的行上。 但也许我误解了在这种情况下“略读”的含义。 我用它来表示“主要跳过但要注意”。

综上所述,gofmt 的建议是基于采取比try更保守的步骤,并且对try没有影响,除非它足够了。 听起来不是,所以如果我想进一步讨论它,我将打开一个新问题/提案。 :+1:

我发现将行数从 3 减少到 1 会更加轻量级。

我认为每个人都同意代码可能过于密集。 例如,如果您的整个包裹是一条线,我想我们都同意这是一个问题。 我们可能都不同意确切的路线。 对我来说,我们已经建立

n, err := src.Read(buf)
if err == io.EOF {
    return nil
} else if err != nil {
    return err
}

作为格式化该代码的方式,我认为尝试转向您的示例会很不和谐

n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

反而。 如果我们是这样开始的,我相信会没事的。 但我们没有,也不是我们现在所处的位置。

就个人而言,我确实发现前者在页面上的重量更轻,因为它更容易浏览。 您无需阅读任何实际字母即可一目了然地看到 if-else。 相比之下,更密集的版本很难从三个语句的序列中一眼看出,这意味着你必须更仔细地看它的含义才能变得清晰。

最后,如果我们在不同的地方画出密度与可读性的线就可以了,就换行的数量而言。 try 提案的重点不仅是删除换行符,而且是完全删除结构,这会产生与 gofmt 问题分开的更轻量级的页面存在。

一些人的逻辑相反:人们可以编写复杂的表达式,所以他们不可避免地会,所以你需要 IDE 或其他工具支持才能找到 try 表达式,所以 try 是个坏主意。 不过,这里有一些不受支持的飞跃。 主要的观点是,因为它是_可能_编写复杂的、不可读的代码,所以这样的代码将变得无处不在。 正如@josharian所说,“用 Go 编写可恶的代码已经成为可能” 。 这并不常见,因为开发人员有规范试图找到最易读的方式来编写特定的代码。 因此,毫无疑问,在读取涉及 try 的程序时需要 IDE 支持。 在少数情况下,人们编写了真正糟糕的代码滥用尝试,IDE 支持不太可能有多大用处。 这种反对意见——人们可以使用新特性编写非常糟糕的代码——几乎在每一种语言的每一种新语言特性的每次讨论中都会提出。 这不是很有帮助。

这不是Go 没有三元运算符的全部原因吗?

这难道不是 Go 没有三元运算符的全部原因吗?

不,我们可以而且应该区分“此功能可用于编写非常可读的代码,但也可能被滥用以编写不可读的代码”和“此功能的主要用途将是编写不可读的代码”。

C 的经验表明 ? : 完全属于第二类。 (除了 min 和 max 可能的例外,我不确定我是否见过使用 ? 的代码:重写它以使用 if 语句并没有改进。但是这一段离题了。)

句法

该讨论确定了六种不同的语法来编写提案中的相同语义:

  • f := try(os.Open(file)) ,来自提案(内置函数)
  • f := try os.Open(file)使用关键字(前缀关键字)
  • f := os.Open(file)?就像在 Rust中一样(调用后缀运算符)
  • f := os.Open?(file)由@rogpeppe (呼叫中缀运算符)建议
  • try f := os.Open(file)由@thepudds 建议(try 语句)
  • try ( f := os.Open(file); f.Close() )由@bakul 建议(尝试块)

(对不起,如果我把起源故事弄错了!)

所有这些都有优点和缺点,好的是因为它们都具有相同的语义,因此在各种语法之间进行选择以进行进一步实验并不是很重要。

我发现@brynbellomy 发人深省的这个例子

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

当然,这些具体示例之间没有太大区别。 如果尝试在所有行中都有,为什么不将它们排列起来或将它们排除在外呢? 这不是更干净吗? 我也想知道这个。

但正如@ianlancetaylor 所观察到的那样,“尝试埋葬了领导者。 代码变成了一系列的 try 语句,它掩盖了代码实际在做什么。”

我认为这是一个关键点:以这种方式排列尝试,或将其分解为块,意味着错误的并行性。 这意味着这些陈述的重要之处在于它们都在尝试。 这通常不是关于代码的最重要的事情,也不是我们在阅读它时应该关注的内容。

假设为了论证, AsCommit 永远不会失败,因此不会返回错误。 现在我们有:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

乍一看,中间的两条线与其他线明显不同。 为什么? 原来是因为错误处理。 这是关于这段代码最重要的细节,你应该第一眼注意到的东西吗? 我的回答是否定的。 我认为您应该首先注意程序在做什么的核心逻辑,然后是错误处理。 在这个例子中,try 语句和 try 块阻碍了核心逻辑的观点。 对我来说,这表明它们不是这些语义的正确语法。

剩下的前四种语法彼此更加相似:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

headRef := try r.Head()
parentObjOne := try headRef.Peel(git.ObjectCommit)
parentObjTwo := try remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try index.WriteTree()
tree := try r.LookupTree(treeOid)

// vs

headRef := r.Head()?
parentObjOne := headRef.Peel(git.ObjectCommit)?
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)?
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()?
tree := r.LookupTree(treeOid)?

// vs

headRef := r.Head?()
parentObjOne := headRef.Peel?(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel?(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree?()
tree := r.LookupTree?(treeOid)

很难因为选择一个而不是其他而过于激动。 他们都有自己的优点和缺点。 内置表单最重要的优点是:

(1) 确切的操作数非常清楚,尤其是与前缀运算符try x.y().z()相比。
(2) 不需要了解 try 的工具可以将其视为普通函数调用,因此例如 goimports 无需任何调整即可正常工作,并且
(3)如有需要,未来有一定的扩展和调整空间。

在看到使用这些结构的真实代码之后,我们完全有可能更好地了解其他三种语法之一的优势是否超过函数调用语法的这些优势。 只有实验和经验才能告诉我们这一点。

感谢所有的澄清。 我越想越喜欢这个提议,看看它是否符合目标。

为什么不使用像recover()这样的函数,而不是我们不知道它来自哪里的err呢? 它会更加一致,也许更容易实现。

func f() error {
 defer func() {
   if err:=error();err!=nil {
     ...
   }
 }()
}

编辑:我从不使用命名返回,那么为此添加命名返回对我来说会很奇怪

@flibustenet ,另请参阅https://swtch.com/try.html#named以获得一些类似的建议。
(回答所有问题:我们可以这样做,但给定命名结果并不是绝对必要的,因此我们不妨在决定是否需要提供第二种方式之前尝试使用现有概念。)

try()的意外后果可能是项目放弃 _go fmt_ 以获得单行错误检查。 这几乎是try()的所有好处,而没有任何成本。 我已经这样做了几年了; 它运作良好。

但我宁愿能够为 package 定义一个最后的错误处理程序,并消除所有需要它的错误检查。 我定义的不是try()

@networkimprov ,您似乎来自与我们所针对的 Go 用户不同的位置,如果您的消息包含更多详细信息或链接,那么您的消息将对对话做出更多贡献,以便我们更好地理解您的观点。

目前尚不清楚您认为尝试有什么“成本”。 虽然您说放弃 gofmt 没有尝试的“成本”(无论是什么),但您似乎忽略了 gofmt 的格式是所有有助于重写 Go 源代码的程序所使用的格式,例如 goimports,例如 gorename , 等等。 你放弃 go fmt 的代价是放弃这些助手,或者至少在你调用它们时忍受对你的代码的大量附带编辑。 即便如此,如果这样做对您来说效果很好,那就太好了:一定要继续这样做。

还不清楚“为包定义最后的错误处理程序”是什么意思,或者为什么将错误处理策略应用于整个包而不是一次应用于单个函数是合适的。 如果您想要在错误处理程序中做的主要事情是添加上下文,那么相同的上下文将不适用于整个包。

@rsc ,正如您可能已经看到的,虽然我建议使用 try 块语法,但我后来又恢复到此功能的“否”方面——部分原因是我对在语句或函数应用程序中隐藏一个或多个条件错误返回感到不舒服。 但让我澄清一点。 在 try block 提案中,我明确允许不需要try的语句。 因此,您的最后一个尝试块示例将是:

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
        parentCommitOne := parentObjOne.AsCommit()
        parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

这只是说try 块中返回的任何错误都会返回给调用者。 如果控件通过了 try 块,则该块中没有错误。

你说

我认为您应该首先注意程序在做什么的核心逻辑,然后是错误处理。

这正是我想到 try 块的原因! 考虑的不仅仅是关键字,还有错误处理。 我不想考虑可能会产生错误的 N 个不同的地方(除非我明确地尝试处理特定错误)。

还有一些可能值得一提的观点:

  1. 调用者不知道错误来自被调用者的确切位置。 这也适用于您正在考虑的简单提案。 我推测可以使编译器在错误返回点添加自己的注释。 但我没有想太多。
  2. 我不清楚是否允许使用诸如try(try(foo(try(bar)).fum())之类的表达式。 这种用法可能不受欢迎,但需要指定它们的语义。 在 try 块的情况下,编译器必须更加努力地检测这种使用并将所有错误处理挤出到 try 块级别。
  3. 我更喜欢return-on-error而不是try 。 这在块级上更容易吞咽!
  4. 另一方面,任何长关键字都会降低可读性。

FWIW,我仍然认为这不值得。

@rsc

[...]
主要的观点是,由于可以编写复杂的、不可读的代码,这样的代码将变得无处不在。 正如@josharian 所指出的,“用 Go 编写可恶的代码已经成为可能”。
[...]

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())

我理解你对“坏代码”的立场是我们今天可以写出糟糕的代码,就像下面的代码块一样。

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

你对禁止嵌套的try调用有什么想法,这样我们就不会不小心写出错误的代码?

如果您在第一个版本中禁止嵌套try ,您可以稍后在需要时删除此限制,反之则不可能。

我已经讨论过这一点,但它似乎是相关的——代码复杂性应该垂直扩展,而不是水平扩展。

try作为一个表达式,通过鼓励嵌套调用来鼓励代码复杂性水平扩展。 try作为语句鼓励代码复杂性垂直扩展。

@rsc ,对于您的问题,

我最后的包级处理程序——当错误不是预期的时候:

func quit(err error) {
   fmt.Fprintf(os.Stderr, "quit after %s\n", err)
   debug.PrintStack()      // because panic(err) produces a pile of noise
   os.Exit(3)
}

背景:我大量使用 os.File(我发现了两个错误:#26650 & #32088)

添加基本​​上下文的包级装饰器需要一个caller参数——一个生成的结构,它提供 runtime.Caller() 的结果。

我希望 _go fmt_ 重写器使用现有格式,或者让您指定每个转换的格式。 我凑合着用其他工具。

try()的成本(即缺点)在上面有详细记录。

老实说,Go 团队首先为我们提供了check/handle (慈善地,一个新颖的想法),然后是三元式的try() ,这让我感到很震惊。 我不明白您为什么不发布 RFP 重新错误处理,然后收集社区对一些由此产生的提案的评论(参见 #29860)。 这里有很多你可以利用的智慧!

@rsc

句法

该讨论确定了六种不同的语法来编写提案中的相同语义:

  • f := try(os.Open(file)) ,来自提案(内置函数)
  • f := try os.Open(file)使用关键字(前缀关键字)
  • f := os.Open(file)?就像在 Rust中一样(调用后缀运算符)
  • f := os.Open?(file)由@rogpeppe (呼叫中缀运算符)建议
  • try f := os.Open(file)由@thepudds 建议(try 语句)
  • try ( f := os.Open(file); f.Close() )由@bakul 建议(尝试块)

try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

...而且,IMO,提高可读性(通过头韵)以及语义准确性:

f, err := os.Open(file)
relay err

要么

f, err := os.Open(file)
relay err wrap

要么

f, err := os.Open(file)
relay err wrap { a, b }

要么

f, err := os.Open(file)
relay err { a, b }

我知道提倡中继与尝试很容易被认为是题外话,但我可以想象尝试解释尝试如何不尝试任何东西并且不抛出任何东西。 目前尚不清楚并且有行李。 relay是一个新术语,可以进行清晰的解释,并且该描述具有电路的基础(无论如何,这就是全部内容)。

编辑以澄清:
尝试可能意味着 - 1. 体验某事,然后主观地判断它 2. 客观地验证某事 3. 尝试做某事 4. 启动多个可以中断的控制流,如果是,则启动可拦截通知

在这个提议中,try 没有做这些。 我们实际上正在运行一个函数。 然后它根据错误值重新连接控制流。 这实际上是保护继电器的定义。 我们是根据测试错误的值直接重新铺设电路(即短路当前功能范围)。

在 try 块提案中,我明确允许不需要 try 的语句

我在 Java 和 Python 等语言的 try-catch 系统中看到的 Go 错误处理的主要优势在于,始终清楚哪些函数调用可能导致错误,哪些不会。 原始提案中记录的try的美妙之处在于,它可以减少简单的错误处理样板,同时仍保持这一重要功能。

借用@Goodwine的示例,尽管它很丑,但从错误处理的角度来看,即使这样:

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

... 比你经常在 try-catch 语言中看到的要好

parentCommitOne := r.Head().Peel(git.ObjectCommit).AsCommit()
parentCommitTwo := remoteBranch.Reference.Peel(git.ObjectCommit).AsCommit()

...因为您仍然可以分辨出代码的哪些部分可能会因错误而转移控制流,哪些不能。

我知道@bakul并不提倡这个块语法提案,但我认为它提出了一个有趣的观点,即与其他人相比,Go 的错误处理。 我认为 Go 采用的任何错误处理建议都不应混淆代码的哪些部分可以出错和不能出错,这一点很重要。

我写了一个小工具: tryhard (目前不怎么努力)在逐个文件的基础上运行,并使用简单的 AST 模式匹配来识别try的潜在候选者文档了解详细信息。

将其应用于$GOROOT/src的小费报告 > 5000 (!) 机会try 。 可能有很多误报,但手工检查一个体面的样本表明大多数机会都是真实的。

使用重写功能显示了使用try时代码的样子。 再一次,粗略地看一下输出表明我的想法有了很大的改善。

注意:重写功能会破坏文件!使用风险自负。

希望这将为使用try的代码提供一些具体的见解,并让我们摆脱闲置和非生产性的猜测。

谢谢和享受。

我理解你对“坏代码”的立场是我们今天可以写出糟糕的代码,就像下面的代码块一样。

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

我的立场是,Go 开发人员在编写清晰的代码方面做得不错,而且几乎可以肯定,编译器并不是唯一阻碍你或你的同事编写类似代码的东西。

你对禁止嵌套的try调用有什么想法,这样我们就不会不小心写出糟糕的代码?

Go 的简单性很大一部分源于对独立组合的正交特征的选择。 添加限制会破坏正交性、可组合性、独立性,并且这样做会破坏简单性。

今天,如果您有以下情况,这是一条规则:

x := expression
y := f(x)

在任何地方都没有使用 x 的情况下,这是一个有效的程序转换,可以将其简化为

y := f(expression)

如果我们要对 try 表达式进行限制,那么它将破坏任何假定这始终是有效转换的工具。 或者,如果您有一个使用表达式并可能处理 try 表达式的代码生成器,则它必须不遗余力地引入临时变量以满足限制。 等等等等。

简而言之,限制增加了显着的复杂性。 他们需要充分的理由,而不是“让我们看看是否有人撞到这堵墙并要求我们拆除它”。

两年前,我在https://github.com/golang/go/issues/18130#issuecomment -264195616(在类型别名的上下文中)写了一个更长的解释,在这里同样适用。

@巴库尔

但让我澄清一点。 在 try block 提案中,我明确允许_不需要_ try的语句。

这样做会达不到第二个目标:“错误检查和错误处理都必须保持明确,这意味着在程序文本中可见。我们不想重复异常处理的陷阱。”

传统异常处理的主要缺陷是不知道检查在哪里。 考虑:

try {
    s = canThrowErrors()
    t = cannotThrowErrors()
    u = canThrowErrors() // a second call
} catch {
    // how many ways can you get here?
}

如果函数的命名不那么有用,那么很难判断哪些函数可能失败,哪些保证成功,这意味着您无法轻松推断哪些代码片段可以被异常中断,哪些不能。

将此与Swift 的方法进行比较,后者采用了一些传统的异常处理语法,但实际上是在进行错误处理,在每个检查函数上都有一个显式标记,并且没有办法在当前堆栈帧之外展开:

do {
    let s = try canThrowErrors()
    let t = cannotThrowErrors()
    let u = try canThrowErrors() // a second call
} catch {
    handle error from try above
}

无论是 Rust 还是 Swift 还是这个提案,对异常处理的关键、关键改进是在文本中明确标记 - 即使使用非常轻量级的标记 - 每个地方都有检查。

有关隐式检查问题的更多信息,请参阅去年 8 月问题概述的问题部分,特别是 Raymond Chen 的两篇文章的链接。

编辑:另请参阅@velovix的评论三,这是我在做这个的时候出现的。

@daved ,我很高兴“保护继电器”类比对您有用。 它对我不起作用。 程序不是电路。

任何词都可能被误解:
“break”不会破坏你的程序。
“继续”不会像往常一样在下一条语句继续执行。
“goto”……好吧 goto 实际上不可能被误解。 :-)

https://www.google.com/search?q=define+try表示“尝试或努力做某事”和“受审”。 这两个都适用于“f := try(os.Open(file))”。 它尝试执行 os.Open(或者,它使错误结果进行试验),如果尝试(或错误结果)失败,它从函数返回。

我们去年八月使用了支票。 那也是一个好词。 尽管有 C++/Java/Python 的历史包袱,我们还是改用了 try,因为这个提案中 try 的当前含义与Swift 的 try(没有周围的 do-catch)和 Rust 的原始 try 中的含义相匹配! . 如果我们稍后决定 check 是正确的词,这并不可怕,但现在我们应该专注于名称以外的事情。

这是一个有趣的tryhard假阴性,来自github.com/josharian/pct 。 我在这里提到它是因为:

  • 它显示了自动化try检测很棘手的方法
  • 它说明了if err != nil的视觉成本会影响人们(至少我)如何构建他们的代码,而try可以帮助解决这个问题

前:

var err error
switch {
case *flagCumulative:
    _, err = fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s)
case *flagQuiet:
    _, err = fmt.Fprintln(w, line.s)
default:
    _, err = fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s)
}
if err != nil {
    return err
}

之后(手动重写):

switch {
case *flagCumulative:
    try(fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s))
case *flagQuiet:
    try(fmt.Fprintln(w, line.s))
default:
    try(fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s))
}

更改https://golang.org/cl/182717提到这个问题: src: apply tryhard -r $GOROOT/src

有关 std 库中try的视觉概念,请前往CL 182717

谢谢, @josharian为此。 是的,即使是一个好的工具也可能无法检测try的所有可能使用候选者。 但幸运的是,这不是(本提案的)主要目标。 拥有一个工具很有用,但我在尚未编写的代码中看到了try的主要好处(因为这将比我们已经拥有的代码多得多)。

“break”不会破坏你的程序。
“继续”不会像往常一样在下一条语句继续执行。
“goto”……好吧 goto 实际上不可能被误解。 :-)

break确实打破了循环。 continue确实会继续循环,并且goto确实会到达指定的目的地。 最终,我确实听到了你的声音,但请考虑当函数大部分完成并返回错误但不回滚时会发生什么。 这不是一次尝试/审判。 我确实认为check在这方面要优越得多(通过“考试”来“停止前进”当然是恰当的)。

更相关的是,我很好奇我提供的 try/check 的形式,而不是其他语法。
try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

标准库最终不能代表“真正的” Go 代码,因为它不会花费太多时间来协调或连接其他包。 过去我们已经注意到这一点,这是标准库中的通道使​​用量与依赖食物链更远的软件包相比如此之少的原因。 我怀疑错误处理和传播最终在这方面类似于渠道:你会发现越往上走越多。

出于这个原因,有人在一些更大的应用程序代码库上运行 tryhard 并查看在该上下文中可以发现哪些有趣的东西会很有趣。 (标准库也很有趣,但它更像是一个缩影,而不是对世界的准确采样。)

我很好奇我提供的 try/check 的形式,而不是其他语法。

我认为这种形式最终会重建现有的控制结构

@networkimprov ,重新https://github.com/golang/go/issues/32437#issuecomment -502879351

老实说,Go 团队为我们提供了首先检查/处理(仁慈地,一个新颖的想法),然后是三元式的 try(),这让我感到很震惊。 我不明白您为什么不发布 RFP 重新错误处理,然后收集社区对一些由此产生的提案的评论(参见 #29860)。 这里有很多你可以利用的智慧!

正如我们在 #29860 中讨论的那样,老实说,就征求社区反馈而言,我认为您建议我们应该做的事情与我们实际做的事情之间没有太大区别。 设计草案页面明确表示它们是“讨论的起点,最终目标是产生足够好的设计以转化为实际提案。” 人们确实写了很多东西,从简短的反馈到完整的替代提案。 大部分内容都很有帮助,我感谢您的帮助,特别是在组织和总结方面的帮助。 你似乎执着于给它起一个不同的名字或引入额外的官僚机构,正如我们在那个问题上讨论的那样,我们并不认为有必要这样做。

但请不要声称我们没有征求社区建议或忽略它。 这根本不是真的。

我也看不出尝试是如何以任何方式“三元式”的,无论这意味着什么。

同意,我认为这是我的目标; 我不认为更复杂的机制是值得的。 如果我站在你的立场上,我最多只能提供一点语法糖来消除大多数抱怨,仅此而已。

@rsc ,为偏离主题而道歉!
我在https://github.com/golang/go/issues/32437#issuecomment -502840914 中提出了包级处理程序
并在https://github.com/golang/go/issues/32437#issuecomment -502879351 中回复了您的澄清请求

我将包级处理程序视为几乎每个人都可以落后的功能。

请使用 try {} catch{} 语法,不要构建更多轮子

请使用 try {} catch{} 语法,不要构建更多轮子

我认为当其他人使用的轮子形状像正方形时,制造更好的轮子是合适的

@jimwei

基于异常的错误处理可能是一个预先存在的轮子,但它也有很多已知的问题。 原始设计草案中的问题陈述很好地概述了这些问题。

加上我自己不太深思熟虑的评论,我认为有趣的是,许多非常成功的新语言(即 Swift、Rust 和 Go)没有采用异常。 这告诉我,在我们不得不与他们合作多年之后,更广泛的软件社区正在重新考虑例外情况。

回应https://github.com/golang/go/issues/32437#issuecomment -502837008 ( @rsc关于try的评论作为声明)

你提出了一个很好的观点。 很抱歉,在发表此评论之前,我以某种方式错过了该评论: https ://github.com/golang/go/issues/32437#issuecomment -502871889

您使用try作为表达式的示例看起来比使用try作为语句的示例要好得多。 该语句以try开头的事实确实使它更难阅读。 但是,我仍然担心人们会将 try 调用嵌套在一起以生成糟糕的代码,因为try作为表达式在我看来确实_鼓励_了这种行为。

如果golint禁止嵌套try调用,我想我会更感激这个提议。 我认为在其他表达式中禁止所有try调用有点过于严格,将try作为表达式确实有其优点。

借用您的示例,即使只是将 2 个 try 调用嵌套在一起看起来也很可怕,而且我可以看到 Go 程序员这样做,特别是如果他们在没有代码审查员的情况下工作。

parentCommitOne := try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit()
parentCommitTwo := try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()
tree := try(r.LookupTree(try(index.WriteTree())))

最初的示例实际上看起来相当不错,但是这个示例表明嵌套 try 表达式(即使只有 2 层)确实会极大地损害代码的可读性。 拒绝嵌套的try调用也有助于解决“可调试性”问题,因为如果try if会容易得多。

同样,我几乎想说,子表达式中的try应该由golint标记,但我认为这可能有点太严格了。 它还会标记这样的代码,在我看来这很好:

x := 5 + try(strconv.Atoi(input))

这样,我们得到了将try作为表达式的好处,但我们并没有提倡在水平轴上增加太多的复杂性。

也许另一种解决方案是golint应该只允许每个语句最多 1 个try ,但是已经晚了,我累了,我需要更理性地考虑一下。 无论哪种方式,我在某些时候对这个提议持相当消极的态度,但我想我实际上可以转向真正喜欢它,只要有一些与之相关的golint标准。

@rsc

我们可以而且应该区分_“这个特性可以用来编写可读性很强的代码,但也可能被滥用来编写不可读的代码”_和“这个特性的主要用途是编写不可读的代码”。
C 的经验表明 ? : 完全属于第二类。 (除了 min 和 max 可能的例外,

关于try() (与try作为语句相比)首先让我印象深刻的是,它与三元运算符的可嵌套性有多么相似,而try()和反对三元运算符的论点又是多么相反被_(释义):_

  • 三元:_“如果我们允许它,人们会嵌套它,结果将是很多糟糕的代码”_忽略有些人用它们编写更好的代码,vs.
  • try(): _"你可以嵌套它,但我们怀疑很多人会这样做,因为大多数人都想编写好的代码"_,

恕我直言,两者之间差异的理性感觉如此主观,我会要求进行一些反省,并至少考虑您是否可能将您喜欢的功能与您不喜欢的功能的差异合理化? #please_dont_shoot_the_messenger

_“我不确定我是否见过使用 ? 的代码:重写它以改用 if 语句并没有改进。但是这一段离题了。)”_

在其他语言中,我经常通过将语句从if重写为三元运算符来改进语句,例如我今天用 PHP 编写的代码:

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

相比于:

if ( isset( $_COOKIE[ CookieNames::CART_ID ] ) ) {
    return intval( $_COOKIE[ CookieNames::CART_ID ] );
} else { 
    return null;
}

就我而言,前者比后者改进了很多。

关注

我认为对该提案的批评主要是由于之前的提案提出了很高的期望,这本来会更全面。 但是,我认为出于一致性的原因,如此高的期望是合理的。 我认为许多人希望看到的是一个单一的、全面的错误处理结构,它在所有用例中都很有用。

例如,将此功能与内置的append()功能进行比较。 创建追加是因为追加到切片是一个非常常见的用例,虽然可以手动完成,但也很容易出错。 现在append()不仅允许追加一个元素,还允许追加许多元素,甚至是整个切片,它甚至允许将字符串追加到 []byte 切片。 它足够强大,可以涵盖所有附加到切片的用例。 因此,没有人再手动添加切片了。

但是, try()不同。 它不够强大,因此我们可以在所有错误处理情况下使用它。 我认为这是该提案最严重的缺陷。 try()内置函数只是真正有用,因为它减少了样板,在最简单的情况下,即只是将错误传递给调用者,并使用 defer 语句,如果功能需要以相同的方式处理。

对于更复杂的错误处理,我们仍然需要使用if err != nil {} 。 这导致了两种不同的错误处理风格,而以前只有一种。 如果这个提议是我们在 Go 中帮助处理错误的全部内容,那么,我认为最好什么都不做,像往常一样继续使用if处理错误处理,因为至少这是一致的并具有“只有一种方法可以做到”的好处。

@rsc ,为偏离主题而道歉!
我在#32437(评论)中提出了包级处理程序
并在#32437(评论)中回复了您的澄清请求

我将包级处理程序视为几乎每个人都可以落后的功能。

我看不出是什么将包的概念与特定的错误处理联系在一起。 很难想象包级处理程序的概念对例如net/http有用。 同样,尽管编写的包通常比net/http更小,但我想不出有一个用例会更喜欢包级构造来进行错误处理。 总的来说,我发现每个人都分享自己的经验、用例和意见的假设是危险的:)

@beoran我相信这个提议可以进一步改进。 就像try(..., func(err) error)的最后一个参数或tryf(..., "context of my error: %w")的装饰器?

@flibustenet虽然以后可以进行此类扩展,但现在的提议似乎不鼓励此类扩展,主要是因为添加错误处理程序对于 defer 来说是多余的。

我想困难的问题是如何在不重复defe的功能的情况下进行全面的错误处理。 也许 defer 语句本身可以以某种方式增强,以便在更复杂的情况下更轻松地处理错误……但是,这是一个不同的问题。

https://github.com/golang/go/issues/32437#issuecomment -502975437

这导致了两种不同的错误处理风格,而以前只有一种。 如果这个提议是我们在 Go 中帮助处理错误处理的全部内容,那么,我认为最好什么都不做,像往常一样继续使用if处理错误处理,因为至少这是一致的并受益于“只有一种方法可以做到”。

@beoran同意。 这就是为什么我建议我们将绝大多数错误案例统一在try关键字下( trytry / else )。 尽管try / else语法与现有的if err != nil样式相比并没有显着减少代码长度,但它使我们与try保持一致else )案例。 这两种情况(try 和 try-else)可能涵盖了绝大多数错误处理情况。 我将其与try的内置 no-else 版本相反,该版本仅适用于程序员除了返回之外实际上没有做任何事情来处理错误的情况(正如其他人在这个线程中提到的那样,不一定是我们真正想要鼓励的东西)。

一致性对于可读性很重要。

append是将元素添加到切片的最终方法。 make是构建新通道或映射或切片的明确方法(文字除外,我对此并不感到兴奋)。 但是try() (作为内置,并且没有else )会散布在整个代码库中,这取决于程序员需要如何处理给定的错误,这种方式可能有点混乱和令人困惑读者。 它似乎不符合其他内置函数的精神(即,处理一个非常困难或完全不可能以其他方式处理的案例)。 如果这是成功的try版本,一致性和可读性将迫使我不使用它,就像我试图避免使用 map/slice 文字(并避免像瘟疫一样避免使用new )。

如果想法是改变处理错误的方式,那么尝试在尽可能多的情况下统一方法似乎是明智的,而不是添加一些充其量是“接受或放弃”的东西。 我担心后者实际上会增加噪音而不是减少噪音。

@deanveloper写道:

如果 golint 禁止嵌套的 try 调用,我想我会更加欣赏这个提议。

我同意深层嵌套的try可能难以阅读。 但这也适用于标准函数调用,而不仅仅是try内置函数。 因此,我不明白为什么golint应该禁止这样做。

@brynbellomy写道:

尽管与现有的 if err != nil 风格相比,try/else 语法没有给我们任何显着减少代码长度,但它使我们与 try (no else) 情况保持一致。

try内置函数的独特目标是减少样板文件,因此当您承认它“并没有给我们任何显着减少”时,很难理解为什么我们应该采用您建议的 try/else 语法在代码长度”。

您还提到您建议的语法使 try 案例与 try/else 案例一致。 但是,当我们已经有了 if/else 时,它​​也会创建一种不一致的分支方式。 您在特定用例上获得了一些一致性,但在其他用例上失去了很多不一致。

我觉得有必要就它们的价值表达我的意见。 尽管并非所有这些本质上都是学术和技术性的,但我认为有必要说一下。

我相信这种变化是为了工程而进行工程并且使用“进步”来证明理由的情况之一。 Go 中的错误处理没有被破坏,这个提议违反了我喜欢 Go 的很多设计理念。

让事情容易理解,不容易做
这个提议是选择优化懒惰而不是正确性。 重点是使错误处理更容易,作为回报,大量的可读性正在丢失。 由于可读性和可调试性的提高,错误处理的偶尔繁琐的性质是可以接受的。

避免命名返回参数
有一些带有defer语句的极端情况,其中命名返回参数是有效的。 在这些之外,应该避免。 该提案提倡使用命名返回参数。 这无助于让 Go 代码更具可读性。

封装应该创建一个绝对精确的新语义
这种新语法没有精确性。 隐藏错误变量和返回值无助于使事情更容易理解。 事实上,语法与我们今天在 Go 中所做的任何事情都感觉很陌生。 如果有人写了一个类似的函数,我相信社区会同意抽象隐藏了成本,不值得它试图提供的简单性。

我们想帮助谁?
我担心这种变化是为了吸引企业开发人员远离他们当前的语言而转向 Go。 实施语言更改,只是为了增加数量,开创了一个不好的先例。 我认为提出这个问题并获得试图解决的业务问题和试图实现的预期收益的答案是公平的吗?

我之前已经看过好几次了。 看起来很清楚,随着语言团队最近的所有活动,这个提议基本上是一成不变的。 对实施的辩护比对实施本身的实际辩论更多。 这一切都始于 13 天前。 我们将看到这种变化对 Go 语言、社区和未来的影响。

Go 中的错误处理没有被破坏,这个提议违反了我喜欢 Go 的很多设计理念。

比尔完美地表达了我的想法。

我无法阻止try被介绍,但如果是,我不会自己使用它; 我不会教它,也不会在我审查的 PR 中接受它。 它将被简单地添加到其他“我从不使用的 Go 东西”列表中(有关更多这些内容,请参见 Mat Ryer 在 YouTube 上的有趣演讲)。

@ardan-bkennedy,感谢您的评论。

您询问了“试图解决的业务问题”。 除了“Go 编程”之外,我认为我们不会针对任何特定业务的问题。 但更一般地说,我们在去年 8 月的 Gophercon 设计草案讨论启动中阐明了我们试图解决的问题(请参阅问题概述,尤其是目标部分)。 这一对话自去年 8 月以来一直在进行,这一事实也与您声称“所有这一切始于 13 天前”的说法完全矛盾。

您不是唯一一个建议这不是问题或不值得解决的问题的人。 有关其他此类评论,请参阅https://swtch.com/try.html#nonissue 。 我们已经注意到了这些,并且确实希望确保我们正在解决实际问题。 找出答案的部分方法是在真实代码库上评估提案。 像 Robert's tryhard 这样的工具可以帮助我们做到这一点。 我之前要求人们让我们知道他们在自己的代码库中发现了什么。 该信息对于评估更改是否值得至关重要。 你有一个猜测,我有一个不同的猜测,这很好。 答案是用数据代替这些猜测。

我们将做必要的事情来确保我们正在解决实际问题。

同样,前进的道路是实验数据,而不是直觉反应。 不幸的是,收集数据需要付出更多努力。 在这一点上,我会鼓励那些想帮忙的人出去收集数据。

@ardan-bkennedy,对于第二次跟进感到抱歉,但关于:

我担心这种变化是为了吸引企业开发人员远离他们当前的语言而转向 Go。 实施语言更改,只是为了增加数量,开创了一个不好的先例。

这条线有两个严重的问题,我不能走过去。

首先,我拒绝隐含的说法,即有些类别的开发人员——在这种情况下是“企业开发人员”——不知何故不值得使用 Go 或考虑他们的问题。 在“企业”的具体案例中,我们看到很多大小公司都非常有效地使用 Go 的例子。

其次,从 Go 项目开始,我们 - Robert、Rob、Ken、Ian 和我 - 根据我们构建许多系统的集体经验评估了语言变化和特性。 我们问“这在我们编写的程序中能很好地工作吗?” 这是一个具有广泛适用性的成功秘诀,也是我们打算继续使用的秘诀,我在之前的评论和更普遍的经验报告中要求的数据再次增强了这一点。 我们不会建议或支持我们无法在自己的程序中使用或我们认为不适合 Go 的语言更改。 我们当然不会建议或支持一个糟糕的改变,只是为了让更多的 Go 程序员。 毕竟我们也使用 Go。

@rsc
将不乏可以放置这种便利的位置。 除此之外,正在寻求什么指标来证明机制的实质? 是否有分类错误处理案例的列表? 当大部分公共流程由情绪驱动时,如何从数据中获取价值?

tryhard工具信息量很大!
我可以看到我经常使用return ...,err ,但只有当我知道我调用了一个已经包装错误的函数时(使用pkg/errors ),主要是在 http 处理程序中。 我以更少的代码行赢得了可读性。
然后在这些 http 处理程序中,我将添加一个defer fmt.HandleErrorf(&err, "handler xyz")并最后添加比以前更多的上下文。

我还看到很多情况,我根本不关心错误fmt.Printf ,我会用try来做。
例如可以做defer try(f.Close())吗?

因此,也许try最终将有助于添加上下文并推动最佳实践,而不是相反。

我非常迫不及待地要进行真实的测试!

@flibustenet该提案不允许defer try(f()) (请参阅基本原理)。 有各种各样的问题。

当使用这个tryhard工具查看代码库中的变化时,我们是否还可以比较if err != nil前后的比率,看看添加上下文更常见还是只是将错误传回?

我的想法是,也许一个假设的大型项目可以看到 1000 个添加try()的地方,但是有 10000 if err != nil添加了上下文,所以即使 1000 个看起来很大,它也只是全部内容的 10% .

@Goodwine是的。 本周我可能无法进行此更改,但代码非常简单且独立。 随意尝试(没有双关语),克隆并根据需要进行调整。

defer try(f())不等于

defer func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

这个(if 版本)目前是不允许的,对吧? 在我看来,你不应该在这里例外——可能会产生警告? 并且不清楚上面的延迟代码是否一定是错误的。 如果close(file)defer语句中失败怎么办? 我们是否应该报告该错误?

我读了似乎在谈论defer try(f)而不是defer try(f())的理由。 可能是笔误?

可以对go try(f())进行类似的论证,翻译为

go func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

这里try没有做任何有用但无害的事情。

@ardan-bkennedy 感谢您的想法。 恕我直言,我认为您歪曲了该提案的意图,并提出了一些未经证实的主张

关于@rsc之前没有解决的一些问题:

  • 我们从来没有说过错误处理被破坏了。 该设计基于(由 Go 社区!)的观察,即当前的处理很好,但在许多情况下很冗长 - 这是无可争议的。 这是该提案的一个主要前提。

  • 让事情变得更容易做也可以让他们更容易理解——这两者并不相互排斥,甚至相互暗示。 我敦促您查看此代码作为示例。 使用try删除了大量的样板,而样板几乎没有增加代码的可理解性。 排除重复代码是提高代码质量的标准且被广泛接受的编码实践。

  • 关于“这个提案违反了很多设计哲学”:重要的是我们不要对“设计哲学”变得教条主义——这往往是好想法的失败(此外,我认为我们知道一两件事Go 的设计理念)。 围绕命名和未命名的结果参数有很多“宗教狂热”(因为没有更好的术语)。 诸如“永远不要使用命名结果参数”这样的口头禅是没有意义的。 它们可以作为一般准则,但不是绝对真理。 命名的结果参数本质上并不是“坏的”。 命名良好的结果参数可以以有意义的方式添加到 API 的文档中。 简而言之,我们不要使用口号来做出语言设计决策。

  • 该提案的重点是不引入新语法。 它只是提出了一个新功能。 我们不能用该语言编写该函数,因此内置函数是 Go 中的自然位置。 它不仅是一个简单的函数,而且定义得非常精确。 我们之所以选择这种最小的方法而不是更全面的解决方案,正是因为它很好地完成了一件事,并且几乎没有给任意的设计决策留下任何影响。 由于其他语言(例如 Rust)具有非常相似的结构,因此我们也没有偏离常规。 暗示“社区会同意抽象隐藏了成本,不值得它试图提供的简单性”是在别人嘴里说的话。 虽然我们可以清楚地听到反对该提议的声音,但仍有相当一部分人(估计有 40%)表示同意继续进行该实验。 我们不要夸大其词地剥夺他们的权利。

谢谢。

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

很确定这应该是return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; FWIW。 😁

@bakul因为参数被立即评估,它实际上大致相当于:

<result list> := f()
defer try(<result list>)

这对某些人来说可能是意外的行为,因为f()不会延迟到以后,它会立即执行。 同样的事情也适用于go try(f())

@bakul文档提到defer try(f) (而不是defer try(f())因为try通常适用于任何表达式,而不仅仅是函数调用(你可以说try(err)为例如,如果err的类型是error )。所以不是拼写错误,但一开始可能会令人困惑。 f仅代表一个表达式,通常恰好是一个函数称呼。

@deanveloper@griesemer没关系 :-) 谢谢。

@carl-mastrangelo

_"很确定这应该是return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; _

您假设 PHP 7.x。 我不是。 但是话又说回来,考虑到你尖刻的脸,你知道那不是重点。 :眨眼:

我正在准备一个简短的演示,以在明天举行的 go meetup 期间展示这个讨论,并听到一些新的想法,因为我相信这个线程上的大多数参与者(贡献者或观察者)是那些更深入地参与语言的人,并且很可能“不是普通的开发人员”(只是一种预感)。

在这样做的时候,我记得我们实际上举行了一次关于错误的聚会,并讨论了两种模式:

  1. 在支持错误接口 mystruct.Error() 的同时扩展错误结构
  2. 将错误嵌入为结构的字段或匿名字段
type ExtErr struct{
  error
  someOtherField string
}  

这些用于我的团队实际构建的几个堆栈中。

提案问答指出
问:传递给 try 的最后一个参数必须是 error 类型。 为什么将传入的参数分配给错误是不够的?
A:“……如果有必要,我们可以在未来重新考虑这个决定”

任何人都可以评论类似的用例,以便我们了解上述错误扩展选项是否普遍需要这种需求?

@mikeschinkel我不是你要找的卡尔。

@daved ,回复:

将不乏可以放置这种便利的位置。 除此之外,正在寻求什么指标来证明机制的实质? 是否有分类错误处理案例的列表? 当大部分公共流程由情绪驱动时,如何从数据中获取价值?

该决定基于它在实际程序中的运行情况。 如果人们告诉我们尝试在他们的大部分代码中是无效的,那就是重要的数据。 这个过程是由这种数据驱动的。 它_不是_受情绪驱动的。

错误上下文

在这个问题中提出的最重要的语义问题是 try 是否会鼓励对上下文错误进行更好或更差的注释。

去年 8 月的问题概述在问题和目标部分提供了一系列示例 CopyFile 实现。 在过去和今天,任何解决方案都_更有可能_让用户为错误添加适当的上下文,这是一个明确的目标。 我们认为 try 可以做到这一点,否则我们不会提出它。

但在我们开始尝试之前,有必要确保我们都在同一页面上了解适当的错误上下文。 典型的例子是 os.Open。 引用 Go 博客文章“错误处理和 Go ”:

总结上下文是错误实现的责任。
os.Open 返回的错误格式为“打开 /etc/passwd:权限被拒绝”,而不仅仅是“权限被拒绝”。

另请参阅Effective Go 的错误部分

请注意,此约定可能与您熟悉的其他语言不同,并且也只是在 Go 代码中不一致地遵循。 尝试简化错误处理的一个明确目标是使人们更容易遵循此约定并添加适当的上下文,从而使其更一致地遵循。

今天有很多代码遵循 Go 约定,但也有很多代码假设相反的约定。 看到这样的代码太常见了:

f, err := os.Open(file)
if err != nil {
    log.Fatalf("opening %s: %v", file, err)
}

这当然会打印两次相同的东西(这个讨论中的许多例子看起来像这样)。 这项工作的一部分必须确保每个人都知道并遵守公约。

在遵循 Go 错误上下文约定的代码中,我们期望大多数函数将正确地为每个错误返回添加相同的上下文,以便通常应用一种修饰。 例如,在 CopyFile 示例中,在每种情况下都需要添加有关正在复制的内容的详细信息。 其他特定回报可能会增加更多上下文,但通常是附加而不是替代。 如果我们对这种期望有误,那很高兴知道。 来自真实代码库的明确证据会有所帮助。

Gophercon 检查/处理草案设计将使用如下代码:

func CopyFile(src, dst string) error {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

该提案对此进行了修改,但想法是相同的:

func CopyFile(src, dst string) (err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        }
    }()

    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    ...
}

我们想为这个通用模式添加一个尚未命名的助手:

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    ...
}

简而言之,这种方法的合理性和成功取决于这些假设和逻辑步骤:

  1. 人们应该遵循 Go 约定“被调用者添加它知道的相关上下文”。
  2. 因此大多数函数只需要添加描述整体的函数级上下文
    操作,而不是失败的特定子片(该子片已经自我报告)。
  3. 今天很多 Go 代码没有添加函数级上下文,因为它太重复了。
  4. 提供一种编写函数级上下文的方法将使其更有可能
    开发人员这样做。
  5. 最终结果将是更多遵循约定并添加适当上下文的 Go 代码。

如果有您认为错误的假设或逻辑步骤,我们想知道。 告诉我们的最好方法是指出实际代码库中的证据。 向我们展示您在尝试不合适或使事情变得更糟的常见模式。 向我们展示尝试比您预期更有效的事情的例子。 尝试量化您的代码库中有多少位于一侧或另一侧。 等等。 数据很重要。

谢谢。

感谢@rsc提供有关错误上下文最佳实践的附加信息。 关于最佳实践的这一点特别提到了我,但显着改善了try与错误上下文的关系。

因此大多数函数只需要添加描述整体的函数级上下文
操作,而不是失败的特定子片(该子片已经自我报告)。

所以try没有帮助的地方是当我们需要对错误做出反应时,而不仅仅是将它们上下文化。

为了改编一个来自Cleaner, more Elegant, and wrong的例子,这里是他们的一个函数的例子,它在错误处理中存在细微的错误。 我已经使用trydefer风格的错误包装将其改编为 Go:

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    try(AddToLeague(guy))
    return guy, nil
}

此函数不正确,因为如果guy.Team.Add(guy)成功但AddToLeague(guy)失败,则团队将拥有一个不在联赛中的无效 Guy 对象。 正确的代码如下所示,我们回滚guy.Team.Add(guy)并且不能再使用try

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    if err := AddToLeague(guy); err != nil {
        guy.Team.Remove(guy)
        return Guy{}, err
    }
    return guy, nil
}

或者,如果我们想避免为非错误返回值提供零值,我们可以将return Guy{}, err替换为try(err) 。 无论如何, defer -ed 函数仍在运行并添加了上下文,这很好。

同样,这意味着try对错误做出反应,而不是为错误添加上下文。 这个区别暗示了我,也许还有其他人。 这是有道理的,因为函数向错误添加上下文的方式并不是读者特别感兴趣的,但函数对错误的反应方式很重要。 我们应该让代码中不那么有趣的部分不那么冗长,这就是try所做的。

您不是唯一一个建议这不是问题或不值得解决的问题的人。 有关其他此类评论,请参阅https://swtch.com/try.html#nonissue 。 我们已经注意到了这些,并且确实希望确保我们正在解决实际问题。

@rsc我也认为当前的错误代码没有问题。 所以,请把我算进去。

像 Robert's tryhard 这样的工具可以帮助我们做到这一点。 我之前要求人们让我们知道他们在自己的代码库中发现了什么。 该信息对于评估更改是否值得至关重要。 你有一个猜测,我有一个不同的猜测,这很好。 答案是用数据代替这些猜测。

我查看了https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go ,我更喜欢旧代码。 令我惊讶的是,try 函数调用可能会中断当前的执行。 这不是当前 Go 的工作方式。

我怀疑,你会发现意见会有所不同。 我认为这是非常主观的。

而且,我怀疑大多数用户都没有参与这场辩论。 他们甚至不知道这种变化即将到来。 我自己对 Go 非常感兴趣,但我不参与这种变化,因为我没有空闲时间。

我认为我们现在需要重新教育所有现有的 Go 用户以不同的方式思考。

我们还需要决定如何处理一些拒绝在其代码中使用 try 的用户/公司。 肯定会有一些。

也许我们必须更改 gofmt 以自动重写当前代码。 强制此类“流氓”用户使用新的试用功能。 是否有可能让 gofmt 做到这一点?

当人们使用 go1.13 及之前使用 try 构建代码时,我们将如何处理编译错误?

我可能错过了许多其他我们必须克服的问题才能实现这一改变。 值得麻烦吗? 我不相信。

亚历克斯

@griesemer
在尝试尝试使用 97 错误未捕获的文件时,我发现 2 种模式未翻译
1:

    if err := updateItem(tx, fields, entityView.DataBinding, entityInstance); err != nil {
        tx.Rollback()
        return nil, err
    }

没有被替换,可能是因为 err := 和返回行之间的 tx.Rollback() ,
我认为只能由 defer 处理 - 如果所有错误路径都需要 tx.Rollback()
这是正确的吗 ?

  1. 它也不建议:
if err := db.Error; err != nil {
        return nil, err
    } else if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
        return nil, err
    } else {
        return itemDb, nil
    }

要么

    if err := db.Error; err != nil {
        return nil, err
    } else {
            if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
                return nil, err
            } else {
                return itemDb, nil
            }
        return result, nil
    }

这是因为阴影或嵌套尝试会转化为? 含义 - 这个用法应该尝试还是建议保留为 err := ... return err ?

@guybrand Re:您发现的两种模式:

1) 是的, tryhard不会很努力。 对于更复杂的情况,类型检查是必要的。 如果tx.Rollback()应该在所有路径中完成, defer可能是正确的方法。 否则,保留if可能是正确的方法。 这取决于具体的代码。

2) 此处相同: tryhard不寻找这种更复杂的模式。 也许可以。

同样,这是获得一些快速答案的实验性工具。 正确地做这件事需要更多的工作。

@alexbrainman

当人们使用 go1.13 及之前使用 try 构建代码时,我们将如何处理编译错误?

我的理解是语言本身的版本将由go.mod文件中的go语言版本指令控制,用于每段正在编译的代码。

飞行中的go.mod文档描述了go语言版本指令,如下所示:

go指令设置的预期语言版本确定
编译模块时可以使用哪些语言功能。
该版本中可用的语言功能将可供使用。
在早期版本中删除或在更高版本中添加的语言功能,
将不可用。 注意语言版本不影响
构建标签,由使用的 Go 版本决定。

如果假设像新的try内置在 Go 1.15 之类的东西中,那么此时go.mod文件读取go 1.12的人将无法访问新的try内置,即使它们使用 Go 1.15 工具链编译。 我对当前计划的理解是,如果他们想使用新的 Go,他们需要将go.mod中声明的 Go 语言版本从go 1.12更改为go 1.15 try的 1.15 语言功能。

另一方面,如果您有使用try的代码并且该代码位于一个模块中,该模块的go.mod文件将其 Go 语言版本声明为go 1.15 ,但随后有人试图使用 Go 1.12 工具链构建它,此时 Go 1.12 工具链将失败并出现编译错误。 Go 1.12 工具链对try一无所知,但它知道足以打印一条附加消息,即编译失败的代码声称需要 Go 1.15,基于go.mod文件中的内容. 实际上,您现在可以使用今天的 Go 1.12 工具链尝试此实验,并查看生成的错误消息:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

Go2 过渡提案文档中有更长的讨论。

也就是说,其确切细节可能会在其他地方更好地讨论(例如,可能在 #30791 或这个最近的 golang-nuts 线程中)。

@griesemer ,抱歉,如果我错过了对格式的更具体请求,但我很乐意分享一些结果,并有权访问(可能的许可)某些公司的源代码。
下面是一个小项目的真实示例,我认为所附结果提供了一个很好的示例,如果是这样,我们可能可以分享一些具有相似结果的表格:

总计 = 代码行数
$find /path/to/repo -name '*.go' -exec cat {} \; | wc -l
Errs = 带有 err := 的行数(这可能会错过 err = 和 myerr := ,但我认为在大多数情况下它涵盖了)
$find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
tryhard = tryhard 找到的行数

我测试研究的第一个案例返回:
总计 = 5106
错误 = 111
努力 = 16

更大的代码库
总计 = 131777
错误 = 3289
努力 = 265

如果这种格式是可以接受的,请告诉我们您希望如何获得结果,我认为只是把它扔在这里不会是正确的格式
此外,尝试计算行数可能会很快,err := 的场合(可能是 err = ,我试图学习的代码库中只有 4 个)

谢谢。

来自https://github.com/golang/go/issues/32437#issuecomment -503276339中的@griesemer

我敦促您查看此代码作为示例。

关于该代码,我注意到此处创建的输出文件似乎从未关闭。 此外,检查关闭已写入文件的错误也很重要,因为这可能是您唯一一次被告知写入存在问题。

我提出这个不是作为错误报告(尽管也许应该是?),而是作为一个机会来看看try是否对如何修复它有影响。 我将列举我能想到的修复它的所有方法,并考虑添加try是否会有所帮助或有害。 这里有一些方法:

  1. 在任何错误返回之前添加对outf.Close()的显式调用。
  2. 命名返回值并添加延迟以关闭文件,如果错误不存在则记录错误。 例如
func foo() (err error) {
    outf := try(os.Create())
    defer func() {
        cerr := outf.Close()
        if err == nil {
            err = cerr
        }
    }()

    ...
}
  1. “双重关闭”模式,其中一个执行defer outf.Close()以确保资源清理,并在返回之前执行try(outf.Close())以确保没有错误。
  2. 重构让帮助函数获取打开的文件而不是路径,以便调用者可以确保文件正确关闭。 例如
func foo() error {
    outf := try(os.Create())
    if err := helper(outf); err != nil {
        outf.Close()
        return err
    }
    try(outf.Close())
    return nil
}

我认为在除第 1 种情况外的所有情况下, try最坏的情况是中性的,通常是积极的。 考虑到该函数中错误可能性的大小和数量,我认为数字 1 是最不受欢迎的选项,因此添加try会降低否定选择的吸引力。

我希望这个分析是有用的。

如果假设像新的try内置在 Go 1.15 之类的东西中,那么此时go.mod文件读取go 1.12的人将无权访问

@thepudds谢谢你的解释。 但我不使用模块。 所以你的解释超出了我的想象。

亚历克斯

@alexbrainman

当人们使用 go1.13 及之前使用 try 构建代码时,我们将如何处理编译错误?

如果try假设要使用 Go 1.15 之类的东西,那么对您的问题的简短回答是,有人使用 Go 1.13 来构建带有try的代码会看到如下编译错误:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(至少据我了解关于过渡提案的说明)。

@alexbrainman感谢您的反馈。

该线程上的大量评论的形式是“这看起来不像 Go”,或者“Go 不能那样工作”,或者“我不希望这会在这里发生”。 这都是正确的,_existing_ Go 不是那样工作的。

这可能是第一个建议的语言更改,它以更实质性的方式影响语言的感觉。 我们意识到这一点,这就是为什么我们将其保持在最低限度。 (我很难想象一个具体的泛型提案可能会引起轩然大波——谈论语言变化)。

但回到你的观点:程序员习惯于编程语言的工作方式和感觉。 如果我在大约 35 年的编程过程中学到了什么,那就是习惯了几乎任何语言,而且发生得非常快。 在学习了原始 Pascal 作为我的第一门高级语言之后,编程语言不会将其所有关键字都大写是_不可想象的。 但是只用了一周左右的时间就习惯了 C 语言的“词海”,其中“一个人看不到代码的结构,因为它都是小写的”。 在使用 C 的最初几天之后,Pascal 代码看起来非常吵闹,所有实际代码似乎都埋在一堆乱七八糟的关键字中。 快进到 Go,当我们引入大写来标记导出的标识符时,如果我没记错的话,这是一个令人震惊的变化,如果我没记错的话,基于关键字的方法(这是在 Go 公开之前)。 现在我们认为这是更好的设计决策之一(具体想法实际上来自 Go 团队之外)。 或者,考虑以下思想实验:想象一下 Go 没有defer语句,现在有人为defer做了一个强有力的案例。 defer不像语言中的其他任何东西那样具有语义,新语言不再像defer Go 之前那样。 然而,在与它一起生活了十年之后,它似乎完全“像围棋”。

关键是,如果没有在实际代码中实际尝试该机制并收集具体反馈,对语言更改的最初反应几乎是毫无意义的。 当然,现有的错误处理代码很好,看起来比使用try的替换代码更清晰 - 我们已经接受了十年的训练,可以考虑那些if语句。 当然, try代码看起来很奇怪,并且具有“奇怪”的语义,我们以前从未使用过它,而且我们不会立即将其识别为语言的一部分。

这就是为什么我们要求人们通过在您自己的代码中进行试验来实际参与更改; 即,实际编写它,或者让tryhard运行现有代码,并考虑结果。 我建议让它静置一段时间,也许一周左右。 再看一遍,回来汇报。

最后,我同意你的评估,即大多数人不知道这个提议,或者没有参与。 很明显,这个讨论可能由十几个人主导。 不过现在还早,这个提案才出来两个星期,还没有做出决定。 有足够的时间让更多不同的人参与其中。

https://github.com/golang/go/issues/32437#issuecomment -503297387 几乎说如果你在一个函数中以多种方式包装错误,你显然做错了。 同时,我有很多看起来像这样的代码:

        if err := gen.Execute(tmp, s); err != nil {
                return fmt.Errorf("template error: %v", err)
        }

        if err := tmp.Close(); err != nil {
                return fmt.Errorf("cannot write temp file: %v", err)
        }
        closed = true

        if err := os.Rename(tmp.Name(), *genOutput); err != nil {
                return fmt.Errorf("cannot finalize file: %v", err)
        }
        removed = true

closedremoved被 defers 用于清理,视情况而定)

我真的不认为所有这些都应该被赋予相同的上下文来描述这个函数的顶级任务。 我真的不认为用户应该只看到

processing path/to/dir: template: gen:42:17: executing "gen" at <.Broken>: can't evaluate field Broken in type main.state

当模板搞砸时,我认为模板执行调用的错误处理程序有责任添加“执行模板”或一些额外的部分。 (这不是最重要的上下文,但我想复制粘贴真实代码而不是虚构的示例。)

我认为用户不应该看到

processing path/to/dir: rename /tmp/blahDs3x42aD commands.gen.go: No such file or directory

没有一些_为什么_的线索,我的程序试图让重命名发生,语义是什么,意图是什么。 我相信添加一点“无法完成文件:”确实有帮助。

如果这些示例不足以说服您,请想象一下命令行应用程序的以下错误输出:

processing path/to/dir: open /some/path/here: No such file or directory

那是什么意思? 我想添加应用程序尝试在那里创建文件的原因(您甚至不知道这是一个创建,而不仅仅是 os.Open!它是 ENOENT,因为不存在中间路径。)。 这不是应该添加到此函数的 _every_ 错误返回中的内容。

那么,我错过了什么。 我是不是“拿错了”? 我是否应该将这些东西中的每一个都推入一个单独的小函数中,这些函数都使用 defer 来包装所有错误?

@guybrand感谢这些数字。 最好了解一下为什么tryhard数字是这样的。 也许有很多特定的错误装饰正在进行? 如果是这样,那就太好了, if语句是正确的选择。

当我得到它时,我会改进这个工具。

谢谢@zeebo分析。 我不具体了解这段代码,但看起来outfloadCmdReader (第 173 行)的一部分,然后在第 204 行传递。也许这就是outf的原因

@tv42从您的https://github.com/golang/go/issues/32437#issuecomment -503340426 中的示例中,假设您没有做“错误”,似乎使用if语句如果它们都需要不同的响应,这是处理这些情况的方法。 try无济于事, defer只会让它变得更难(此线程中任何其他试图使此代码更易于编写的语言更改提案都非常接近if声明不值得引入新机制)。 另请参阅详细提案的常见问题解答。

@griesemer然后我能想到的就是你和@rsc不同意。 或者我确实是“做错了”,并想就此进行对话。

在过去和今天,任何解决方案都可以让用户更有可能为错误添加适当的上下文,这是一个明确的目标。 我们认为 try 可以做到这一点,否则我们不会提出它。

@tv42 @rsc帖子是关于好的代码的整体错误处理结构,我同意。 如果您有一段不完全符合此模式的现有代码并且您对代码感到满意,请不要理会它。

推迟

Gophercon 检查/处理草案到该提案的主要变化是放弃handle以支持重用defer 。 现在错误上下文将通过类似这个延迟调用的代码添加(参见我之前关于错误上下文的评论):

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

在这个例子中,defer 作为错误注释机制的可行性取决于几件事。

  1. _命名错误结果。_添加命名错误结果引起了很多关注。 确实,过去我们不鼓励在文档目的不需要的地方这样做,但这是我们在没有任何更强大的决定因素的情况下选择的约定。 即使在过去,更强有力的决定因素(例如在文档中引用特定结果)也超过了对未命名结果的一般约定。 现在有第二个更强的决定因素,即想要在延迟中引用错误。 这似乎不应该比在文档中使用命名结果更令人反感。 很多人对此反应非常消极,老实说,我不明白为什么。 人们似乎将没有表达式列表的返回(所谓的“裸返回”)与命名结果混为一谈。 确实,没有表达式列表的返回会导致较大函数的混乱。 通过避免长函数中的返回来避免这种混淆通常是有意义的。 使用相同的画笔绘制命名结果不会。

  2. _地址表达式。_ 一些人提出了使用这种模式将要求 Go 开发人员理解地址表达式的担忧。 使用指针方法将任何值存储到接口中已经需要这样做,因此这似乎不是一个明显的缺点。

  3. _Defer 本身。_ 一些人对使用 defer 作为一个语言概念提出了担忧,因为新用户可能不熟悉它。 与地址表达式一样,defer 是最终必须学习的核心语言概念。 围绕defer f.Close()defer l.mu.Unlock()之类的标准习语非常常见,以至于很难证明避免将 defer 作为语言的一个晦涩角落是合理的。

  4. _Performance._ 我们多年来一直在讨论如何使常见的延迟模式(例如函数顶部的延迟)与在每次返回时手动插入该调用相比具有零开销。 我们认为我们知道如何做到这一点,并将在下一个 Go 版本中进行探索。 即使没有,但对于大多数需要添加错误上下文的调用来说,大约 50 ns 的当前开销不应过高。 并且少数对性能敏感的调用可以继续使用 if 语句,直到 defer 更快。

前三个关注点都反对重用现有的语言特性。 但是重用现有的语言特性正是这个提议相对于 check/handle 的进步:添加到核心语言中的内容更少,要学习的新内容更少,令人惊讶的交互更少。

尽管如此,我们仍然赞赏以这种方式使用 defer 是新的,并且我们需要给人们时间来评估 defer 在实践中是否足够好地满足他们需要的错误处理习惯用法。

自从我们去年八月开始讨论以来,我一直在做“这段代码在检查/句柄下看起来如何?”的心理练习。 以及最近的“使用 try/defer?” 每次我写新代码。 通常答案意味着我编写了不同的、更好的代码,将上下文添加到一个地方(延迟)而不是每次返回或完全省略。

鉴于使用延迟处理程序对错误采取措施的想法,我们可以使用简单的库包启用多种模式。 我已经提交了 #32676 以对此进行更多思考,但是在该问题中使用包 API,我们的代码将如下所示:

func CopyFile(src, dst string) (err error) {
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

如果我们在调试 CopyFile 并且想要查看任何返回的错误和堆栈跟踪(类似于想要插入调试打印),我们可以使用:

func CopyFile(src, dst string) (err error) {
    defer errd.Trace(&err)
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

等等。

以这种方式使用 defer 最终会变得相当强大,并且它保留了检查/处理的优势,您可以在函数顶部写一次“对任何错误都执行此操作”,然后在其余部分不必担心身体。 这以与早期快速退出大致相同的方式提高了可读性。

这在实践中会起作用吗? 我们想知道。

在我自己的代码中对 defer 的外观进行了几个月的心理实验后,我认为它可能会起作用。 但当然,在实际代码中使用它并不总是相同的。 我们需要通过实验来找出答案。

今天,人们可以通过继续编写if err != nil语句来试验这种方法,但复制 defer 助手并酌情使用它们。 如果您愿意这样做,请告诉我们您学到了什么。

@tv42 ,我同意@griesemer。 如果您发现需要额外的上下文来平滑连接,例如将重命名为“finalize”步骤,那么使用 if 语句添加额外的上下文没有任何问题。 然而,在许多功能中,几乎不需要这种额外的上下文。

@guybrand ,尝试性的数字很棒,但更好的是描述为什么特定示例没有转换,而且不适合重写以便可以转换。 @tv42的例子和解释就是一个例子。

@griesemer关于您对 defer 的关注。 我打算使用emit最初的提案handle 。 如果err不为零,则将调用 $#$ emit/handle $#$。 并且将在那个时刻而不是在函数结束时启动。 延迟在最后被调用。 emit/handle将根据err是否为 nil 来结束函数。 这就是为什么延迟不起作用的原因。

一些数据:

在我兜售以虔诚地消除“赤裸裸的错误回报”的〜70k LOC项目中,我们仍然有612个赤裸裸的错误回报。 主要处理记录错误的情况,但消息仅在内部重要(给用户的消息是预定义的)。 但是,try() 将比每次裸返回仅节省两行代码节省更多,因为有了预定义的错误,我们可以推迟处理程序并在更多地方使用 try。

更有趣的是,在供应商目录中,在 ~620k+ LOC 中,我们只有 1600 个裸错误返回。 我们选择的图书馆往往比我们更虔诚地修饰错误。

@rsc如果稍后将处理程序添加到try是否会有一个带有func Wrap(msg string) func(error) error之类的函数的错误/errc 包,因此您可以执行try(f(), errc.Wrap("f failed"))吗?

@damienfamed75感谢您的解释。 因此,当try发现错误时,将调用emit ,并使用该错误调用它。 这似乎很清楚。

您还说emit将在出现错误时结束函数,而不是在以某种方式处理错误时结束。 如果不结束函数,代码在哪里继续? 大概是从try返回(否则我不明白emit不会结束函数)。 在这种情况下,仅使用if而不是try会不会更容易和更清晰? 在这些情况下,使用emithandle会极大地模糊控制流,尤其是因为emit子句可以位于函数中完全不同的部分(可能更早)。 (在那张纸条上,一个人可以有多个emit吗?如果不能,为什么不呢?如果没有emit会发生什么?很多与原始check相同的问题handle设计草图。)

只有当一个人想要从一个除了错误装饰之外没有太多额外工作的函数返回,或者总是做相同的工作时,使用try和某种处理程序才有意义。 在函数返回之前运行的处理程序机制已经存在于defer中。

@guybrand (和@griesemer)关于您的第二个无法识别的模式,请参阅https://github.com/griesemer/tryhard/issues/2

@daved

当大部分公共流程由情绪驱动时,如何从数据中获取价值?

也许其他人可能有像我在这里报道的经历。 我希望翻阅由tryhard try实例,发现它们看起来或多或少类似于该线程中已经存在的内容,然后继续。 相反,我惊讶地发现try以一种以前从未讨论过的方式导致明显更好的代码。

所以至少还有希望。 :)

对于尝试tryhard的人,如果您还没有尝试过,我会鼓励您不仅要查看该工具所做的更改,还要查看err != nil的剩余实例并查看它留下了什么,为什么。

(还要注意在 https://github.com/griesemer/tryhard/ 上有几个问题和 PR。)

@rsc这里是我对为什么我个人不喜欢defer HandleFunc(&err, ...)模式的见解。 不是因为我把它和赤裸裸的回报什么的联系在一起,只是感觉太“聪明”了。

几个月前(也许一年?)有一个错误处理提案,但是我现在已经忘记了。 我忘记了它的要求,但是有人回应了以下内容:

func myFunction() (i int, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("wrapping the error: %s", err)
        }
    }()

    // ...
    return 0, err

    // ...
    return someInt, nil
}

至少可以说很有趣。 这是我第一次看到defer用于错误处理,现在在这里展示。 我认为它是“聪明的”和“hacky”的,而且,至少在我提出的例子中,它不像 Go。 然而,将它包装在一个适当的函数调用中,比如fmt.HandleErrorf确实有助于它感觉更好。 不过,我仍然对此感到消极。

我看到人们不喜欢它的另一个原因是,当人们写return ..., err时,看起来应该返回err 。 但它不会被返回,而是在发送之前修改值。 我之前说过return在 Go 中一直看起来像是一个“神圣”的操作,鼓励在实际返回之前修改返回值的代码感觉是错误的。

好的,然后是数字和数据。 :)

我在我们微服务平台的几个服务的源上运行了 tryhard,并将其与 loccount 和 grep 'if err' 的结果进行了比较。 我以 loccount / grep 'if err' | 的顺序得到了以下结果厕所/努力:

1382 / 64 / 14
108554 / 66 / 5
58401 / 22 / 5
2052/247/39
12024 / 1655 / 1

我们的一些微服务做了很多错误处理,有些只做很少,但不幸的是,tryhard 只能自动改进代码,充其量是 22% 的情况,更糟糕的是不到 1%。 现在,我们不打算手动重写我们的错误处理,所以像 tryhard 这样的工具对于在我们的代码库中引入try()是必不可少的。 我很欣赏这是一个简单的初步工具,但我很惊讶它很少能提供帮助。

但我认为,现在,有了数字,我可以说,对于我们的使用,try() 并没有真正解决任何问题,或者,至少在 tryhard 变得更好之前不会。

我还在我们的代码库中发现,$#$ try() $#$ 的if err != nil { return err }用例实际上非常罕见,这与 go 编译器不同,后者很常见。 恕我直言,但我认为 Go 设计者,他们比其他代码库更频繁地查看 Go 编译器源代码,因此高估了try()的有用性。

@beoran tryhard目前非常初级。 您是否知道为什么try在您的代码库中很少见的最常见原因? 例如,因为你装饰了错误? 因为你在回来之前做了其他额外的工作? 还有什么?

@rsc, @ griesemer

至于例子,我在这里给出了两个重复的样本,tryHard 错过了,一个可能会保持为“if Err :=",另一个可能会被解决

至于错误装饰,我在代码中看到的两种重复模式是(我将两者放在一个代码片段中):

if v, err := someFunction(vars...) ; err != nil {
        return fmt.Errorf("extra data to help with where did error occur and params are %s , %d , err : %v",
            strParam, intParam, err)
    } else if v2, err := passToAnotherFunc(v,vars ...);err != nil {
        extraData := DoSomethingAccordingTo(v2,err)
        return formatError(err,extraData)
    } else {

    }

很多时候 formatError 是应用程序的一些标准或曾经交叉存储库,最重复的是 DbError 格式(所有应用程序/应用程序中的一个函数,在几十个位置使用),在某些情况下(没有进入“这是一个正确的模式”)保存一些数据到日志(失败的 sql 查询你不想向上传递堆栈)和一些其他文本到错误。

换句话说,如果我想“用额外的数据做任何聪明的事情,比如记录错误 A 和引发错误 B,除了我提到这两个选项来扩展错误处理
这是“不仅仅是返回错误并让'其他人'或'其他一些功能'处理它”的另一种选择

这意味着在“库”中 try() 的使用可能比在“可执行程序”中更多,也许我会尝试运行 Total/Errs/tryHard 比较来区分库和可运行程序(“应用程序”)。

我发现自己完全处于https://github.com/golang/go/issues/32437#issuecomment -503297387 中描述的情况
在某些级别我单独包装错误,我不会用try来改变它,用if err!=nil就可以了。
在其他级别,我只是return err为所有返回添加相同的上下文很痛苦,然后我将使用trydefer
我什至已经使用我在函数开始时使用的特定记录器来执行此操作,以防出现错误。 对我来说try和按功能装饰已经很糟糕了。

@thepudds

如果try假设要使用 Go 1.15 之类的东西,那么对您的问题的简短回答是有人使用 Go 1.13

Go 1.13 甚至还没有发布,所以我不能使用它。 而且,鉴于我的项目不使用 Go 模块,我将无法升级到 Go 1.13。 (我相信 Go 1.13 会要求每个人都使用 Go 模块)

使用try构建代码会看到如下编译错误:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(至少据我了解关于过渡提案的说明)。

这都是假设的。 我很难评论虚构的东西。 而且,也许您喜欢这个错误,但我发现它令人困惑且无益。

如果 try 未定义,我会 grep 。 我什么也找不到。 那我该怎么办?

在这种情况下, note: module requires Go 1.15是最糟糕的帮助。 为什么是module ? 为什么是Go 1.15

@griesemer

这可能是第一个建议的语言更改,它以更实质性的方式影响语言的感觉。 我们意识到这一点,这就是为什么我们将其保持在最低限度。 (我很难想象一个具体的泛型提案可能会引起轩然大波——谈论语言变化)。

我宁愿你花时间在泛型上,而不是尝试。 也许在 Go 中使用泛型是有好处的。

但回到你的观点:程序员习惯于编程语言的工作方式和感觉。 ...

我同意你的所有观点。 但是我们正在讨论用 try 函数调用替换特定形式的 if 语句。 这是一种以简单性和正交性为荣的语言。 我可以习惯一切,但有什么意义呢? 为了节省几行代码?

或者,考虑以下思想实验:想象一下 Go 没有defer语句,现在有人为defer做了一个强有力的案例。 defer不像语言中的其他任何东西那样具有语义,新语言不再像defer Go 之前那样。 然而,在与它一起生活了十年之后,它似乎完全“像围棋”。

多年后,我仍然被defer body 欺骗并关闭了变量。 但是,当涉及到资源管理时, defer付出了它的代价。 我无法想象没有defer的情况。 但我不准备为try支付类似的价格,因为我认为这里没有任何好处。

这就是为什么我们要求人们通过在您自己的代码中进行试验来实际参与更改; 即,实际编写它,或者让tryhard运行现有代码,并考虑结果。 我建议让它静置一段时间,也许一周左右。 再看一遍,回来汇报。

我尝试改变我的小项目(大约 1200 行代码)。 它看起来类似于您在https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go上的更改我看不到我的意见一周后改变这个。 我的脑子里总是被什么东西占据,我会忘记的。

……不过现在还早,这个提案才出来两周,……

而且我可以在这个线程上看到关于这个提案的 504 条消息。 如果我有兴趣推动这种变化,我需要几天甚至几周的时间来阅读和理解这一切。 我不羡慕你的工作。

感谢您花时间回复我的信息。 抱歉,如果我不回复此线程 - 它太大了,我无法监控,无论消息是否发给我。

亚历克斯

@griesemer感谢您的精彩提议,tryhard 似乎比我期望的更有用。 我也会想欣赏。

@rsc感谢清晰的响应和工具。

关注这个帖子有一段时间了, @beoran的以下评论让我不寒而栗

隐藏错误变量和返回无助于使事情更容易理解

之前已经管理过几个bad written code ,我可以证明这是每个开发人员最糟糕的噩梦。

文档说使用A喜欢的事实并不意味着它会被遵循,事实仍然存在,如果可以使用AAAB那么如何使用没有限制它可以使用。

To my surprise, people already think the code below is cool ...我认为it's an abomination向任何冒犯的人道歉。

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

等到你检查AsCommit你会看到

func AsCommit() error(){
    return try(try(try(tail()).find()).auth())
}

疯狂还在继续,老实说,我不想相信这是@robpike simplicity is complicated的定义(幽默)

基于@rsc示例

// Example 1
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// Example 2 
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// Example 3 
try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

我赞成Example 2加一点else ,但请注意,这可能不是最好的方法

  • 很容易清楚地看到错误
  • 最不可能变异成其他人可以生出的abomination
  • try的行为不像普通函数。 给它类似函数的语法是很少的。 go使用if如果我可以将其更改为try tree := r.LookupTree(treeOid) else {感觉更自然
  • 错误可能非常昂贵,它们需要尽可能多的可见性,我认为这就是 go 不支持传统的trycatch的原因
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)

parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()

try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid) else { 
    // Heal the world 
   // I may return with return keyword 
   // I may not return but set some values to 0 
   // I may remember I need to log only this 
   // I may send a mail to let the cute monkeys know the server is on fire 
}

我想再次为自己有点自私而道歉。

@josharian我不能在这里透露太多,但是,原因是多种多样的。 正如您所说,我们确实装饰了错误,或者也进行了不同的处理,而且,一个重要的用例是我们记录它们,其中日志消息对于函数可以返回的每个错误都不同,或者因为我们使用if err := foo() ; err != nil { /* various handling*/ ; return err }形式,或其他原因。

我想强调的是: try()设计的简单用例在我们的代码库中很少出现。 所以,对我们来说,在语言中添加 'try()' 并没有什么好处。

编辑:如果 try() 将被实现,那么我认为下一步应该是让 tryhard 变得更好,因此它可以广泛用于升级现有的代码库。

@griesemer我将尝试从您上次的回复中一一解决您的所有疑虑。
首先,您询问处理程序是否不以某种方式返回或退出函数,然后会发生什么。 是的,在某些情况下, emit / handle子句不会返回或退出函数,而是从中断处继续。 例如,如果我们尝试使用阅读器查找分隔符或简单的东西,并且我们到达EOF ,我们可能不希望在遇到该错误时返回错误。 所以我建立了这个可能看起来像这样的快速示例:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err {
        // if this doesn't return then continue from where we left off
        // at the try function that was called last.
        if err != io.EOF {
            return nil, err
        }
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

甚至可以进一步简化为:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err != io.EOF {
        return nil, err
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

第二个问题是控制流的中断。 是的,它会扰乱流程,但公平地说,大多数提案都在某种程度上扰乱了流程,以拥有一个中央错误处理功能等。 我相信这没有什么不同。
接下来,您问我们是否多次使用emit / handle ,我说它被重新定义了。
如果您多次使用emit ,它将覆盖最后一个,依此类推。 如果你没有,那么try将有一个默认处理程序,它只返回 nil 值和错误。 这意味着这里的示例:

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        return nil, err
    }

    f := try(os.Open(filename))

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

会做和这个例子一样的事情:

func writeStuff(filename string) (io.ReadCloser, error) {
    // when not defining a handler then try's default handler kicks in to
    // return nil valued then error as usual.

    f := try(os.Open(filename))

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

您的最后一个问题是关于声明一个在defer中调用的处理程序函数,我假设引用了error 。 这个设计的工作方式与这个提议的工作方式不同,理由是defer不能在给定条件本身的情况下立即停止函数。

我相信我在您的回复中解决了所有问题,我希望这能进一步澄清我的提议。 如果还有问题,请告诉我,因为我认为与每个人的整个讨论对于思考新想法很有趣。 大家继续努力!

@velovix ,重新https://github.com/golang/go/issues/32437#issuecomment -503314834:

同样,这意味着try对错误做出反应,而不是为错误添加上下文。 这个区别暗示了我,也许还有其他人。 这是有道理的,因为函数向错误添加上下文的方式并不是读者特别感兴趣的,但函数对错误的反应方式很重要。 我们应该使代码中不那么有趣的部分不那么冗长,这就是try所做的。

这是一个非常好的表达方式。 谢谢。

@olekukonko ,回复https://github.com/golang/go/issues/32437#issuecomment -503508478:

To my surprise, people already think the code below is cool ...我认为it's an abomination向任何冒犯的人道歉。

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Grepping https://swtch.com/try.html ,该表达式在此线程中出现了 3 次。
@goodwine提出它是糟糕的代码,我同意, @velovix说“尽管它很丑......比你经常在 try-catch 语言中看到的要好......因为你仍然可以分辨出代码的哪些部分可能会转移由于错误而不能控制流。”

没有人说它“很酷”或可以作为伟大的代码提出。 同样,总是有可能写出糟糕的代码

我也只想说重新

错误可能非常昂贵,它们需要尽可能多的可见性

Go 中的错误并不意味着代价高昂。 它们是日常的、普通的事件,并且是轻量级的。 (这与一些异常的实现形成鲜明对比。我们曾经有一个服务器花费了太多的 CPU 时间来准备和丢弃包含堆栈跟踪的异常对象,以在循环中检查已知列表中失败的“文件打开”调用给定文件的位置。)

@alexbrainman ,我很抱歉如果旧版本的 Go 构建代码包含 try. 简短的回答是,它就像我们更改语言的任何其他时间一样:旧编译器将拒绝新代码,并带有几乎无用的消息(在这种情况下为“未定义:尝试”)。 该消息是无用的,因为旧的编译器不知道新的语法,不能真正提供更多的帮助。 那时人们可能会在网络上搜索“go undefined try”并了解新功能。

@thepudds的示例中,使用 try 的代码有一个 go.mod,其中包含“go 1.15”行,这意味着模块的作者说代码是针对 Go 语言的版本编写的。 这可以作为旧的 go 命令的信号,在编译错误后建议可能无用的消息是由于 Go 版本太旧。 这明确地试图使消息更有帮助,而不强迫用户诉诸网络搜索。 如果有帮助,很好; 如果没有,无论如何,网络搜索似乎都非常有效。

@guybrand ,重新https://github.com/golang/go/issues/32437#issuecomment -503287670 并为您的聚会可能为时已晚表示歉意:

返回不完全错误类型的函数通常存在的一个问题是,对于非接口,转换为错误不会保持零性。 因此,例如,如果您有自己的自定义 *MyError 具体类型(例如,指向结构的指针)并使用 err == nil 作为成功信号,那很好,直到您拥有

func f() (int, *MyError)
func g() (int, error) { x, err := f(); return x, err }

如果 f 返回 nil *MyError,则 g 返回与非 nil 错误相同的值,这可能不是预期的。 如果 *MyError 是一个接口而不是一个结构指针,那么转换会保留 nilness,但即便如此,它也是一个微妙之处。

对于 try,你可能会认为因为 try 只会触发非 nil 值,所以没问题。 例如,这实际上可以在 f 失败时返回非 nil 错误,并且在 f 成功时返回 nil 错误也可以:

func g() (int, error) {
    return try(f()), nil
}

所以这实际上很好,但是你可能会看到这个并考虑将其重写为

func g() (int, error) {
    return f()
}

这似乎应该是相同的,但不是。

尝试提案有足够多的其他细节需要在实际经验中仔细检查和评估,似乎最好推迟决定这个特别的微妙之处。

感谢大家迄今为止的所有反馈。 至此,我们似乎已经确定了try的主要好处、关注点以及可能的好坏影响。 为了取得进展,需要进一步评估try对实际代码库的意义。 在这一点上的讨论是围绕并重复这些相同的观点。

经验现在比继续讨论更有价值。 我们希望鼓励人们花时间试验try在他们自己的代码库中的样子,并在反馈页面上编写和链接体验报告

为了给每个人一些喘息和试验的时间,我们将暂停这个对话并将这个问题锁定在接下来的一个半星期。

锁定将在 1p PDT/4p EDT 左右开始(从现在起大约 3 小时),让人们有机会提交待处理的帖子。 我们将在 7 月 1 日重新讨论这个问题。

请放心,我们无意在不花时间充分理解它们并确保它们在实际代码中解决实际问题的情况下匆忙推出任何新的语言功能。 就像我们过去所做的那样,我们将花时间来解决这个问题。

该 wiki 页面挤满了对检查/处理的响应。 我建议你开始一个新的页面。

无论如何,我没有时间继续在 wiki 中园艺。

@networkimprov ,感谢您对园艺的帮助。 我在https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback 中创建了一个新的顶部部分。 我认为这应该比一个全新的页面更好。

我也错过了 Robert 的 1p PDT / 4p EDT note 的锁,所以我短暂地锁定它有点太早了。 又开了,时间有点长。

我正计划写这篇文章,只是想在它被锁定之前完成它。

我希望围棋团队不要看到批评,并认为它代表了大多数人的情绪。 少数声音总是倾向于压倒谈话,我觉得这可能发生在这里。 当每个人都在切线时,它会阻止其他只想按原样谈论提案的人。

所以 - 我想表达我对它的价值的积极立场。

我的代码已经使用 defer 来装饰/注释错误,甚至用于吐出堆栈跟踪,正是这个原因。

看:
https://github.com/ugorji/go-ndb/blob/master/ndb/ndb.go#L331
https://github.com/ugorji/go-serverapp/blob/master/app/baseapp.go#L129
https://github.com/ugorji/go-serverapp/blob/master/app/webrouter.go#L180

都调用 errorutil.OnError(*error)

https://github.com/ugorji/go-common/blob/master/errorutil/errors.go#L193

这与 Russ/Robert 之前提到的 defer helpers 类似。

这是我已经使用的模式,FWIW。 这不是魔术。 这完全像恕我直言。

我还将它与命名参数一起使用,并且效果很好。

我这样说是为了反驳这里推荐的任何东西都是魔法的观点。

其次,我想在 try(...) 作为函数添加一些评论。
它比关键字有一个明显的优势,因为它可以扩展为接受参数。

这里讨论了 2 种扩展模式:

  • 扩展尝试带标签跳转到
  • 扩展尝试采用 func(error) 错误形式的处理程序

对于它们中的每一个,都需要将 try 作为函数采用单个参数,并且可以在以后扩展它以在必要时采用第二个参数。

尚未决定是否有必要延长尝试,如果需要,采取什么方向。 因此,第一个方向是提供尝试消除大多数“if err != nil { return err }”的口吃,我一直讨厌这种口吃,但把它当作做生意的成本。

我个人很高兴 try 是一个函数,我可以调用 inline 例如我可以写

var u User = db.loadUser(try(strconv.Atoi(stringId)))

反对:

var id int // i have to define this on its own if err is already defined in an enclosing block
id, err = strconv.Atoi(stringId)
if err != nil {
  return
}
var u User = db.loadUser(id)

如您所见,我只是将 6 行缩减为 1 行。其中 5 行是真正的样板。
这是我处理过很多次的事情,并且我已经编写了很多 go 代码和包 - 你可以查看我的 github 以查看我在线发布的一些内容,或者我的 go-codec 库。

最后,这里的许多评论并没有真正表明提案存在问题,尽管他们已经提出了他们自己喜欢的解决问题的方法。

我个人很高兴 try(...) 的出现。我很欣赏为什么 try 作为函数是首选解决方案的原因。 我显然喜欢在这里使用延迟,因为它只是有意义的。

让我们记住 Go 的核心原则之一——可以很好地组合的正交概念。 这个提议利用了一堆 Go 的正交概念(延迟、命名返回参数、内置函数来完成通过用户代码不可能完成的事情等)来提供关键优势:
Go 用户多年来普遍要求减少/消除 if err != nil { return err } 样板。 Go 用户调查表明这是一个真正的问题。 Go 团队意识到这是一个真正的问题。 我很高兴少数人的大声喧哗并没有过多地扭曲围棋队的立场。

如果 err != nil,我有一个关于 try 作为隐式 goto 的问题。

如果我们决定这是方向,是否很难将“try do a return”改造成“try do a goto”,
鉴于 goto 已经定义了你不能越过未分配变量的语义?

感谢您的留言,@ugorji。

如果 err != nil,我有一个关于 try 作为隐式 goto 的问题。

如果我们决定这是方向,是否很难将“try do a return”改造成“try do a goto”,
鉴于 goto 已经定义了你不能越过未分配变量的语义?

是的,完全正确。 关于#26058 有一些讨论。
我认为 'try-goto' 至少有 3 次打击:
(1) 你必须回答未分配的变量,
(2) 您丢失了有关哪个尝试失败的堆栈信息,相比之下,您仍然可以在 return+defer 情况下捕获这些信息,并且
(3) 人人都爱恨goto。

是的, try是要走的路。
我试过一次添加try ,我喜欢它。
补丁 - https://github.com/ascheglov/go/pull/1
Reddit 上的主题 - https://www.reddit.com/r/golang/comments/6vt3el/the_try_keyword_proofofconcept/

@griesemer

继续https://github.com/golang/go/issues/32825#issuecomment -507120860 ...

伴随着try的滥用将通过代码审查、审查和/或社区标准得到缓解的前提,我可以看到避免更改语言以限制try灵活性的智慧

在对此进行分解时,似乎有两种形式的错误路径控制流被表达:手动和自动。 关于错误包装,似乎表达了三种形式:直接、间接和传递。 这导致错误处理的总共六种“模式”。

手动直接和自动直接模式似乎很合适:

wrap := func(err error) error {
  return fmt.Errorf("failed to process %s: %v", filename, err)
}

f, err := os.Open(filename)
if err != nil {
    return nil, wrap(err)
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, wrap(err)
}
// in errors, named better, and optimized
WrapfFunc := func(format string, args ...interface{}) func(error) error {
  return func(err error) error {
    if err == nil {
      return nil
    }
    s := fmt.Sprintf(format, args...)
    return errors.Errorf(s+": %w", err)
  }
}

```去
wrap := errors.WrapfFunc("无法处理 %s", 文件名)

f, err := os.Open(文件名)
尝试(包装(错误))
推迟 f.Close()

信息,错误:= f.Stat()
尝试(包装(错误))

Manual Pass-through, and Automatic Pass-through modes are also simple enough to be agreeable (despite often being a code smell):
```go
f, err := os.Open(filename)
if err != nil {
    return nil, err
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, err
}
f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

然而,手动间接模式和自动间接模式都非常令人不快,因为很可能会出现细微的错误:

defer errd.Wrap(&err, "failed to do X for %s", filename)

var f *os.File
f, err = os.Open(filename)
if err != nil {
    return
}
defer f.Close()

var info os.FileInfo
info, err = f.Stat()
if err != nil {
    return
}
defer errd.Wrap(&err, "failed to do X for %s", filename)

f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

再一次,我可以理解不禁止它们,但促进/祝福间接模式是这仍然对我提出明确的危险信号的地方。 足够了,在这个时候,让我对整个前提保持强烈的怀疑。

尝试不能是避免该死的功能

info := try(try(os.Open(filename)).Stat())

文件泄漏。

我的意思是try语句不允许链接。 作为奖励,它看起来更好。 虽然存在兼容性问题。

@sirkon由于try是特殊的,如果这很重要,该语言可能不允许嵌套try ——即使try看起来像一个函数。 同样,如果这是try的唯一障碍,则可以通过各种方式轻松解决( go vet或语言限制)。 让我们继续前进——我们现在已经听过很多次了。 谢谢。

让我们继续前进——我们之前已经听过很多次了

“这太无聊了,让我们继续前进吧”

还有一个很好的类比:

- 你的理论与事实相矛盾!
- 事实更糟!

黑格尔

我的意思是你正在解决一个实际上不存在的问题。 以及丑陋的方式。

让我们看看这个问题实际上出现在哪里:处理来自外部世界的副作用,就是这样。 这实际上是软件工程中逻辑上最简单的部分之一。 最重要的是。 我不明白为什么我们需要对最简单的事情进行简化,这会降低我们的可靠性。

IMO 这类最难解决的问题是分布式系统中的数据一致性保存(实际上并非如此分布式)。 在解决这些问题时,错误处理并不是我在 Go 中遇到的问题。 缺乏切片和映射理解,缺乏总和/代数/方差/任何类型都更烦人。

既然这里的争论似乎有增无减,让我再重复一遍

经验现在比继续讨论更有价值。 我们希望鼓励人们花时间试验try在他们自己的代码库中的样子,并在反馈页面上编写和链接体验报告。

如果具体经验提供了支持或反对该提议的重要证据,我们希望在这里听到。 我们可以承认个人的烦恼、假设场景、替代设计等,但它们的可操作性较差。

谢谢。

我不想在这里粗鲁,我感谢您的节制,但是社区已经非常强烈地表示要更改错误处理。 更改事物或添加新代码会使_所有_喜欢当前系统的人感到不安。 你不能让每个人都开心,所以让我们关注可以让每个人开心的 88%(数字来自下面的投票率)。

在撰写本文时,“别管它”线程是 1322 票赞成和 158 票反对。 该线程在 158 上升和 255 下降。 如果这不是该线程关于错误处理的直接结束,那么我们应该有一个很好的理由继续推动这个问题。

有可能总是做你的社区尖叫的事情,并在同一时间摧毁你的产品。

至少,我认为这个具体的提议应该被认为是失败的。

幸运的是, go不是由委员会设计的。 我们需要相信,我们都喜欢的语言的保管人将继续根据可用的所有数据做出最佳决定,而不是根据大众的普遍意见做出决定。 请记住 - 他们也使用 go,就像我们一样。 他们和我们一样感受到痛点。

如果你有一个职位,花时间去捍卫它,就像围棋团队捍卫他们的提议一样。 否则,您只是在用不可操作且无法推进对话的夜间情绪淹没对话。 这让想要参与的人变得更加困难,因为人们可能只想等到噪音平息后再进行。

当提案过程开始时,Russ 非常重视宣传经验报告的必要性,以此作为影响提案或让您的请求被听到的一种方式。 让我们至少努力尊重这一点。

Go 团队一直在考虑所有可操作的反馈。 他们还没有让我们失望。 请参阅为别名、模块等生成的详细文档。让我们至少给予他们同样的重视,花时间思考我们的反对意见,回应他们对您的反对意见的立场,让您的反对意见更难被忽视。

Go 的优势一直在于它是一种小型、简单的语言,具有正交结构,由一小群人设计,他们会在确定某个职位之前进行批判性思考。 让我们在力所能及的地方帮助他们,而不是仅仅说“看,大众投票说不”——在这种情况下,许多投票的人甚至可能没有太多的 Go 经验或完全理解 Go。 我读过连载海报,他们承认他们不知道这种公认的小而简单的语言的一些基本概念。 这使得我们很难认真对待您的反馈。

无论如何,我在这里做这个很糟糕 - 随时删除此评论。 我不会被冒犯的。 但是不得不说这话的人!

整个第二个提案看起来非常类似于数字影响者为我组织集会。 人气竞赛不评估技术价值。

人们可能保持沉默,但他们仍然期待 Go 2。我个人期待 Go 2 和 Go 2 的其余部分。Go 1 是一门很棒的语言,非常适合不同类型的程序。 我希望 Go 2 能扩展它。

最后,我还将改变我对使用try作为声明的偏好。 现在我支持该提案。 在“Go 1”兼容承诺这么多年之后,人们认为 Go 已经一成不变。 由于这个有问题的假设,在我看来,在这种情况下不改变语言语法似乎是一个更好的折衷方案。 编辑:我也期待看到用于事实核查的经验报告。

PS:不知道提出仿制药时会发生什么样的反对。

我们公司一次性编写了大约十几种工具。 我对我们的代码库运行了 tryhard 工具,发现了 933 个潜在的 try() 候选对象。 就个人而言,我相信 try() 函数是一个绝妙的主意,因为它解决的不仅仅是代码样板问题。

它强制调用者和被调用函数/方法将错误作为最后一个参数返回。 这是不允许的:

var file= try(parse())

func parse()(err, result) {
}

它强制执行一种处理错误的方法,而不是声明错误变量并松散地允许 err!=nil err==nil 模式,这阻碍了可读性,增加了 IMO 中易出错代码的风险:

func Foo() (err error) {
    var file, ferr = os.Open("file1.txt")
    if ferr == nil {
               defer file.Close()
        var parsed, perr = parseFile(file)
        if perr != nil {
            return
        }
        fmt.Printf("%s", parsed)
    }
    return nil
}

在我看来,使用 try(),代码更具可读性、一致性和安全性:

func Foo() (err error) {
    var file = try(os.Open("file.txt"))
        defer file.Close()
    var parsed = try(parseFile(file))
    fmt.Printf(parsed)
    return
}

我进行了一些类似于@lpar在 Heroku 的所有未归档 Go 存储库(公共和私有)上所做的实验。

结果在这个要点中: https ://gist.github.com/freeformz/55abbe5da61a28ab94dbb662bfc7f763

抄送@davecheney

@ubikenobi您更安全的功能 ~is~ 正在泄漏。

此外,我从未见过错误后返回的值。 不过,我可以想象,当一个函数完全与错误有关,而返回的其他值不取决于错误本身(可能导致两个错误返回,第二个“保护”先前的值)。

最后,虽然不常见,但err == nil为一些早期回报提供了合法的测试。

@戴维德

感谢您指出泄漏,我忘了在两个示例中添加 defer.Close() 。 (现在更新)。

我也很少看到 err 以这种顺序返回,但如果它们是错误而不是设计使然,那么能够在编译时捕获它们仍然是件好事。

在大多数情况下,我认为 err==nil 情况是一个例外而不是常态。 正如您所提到的,它在某些情况下可能很有用,但我不喜欢开发人员在没有正当理由的情况下做出不一致的选择。 幸运的是,在我们的代码库中,绝大多数语句都是 err!=nil,这很容易从 try() 函数中受益。

  • 我针对一个大型 Go API 运行tryhard ,我与其他四名工程师组成的团队全职维护该 API。 在 45580 行 Go 代码中, tryhard确定了要重写的 301 个错误(因此,这将是 +301/-903 更改),或者假设每个错误大约需要 3 行代码,将重写大约 2% 的代码。 考虑到对我来说很重要的评论、空白、导入等。
  • 我一直在使用 tryhard 的线工具来探索try将如何改变我的工作,并且主观上它对我来说非常好! 动词try对我来说更清楚地表明调用函数中可能出现问题,并且紧凑地完成了它。 我非常习惯写if err != nil ,我并不介意,但也不介意改变。 重复编写和重构错误之前的空变量(即返回空切片/映射/变量)可能比err本身更乏味。
  • 跟踪所有讨论线程有点困难,但我很好奇这对包装错误意味着什么。 如果您想选择性地添加诸如try(json.Unmarshal(b, &accountBalance), "failed to decode bank account info for user %s", user)之类的上下文,如果try是可变参数,那就太好了。 编辑:这点可能离题了; 不过,从非尝试重写来看,这就是发生这种情况的地方。
  • 我真的很感激为此付出的思想和关怀! 向后兼容性和稳定性对我们来说非常重要,迄今为止,Go 2 的努力对于维护项目来说非常顺利。 谢谢!

这不应该在有经验的 Gophers 审查过的源上完成,以确保替换是合理的吗? 应该用显式处理重写多少“2%”的重写? 如果我们不知道这一点,那么 LOC 仍然是一个相对无用的指标。

*这正是我今天早上早些时候的帖子关注错误处理“模式”的原因。 讨论try的错误处理模式更容易,更实质性,然后与我们可能编写的代码的潜在危险作斗争,而不是运行一个相当随意的行计数器。

@kingishb在非主包的公共函数中有多少找到的 _try_ 点? 通常公共函数应该返回包原生(即包装或装饰)错误....

@networkimprov对于我的感受来说,这是一个过于简单化的公式。 在返回可检查错误的 API 表面方面,这是正确的。 通常根据上下文的相关性而不是它在调用堆栈中的位置将上下文添加到错误消息中是合适的。

许多误报可能会在当前指标中通过。 由于遵循建议的做法(https://blog.golang.org/errors-are-values)而发生的失误呢? try可能会减少此类做法的使用,从这个意义上说,它们是替换的主要目标(可能是唯一真正让我感兴趣的用例之一)。 因此,再一次,在没有更多尽职调查的情况下抓取现有资源似乎毫无意义。

感谢@ubikenobi@freeformz@kingishb收集您的数据,非常感谢! 顺便说一句,如果您使用选项-err=""运行tryhard $ if 还将尝试使用错误变量被称为err以外的其他内容的代码(例如e )。 这可能会产生更多的情况,具体取决于代码库(但也可能增加误报的机会)。

@griesemer ,以防您正在寻找更多数据点。 我已经针对我们的两个微服务运行了tryhard ,结果如下:

cloc v 1.82 / tryhard
13280 个 Go 代码行 / 148 个被识别为 try (1%)

另一项服务:
9768 个 Go 代码行 / 50 个被确定为 try (0.5%)

随后tryhard检查了更广泛的各种微服务:

314343 Go 代码行 / 1563 识别为 try (0.5%)

进行快速检查。 try可以优化的包类型通常是适配器/服务包装器,它们透明地返回从包装服务返回的(GRPC)错误。

希望这可以帮助。

这绝对是个坏主意。

  • err var 什么时候出现defer ? “显式优于隐式”呢?
  • 我们使用一个简单的规则:您应该快速找到返回错误的确切位置。 每个错误都包含上下文,以了解哪里出了问题。 defer会创建很多丑陋且难以理解的代码。
  • @davecheney了一篇关于错误的精彩帖子,该提案完全反对这篇文章中的所有内容。
  • 最后,如果你使用os.Exit ,你的错误将被取消检查。

我刚刚在一个包(与供应商)上运行tryhard并且它报告2478代码计数从873934下降到851178但我不确定如何解释它,因为我不知道其中有多少是由于过度包装(stdlib 缺乏对堆栈跟踪错误包装的支持)或者有多少代码甚至与错误处理有关。

然而,我所知道的是,仅在这一周,我就因为像if err != nil { return nil }这样的复制意大利面和看起来像error: cannot process ....file: cannot parse ...file: cannot open ...file的错误而浪费了令人尴尬的时间。

\ 除非您认为那里只有约 3000 名 Go 开发人员,否则我不会对投票数量过于重视。 另一个非提案的高票数仅仅是因为这个问题登上了 HN 和 Reddit 的榜首——围棋社区并不完全以缺乏教条和/或反对而闻名,所以不-人们应该对投票数感到惊讶。

我也不会太认真地对待上诉当局的尝试,因为众所周知,即使在他们自己的无知和/或误解被指出之后,这些当局也会拒绝新的想法和提议。
\

我们在最大的(包括测试在内的±163k 行代码)服务上运行了tryhard -err="" - 它发现了 566 次出现。 我怀疑它在实践中会更多,因为一些代码是在考虑if err != nil的情况下编写的,所以它是围绕它设计的(我想到了 Rob Pike 关于避免重复的“错误是价值”文章)。

@griesemer我在要点中添加了一个新文件。 它是使用 -err="" 生成的。 我抽查了一下,有一些变化。 今天早上我也更新了tryhard,所以也使用了较新的版本。

@griesemer我认为tryhard如果可以计数会更有用:

a) 产生错误的呼叫站点的数量
b) 单语句if err != nil [&& ...]处理程序的数量( on err #32611 的候选人)
c) 返回任何内容的人数( defer #32676 的候选人)
d) 返回err的人数( try()的候选人)
e) 非主包的导出函数中的数量(可能误报)

将总 LoC 与return err sorta 的实例进行比较缺乏上下文,IMO。

@networkimprov同意 - 之前已经提出过类似的建议。 在接下来的几天里,我会尝试找一些时间来改进这一点。

以下是在我们的内部代码库上运行 tryhard 的统计数据(只有我们的代码,而不是依赖项):

前:

  • 882个.go文件
  • 352434 位置
  • 329909 非空位置

努力之后:

  • 2701 次替换(平均 3.1 次替换/文件)
  • 345364 位置 (-2.0%)
  • 322838 个非空位置 (-2.1%)

编辑:现在@griesemer更新了 tryhard 以包含汇总统计信息,这里还有一些:

  • 39.2% 的if语句是if <err> != nil
  • 其中 69.6% 是try候选人

查看 tryhard 发现的替换,肯定有一些类型的代码使用try非常普遍,而其他类型的代码很少使用。

我也注意到了一些tryhard无法改变的地方,但会从try中受益匪浅。 例如,这里有一些代码,用于根据简单的有线协议解码消息(为简单/清晰而编辑):

func (req *Request) Decode(r Reader) error {
    typ, err := readByte(r)
    if err != nil {
        return err
    }
    req.Type = typ
    req.Body, err = readString(r)
    if err != nil {
        return unexpected(err)
    }

    req.ID, err = readID(r)
    if err != nil {
        return unexpected(err)
    }
    n, err := binary.ReadUvarint(r)
    if err != nil {
        return unexpected(err)
    }
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        if err != nil {
            return unexpected(err)
        }
    }
    return nil
}

// unexpected turns any io.EOF into an io.ErrUnexpectedEOF.
func unexpected(err error) error {
    if err == io.EOF {
        return io.ErrUnexpectedEOF
    }
    return err
}

没有try ,我们只是在需要它的返回点写了unexpected ,因为在一个地方处理它并没有很大的改进。 但是,使用try ,我们可以通过延迟应用unexpected错误转换,然后显着缩短代码,使其更清晰、更容易浏览:

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@cespare 很棒的报告!

完全缩减的片段通常更好,但括号比我预期的还要糟糕,循环中的try和我预期的一样糟糕。

一个关键字的可读性要高得多,这有点超现实,这是许多其他人的不同之处。 以下内容是可读的,并且不会因为只返回一个值而让我担心细微之处(尽管它仍然可能出现在更长的函数和/或具有大量嵌套的函数中):

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = wrapEOF(err) }()

    req.Type = try readByte(r)
    req.Body = try readString(r)
    req.ID = try readID(r)

    n := try binary.ReadUvarint(r)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        try err
    }
    return nil
}

*公平地说,代码突出显示会有很大帮助,但这看起来就像便宜的口红。

您是否了解在代码非常糟糕的情况下获得的最大优势?

如果您使用unexpected()或按原样返回错误,那么您对代码和应用程​​序一无所知。

try不能帮助你编写更好的代码,但会产生更多糟糕的代码。

@cespare解码器也可以是其中包含错误类型的结构,方法在每次操作之前检查err == nil并返回布尔值 ok。

因为这是我们用于编解码器的过程,所以try绝对没用,因为人们可以轻松地制作一个非魔法、更短、更简洁的习语来处理这种特定情况下的错误。

@makhov通过“非常糟糕的代码”,我假设您的意思是不包含错误的代码。

如果是这样,那么您可以采用如下所示的代码:

a, b, c, err := someFn()
if err != nil {
  return ..., errors.Wrap(err, ...)
}

并将其转换为语义相同的 [1] 代码,如下所示:

a, b, c, err := someFn()
try(errors.Wrap(err, ...))

该提案并不是说您必须使用 defer 进行错误包装,只是解释了为什么不需要该提案的上一个迭代的handle关键字,因为它可以在没有任何语言更改的情况下以 defer 的方式实现。

(您的其他评论似乎也基于提案中的示例或伪代码,而不是提案的核心)

我用 54K LOC 在我的代码库上运行tryhard ,找到了 1116 个实例。
我看到了差异,我不得不说我只有很少的构造可以从尝试中受益匪浅,因为几乎我对if err != nil类型的构造的整个使用都是一个简单的单级块,它只返回添加上下文的错误。 我想我只发现了几个例子,其中try实际上会改变代码的结构。

换句话说,我认为当前形式的try给了我:

  • 更少的打字(每次出现减少约 30 个字符,由下面的“**”表示)
-       **if err := **json.NewEncoder(&buf).Encode(in)**; err != nil {**
-               **return err**
-       **}**
+       try(json.NewEncoder(&buf).Encode(in))

虽然它为我介绍了这些问题:

  • 处理错误的另一种方法
  • 缺少执行路径拆分的视觉提示

正如我之前在这个线程中所写的,我可以接受try ,但在我的代码上尝试过之后,我认为我个人宁愿不将它引入语言。 我的 $.02

无用的功能,它可以节省打字,但没什么大不了的。
我宁愿选择旧的方式。
编写更多的错误处理程序使程序易于故障排除。

只是一些想法...

这个成语在 go 中很有用,但它只是:一个你必须的成语
教新人。 一个新的围棋程序员必须学会这一点,否则他们
甚至可能想重构“隐藏”的错误处理。 此外,该
除非您忘记,否则使用该成语的代码不会更短(恰恰相反)
计算方法。

现在让我们想象一下 try 被实现了,这个习语有多大用处
那个用例? 考虑:

  • 尝试使实现更紧密,而不是分散在方法中。
  • 程序员将比尝试更频繁地阅读和编写代码
    特定的成语(除了每个特定的任务外很少使用)。 一种
    更多使用的习语变得更自然和可读,除非有一个明确的
    缺点,如果我们将两者与
    开放的心态。

所以也许这个成语会被认为被try取代。

Em ter,2019 年 7 月 2 日 18:06,作为[email protected] escreveu:

@cespare https://github.com/cespare解码器也可以是一个结构体
里面有一个错误类型,方法在之前检查 err == nil
每个操作并返回一个布尔值 ok。

因为这是我们用于编解码器的过程,try 是绝对没用的
因为人们可以很容易地做出一个非魔法、更短、更简洁的成语
用于处理此特定情况的错误。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAT5WM3YDDRZXVXOLDQXKH3P5O7L5A5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODZCRHXA#issuecomment-5078
或使线程静音
https://github.com/notifications/unsubscribe-auth/AAT5WMYXLLO74CIM6H4Y2RLP5O7L5ANCNFSM4HTGCZ7Q
.

在我看来,错误处理的冗长是一件好事。 换句话说,我没有看到一个强大的尝试用例。

我对这个想法持开放态度,但我认为它应该包括一些机制来确定执行拆分发生的位置。 Xerror/Is 在某些情况下会很好(例如,如果错误是 ErrNotExists,您可以推断它发生在 Open 上),但对于其他情况 - 包括库中的遗留错误 - 没有替代品。

是否可以包含一个类似于 recovery 的内置函数来提供有关控制流更改位置的上下文信息? 可能,为了保持便宜,使用一个单独的函数代替 try()。

或者也许是一个 debug.Try,它的语法与 try() 相同,但添加了调试信息? 这种方式 try() 可以与使用旧错误的代码一样有用,而不会强迫您诉诸旧的错误处理。

另一种方法是让 try() 包装和添加上下文,但在大多数情况下,这会无缘无故地降低性能,因此建议使用附加功能。

编辑:在写完这篇文章后,我想到编译器可以根据是否有任何 defer 语句使用类似于“恢复”的上下文提供函数来确定使用哪个变体 try()。 虽然不确定这的复杂性

@lestrrat我不会在此评论中发表我的意见,但如果有机会向您解释“尝试”可能对我们产生怎样的影响,那么可以在 if 语句中写入两个或更多标记。 因此,如果您在 if 语句中编写 200 个条件,您将能够减少很多行。

if try(foo()) == 1 && try(bar()) == 2 {
  // err
}
n1, err := foo()
if err != nil {
  // err
}
n2, err := bar()
if err != nil {
  // err
}
if n1 == 1 && n2 == 2 {
  // err
}

@mattn就是这样,_理论上_你是绝对正确的。 我相信我们可以提出try非常适合的情况。

我刚刚提供的数据表明,在现实生活中,至少_我_发现几乎没有出现这样的结构,这些结构可以从翻译中受益,以便在_我的代码_中尝试。

可能我编写的代码与世界其他地方不同,但我只是认为值得有人加入,基于 PoC 翻译,我们中的一些人实际上并没有从引入try中获得太多收益

顺便说一句,我仍然不会在我的代码中使用你的风格。 我会把它写成

n1 := try(foo())
n2 := try(bar())
if n1 == 1 && n2 == 2 {
   return errors.New(`boo`)
}

所以我仍然会为那些 n1/n2/....n(n)s 的每个实例节省大约相同数量的打字

为什么要有关键字(或函数)?

如果调用上下文需要 n+1 个值,那么一切都和以前一样。

如果调用上下文需要 n 个值,则 try 行为开始执行。

(这在 n=1 的情况下特别有用,这是所有可怕混乱的来源。)

我的 ide 已经突出了被忽略的返回值; 如果需要,为此提供视觉提示将是微不足道的。

@balasanjay是的,包装错误就是这种情况。 但我们也有日志记录、对不同错误的不同反应(我们应该如何处理错误变量,例如sql.NoRows ?)、可读代码等等。 我们在打开文件后立即写defer f.Close()以使读者清楚。 出于同样的原因,我们会立即检查错误。

最重要的是,这个提议违反了“错误就是价值”的规则。 这就是 Go 的设计方式。 而这个提议直接违反了规则。

try(errors.Wrap(err, ...))是另一段糟糕的代码,因为它与这个提议和当前的 Go 设计相矛盾。

我倾向于同意@lestrrat
通常 foo() 和 bar() 实际上是:
SomeFunctionWithGoodName(Parm1, Parms2)

那么建议的@mattn语法实际上是:

if  try(SomeFunctionWithGoodName(Parm1, Parms2)) == 1 && try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) == 2 {


} 

可读性通常会是一团糟。

考虑一个返回值:
someRetVal, err := SomeFunctionWithGoodName(Parm1, Parms2)
比仅与 1 或 2 之类的 const 相比更频繁地使用,并且它不会变得更糟,但需要双重分配功能:

if  a := try(SomeFunctionWithGoodName(Parm1, Parms2)) && b:= try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) {


} 

至于所有用例(“tryhard 对我有多大帮助”):

  1. 我认为您会看到库和可执行文件之间存在很大差异,如果他们也得到这个差异,从其他人那里看到会很有趣
  2. 我的建议是不要比较代码中的 %save 行数,而是比较代码中的错误数与重构数。
    (我对此的看法是
    $find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
    )

@makhov

这个提议违反了“错误就是价值”的规则

并不真地。 错误仍然是此提案中的值。 try()只是通过成为if err != nil { return ...,err }的快捷方式来简化控制流。 error类型作为内置接口类型已经在某种程度上“特殊”了。 这个提议只是添加了一个补充error类型的内置函数。 这里没有什么特别的。

@ngrilly简化? 如何?

func (req *Request) Decode(r Reader) error {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

我应该如何理解错误是在循环内返回的? 为什么将其分配给err var,而不是foo
记住它而不把它保存在代码中更简单吗?

@daved

括号甚至比我预期的还要糟糕 [...] 关键字的可读性要高得多,这有点超现实,这是许多其他人的不同之处。

在关键字和内置函数之间进行选择主要是美学和句法问题。 老实说,我不明白为什么这对你的眼睛如此重要。

PS:内置函数的优点是向后兼容,未来可以用其他参数扩展,避免了运算符优先级的问题。 关键字的优点是......作为关键字,并且信号try是“特殊的”。

@makhov

简化?

好的。 正确的词是“缩短”。

try() $ 通过调用内置的try()函数来替换模式if err != nil { return ..., err }来缩短我们的代码。

这就像您在代码中识别出重复出现的模式,然后将其提取到新函数中一样。

我们已经有像 append() 这样的内置函数,每次我们需要向切片附加一些东西时,我们可以通过自己“扩展”编写代码来替换它们。 但是因为我们一直在这样做,所以它被集成到了语言中。 try()没有什么不同。

我应该如何理解错误是在循环内返回的?

循环中的try()的行为与循环外函数其余部分中的try()完全相同。 如果readID()返回错误,则函数返回错误(在修饰 if 之后)。

为什么将它分配给 err var,而不是 foo?

我在您的代码示例中看不到foo变量...

@makhov我认为该片段不完整,因为没有命名返回的错误(我很快重新阅读了提案,但如果没有设置变量名称err ,则无法查看变量名称是否是默认名称)。

必须重命名返回的参数是拒绝此提议的人不喜欢的一点。

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@pierrec如果不在命名参数中,也许我们可以有一个像recover()这样的函数来检索错误?
defer func() {err = unexpected(tryError())}

@makhov您可以使其更明确:

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        try(err) // or add annotation like try(annotate(err, ...))
    req.Body, err := readString(r)
        try(err)
    req.ID, err := readID(r)
        try(err)

    n, err := binary.ReadUvarint(r)
        try(err)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err := readID(r)
                try(err)
    }
    return nil
}

@pierrec好的,让我们改变它:

func (req *Request) Decode(r Reader) error {
        var errOne, errTwo error
    defer func() { err = unexpected(???) }()

    req.Type = try(readByte(r))
    …
}

@reusee为什么它比这更好?

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        if err != nil { return err }
        …
}

在什么时候,我们都认为简短比可读性更好?

@flibustenet感谢您理解这个问题。 它看起来好多了,但我仍然不确定我们是否需要为这个小的“改进”打破向后兼容性。 如果我的应用程序停止在新版本的 Go 上构建,那将非常烦人:

package main

func main() {
    // ...
   try("a", "b")
    // ...
}

func try(a, b string) {
    // ...
}

@makhov我同意这需要澄清:当编译器无法找出变量时会出错吗? 我以为会的。
也许提案需要澄清这一点? 还是我在文档中遗漏了它?

@flibustenet是的,这是使用 try() 的一种方式,但在我看来,这不是使用 try 的惯用方式。

@cespare从您写的内容看来,在 defer 中修改返回值是try的一个功能,但您已经可以这样做了。

https://play.golang.com/p/ZMauFmt9ezJ

(对不起,如果我误解了你所说的)

@jan-g 关于https://github.com/golang/go/issues/32437#issuecomment -507961463:隐形处理错误的想法已经多次出现。 这种隐式方法的问题是向被调用函数添加错误返回可能会导致调用函数默默地和不可见地表现不同。 当检查错误时,我们绝对希望明确。 隐式方法也违背了 Go 中一切都是显式的一般原则。

@griesemer

我在我的一个项目(https://github.com/komuw/meli)上尝试tryhand并没有做出任何改变。

gobin github.com/griesemer/tryhard
     Installed github.com/griesemer/[email protected] to ~/go/bin/tryhard

```重击
~/go/bin/tryhard -err "" -r
0

most of my err handling looks like;
```Go
import "github.com/pkg/errors"

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

@komuw首先,确保向tryhard提供文件名或目录参数,如

tryhard -err="" -r .  // <<< note the dot
tryhard -err="" -r filename

此外,您在评论中的代码不会被重写,因为它在if块中进行特定的错误处理。 请阅读tryhard的文档以了解何时适用。 谢谢。

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

这是一个有点有趣的例子。 我看到它时的第一反应是问这是否会产生口吃的错误字符串,例如:

unable to create docker volume: VolumeName: could not create volume VolumeName: actual problem

答案是它没有,因为VolumeCreate函数(来自不同的仓库)是:

func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error) {
        var volume types.Volume
        resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
        defer ensureReaderClosed(resp)
        if err != nil {
                return volume, err
        }
        err = json.NewDecoder(resp.body).Decode(&volume)
        return volume, err
}

换句话说,错误的附加修饰很有用,因为底层函数没有修饰它的错误。 可以使用try稍微简化该基础功能。

也许VolumeCreate函数真的应该修饰它的错误。 然而,在这种情况下,我不清楚CreateDockerVolume函数是否应该添加额外的装饰,因为它没有提供新的信息。

@尼尔德
即使VolumeCreate会修饰错误,我们仍然需要CreateDockerVolume来添加它的修饰,因为VolumeCreate可能会从各种其他函数调用,并且如果出现故障(希望记录)您想知道失败的原因 - 在这种情况下是CreateDockerVolume
尽管如此,考虑到VolumeCreate是 APIclient 接口的一部分。

其他库也是如此 - os.Open可以很好地修饰文件名、错误原因等,但是
func ReadConfigFile(...
func WriteDataFile(...
等等 - 调用os.Open是您希望看到的实际失败部分,以便记录、跟踪和处理您的错误 - 特别是,但不仅限于生产环境。

@neild谢谢。

我不想破坏这个线程,但是......

也许 VolumeCreate 函数真的应该修饰它的错误。
但是,在这种情况下,我不清楚
创建DockerVolume 函数
应该添加额外的装饰,

问题是,作为CreateDockerVolume函数的作者,我可能不会
知道VolumeCreate的作者是否修饰了他们的错误,所以我
不需要装饰我的。
即使我知道他们有,他们也可以决定不装饰他们的
在以后的版本中起作用。 而且由于这种变化不是 api 改变它们
将它作为补丁/次要版本发布,现在我的功能是
依赖于他们的功能有修饰错误没有所有
我需要的信息。
所以通常我发现自己在装饰/包装,即使我在图书馆
通话已经结束。

在和同事谈论try时,我有一个想法。 也许try应该只在 1.14 中为标准库启用。 @crawshaw@jimmyfrasche都快速浏览了一些案例并给出了一些观点,但实际上尽可能多地使用try重写标准库代码将是有价值的。

这让 Go 团队有时间使用它重新编写一个重要的项目,并且社区可以获得关于它如何工作的经验报告。 我们会知道它的使用频率、需要与defer配对的频率、它是否会改变代码的可读性、 tryhard的用处等。

这有点违背标准库的精神,允许它使用常规 Go 代码不能使用的东西,但它确实为我们提供了一个游乐场,让我们了解try如何影响现有代码库。

抱歉,如果其他人已经想到了这一点; 我进行了各种讨论,但没有看到类似的提议。

@jonbodner https://go-review.googlesource.com/c/go/+/182717让您对它的外观有一个很好的了解。

我忘了说:我参加了你的调查,我投票支持更好的错误处理,而不是这个。

我的意思是我希望看到更严格的不可能忘记错误处理。

@jonbodner https://go-review.googlesource.com/c/go/+/182717让您对它的外观有一个很好的了解。

总结:

  1. 1 行普遍替换 4 行(使用if ... { return err }的人为 2 行)
  2. 可以优化返回结果的评估 - 但仅在失败路径上。

总共大约 6,000 次替换,看起来只是表面上的改变:不会暴露现有的错误,也许不会引入新的错误(如果我错了,请纠正我。)

我会以维护者的身份,费心用自己的代码做这样的事情吗? 除非我自己编写替换工具。 这使得golang/go存储库没问题。

PS CL 中一个有趣的免责声明:

... Some transformations may be incorrect due to the limitations of the tool (see https://github.com/griesemer/tryhard)...

xerrors一样,第一步将其用作第三方包怎么样?

例如,尝试使用下面的包。

https://github.com/junpayment/gotry

  • 对于您的用例来说,它可能很短,因为我做了它。

但是,我认为尝试本身是一个好主意,所以我认为还有一种方法可以实际使用它,但影响较小。

===

顺便说一句,我担心尝试两件事。

1.有意见认为该行可以省略,但似乎没有考虑defer(或handler)子句。

例如,当详细处理错误时。

foo, err: = Foo ()
if err! = nil {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}

如果你简单地用try替换它,它将如下所示。

handler: = func (err error) {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}
foo: = try (Foo (), handler)

2.可能有其他坏包不小心实现了错误接口。

type Bad struct {}
func (bad * Bad) Error () {
  return "i really do not intend to be an error"
}

@junpayment感谢您的gotry软件包-我想这是一种对try有一点感觉的方法,但是必须键入断言所有Try会有点烦人interface{}的结果。

关于你的两个问题:
1)我不确定你要去哪里。 您是否建议try应该像您的示例中那样接受处理程序? (就像我们在try的早期内部版本中所做的那样?)
2)我不太担心函数意外实现错误接口。 据我们所知,这个问题并不新鲜,似乎并没有造成严重的问题。

@jonbodner https://go-review.googlesource.com/c/go/+/182717让您对它的外观有一个很好的了解。

谢谢你做这个练习。 然而,这证实了我的怀疑,go 源代码本身有很多地方try()会很有用,因为错误只是传递。 但是,正如我从上面其他人和我自己提交的tryhard的实验中看到的那样,对于许多其他代码库, try()并不是很有用,因为在应用程序中,代码错误往往会被实际处理,而不是刚刚过去。

我认为 Go 设计者应该牢记这一点,go 编译器和运行时是有点“独特”的 Go 代码,不同于 Go 应用程序代码。 因此,我认为应该增强try()以便在实际必须处理错误以及使用 defer 语句进行错误处理并不是非常理想的其他情况下也有用。

@griesemer

在实际使用中必须从 interface{} 中对所有 Try 结果进行类型断言会有点烦人。

你说得对。 此方法要求调用者转换类型。

我不确定你要去哪里。 您是否建议尝试像您的示例中那样接受处理程序? (正如我们在早期内部版本的 try 中所做的那样?)

我犯了一个错误。 应该使用 defer 而不是处理程序来解释。 对不起。

我想说的是,有一种情况,由于错误处理过程的结果,它对代码量没有贡献,无论如何都需要在 defer 中描述。

当您想要详细处理错误时,预计影响会更加明显。

所以,与其减少代码行数,不如理解proposal,它组织了错误处理的位置。

我不太担心函数会意外实现错误接口。 据我们所知,这个问题并不新鲜,似乎并没有造成严重的问题。

确切地说,这是罕见的情况。

@beoran我对 Go 语料库(https://github.com/rsc/corpus)做了一些初步分析。 我相信当前状态下的tryhard可以消除语料库中所有err != nil检查的 41.7%。 如果我排除模式“_test.go”,这个数字会上升到 51.1%( tryhard只对返回错误的函数起作用,而且它往往在测试中找不到很多错误)。 警告,对这些数字持保留态度,我通过使用破解版的tryhard得到了分母(即我们执行err != nil检查的代码中的位置数),理想情况下我们会等到tryhard自己报告这些统计数据。

此外,如果tryhard能够识别类型,理论上它可以执行如下转换:

// Before.
a, err := foo()
if err != nil {
  return 0, nil, errors.Wrapf(err, "some message %v", b)
}

// After.
a, err := foo()
try(errors.Wrapf(err, "some message %v", b))

这利用了 errors.Wrap 在传入的错误参数为nil nil的行为。 (github.com/pkg/errors 在这方面也不是唯一的,我用于进行错误包装的内部库也保留nil错误,并且也可以使用这种模式,就像大多数错误处理库一样后try ,我想)。 新一代的支持库可能也会对这些传播助手的命名略有不同。

鉴于这将适用于 50% 的非测试err != nil开箱即用检查,在任何支持该模式的库演变之前,Go 编译器和运行时似乎并不像您建议的那样是独一无二的.

关于CreateDockerVolume的示例https://github.com/golang/go/issues/32437#issuecomment -508199875
我发现了完全相同的用法。 在 lib 中,我在每个错误处用上下文包装错误,在使用 lib 时,我想使用try并在defer中为整个函数添加上下文。

我试图通过在开始时添加一个错误处理函数来模仿它,它工作正常:

func MyLib() error {
    return errors.New("Error from my lib")
}
func MyOtherLib() error {
    return errors.New("Error from my otherLib")
}

func Caller(a, b int) error {
    eh := func(err error) error {
        return fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, err)
    }

    err := MyLib()
    if err != nil {
        return eh(err)
    }

    err = MyOtherLib()
    if err != nil {
        return eh(err)
    }

    return nil
}

使用try+defer看起来会很好而且很惯用

func Caller(a, b int) (err error) {
    defer fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, &err)

    try(MyLib())
    try(MyOtherLib())

    return nil
}

@griesemer

设计文档目前有以下声明:

如果封闭函数声明了其他命名的结果参数,那么这些结果参数将保留它们所具有的任何值。 如果函数声明了其他未命名的结果参数,则它们假定它们对应的零值(这与保持它们已有的值相同)。

这意味着该程序将打印 1,而不是 0:https: //play.golang.org/p/KenN56iNVg7。

正如在 Twitter 上向我指出的那样,这使得try表现得像一个裸返回,其中返回的值是隐式的; 要弄清楚返回的实际值是什么,可能需要查看距调用try本身很远的代码。

鉴于通常不喜欢这种裸返回(非局部性)的属性,您对让try始终返回非错误参数的零值(如果它完全返回)有何想法?

一些考虑:

这可能会使一些涉及使用命名返回值的模式无法使用try 。 例如,对于io.Writer的实现,即使在部分写入的情况下,它也需要返回写入的字节数。 也就是说,在这种情况下,似乎try无论如何都容易出错(例如,如果返回错误, n += try(wrappedWriter.Write(...))不会将n设置为正确的数字)。 在我看来, try对于这些用例将变得不可用,因为根据我的经验,我们需要值和错误的情况相当罕见。

如果有一个函数多次使用try ,这可能会导致代码膨胀,函数中有很多地方需要将输出变量归零。 首先,编译器现在非常擅长优化不必要的写入。 其次,如果证明有必要,将所有try生成的块goto设置为公共共享函数范围标签,这似乎是一种简单的优化,它将非错误输出值归零。

另外,我相信您知道, tryhard已经以这种方式实现,因此作为附带好处,这将追溯使tryhard更正确。

@jonbodner https://go-review.googlesource.com/c/go/+/182717让您对它的外观有一个很好的了解。

谢谢你做这个练习。 然而,这证实了我的怀疑,go 源代码本身有很多地方try()会很有用,因为错误只是传递。 但是,正如我从上面其他人和我自己提交的tryhard的实验中看到的那样,对于许多其他代码库, try()并不是很有用,因为在应用程序中,代码错误往往会被实际处理,而不是刚刚过去。

我会以不同的方式解释这一点。

我们没有泛型,因此很难在野外找到可以直接从基于编写的代码的泛型中受益的代码。 这并不意味着泛型不会有用。

对我来说,我在代码中使用了 2 种模式来进行错误处理

  1. 在包中使用恐慌,并在少数导出的方法中恢复恐慌并返回错误结果
  2. 在某些方法中选择性地使用延迟处理程序,以便我可以使用丰富的堆栈文件/行号 PC 信息和更多上下文来装饰错误

这些模式并不普遍,但它们有效。 1) 在标准库中未导出的函数中使用,2) 在过去几年中在我的代码库中广泛使用,因为我认为这是使用正交特征进行简化错误修饰的好方法,并且提案建议和祝福了这种方法。 它们不普及的事实并不意味着它们不好。 但与所有事情一样,Go 团队推荐它的指导方针将导致它们在未来被更多地用于实践。

最后一点要注意的是,在代码的每一行中修饰错误可能有点过多。 有些地方修饰错误是有意义的,而有些地方则没有。 因为我们之前没有很好的指导方针,所以人们认为总是修饰错误是有意义的。 但是,每次文件未打开时总是进行装饰可能不会增加太多价值,因为在包中只出现“无法打开文件:conf.json”的错误可能就足够了,而不是:“无法打开获取用户名:无法获取数据库连接:无法加载系统文件:无法打开文件:conf.json”。

通过结合错误值和简洁的错误处理,我们现在获得了关于如何处理错误的更好指南。 偏好似乎是:

  • 错误很简单,例如“无法打开文件:conf.json”
  • 可以附加一个包含上下文的错误框架:GetUserName --> GetConnection --> LoadSystemFile。
  • 如果它添加到上下文中,您可以稍微包装该错误,例如 MyAppError{error}

我倾向于觉得我们一直在忽略 try 提案的目标,以及它试图解决的高级问题:

  1. 减少 if err != nil { return err } 的样板文件,以便将错误向上传播以便在堆栈更高的位置处理它是有意义的
  2. 允许简化使用返回值,其中 err == nil
  3. 允许稍后扩展解决方案,例如,在站点上进行更多错误修饰、跳转到错误处理程序、使用 goto 而不是返回语义等。
  4. 允许错误处理不会弄乱代码库的逻辑,即把它放在一边有点错误处理程序的种类。

许多人仍然有1)。 许多人围绕 1) 工作,因为以前不存在更好的指导方针。 但这并不意味着,在他们开始使用它之后,他们的负面反应不会变得更加积极。

许多人可以使用2)。 关于多少可能存在分歧,但我举了一个例子,它使我的代码更容易。

var u user = try(db.LoadUser(try(strconv.ParseInt(stringId)))

在以异常为常态的 java 中,我们将有:

User u = db.LoadUser(Integer.parseInt(stringId)))

没有人会看这段代码并说我们必须用 2 行来完成,即。

int id = Integer.parseInt(stringId)
User u = db.LoadUser(id))

我们不应该在这里这样做,在try 不能被称为 inline 并且必须总是在它自己的 line的指导下。

此外,今天,大多数代码将执行以下操作:

var u user
var err error
var id int
id, err = strconv.ParseInt(stringId)
if err != nil {
  return u, errors.Wrap("cannot load userid from string: %s: %v", stringId, err)
}
u, err = db.LoadUser(id)
if err != nil {
  return u, errors.Wrap("cannot load user given user id: %d: %v", id, err)
}
// now work with u

现在,阅读本文的人必须解析这 10 行,在 java 中应该是 1 行,而这里的提案可能是 1 行。 当我阅读这段代码时,我必须在视觉上尝试看看这里的哪些行是真正相关的。 样板文件使这段代码更难阅读和理解。

我记得在我过去的生活中从事/使用 Java 中的面向方面编程。 在那里,目标是

这允许将不属于业务逻辑核心的行为(例如日志记录)添加到程序中,而不会弄乱代码,这是功能的核心。 (引自维基百科 https://en.wikipedia.org/wiki/Aspect-oriented_programming )。
错误处理不是业务逻辑的核心,而是正确性的核心。 这个想法是一样的——我们不应该把我们的代码与业务逻辑不核心的东西混在一起,因为“但错误处理非常重要”。 是的,是的,我们可以把它放在一边。

关于 4),许多提案都提出了错误处理程序,即处理错误但不会弄乱业务逻辑的代码。 最初的提案有句柄关键字,人们提出了其他建议。 这个提议说我们可以利用延迟机制,让它更快,这是它之前的致命弱点。 我知道 - 我已经多次向 go 团队发出关于延迟机制性能的噪音。

请注意, tryhard不会将此代码标记为可以简化的代码。 但是有了try和新的指导方针,人们可能希望将此代码简化为 1-liner 并让错误帧捕获所需的上下文。

上下文在基于异常的语言中得到了很好的应用,它将捕获由于用户 ID 不存在或 stringId 不是整数 ID 可能的格式而导致加载用户时发生的错误从中解析出来的。

将它与错误格式化程序结合起来,我们现在可以丰富地检查错误框架和错误本身,并为用户很好地格式化消息,而不用像许多人已经做过而我们没有做过的难以阅读的a: b: c: d: e: underlying error样式有很好的指导方针。

请记住,所有这些建议一起为我们提供了我们想要的解决方案:简洁的错误处理,没有不必要的样板,同时为用户提供更好的诊断和更好的错误格式。 这些是正交的概念,但一起变得非常强大。

最后,鉴于上面的 3),很难使用关键字来解决这个问题。 根据定义,关键字不允许扩展以在将来通过名称传递处理程序,或允许现场错误修饰,或支持 goto 语义(而不是 return 语义)。 使用关键字,我们必须首先考虑完整的解决方案。 并且关键字不向后兼容。 Go 团队在 Go 2 开始时表示,他们希望尽可能地保持向后兼容性。 try函数维护了这一点,如果我们稍后看到不需要扩展,一个简单的 gofix 可以轻松修改代码以将try函数更改为关键字。

又是我的 2 美分!

2019 年 7 月 4 日,Sanjay Menakuru [email protected]写道:

@griesemer

[ ... ]
正如在 Twitter 上向我指出的那样,这使得try表现得像一个裸体
return,其中返回的值是隐式的; 弄清楚什么
正在返回实际值,可能需要查看代码
从调用到try本身的距离很大。

鉴于裸回报(非局部性)的这种性质通常是
不喜欢,你对让try总是返回零有什么想法
非错误参数的值(如果它完全返回)?

仅当命名返回参数时才允许裸返回。 它
似乎尝试遵循不同的规则?

我喜欢重用defer来解决问题的总体思路。 但是,我想知道try关键字是否是正确的方法。 如果我们可以重用已经存在的模式会怎样。 每个人都已经从进口中知道的东西:

显式处理

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

显式忽略

res, _ := doSomething()

延迟处理

类似于try的行为。

res, . := doSomething()

@piotrkowalczuk
这可能是更好的语法,但我不知道在 Go 和语法荧光笔中调整 Go 使其合法化有多容易。

@balasanjay (和@lootch):根据您的评论是的,程序https://play.golang.org/p/KenN56iNVg7将打印 1。

由于try只与错误结果相关,因此它不理会其他所有内容。 它可以将其他返回值设置为零值,但不清楚为什么会更好。 一方面,当结果值被命名时,它可能会导致更多的工作,因为它们可能必须设置为零; 但是,如果出现错误,调用者(可能)会忽略它们。 但这是一个设计决策,如果有充分的理由可以改变。

[编辑:请注意,这个问题(是否在遇到错误时清除非错误结果)并非特定于try提案。 任何不需要明确return的建议替代方案都必须回答相同的问题。]

关于您的作家示例n += try(wrappedWriter.Write(...)) :是的,在即使出现错误也需要增加n的情况下,不能使用try - 即使try不会将非错误结果值归零。 这是因为try只有在没有错误的情况下才会返回任何内容: try的行为就像一个函数(但一个函数可能不会返回给调用者,而是返回给调用者的调用者)。 请参阅try的实现中临时变量的使用。

但是在像您的示例这样的情况下,还必须小心使用if语句,并确保将返回的字节数合并到n中。

但也许我误解了你的担忧。

@griesemer :我建议最好将其他返回值设置为零值,因为这样就很清楚try仅检查调用站点会做什么。 它要么 a) 什么都不做,要么 b) 从函数返回零值和要尝试的参数。

如指定的那样, try将保留非错误命名返回值的值,因此需要检查整个函数以清楚try返回的值是什么。

这是与裸返回相同的问题(必须扫描整个函数以查看返回的值),并且可能是提交https://github.com/golang/go/issues/21291 的原因。 对我来说,这意味着在具有命名返回值的大型函数中,必须在与裸返回相同的基础上不鼓励try (https://github.com/golang/go/wiki/CodeReviewComments #命名结果参数)。 相反,我建议将try指定为始终返回非错误参数的零值。

最近对围棋队感到困惑和难过。 try是针对它试图解决的特定问题的简洁且易于理解的解决方案:错误处理中的冗长。

提案内容如下:经过一年的讨论,我们正在添加这个内置的。 如果您想要更少冗长的代码,请使用它,否则请继续执行您的操作。 对于团队成员已显示出明显优势的选择加入功能,反应是一些不完全合理的抵制!

如果这很容易做到,我会进一步鼓励 go 团队将try内置为可变参数

try(outf.Seek(linkstart, 0))
try(io.Copy(outf, exef))

变成

try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

下一个冗长的事情可能是对try的连续调用。

我在很大程度上同意nvictor ,除了try的可变参数。 我仍然相信它应该有一个处理程序的位置,并且可变参数提案可以为我自己推动可读性边界。

@nvictor Go 是一种不喜欢非正交特征的语言。 这意味着,如果我们在未来找到一个不是try的更好的错误处理解决方案,那么切换会更加复杂(如果它没有因为我们当前的解决方案是“足够好”)。

我认为有一个比try更好的解决方案,我宁愿慢慢来找到解决方案,而不是满足于这个解决方案。

但是,如果添加了这个,我不会生气。 这不是一个糟糕的解决方案,我只是认为我们可以找到一个更好的解决方案。

在我看来,我想尝试一个块代码,现在try就像一个句柄 err func

在阅读这个讨论(以及 Reddit 上的讨论)时,我并不总是觉得每个人都在同一个页面上。

因此,我写了一篇小博文来演示如何使用tryhttps://faiface.github.io/post/how-to-use-try/。

我试图展示该提案的多个方面,以便每个人都可以看到它可以做什么并形成更明智的(即使是负面的)意见。

如果我错过了重要的事情,请告诉我!

@faiface我很确定你可以替换

if err != nil {
    return resps, err
}

try(err)

除此之外 - 很棒的文章!

@DmitriyMV真的! 但我想我会保持原样,所以至少有一个经典的例子if err != nil ,虽然不是一个很好的例子。

我有两个担忧:

  • 命名返回非常令人困惑,这鼓励他们使用新的重要用例
  • 这将阻止向错误添加上下文

根据我的经验,在每个调用站点之后立即为错误添加上下文对于拥有易于调试的代码至关重要。 在某些时候,我认识的几乎所有 Go 开发人员都对命名返回造成了困惑。

一个更次要的文体问题是,不幸的是,现在有多少行代码将被包装在try(actualThing())中。 我可以想象看到包含在try()中的代码库中的大多数行。 这感觉很不幸。

我认为这些问题可以通过调整来解决:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check()的行为很像try() ,但会放弃一般传递函数返回值的行为,而是提供添加上下文的能力。 它仍然会触发返回。

这将保留try()的许多优点:

  • 这是一个内置的
  • 它遵循现有的控制流 WRT 来推迟
  • 它与为错误添加上下文的现有做法很好地保持一致
  • 它与当前的错误包装建议和库保持一致,例如errors.Wrap(err, "context message")
  • 它会产生一个干净的调用站点: a, b, err := myFunc()行上没有样板
  • 仍然可以使用defer fmt.HandleError(&err, "msg")描述错误,但不需要鼓励。
  • check的签名稍微简单一些,因为它不需要从它包装的函数返回任意数量的参数。

这个不错,我觉得go队真的应该拿这个。 这比尝试更好,更清楚!

@buchanae我会对您对我的博客文章的看法感兴趣,因为您认为try会阻止为错误添加上下文,而我认为至少在我的文章中它比平时更容易。

我只是要把这个扔在现阶段。 我会再考虑一下,但我想我在这里发帖看看你的想法。 也许我应该为此打开一个新问题? 我也在 #32811 上发布了这个

那么,不如做一些通用的 C 宏类的东西来打开更大的灵活性呢?

像这样:

define returnIf(err error, desc string, args ...interface{}) {
    if (err != nil) {
        return fmt.Errorf("%s: %s: %+v", desc, err, args)
    }
}

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    :returnIf(err, "Error opening src", src)
    defer r.Close()

    w, err := os.Create(dst)
    :returnIf(err, "Error Creating dst", dst)
    defer w.Close()

    ...
}

本质上 returnIf 将被上面定义的替换/内联。 那里的灵活性在于它的作用取决于您。 调试这可能有点奇怪,除非编辑器以某种好的方式在编辑器中替换它。 这也使它不那么神奇,因为您可以清楚地阅读定义。 而且,这使您可以拥有一条可能会返回错误的行。 并且能够根据发生的位置(上下文)产生不同的错误消息。

编辑:还在宏前面添加了冒号,以表明也许可以这样做以澄清它是宏而不是函数调用。

@nvictor

我会进一步鼓励 Go 团队将try内置为可变参数

如果foobar不返回相同的东西, try(foo(), bar())会返回什么?

我只是要把这个扔在现阶段。 我会再考虑一下,但我想我在这里发帖看看你的想法。 也许我应该为此打开一个新问题? 我也在 #32811 上发布了这个

那么,不如做一些通用的 C 宏类的东西来打开更大的灵活性呢?

@Chillance ,恕我直言,我认为像 Rust(和许多其他语言)这样的卫生宏系统会让人们有机会尝试像try或泛型这样的想法,然后在获得经验后,最好的想法可以变成语言和库的一部分。 但我也认为这样的东西被添加到 Go 中的可能性很小。

@jonbodner目前有人提议在 Go 中添加卫生宏。 还没有建议的语法或任何东西,但是没有太多_反对_添加卫生宏的想法。 #32620

@Allenyn ,关于您刚刚引用@buchanae先前的建议:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

从我所看到的讨论来看,我的猜测是fmt的语义被拉入内置函数是不太可能的结果。 (例如参见@josharian回复)。

也就是说,它并不是真正需要的,包括因为允许处理函数可以回避将fmt语义直接拉入内置函数。 @eihigh在这里讨论的第一天左右提出了一种这样的方法,这与@buchanae的建议的精神相似,它建议调整try内置函数以改为具有以下签名:

func try(error, optional func(error) error)

因为这个替代try不返回任何东西,所以该签名意味着:

  • 它不能嵌套在另一个函数调用中
  • 它必须在行首

我不想触发这个名称的自行车脱落,但是这种形式的try可能会更好地阅读其他名称,例如check 。 可以想象标准库助手可以使可选的就地注释方便,而defer可以在需要时保留统一注释的选项。

稍后在 #32811 ( catch作为内置函数)和 #32611 ( on关键字允许on err, <statement> )中创建了一些相关的提案。 这些可能是进一步讨论的好地方,或者添加赞许或反对,或者建议对这些提案进行可能的调整。

@jonbodner目前有人提议在 Go 中添加卫生宏。 还没有建议的语法或任何东西,但是没有太多_反对_添加卫生宏的想法。 #32620

有提案很好,但我怀疑核心 Go 团队不打算添加宏。 但是,我很高兴对此有误,因为它将结束所有关于当前需要修改语言核心的更改的争论。 引用一个著名的木偶,“做。或不做。没有尝试。”

@jonbodner我不认为添加卫生宏会结束争论。 恰恰相反。 一个常见的批评是try “隐藏”了回报。 从这个角度来看,宏会更糟,因为在宏中任何事情都是可能的。 即使 Go 允许用户定义卫生宏,我们仍然需要争论try是否应该是在 Universe 块中预先声明的内置宏。 对于那些反对try的人来说,更反对卫生宏是合乎逻辑的;-)

@ngrilly有几种方法可以确保宏突出并且易于查看。 Rust 的做法是宏总是以! (即try!(...)println!(...) )。

我认为如果采用卫生宏并且易于查看,并且看起来不像正常的函数调用,它们会更适合。 我们应该选择更通用的解决方案,而不是解决个别问题。

@thepudds我同意添加func(error) error类型的可选参数可能很有用(提案中讨论了这种可能性,有些问题需要解决),但我不明白try的意义try是一个更通用的工具。

@deanveloper是的,Rust 中宏末尾的!很聪明。 它提醒在 Go 中以大写字母开头的导出标识符:-)

当且仅当我们可以保持编译速度并解决有关工具的复杂问题时,我同意在 Go 中使用卫生宏(重构工具需要扩展宏以理解代码的语义,但必须生成未扩展宏的代码) . 这个很难(硬。 与此同时,也许try可以重命名为try! ? ;-)

一个轻量级的想法:如果 if/for 构造的主体包含单个语句,则不需要大括号,前提是该语句与iffor位于同一行。 例子:

fd, err := os.Open("foo")
if err != nil return err

请注意,目前error类型只是一个普通的接口类型。 编译器不会把它当作任何特殊的东西。 try改变了这一点。 如果允许编译器将error视为特殊的,我更喜欢受/bin/sh启发的||

fd, err := os.Open("foo") || return err

此类代码的含义对大多数程序员来说是相当明显的,没有隐藏的控制流,并且由于目前该代码是非法的,因此不会损害任何工作代码。

虽然我可以想象你们中的一些人在恐惧中退缩。

@bakulif err != nil return err中,你怎么知道表达式err != nil的结束位置和语句return err的开始位置? 您的想法将是对语言语法的重大改变,比try提出的要大得多。

您的第二个想法在Zig中看起来像catch |err| return err 。 就个人而言,我不是“惊恐地退缩”,我会说为什么不呢? 但应该注意的是,Zig 还有一个try关键字,它是catch |err| return err的快捷方式,几乎等同于 Go 团队在这里提出的内置函数。 所以也许try就足够了,我们不需要catch关键字? ;-)

@ngrilly ,目前<expr> <statement>无效,所以我认为这种更改不会使语法更加模棱两可,但可能会更加脆弱。

这将生成与 try 提议完全相同的代码,但是 a) 此处的返回是明确的 b) 与 try 和 c) 没有嵌套可能,这对于 shell 用户来说是熟悉的语法(远远超过 zig 用户)。 这里没有catch

我提出了这个作为替代方案,但坦率地说,无论核心 Go 语言设计者决定什么,我都非常满意。

我上传了一个稍微改进的版本tryhard 。 它现在报告有关输入文件的更详细信息。 例如,针对它现在报告的 Go 存储库的提示运行:

$ tryhard $HOME/go/src
...
--- stats ---
  55620 (100.0% of   55620) function declarations
  14936 ( 26.9% of   55620) functions returning an error
 116539 (100.0% of  116539) statements
  27327 ( 23.4% of  116539) if statements
   7636 ( 27.9% of   27327) if <err> != nil statements
    119 (  1.6% of    7636) <err> name is different from "err" (use -l flag to list file positions)
   6037 ( 79.1% of    7636) return ..., <err> blocks in if <err> != nil statements
   1599 ( 20.9% of    7636) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     17 (  0.2% of    7636) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5907 ( 77.4% of    7636) try candidates (use -l flag to list file positions)

还有更多工作要做,但这给出了更清晰的画面。 具体来说,所有if语句中有 28% 似乎用于错误检查; 这证实了存在大量重复代码。 在这些错误检查中,77% 可以接受try

$ 努力。
--- 统计数据 ---
2930(2930 的 100.0%)函数声明
1408 (48.1% of 2930) 函数返回错误
10497(10497 的 100.0%)报表
2265(10497 的 21.6%)if 语句
1383(2265 的 61.1%)如果!= nil 语句
0(1383 的 0.0%)名称与“err”不同(使用 -l 标志
列出文件位置)
645(1383 的 46.6%)返回 ...,阻止如果!= 无
陈述
738 (53.4% of 1383) if 中更复杂的错误处理程序!= 无
陈述; 防止使用 try(使用 -l 标志列出文件位置)
1(1383 的 0.1%)非空 else 块在 if!= 无
陈述; 防止使用 try(使用 -l 标志列出文件位置)
638(1383 的 46.1%)尝试候选(使用 -l 标志列出文件
职位)
$去mod供应商
$ 努力的供应商
--- 统计数据 ---
37757(37757 的 100.0%)函数声明
12557 (33.3% of 37757) 函数返回错误
88919(88919 的 100.0%)报表
20143(88919 的 22.7%)if 语句
6555(20143 的 32.5%)如果!= nil 语句
109(6555 的 1.7%)名称与“err”不同(使用 -l 标志
列出文件位置)
5545(6555 的 84.6%)返回 ...,阻止如果!= 无
陈述
1010(6555 的 15.4%)if 中更复杂的错误处理程序!= 无
陈述; 防止使用 try(使用 -l 标志列出文件位置)
12(6555 的 0.2%)非空 else 块在 if!= 无
陈述; 防止使用 try(使用 -l 标志列出文件位置)
5427(6555 的 82.8%)尝试候选人(使用 -l 标志列出文件
职位)

所以,这就是我在宏示例中添加冒号的原因,所以它会突出而不像函数调用。 当然不必是冒号。 这只是一个例子。 此外,宏不会隐藏任何内容。 你只要看看宏做了什么,就可以了。 就像它是一个函数一样,但它将被内联。 就像您进行了搜索并将宏中的代码片段替换到完成宏使用的函数中。 自然,如果人们制作宏的宏并开始使事情复杂化,那么,责备自己让代码变得更复杂。 :)

@mirtchovski

$ tryhard .
--- stats ---
   2930 (100.0% of    2930) function declarations
   1408 ( 48.1% of    2930) functions returning an error
  10497 (100.0% of   10497) statements
   2265 ( 21.6% of   10497) if statements
   1383 ( 61.1% of    2265) if <err> != nil statements
      0 (  0.0% of    1383) <err> name is different from "err" (use -l flag to list file positions)
    645 ( 46.6% of    1383) return ..., <err> blocks in if <err> != nil statements
    738 ( 53.4% of    1383) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
      1 (  0.1% of    1383) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
    638 ( 46.1% of    1383) try candidates (use -l flag to list file
positions)
$ go mod vendor
$ tryhard vendor
--- stats ---
  37757 (100.0% of   37757) function declarations
  12557 ( 33.3% of   37757) functions returning an error
  88919 (100.0% of   88919) statements
  20143 ( 22.7% of   88919) if statements
   6555 ( 32.5% of   20143) if <err> != nil statements
    109 (  1.7% of    6555) <err> name is different from "err" (use -l flag to list file positions)
   5545 ( 84.6% of    6555) return ..., <err> blocks in if <err> != nil statements
   1010 ( 15.4% of    6555) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     12 (  0.2% of    6555) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5427 ( 82.8% of    6555) try candidates (use -l flag to list file
positions)
$

@av86743 ,

抱歉,没有考虑到“电子邮件回复不支持 Markdown”

有些人评论说,在tryhard结果中计算供应商代码是不公平的。 例如,在 std 库中,vendored 代码包括生成的syscall包,其中包含大量错误检查,可能会扭曲整体情况。 最新版本的tryhard现在默认排除包含"vendor"的文件路径(这也可以通过新的-ignore标志来控制)。 在提示处应用于 std 库:

tryhard $HOME/go/src
/Users/gri/go/src/cmd/go/testdata/src/badpkg/x.go:1:1: expected 'package', found pkg
/Users/gri/go/src/cmd/go/testdata/src/notest/hello.go:6:1: expected declaration, found Hello
/Users/gri/go/src/cmd/go/testdata/src/syntaxerror/x_test.go:3:11: expected identifier
--- stats ---
  45424 (100.0% of   45424) func declarations
   8346 ( 18.4% of   45424) func declarations returning an error
  71401 (100.0% of   71401) statements
  16666 ( 23.3% of   71401) if statements
   4812 ( 28.9% of   16666) if <err> != nil statements
     86 (  1.8% of    4812) <err> name is different from "err" (-l flag lists details)
   3463 ( 72.0% of    4812) return ..., <err> blocks in if <err> != nil statements
   1349 ( 28.0% of    4812) complex error handler in if <err> != nil statements; cannot use try (-l flag lists details)
     17 (  0.4% of    4812) non-empty else blocks in if <err> != nil statements; cannot use try (-l flag lists details)
   3345 ( 69.5% of    4812) try candidates (-l flag lists details)

现在,所有if语句中有 29% (28.9%) 似乎用于错误检查(比以前稍微多一点),其中大约 70% 似乎是try的候选者(有点比以前少)。

更改https://golang.org/cl/185177提到这个问题: src: apply tryhard -err="" -ignore="vendor" -r $GOROOT/src

@griesemer您计算了“复杂错误处理程序”,但没有计算“单语句错误处理程序”。

如果大多数“复杂”处理程序是单个语句,那么on err #32611 将产生与try()一样多的样板代码节省 - 2 行对 3 行 x 70%。 并且on err为绝大多数错误增加了一致模式的好处。

@nvictor

try是它试图解决的特定问题的干净且易于理解的解决方案:
错误处理中的冗长。

错误处理的冗长不是问题,而是 Go 的强项。

提案内容如下:经过一年的讨论,我们正在添加这个内置的。 如果您想要更少冗长的代码,请使用它,否则请继续执行您的操作。 对于团队成员已显示出明显优势的选择加入功能,反应是一些不完全合理的抵制!

你在写作时的_opt-in_是所有读者的_必须_ ,包括未来的你。

优势明显

如果混淆控制流可以被称为“优势”,那么可以。

try ,为了 java 和 C++ 外籍人士的习惯,引入了所有 Gopher 需要理解的魔法。 同时保留少数几行在几个地方写(如tryhard运行所示)。

我认为我的方式更简单的 onErr 宏可以节省更多的行数,并且对于大多数人来说:

x, err = fa()
onErr break

r, err := fb(x)
onErr return 0, nil, err

if r, err := fc(x); onErr && triesleft > 0 {
  triesleft--
  continue retry
}

_(请注意,我在“让if err!= nil一个人呆着”阵营中,并且发布了柜台提案以展示一个更简单的解决方案,可以让更多的抱怨者开心。)_

编辑:

如果这很容易做到,我会进一步鼓励 Go 团队将try内置为可变参数
try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

~写的短,读的长,容易滑倒或误解,维护阶段易碎和危险。~

我错了。 实际上,可变参数try比嵌套要好得多,因为我们可以逐行编写它:

try( outf.Seek(linkstart, 0),
 io.Copy(outf, exef),
)

并在第一个错误后返回try(…)

我不认为像 try 这样的隐式错误句柄(语法糖)很好,因为您无法直观地处理多个错误,尤其是当您需要顺序执行多个函数时。

我会建议像 Elixir 的 with 声明: https ://www.openmymind.net/Elixirs-With-Statement/

在golang下面是这样的:

switch a, b, err1 := go_func_01(),
       apple, banana, err2 := go_func_02(),
       fans, dissman, err3 := go_func_03()
{
   normal_func()
else
   err1 -> handle_err1()
   err2 -> handle_err2()
   _ -> handle_other_errs()
}

这种是不是违背了“Go 喜欢少一些特性”和“给 Go 添加特性不会让它变得更好而是更大”? 我不知道...

我只想说,我个人对旧方式非常满意

if err != nil {
    return …, err
}

而且我绝对不想阅读其他人使用try编写的代码......原因可能有两个:

  1. 乍一看有时很难猜出里面是什么
  2. try可以嵌套,即try( ... try( ... try ( ... ) ... ) ... ) ,难以阅读

如果您认为以旧方式编写代码以传递错误是乏味的,为什么不直接复制和粘贴,因为它们总是在做同样的工作?

好吧,你可能会认为,我们并不总是想做同样的工作,但是你必须编写你的“处理程序”函数。 因此,如果您仍然以旧方式写作,也许您不会失去任何东西。

这个提议的解决方案的延迟性能不是问题吗? 我已经对有和没有延迟的函数进行了基准测试,并且对性能产生了重大影响。 我只是用谷歌搜索了做过这样一个基准测试的其他人,发现成本是 16 倍。 我不记得我的情况有那么糟糕,但慢了 4 倍就敲响了警钟。 如何将可能使许多函数的运行时间加倍或更差的东西视为可行的通用解决方案?

@eric-hawthorne 推迟性能是一个单独的问题。 Try 本身并不需要 defer,也不会删除没有它处理错误的能力。

@fabian-f 但是这个提议可以鼓励替换代码,其中有人在 if err != nil 块的范围内为每个内联错误单独修饰错误。 这将是一个显着的性能差异。

@eric-hawthorne 引用设计文档:

问:使用 defer 包装错误会不会很慢?

A:目前,与普通控制流相比,defer 语句相对昂贵。 但是,我们相信可以使延迟错误处理的常见用例在性能上与当前的“手动”方法相媲美。 另请参阅 CL 171758,它有望将 defer 的性能提高约 30%。

这是来自 Reddit 上的 Rust 的一个有趣的演讲。 最相关的部分从47:55开始

我在我最大的公共仓库https://github.com/dpinela/mflg上尝试过,得到了以下结果:

--- stats ---
    309 (100.0% of     309) func declarations
     36 ( 11.7% of     309) func declarations returning an error
    305 (100.0% of     305) statements
     73 ( 23.9% of     305) if statements
     29 ( 39.7% of      73) if <err> != nil statements
      0 (  0.0% of      29) <err> name is different from "err"
     19 ( 65.5% of      29) return ..., <err> blocks in if <err> != nil statements
     10 ( 34.5% of      29) complex error handler in if <err> != nil statements; cannot use try
      0 (  0.0% of      29) non-empty else blocks in if <err> != nil statements; cannot use try
     15 ( 51.7% of      29) try candidates

该仓库中的大部分代码都在管理内部编辑器状态并且不执行任何 I/O,因此几乎没有错误检查 - 因此可以使用 try 的地方相对有限。 我继续手动重写代码以尽可能使用 try; git diff --stat返回以下内容:

 application.go                  | 42 +++++++++++-------------------------------
 internal/atomicwrite/write.go   | 35 ++++++++++++++---------------------
 internal/clipboard/clipboard.go | 17 +++--------------
 internal/config/config.go       | 15 +++++++--------
 internal/termesc/term.go        |  5 +----
 render.go                       |  8 ++------
 6 files changed, 38 insertions(+), 84 deletions(-)

这里完全不同。)

在 tryhard 报告为“复杂”的 10 个处理程序中,有 5 个是 internal/atomicwrite/write.go 中的误报; 他们正在使用 pkg/errors.WithMessage 来包装错误。 它们的包装完全相同,因此我重写了该函数以使用 try 和 deferred 处理程序。 我最终得到了这个差异(+14,-21 行):

@@ -20,21 +20,20 @@ const (
 // The file is created with mode 0644 if it doesn't already exist; if it does, its permissions will be
 // preserved if possible.
 // If some of the directories on the path don't already exist, they are created with mode 0755.
-func Write(filename string, contentWriter func(io.Writer) error) error {
+func Write(filename string, contentWriter func(io.Writer) error) (err error) {
+       defer func() { err = errors.WithMessage(err, errString(filename)) }()
+
        dir := filepath.Dir(filename)
-       if err := os.MkdirAll(dir, defaultDirPerms); err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
-       tf, err := ioutil.TempFile(dir, "mflg-atomic-write")
-       if err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(os.MkdirAll(dir, defaultDirPerms))
+       tf := try(ioutil.TempFile(dir, "mflg-atomic-write"))
        name := tf.Name()
-       if err = contentWriter(tf); err != nil {
-               os.Remove(name)
-               tf.Close()
-               return errors.WithMessage(err, errString(filename))
-       }
+       defer func() {
+               if err != nil {
+                       tf.Close()
+                       os.Remove(name)
+               }
+       }()
+       try(contentWriter(tf))
        // Keep existing file's permissions, when possible. This may race with a chmod() on the file.
        perms := defaultPerms
        if info, err := os.Stat(filename); err == nil {
@@ -42,14 +41,8 @@ func Write(filename string, contentWriter func(io.Writer) error) error {
        }
        // It's better to save a file with the default TempFile permissions than not save at all, so if this fails we just carry on.
        tf.Chmod(perms)
-       if err = tf.Close(); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
-       if err = os.Rename(name, filename); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(tf.Close())
+       try(os.Rename(name, filename))
        return nil
 }

注意第一个 defer,它注释了错误 - 由于 WithMessage 返回 nil 错误,我能够轻松地将其放入一行中。 似乎这种包装器与提案中建议的方法一样适用于这种方法。

其他两个“复杂”处理程序在 ReadFrom 和 WriteTo 的实现中:

var line string
line, err = br.ReadString('\n')
b.lines = append(b.lines, line)
if err != nil {
  if err == io.EOF {
    err = nil
  }
  return
}
func (b *Buffer) WriteTo(w io.Writer) (int64, error) {
    var n int64
    for _, line := range b.lines {
        nw, err := w.Write([]byte(line))
        n += int64(nw)
        if err != nil {
            return n, err
        }
    }
    return n, nil
}

这些真的不适合尝试,所以我让他们一个人呆着。

另外两个是这样的代码,我返回的错误与我检查的错误完全不同(不仅仅是包装它)。 我也让它们保持不变:

n, err := strconv.ParseInt(s[1:], 16, 32)
if err != nil {
    return Color{}, errors.WithMessage(err, fmt.Sprintf("color: parse %q", s))
}

最后一个是在加载配置文件的函数中,即使出现错误,它也总是返回(非零)配置。 它只有一个错误检查,所以如果从尝试中受益,它并没有太大的好处:

-func Load() (*Config, error) {
-       c := Config{
+func Load() (c *Config, err error) {
+       defer func() { err = errors.WithMessage(err, "error loading config file") }()
+
+       c = &Config{
                TabWidth:    4,
                ScrollSpeed: 1,
                Lang:        make(map[string]LangConfig),
        }
-       f, err := basedir.Config.Open(filepath.Join("mflg", "config.toml"))
-       if err != nil {
-               return &c, errors.WithMessage(err, "error loading config file")
-       }
+       f := try(basedir.Config.Open(filepath.Join("mflg", "config.toml")))
        defer f.Close()
-       _, err = toml.DecodeReader(f, &c)
+       _, err = toml.DecodeReader(f, c)
        if c.TextStyle.Comment == (Style{}) {
                c.TextStyle.Comment = Style{Foreground: &color.Color{R: 0, G: 200, B: 0}}
        }
        if c.TextStyle.String == (Style{}) {
                c.TextStyle.String = Style{Foreground: &color.Color{R: 0, G: 0, B: 200}}
        }
-       return &c, errors.WithMessage(err, "error loading config file")
+       return c, err
 }

事实上,在我看来,依靠 try 保持返回参数值的行为——就像一个裸返回——感觉有点难以理解。 除非我添加更多错误检查,否则在这种特殊情况下我会坚持使用if err != nil

TL;DR:try 仅在此代码的一小部分(按行数)中有用,但在它有帮助的地方,它确实有帮助。

(这里是菜鸟)。 多个参数的另一个想法。 怎么样:

package trytest

import "fmt"

func errorInner() (string, error) {
   return "", fmt.Errorf("inner error")
}

func errorOuter() (string, error) {
   tryreturn errorInner()
   return "", nil
}

func errorOuterWithArg() (string, error) {
   var toProcess string
   tryreturn toProcess, _ = errorOuter()
   return toProcess + "", nil
}

func errorOuterWithArgStretch() (bool, string, error) {
   var toProcess string
   tryreturn false, ( toProcess,_ = errorOuterWithArg() )
   return true, toProcess + "", nil
}

即如果最后出错,tryreturn 会触发所有值的返回
值,否则继续执行。

我同意的原则:
-

  • 处理函数调用的错误值得单独写一行。 Go 在控制流中刻意明确,我认为将其打包到表达式中与其明确性不一致。
  • 有一个适合一行的错误处理方法将是有益的。 (理想情况下,在实际错误处理之前只需要一个单词或几个字符的样板文件)。 每个函数调用的 3 行错误处理是该语言中值得关注和关注的一个摩擦点。
  • 任何返回的内置函数(如提议的try )至少应该是一个语句,并且最好在其中包含 return 这个词。 同样,我认为 Go 中的控制流应该是明确的。
  • Go 的错误在包含额外上下文时最有用(我几乎总是将上下文添加到我的错误中)。 此问题的解决方案还应支持添加上下文的错误处理代码。

我支持的语法:
-

  • reterr _x_语句( if err != nil { return _x_ }的语法糖,明确命名以表明它将返回)

所以常见的情况可能是一个简短而明确的行:

func foo() error {
    a, err := bar()
    reterr err

    b, err := baz(a)
    reterr fmt.Errorf("getting the baz of %v: %v", a, err)

    return nil
}

而不是现在的 3 行:

func foo() error {
    a, err := bar()
    if err != nil {
        return err
    }

    b, err := baz()
    if err != nil {
        return fmt.Errorf("getting the baz of %v: %v", a, err)
    }

    return nil
}

我不同意的事情:



    • “这个改动太小了,不值得改变语言”

      我不同意,这是一种生活质量的改变,它消除了我在编写 Go 代码时遇到的最大摩擦源。 调用函数时需要4行

  • “最好等待更通用的解决方案”
    我不同意,我认为这个问题值得自己专门解决。 这个问题的通用版本是减少样板代码,通用的答案是宏——这与显式代码的 Go 精神背道而驰。 如果 Go 不打算提供通用的宏工具,那么它应该提供一些特定的、非常广泛使用的宏,例如reterr (每个编写 Go 的人都会从 reterr 中受益)。

@Qhesz与尝试没有太大区别:

func foo() error {
    a, err := bar()
    try(err)

    b, err := baz(a)
    try(wrap(err, "getting the baz of %v", a))

    return nil
}

@reusee我很欣赏这个建议,我没有意识到它可以这样使用。 不过,这对我来说似乎有点刺耳,我试图找出原因。

我认为以这种方式使用“尝试”是一个奇怪的词。 “try(action())”在英语中是有意义的,而“try(value)”则不是。 如果它是一个不同的词,我会更好。

try(wrap(...))也首先评估wrap(...)对吗? 您认为其中有多少被编译器优化掉了? (与仅运行if err != nil相比?)

此外,#32611 也是一个模糊相似的提案,评论中有一些来自核心 Go 团队和社区成员的启发性意见,特别是围绕关键字和内置函数之间的差异。

@Qhesz我同意你的命名。 也许check更合适,因为 "check(action())" 或 "check(err)" 读起来都很好。

@reusee这有点讽刺,因为最初的设计草案使用check

2019 年 7 月 6 日,mirtchovski [email protected]写道:

$ 努力。
--- 统计数据 ---
2930(2930 的 100.0%)函数声明
1408 (48.1% of 2930) 函数返回错误
[ ... ]

我不禁在这里恶作剧:“函数返回一个
错误作为最后一个参数”?

卢西奥。

关于我上面的问题的最终想法,我仍然更喜欢语法try(err, wrap("getting the baz of %v: %v", a, err)) ,只有在 err 不为零时才执行 wrap() 。 而不是try(wrap(err, "getting the baz of %v", a))

@Qhesz wrap的可能实现可能是:

func wrap(err error, format string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

如果编译器可以内联wrap ,则wrapif err != nil子句之间没有性能差异。

@reusee我认为你的意思是if err == nil ;)

@Qhesz wrap的可能实现可能是:

func wrap(err error, format string, args ...interface{}) error {
  if err == nil {
      return nil
  }
  return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

如果编译器可以内联wrap ,则wrapif err != nil子句之间没有性能差异。

%w无效go动词

(我猜他的意思是 %v...)

因此,虽然最好写一个关键字,但我知道内置是实现它的首选方式。

我想我会同意这个提议,如果

  • 它是check而不是try
  • Go 工具的某些部分强制它只能用作语句(即,将其视为内置“语句”,而不是内置“函数”。出于实用性原因,它只是一个内置语句,它试图成为一个语句而不是由语言实现。)例如,如果它没有返回任何内容,那么在表达式中永远不会有效,例如panic()
  • 〜也许一些指标表明它是一个宏并影响控制流,这将它与函数调用区分开来。 (例如像Rust那样的check!(...) ,但我对具体的语法没有强烈的意见)~改变了主意

那太好了,我会在我进行的每个函数调用中使用它。

对这个帖子表示轻微的歉意,我现在才发现上面的评论与我刚才所说的差不多。

@deanveloper 已修复,谢谢。

@olekukonko @Qhesz %w 在提示中新增: https ://tip.golang.org/pkg/fmt/#Errorf

我很抱歉没有阅读本主题的所有内容,但我想提一些我没看过的东西。

我看到 Go1 的错误处理可能令人讨厌的两种不同情况:正确但有点重复的“好”代码; 和错误的“坏”代码,但大多数情况下都有效。

在第一种情况下,if-err 块中确实应该有一些逻辑,而转向 try 风格的构造会因为难以添加额外的逻辑而阻碍这种良好实践。

在第二种情况下,糟糕的代码通常是以下形式:

..., _ := might_error()

要不就

might_error()

发生这种情况通常是因为作者认为花时间处理错误不够重要,只是希望一切正常。 这种情况可以通过非常接近零努力的方式来改善,例如:

..., XXX := might_error()

其中 XXX 是一个符号,意思是“这里的任何东西都应该以某种方式停止执行”。 这将清楚地表明这不是生产就绪代码 - 作者知道错误情况,但没有花时间决定要做什么。

当然,这并不排除returnif handle(err)类型的解决方案。

总的来说,我反对尝试对极简设计的贡献者表示赞赏。 我不是 Go 专家,但我是早期采用者,并且在生产环境中到处都有代码。 我在 AWS 的无服务器组工作,看起来我们将在今年晚些时候发布基于 Go 的服务,其中的第一个签到基本上是由我编写的。 我是一个非常老的人,我的道路是通过 C、Perl、Java 和 Ruby 来引导的。 我的问题以前曾出现在非常有用的辩论摘要中,但我仍然认为它们值得重申。

  1. Go 是一种小而简单的语言,因此具有无与伦比的可读性。 我条件反射地反对在其中添加任何东西,除非它的好处在质量上真的很可观。 一个人通常不会注意到一个滑坡,直到一个人在上面,所以我们不要迈出第一步。
  2. 上面关于促进调试的论点深深地影响了我。 我喜欢低级基础设施代码中的视觉节奏,即“Do A. Check if it是否有效”这样的小节代码。 做 B. 检查它是否有效……等等”因为“检查”行是你放置 printf 或断点的地方。也许其他人都更聪明,但我最终经常使用那个断点习语。
  3. 假设命名返回值,“try”大约相当于if err != nil { return } (我认为?)我个人喜欢命名返回值,并且考虑到错误装饰器的好处,我怀疑命名错误返回值的比例将单调递增; 这削弱了尝试的好处。
  4. 我最初喜欢在上面一行中让 gofmt 祝福 one-liner 的提议,但总的来说,IDE 毫无疑问无论如何都会采用这种显示习惯,而 one-liner 会牺牲 debug-here 的好处。
  5. 某些形式的包含“try”的表达式嵌套似乎很可能会为我们专业的复杂化者打开大门,从而造成与 Java 流和拆分器等相同的破坏。 Go 比大多数其他语言更成功地拒绝了我们当中聪明的人展示他们技能的机会。

再次祝贺社区提出了漂亮干净的提案和建设性的讨论。

在过去的几年里,我花费了大量时间进入和阅读不熟悉的库或代码片段。 尽管单调乏味, if err != nil提供了一个非常容易阅读的,尽管垂直冗长的习语。 try()试图实现的精神是崇高的,我确实认为有一些事情要做,但是这个功能感觉优先级错误,而且提案太早了(即它应该来xerr和仿制药有机会在稳定版本中腌制 6-12 个月)。

引入try()似乎是一个崇高而有价值的提议(例如, 29% - ~40%if语句用于if err != nil检查)。 从表面上看,与错误处理相关的减少样板似乎会改善开发人员的体验。 引入try()的折衷是以来自半微妙特殊情况的认知负荷的形式出现的。 Go 最大的优点之一是它很简单,完成某事所需的认知负荷很少(与语言规范庞大且细致入微的 C++ 相比)。 减少一个量化指标( if err != nil的 LoC)以换取增加心理复杂性的量化指标是难以下咽的药丸(即对我们拥有的最宝贵资源——脑力的精神税)。

特别是try()处理方式的新特殊情况godefer和命名返回变量使得try()足够神奇,可以减少代码明确的,这样所有 Go 代码的作者或读者都必须知道这些新的特殊情况才能正确读取或编写 Go,而这种负担以前不存在。 我喜欢这些情况有明确的特殊情况——尤其是与引入某种形式的未定义行为相比,但它们首先需要存在的事实表明目前这还不完整。 如果特殊情况是针对除错误处理之外的任何事情,那么它可能是可以接受的,但如果我们已经在谈论可能影响多达 40% 的所有 LoC 的事情,那么这些特殊情况将需要在整个社区中进行培训,并且这将这个提议的认知负荷成本提高到足以引起关注的水平。

在围棋中还有一个例子,特殊情况规则已经是一个滑坡,即固定变量和未固定变量。 需要固定变量在实践中并不难理解,但它被遗漏了,因为这里存在隐式行为,这会导致作者、读者和运行时编译的可执行文件之间的不匹配。 即使使用诸如scopelint之类的 linter,许多开发人员似乎仍然没有掌握这个问题(或者更糟糕的是,他们知道但错过了它,因为这个问题让他们忘记了)。 一些来自功能程序的最意想不到和最难诊断的运行时错误来自这个特定的问题(例如,N 个对象都被填充了相同的值,而不是遍历切片并获得预期的不同值)。 try()的失败域与固定变量不同,但结果会对人们编写代码的方式产生影响。

IMNSHO, xerr和仿制药提案需要时间在生产中烘烤 6-12 个月,然后才能尝试从if err != nil征服样板。 泛型可能会为更丰富的错误处理和新的错误处理惯用方式铺平道路。 一旦使用泛型的惯用错误处理开始出现,那么并且只有到那时,重新讨论围绕try()或其他什么的讨论才有意义。

我不假装知道泛型将如何影响错误处理,但在我看来,泛型将用于创建几乎肯定会用于错误处理的丰富类型。 一旦泛型已经渗透到库中并被添加到错误处理中,可能会有一种明显的方法来重新调整try()的用途,以改善开发人员在错误处理方面的体验。

我的关注点是:

  1. try()孤立起来并不复杂,但它是认知开销,以前没有任何东西存在。
  2. 通过将err != nil try()假定行为中,该语言正在阻止使用err作为在堆栈上通信状态的一种方式。
  3. 从美学上看, try()感觉像是被迫的聪明,但不够聪明,无法满足大多数 Go 语言喜欢的明确和明显的测试。 像大多数涉及主观标准的事情一样,这是个人品味和经验的问题,很难量化。
  4. 使用switch / case语句和错误包装的错误处理似乎没有受到这个提议的影响,并且错失了一个机会,这让我相信这个提议对于让一个未知的未知成为已知的事情有点困难- 已知(或最坏的情况是已知-未知)。

最后, try()提案感觉就像是大坝中的一个新突破,它阻止了大量特定于语言的细微差别,就像我们通过将 C++ 抛在后面而逃脱的那样。

TL;DR: 与其说是#nevertry的回应,不如说是,“不是现在,还不是,让我们在未来xerr和仿制药在生态系统中成熟之后再次考虑这个问题。 "

上面链接的#32968 并不完全是一个完全的反建议,但它建立在我不同意try宏所具有的危险嵌套能力的基础上。 与#32946 不同的是,这是一个严肃的提议,我希望它没有严重的缺陷(当然,你可以看到、评估和评论)。 摘抄:

  • _ check不是单行的:它在许多重复的地方最有帮助
    应在附近执行使用相同表达式的检查。_
  • _它的隐式版本已经在操场上编译了。_

设计约束(满足)

它是内置的,它不会嵌套在一行中,它允许比try更多的流程,并且对其中的代码形状没有任何期望。 它不鼓励裸退货。

使用示例

// built-in 'check' macro signature: 
func check(Condition bool) {}

check(err != nil) // explicit catch: label.
{
    ucred, err := getUserCredentials(user)
    remote, err := connectToApi(remoteUri)
    err, session, usertoken := remote.Auth(user, ucred)
    udata, err := session.getCalendar(usertoken)

  catch:               // sad path
    ucred.Clear()      // cleanup passwords
    remote.Close()     // do not leak sockets
    return nil, 0, err // dress before leaving
}
// happy path

// implicit catch: label is above last statement
check(x < 4) 
  {
    x, y = transformA(x, z)
    y, z = transformB(x, y)
    x, y = transformC(y, z)
    break // if x was < 4 after any of above
  }

希望这会有所帮助,享受!

我已尽可能多地阅读以了解该线程。 我赞成让事情保持原样。

我的理由:

  1. 我和我教过 Go 的人都_永远_不懂错误处理
  2. 我发现自己从不跳过错误陷阱,因为在那时和那里就可以很容易地做到这一点

另外,也许我误解了这个提议,但通常,其他语言中的try构造会导致多行代码,这些代码都可能会产生错误,因此它们需要错误类型。 增加复杂性,通常会增加某种前期错误架构和设计工作。

在这些情况下(我自己已经这样做了),会添加多个 try 块。 这延长了代码,并掩盖了实现。

如果try的 Go 实现与其他语言的不同,那么就会出现更多的混乱。

我的建议是让错误处理保持原样

我知道很多人都在权衡,但我想按原样添加我对规范的批评。

规范中最困扰我的部分是这两个请求:

因此我们建议在 go 语句中禁止 try 作为被调用函数。
...
因此,我们建议在 defer 语句中也不允许 try 作为被调用函数。

这将是第一个这样的内置函数(您甚至可以defergopanic编辑,因为不需要丢弃结果。 创建一个要求编译器考虑特殊控制流的新内置函数似乎是一个很大的问题,并且破坏了 go 的语义连贯性。 go 中的所有其他控制流令牌都不是函数。

与我的抱怨相反的论点是,能够defergopanic可能是偶然的,而且不是很有用。 然而,我的观点是,go 中函数的语义连贯性被这个提议破坏了,并不是说defergo总是有意义的使用很重要。 可能有很多非内置函数使用defergo永远没有意义,但是从语义上讲,没有明确的理由为什么不能这样做。 为什么这个内置函数可以免除 go 函数的语义契约?

我知道@griesemer不希望将有关该提案的美学意见注入讨论中,但我确实认为人们发现该提案在美学上令人反感的一个原因是他们可以感觉到它并没有完全加起来作为一项功能。

该提案说:

我们建议添加一个新的类似函数的内置函数,称为 try with signature(伪代码)

func try(expr) (T1, T2, … Tn)

除了这不是一个功能(提案基本上承认)。 实际上,它是语言规范中内置的一次性宏(如果要接受的话)。 这个签名有一些问题。

  1. 函数接受泛型表达式作为参数意味着什么,更不用说被调用的表达式了。 每隔一次在规范中使用“表达式”这个词,它就意味着一个未调用的函数。 当在所有其他上下文中,它的返回值在语义上是活跃的时,如何将“被调用”函数视为一个表达式。 IE 我们认为一个被调用的函数是它的返回值。 很明显,例外是godefer ,它们都是原始标记而不是内置函数。

  2. 此外,该提案的函数签名也不正确,或者至少没有意义,实际签名是:

func try(R1, R2, ... Rn) ((R|T)1, (R|T)2, ... (R|T)(n-1), ?Rn) 
// where T is the return params of the function that try is being called from
// where `R` is a return value from a function, `Rn` must be an error
// try will return the R values if Rn is nil and not return Tn at all
// if Rn is not nil then the T values will be returned as well as Rn at the end 
  1. 该提案不包括在使用参数调用 try 的情况下发生的情况。 如果使用参数调用try会发生什么:
try(arg1, arg2,..., err)

我认为未解决此问题的原因是因为try试图接受一个expr参数,该参数实际上表示来自函数的 n 个返回参数加上其他内容,进一步说明了这一事实这个提议打破了函数的语义连贯性。

我对这个提议的最后抱怨是它进一步破坏了内置函数的语义。 我对内置函数有时需要免除“正常”函数的语义规则(比如不能将它们分配给变量等)的想法并不无动于衷,但是这个提议创建了大量的“正常”的规则,似乎管理着 golang 内部的功能。

这个提议有效地使try成为 go 没有的新事物,它不是一个令牌,也不是一个函数,两者兼而有之,这似乎是一个糟糕的先例,无法在整个过程中创建语义连贯性语言。

如果我们要添加一个新的控制流事物,我认为将其作为原始令牌更有意义,例如goto等。 我知道我们不应该在这次讨论中兜售提案,但通过简单的例子,我认为这样的事情更有意义:

f, err := os.Open("/dev/stdout")
throw err

虽然这确实添加了额外的代码行,但我认为它解决了我提出的每个问题,并且还消除了try的整个“替代”函数签名缺陷。

edit1 :注意defergo情况的例外情况,其中内置函数无法使用,因为结果将被忽略,而使用try甚至不能表示函数有结果。

@nathanjsweet您寻求的建议是#32611 :-)

@nathanjsweet您所说的某些事实并非如此。 该语言不允许将defergo与预先声明的函数append cap complex imag len make new real一起使用。 它也不允许defergo使用规范定义的函数unsafe.Alignof unsafe.Offsetof unsafe.Sizeof

感谢@nathanjsweet的广泛评论—— @ianlancetaylor已经指出你的论点在技术上是不正确的。 让我稍微扩展一下:

1)您提到规范中禁止trygodefer的部分最让您困扰,因为try将是第一个内置的这是真的。 这是不正确的。 编译器已经不允许,例如defer append(a, 1) 。 对于其他产生结果然后掉在地板上的内置插件也是如此。 这个限制也适用于try (除非try不返回结果)。 (我们在设计文档中甚至提到这些限制的原因是为了尽可能彻底——它们在实践中确实无关紧要。另外,如果你仔细阅读设计文档,它并没有说我们不能赚trygodefer一起工作 - 它只是表明我们不允许它;主要是作为一种实际措施。这是一个“大要求” - 用你的话 - 赚trygodefer一起使用,即使它实际上没用。)

2)你建议有些人发现try “审美反感”是因为它在技术上不是一个功能,然后你专注于签名的特殊规则。 考虑newmakeappendunsafe.Offsetof :它们都有我们无法用普通 Go 函数表达的特殊规则。 看看unsafe.Offsetof它的参数的语法要求(它必须是一个结构字段!),我们需要try的参数(它必须是类型的单个值) error或返回error作为最后结果的函数调用)。 我们没有在规范中正式表达这些签名,因为这些内置函数都没有,因为它们不适合现有的形式——如果它们愿意,它们就不必是内置的。 相反,我们用散文表达他们的规则。 这就是_为什么_它们是内置的_是_从第一天开始设计的围棋逃生舱口。 另请注意,设计文档对此非常明确。

3) 该提案还确实解决了使用参数(多个)调用try时会发生的情况:这是不允许的。 设计文档明确指出try接受(一个)传入参数表达式。

4)你说“这个提议破坏了内置函数的语义”。 Go 没有任何地方限制内置程序可以做什么和不能做什么。 我们在这里拥有完全的自由。

谢谢。

@griesemer

另请注意,设计文档对此非常明确。

你能指出这一点。 读到这里我很惊讶。

您说的是“这个提议破坏了内置函数的语义”。 Go 没有任何地方限制内置程序可以做什么和不能做什么。 我们在这里拥有完全的自由。

我认为这是一个公平的观点。 但是,我确实认为设计文档中有详细说明,并且感觉像是“开始”(这是 Rob Pike 经常谈论的内容)。 我认为我认为try提案扩展了内置函数打破我们期望函数行为的规则的方式对我来说是公平的,我确实承认我理解为什么这对于其他内置函数是必要的,但我认为在这种情况下,打破规则的扩展是:

  1. 在某些方面违反直觉。 这是第一个以不展开堆栈的方式更改控制流逻辑的函数(如panicos.Exit所做的)
  2. 函数调用约定如何工作的新例外。 您给出了unsafe.Offsetof的示例作为函数调用有语法要求的情况(实际上这导致编译时错误令我感到惊讶,但这是另一个问题),但是语法要求,在这种情况下,是与您所说的不同的语法要求。 unsafe.Offsetof需要一个参数,而try需要一个表达式,该表达式在所有其他上下文中看起来都像从函数返回的值(即try(os.Open("/dev/stdout")) )并且可以安全地假设在所有其他上下文中只返回一个值(除非表达式看起来像try(os.Open("/dev/stdout")...) )。

@nathanjsweet写道:

另请注意,设计文档对此非常明确。

你能指出这一点。 读到这里我很惊讶。

它在提案的“结论”部分

在 Go 中,内置函数是选择的语言转义机制,用于以某种方式不规则但不能证明特殊语法的操作。

我很惊讶你错过了它;-)

@ngrilly我的意思不是在这个提案中,而是在 go 语言规范中。 我的印象是@griesemer说 go 语言规范调用内置函数是打破句法约定的特别有用的机制。

@nathanjsweet

在某些方面违反直觉。 这是第一个以不展开堆栈的方式更改控制流逻辑的函数(如 panic 和 os.Exit 所做的)

我认为os.Exit不会在任何有用的意义上展开堆栈。 它立即终止程序而不运行任何延迟函数。 在我看来os.Exit是奇怪的,因为panictry都运行延迟函数并在堆栈中向上移动。

我同意os.Exit是奇数,但必须如此。 os.Exit停止所有 goroutines; 只运行调用os.Exit的 goroutine 的延迟函数是没有意义的。 它应该运行所有延迟函数,或者不运行。 而且没有运行要容易得多。

在我们的代码库上执行tryhard ,这就是我们得到的:

--- stats ---
  15298 (100.0% of   15298) func declarations
   3026 ( 19.8% of   15298) func declarations returning an error
  33941 (100.0% of   33941) statements
   7765 ( 22.9% of   33941) if statements
   3747 ( 48.3% of    7765) if <err> != nil statements
    131 (  3.5% of    3747) <err> name is different from "err"
   1847 ( 49.3% of    3747) return ..., <err> blocks in if <err> != nil statements
   1900 ( 50.7% of    3747) complex error handler in if <err> != nil statements; cannot use try
     19 (  0.5% of    3747) non-empty else blocks in if <err> != nil statements; cannot use try
   1789 ( 47.7% of    3747) try candidates

首先,我想澄清一下,因为 Go(1.13 之前)在错误中缺少上下文,我们实现了自己的错误类型,它实现了error接口,一些函数被声明为返回foo.Error而不是error ,看起来这个分析器没有捕捉到这一点,所以这些结果并不“公平”。

我在“是的!让我们这样做”的阵营中,我认为这将是 1.13 或 1.14测试版的一个有趣的实验,但我担心 _“ 47.7% ......尝试候选人”_。 它现在意味着有两种做事方式,我不喜欢。 但是,也有 2 种创建指针的方法( new(Foo)&Foo{} )以及 2 种使用make([]Foo)[]Foo{}创建切片或映射的方法.

现在我在“让我们_试试_这个”的阵营:^) 看看社区的想法。 也许我们会将我们的编码模式更改为惰性并停止添加上下文,但如果错误从即将到来的xerrors impl 中获得更好的上下文,那也许没关系。

谢谢@Goodwine提供更具体的数据!

(顺便说一句,我昨晚对tryhard做了一个小改动,因此它将“复杂错误处理程序”计数分为两个计数:复杂处理程序和返回形式return ..., expr的最后一个结果值不是<err> 。这应该提供一些额外的见解。)

将提案修改为可变参数而不是这个奇怪的表达式参数怎么样?

那会解决很多问题。 在人们只想返回错误的情况下,唯一会改变的是显式可变参数... 。 例如:

try(os.Open("/dev/stdout")...)

但是,想要更灵活的情况的人可以执行以下操作:

f, err := os.Open("/dev/stdout")
try(WrapErrorf(err, "whatever wrap does: %v"))

这个想法所做的一件事是使try这个词不太合适,但它保持了向后兼容性。

@nathanjsweet写道:

我的意思不是在这个提案中,我的意思是在 go 语言规范中。

以下是您在语言规范中寻找的摘录:

在“表达式语句”部分:

语句上下文中不允许使用以下内置函数: append cap complex imag len make new real unsafe.Alignof unsafe.Offsetof unsafe.Sizeof

在“Go 语句”和“Defer 语句”部分:

内置函数的调用与表达式语句一样受到限制。

在“内置函数”部分:

内置函数没有标准的 Go 类型,所以只能出现在调用表达式中; 它们不能用作函数值。

@nathanjsweet写道:

我的印象是@griesemer说 go 语言规范将内置函数称为打破句法约定的特别有用的机制。

内置函数不会破坏 Go 语法约定(括号、参数之间的逗号等)。 它们使用与用户定义函数相同的语法,但它们允许在用户定义函数中无法完成的事情。

@nathanjsweet这已经被考虑过了(实际上这是一个疏忽),但它使try不可扩展。 请参阅https://go-review.googlesource.com/c/proposal/+/181878

更一般地说,我认为您将批评集中在错误的事情上: try参数的特殊规则实际上不是问题——几乎每个内置函数都有特殊规则。

@griesemer感谢您为此开展工作并花时间回应社区关注的问题。 我相信你在这一点上已经回答了很多相同的问题。 我意识到解决这些问题并同时保持向后兼容性真的很困难。 谢谢!

@nathanjsweet关于您评论:

请参阅结论部分,该部分重点讨论了 Go 中内置函数的作用。

关于您对try以不同方式扩展内置函数的评论:是的, unsafe.Offsetof对其参数的要求与try的要求不同。 但两者都期望在语法上是一个表达式。 两者都对该表达式有一些额外的限制。 try的要求很容易适应 Go 的语法,以至于不需要调整任何前端解析工具。 我知道你觉得我觉得不寻常,但这与反对它的技术原因不同。

@griesemer最新的 _tryhard_ 计算“复杂错误处理程序”,但不计算“单语句错误处理程序”。 可以添加吗?

@networkimprov什么是单语句错误处理程序? 包含单个非返回语句的if块?

@griesemer ,单语句错误处理程序是一个if err != nil块,其中包含 _any_ 单个语句,包括返回。

@networkimprov完成。 “复杂处理程序”现在分为“单语句然后分支”和“复杂然后分支”。

也就是说,请注意这些计数可能具有误导性:例如,这些计数包括任何检查任何变量是否为零的if语句(如果-err=""现在是tryhard的默认值) tryhard高估了复杂或单语句处理程序机会的数量。 例如,参见archive/tar/common.go ,第 701 行。

@networkimprov tryhard现在提供更准确的计数,说明为什么错误检查不是try候选者。 try计数的总数没有变化,但更多单一和复杂处理程序的机会数量现在更准确(并且比以前减少了大约 50%,因为之前任何复杂的then只要if包含<varname> != nil检查,就考虑if语句的分支,无论它是否涉及错误检查)。

如果有人想通过更多的手来尝试try ,我在这里创建了一个带有原型实现的 WASM 游乐场:

https://ccbrown.github.io/wasm-go-playground/experimental/try-builtin/

如果有人真的对使用 try 在本地编译代码感兴趣,我有一个 Go fork,我认为这是一个功能齐全/最新的实现: https ://github.com/ccbrown/go/pull/1

我喜欢“尝试”。 我发现管理 err 的本地状态,并使用 := vs = with err,以及相关的导入,经常分散​​注意力。 另外,我不认为这是创建两种方法来做同样的事情,更像是两种情况,一种是您想要传递错误而不对其采取行动,另一种是您明确希望在调用函数中处理它例如。 记录。

我在一年多前从事的一个小型内部项目中运行了tryhard 。 有问题的目录包含 3 个服务器的代码(我想是“微服务”)、一个作为 cron 作业定期运行的爬虫以及一些命令行工具。 它也有相当全面的单元测试。 (FWIW,各个部分已经顺利运行了一年多,事实证明可以直接调试和解决出现的任何问题)

以下是统计数据:

--- stats ---
    370 (100.0% of     370) func declarations
    115 ( 31.1% of     370) func declarations returning an error
   1159 (100.0% of    1159) statements
    258 ( 22.3% of    1159) if statements
    123 ( 47.7% of     258) if <err> != nil statements
     64 ( 52.0% of     123) try candidates
      0 (  0.0% of     123) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     54 ( 43.9% of     123) { return ... zero values ..., expr }
      2 (  1.6% of     123) single statement then branch
      3 (  2.4% of     123) complex then branch; cannot use try
      1 (  0.8% of     123) non-empty else branch; cannot use try

一些评论:
1) 此代码库中所有if语句的 50% 都在进行错误检查,而try可以替换其中的大约一半。 这意味着这个(小)代码库中所有if语句的四分之一是 $#$5 try #$ 的输入版本。

2)我应该注意到这对我来说非常高,因为在开始这个项目前几周,我碰巧读到了一系列内部帮助函数( status.Annotate )注释错误消息但保留gRPC 状态码。 例如,如果您调用 RPC 并且它返回一个带有关联状态码 PERMISSION_DENIED 的错误,则此帮助函数返回的错误仍将具有关联状态码 PERMISSION_DENIED(理论上,如果该关联状态码已传播到所有直到 RPC 处理程序,然后 RPC 将失败并显示相关的状态代码)。 我已经决定在这个新项目的所有内容中使用这些功能。 但显然,对于 50% 的错误,我只是简单地传播了一个未注释的错误。 (在运行tryhard之前,我曾预测为 10%)。

3) status.Annotate碰巧保留nil错误(即status.Annotatef(err, "some message: %v", x)将返回nil iff err == nil )。 我查看了第一类的所有未尝试的候选人,似乎所有人都可以接受以下重写:

```
// Before
enc, err := keymaster.NewEncrypter(encKeyring)                                                     
if err != nil {                                                                                    
  return status.Annotate(err, "failed to create encrypter")                                        
}

// After
enc, err := keymaster.NewEncrypter(encKeyring)                                                                                                                                                                  
try(status.Annotate(err, "failed to create encrypter"))
```

To be clear, I'm not saying this transformation is always necessarily a good idea, but it seemed worth mentioning since it boosts the count significantly to a bit under half of all `if` statements.

4) 老实说,基于defer的错误注释似乎与try有点正交,因为它可以在有和没有try的情况下工作。 但是在查看这个项目的代码时,由于我仔细研究了错误处理,我碰巧注意到了几个被调用者生成的错误更有意义的例子。 举个例子,我注意到几个调用 gRPC 客户端的代码实例,如下所示:

```
resp, err := s.someClient.SomeMethod(ctx, req)
if err != nil {
  return ..., status.Annotate(err, "failed to call SomeMethod")
}
```

This is actually a bit redundant in retrospect, since gRPC already prefixes its errors with something like "/Service.Method to [ip]:port : ".

There was also code that called standard library functions using the same pattern:

```
hreq, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
  return status.Annotate(err, "http.NewRequest failed")
}
```

In retrospect, this code demonstrates two issues: first, `http.NewRequest` isn't calling a gRPC API, so using `status.Annotate` was unnecessary, and second, assuming the standard library also return errors with callee context, this particular use of error annotation was unnecessary (although I am fairly certain the standard library does not consistently follow this pattern).

无论如何,我认为回到这个项目并仔细研究它如何处理错误是一个有趣的练习。

一件事, @griesemertryhard是否对“未尝试的候选人”有正确的分母?
编辑:在下面回答,我误读了统计数据。

编辑:本应反馈的内容被转化为提案,我们被明确要求不要在这里做。 我将我的评论移至gist

@balasanjay感谢您基于事实的评论; 这很有帮助。

关于您关于tryhard的问题:“未尝试的候选人”(欢迎更好的标题建议)只是if语句满足“错误检查”的所有标准(即,我们看起来像是对错误变量<err>的赋值,然后在源代码中进行了if <err> != nil检查),但是我们不能轻易使用try因为if块中的代码。 具体来说,按照在“非尝试候选”输出中出现的顺序,这些是if语句,其中有一个return语句,最后返回的不是<err>if语句带有一个更复杂的return (或其他)语句, if语句在“then”分支中带有多个语句, if语句带有非空else分支。 其中一些if语句可能同时满足多个这些条件,因此这些数字不只是相加。 它们旨在说明try可用的问题所在。

我今天对此进行了最新调整(您运行的是最新版本)。 在最后一次更改之前,即使没有涉及错误检查,其中一些条件也会被计算在内,这似乎没有多大意义,因为它看起来像try不能在更多情况下使用,而实际上try首先在这些情况下,

最重要的是,对于给定的代码库, try候选者的总数并没有随着这些改进而改变,因为try的相关条件保持不变。

如果您对如何衡量和/或衡量什么有更好的建议,我很乐意听到。 我根据社区反馈做了一些调整。 谢谢。

@subfuzion感谢您的评论,但我们不是在寻找替代建议。 请参阅https://github.com/golang/go/issues/32437#issuecomment -501878888 。 谢谢。

为了被计算在内,无论结果如何:

我和我的团队认为,虽然 Rob 提出的try框架是一个合理且有趣的想法,但它并没有达到适合作为内置程序的水平。 在实践中建立使用模式之前,标准库包将是一种更合适的方法。 如果try以这种方式进入语言,我们会在许多不同的地方使用它。

更笼统地说,Go 将非常稳定的核心语言和非常丰富的标准库相结合,值得保留。 语言团队在核心语言变化上的进展越慢越好。 x -> stdlib管道对于这类事情仍然是一种强有力的方法。

@griesemer啊,对不起。 我误读了统计数据,它使用“if err != nil statements”计数器(123)作为分母,而不是“尝试候选人”计数器(64)作为分母。 我会提出这个问题。

谢谢!

@mattpalmer使用模式已经建立了大约十年。 正是这些确切的使用模式直接影响了try的设计。 你指的是什么使用模式?

@griesemer抱歉,那是我的错——在我的脑海中开始解释try让我感到困扰的事情变成了它自己的提议,以表明我不添加它的观点。 这显然违反了规定的基本规则(更不用说与这个新内置函数的提议不同,它引入了一个新的运算符)。 删除评论以简化对话是否有帮助(或者这被认为是糟糕的形式)?

@subfuzion我不会担心的。 这是一个有争议的建议,并且有很多建议。 很多都很古怪

我们已经对该设计进行了多次迭代,并征求了许多人的反馈,然后我们才觉得可以发布它并建议将其推进到实际的实验阶段,但我们还没有完成实验。 如果实验失败,或者如果反馈提前告诉我们它显然会失败,那么回到绘图板确实是有意义的。

@griesemer您能否详细说明团队将用于确定实验成功或失败的具体指标?

@我和

我刚才问过@rsc (https://github.com/golang/go/issues/32437#issuecomment-503245958):

@rsc
将不乏可以放置这种便利的位置。 除此之外,正在寻求什么指标来证明机制的实质? 是否有分类错误处理案例的列表? 当大部分公共流程由情绪驱动时,如何从数据中获取价值?

答案是有目的的,但缺乏启发性且缺乏实质内容(https://github.com/golang/go/issues/32437#issuecomment-503295558):

该决定基于它在实际程序中的运行情况。 如果人们告诉我们尝试在他们的大部分代码中是无效的,那就是重要的数据。 这个过程是由这种数据驱动的。 它不是由情绪驱动的。

提供了额外的情绪(https://github.com/golang/go/issues/32437#issuecomment-503408184):

我惊讶地发现try以一种以前从未讨论过的方式导致明显更好的代码的情况。

最终,我回答了我自己的问题“是否有分类错误处理案例的列表?”。 将有 6 种有效的错误处理模式 - 手动直接、手动传递、手动间接、自动直接、自动传递、自动间接。 目前,通常只使用其中两种模式。 间接模式,在促进方面投入了大量的努力,对于大多数资深的 Gophers 来说似乎是非常令人望而却步的,而且这种担忧似乎被忽略了。 (https://github.com/golang/go/issues/32437#issuecomment-507332843)。

此外,我建议在转换之前对自动转换进行审查,以确保结果的价值(https://github.com/golang/go/issues/32437#issuecomment-507497656)。 值得庆幸的是,随着时间的推移,提供的更多结果似乎确实具有更好的回顾性,但这仍然没有以清醒和协调的方式解决间接方法的影响。 毕竟(在我看来),就像用户应该被视为敌对一样,开发人员也应该被视为懒惰。

还指出了当前方法未能错过有价值的候选人(https://github.com/golang/go/issues/32437#issuecomment-507505243)。

我认为这个过程普遍缺乏,尤其是音盲,值得一提。

@iand @rsc给出答案仍然有效。 我不确定该答案的哪一部分是“缺乏实质内容”或“鼓舞人心”需要什么。 但是让我尝试添加更多“实质”:

提案评估流程的目的是最终确定“变更是否带来了预期收益或产生了任何意外成本”(流程中的第 5 步)。

我们已经通过了第 1 步:Go 团队选择了看起来值得接受的具体提案; 该提案就是其中之一。 如果我们没有认真考虑并认为它值得,我们就不会选择它。 具体来说,我们确实认为 Go 代码中有大量仅与错误处理相关的样板。 该提案也不是凭空而来的——我们已经以各种形式讨论了一年多。

我们目前处于第 2 步,因此距离最终决定还有相当大的距离。 第 2 步是收集反馈和疑虑——似乎有很多。 但在这里要明确一点:到目前为止,只有一条评论指出了设计的_技术_缺陷,我们对此进行了纠正。 也有不少评论基于真实代码的具体数据,这表明try确实会减少样板并简化代码; 还有一些评论——同样基于真实代码的数据——表明try并没有多大帮助。 这种基于实际数据或指出技术缺陷的具体反馈是可行的,非常有帮助。 我们绝对会考虑到这一点。

然后有大量评论基本上是个人情绪。 这是不太可操作的。 这并不是说我们忽略了它。 但仅仅因为我们坚持这个过程并不意味着我们是“聋哑人”。

关于这些评论:这个提议可能有两到三打反对者——你知道你是谁。 他们以频繁的帖子主导了这场讨论,有时甚至一天发布多个帖子。 从中获得的新信息很少。 帖子数量的增加也不反映社区“更强烈”的情绪; 这只是意味着这些人比其他人更有发言权。

@iand @rsc给出答案仍然有效。 我不确定该答案的哪一部分是“缺乏实质内容”或“鼓舞人心”需要什么。 但是让我尝试添加更多“实质”:

@griesemer我确定这是无意的,但我想指出,您引用的所有词都不是我的,而是后来评论者的。

除此之外,我希望除了减少样板和简化try的成功之外,还可以根据它是否允许我们编写更好、更清晰的代码来判断。

@iand确实 - 这只是我的疏忽。 我很抱歉。

我们确实相信try确实允许我们编写更具可读性的代码 - 我们从真实代码和我们自己对tryhard的实验中获得的大部分证据都显示出显着的清理效果。 但可读性更主观,更难量化。

@griesemer

你指的是什么使用模式?

我指的是随着时间的推移将围绕try发展的使用模式,而不是现有的用于处理错误的 nil-check 模式。 误用和滥用的可能性是一个很大的未知数,尤其是在不断涌入的程序员中,他们在其他语言中使用了语义不同版本的 try-catch。

所有这些以及对核心语言长期稳定性的考虑让我认为在 x 包或标准库级别引入此功能(作为包errors/try或作为errors.Try() ) 比将其作为内置引入更好。

@mattparlmer如果我错了,请纠正我,但我相信这个提议必须在 Go 运行时中才能使用 g's、m's(需要覆盖执行流程)。

@fabian-f

@mattparlmer如果我错了,请纠正我,但我相信这个提议必须在 Go 运行时中才能使用 g's、m's(需要覆盖执行流程)。

事实并非如此。 正如设计文档所指出的,它可以作为编译时语法树转换来实现。

这是可能的,因为try的语义可以完全用ifreturn表示; 它并没有像ifreturn那样真正“覆盖执行流程”。

这是我公司 30 万行 Go 代码库中的tryhard报告:

初始运行:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
    453 ( 10.1% of    4496) try candidates
      4 (  0.1% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3066 ( 68.2% of    4496) { return ... zero values ..., expr }
    356 (  7.9% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

我们约定使用 juju 的 errgo 包 (https://godoc.org/github.com/juju/errgo) 来屏蔽错误并向它们添加堆栈跟踪信息,这将防止大多数重写发生。 这确实意味着我们不太可能采用try ,原因与我们通常避免裸错误返回的原因相同。

因为它看起来可能是一个有用的指标,所以我删除errgo.Mask()调用(它返回没有注释的错误)并重新运行tryhard 。 这是对如果我们不使用 errgo 可以重写多少错误检查的估计:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
   3114 ( 69.3% of    4496) try candidates
      7 (  0.2% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
    381 (  8.5% of    4496) { return ... zero values ..., expr }
    358 (  8.0% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

所以,我猜大约 70% 的错误返回将与try兼容。

最后,我对提案的主要关注似乎并没有体现在我阅读的任何评论和讨论摘要中:

该提议显着增加了注释错误的相对成本。

目前,为错误添加一些上下文的边际成本非常低; 它只不过是输入格式字符串而已。 如果这个提议被采纳,我担心工程师会越来越喜欢try提供的美学,因为它使他们的代码“看起来更时尚”(我很遗憾地说这是一些人的考虑,根据我的经验),现在需要一个额外的块来添加上下文。 他们可以基于“可读性”论点来证明它的合理性,添加上下文如何将方法扩展另外 3 行并分散读者的注意力。 我认为公司代码库与 Go 标准库不同,因为让做正确的事情变得容易可能会对最终的代码质量产生可衡量的影响,代码审查的质量参差不齐,团队实践彼此独立变化. 无论如何,正如你之前所说,我们总是不能为我们的代码库采用try

感谢您的考虑

@mattparlmer

所有这些以及对核心语言长期稳定性的考虑使我认为在 x 包或标准库的级别引入此功能(作为包errors/try或作为errors.Try() ) 比将它作为内置引入更好。

try不能作为库函数实现; 函数无法从其调用者返回(启用已被提议为 #32473),并且与大多数其他内置函数一样,也无法在 Go 中表达try的签名。 即使使用泛型,这也不太可能; 请参阅设计文档常见问题解答,接近尾声。

此外,将try实现为库函数将要求它具有更详细的名称,这在一定程度上违背了使用它的意义。

但是,它可以 - 并且已经两次 - 实现为源代码预处理器:请参阅https://github.com/rhysd/trygohttps://github.com/lunixbochs/og。

看起来大约 60% 的来自 tegola 的代码库能够利用这个特性。

这是 tegola 项目的 tryhard 输出:(http://github.com/go-spatial/tegola)

--- try candidates ---
      1  tegola/atlas/atlas.go:84
      2  tegola/atlas/map.go:232
      3  tegola/atlas/map.go:238
      4  tegola/atlas/map.go:248
      5  tegola/atlas/map.go:253
      6  tegola/basic/geometry_math.go:248
      7  tegola/basic/geometry_math.go:251
      8  tegola/basic/geometry_math.go:268
      9  tegola/basic/geometry_math.go:276
     10  tegola/basic/json_marshal.go:33
     11  tegola/basic/json_marshal.go:153
     12  tegola/basic/json_marshal.go:276
     13  tegola/cache/azblob/azblob.go:54
     14  tegola/cache/azblob/azblob.go:61
     15  tegola/cache/azblob/azblob.go:67
     16  tegola/cache/azblob/azblob.go:74
     17  tegola/cache/azblob/azblob.go:80
     18  tegola/cache/azblob/azblob.go:105
     19  tegola/cache/azblob/azblob.go:109
     20  tegola/cache/azblob/azblob.go:204
     21  tegola/cache/azblob/azblob.go:259
     22  tegola/cache/file/file.go:42
     23  tegola/cache/file/file.go:56
     24  tegola/cache/file/file.go:110
     25  tegola/cache/file/file.go:116
     26  tegola/cache/file/file.go:129
     27  tegola/cache/redis/redis.go:41
     28  tegola/cache/redis/redis.go:46
     29  tegola/cache/redis/redis.go:51
     30  tegola/cache/redis/redis.go:56
     31  tegola/cache/redis/redis.go:70
     32  tegola/cache/redis/redis.go:79
     33  tegola/cache/redis/redis.go:84
     34  tegola/cache/s3/s3.go:85
     35  tegola/cache/s3/s3.go:102
     36  tegola/cache/s3/s3.go:112
     37  tegola/cache/s3/s3.go:118
     38  tegola/cache/s3/s3.go:123
     39  tegola/cache/s3/s3.go:138
     40  tegola/cache/s3/s3.go:164
     41  tegola/cache/s3/s3.go:172
     42  tegola/cache/s3/s3.go:179
     43  tegola/cache/s3/s3.go:284
     44  tegola/cache/s3/s3.go:340
     45  tegola/cmd/tegola/cmd/cache/format.go:97
     46  tegola/cmd/tegola/cmd/cache/seed_purge.go:94
     47  tegola/cmd/tegola/cmd/cache/seed_purge.go:103
     48  tegola/cmd/tegola/cmd/cache/seed_purge.go:170
     49  tegola/cmd/tegola/cmd/cache/tile_list.go:51
     50  tegola/cmd/tegola/cmd/cache/tile_list.go:64
     51  tegola/cmd/tegola/cmd/cache/tile_name.go:35
     52  tegola/cmd/tegola/cmd/cache/tile_name.go:43
     53  tegola/cmd/tegola/cmd/root.go:58
     54  tegola/cmd/tegola/cmd/root.go:61
     55  tegola/cmd/xyz2svg/cmd/draw.go:62
     56  tegola/cmd/xyz2svg/cmd/draw.go:70
     57  tegola/cmd/xyz2svg/cmd/draw.go:214
     58  tegola/config/config.go:96
     59  tegola/internal/env/parse.go:30
     60  tegola/internal/env/parse.go:69
     61  tegola/internal/env/parse.go:116
     62  tegola/internal/env/parse.go:174
     63  tegola/internal/env/parse.go:221
     64  tegola/internal/env/types.go:67
     65  tegola/internal/env/types.go:86
     66  tegola/internal/env/types.go:105
     67  tegola/internal/env/types.go:124
     68  tegola/internal/env/types.go:143
     69  tegola/maths/makevalid/main.go:189
     70  tegola/maths/makevalid/main.go:207
     71  tegola/maths/makevalid/main.go:221
     72  tegola/maths/makevalid/main.go:295
     73  tegola/maths/makevalid/main.go:504
     74  tegola/maths/makevalid/makevalid.go:77
     75  tegola/maths/makevalid/makevalid.go:89
     76  tegola/maths/makevalid/makevalid.go:118
     77  tegola/maths/makevalid/makevalid_test.go:93
     78  tegola/maths/makevalid/makevalid_test.go:163
     79  tegola/maths/makevalid/plyg/ring.go:518
     80  tegola/maths/triangle.go:1023
     81  tegola/mvt/layer.go:73
     82  tegola/mvt/layer.go:79
     83  tegola/mvt/vector_tile/vector_tile.pb.go:64
     84  tegola/provider/gpkg/gpkg.go:138
     85  tegola/provider/gpkg/gpkg.go:223
     86  tegola/provider/gpkg/gpkg_register.go:46
     87  tegola/provider/gpkg/gpkg_register.go:51
     88  tegola/provider/gpkg/gpkg_register.go:186
     89  tegola/provider/gpkg/gpkg_register.go:227
     90  tegola/provider/gpkg/gpkg_register.go:240
     91  tegola/provider/gpkg/gpkg_register.go:245
     92  tegola/provider/gpkg/gpkg_register.go:256
     93  tegola/provider/gpkg/gpkg_register.go:377
     94  tegola/provider/postgis/postgis.go:112
     95  tegola/provider/postgis/postgis.go:117
     96  tegola/provider/postgis/postgis.go:122
     97  tegola/provider/postgis/postgis.go:127
     98  tegola/provider/postgis/postgis.go:136
     99  tegola/provider/postgis/postgis.go:142
    100  tegola/provider/postgis/postgis.go:148
    101  tegola/provider/postgis/postgis.go:153
    102  tegola/provider/postgis/postgis.go:158
    103  tegola/provider/postgis/postgis.go:163
    104  tegola/provider/postgis/postgis.go:181
    105  tegola/provider/postgis/postgis.go:198
    106  tegola/provider/postgis/postgis.go:264
    107  tegola/provider/postgis/postgis.go:441
    108  tegola/provider/postgis/postgis.go:446
    109  tegola/provider/postgis/postgis.go:529
    110  tegola/provider/postgis/postgis.go:559
    111  tegola/provider/postgis/postgis.go:603
    112  tegola/provider/postgis/util.go:31
    113  tegola/provider/postgis/util.go:36
    114  tegola/provider/postgis/util.go:200
    115  tegola/server/bindata/bindata.go:89
    116  tegola/server/bindata/bindata.go:109
    117  tegola/server/bindata/bindata.go:129
    118  tegola/server/bindata/bindata.go:149
    119  tegola/server/bindata/bindata.go:169
    120  tegola/server/bindata/bindata.go:189
    121  tegola/server/bindata/bindata.go:209
    122  tegola/server/bindata/bindata.go:229
    123  tegola/server/bindata/bindata.go:370
    124  tegola/server/bindata/bindata.go:374
    125  tegola/server/bindata/bindata.go:378
    126  tegola/server/bindata/bindata.go:382
    127  tegola/server/bindata/bindata.go:386
    128  tegola/server/bindata/bindata.go:402
    129  tegola/server/middleware_gzip.go:71
    130  tegola/server/middleware_gzip.go:78
    131  tegola/server/server_test.go:85

--- <err> name is different from "err" ---
      1  tegola/basic/json_marshal.go:276

--- { return ... zero values ..., expr } ---
      1  tegola/basic/geometry_math.go:214
      2  tegola/basic/geometry_math.go:222
      3  tegola/basic/geometry_math.go:230
      4  tegola/cache/azblob/azblob.go:131
      5  tegola/cache/azblob/azblob.go:140
      6  tegola/cache/azblob/azblob.go:149
      7  tegola/cache/azblob/azblob.go:171
      8  tegola/cache/file/file.go:47
      9  tegola/cache/s3/s3.go:92
     10  tegola/cmd/internal/register/maps.go:108
     11  tegola/cmd/tegola/cmd/cache/flags.go:20
     12  tegola/cmd/tegola/cmd/cache/tile_name.go:51
     13  tegola/cmd/tegola/cmd/cache/worker.go:112
     14  tegola/cmd/tegola/cmd/cache/worker.go:123
     15  tegola/cmd/tegola/cmd/root.go:73
     16  tegola/cmd/tegola/cmd/root.go:78
     17  tegola/cmd/xyz2svg/cmd/root.go:60
     18  tegola/provider/gpkg/gpkg.go:90
     19  tegola/provider/gpkg/gpkg.go:95
     20  tegola/provider/gpkg/gpkg_register.go:264
     21  tegola/provider/gpkg/gpkg_register.go:297
     22  tegola/provider/gpkg/gpkg_register.go:302
     23  tegola/provider/gpkg/gpkg_register.go:313
     24  tegola/provider/gpkg/gpkg_register.go:328
     25  tegola/provider/postgis/postgis.go:193
     26  tegola/provider/postgis/postgis.go:208
     27  tegola/provider/postgis/postgis.go:222
     28  tegola/provider/postgis/postgis.go:228
     29  tegola/provider/postgis/postgis.go:234
     30  tegola/provider/postgis/postgis.go:243
     31  tegola/provider/postgis/postgis.go:249
     32  tegola/provider/postgis/postgis.go:255
     33  tegola/provider/postgis/postgis.go:304
     34  tegola/provider/postgis/postgis.go:315
     35  tegola/provider/postgis/postgis.go:319
     36  tegola/provider/postgis/postgis.go:364
     37  tegola/provider/postgis/postgis.go:456
     38  tegola/provider/postgis/postgis.go:520
     39  tegola/provider/postgis/postgis.go:534
     40  tegola/provider/postgis/postgis.go:565
     41  tegola/provider/postgis/util.go:108
     42  tegola/provider/postgis/util.go:113
     43  tegola/server/bindata/bindata.go:29
     44  tegola/server/bindata/bindata.go:245
     45  tegola/server/bindata/bindata.go:271
     46  tegola/server/bindata/bindata.go:396

--- single statement then branch ---
      1  tegola/cache/azblob/azblob.go:241
      2  tegola/cache/file/file.go:87
      3  tegola/cache/s3/s3.go:321
      4  tegola/cmd/internal/register/caches.go:18
      5  tegola/cmd/internal/register/providers.go:43
      6  tegola/cmd/internal/register/providers.go:62
      7  tegola/cmd/internal/register/providers.go:75
      8  tegola/config/config.go:192
      9  tegola/config/config.go:207
     10  tegola/config/config.go:217
     11  tegola/internal/env/dict.go:43
     12  tegola/internal/env/dict.go:121
     13  tegola/internal/env/dict.go:197
     14  tegola/internal/env/dict.go:273
     15  tegola/internal/env/dict.go:348
     16  tegola/internal/env/parse.go:79
     17  tegola/internal/env/parse.go:126
     18  tegola/internal/env/parse.go:184
     19  tegola/internal/env/parse.go:231
     20  tegola/maths/makevalid/plyg/ring.go:541
     21  tegola/maths/maths.go:239
     22  tegola/maths/validate/validate.go:49
     23  tegola/maths/validate/validate.go:53
     24  tegola/maths/validate/validate.go:59
     25  tegola/maths/validate/validate.go:69
     26  tegola/mvt/feature.go:94
     27  tegola/mvt/feature.go:99
     28  tegola/mvt/feature.go:592
     29  tegola/mvt/feature.go:603
     30  tegola/mvt/layer.go:90
     31  tegola/mvt/tile.go:48
     32  tegola/provider/postgis/postgis.go:570
     33  tegola/provider/postgis/postgis.go:586
     34  tegola/tile.go:172

--- complex then branch; cannot use try ---
      1  tegola/cache/azblob/azblob.go:226
      2  tegola/cache/file/file.go:78
      3  tegola/cache/file/file.go:122
      4  tegola/cache/s3/s3.go:195
      5  tegola/cache/s3/s3.go:206
      6  tegola/cache/s3/s3.go:219
      7  tegola/cache/s3/s3.go:307
      8  tegola/provider/gpkg/gpkg.go:39
      9  tegola/provider/gpkg/gpkg.go:45
     10  tegola/provider/gpkg/gpkg.go:131
     11  tegola/provider/gpkg/gpkg.go:154
     12  tegola/provider/gpkg/gpkg_register.go:171
     13  tegola/provider/gpkg/gpkg_register.go:195

--- stats ---
   1294 (100.0% of    1294) func declarations
    246 ( 19.0% of    1294) func declarations returning an error
   2693 (100.0% of    2693) statements
    551 ( 20.5% of    2693) if statements
    238 ( 43.2% of     551) if <err> != nil statements
    131 ( 55.0% of     238) try candidates
      1 (  0.4% of     238) <err> name is different from "err"
--- non-try candidates ---
     46 ( 19.3% of     238) { return ... zero values ..., expr }
     34 ( 14.3% of     238) single statement then branch
     13 (  5.5% of     238) complex then branch; cannot use try
      0 (  0.0% of     238) non-empty else branch; cannot use try

以及配套项目:(http://github.com/go-spatial/geom)

--- try candidates ---
      1  geom/bbox.go:202
      2  geom/encoding/geojson/geojson.go:152
      3  geom/encoding/geojson/geojson.go:157
      4  geom/encoding/wkb/internal/tcase/symbol/symbol.go:73
      5  geom/encoding/wkb/internal/tcase/tcase.go:161
      6  geom/encoding/wkb/internal/tcase/tcase.go:172
      7  geom/encoding/wkb/wkb.go:50
      8  geom/encoding/wkb/wkb.go:110
      9  geom/encoding/wkt/internal/token/token.go:176
     10  geom/encoding/wkt/internal/token/token.go:252
     11  geom/internal/parsing/parsing.go:44
     12  geom/internal/parsing/parsing.go:85
     13  geom/internal/rtreego/rtree_test.go:110
     14  geom/multi_line_string.go:34
     15  geom/multi_polygon.go:35
     16  geom/planar/clip/linestring.go:82
     17  geom/planar/clip/linestring.go:181
     18  geom/planar/clip/point.go:23
     19  geom/planar/intersect/xsweep.go:106
     20  geom/planar/makevalid/makevalid.go:92
     21  geom/planar/makevalid/makevalid.go:191
     22  geom/planar/makevalid/setdiff/polygoncleaner.go:283
     23  geom/planar/makevalid/setdiff/polygoncleaner.go:345
     24  geom/planar/makevalid/setdiff/polygoncleaner.go:543
     25  geom/planar/makevalid/setdiff/polygoncleaner.go:554
     26  geom/planar/makevalid/setdiff/polygoncleaner.go:572
     27  geom/planar/makevalid/setdiff/polygoncleaner.go:578
     28  geom/planar/simplify/douglaspeucker.go:84
     29  geom/planar/simplify/douglaspeucker.go:88
     30  geom/planar/simplify.go:13
     31  geom/planar/triangulate/constraineddelaunay/triangle.go:186
     32  geom/planar/triangulate/constraineddelaunay/triangulator.go:134
     33  geom/planar/triangulate/constraineddelaunay/triangulator.go:138
     34  geom/planar/triangulate/constraineddelaunay/triangulator.go:142
     35  geom/planar/triangulate/constraineddelaunay/triangulator.go:173
     36  geom/planar/triangulate/constraineddelaunay/triangulator.go:176
     37  geom/planar/triangulate/constraineddelaunay/triangulator.go:203
     38  geom/planar/triangulate/constraineddelaunay/triangulator.go:248
     39  geom/planar/triangulate/constraineddelaunay/triangulator.go:396
     40  geom/planar/triangulate/constraineddelaunay/triangulator.go:466
     41  geom/planar/triangulate/constraineddelaunay/triangulator.go:553
     42  geom/planar/triangulate/constraineddelaunay/triangulator.go:583
     43  geom/planar/triangulate/constraineddelaunay/triangulator.go:667
     44  geom/planar/triangulate/constraineddelaunay/triangulator.go:672
     45  geom/planar/triangulate/constraineddelaunay/triangulator.go:677
     46  geom/planar/triangulate/constraineddelaunay/triangulator.go:814
     47  geom/planar/triangulate/constraineddelaunay/triangulator.go:818
     48  geom/planar/triangulate/constraineddelaunay/triangulator.go:823
     49  geom/planar/triangulate/constraineddelaunay/triangulator.go:865
     50  geom/planar/triangulate/constraineddelaunay/triangulator.go:870
     51  geom/planar/triangulate/constraineddelaunay/triangulator.go:875
     52  geom/planar/triangulate/constraineddelaunay/triangulator.go:897
     53  geom/planar/triangulate/constraineddelaunay/triangulator.go:901
     54  geom/planar/triangulate/constraineddelaunay/triangulator.go:907
     55  geom/planar/triangulate/constraineddelaunay/triangulator.go:1107
     56  geom/planar/triangulate/constraineddelaunay/triangulator.go:1146
     57  geom/planar/triangulate/constraineddelaunay/triangulator.go:1157
     58  geom/planar/triangulate/constraineddelaunay/triangulator.go:1202
     59  geom/planar/triangulate/constraineddelaunay/triangulator.go:1206
     60  geom/planar/triangulate/constraineddelaunay/triangulator.go:1216
     61  geom/planar/triangulate/delaunaytriangulationbuilder.go:66
     62  geom/planar/triangulate/incrementaldelaunaytriangulator.go:46
     63  geom/planar/triangulate/incrementaldelaunaytriangulator.go:78
     64  geom/planar/triangulate/quadedge/lastfoundquadedgelocator.go:65
     65  geom/planar/triangulate/quadedge/quadedgesubdivision.go:976
     66  geom/slippy/tile.go:133

--- { return ... zero values ..., expr } ---
      1  geom/internal/parsing/parsing.go:125
      2  geom/planar/triangulate/constraineddelaunay/triangulator.go:428
      3  geom/planar/triangulate/constraineddelaunay/triangulator.go:447
      4  geom/planar/triangulate/constraineddelaunay/triangulator.go:460

--- single statement then branch ---
      1  geom/bbox.go:259
      2  geom/encoding/wkb/internal/decode/decode.go:29
      3  geom/encoding/wkb/internal/decode/decode.go:55
      4  geom/encoding/wkb/internal/decode/decode.go:63
      5  geom/encoding/wkb/internal/decode/decode.go:70
      6  geom/encoding/wkb/internal/decode/decode.go:79
      7  geom/encoding/wkb/internal/decode/decode.go:84
      8  geom/encoding/wkb/internal/decode/decode.go:93
      9  geom/encoding/wkb/internal/decode/decode.go:99
     10  geom/encoding/wkb/internal/decode/decode.go:105
     11  geom/encoding/wkb/internal/decode/decode.go:114
     12  geom/encoding/wkb/internal/decode/decode.go:119
     13  geom/encoding/wkb/internal/decode/decode.go:135
     14  geom/encoding/wkb/internal/decode/decode.go:140
     15  geom/encoding/wkb/internal/decode/decode.go:149
     16  geom/encoding/wkb/internal/decode/decode.go:155
     17  geom/encoding/wkb/internal/decode/decode.go:161
     18  geom/encoding/wkb/internal/decode/decode.go:170
     19  geom/encoding/wkb/internal/decode/decode.go:176
     20  geom/encoding/wkb/internal/tcase/token/token.go:162
     21  geom/encoding/wkt/internal/token/token.go:136

--- complex then branch; cannot use try ---
      1  geom/encoding/wkb/internal/tcase/tcase.go:74
      2  geom/encoding/wkt/internal/symbol/symbol.go:125
      3  geom/planar/intersect/xsweep.go:165
      4  geom/planar/makevalid/makevalid.go:85
      5  geom/planar/makevalid/makevalid.go:172
      6  geom/planar/makevalid/triangulate.go:19
      7  geom/planar/makevalid/triangulate.go:28
      8  geom/planar/makevalid/triangulate.go:36
      9  geom/planar/makevalid/triangulate.go:58
     10  geom/planar/triangulate/constraineddelaunay/triangulator.go:358
     11  geom/planar/triangulate/constraineddelaunay/triangulator.go:373
     12  geom/planar/triangulate/constraineddelaunay/triangulator.go:453
     13  geom/planar/triangulate/constraineddelaunay/triangulator.go:1237
     14  geom/planar/triangulate/constraineddelaunay/triangulator.go:1243
     15  geom/planar/triangulate/constraineddelaunay/triangulator.go:1249

--- stats ---
    820 (100.0% of     820) func declarations
    146 ( 17.8% of     820) func declarations returning an error
   1715 (100.0% of    1715) statements
    391 ( 22.8% of    1715) if statements
    111 ( 28.4% of     391) if <err> != nil statements
     66 ( 59.5% of     111) try candidates
      0 (  0.0% of     111) <err> name is different from "err"
--- non-try candidates ---
      4 (  3.6% of     111) { return ... zero values ..., expr }
     21 ( 18.9% of     111) single statement then branch
     15 ( 13.5% of     111) complex then branch; cannot use try
      0 (  0.0% of     111) non-empty else branch; cannot use try

关于意外成本的问题,我从 #32611 重新发布...

我看到三类成本:

  1. 规范的成本,在设计文档中详细说明。
  2. 工具成本(即软件修订)也在设计文档中进行了探讨。
  3. 生态系统的成本,社区在上面和 #32825 中详细说明了这一点。

重新编号 1 和 2, try()的成本适中。

过于简单化没有。 3,大多数评论者认为try()会破坏我们的代码和/或我们所依赖的代码生态系统,从而降低我们的生产力和产品质量。 这种广泛而合理的看法不应被贬低为“非事实”或“审美”。

生态系统的成本远比规范或工具的成本重要。

@griesemer声称“三打声音反对者”是反对派的主体,这显然是不公平的。 数百人在此处和 #32825 中发表了评论。 你在 6 月 12 日告诉我,“我知道大约 2/3 的受访者对提案不满意。” 从那时起,超过 2,000 人以 90% 的赞许投票“别管err != nil ”。

@gdey您能否修改您的帖子以仅包含 _stats & non-try Candidates_ ?

@robfig@gdey感谢您提供这些数据,尤其是之前/之后的比较。

@griesemer
您当然添加了一些内容,以澄清我(和其他人)的担忧可能会得到直接解决。 那么,我的问题是,Go 团队是否确实将间接模式的可能滥用(即裸返回和/或通过 defer 的函数后范围错误突变)视为值得在步骤 5 中讨论的成本,并且可能值得采取行动来缓解。 目前的情绪是,提案中最令人不安的方面被 Go 团队视为一个聪明/新颖的功能(这个问题没有通过自动化转换的评估得到解决,并且似乎受到积极的鼓励/支持。 - errd ,在谈话中等)。

编辑以添加... Go 团队鼓励资深 Gophers 认为令人望而却步的担忧正是我对音聋的意思。
... 间接成本是我们许多人深切关注的体验痛苦问题。 它可能不是可以轻松进行基准测试的东西(如果完全合理的话),但将这种担忧视为感性本身是不诚实的。 相反,无视共享经验的智慧而支持没有可靠上下文判断的简单数字是我/我们正在努力反对的那种情绪。

@networkimprov对不够清楚表示歉意。 我的是:

这个提议可能有两到三打的声音反对者——你知道你是谁。 他们以频繁的帖子主导了这场讨论,有时甚至一天发布多个帖子。

我说的是实际评论(如“常见帖子”),而不是表情符号。 只有相对较少的人在这里_repeatedly_发帖,我相信这仍然是正确的。 我也不是在谈论#32825; 我在谈论这个提议。

从 emoji 来看,情况与一个月前几乎没有变化:1/3 的 emoji 表示赞成,2/3 表示反对。

@griesemer

我在上面写评论时想起了一些事情:虽然设计文档说try可以实现为简单的语法树转换,并且在许多情况下显然是这种情况,但在某些情况下我不这样做看到一个简单的方法。 例如,假设我们有以下内容:

switch x {
case rand.Int():
  a()
case 5, try(strconv.Atoi(y)):
  b()
}

鉴于switch的评估顺序,我看不出如何在保留预期语义的同时轻松地将strconv.Atoi(y)case子句中取出; 我能想到的最好的方法是将switch重写为if / else语句的等效链,如下所示:

if x == rand.Int() {
  a()
} else if x == 5 {
  b()
} else if _v, _err := strconv.Atoi(y); _err != nil {
  return _err
} else if x == _v {
  b()
}

(还有其他情况会出现这种情况,但这是最简单的例子之一,也是我想到的第一个例子。)

实际上,在您发布此提案之前,我一直在研究 AST 转换器以实现设计草案中的check运算符并遇到了这个问题。 但是,我使用的是go/* stdlib 软件包的黑客版本; 也许编译器前端的结构使这更容易? 还是我错过了什么,真的有一种简单的方法可以做到这一点?

另见https://github.com/rhysd/trygo; 根据自述文件,它没有实现try表达式,并注意到我在这里提出的基本相同的问题; 我怀疑这可能是作者没有实现该功能的原因。

@daved Professional 代码不是在真空中开发的——有本地约定、样式推荐、代码审查等(我之前已经说过)。 因此,我不明白为什么滥用会“可能”(这是可能的,但对于任何语言结构都是如此)。

请注意,无论是否使用 $#$ try $#$ ,都可以使用defer来修饰错误。 包含许多错误检查的函数当然有充分的理由,所有这些错误检查都以相同的方式装饰错误,进行一次装饰,例如使用defer 。 或者也许使用一个包装函数来进行装饰。 或任何其他符合法案和本地编码建议的机制。 毕竟,“错误只是值”,编写和分解处理错误的代码是完全有意义的。

当以无纪律的方式使用时,赤裸裸的回报可能会出现问题。 这并不意味着它们通常是坏的。 例如,如果一个函数的结果只有在没有错误的情况下才有效,那么在出现错误的情况下使用裸返回似乎完全没问题——只要我们在设置错误方面有纪律(因为其他返回值没有'在这种情况下无关紧要)。 try确保了这一点。 我在这里看不到任何“滥用”。

@dpinela编译器已经将您的switch语句翻译为if-else-if的序列,所以我在这里看不到问题。 此外,编译器使用的“语法树”不是“go/ast”语法树。 编译器的内部表示允许更灵活的代码,这些代码不一定会被翻译回 Go。

@griesemer
是的,当然,你所说的一切都是有根据的。 然而,灰色区域并不像你想象的那么简单。 我们这些教导他人的人(我们努力发展/促进社区)通常会非常谨慎地对待赤裸裸的回报。 我很欣赏 stdlib 到处乱扔垃圾。 但是,在教别人时,总是强调明确的回报。 让个人达到自己的成熟度以转向更“幻想”的方法,但从一开始就鼓励它肯定会培养难以阅读的代码(即坏习惯)。 这又是我试图揭示的音盲。

就个人而言,我不希望禁止裸退货或递延价值操纵。 当它们真正适合时,我很高兴这些功能可用(尽管其他有经验的用户可能会采取更严格的立场)。 尽管如此,以如此普遍的方式鼓励应用这些不太常见且通常很脆弱的功能,这与我想象的 Go 采取的方向完全相反。 避免魔法和不稳定的间接形式的明显变化是有目的的转变吗? 我们是否也应该开始强调使用 DIC 和其他难以调试的机制?

ps 非常感谢您的时间。 你的团队和语言有我的尊重和关心。 我不希望任何人大声疾呼; 我希望您能听到我/我们关注的本质,并尝试从我们的“前线”角度看待问题。

在我的反对票中添加一些评论。

对于手头的具体提案:

1)我非常希望这是一个关键字而不是一个内置函数,因为之前明确表达了控制流和代码可读性的原因。

2)语义上,“try”是避雷针。 而且,除非抛出异常,否则“try”最好重命名为guardensure之类的名称。

3)除了这两点,我认为这是我见过的最好的建议。

还有一些评论表达了我对任何添加try/guard/ensure概念的反对意见,而不是单独留下if err != nil

1)这与 golang 的原始任务之一背道而驰(至少在我看来)是明确的,易于阅读/理解的,几乎没有“魔法”。

2)这将在需要思考的确切时刻鼓励懒惰:“在出现此错误的情况下,我的代码最好做的事情是什么?”。 在执行“样板”操作(例如打开文件、通过网络传输数据等)时可能会出现许多错误。虽然您可能会从一堆忽略非常见故障场景的“尝试”开始,但最终其中很多“ trys" 将消失,因为您可能需要实现自己的退避/重试、日志记录/跟踪和/或清理任务。 “低概率事件”在规模上得到保证。

这里有一些更原始的tryhard统计数据。 这只是经过轻微验证,因此请随时指出错误。 ;-)

godoc.org 上的前 20 个“流行包”

这些是对应于https://godoc.org上前 20 个热门软件包的存储库,按尝试候选百分比排序。 这是使用默认的tryhard设置,理论上应该排除vendor目录。

这 20 个存储库中尝试候选的中值为 58%。

| 项目 | 位置 | 如果 stmts | 如果!= nil(如果的百分比)| 尝试候选人(如果!= nil 的百分比)|
|---------|-----|--------------|-- -----|---------------
| github.com/google/uuid | 1714 | 12 | 16.7% | 0.0% |
| github.com/pkg/errors | 1886 | 10 | 0.0% | 0.0% |
| github.com/aws/aws-sdk-go | 1911309 | 32015 | 9.4% | 8.9% |
| github.com/jinzhu/gorm | 15246 | 44 | 11.4% | 20.0% |
| github.com/robfig/cron | 1911 | 20 | 35.0% | 28.6% |
| github.com/gorilla/websocket | 6959 | 212 | 32.5% | 39.1% |
| github.com/dgrijalva/jwt-go | 3270 | 118 | 29.7% | 40.0% |
| github.com/gomodule/redigo | 7119 | 187 | 34.8% | 41.5% |
| github.com/unixpickle/kahoot-hack | 1743 | 52 | 75.0% | 43.6% |
| github.com/lib/pq | 13396 | 239 | 30.1% | 55.6% |
| github.com/sirupsen/logrus | 5063 | 29 | 17.2% | 60.0% |
| github.com/prometheus/client_golang | 17791 | 194 | 49.0% | 62.1% |
| github.com/go-redis/redis | 21182 | 326 | 42.6% | 73.4% |
| github.com/mongodb/mongo-go-driver | 86605 | 2097 | 37.8% | 73.9% |
| github.com/uber-go/zap | 15363 | 84 | 36.9% | 74.2% |
| github.com/golang/protobuf | 42959 | 685 | 22.9% | 77.1% |
| github.com/gin-gonic/gin | 14574 | 96 | 53.1% | 86.3% |
| github.com/go-pg/pg | 26369 | 第831章 37.7% | 86.9% |
| github.com/Shopify/sarama | 36427 | 第1369章 68.2% | 91.0% |
| github.com/stretchr/testify | 13496 | 32 | 43.8% | 92.9% |

if stmts ” 列仅计算返回错误的函数中的if语句,这就是tryhard报告它的方式,并且希望能解释为什么它对于像gorm这样的东西如此之低

10 杂项 “大”围棋项目

鉴于 godoc.org 上的流行包往往是库包,我还想检查一些较大项目的统计信息。

这些是杂项。 碰巧对我来说是头等大事的大型项目(即,这 10 个背后没有真正的逻辑)。 这再次按尝试候选百分比排序。

这 10 个存储库中尝试候选的中值为 59%。

| 项目 | 位置 | 如果 stmts | 如果!= nil(如果的百分比)| 尝试候选人(如果!= nil 的百分比)|
|---------|-----|--------------|-- --|------------------------------------------------|
| github.com/juju/juju | 1026473 | 26904 | 51.9% | 17.5% |
| github.com/go-kit/kit | 38949 | 第467章 57.0% | 51.9% |
| github.com/boltdb/bolt | 12426 | 228 | 46.1% | 53.3% |
| github.com/hashicorp/consul | 249369 | 5477 | 47.6% | 54.5% |
| github.com/docker/docker | 251152 | 8690 | 48.7% | 56.8% |
| github.com/istio/istio | 429636 | 7564 | 40.4% | 61.9% |
| github.com/gohugoio/hugo | 94875 | 1853 | 42.4% | 64.8% |
| github.com/etcd-io/etcd | 209603 | 4657 | 38.3% | 65.5% |
| github.com/kubernetes/kubernetes | 1789172 | 40289 | 43.3% | 66.5% |
| github.com/cockroachdb/cockroach | 1038529 | 22018 | 39.9% | 74.0% |


当然,这两个表仅代表开源项目的一个样本,并且仅代表相当知名的项目。 我已经看到人们推测私有代码库会显示出更大的多样性,并且至少有一些证据表明基于不同人发布的一些数字。

@thepudds ,这看起来不像最近的 _tryhard_,它给出了“未尝试的候选人”。

@networkimprov我可以确认至少对于gorm这些是最新tryhard的结果。 “未尝试的候选人”根本没有在上表中报告。

@daved首先,让我向您保证,我/我们听到您的声音响亮而清晰。 尽管我们仍处于早期阶段,很多事情都可能发生变化。 让我们不要开枪。

我理解(并欣赏)在教授围棋时可能想要选择一种更保守的方法。 谢谢。

@griesemer仅供参考,这是在我参与的 233k 行代码上运行最新版本的 tryhard 的结果,其中大部分不是开源的:

--- stats ---
   8760 (100.0% of    8760) functions (function literals are ignored)
   2942 ( 33.6% of    8760) functions returning an error
  22991 (100.0% of   22991) statements in functions returning an error
   5548 ( 24.1% of   22991) if statements
   2929 ( 52.8% of    5548) if <err> != nil statements
    163 (  5.6% of    2929) try candidates
      0 (  0.0% of    2929) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   2213 ( 75.6% of    2929) { return ... zero values ..., expr }
    167 (  5.7% of    2929) single statement then branch
    253 (  8.6% of    2929) complex then branch; cannot use try
     14 (  0.5% of    2929) non-empty else branch; cannot use try

大部分代码使用类似于以下的成语:

 if err != nil {
     return ... zero values ..., errors.Wrap(err)
 }

如果tryhard可以识别函数中的所有此类表达式何时使用相同的表达式可能会很有趣 - 即何时可以使用单个通用defer处理程序重写函数。

以下是用于自动创建用户和项目的小型 GCP 辅助工具的统计信息:

$ tryhard -r .
--- stats ---
    129 (100.0% of     129) functions (function literals are ignored)
     75 ( 58.1% of     129) functions returning an error
    725 (100.0% of     725) statements in functions returning an error
    164 ( 22.6% of     725) if statements
     93 ( 56.7% of     164) if <err> != nil statements
     64 ( 68.8% of      93) try candidates
      0 (  0.0% of      93) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     17 ( 18.3% of      93) { return ... zero values ..., expr }
      7 (  7.5% of      93) single statement then branch
      1 (  1.1% of      93) complex then branch; cannot use try
      0 (  0.0% of      93) non-empty else branch; cannot use try

在此之后,我继续检查代码中仍在处理err变量的所有地方,看看是否能找到任何有意义的模式。

收集err s

在一些地方,我们不想在第一个错误时停止执行,而是能够看到在运行结束时发生的所有错误。 也许有一种不同的方法可以很好地与try集成,或者 Go 本身添加了对多错误的某种形式的支持。

var errs []error
for _, p := range toDelete {
    fmt.Println("delete:", p.ProjectID)
    if err := s.DeleteProject(ctx, p.ProjectID); err != nil {
        errs = append(errs, err)
    }
}

错误装修责任

再次阅读此评论后,突然有很多潜在的try案例引起了我的注意。 它们都是相似的,因为调用函数正在用被调用函数可能已经添加到错误中的信息来修饰被调用函数的错误:

func run() error {
    key := "MY_ENV_VAR"
    client, err := ClientFromEnvironment(key)
    if err != nil {
        // "github.com/pkg/errors"
        return errors.Wrap(err, key)
    }
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, errors.New("environment variable not set")
    }
    return ClientFromFile(filename)
}

为了清楚起见,这里再次引用Go 博客的重要部分:

总结上下文是错误实现的责任。 os.Open 返回的错误格式为“打开 /etc/passwd:权限被拒绝”,而不仅仅是“权限被拒绝”。 我们的 Sqrt 返回的错误是缺少有关无效参数的信息。

考虑到这一点,上面的代码现在变为:

func run() error {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, fmt.Errorf("environment variable not set: %s", key)
    }
    return ClientFromFile(filename)
}

乍一看,这似乎是一个微小的变化,但据我估计,这可能意味着try实际上是在激励将更好、更一致的错误处理推向函数链并更接近源代码或包。

最后的笔记

总的来说,我认为try带来的长期价值高于我目前看到的潜在问题,它们是:

  1. try正在改变控制流时,关键字可能会“感觉”更好。
  2. 使用try意味着您不能再在return err情况下放置调试停止器。

由于围棋团队已经知道这些问题,我很想知道这些问题将如何在“现实世界”中发挥作用。 感谢您花时间阅读和回复我们所有的信息。

更新

修复了之前未返回error的函数签名。 感谢@magical发现这一点!

func main() {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

@mrkanister Nitpicking,但您实际上不能在此示例中使用try ,因为main不返回error

这是一个赞赏评论;
感谢@griesemer的园艺和所有工作,您在这个问题上以及其他地方一直在做。

如果您有很多这样的行(来自 https://github.com/golang/go/issues/32437#issuecomment-509974901):

if !ok {
    return nil, fmt.Errorf("environment variable not set: %s", key)
}

您可以使用仅在某些条件为真时才返回非零错误的辅助函数:

try(condErrorf(!ok, "environment variable not set: %s", key))

一旦确定了常见模式,我认为只需要几个助手就可以处理其中的许多模式,首先是在包级别,也许最终会到达标准库。 Tryhard 很棒,它做得很好,提供了很多有趣的信息,但还有更多。

紧凑的单行 if

作为对@zeebo和其他人提出的单行 if 提议的补充,if 语句可以有一个紧凑的形式,删除!= nil和花括号:

if err return err
if err return errors.Wrap(err, "foo: failed to boo")
if err return fmt.Errorf("foo: failed to boo: %v", err)

我认为这很简单,轻量级且可读。 有两个部分:

  1. 让 if 语句隐式检查 nil 的错误值(或者更普遍的接口)。 恕我直言,这通过降低密度来提高可读性,并且行为非常明显。
  2. 添加对if variable return ...的支持。 由于return非常靠近左侧,因此略读代码似乎仍然很容易——这样做的额外困难是反对单行 ifs (?) 的主要论据之一Go 也已经有了简化语法的先例,例如从 if 语句中删除括号。

当前风格:

a, err := BusinessLogic(state)
if err != nil {
   return nil, err
}

单行如果:

a, err := BusinessLogic(state)
if err != nil { return nil, err }

单行紧缩如果:

a, err := BusinessLogic(state)
if err return nil, err
a, err := BusinessLogic(state)
if err return nil, errors.Wrap(err, "some context")
func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err return nil, errors.WithMessage(err, "load config dir")

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err return nil, errors.WithMessage(err, "execute main template")

    buf, err := format.Source(b.Bytes())
    if err return nil, errors.WithMessage(err, "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err return nil, errors.WithMessagef(err, "write file %s", target)

    // ...
}

@eug48见 #32611

以下是 monorepo 的尝试统计数据(go 代码行,不包括供应商代码:2,282,731):

--- stats ---
 117551 (100.0% of  117551) functions (function literals are ignored)
  35726 ( 30.4% of  117551) functions returning an error
 263725 (100.0% of  263725) statements in functions returning an error
  50690 ( 19.2% of  263725) if statements
  25042 ( 49.4% of   50690) if <err> != nil statements
  12091 ( 48.3% of   25042) try candidates
     36 (  0.1% of   25042) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3561 ( 14.2% of   25042) { return ... zero values ..., expr }
   3304 ( 13.2% of   25042) single statement then branch
   4966 ( 19.8% of   25042) complex then branch; cannot use try
    296 (  1.2% of   25042) non-empty else branch; cannot use try

鉴于人们仍在提出替代方案,我想更详细地了解更广泛的 Go 社区实际上希望从任何提议的新错误处理功能中获得什么功能。

我整理了一份调查表,列出了我看到人们提出的一系列不同的功能和错误处理功能。 我已经小心地_省略了任何提议的命名或语法_,当然也试图使调查保持中立,而不是支持我自己的意见。

如果人们想参与,这里是链接,缩短分享:

https://forms.gle/gaCBgxKRE4RMCz7c7

每个参与的人都应该能够看到汇总结果。 也许这可能有助于集中讨论?

if err := os.Setenv("GO111MODULE", "on"); err != nil {
    return err
}

在这种情况下,添加上下文的延迟处理程序不起作用,或者是吗? 如果不是,那么最好让它更明显,如果可能的话,因为它发生得很快,特别是因为这是迄今为止的标准选择。

哦,请介绍一下try ,在这里也找到了很多用例。

--- stats ---
    929 (100.0% of     929) functions (function literals are ignored)
    230 ( 24.8% of     929) functions returning an error
   1480 (100.0% of    1480) statements in functions returning an error
    320 ( 21.6% of    1480) if statements
    206 ( 64.4% of     320) if <err> != nil statements
    109 ( 52.9% of     206) try candidates
      2 (  1.0% of     206) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     53 ( 25.7% of     206) { return ... zero values ..., expr }
     18 (  8.7% of     206) single statement then branch
     17 (  8.3% of     206) complex then branch; cannot use try
      2 (  1.0% of     206) non-empty else branch; cannot use try

@lpar欢迎您讨论替代方案,但请不要在本期中这样做。 这是关于try提案。 最好的地方实际上是邮件列表之一,例如 go-nuts。 问题跟踪器确实最适合跟踪和讨论特定问题,而不是一般性讨论。 谢谢。

@fabstu defer处理程序将在您的示例中正常工作,无论是否有try 。 使用封闭函数扩展您的代码:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

(请注意,结果err将由return err设置;而err使用的return是使用if本地声明的那个

或者,使用try ,这将消除对本地err变量的需要:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

最有可能的是,您想使用建议errors/errd函数之一:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

如果你不需要包装它只是:

func f() error {
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

@fastu最后,您也可以在没有try的情况下使用errors/errd $ ,然后您会得到:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

我越想越喜欢这个提议。
唯一让我感到不安的是在任何地方都使用命名返回。 这最终是一个好习惯吗?我应该使用它(从未尝试过)?

无论如何,在更改我所有的代码之前,它会这样工作吗?

func f() error {
  var err error
  defer errd.Wrap(&err,...)
  try(...)
}

@flibustenet命名结果参数本身并不是一个坏习惯; 命名结果的常见问题是它们启用naked returns ; 即,可以简单地编写return而不需要指定实际结果_with return _。 通常(但并非总是如此!)这种做法会使代码更难阅读和推理,因为不能简单地查看return语句并得出结论。 必须扫描代码以获取结果参数。 可能会错过设置结果值,等等。 因此,在某些代码库中,不鼓励裸返回。

但是,正如我之前提到的,如果在发生错误的情况下结果无效,那么设置错误并忽略其余部分是完全可以的。 只要始终设置错误结果,在这种情况下裸返回是完全可以的。 try将确保这一点。

最后,仅当您想使用defer来增加错误返回时,才需要命名结果参数。 设计文档还简要讨论了提供另一个内置来访问错误结果的可能性。 这将完全消除命名回报的需要。

关于您的代码示例:这将无法按预期工作,因为try _always_ 设置了 _result error_ (在这种情况下未命名)。 但是您声明了一个不同的局部变量err并且errd.Wrap对该变量进行操作。 它不会由try设置。

快速体验报告:我正在编写一个如下所示的 HTTP 请求处理程序:

func Handler(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        id := chi.URLParam(r, "id")

        var err error
        // starts as bad request, then it's an internal server error after we parse inputs
        var statusCode = http.StatusBadRequest

        defer func() {
            if err != nil {
                wrap := xerrors.Errorf("handler fail: %w", err)
                logger.With(zap.Error(wrap)).Error("error")
                http.Error(w, wrap.Error(), statusCode)
            }
        }()
        var c Thingie
        err = unmarshalBody(r, &c)
        if err != nil {
            return
        }
        statusCode = http.StatusInternalServerError
        s, err := DoThing(ctx, c)
        if err != nil {
            return
        }
        d, err := DoThingWithResult(ctx, id, s)
        if err != nil {
            return
        }
        data, err := json.Marshal(detail)
        if err != nil {
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        _, err = w.Write(data)
        if err != nil {
            return
        }
}

乍一看,这似乎是try的理想候选者,因为有很多错误处理,除了返回消息外无事可做,所有这些都可以延迟完成。 但是您不能使用try因为请求处理程序不返回error 。 为了使用它,我必须将主体包装在带有签名func() error的闭包中。 感觉......不优雅,我怀疑看起来像这样的代码是一种常见的模式。

@jonbodner

这有效(https://play.golang.org/p/NaB​​Ze-QShpu):

package main

import (
    "errors"
    "fmt"

    "golang.org/x/xerrors"
)

func main() {
    var err error
    defer func() {
        filterCheck(recover())
        if err != nil {
            wrap := xerrors.Errorf("app fail (at count %d): %w", ct, err)
            fmt.Println(wrap)
        }
    }()

    check(retNoErr())

    n, err := intNoErr()
    check(err)

    n, err = intErr()
    check(err)

    check(retNoErr())

    check(retErr())

    fmt.Println(n)
}

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

func filterCheck(r interface{}) {
    if r != nil {
        if _, ok := r.(struct{}); !ok {
            panic(r)
        }
    }
}

var ct int

func intNoErr() (int, error) {
    ct++
    return 0, nil
}

func retNoErr() error {
    ct++
    return nil
}

func intErr() (int, error) {
    ct++
    return 0, errors.New("oops")
}

func retErr() error {
    ct++
    return errors.New("oops")
}

啊,第一次投反对票! 好的。 让实用主义流经你。

在我的一些代码库上运行tryhard 。 不幸的是,我的一些包有0 try 候选者,尽管它们非常大,因为它们中的方法使用自定义错误实现。 例如,在构建服务器时,我喜欢我的业务逻辑层方法只发出SanitizedError s 而不是error s,以确保在编译时不会像文件系统路径或系统信息这样的东西在错误消息中泄露给用户。

例如,使用此模式的方法可能如下所示:

func (a *App) GetFriendsOfUser(userId model.Id) ([]*model.User, SanitizedError) {
    if user, err := a.GetUserById(userId); err != nil {
        // (*App).GetUserById returns (*model.User, SanitizedError)
        // This could be a try() candidate.
        return err
    } else if user == nil {
        return NewUserError("The specified user doesn't exist.")
    }

    friends, err := a.Store.GetFriendsOfUser(userId)
    // (*Store).GetFriendsOfUser returns ([]*model.User, error)
    // This could be a SQL error or a network error or who knows what.
    return friends, NewInternalError(err)
}

只要封闭函数和try函数表达式的最后一个返回值都实现错误并且是相同的类型,我们是否有任何理由不能放松当前的提议? 这仍然可以避免任何具体的 nil -> 接口混淆,但可以在上述情况下进行尝试。

谢谢@jonbodner ,您的示例。 我会按如下方式编写该代码(尽管有翻译错误):

func Handler(w http.ResponseWriter, r *http.Request) {
    statusCode, err := internalHandler(w, r)
    if err != nil {
        wrap := xerrors.Errorf("handler fail: %w", err)
        logger.With(zap.Error(wrap)).Error("error")
        http.Error(w, wrap.Error(), statusCode)
    }
}

func internalHandler(w http.ResponseWriter, r *http.Request) (statusCode int, err error) {
    ctx := r.Context()
    id := chi.URLParam(r, "id")

    // starts as bad request, then it's an internal server error after we parse inputs
    statusCode = http.StatusBadRequest
    var c Thingie
    try(unmarshalBody(r, &c))

    statusCode = http.StatusInternalServerError
    s := try(DoThing(ctx, c))
    d := try(DoThingWithResult(ctx, id, s))
    data := try(json.Marshal(detail))

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    try(w.Write(data))

    return
}

它使用两个函数,但要短得多(29 行对 40 行)——而且我使用了很好的间距——而且这段代码不需要defer 。 尤其是defer ,加上 statusCode 在下降过程中被更改并在defer中使用,使得原始代码比必要的更难遵循。 新代码虽然使用命名结果和裸返回(如果需要,您可以轻松地将其替换为return statusCode, nil )更简单,因为它将错误处理与“业务逻辑”完全分开。

只需在另一个问题中重新发布我的评论https://github.com/golang/go/issues/32853#issuecomment -510340544

我想如果我们可以提供另一个参数funcname ,那就太好了,否则我们仍然不知道错误是由哪个函数返回的。

func foo() error {
    handler := func(err error, funcname string) error {
        return fmt.Errorf("%s: %v", funcname, err) // wrap something
        //return nil // or dismiss
    }

    a, b := try(bar1(), handler) 
    c, d := try(bar2(), handler) 
}

@ccbrown我想知道您的示例是否可以接受与上述相同的处理; 即,如果将代码分解为有意义的代码,以便内部错误在它们消失之前(通过封闭函数)包装一次(而不是在任何地方包装它们)。 在我看来(对您的系统了解不多)这将是可取的,因为它将错误集中在一个地方而不是任何地方。

但是关于您的问题:我必须考虑让try接受更一般的错误类型(并且也返回一个)。 我目前没有看到它有问题(除了解释起来更复杂) - 但毕竟可能存在问题。

沿着这些思路,我们很早就想知道try是否可以泛化,因此它不仅适用于error类型,而且适用于任何类型,然后测试err != nil将变成x != zero其中x相当于err (最后一个结果), zerox类型的相应零值bool的零值是falseok != false正是与我们想要测试的相反。

@lunny try的建议版本不接受处理函数。

@griesemer哦。 有什么遗憾! 否则我可以删除github.com/pkg/errors和所有errors.Wrap

@ccbrown我想知道您的示例是否可以接受与上述相同的处理; 即,如果将代码分解为有意义的代码,以便内部错误在它们消失之前(通过封闭函数)包装一次(而不是在任何地方包装它们)。 在我看来(对您的系统了解不多)这将是可取的,因为它将错误集中在一个地方而不是任何地方。

@griesemererror返回到封闭函数可能会忘记将每个错误分类为内部错误、用户错误、授权错误等。按原样,编译器会捕获它,并使用try不值得将这些编译时检查换成运行时检查。

我想说我喜欢try的设计,但是当您使用try时, defer处理程序中仍然有if语句。 我认为这不会比没有trydefer处理程序的if语句更简单。 也许只使用try会好得多。

@ccbrown知道了。 回想起来,我认为你建议的放松应该没有问题。 我相信我们可以放宽try以使用任何接口类型(和匹配的结果类型),就此而言,不仅仅是error ,只要相关测试保持x != nil . 需要考虑的事情。 这可以提前完成,或者追溯,因为我相信这将是一个向后兼容的变化。

@jonbodner example以及@griesemer重写它的方式正是我真正想使用的那种代码try

没有人对这种类型的 try 使用感到困扰:

数据 := try(json.Marshal(detail))

不管编组错误会导致在书面代码中找到正确的行这一事实,知道这是一个赤裸裸的错误被返回而没有包含行号/调用者信息,我感到很不舒服。 知道源文件、函数名和行号通常是我在处理错误时包括的内容。 也许我误解了一些东西。

@griesemer我不打算在这里讨论替代方案。 事实上,每个人都在不断提出替代方案,这正是我认为调查人们真正想要什么是一个好主意的原因。 我刚刚在这里发布了它,试图吸引尽可能多的对改进 Go 错误处理的可能性感兴趣的人。

@trende-jp 我真的依赖于这行代码的上下文——它本身不能以任何有意义的方式来判断。 如果这是对json.Marshal的唯一调用并且您想增加错误,那么if语句可能是最好的。 如果有很多json.Marshal调用,则可以使用defer很好地为错误添加上下文; 或者也许通过将所有这些调用包装在返回错误的本地闭包中。 如果需要,有多种方法可以考虑到这一点(即,如果同一函数中有许多这样的调用)。 “错误就是价值”在这里也是正确的:使用代码使错误处理易于管理。

try不会解决你所有的错误处理问题——这不是目的。 它只是工具箱中的另一个工具。 而且它也不是真正的新机器,它是一种语法糖的形式,我们在近十年的时间里经常观察到这种模式。 我们有一些证据表明它在某些代码中可以很好地工作,并且在其他代码中也没有太大帮助。

@趋势-jp

不能用defer解决吗?

defer fmt.HandleErrorf(&err, "decoding %q", path)

错误消息中的行号也可以解决,正如我在我的博客中所展示的那样:如何使用 'try'

@trende-jp @faiface除了行号之外,您还可以将装饰器字符串存储在变量中。 这将让您隔离失败的特定函数调用。

我真的认为这绝对不应该是一个内置函数

有几次提到panic()recover()也会改变控制流。 很好,我们不要再添加了。

@networkimprov写道https://github.com/golang/go/issues/32437#issuecomment -498960081:

它不像 Go 那样读。

我完全同意。

如果有的话,我相信任何解决根本问题的机制(我不确定是否存在),它应该由关键字(或关键符号?)触发。

如果go func()go(func()) ,你会有什么感觉?

如何使用 bang(!) 而不是try函数。 这可以使函数链成为可能:

func foo() {
    f := os.Open!("")
    defer f.Close()
    // etc
}

func bar() {
    count := mustErr!().Read!()
}

@sylr

如果 go func() 变成 go(func()) 你会怎么想?

来吧,那是可以接受的。

@sylr谢谢,但我们不会在此线程上征求替代建议。 另参阅有关保持专注的内容。

关于您的评论:Go 是一种实用语言 - 在这里使用内置是一种实用的选择。 如设计文档中详细说明的那样,与使用关键字相比,它有几个优点。 请注意, try只是一种常见模式的语法糖(与go相比,它实现了 Go 的一个主要功能并且不能用其他 Go 机制实现),例如append , copy等。使用内置是一个不错的选择。

(但正如我之前所说,如果 _that_ 是唯一阻止try被接受的东西,我们可以考虑将其设为关键字。)

我只是在思考我自己的一段代码,以及try的样子:

slurp, err := ioutil.ReadFile(path)
if err != nil {
    return err
}
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

可能变成:

return ioutil.WriteFile(path, append(copyrightText, try(ioutil.ReadFile(path))...), 0666)

我不确定这是否更好。 它似乎使代码更难阅读。 但这可能只是习惯的问题。

@gbbr您可以在这里选择。 你可以把它写成:

slurp := try(ioutil.ReadFile(path))
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

这仍然为您节省了大量样板文件,但使其更加清晰。 这不是try所固有的。 仅仅因为您可以将所有内容压缩到一个表达式中并不意味着您应该这样做。 这普遍适用。

@griesemer这个例子_is_ 是固有的尝试,你不能嵌套今天可能失败的代码——你被迫处理控制流的错误。 我想从https://github.com/golang/go/issues/32825#issuecomment -507099786 / https://github.com/golang/go/issues/32825#issuecomment -507136111 中清除一些东西回复https://github.com/golang/go/issues/32825#issuecomment -507358397。 后来在https://github.com/golang/go/issues/32825#issuecomment -508813236 和https://github.com/golang/go/issues/32825#issuecomment -508937177 中再次讨论了同样的问题 - 最后一个其中我声明:

很高兴您阅读了我反对 try 的中心论点:实施不够严格。 我相信任何一个实现都应该与所有简洁易读的提案使用示例相匹配。

_或者_提案应该包含与实现相匹配的示例,以便所有考虑它的人都可以接触到将不可避免地出现在 Go 代码中的内容。 以及我们在对不理想编写的软件进行故障排除时可能面临的所有极端情况,这种情况发生在任何语言/环境中。 它应该回答诸如具有多个嵌套级别的堆栈跟踪会是什么样子之类的问题,错误的位置是否易于识别? 方法值,匿名函数文字呢? 如果包含对 fn() 的调用的行失败,下面会产生什么类型的堆栈跟踪?

fn := func(n int) (int, error) { ... }
return try(func() (int, error) { 
    mu.Lock()
    defer mu.Unlock()
    return try(try(fn(111111)) + try(fn(101010)) + try(func() (int, error) {
       // yea...
    })(2))
}(try(fn(1)))

我很清楚会编写很多合理的代码,但我们现在提供了一个以前从未存在过的工具:在没有明确控制流的情况下编写代码的能力。 所以我想证明为什么我们一开始就允许它,我不想把时间浪费在调试这种代码上。 因为我知道我会的,经验告诉我,如果你允许,有人会这样做。 那个人往往是一个不知情的我。

Go 通过限制我们使用相同的普通结构,为其他开发人员和我提供了最少的方式来浪费彼此的时间。 如果没有压倒性的好处,我不想失去它。 我不相信“因为尝试是作为一个函数实现的”是一个压倒性的好处。 你能提供一个原因吗?

有一个显示上述失败位置的堆栈跟踪会很有用,也许添加一个复合文字,其中包含调用该函数的字段? 我要求这个是因为我知道今天堆栈跟踪对于此类问题的外观,Go 不会在堆栈信息中提供易于理解的列信息,仅提供十六进制函数入口地址。 有几件事让我担心,例如跨架构的堆栈跟踪一致性,例如考虑以下代码:

package main
import "fmt"
func dopanic(b bool) int { if b { panic("panic") }; return 1 }
type bar struct { a, b, d int; b *bar }
func main() {
    fmt.Println(&bar{
        a: 1,
        c: 1,
        d: 1,
        b: &bar{
            a: 1,
            c: 1,
            d: dopanic(true) + dopanic(false),
        },
    })
}

注意第一个 Playground 如何在左侧 dopanic 失败,第二个在右侧失败,但两者都打印相同的堆栈跟踪:
https://play.golang.org/p/SYS1r4hBS7O
https://play.golang.org/p/YMKkflcQuav

panic: panic

goroutine 1 [running]:
main.dopanic(...)
    /tmp/sandbox709874298/prog.go:7
main.main()
    /tmp/sandbox709874298/prog.go:27 +0x40

我本来希望第二个是 +0x41 或 0x40 之后的某个偏移量,这可用于确定在恐慌中失败的实际调用。 即使我们得到了正确的十六进制偏移量,如果没有额外的调试,我也无法确定故障发生在哪里。 今天,这是一个边缘案例,人们很少会遇到这种情况。 如果你发布了 try 的可嵌套版本,它将成为常态,因为即使提案也包含一个 try() + try() strconv,表明以这种方式使用 try 是可行和可接受的。

1) 鉴于上述信息,您计划对堆栈跟踪进行哪些更改(如果有的话),以便我仍然可以知道我的代码在哪里失败?

2)是否允许尝试嵌套,因为您认为应该这样做? 如果是这样,您认为尝试嵌套的好处是什么?您将如何防止滥用? 我认为应该调整 tryhard 以在您认为可以接受的地方执行嵌套尝试,以便人们可以就它如何影响他们的代码做出更明智的决定,因为目前我们只获得最佳/最严格的使用示例。 这将使我们了解将施加哪种类型的vet限制,到目前为止,您已经说过 vet 将是对不合理尝试的防御,但是这将如何实现?

3)尝试嵌套是因为它恰好是实现的结果吗? 如果是这样,对于自 Go 发布以来最显着的语言变化,这似乎不是一个非常薄弱的​​论据吗?

我认为这种变化需要更多地考虑尝试嵌套。 每次我想到某个地方都会出现一些新的痛点,我非常担心所有潜在的负面影响都不会出现,直到它在野外暴露出来。 如https://github.com/golang/go/issues/32825#issuecomment -506882164 中所述,嵌套还提供了一种泄漏资源的简单方法,这在今天是不可能的。 我认为“兽医”的故事需要一个更具体的计划,如果它被用作防御我在这里给出的有害 try() 示例,它将如何提供反馈,或者实现应该提供编译时错误用于您理想的最佳实践之外的用途。

编辑:我在 gophers 中询问了 play.golang.org 架构,有人提到它是通过 NaCl 编译的,所以可能只是一个结果/错误。 但是我可以看到这在其他拱门上是一个问题,我认为由于大多数使用都围绕着理智和干净的单线使用,所以没有充分探索每行引入多个返回可能引起的许多问题。

哦不,请不要在语言中添加这种“魔法”。
这些看起来和感觉不像其他语言。
我已经看到这样的代码无处不在。

a, b := try( f() )
if a != 0 && b != "" {
...
}
...

代替

a,b,err := f()
if err != nil {
...
}
...

要么

a,b,_:= f()

call if err....模式一开始对我来说有点不自然,但现在我已经习惯了
我觉得更容易处理错误,因为它们可能会到达执行流程,而不是编写包装器/处理程序,这些包装器/处理程序必须跟踪某种状态才能在触发后采取行动。
如果我决定忽略错误以挽救键盘的生命,我知道有一天我会惊慌失措。

我什至将我在 vbscript 中的习惯改为:

on error resume next
a = f()
if er.number <> 0 then
    ...
end if
...

我喜欢这个提议

我所关心的所有问题(例如,理想情况下应该是关键字而不是内置的)都在深入的文档中得到解决

它不是 100% 完美,但它是一个足够好的解决方案,a) 解决了实际问题,b) 在考虑许多向后兼容和其他问题的同时这样做

当然它确实有一些“魔法”,但defer也是如此。 唯一的区别是关键字与内置的,在这里避免关键字的选择是有意义的。

我觉得对try()提案的所有重要反馈都已经表达出来了。 但让我试着总结一下:

1) try() 将垂直代码复杂度移动到水平
2) 嵌套的 try() 调用与三元运算符一样难以阅读
3) 引入了不可见的“返回”控制流,在视觉上没有区别(与以return关键字开头的缩进块相比)
4)使错误包装实践变得更糟(函数上下文而不是特定操作)
5)拆分#golang社区和代码风格(反gofmt)
6) 会使开发人员经常将 try() 重写为 if-err-nil,反之亦然(tryhard 与添加清理逻辑/附加日志/更好的错误上下文)

@VojtechVitek我认为您提出的观点是主观的,只有在人们开始认真使用它时才能进行评估。

但是我相信有一个技术点没有被太多讨论。 使用defer进行错误包装/修饰的模式对性能的影响超出了defer本身的简单成本,因为使用defer的函数不能内联。

这意味着与err != nil检查后直接返回包装错误相比,采用带有错误包装的try会产生两个潜在成本:

  1. 延迟通过函数的所有路径,即使是成功的路径
  2. 内联丢失

尽管defer即将出现一些令人印象深刻的性能改进,但成本仍然不为零。

try有很大的潜力,所以如果 Go 团队可以重新审视设计以允许在失败点进行某种包装而不是通过defer抢先完成,那就太好了.

兽医”的故事需要一个更具体的计划

兽医的故事是童话。 它仅适用于已知类型,对自定义类型无用。

大家好,

我们提出此类提案的目标是在整个社区范围内就影响、权衡以及如何进行进行讨论,然后利用该讨论来帮助决定前进的道路。

基于压倒性的社区反应和广泛的讨论,我们标记此提案提前被拒绝。

就技术反馈而言,本次讨论有助于确定我们错过的一些重要考虑因素,最值得注意的是添加调试打印和分析代码覆盖率的含义。

更重要的是,我们清楚地听到了许多人认为该提案不是针对一个有价值的问题。 我们仍然认为 Go 中的错误处理并不完美,可以进行有意义的改进,但很明显,作为一个社区,我们需要更多地讨论错误处理的哪些具体方面是我们应该解决的问题。

至于讨论要解决的问题,我们在去年 8 月的“ Go 2 错误处理问题概述”中试图提出我们对问题的看法,但回想起来我们没有引起足够的重视,也没有给予足够的鼓励讨论具体问题是否正确。 try提案可能是解决那里概述的问题的好方法,但对你们中的许多人来说,这根本不是要解决的问题。 未来,我们需要更好地引起人们对这些早期问题陈述的关注,并确保就需要解决的问题达成广泛共识。

(也有可能因为在同一天发布了一个泛型设计草案,错误处理问题陈述被完全抢了风头。)

关于如何改进 Go 错误处理的更广泛主题,我们很高兴看到有关 Go 错误处理的哪些方面在您自己的代码库和工作环境中对您来说最有问题的经验报告,以及一个好的解决方案会产生多大的影响有在自己的发展。 如果您确实撰写了此类报告,请在Go2ErrorHandlingFeedback 页面上发布链接。

感谢在这里和其他地方参与此讨论的所有人。 正如 Russ Cox 之前指出的那样,像这样的社区范围内的讨论是开源的。 我们非常感谢大家帮助检查这个特定的提案,以及更广泛地讨论改善 Go 中错误处理状态的最佳方法。

Robert Griesemer,提案审查委员会。

感谢 Go Team 为尝试提案所做的工作。 并感谢与之抗争并提出替代方案的评论者。 有时,这些东西有自己的生命。 感谢 Go Team 的倾听和适当的回应。

耶!

感谢大家对此进行讨论,以便我们获得最好的结果!

呼吁列出 Go 错误处理的问题和负面经验。 然而,
我和团队对生产中的 xerrors.As、xerrors.Is 和 xerrors.Errorf 非常满意。 既然我们已经完全接受了这些变化,这些新增功能对我们来说以一种奇妙的方式彻底改变了错误处理。 目前,我们还没有遇到任何未解决的问题或需求。

@griesemer只是想说谢谢你(可能还有许多与你一起工作的人)的耐心和努力。

好的!

@griesemer感谢您和 Go 团队的其他所有人不知疲倦地倾听所有反馈并忍受我们所有不同的意见。

所以也许现在是结束这个线程并继续处理未来事情的好时机?

@griesemer @rsc@all ,很酷,谢谢大家。 对我来说,这是一个很好的讨论/识别/澄清。 go 中某些部分的增强,例如“错误”问题,需要更公开的讨论(在提案和评论中......)以首先识别/澄清核心问题。

ps,x/xerrors 现在很好。

(锁定这个线程也可能有意义......)

感谢团队和社区参与其中。 我喜欢有多少人关心 Go。

我真的希望社区首先看到尝试提案中的努力和技巧,然后是帮助我们做出这个决定的参与精神。 如果我们能坚持下去,围棋的未来是非常光明的,尤其是如果我们都能保持积极的态度。

func M()(数据,错误){
a, err1 := A()
b, err2 := B()
返回 b,无
} => (如果 err1 != nil){ 返回 a, err1}。
(如果 err2 != nil){ 返回 b, err2}

好的...我喜欢这个提议,但我喜欢社区和 Go 团队的反应方式并参与建设性的讨论,尽管有时有点粗糙。

我有两个关于这个结果的问题:
1/ “错误处理”仍然是一个研究领域吗?
2/ 延迟改进是否会重新排序?

这再次证明 Go 社区正在被倾听并能够讨论有争议的语言更改提案。 就像把它变成语言的变化一样,没有变化的变化是一种改进。 感谢 Go 团队和社区围绕该提案的辛勤工作和文明讨论!

优秀!

很棒,很有帮助

也许我对 Go 太执着了,但我认为这里显示了一点,因为
Russ 描述:在某一点上,社区不仅仅是一个
无头鸡,是一股不可忽视的力量
为自己的利益而利用。

多亏了 Go Team 提供的协调,我们可以
所有人都为我们得出一个结论而感到自豪,一个我们可以忍受并且
毫无疑问,当条件更成熟时,将重新审视。

让我们希望在这里感受到的痛苦在未来对我们有好处
(否则会不会很伤心?)。

卢西奥。

我不同意这个决定。 但是,我绝对赞同 go 团队所采取的方法。 进行社区广泛的讨论并考虑开发人员的反馈是开源的意义所在。

我想知道延迟优化是否也会出现。 我非常喜欢用它和 xerrors 一起注释错误,而且现在它的成本太高了。

@pierrec我认为我们需要更清楚地了解错误处理中的哪些变化是有用的。 一些错误值更改将在即将发布的 1.13 版本中(https://tip.golang.org/doc/go1.13#errors),我们将获得经验。 在本次讨论的过程中,我们看到了很多很多句法错误处理建议,如果人们可以对任何看起来特别有用的内容进行投票和评论,那将是很有帮助的。 更一般地说,正如@griesemer所说,经验报告会有所帮助。

更好地理解错误处理语法在多大程度上对语言新手来说是有问题的也是有用的,尽管这很难确定。

https://golang.org/cl/183677中正在积极改进defer的性能,除非遇到一些重大障碍,否则我希望它能够进入 1.14 版本。

@griesemer @ianlancetaylor @rsc您是否仍计划解决错误处理的冗长问题,并提出另一个解决此处提出的部分或全部问题的提案?

所以,迟到了,因为这已经被拒绝了,但是对于这个话题的未来讨论,类似三元的条件返回语法怎么样? (在我对主题的扫描或查看 Russ Cox 在 Twitter 上发布的视图时,我没有看到任何类似的内容。)示例:

f, err := Foo()
return err != nil ? nil, err

如果 err 为非 nil,则返回nil, err ,如果 err 为 nil,则继续执行。 声明表格将是

return <boolean expression> ? <return values>

这将是语法糖:

if <boolean expression> {
    return <return values>
}

主要的好处是这比check关键字或try内置函数更灵活,因为它可以触发的不仅仅是错误(例如return err != nil || f == nil ? nil, fmt.Errorf("failed to get Foo") ,更多不仅仅是错误是非零(例如return err != nil && err != io.EOF ? nil, err )等,同时在阅读时仍然相当直观(特别是对于那些习惯于阅读其他语言的三元运算符的人)。

它还确保错误处理_仍然发生在调用位置_,而不是基于某些 defer 语句自动发生。 我对原始提案的最大抱怨之一是它试图在某些方面使实际的错误处理成为一个隐式过程,当错误非零时自动发生,没有明确的迹象表明控制流如果函数调用返回非 nil 错误,将返回。 Go 的整个 _point_ 使用显式错误返回而不是类似异常的系统是为了鼓励开发人员显式地和有意识地检查和处理他们的错误,而不是仅仅让它们传播到堆栈上,以便在理论上在更高的某个点进行处理向上。 至少一个明确的,如果有条件的,return 语句清楚地注释了正在发生的事情。

@ngrilly正如@griesemer所说,我认为我们需要更好地了解错误处理的哪些方面 Go 程序员认为最有问题。

就个人而言,我认为删除少量冗长的提案不值得做。 毕竟,这种语言在今天已经足够好用了。 每一次改变都需要付出代价。 如果我们要做出改变,我们需要一个显着的好处。 我认为这个提议确实在减少冗长方面提供了显着的好处,但显然有很大一部分 Go 程序员认为它带来的额外成本太高了。 我不知道这里是否有中间立场。 而且我不知道这个问题是否值得解决。

@kaedys这个封闭且极其冗长的问题绝对不适合讨论用于错误处理的特定替代语法。

@ianlancetaylor

我认为这个提议确实在减少冗长方面提供了显着的好处,但显然有很大一部分 Go 程序员认为它带来的额外成本太高了。

恐怕存在自我选择的偏见。 Go 以其冗长的错误处理和缺乏泛型而闻名。 这自然会吸引不关心这两个问题的开发者。 与此同时,其他开发人员继续使用他们当前的语言(Java、C++、C#、Python、Ruby 等)和/或切换到更现代的语言(Rust、TypeScript、Kotlin、Swift、Elixir 等)。 . 我知道许多开发人员主要出于这个原因避免使用 Go。

我也认为存在确认偏差。 当人们批评 Go 时,Gophers 被用来保护冗长的错误处理和缺乏错误处理。 这使得客观评估像尝试这样的提议变得更加困难。

几天前,Steve Klabnik在 Reddit 上发表了一篇有趣的评论。 他反对在 Rust 中引入? ,因为它是“写同一件事的两种方式”而且“太隐晦”。 但是现在,在编写了几行代码之后, ?是他最喜欢的功能之一。

@ngrilly我同意你的意见。 这些偏见很难避免。 非常有帮助的是更清楚地了解有多少人由于冗长的错误处理而避免使用 Go。 我确定这个数字不是零,但很难衡量。

也就是说, try确实在控制流中引入了一个很难看到的新变化,尽管try旨在帮助处理错误,但它无助于注释错误.

感谢 Steve Klabnik 的报价。 虽然我欣赏并同意这种观点,但值得考虑的是,作为一门语言,Rust 似乎比 Go 更愿意依赖语法细节。

作为这个提议的支持者,我自然对它现在被撤回感到失望,尽管我认为 Go 团队在这种情况下做了正确的事情。

现在似乎很清楚的一件事是,大多数 Go 用户并不认为错误处理的冗长是一个问题,我认为这是我们其他人必须忍受的事情,即使它确实推迟了潜在的新用户.

我已经数不清我读过多少备选提案了,虽然有些提案相当不错,但我还没有看到任何我认为值得采用的提案,如果try被尘埃落定。 所以现在出现一些中间立场提案的机会对我来说似乎很遥远。

在更积极的方面,当前的讨论指出了可以在相同的方式和相同的位置修饰函数中所有潜在错误的方法(使用defer甚至goto )我以前没有考虑过,我确实希望 Go 团队至少会考虑更改go fmt以允许将单个语句if写在一行上,这至少会进行错误处理_look_更紧凑,即使它实际上并没有删除任何样板。

@pierrec

1/ “错误处理”仍然是一个研究领域吗?

50多年了! 对于一致和系统的错误处理,似乎没有一个整体的理论,甚至没有一个实用的指南。 在 Go 领域(与其他语言一样)甚至对错误是什么感到困惑。 例如,当您尝试读取文件时,EOF 可能是一个异常情况,但为什么它是一个错误? 这是否是实际错误取决于上下文。 还有其他这样的问题。

也许需要进行更高级别的讨论(不过,这里不是)。

谢谢@griesemer @rsc和所有参与提议的人。 许多其他人在上面已经说过,值得重申的是,您在思考问题、编写提案和真诚地讨论它方面所做的努力是值得赞赏的。 谢谢你。

@ianlancetaylor

感谢 Steve Klabnik 的报价。 虽然我欣赏并同意这种观点,但值得考虑的是,作为一门语言,Rust 似乎比 Go 更愿意依赖语法细节。

我一般同意 Rust 不仅仅依赖于语法细节,但我认为这不适用于关于错误处理冗长的具体讨论。

错误是 Rust 中的值,就像它们在 Go 中一样。 您可以使用标准控制流来处理它们,就像在 Go 中一样。 在 Rust 的第一个版本中,它是处理错误的唯一方法,就像在 Go 中一样。 然后他们引入了try!宏,它与try内置函数提议惊人地相似。 他们最终添加了?运算符,它是try!宏的语法变体和泛化,但这并不是证明try的有用性所必需的,事实上Rust 社区并不后悔添加它。

我很清楚 Go 和 Rust 之间的巨大差异,但是关于错误处理冗长的话题,我认为他们的经验可以转置到 Go。 与try!?相关的 RFC 和讨论非常值得一读。 我真的很惊讶支持和反对语言变化的问题和论点如此相似。

@griesemer ,你宣布了拒绝当前形式的try提案的决定,但你没有说明 Go 团队接下来打算做什么。

您是否仍计划解决错误处理的冗长问题,并提出另一个可以解决本次讨论中提出的问题(调试打印、代码覆盖、更好的错误修饰等)的提案?

我一般同意 Rust 不仅仅依赖于语法细节,但我认为这不适用于关于错误处理冗长的具体讨论。

由于其他人仍在增加他们的两分钱,我想我仍有空间做同样的事情。

虽然我从 1987 年就开始编程,但我只使用 Go 大约一年。 大约 18 个月前,当我在寻找一种新的语言来满足某些需求时,我同时研究了 Go 和 Rust。 我决定使用 Go 是因为我觉得 Go 代码更容易学习和使用,而且 Go 代码更具可读性,因为 Go 似乎更喜欢用文字来传达含义而不是简洁的符号。

所以我会很不高兴看到 Go 变得更像 Rust ,包括使用感叹号( ! )和问号( ? )来暗示意义。

与此类似,我认为宏的引入将改变 Go 的性质,并会产生数千种 Go 方言,就像 Ruby 一样。 所以我希望宏永远不会被添加到 Go 中,尽管我猜测这种情况发生的可能性很小,幸运的是 IMO。

jmtcw

@ianlancetaylor

非常有帮助的是更清楚地了解有多少人由于冗长的错误处理而避免使用 Go。 我确定这个数字不是零,但很难衡量。

这本身并不是一个“衡量标准”,但是这个 Hacker News 讨论提供了来自开发人员的数十条评论,这些评论由于 Go 的冗长而对错误处理不满意(一些评论解释了他们的推理并给出了代码示例): https://news.ycombinator。 com/item?id=20454966。

首先,感谢大家对最终决定的支持性反馈,即使该决定对许多人来说并不令人满意。 这确实是一个团队的努力,我真的很高兴我们都设法以一种整体文明和尊重的方式度过了激烈的讨论。

@ngrilly就我自己而言,我仍然认为在某些时候解决错误处理的冗长问题会很好。 也就是说,在过去的半年里,尤其是最近三个月,我们在这方面投入了相当多的时间和精力,我们对这个提议非常满意,但我们显然低估了对它的可能反应。 现在,退后一步,消化和提炼反馈,然后决定最好的下一步,确实很有意义。

此外,实际上,由于我们没有无限的资源,我认为考虑对错误处理的语言支持会被搁置一旁,以便在其他方面取得更多进展,尤其是在泛型方面的工作,至少对于接下来的几个月。 if err != nil可能很烦人,但这不是紧急行动的理由。

如果您想继续讨论,我想温和地建议大家离开这里并在其他地方继续讨论,在一个单独的问题中(如果有明确的建议),或者在其他更适合公开讨论的论坛中。 毕竟,这个问题已经结束了。 谢谢。

恐怕存在自我选择的偏见。

我想在这里和现在创造一个新术语:“创作者偏见”。 如果有人愿意投入工作,他们应该得到怀疑的好处。

花生画廊很容易在不相关的论坛上大声喊叫他们如何不喜欢提出的问题解决方案。 每个人也很容易为不同的解决方案写一个 3 段的不完整尝试(没有实际工作在场边展示)。 如果有人同意现状,好的。 有道理。 在没有完整提案的情况下将其他任何内容作为解决方案提供给您-10k 分。

我不支持也不反对 try,但我相信 Go Teams 对此事的判断,到目前为止他们的判断提供了出色的语言,所以我认为无论他们决定对我有用,尝试或不尝试,我认为作为局外人,我们需要了解,维护者对此事有更广泛的了解。 我们可以讨论一整天的语法。 我要感谢目前正在努力或试图改进 go 的每个人的努力,我们很感激并期待语言库和运行时的新(非向后破坏)改进(如果有的话)对你们有用。

每个人也很容易为不同的解决方案写一个 3 段的不完整尝试(没有实际工作在场边展示)。

我(和其他一些人)唯一想让try有用的是一个可选参数,允许它返回错误的包装版本而不是未更改的错误。 我认为这不需要大量的设计工作。

不好了。

我知道了。 Go 想要做出与其他语言不同的东西。

也许有人应该锁定这个问题? 讨论可能更适合其他地方。

这个问题已经很长了,锁定它似乎毫无意义。

各位,请注意这个问题已经关闭,你在这里发表的评论几乎肯定会被永远忽略。 如果你觉得没问题,请发表评论。

如果有人讨厌让他们想到 Java、C* 语言的 try 词,我建议不要使用“try”,而是使用“help”或“must”或“checkError”等其他词。(忽略我)

如果有人讨厌让他们想到 Java、C* 语言的 try 词,我建议不要使用“try”,而是使用“help”或“must”或“checkError”等其他词。(忽略我)

总是会有重叠的关键字和概念,它们在彼此合理接近的语言(如 C 系列语言)中具有或小或大的语义差异。 语言特性不应该在语言本身内部引起混淆,语言之间的差异总是会发生的。

坏的。 这是反模式,不尊重该提案的作者

@alersenkevich请有礼貌。 请参阅https://golang.org/conduct。

我想我很高兴决定不再继续这样做。 对我来说,这感觉像是一个快速解决关于 if err != nil 出现在多行的小问题的方法。 我们不想用次要的关键字来膨胀 Go 来解决这样的次要问题,对吗? 这就是为什么带有卫生宏的提案https://github.com/golang/go/issues/32620感觉更好的原因。 它试图成为一个更通用的解决方案,为更多的东西打开更多的灵活性。 那里正在进行语法和用法讨论,所以不要只考虑它是否是 C/C++ 宏。 重点是讨论一种更好的方法来做宏。 有了它,您可以实现自己的尝试。

我希望收到有关解决当前错误处理问题的类似提案的反馈https://github.com/golang/go/issues/33161。

老实说,这应该重新打开,在所有错误处理建议中,这是最理智的一个。

@OneOfOne尊敬的,我不同意这应该重新打开。 该线程已确定语法存在真正的限制。 也许你是对的,这是最“理智”的提议:但我相信现状仍然更加理智。

我同意if err != nil在 Go 中写得太频繁了——但是从函数返回的单一方式极大地提高了可读性。 虽然我通常可以支持减少样板代码的建议,但代价永远不应该是可读性恕我直言。

我知道很多开发人员都对 Go 中的“普通”错误检查感到遗憾,但老实说,简洁性通常与可读性不符。 Go 在这里和其他地方有许多既定的模式,它们鼓励一种特定的做事方式,并且根据我的经验,结果是可靠的代码,可以很好地使用。 这很关键:现实世界的代码在其生命周期中必须多次阅读和理解,但只能编写一次。 即使对于经验丰富的开发人员来说,认知开销也是一项真正的成本。

代替:

f := try(os.Open(filename))

我希望:

f := try os.Open(filename)

各位,请注意这个问题已经关闭,你在这里发表的评论几乎肯定会被永远忽略。 如果你觉得没问题,请发表评论。
—@ianlancetaylor

如果我们可以将 try 用于代码块以及当前处理错误的方式,那就太好了。
像这样的东西:

// Generic Error Handler
handler := func(err error) error {
    return fmt.Errorf("We encounter an error: %v", err)  
}
a := "not Integer"
b := "not Integer"

try(handler){
    f := os.Open(filename)
    x := strconv.Atoi(a)
    y, err := strconv.Atoi(b) // <------ If you want a specific error handler
    if err != nil {
        panic("We cannot covert b to int")   
    }
}

上面的代码似乎比最初的评论更干净。 我希望我能有这个目的。

我提出了一个新提案 #35179

val := 尝试 f() (err){
恐慌(错误)
}

但愿如此:

i, err := strconv.Atoi("1")
if err {
    println("ERROR")
} else {
    println(i)
}

要么

i, err := strconv.Atoi("1")
if err {
    io.EOF:
        println("EOF")
    io.ErrShortWrite:
        println("ErrShortWrite")
} else {
    println(i)
}

@myroid我不介意让您的第二个示例以switch-else语句的形式更加通用:

```去
我,错误:= strconv.Atoi("1")
切换错误!= nil; 呃 {
案例 io.EOF:
println("EOF")
案例 io.ErrShortWrite:
println("ErrShortWrite")
} 别的 {
打印(i)
}

@piotrkowalczuk你的代码看起来比我的好多了。 我认为代码可以更简洁。

i, err := strconv.Atoi("1")
switch err {
case io.EOF:
    println("EOF")
case io.ErrShortWrite:
    println("ErrShortWrite")
} else {
    println(i)
}

这不考虑选项会有不同类型的眼睛

需要有
案例错误!=无

对于开发人员未明确捕获的错误

2019 年 11 月 8 日星期五 12:06 杨帆, notifications @github.com 写道:

@piotrkowalczuk https://github.com/piotrkowalczuk你的代码看起来很
比我的好。 我认为代码可以更简洁。

i, err := strconv.Atoi("1")switch err {case io.EOF:
println("EOF")case io.ErrShortWrite:
println("ErrShortWrite")
} 别的 {
打印(i)
}


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4VH4KS2OX4M5BVH673QSU24DA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDPTTYY#issuecomment-5515002
或退订
https://github.com/notifications/unsubscribe-auth/ABNEY4W4XIIHGUGIW2KXRPTQSU24DANCNFSM4HTGCZ7Q
.

switch不需要else ,它有default

我已经打开了https://github.com/golang/go/issues/39890 ,它提出了类似于 Swift 的guard的内容,应该解决这个提案的一些问题:

  • 控制流
  • 错误处理的局部性
  • 可读性

它没有获得太大的吸引力,但可能对那些在这里发表评论的人感兴趣。

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