Go: 建议:Go 2:使用 || 简化错误处理错误后缀

创建于 2017-07-25  ·  519评论  ·  资料来源: golang/go

关于如何简化 Go 中的错误处理,已经有很多建议,所有的建议都是基于对太多 Go 代码包含这些行的普遍抱怨

if err != nil {
    return err
}

我不确定这里是否有问题需要解决,但由于它不断出现,我将提出这个想法。

大多数关于简化错误处理的建议的核心问题之一是它们只简化了两种处理错误的方式,但实际上有三种:

  1. 忽略错误
  2. 返回未修改的错误
  3. 返回带有附加上下文信息的错误

忽略错误已经很容易(也许太容易了)(参见 #20803)。 许多现有的错误处理建议使返回未修改的错误更容易(例如,#16225、#18721、#21146、#21155)。 很少有人可以更轻松地返回带有附加信息的错误。

这个提议松散地基于 Perl 和 Bourne shell 语言,这是语言思想的丰富来源。 我们引入了一种新的语句,类似于表达式语句:调用表达式后跟|| 。 语法是:

PrimaryExpr Arguments "||" Expression

同样,我们引入了一种新的赋值语句:

ExpressionList assign_op PrimaryExpr Arguments "||" Expression

尽管语法在非赋值情况下接受||之后的任何类型,但唯一允许的类型是预先声明的类型error||后面的表达式必须具有可分配给error 。 它可能不是布尔类型,甚至不是可分配给error的命名布尔类型。 (后一个限制是使该提案与现有语言向后兼容所必需的。)

这些新语句只允许在至少有一个结果参数的函数体中出现,并且最后一个结果参数的类型必须是预先声明的类型error 。 被调用的函数同样必须至少有一个结果参数,并且最后一个结果参数的类型必须是预先声明的类型error

执行这些语句时,调用表达式会照常计算。 如果是赋值语句,调用结果会像往常一样赋值给左边的操作数。 然后将最后一个调用结果(如上所述)与error类型进行比较,并与nil 。 如果最后一次调用结果不是nil ,则隐式执行 return 语句。 如果调用函数有多个结果,则除最后一个之外的所有结果都返回零值。 ||后面的表达式作为最后一个结果返回。 如上所述,调用函数的最后一个结果必须具有类型error ,并且表达式必须可分配给类型error

在未赋值的情况下,表达式在引入新变量err并设置为函数调用的最后一个结果的值的范围内计算。 这允许表达式轻松引用调用返回的错误。 在赋值的情况下,表达式是在调用结果的范围内计算的,因此可以直接引用错误。

这就是完整的提案。

例如, os.Chdir函数当前是

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

在这个提议下,它可以写成

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir, err}
    return nil
}

我写这个提案主要是为了鼓励那些想要简化 Go 错误处理的人思考如何轻松地围绕错误包装上下文,而不仅仅是返回未修改的错误。

FrozenDueToAge Go2 LanguageChange NeedsInvestigation Proposal error-handling

最有用的评论

一个简单的想法,支持错误修饰,但需要更剧烈的语言更改(显然不适用于 go1.10)是引入新的check关键字。

它有两种形式: check Acheck A, B

AB需要是error 。 第二种形式只会在错误修饰时使用; 不需要或希望修饰错误的人将使用更简单的形式。

第一种形式(检查 A)

check A计算A 。 如果nil ,它什么都不做。 如果不是nilcheck行为就像return {<zero>}*, A

例子

  • 如果一个函数只是返回一个错误,它可以与check内联使用,所以
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

变成

check UpdateDB()
  • 对于具有多个返回值的函数,您需要进行赋值,就像我们现在所做的那样。
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

变成

a, b, err := Foo()
check err

// use a and b

第二种形式(检查 A、B)

check A, B计算A 。 如果nil ,它什么都不做。 如果不是nilcheck就像return {<zero>}*, B

这是为了错误修饰的需要。 我们仍然检查A ,但是B用于隐式return

例子

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

变成

a, err := Foo()
check err, &BarError{"Bar", err}

笔记

这是一个编译错误

  • 对不计算为error事物使用check语句
  • 在返回值不是{ type }*, error形式的函数中使用check { type }*, error

两个表达式check A, B是短路的。 B如果不评估Anil

实用性注意事项

有对修饰错误的支持,但只有当您确实需要修饰错误时,您才需要为笨拙的check A, B语法付费。

对于if err != nil { return nil, nil, err }样板文件(非常常见), check err在不牺牲清晰度的情况下尽可能简短(请参阅下面的语法注释)。

语法注意事项

我认为这种语法( check .. ,在行的开头,类似于return )是消除错误检查样板而不隐藏控制流中断的好方法隐式回报引入。

上面的<do-stuff> || <handle-err><do-stuff> catch <handle-err>或另一个线程中提出的a, b = foo()?等想法的缺点是,它们隐藏了控制流修改,使流程更难跟随; 前者普通代码行|| <handle-err>机器,后者带有一个小符号,可以出现在任何地方,包括普通代码行末尾,可能多次。

check语句将始终是当前块中的顶级语句,与修改控制流的其他语句具有相同的重要性(例如,早期的return )。

所有519条评论

    syscall.Chdir(dir) || &PathError{"chdir", dir, e}

e是从哪里来的? 错别字?

或者你的意思是:

func Chdir(dir string) (e error) {
    syscall.Chdir(dir) || &PathError{"chdir", dir, e}
    return nil
}

(即隐式err != nil检查是否先将error赋值给result参数,可以在隐式返回之前命名为再次修改?)

唉,搞砸了我自己的例子。 现在固定: e应该是err 。 该提案将err放在范围内,以在不在赋值语句中时保存函数调用的错误值。

虽然我不确定我是否同意这个想法或语法,但我必须感谢您在返回错误之前注意为错误添加上下文。

编写https://github.com/pkg/errors 的@davecheney可能会对此感兴趣

这段代码会发生什么:

if foo, err := thing.Nope() || &PathError{"chdir", dir, err}; err == nil || ignoreError {
}

(如果没有|| &PathError{"chdir", dir, e}部分这甚至不可能,我很抱歉;我试图表达这感觉像是对现有行为的混淆覆盖,而隐式回报是......偷偷摸摸的?)

@object88我不会允许在ifforswitch语句中使用的 SimpleStmt 中的这个新案例。 这可能是最好的,尽管它会使语法稍微复杂化。

但是如果我们不这样做,那么会发生的情况是,如果thing.Nope()返回一个非零错误,那么调用函数将返回&PathError{"chdir", dir, err} (其中err是由调用thing.Nope()设置的变量)。 如果thing.Nope()返回nil错误,那么我们肯定知道err == nilif语句的条件下为真,因此if 语句被执行。 ignoreError变量永远不会被读取。 这里没有对现有行为的歧义或覆盖; 这里介绍的||的处理只有在||之后的表达式不是布尔值时才被接受,这意味着它当前不会编译。

我同意隐性回报是偷偷摸摸的。

是的,我的例子很差。 但是不允许在ifforswitch会解决很多潜在的混淆。

由于考虑的障碍通常在语言中很难做到,我决定看看这个变体在语言中编码有多难。 并不比其他人难多少: https :

我真的不喜欢所有这些使返回参数中的一种类型的值和一个位置特殊的建议。 这个在某些方面实际上更糟,因为它也使err在此特定上下文中成为一个特殊名称。

虽然我当然同意人们(包括我!)应该更厌倦在没有额外上下文的情况下返回错误。

当有其他返回值时,例如

if err != nil {
  return 0, nil, "", Struct{}, wrap(err)
}

阅读肯定会让人厌烦。 我有点喜欢@nigeltaohttps://github.com/golang/go/issues/19642#issuecomment -288559297 中对return ..., err的建议

如果我理解正确,要构建语法树,解析器需要知道要区分的变量的类型

boolean := BoolFunc() || BoolExpr

err := FuncReturningError() || Expr

看起来不太好。

少即是多...

当返回的 ExpressionList 包含两个或更多元素时,它是如何工作的?

顺便说一句,我想要 panicIf 。

err := doSomeThing()
panicIf(err)

err = doAnotherThing()
panicIf(err)

@ianlancetaylor在您的提案示例中, err仍未明确声明,并以“神奇”(预定义语言)的形式引入,对吗?

或者它会是这样的

func Chdir(dir string) error {
    return (err := syscall.Chdir(dir)) || &PathError{"chdir", dir, err}
}

?

另一方面(因为它已经被标记为“语言更改”......)
引入一个新的运算符(!! 或 ??),它在错误 != nil(或任何可空?)

func DirCh(dir string) (string, error) {
    return dir, (err := syscall.Chdir(dir)) !! &PathError{"chdir", dir, err}
}

对不起,如果这太远了:)

我同意 Go 中的错误处理可能是重复的。 我不介意重复,但太多会影响可读性。 “圈复杂度”(无论您是否相信)使用控制流作为复杂度的衡量标准是有原因的。 “if”语句会增加额外的噪音。

但是,建议的语法“||” 阅读起来不是很直观,尤其是因为该符号通常称为 OR 运算符。 另外,如何处理返回多个值和错误的函数?

我只是在这里抛出一些想法。 我们使用错误作为输入而不是使用错误作为输出怎么样? 示例: https :

感谢所有的评论。

@opennota好点。 它仍然可以工作,但我同意这个方面很尴尬。

@mattn我不认为有返回 ExpressionList,所以我不确定你在问什么。 如果调用函数有多个结果,除最后一个之外的所有结果都作为该类型的零值返回。

@mattn panicif没有解决该提案的关键要素,这是一种返回带有附加上下文的错误的简单方法。 而且,当然,今天可以很容易地写出panicif

@tandr是的, err是神奇地定义的,这非常糟糕。 另一种可能性是允许错误表达式使用error来引用错误,这以不同的方式是可怕的。

@tandr我们可以使用不同的运算符,但我没有看到任何大的优势。 它似乎并没有使结果更具可读性。

@henryas我认为该提案解释了它如何处理多个结果。

@henryas谢谢你的例子。 我不喜欢这种方法的一点是它使错误处理成为代码中最突出的方面。 我希望错误处理存在且可见,但我不希望它成为第一件事。 今天确实如此,使用if err != nil习惯用法和错误处理代码的缩进,如果添加任何新功能用于错误处理,它应该保持正确。

再次感谢。

@ianlancetaylor我不知道你是否查看了我的游乐场链接,但它有一个“panicIf”,可以让你添加额外的上下文。

我将在这里重现一个稍微简化的版本:

func panicIf(err error, transforms ...func(error) error) {
  if err == nil {
    return
  }
  for _, transform := range transforms {
    err = transform(err)
  }
  panic(err)
}

巧合的是,我刚刚在 GopherCon做了一个

func DirCh(dir string) (string, error) {
    dir := syscall.Chdir(dir)        =: err; if err != nil { return "", err }
}

其中=:是新的语法,它是:=的镜像,在另一个方向上赋值。 显然,我们也需要一些东西用于= ,这无疑是有问题的。 但总体思路是让读者更容易理解快乐路径,同时又不会丢失信息。

另一方面,当前的错误处理方式确实有一些优点,因为它可以明显地提醒您在一个函数中可能做了太多的事情,并且一些重构可能已经过期了。

我真的很喜欢@billyh在这里提出的语法

func Chdir(dir string) error {
    e := syscall.Chdir(dir) catch: &PathError{"chdir", dir, e}
    return nil
}

或更复杂的例子使用https://github.com/pkg/errors

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

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

    return &p, nil
}

一个简单的想法,支持错误修饰,但需要更剧烈的语言更改(显然不适用于 go1.10)是引入新的check关键字。

它有两种形式: check Acheck A, B

AB需要是error 。 第二种形式只会在错误修饰时使用; 不需要或希望修饰错误的人将使用更简单的形式。

第一种形式(检查 A)

check A计算A 。 如果nil ,它什么都不做。 如果不是nilcheck行为就像return {<zero>}*, A

例子

  • 如果一个函数只是返回一个错误,它可以与check内联使用,所以
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

变成

check UpdateDB()
  • 对于具有多个返回值的函数,您需要进行赋值,就像我们现在所做的那样。
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

变成

a, b, err := Foo()
check err

// use a and b

第二种形式(检查 A、B)

check A, B计算A 。 如果nil ,它什么都不做。 如果不是nilcheck就像return {<zero>}*, B

这是为了错误修饰的需要。 我们仍然检查A ,但是B用于隐式return

例子

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

变成

a, err := Foo()
check err, &BarError{"Bar", err}

笔记

这是一个编译错误

  • 对不计算为error事物使用check语句
  • 在返回值不是{ type }*, error形式的函数中使用check { type }*, error

两个表达式check A, B是短路的。 B如果不评估Anil

实用性注意事项

有对修饰错误的支持,但只有当您确实需要修饰错误时,您才需要为笨拙的check A, B语法付费。

对于if err != nil { return nil, nil, err }样板文件(非常常见), check err在不牺牲清晰度的情况下尽可能简短(请参阅下面的语法注释)。

语法注意事项

我认为这种语法( check .. ,在行的开头,类似于return )是消除错误检查样板而不隐藏控制流中断的好方法隐式回报引入。

上面的<do-stuff> || <handle-err><do-stuff> catch <handle-err>或另一个线程中提出的a, b = foo()?等想法的缺点是,它们隐藏了控制流修改,使流程更难跟随; 前者普通代码行|| <handle-err>机器,后者带有一个小符号,可以出现在任何地方,包括普通代码行末尾,可能多次。

check语句将始终是当前块中的顶级语句,与修改控制流的其他语句具有相同的重要性(例如,早期的return )。

@ALTree ,我不明白你的例子:

a, b, err := Foo()
check err

实现了原著的三值回报:

return "", "", err

除了最终错误之外,它是否只是为所有声明的返回返回零值? 如果您想返回一个有效值和一个错误,例如 Write() 失败时写入的字节数呢?

无论我们采用什么解决方案,都应该最大限度地限制错误处理的普遍性。

关于在行首使用check ,我个人的偏好是在每行的开头看到主要控制流,并且错误处理对该主要控制流的可读性的干扰很小尽可能。 此外,如果错误处理由checkcatch等保留字分开,那么几乎所有现代编辑器都会以某种方式突出显示保留字语法,并使其引人注目甚至如果它在右手边。

@billyh这在上面有解释,在一行上说:

如果不是 nil,则检查就像return {<zero>}*, A

check将返回任何返回值的零值,错误除外(在最后一个位置)。

如果您想返回一个有效值和一个错误呢?

然后您将使用if err != nil {习语。

在许多情况下,您需要更复杂的错误恢复过程。 例如,您可能需要在捕获错误后回滚某些内容,或在日志文件中写入某些内容。 在所有这些情况下,您的工具箱中仍然会有通常的if err习惯用法,并且您可以使用它来启动一个新块,在那里任何类型的错误处理相关操作,无论多么清晰,都可以进行。

无论我们采用什么解决方案,都应该最大限度地限制错误处理的普遍性。

看我上面的回答。 您仍将拥有if以及该语言现在为您提供的任何其他东西。

几乎所有现代编辑器都会突出显示保留字

或许。 但是引入不透明的语法,需要语法高亮才能阅读,并不理想。

这个特定的错误可以通过在语言中引入双重返回功能来修复。
在这种情况下,函数 a() 返回 123:

func a() int {
乙()
返回 456
}
功能 b() {
返回返回 int(123)
}

此工具可用于简化错误处理,如下所示:

func 句柄(var *foo, err error )(var *foo, err error ) {
如果错误!= nil {
返回返回零,错误
}
返回变量,零
}

func client_code() (*client_object, error) {
var obj, err =handle(something_that_can_fail())
// 只有在某些事情没有失败时才会到达
// 否则 client_code 函数会将错误向上传播
断言(错误 == 零)
}

这允许人们编写一个错误处理函数,可以将错误向上传播到堆栈
这样的错误处理函数可以与主代码分开

对不起,如果我弄错了,但我想澄清一点,下面的函数会产生错误, vet警告还是会被接受?

func Chdir(dir string) (err error) {
    syscall.Chdir(dir) || err
    return nil
}

@rodcorsi在这个提议下,你的例子将被接受,没有兽医警告。 这将相当于

if err := syscall.Chdir(dir); err != nil {
    return err
}

如何扩展使用 Context 来处理错误? 例如,给定以下定义:
type ErrorContext interface { HasError() bool SetError(msg string) Error() string }
现在在容易出错的函数中......
func MyFunction(number int, ctx ErrorContext) int { if ctx.HasError() { return 0 } return number + 1 }
在中间函数...
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) return number }
而在上层函数中
func main() { ctx := context.New() no := MyIntermediateFunction(ctx) if ctx.HasError() { log.Fatalf("Error: %s", ctx.Error()) return } fmt.Printf("%d\n", no) }
使用这种方法有几个好处。 首先,它不会分散读者对主要执行路径的注意力。 有最少的“if”语句来指示与主执行路径的偏差。

其次,它不会隐藏错误。 从方法签名中可以看出,如果它接受ErrorContext,那么该函数可能会出错。 在函数内部,它使用正常的分支语句(例如“if”),它显示了如何使用正常的 Go 代码处理错误。

第三,错误会自动冒泡给感兴趣的一方,在这种情况下是上下文所有者。 如果有额外的错误处理,它会清楚地显示出来。 例如,让我们对中间函数进行一些更改以包装任何现有错误:
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) if ctx.HasError() { ctx.SetError(fmt.Sprintf("wrap msg: %s", ctx.Error()) return } number *= 20 number = MyFunction(number, ctx) return number }
基本上,您只需根据需要编写错误处理代码。 您无需手动将它们冒泡。

最后,您作为函数编写者可以决定是否应该处理错误。 使用当前的 Go 方法,很容易做到这一点......
````
//给定以下定义
func MyFunction(number int) 错误

//然后这样做
MyFunction(8) //不检查错误
With the ErrorContext, you as the function owner can make the error checking optional with this:
func MyFunction(ctx ErrorContext) {
如果 ctx != nil && ctx.HasError() {
返回
}
//...
}
Or make it compulsory with this:
func MyFunction(ctx ErrorContext) {
if ctx.HasError() { // 如果 ctx 为 nil,将会恐慌
返回
}
//...
}
If you make error handling compulsory and yet the user insists on ignoring error, they can still do that. However, they have to be very explicit about it (to prevent accidental ignore). For instance:
func UpperFunction(ctx ErrorContext) {
忽略 := context.New()
MyFunction(ignored) //忽略这个

 MyFunction(ctx) //this one is handled

}
````
这种方法对现有语言没有任何改变。

@ALTree Alberto,如何将您的check@ianlancetaylor 的提议混合

所以

func F() (int, string, error) {
   i, s, err := OhNo()
   if err != nil {
      return i, s, &BadStuffHappened(err, "oopsie-daisy")
   }
   // all is good
   return i+1, s+" ok", nil
}

变成

func F() (int, string, error) {
   i, s, err := OhNo()
   check i, s, err || &BadStuffHappened(err, "oopsie-daisy")
   // all is good
   return i+1, s+" ok", nil
}

此外,我们可以限制check只处理错误类型,所以如果你需要多个返回值,它们需要被命名和分配,所以它以某种方式分配“就地”并且表现得像简单的“返回”

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check err |=  &BadStuffHappened(err, "oopsy-daisy")  // assigns in place and behaves like simple "return"
   // all is good
   return i+1, s+" ok", nil
}

如果return有一天会在表达式中被接受,那么check就不需要了,或者成为一个标准函数

func check(e error) bool {
   return e != nil
}

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check(err) || return &BadStuffHappened(err, "oopsy-daisy")
   // all is good
   return i+1, s+" ok", nil
}

虽然最后一个解决方案感觉像 Perl 😄

我不记得最初是谁提出的,但这是另一个语法想法(每个人最喜欢的自行车棚:-)。 我不是说这是个好主意,但如果我们将想法投入锅中......

x, y := try foo()

将相当于:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), err
}

x, y := try foo() catch &FooErr{E:$, S:"bad"}

将相当于:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), &FooErr{E:err, S:"bad"}
}

try形式肯定已经被提出过多次,模表面语法差异。 try ... catch形式很少被提出,但它显然类似于@ALTreecheck A, B构造和@tandr的后续建议。 一个区别是,这是一个表达式,而不是一个语句,因此您可以说:

z(try foo() catch &FooErr{E:$, S:"bad"})

您可以在一个语句中包含多个 try/catch:

p = try q(0) + try q(1)
a = try b(c, d() + try e(), f, try g() catch &GErr{E:$}, h()) catch $BErr{E:$}

虽然我认为我们不想鼓励这样做。 您还需要注意评估顺序。 例如,如果e()返回非零错误,是否评估h()的副作用。

显然,像trycatch这样的新关键字会破坏 Go 1.x 的兼容性。

我建议我们应该挤压这个提案的目标。 此提案将解决什么问题? 将以下三行减少为两行或一行? 这可能是返回/如果的语言改变。

if err != nil {
    return err
}

或者减少检查错误的次数? 这可能是 try/catch 解决方案。

我想建议任何合理的错误处理快捷语法都具有三个属性:

  1. 它不应该出现在它正在检查的代码之前,以便非错误路径突出。
  2. 它不应该在作用域中引入隐式变量,以免读者在存在同名的显式变量时感到困惑。
  3. 它不应该使一个恢复操作(例如, return err )比另一个更容易。 有时一个完全不同的动作可能更可取(比如调用t.Fatal )。 我们也不想阻止人们添加额外的上下文。

鉴于这些限制,似乎一种几乎最小的语法类似于

STMT SEPARATOR_TOKEN VAR BLOCK

例如,

syscall.Chdir(dir) :: err { return err }

这相当于

if err := syscall.Chdir(dir); err != nil {
    return err
}
````
Even though it's not much shorter, the new syntax moves the error path out of the way. Part of the change would be to modify `gofmt` so it doesn't line-break one-line error-handling blocks, and it indents multi-line error-handling blocks past the opening `}`.

We could make it a bit shorter by declaring the error variable in place with a special marker, like

syscall.Chdir(dir) :: { return @err }
``

我想知道这对于返回的非零值和错误是如何表现的。 例如, bufio.Peek 可能同时返回非零值和 ErrBufferFull 。

@mattn您仍然可以使用旧语法。

@nigeltao是的,我明白。 我怀疑这种行为可能会对用户的代码造成错误,因为 bufio.Peek 也返回非零和零。 代码不能隐含值和错误两者。 所以值和错误都应该返回给调用者(在这种情况下)。

ret, err := doSomething() :: err { return err }
return ret, err

@jba你所描述的看起来有点像一个转置的函数组合运算符:

syscall.Chdir(dir) ⫱ func (err error) { return &PathError{"chdir", dir, err} }

但事实上,我们编写的主要是命令式代码,这要求我们不要在第二个位置使用函数,因为部分原因是为了能够提前返回。

所以现在我正在考虑三个相关的观察结果:

  1. 错误处理就像函数组合,但我们在 Go 中做事的方式与 Haskell 的Error monad有点相反:因为我们主要编写命令式而不是顺序代码,我们想要转换错误(以添加上下文)而不是非错误值(我们宁愿只是绑定到一个变量)。

  2. 返回(x, y, error) Go 函数通常意味着更像是(x, y) | error的联合(#19412)。

  3. 在解包或模式匹配联合的语言中,cases 是单独的范围,我们在 Go 中遇到错误的很多麻烦是由于重新声明的变量的意外阴影,可以通过分离这些范围来改进 (#21114)。

所以也许我们真正想要的是=:运算符,但具有某种联合匹配条件:

syscall.Chdir(dir) =? err { return &PathError{"chdir", dir, err} }

```去
n := io.WriteString(w, s) =? 错误{返回错误}

and perhaps a boolean version for `, ok` index expressions and type assertions:
```go
y := m[x] =! { return ErrNotFound }

除了范围界定之外,这与将gofmt更改为更适合单行代码并没有太大区别:

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} }

```去
n, 错误 := io.WriteString(w, s); 如果错误!= nil {返回错误}

```go
y, ok := m[x]; if !ok { return ErrNotFound }

但是范围是一个大问题! 范围问题是这种代码从“有点丑”到“微妙的错误”的界限。

@ianlancetaylor
虽然我是整个想法的粉丝,但我并不是它神秘的类似 perl 的语法的大力支持者。 也许更冗长的语法会不那么令人困惑,例如:

syscall.Chdir(dir) or dump(err): errors.Wrap(err, "chdir failed")

syscall.Chdir(dir) or dump

另外,我不明白在赋值的情况下是否弹出最后一个参数,例如:

resp := http.Get("https://example.com") or dump

让我们不要忘记错误是 go 中的值而不是一些特殊类型。
我们对其他结构无能为力,而对错误则无能为力,反之亦然。 这意味着,如果您了解一般结构,就会了解错误及其处理方式(即使您认为它很冗长)

这种语法需要新老开发人员学习一些新的信息,然后才能开始理解使用它的代码。

仅此一点就使这个提议不值得恕我直言。

我个人更喜欢这种语法

err := syscall.Chdir(dir)
if err != nil {
    return err
}
return nil

超过

if err := syscall.Chdir(dir); err != nil {
    return err
}
return nil

它多出一行,但将预期操作与错误处理分开。 这个表格对我来说是最易读的。

@bcmills

除了范围界定之外,这与将 gofmt 更改为更适合单行程序并没有太大区别

不仅仅是范围界定; 还有左边缘。 我认为这确实会影响可读性。 我认为

syscall.Chdir(dir) =: err; if err != nil { return &PathError{"chdir", dir, err} } 

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} } 

尤其是当它出现在多个连续的行上时,因为您的眼睛可以扫描左边缘以忽略错误处理。

混合@bcmills的想法,我们可以引入条件管道转发运算符。

如果最后一个值不是 nil ,F2函数将被执行。

func F1() (foo, bar){}

first := F1() ?> last: F2(first, last)

带有 return 语句的管道转发的特殊情况

func Chdir(dir string) error {
    syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
    return nil
}

@urandom在另一个 issue 中带来的真实例子
对我来说更具可读性,专注于主要流程

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
    // When bootstrapping, we only want to apt-get update/upgrade
    // and setup the SSH keys. The rest we leave to cloudinit/sshinit.
    udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
    if icfg.Bootstrap != nil {
        udata.ConfigureBasic() ?> err: return nil, err
        return udata, nil
    }
    udata.Configure() ?> err: return nil, err
    return udata, nil
}

func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
    if cloudcfg == nil {
        cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, errors.Trace(err)
    }
    _ = configureCloudinit(icfg, cloudcfg) ?> err: return nil, errors.Trace(err)
    operatingSystem := series.GetOSFromSeries(icfg.Series) ?> err: return nil, errors.Trace(err)
    udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, errors.Trace(err)
    logger.Tracef("Generated cloud init:\n%s", string(udata))
    return udata, nil
}

我同意错误处理不符合人体工程学。 也就是说,当您阅读下面的代码时,您必须将其发音为if error not nil then - 转换为if there is an error then

if err != nil {
    // handle error
}

我希望能够以这种方式表达上述代码 - 在我看来这更具可读性。

if err {
    // handle error
}

只是我的谦虚建议:)

它看起来确实像 perl,它甚至有魔法变量
作为参考,在 perl 中你会做

open (FILE, $file) or die("cannot open $file: $!");

恕我直言,这不值得,我喜欢 go 的一点是错误处理
是明确的并且“在你面前”

如果我们坚持下去,我希望没有魔法变量,我们应该
能够命名错误变量

e := syscall.Chdir(dir) ?> e: &PathError{"chdir", dir, e}

我们不妨使用一个与 || 不同的符号专门针对此任务,
由于向后,我猜像“或”这样的文本符号是不可能的
兼容性

n, _, err, _ = somecall(...) ?> err: &PathError{"somecall", n, err}

2017 年 8 月 1 日星期二下午 2:47,Rodrigo [email protected]写道:

混合想法@bcmills https://github.com/bcmills我们可以介绍
有条件的管道转发操作符。

如果最后一个值不是 nil ,将执行 F2 函数。

func F1() (foo, bar){}
第一个 := F1() ?> 最后一个: F2(第一个,最后一个)

带有 return 语句的管道转发的特殊情况

func Chdir(dir string) 错误 {
syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
返回零
}

真实例子
https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go
@urandom https://github.com/urandom在另一个问题中带来
对我来说更具可读性,专注于主要流程

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
// 引导时,我们只想 apt-get update/upgrade
// 并设置 SSH 密钥。 剩下的我们留给 cloudinit/sshinit。
udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
如果 icfg.Bootstrap != nil {
udata.ConfigureBasic() ?> err: return nil, err
返回 udata,无
}
udata.Configure() ?> err: return nil, err
返回 udata,无
}
func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
如果 cloudcfg == nil {
cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, errors.Trace(err)
}
configureCloudinit(icfg, cloudcfg) ?> err: return nil, errors.Trace(err)
操作系统 := series.GetOSFromSeries(icfg.Series) ?> err: return nil, errors.Trace(err)
udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, errors.Trace(err)
logger.Tracef("生成的云初始化:\n%s", string(udata))
返回 udata,无
}


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

我是唯一一个认为所有这些提议的更改会比当前形式更复杂的人吗?

我认为简单和简洁并不等同或可以互换。 是的,所有这些更改都会缩短一行或多行,但会引入该语言用户必须学习的运算符或关键字。

@rodcorsi我知道这看起来很小,但我认为第二部分是Block很重要:现有的iffor语句都使用块,而selectswitch都使用花括号分隔的语法,因此对于这个特定的控制流操作省略大括号似乎很尴尬。

如果您不必担心新符号后面的任意表达式,那么确保解析树是明确的也容易得多。

我为我的草图考虑的语法和语义是:


NonZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                     ExpressionList assign_op Expression ) "=?" [ identifier ] Block .
ZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                  ExpressionList assign_op Expression ) "=!" Block .

NonZeroGuardStmt执行Block如果的最后一个值Expression不等于它的类型的零值。 如果identifier存在,则它绑定到Block那个值。 甲ZeroGuardStmt执行Block如果的最后一个值Expression等于其类型的零值。

对于:=形式,的另一个(主导)值Expression结合到IdentifierList如在ShortVarDecl 。 标识符在包含范围内声明,这意味着它们在Block中也是可见的。

对于assign_op形式,每个左侧操作数都必须是可寻址的、映射索引表达式或(仅适用于=赋值)空白标识符。 操作数可以用括号括起来。 右侧Expression的其他(前导)值在Assignment中进行评估。 赋值发生在Block执行之前,而不管Block是否随后被执行。


我相信这里建议的语法与 Go 1 兼容: ?不是有效标识符,并且没有使用该字符的现有 Go 运算符,尽管!是有效运算符,但没有现有的生产中,它后面可能跟一个{

@bcmills LGTM,伴随着对 gofmt 的改变。

我原以为你会让=?=!各自成为一个标记,这会使语法变得简单兼容。

我会以为你会=? 和=! 每个标记本身就是一个标记,这将使语法变得简单兼容。

我们可以在语法中这样做,但不能在词法分析器中:序列“=!” 可以出现在有效的 Go 1 代码中 (https://play.golang.org/p/pMTtUWgBN9)。

花括号使我的提案中的解析明确无误: =!目前只能出现在布尔变量的声明或赋值中,并且声明和赋值目前不能立即出现在花括号之前(https ://play.golang.org/p/ncJyg-GMuL) 除非用隐式分号分隔(https://play.golang.org/p/lhcqBhr7Te)。

@romainmenke不。 你并不是唯一的一个。 我看不到单行错误处理的价值。 您可以节省一行,但会增加更多的复杂性。 问题是在许多这些提议中,错误处理部分变得隐藏起来。 这个想法不是因为错误处理很重要而使它们不那么引人注目,而是为了使代码更易于阅读。 简洁不等于易读。 如果您必须对现有的错误处理系统进行更改,我发现传统的 try-catch-finally 比这里的许多想法目的更具吸引力。

我喜欢check提案,因为你也可以扩展它来处理

f, err := os.Open(myfile)
check err
defer check f.Close()

其他提案似乎也不能与defer混合使用。 check也非常易读,如果你不知道的话,对谷歌来说也很简单。 我不认为它需要仅限于error类型。 最后一个位置的任何返回参数都可以使用它。 因此,迭代器可能有一个check用于Next() bool

我曾经写过一个看起来像的扫描仪

func (s *Scanner) Next() bool {
    if s.Error != nil || s.pos >= s.RecordCount {
        return false
    }
    s.pos++

    var rt uint8
    if !s.read(&rt) {
        return false
    }
...

最后一点可能是check s.read(&rt)

@卡姆约翰逊

其他提案似乎也不能与defer混合使用。

如果您假设我们将扩展defer以允许使用新语法从外部函数返回,那么您可以将该假设同样适用于其他提案。

defer f.Close() =? err { return err }

由于@ALTreecheck提案引入了一个单独的语句,我不知道您如何将它与defer混合使用,除了简单地返回错误之外,还可以执行任何其他操作。

defer func() {
  err := f.Close()
  check err, fmt.Errorf(…, err) // But this func() doesn't return an error!
}()

对比:

defer f.Close() =? err { return fmt.Errorf(…, err) }

其中许多建议的理由是更好的“人体工程学”,但我真的不明白这些建议中的任何一个除了制作它之外还有什么更好的地方,所以输入的内容略少。 这些如何提高代码的可维护性? 可组合性? 可读性? 控制流程的难易程度?

@jimmyfrasche

这些如何提高代码的可维护性? 可组合性? 可读性? 控制流程的难易程度?

正如我之前提到的,这些提案中的任何一个的主要优势可能需要来自更清晰的赋值范围和err变量:参见 #19727、#20148、#5634、#21114,以及其他可能的各种人们遇到与错误处理相关的范围界定问题的方式。

@bcmills感谢您提供动力,很抱歉我在您之前的帖子中错过了它。

然而,鉴于这个前提,为所有变量都可以使用的“更清晰的赋值范围”提供更通用的工具不是更好吗? 当然,我也无意中隐藏了我的非错误变量份额。

我记得当:=的当前行为被引入时——很多疯狂的线程都在吵着要一种方法来显式地注释要重用的名称,而不是隐含的“仅当该变量存在于正是当前的范围”,根据我的经验,这是所有难以察觉的微妙问题体现出来的地方。

† 我找不到那个帖子 有人有链接吗?

我认为 Go 有很多可以改进的地方,但是:=的行为总是让我觉得这是一个严重的错误。 也许重新审视:=的行为是解决根本问题或至少减少对其他更极端变化的需求的方法?

@jimmyfrasche

然而,鉴于这个前提,为所有变量都可以使用的“更清晰的赋值范围”提供更通用的工具不是更好吗?

是的。 这是我喜欢@jba和我提出的=?::运算符的其中一件事:它也很好地扩展到(公认的有限子集)非错误。

就我个人而言,我怀疑从长远来看我会更高兴使用更明确的标记联合/varint/algebraic 数据类型功能(另请参阅 #19412),但这是对语言的更大更改:很难看出我们将如何改造到混合 Go 1 / Go 2 环境中的现有 API。

控制流程的难易程度?

在我和@bcmills的建议中,您的眼睛可以向下扫视左侧并轻松接受无错误控制流。

@bcmills我想我至少负责 #

当涉及返回有错误的东西时,有四种情况

  1. 只是一个错误(不需要做任何事情,只返回一个错误)
  2. 东西和错误(你会像现在一样处理它)
  3. 一件事或一个错误(你可以使用 sum 类型!:tada:)
  4. 两件或更多件事情或错误

如果你击中 4,那就是事情变得棘手的地方。 如果不引入元组类型(未标记的产品类型与结构的标记产品类型一起使用),如果您想使用和类型来建模“这个或错误”,则必须通过将所有内容捆绑在一个结构中来将问题减少到情况 3。

引入元组类型会导致各种问题和兼容性问题以及奇怪的重叠( func() (int, string, error)是隐式定义的元组还是多个返回值是一个单独的概念?如果它是一个隐式定义的元组,那么这是否意味着func() (n int, msg string, err error)是一个隐式定义的结构体!?如果这是一个结构体,如果我不在同一个包中,我该如何访问这些字段!)

我仍然认为 sum 类型提供了很多好处,但它们当然没有做任何事情来解决范围界定的问题。 如果有的话,它们会使情况变得更糟,因为您可以隐藏整个“结果或错误”总和,而不仅仅是在结果案例中有某些内容时隐藏错误案例。

@jba我不明白这是一个理想的属性。 除了使控制流二维化的概念普遍缺乏易用性之外,可以这么说,我也想不出为什么它不是。 你能向我解释一下好处吗?

如果不引入元组类型 [...],您必须[捆绑] 结构中的所有内容,如果您想使用和类型来模拟“这个或错误”。

我对此没有意见:我认为这样我们会有更多可读的调用站点(不再有意外转置的位置绑定!),并且#12854 将减轻当前与结构返回相关的大量开销。

最大的问题是迁移:我们如何从 Go 1 的“值和错误”模型到 Go 2 中潜在的“值错误”模型,特别是考虑到像io.Writer这样的 API 确实返回“值”错误”?

我仍然认为 sum 类型提供了很多好处,但它们当然没有做任何事情来解决范围界定的问题。

这取决于你如何打开它们,我想这会让我们回到今天的位置。 如果您更喜欢联合,也许您可​​以将=?的版本设想为“非对称模式匹配”API:

i := match strconv.Atoi(str) | err error { return err }

其中match将是传统的 ML 样式模式匹配操作,但在非穷举匹配的情况下将返回值(如果联合有多个未匹配的替代方案,则作为interface{} )而不是因非彻底的比赛失败而恐慌。

我刚刚在https://github.com/mpvl/errd签入了一个包,该包以编程方式解决了此处讨论的问题(无语言更改)。 这个包最重要的方面是它不仅缩短了错误处理的时间,而且更容易正确地进行处理。 我在文档中举例说明了传统的惯用错误处理如何比看起来更棘手,尤其是在与 defer 的交互中。

不过,我认为这是一个“燃烧器”包; 目的是获得一些很好的经验和洞察力,如何最好地扩展语言。 它与泛型很好地交互,顺便说一句,如果这会成为一件事。

仍在研究更多示例,但此包已准备好进行试验。

@bcmills一百万:+1:对于#12854

正如您所指出的,有“返回 X 和错误”和“返回 X 或错误”,因此如果没有一些宏将旧方式按需转换为新方式,您就无法真正解决这个问题(当然会有错误或者至少在不可避免地将它用于“X 和错误”函数时会出现运行时恐慌)。

我真的不喜欢在语言中引入特殊宏的想法,特别是如果它只是为了错误处理,这是我在很多这些建议中的主要问题。

Go 对糖或魔法的影响不大,这是一个加分项。

当前实践中编码的信息有太多的惯性和太少的信息,无法处理向功能性更强的错误处理范式的大规模飞跃。

如果 Go 2 获得 sum 类型——坦率地说这会让我震惊(以一种很好的方式!)——如果有的话,它必须是一个非常缓慢的渐进过程才能转向“新风格”,与此同时,甚至会有在如何处理错误方面有更多的碎片和混乱,所以我不认为这是一个积极的因素。 (然而,我会立即开始将它用于chan union { Msg1 T; Msg2 S; Err error }类的东西,而不是三个频道)。

如果这是 Go1 之前的比赛,而 Go 团队可以说“我们将在接下来的六个月内将所有东西都搬过来,当它损坏时,请跟上”那将是一回事,但现在我们基本上被卡住了即使我们确实得到了和类型。

正如您所指出的,有“返回 X 和错误”和“返回 X 或错误”,因此如果没有一些根据需要将旧方式转换为新方式的宏,您就无法真正解决这个问题

正如我在上面所说的,我认为新方法没有必要涵盖“返回 X 和错误”,无论它是什么。 如果绝大多数情况是“返回 X 或错误”,而新方法仅对此进行了改进,那就太好了,您仍然可以使用旧的、与 Go 1 兼容的方法来处理更罕见的“返回 X 和错误”。

@nigeltao是的,但我们仍然需要某种方式在转换期间将它们区分开来,除非您提议我们将整个标准库保留在现有样式中。

@jimmyfrasche我不认为我可以为它构建一个论点。 您可以观看我的演讲或查看 repo README 中的示例。 但是,如果视觉证据对您没有说服力,那么我无话可说。

@jba观看了演讲并阅读了自述文件。 我现在明白你从哪里来的括号/脚注/尾注/旁注(我是旁注(和括号)的粉丝)。

如果目标是由于缺乏更好的术语而将不愉快的路径放在一边,那么 $EDITOR 插件将无需更改语言即可工作,并且可以与所有现有代码一起使用,而不管代码作者的偏好如何。

语言更改使语法更加紧凑。 @bcmills提到这可以改进范围,但我真的不明白它是如何做到的,除非它的范围规则与:=但这似乎会引起更多混乱。

@bcmills我不明白你的评论。 您显然可以将它们区分开来。 旧的方式是这样的:

err := foo()
if err != nil {
  return n, err  // n can be non-zero
}

新方式看起来像

check foo()

或者

foo() || &FooError{err}

或任何自行车棚颜色。 我猜大多数标准库都可以转换,但并非所有标准库都必须转换。

添加到@ianlancetaylor的要求:简化错误消息不仅应该使事情更短,而且更容易正确处理错误。 将恐慌和下游错误传递给延迟函数很难做到正确。

例如,考虑写入 Google Cloud Storage 文件,我们希望在出现任何错误时中止写入文件:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer func() {
        if r := recover(); r != nil {
            w.CloseWithError(fmt.Errorf("panic: %v", r))
            panic(r)
        }
        if err != nil {
            _ = w.CloseWithError(err)
        } else {
            err = w.Close()
        }
    }
    _, err = io.Copy(w, r)
    return err
}

这段代码的微妙之处包括:

  • 来自 Copy 的错误通过命名的返回参数偷偷地传递给 defer 函数。
  • 为了完全安全,我们从 r 中捕获恐慌并确保在恢复恐慌之前中止写入。
  • 忽略第一个 Close 的错误是有意的,但有点像懒惰的程序员神器。

使用包 errd,此代码如下所示:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client, err := storage.NewClient(ctx)
        e.Must(err)
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _, err = io.Copy(w, r)
        e.Must(err)
    })
}

errd.Discard是一个错误处理程序。 错误处理程序还可用于包装、记录任何错误。

e.Must相当于foo() || wrapError

e.Defer是额外的,用于处理将错误传递给延迟。

使用泛型,这段代码可能类似于:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.Must(storage.NewClient(ctx))
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _ = e.Must(io.Copy(w, r))
    })
}

如果我们标准化用于 Defer 的方法,它甚至可能看起来像:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.DeferClose(e.Must(storage.NewClient(ctx)), errd.Discard)
       e.Must(io.Copy(e.DeferClose(client.Bucket(bucket).Object(dst).NewWriter(ctx)), r)
    })
}

其中 DeferClose 选择 Close 或 CloseWithError。 不是说这更好,而只是展示了可能性。

无论如何,我上周在阿姆斯特丹的一次聚会上就这个话题做了一个演讲,看起来更容易正确处理错误的能力被认为比缩短它更有用。

改善错误的解决方案应该至少关注使事情更容易做对,而不是让事情变得更短。

@ALTree错误处理

@jimmyfrasche : errd 大致与您的 Playground 示例所做的一样,但也会将错误和恐慌传递给延迟。

@jimmyfrasche :我同意大多数提案并没有对代码中已经可以实现的内容增加太多。

@romainmenke :同意过于关注简洁。 为了更容易正确地做事,应该有更大的重点。

@jba : errd 方法使扫描错误与非错误流变得相当容易,只需查看左侧(以 e. 开头的任何内容都是错误或延迟处理)。 它还可以很容易地扫描哪些返回值被处理为错误或延迟,哪些没有。

@bcmills :虽然 errd 本身并没有解决范围界定问题,但它消除了将下游错误传递给先前声明的错误变量等的需要,从而大大减轻了错误处理的问题,AFAICT。

errd 似乎完全依赖于恐慌和恢复。 听起来它带来了显着的性能损失。 因此,我不确定这是一个整体解决方案。

@urandom :在
如果原始代码:

  • 不使用延迟:使用 errd 的惩罚很大,大约 100ns*。
  • 使用惯用的延迟:运行时间或 errd 是相同的顺序,虽然有点慢
  • 对 defer 使用适当的错误处理:运行时间大致相等; 如果延迟数 > 1,则 errd 会更快

其他开销:

  • 与使用 DeferClose 或 DeferFunc API(参见版本 v0.1.0)相比,将闭包 (w.Close) 传递给 Defer 目前也增加了大约 25ns* 的开销。 在与@rsc讨论后,我将其删除以保持 API 简单并担心以后会优化。
  • 使用 Go 1.8 将内联错误字符串包装为处理程序 ( e.Must(err, msg("oh noes!") ) 的成本约为 30ns。 然而,使用小费 (1.9),虽然仍然是一个分配,但我将成本计时为 2ns。 当然,对于预先声明的错误消息,成本仍然可以忽略不计。

(*) 在我的 2016 款 MacBook Pro 上运行的所有数字。

总而言之,如果您的原始代码使用 defer,成本似乎是可以接受的。 如果没有,Austin 正在努力显着降低延迟成本,因此成本甚至可能会随着时间的推移而下降。

无论如何,这个包的目的是获得关于使用替代错误处理的体验和现在如何有用,这样我们就可以在 Go 2 中构建最好的语言添加。 一个例子是当前的讨论,它过于关注减少简单案例的几行,而有更多的收获,可以说其他点更重要。

@jimmyfrasche

那么 $EDITOR 插件就可以在不改变语言的情况下工作

是的,这正是我在演讲中所争论的。 在这里,我认为如果我们进行语言更改,它应该符合“旁注”概念。

@nigeltao

您显然可以将它们区分开来。 旧的方式是这样的:

我说的是声明点,而不是使用点。

此处讨论的一些建议在呼叫站点上没有区分这两者,但有些建议这样做。 如果我们选择假定“值错误”的选项之一 - 例如||try … catchmatch - 那么使用它应该是编译时错误该语法带有“值错误”函数,应该由函数的实现者来定义它是哪个。

在声明点,目前没有办法区分“值和错误”和“值或错误”:

func Atoi(string) (int, error)

func WriteString(Writer, String) (int, error)

具有相同的返回类型,但不同的错误语义。

@mpvl我正在查看

如果我们忽略诸如对 WithDefault() 的结果进行操作的顶级函数之类的常见助手,并假设,为了简单起见,我们总是使用上下文,而忽略为性能所做的任何决定,那么绝对最小的准系统 API 会减少到下面的操作?

type Handler = func(ctx context.Context, panicing bool, err error) error
Run(context.Context, func(*E), defaults ...Handler) //egregious style but most minimal
type struct E {...}
func (*E) Must(err error, handlers ...Handler)
func (*E) Defer(func() error, handlers ...Handler)

看看代码,我看到了一些很好的理由,它没有像上面那样定义,但我试图了解核心语义以便更好地理解这个概念。 例如,我不确定IsSentinel是否在核心中。

@jimmyfrasche

@bcmills提到这可以改善范围,但我真的不明白它是如何做到的

主要的改进是将err变量保持在范围之外。 这将避免诸如从https://github.com/golang/go/issues/19727链接的错误

    res, err := ctxhttp.Get(ctx, c.HTTPClient, dirURL)
    if err != nil {
        return Directory{}, err
    }
    defer res.Body.Close()
    c.addNonce(res.Header)
    if res.StatusCode != http.StatusOK {
        return Directory{}, responseError(res)
    }

    var v struct {
        …
    }
    if json.NewDecoder(res.Body).Decode(&v); err != nil {
        return Directory{}, err
    }

该错误出现在最后一个 if 语句中:来自Decode的错误已被删除,但这并不明显,因为来自早期检查的err仍在范围内。 相比之下,使用::=?运算符,可以这样写:

    res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err }
    defer res.Body.Close()
    c.addNonce(res.Header)
    (res.StatusCode == http.StatusOK) =! { return Directory{}, responseError(res) }

    var v struct {
        …
    }
    json.NewDecoder(res.Body).Decode(&v) =? err { return Directory{}, err }

这里有两个范围界定改进有助于:

  1. 第一个err (来自较早的Get调用)仅在return块的范围内,因此不会在后续检查中意外使用。
  2. 因为errDecode是在检查它是否为零的同一语句中声明的,所以声明和检查之间不能有偏差。

(1) 本身就足以在编译时揭示错误,但是 (2) 在以明显的方式使用保护语句时很容易避免。

@bcmills感谢您的澄清

因此,在res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err }=?宏扩展为

var res *http.Reponse
{
  var err error
  res, err = ctxhttp.Get(ctx, c.HTTPClient, dirURL)
  if err != nil {
    return Directory{}, err 
  }
}

如果这是正确的,这就是我所说的意思,它必须与:=具有不同的语义。

似乎它会引起自己的困惑,例如:

func f() error {
  var err error
  g() =? err {
    if err != io.EOF {
      return err
    }
  }
  //one could expect that err could be io.EOF here but it will never be so
}

除非我误解了什么。

是的,这是正确的扩展。 你是对的,它与:= ,这是故意的。

似乎它会引起自己的混乱

这是真的。 我不清楚这在实践中是否会造成混淆。 如果是,我们可以提供用于声明的保护语句的“:”变体(并且仅分配“=”变体)。

(现在这让我觉得运算符应该拼写为?!而不是=?=! 。)

res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) ?: err { return Directory{}, err }

func f() error {
  var err error
  g() ?= err { (err == io.EOF) ! { return err } }
  // err may be io.EOF here.
}

@mpvl我对errd主要担忧是 Handler 接口:它似乎鼓励函数式回调管道,但我对回调/延续式代码的经验(无论是在 Go 和 C++ 等命令式语言中,还是在函数式像ML和Haskell)语言是,它往往更难跟踪比同等顺序/势在必行的风格,这也恰好与围棋成语的休息对齐。

您是否将Handler -style 链视为 API 的一部分,或者您的Handler是您正在考虑的其他一些语法的替代品(例如在Block上运行的某些东西? s?)

@bcmills我还是登不上与魔法功能,引进几十理念融入到语言到一个单一的线,并与一件事唯一的工作,但我终于知道为什么他们更不仅仅是一个稍短的方式来写x, err := f(); if err != nil { return err } 。 感谢您帮助我理解并抱歉花了这么长时间。

@bcmills我使用最新的=?提议重写了@mpvl的激励示例,该示例有一些粗糙的错误处理,该提议并不总是声明新的 err 变量:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)

        defer func() {
                if r := recover(); r != nil { // r is interface{} not error so we can't use it here
                        _ = w.CloseWithError(fmt.Errorf("panic: %v", r))
                        panic(r)
                }

                if err != nil { // could use =! here but I don't see how that simplifies anything
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()

        io.Copy(w, r) =? err { return err } // what about n? does this need to be prefixed by a '_ ='?
        return nil
}

大多数错误处理没有改变。 我只能在两个地方使用=? 。 首先,它并没有真正提供我能看到的任何好处。 在第二个中,它使代码变长并掩盖了io.Copy返回两个东西的事实,所以最好不要在那里使用它。

@jimmyfrasche该代码是例外,而不是规则。 我们不应该设计功能以使其更易于编写。

另外,我质疑recover是否应该在那里。 如果w.Writer.Read (或io.Copy !)恐慌,最好终止。

没有recover ,就没有真正需要defer ,函数的底部可能会变成

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

@jimmyfrasche

// r is interface{} not error so we can't use it here

请注意,我的具体措辞(在 https://github.com/golang/go/issues/21161#issuecomment-319434101 中)是关于零值的,而不是专门的错误。

// what about n? does this need to be prefixed by a '_ ='?

事实并非如此,尽管我本可以更明确地说明这一点。

我并不特别喜欢@mpvl在那个例子中使用recover :它鼓励在惯用的控制流上使用恐慌,而如果有的话,我认为我们应该消除无关的recover调用(例如fmt的那些,来自 Go 2 的标准库。

使用这种方法,我会将该代码编写为:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        io.Copy(w, r) =? err {
                w.CloseWithError(err)
                return err
        }
        return w.Close()
}

另一方面,您是正确的,使用单惯恢复几乎没有机会应用旨在支持惯用错误处理的功能。 但是,将恢复与Close操作分开确实会导致 IMO 代码更简洁。

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        defer func() {
                if err != nil {
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()
        defer func() {
                recover() =? r {
                        err = fmt.Errorf("panic: %v", r)
                        panic(r)
                }
        }()

        io.Copy(w, r) =? err { return err }
        return nil
}

@jba延迟处理程序再次恐慌:它在那里尝试通知另一台计算机上的进程,因此它不会意外地提交错误的事务(假设在潜在错误状态下仍然可能)。 如果这不是很常见,它可能应该是。 我同意读/写/复制不应该恐慌,但如果那里有其他代码可以合理地恐慌,无论出于何种原因,我们都会回到我们开始的地方。

@bcmills上次修订确实看起来更好(即使您取出=? ,真的)

@jba

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

这仍然没有涵盖读者恐慌的情况。 诚然,这是一种罕见的情况,但非常重要:在发生恐慌时在此处调用 Close 非常糟糕。

该代码是例外,而不是规则。 我们不应该设计功能以使其更易于编写。

@jba :在这种情况下,我完全不同意。 正确处理错误很重要。 让简单的案例变得更容易将鼓励人们更少考虑正确的错误处理。 我希望看到一些方法,比如errd ,使保守的错误处理变得非常容易,同时需要一些努力来放松规则,而不是任何稍微向另一个方向移动的东西。

@jimmyfrasche :关于您的简化:您大致正确。

  • IsSentinel 不是必需的,只是方便和常用。 我放弃了,至少现在是这样。
  • 状态中的 Err 与 err 不同,因此您的 API 删除了它。 不过,这对于理解并不重要。
  • 处理程序可以是函数,但主要是出于性能原因的接口。 我只知道如果没有优化,很多人不会使用这个包。 (请参阅本期对 errd 的一些第一批评论)
  • 上下文很不幸。 AppEngine 需要它,但我认为其他的并不多。 我会很好地取消对它的支持,直到人们退缩。

@mpvl我只是

@jimmyfrasche :理解,尽管 API 不要求您这样做很好。 :)

@bcmills :处理程序有几个目的,例如,按重要性排序:

  • 包装错误
  • 定义忽略错误(使其明确。参见示例)
  • 记录错误
  • 错误度量

再次按重要性排序,这些需要由以下范围确定:

  • 堵塞
  • 线
  • 包裹

默认错误只是为了更容易保证在某处处理错误,
但我只能忍受块级。 我最初有一个带有选项而不是处理程序的 API。 不过,这导致 API 更大、更笨拙,而且速度更慢。

我不认为回调问题在这里那么糟糕。 用户通过传递一个 Handler 来定义一个 Runner,如果出现错误就会调用它。 在处理错误的块中明确指定了特定的运行器。 在许多情况下,处理程序只是一些内联传递的包装字符串文字。 我会花一些时间来看看什么有用,什么没用。

顺便说一句,如果我们不鼓励在处理程序中记录错误,那么上下文支持可能会被删除。

@jba

另外,我质疑恢复是否应该在那里。 如果 w.Write 或 r.Read(或 io.Copy!)恐慌,最好终止。

如果出现恐慌,writeToGS 仍然会终止,因为它应该(!!!),它只是确保它调用 CloseWithError 并带有非零错误。 如果未处理紧急情况,仍会调用 defer,但会使用 err == nil,从而导致 Cloud Storage 上出现潜在损坏的文件。 这里正确的做法是使用一些临时错误调用 CloseWithError 然后继续恐慌。

我在 Go 代码中发现了一堆这样的例子。 处理 io.Pipes 也经常导致代码有点过于微妙。 处理错误通常并不像您现在看到的那么简单。

@bcmills

我不是特别喜欢@mpvl在那个例子中使用恢复:它鼓励使用恐慌而不是惯用的控制流,

不试图鼓励使用恐慌。 请注意,在 CloseWithError 之后立即重新引发恐慌,因此不会以其他方式改变控制流。 恐慌仍然是恐慌。
但是,在这里不使用recover 是错误的,因为panic 会导致调用defer 并返回nil 错误,这表明可以提交到目前为止已写入的内容。

在这里不使用恢复的唯一有点有效的论点是,即使对于任意 Reader(在此示例中 Reader 的类型未知,原因是 :) ,也不太可能发生恐慌。
但是,对于生产代码,这是不可接受的立场。 特别是当编程规模足够大时,这肯定会在某个时候发生(恐慌可能是由代码中的错误以外的其他原因引起的)。

顺便说一句,请注意errd包消除了用户考虑这一点的需要。 但是,任何其他在发生恐慌的情况下发出错误信号的机制都可以推迟。 不调用延迟恐慌也可以,但这有其自身的问题。

在声明点,目前没有办法区分“值和错误”和“值或错误”:

@bcmills哦,我明白了。 要打开另一罐自行车棚,我想你可以说

func Atoi(string) ?int

代替

func Atoi(string) (int, error)

但 WriteString 将保持不变:

func WriteString(Writer, String) (int, error)

我喜欢@bcmills / @jba=? / =! / :=? / :=!提案比类似提案更好。 它有一些不错的特性:

  • 可组合(您可以在=?块中使用=?
  • 一般(只关心零值,不特定于错误类型)
  • 改进范围
  • 可以与 defer 一起使用(在上面的一种变体中)

它也有一些我觉得不太好的属性。

组成巢。 重复使用将继续缩进越来越远。 这本身并不一定是件坏事,但我想在错误处理非常复杂的情况下,需要处理导致错误的错误导致错误,处理它的代码很快就会比当前的状态更不清晰。 在这种情况下,我们可以将=?用于最外层错误,而if err != nil用于内部错误,但是这真的改善了一般的错误处理还是仅在常见情况下? 也许只需要改进普通案例,但我个人觉得它并不引人注目。

它将虚假性引入语言以获得其一般性。 虚假被定义为“是(不是)零值”是完全合理的,但if err != nil {if err {更好,因为它是明确的,在我看来。 我希望在野外看到尝试使用=? /etc 的扭曲。 通过更自然的控制流来尝试访问其虚假性。 那肯定是单调的,不受欢迎的,但它会发生。 虽然功能的潜在滥用本身并不是反对功能的论据,但它是需要考虑的事情。

改进的范围(对于声明其参数的变体)在某些情况下很好,但如果需要修复范围,一般来说修复范围。

“唯一最右边的结果”语义是有道理的,但对我来说似乎有点奇怪。 这与其说是争论,不如说是一种感觉。

该提案增加了语言的简洁性,但没有额外的权力。 它可以完全作为进行宏扩展的预处理器来实现。 这当然是不可取的:它会使构建和片段开发复杂化,并且任何这样的预处理器都将非常复杂,因为它必须是类型感知和卫生的。 我并不是想通过说“只做一个预处理器”来忽略它。 我提出这个只是为了指出这个提案完全是糖。 它不会让你做任何你现在在 Go 中不能做的事情; 它只是让你写得更紧凑。 我并不教条地反对糖。 精心选择的语言抽象具有强大的力量,但它是糖的事实意味着它应该被视为👎,直到被证明是无辜的,可以这么说。

运算符的 lhs 是一个语句,但是是非常有限的语句子集。 该子集中包含哪些元素是不言而喻的,但是,如果不出意外,则需要重构语言规范中的语法以适应变化。

会不会喜欢

func F() (S, T, error)

func MustF() (S, T) {
  return F() =? err { panic(err) }
}

被)允许?

如果

defer f.Close() :=? err {
    return err
}

被允许必须(以某种方式)等价于

func theOuterFunc() (err error) {
  //...
  defer func() {
    if err2 := f.Close(); err2 != nil {
      err = err2
    }
  }()
  //...
}

这似乎很成问题,并且可能会导致非常混乱的情况,甚至以一种非常类似的方式忽略它,它隐藏了隐式分配闭包的性能影响。 另一种方法是让return从隐式闭包返回,然后有一条错误消息,您不能从func()返回类型error的值,这有点钝。

真的,除了稍微改进的范围修复之外,这并没有解决我在处理 Go 中的错误时遇到的任何问题。 最多输入if err != nil { return err }是一件麻烦事,以我在 #21182 中表达的轻微可读性问题为模。 最大的两个问题是

  1. 思考如何处理错误——语言对此无能为力
  2. 内省错误以确定在某些情况下要做什么——一些额外的约定在errors包的支持下会有很长的路要走,尽管它们不能解决所有问题。

我意识到这些不是唯一的问题,许多人发现其他方面更直接相关,但它们是我花费最多时间的问题,并且比其他任何事情都更令人烦恼和麻烦。

更好的静态分析来检测我何时搞砸了一些东西,当然,总是值得赞赏的(而且一般来说,不仅仅是这种情况)。 语言更改和约定使分析源更容易,因此这些更有用也很有趣。

我刚刚写了很多(很多!对不起!)关于这个,但我并没有拒绝这个提议。 我确实认为它有优点,但我不相信它可以清除酒吧或拉动它的重量。

@jimmyfrasche

我记得当 := 的当前行为被引入时——很多疯狂的线程都在吵着要一种方法来显式地注释要重用的名称,而不是隐式的“仅当该变量存在于当前范围内时才重用” “根据我的经验,这就是所有难以察觉的微妙问题表现出来的地方。

† 我找不到那个帖子 有人有链接吗?

我想你一定记得一个不同的话题,除非你在 Go 发布时参与了它。 2009/11/9 的规范,就在它发布之前,有:

与常规变量声明不同,简短的变量声明可以重新声明变量,前提是它们最初在同一块中以相同的类型声明,并且至少有一个非空变量是新的。

我记得在第一次阅读规范时看到这一点,并认为这是一个很好的规则,因为我以前使用了一种带有 := 的语言,但没有重用规则,并且为同一事物考虑新名称很乏味。

@mpvl
我认为您原始示例的棘手之处更多是由于
你在那里使用的 API 而不是 Go 的错误处理本身。

不过,这是一个有趣的例子,特别是因为
如果您感到恐慌,您不想正常关闭文件,因此
正常的 "defer w.Close()" 成语不起作用。

如果您不需要避免在出现问题时调用 Close
恐慌,那么你可以这样做:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    if err != nil {
        w.CloseWithError(err)
    }
    return err
}

假设语义已更改,以便调用 Close
调用 CloseWithError 之后是一个空操作。

我认为这看起来不再那么糟糕了。

即使要求在出现恐慌时不会无错误地写入文件也不应该太难适应; 例如,通过添加必须在关闭之前显式调用的 Finalize 函数。

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    return w.Finalize(err)

虽然这不能附加恐慌错误消息,但体面的日志记录可以使这一点更清楚。
(Close 方法甚至可以在其中进行恢复调用,尽管我不确定那是不是
实际上是一个非常糟糕的主意......)

然而,我认为这个例子的恐慌恢复方面在这种情况下有点红鲱鱼,因为 99+% 的错误处理案例不做恐慌恢复。

@rogpeppe

虽然这不能附加恐慌错误消息,但体面的日志记录可以使这一点更清楚。

我不认为这是一个问题。

不过,您提议的 API 更改可以缓解但尚未完全解决问题。 所需的语义也需要其他代码才能正常运行。 考虑下面的例子:

r, w := io.Pipe()
go func() {
    var err error                // used to intercept downstream errors
    defer func() {
        w.CloseWithError(err)
    }()

    r, err := newReader()
    if err != nil {
        return
    }
    defer func() {
        if errC := r.Close(); errC != nil && err == nil {
            err = errC
        }
    }
    _, err = io.Copy(w, r)
}()
return r

就其本身而言,这段代码表明错误处理可能很棘手,或者至少是凌乱的(我很好奇如何通过其他提案改进这一点):它通过一个变量悄悄地将下游错误向上传递,并且有点太笨重的 if 语句以确保传递正确的错误。 两者都过多地分散了“业务逻辑”的注意力。 错误处理支配着代码。 这个例子甚至还没有处理恐慌。

为了完整起见,在errd这个 _would_ 会正确处理恐慌,看起来像:

r, w := io.Pipe()
go errd.Run(func(e *errd.E) {
    e.Defer(w.CloseWithError)

    r, err := newReader()
    e.Must(err)
    e.Defer(r.Close)

    _, err = io.Copy(w, r)
    e.Must(err)
})
return r

如果上述读取器(不使用errd )作为读取器传递给 writeToGS,并且 newReader 返回的 io.Reader 恐慌,它仍然会导致您提出的 API 修复的语义错误(可能会成功关闭管道因恐慌关闭后的 GS 文件,错误为零。)

这也证明了这一点。 在 Go 中推理正确的错误处理是很重要的。 当我查看使用errd重写代码的样子时,我发现了一堆有问题的代码。 不过,在为errd包编写单元测试时,我才真正了解到编写正确的 Go 错误处理是多么困难和微妙。 :)

您提议的 API 更改的替代方法是完全不处理任何延迟。 这有其自身的问题,并不能完全解决问题,并且可能是不可撤销的,但会有一些不错的品质。

无论哪种方式,最好是进行一些语言更改,以减轻错误处理的微妙之处,而不是专注于简洁。

@mpvl
我经常发现在 Go 中使用错误处理代码创建另一个函数可以清理事情。 我会在上面写你的代码是这样的:

func something() {
    r, w := io.Pipe()
    go func() {
        err := copyFromNewReader(w)
        w.CloseWithError(err)
    }()
    ...
}

func copyFromNewReader(w io.Writer) error {
    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()
    _, err = io.Copy(w, r)
    return err
}()

我假设 r.Close 不会返回一个有用的错误——如果你已经通读了一个阅读器并且只遇到了 io.EOF,那么它在关闭时是否返回一个错误几乎可以肯定无关紧要。

我对 errd API 不感兴趣——它对启动的 goroutine 太敏感了。 例如: https :
function in 应该不会影响程序的正确性,但是在使用 errd 时,它会影响。 您期望恐慌能够安全地穿越抽象边界,而在 Go 中则不然。

@mpvl

推迟 w.CloseWithError(err)

顺便说一句,这一行总是使用 nil 错误值调用 CloseWithError。 我想你是想
写:

defer func() { 
   w.CloseWithError(err)
}()

@mpvl

请注意, io.Reader上的Close方法返回的错误几乎没有用(请参阅 https://github.com/golang/go/issues/20803#issuecomment-312318808 中的列表)。

这表明我们今天应该把你的例子写成:

r, w := io.Pipe()
go func() (err error) {
    defer func() { w.CloseWithError(err) }()

    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()

    _, err = io.Copy(w, r)
    return err
}()
return r

...这对我来说似乎很好,除了有点冗长。

确实,它会在发生恐慌时将 nil 错误传递给w.CloseWithError ,但无论如何整个程序都会在此时终止。 如果永远不要以零错误关闭很重要,这是一个简单的重命名加上一个额外的行:

-go func() (err error) {
-   defer func() { w.CloseWithError(err) }()
+go func() (rerr error) {
+   rerr = errors.New("goroutine exited by panic")
+   defer func() { w.CloseWithError(rerr) }()

@rogpeppe :确实,谢谢。 :)

是的,我知道 goroutine 问题。 这是令人讨厌的,但可能是通过兽医检查不难发现的东西。 无论如何,我不认为 errd 是最终解决方案,而是将其视为获得如何最好地解决错误处理的经验的一种方式。 理想情况下,语言更改可以解决相同的问题,但会施加适当的限制。

您期望恐慌能够安全地穿越抽象边界,而在 Go 中则不然。

这不是我所期望的。 在这种情况下,我希望 API 在没有成功时不会报告成功。 您的最后一段代码正确处理了它,因为它没有为作者使用 defer。 但这是非常微妙的。 在这种情况下,许多用户会使用 defer,因为它被认为是惯用的。

也许一组兽医检查可以发现延迟的有问题的使用。 尽管如此,在原始的“惯用的”代码和您最后重构的代码中,有很多修补工作来解决错误处理的微妙之处,否则这些代码是一段非常简单的代码。 解决方法代码不是为了弄清楚如何处理某些错误情况,它纯粹是浪费可用于生产用途的大脑周期。

具体来说,我试图从errd中学到的是,当直接使用时,它是否会使错误处理更加简单。 从我所见,许多复杂性和微妙之处都消失了。 最好看看我们是否可以将其语义的各个方面编码为新的语言特征。

@jimmyfrasche

它将虚假性引入语言以获得其一般性。

这是一个很好的观点。 虚假的常见问题来自忘记调用布尔函数或忘记取消引用指向 nil 的指针。

我们可以通过将运算符定义为仅适用于 nillable 类型来解决后者(并且可能因此删除=! ,因为它几乎没用)。

我们可以通过进一步限制它不与函数类型一起使用,或者只与指针或接口类型一起使用来解决前者:那么很明显变量不是布尔值,并且尝试将它用于布尔比较会更多显然是错误的。

会允许 [ MustF ] 之类的东西吗?

是的。

如果允许 [ defer f.Close() :=? err { ] 必须(以某种方式)等价于
[ defer func() { … }() ]。

不一定,没有。 它可以有自己的语义(更像是call/cc不是匿名函数)。 我还没有提出在defer使用=?的规范更改(它至少需要改变语法),所以我不确定这样的定义到底有多复杂.

最大的两个问题是 […] 2. 内省错误以确定在某些情况下要做什么

我同意这在实践中是一个更大的问题,但它似乎或多或少与这个问题正交(这更多的是关于减少样板和相关的错误可能性)。

@rogpeppe@davecheney@dsnet@crawshaw 、我和我肯定忘记的其他一些人在 GopherCon 上就用于检查错误的 API 进行了很好的讨论,我希望我们也能在这方面看到一些好的建议,但我真的认为这是另一个问题。)

@bcmills :这段代码有两个问题 1) 与@rogpeppe提到的相同:传递给 CloseWithError 的 err 始终为零,2) 它仍然不处理恐慌,这意味着 API 将在出现恐慌时明确报告成功(返回的 r 可能会发出 io.EOF,即使不是所有字节都已写入),即使 1 是固定的。

否则我同意 Close 返回的错误通常可以被忽略。 但并非总是如此(参见第一个示例)。

我确实发现有些令人惊讶的是,在我相当简单的示例(包括我自己的一个)中提出了 4 或 5 个错误的建议,我仍然觉得我不得不争辩说 Go 中的错误处理并非微不足道。 :)

@bcmills

确实,它会在发生恐慌时将 nil 错误传递给 w.CloseWithError,但无论如何整个程序都会在此时终止。

可以? 该 goroutine 的延迟仍然会被调用。 据我所知,它们将运行到完成。 在这种情况下,关闭将发出 io.EOF 信号。

例如,参见https://play.golang.org/p/5CFbsAe8zF。 在 goroutine panic 之后,它仍然很高兴地将“foo”传递给另一个 goroutine,然后它仍然让它把它写到 Stdout。

类似地,其他代码可能会从恐慌的 goroutine(如您的示例中的那个)接收到错误的 io.EOF,得出成功的结论,并在恐慌的 goroutine 恢复其恐慌之前愉快地将文件提交给 GS。

你的下一个论点可能是:不要写有缺陷的代码,但是:

  • 然后更容易地防止这些错误,以及
  • 恐慌可能是由外部因素引起的,例如 OOM。

如果永远不要以零错误关闭很重要,这是一个简单的重命名加上一个额外的行:

它应该仍然以 nil 关闭以在完成时发出 io.EOF 信号,这样就行不通了。

如果永远不要以零错误关闭很重要,这是一个简单的重命名加上一个额外的行:

它应该仍然以 nil 关闭以在完成时发出 io.EOF 信号,这样就行不通了。

为什么不? 最后的return err会将rerrnil

@bcmills :啊,我明白你的意思了。 是的,这应该有效。 不过,我并不担心行数,而是担心代码的微妙之处。

我发现这与变量阴影属于同一类问题,只是不太可能遇到(可能会使情况变得更糟。)大多数变量阴影错误您都可以通过良好的单元测试遇到。 任意恐慌更难测试。

当大规模运行时,几乎可以保证您会看到这样的错误表现。 我可能有点偏执,但我见过导致数据丢失和损坏的可能性要小得多。 通常这很好,但不适用于事务处理(例如编写 gs 文件。)

我希望你不介意我用另一种语法劫持你的提议——人们对这样的事情有什么感觉:

return err if f, err := os.Open("..."); err != nil

@SirCmpwn埋葬了

这是公平的,但你的提议也让我感到不舒服——它引入了一种不透明的语法 (||),其行为与用户接受的训练方式不同表现。 不确定正确的解决方案是什么,会再仔细考虑一下。

@SirCmpwn是的,正如我在原帖中所说,“我写这个提案主要是为了鼓励想要​​简化 Go 错误处理的人思考如何轻松地将上下文包装在错误周围,而不仅仅是返回未修改的错误.” 我尽我所能地写了我的提案,但我不希望它被采纳。

明白了。

这有点激进,但也许宏观驱动的方法会更好。

f = try!(os.Open("..."))

try!会吃掉元组中的最后一个值,如果不是 nil 则返回它,否则返回元组的其余部分。

我想建议我们的问题陈述是,

Go 中的错误处理是冗长且重复的。 Go 错误处理的惯用格式使得更难看到非错误控制流,而且冗长乏味,尤其是对新手而言。 迄今为止,针对这个问题提出的解决方案通常需要手工一次性错误处理功能,减少错误处理的局部性,并增加复杂性。 因为 Go 的目标之一是迫使作者考虑错误处理和恢复,所以对错误处理的任何改进也应该建立在这个目标之上。

为了解决这个问题,我提出了 Go 2.x 中错误处理改进的这些目标:

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

评估此提案:

f.Close() =? err { return fmt.Errorf(…, err) }

根据这些目标,我得出的结论是它在目标 1 上取得了成功。 我不确定它对 #2 有什么帮助,但它也不会降低添加上下文的可能性(我自己的提案在 #2 上也有这个弱点)。 不过,它在 #3 和 #4 上并没有真正成功:
1)正如其他人所说,错误值检查和分配是不透明和不寻常的; 和
2) =?语法也很不寻常。 如果与类似但不同的=!语法结合使用,则尤其令人困惑。 人们需要一段时间才能习惯它们的含义; 和
3) 返回一个有效值和错误是很常见的,任何新的解决方案也应该处理这种情况。

使错误处理成为一个块可能是一个好主意,但是,如果正如其他人所建议的那样,它与gofmt 的更改相结合。 相对于我的提议,它提高了通用性,这应该有助于目标 #4 和熟悉性,这有助于目标 #3,代价是为了简单地返回带有附加上下文的错误的常见情况而牺牲了简洁性。

如果您在摘要中问过我,我可能同意更通用的解决方案比错误处理特定的解决方案更可取,只要它满足上述错误处理改进目标。 现在,虽然阅读了这个讨论并考虑了更多,我倾向于相信错误处理特定的解决方案将导致更加清晰和简单。 虽然 Go 中的错误只是值,但错误处理构成了任何编程的重要部分,因此使用一些特定的语法来使错误处理代码清晰简洁似乎是合适的。 如果我们将它与其他目标(例如范围界定和可组合性)混为一谈,恐怕我们会使已经很困难的问题(为错误处理提出一个干净的解决方案)变得更加困难和复杂。

无论如何,正如@rsc在他的文章Toward Go 2 中指出的那样,如果没有经验报告证明问题很重要,问题陈述、目标或任何语法建议都不可能推进。 也许与其争论各种语法建议,不如开始挖掘支持数据?

无论如何,正如@rsc在他的文章 Toward Go 2 中指出的那样,如果没有经验报告证明问题很重要,问题陈述、目标或任何语法建议都不可能推进。 也许与其争论各种语法建议,不如开始挖掘支持数据?

如果我们假设人体工程学很重要,我认为这是不言而喻的。 打开任何 Go 代码库,寻找有机会干掉和/或改进语言可以解决的人体工程学的地方——现在错误处理是一个明显的异常值。 我认为 Toward Go 2 方法可能错误地主张无视有变通方法的问题 - 在这种情况下,人们只是笑着接受它。

if $val, err := $operation($args); err != nil {
  return err
}

当样板比代码多时,恕我直言,问题不言而喻。

@billyh

我觉得格式: f.Close() =? err { return fmt.Errorf(…, err) }过于冗长和混乱。 我个人不认为错误部分应该在一个块中。 不可避免地,这会导致它被分成 3 行而不是 1 行。此外,在关闭更改中,您需要做的不仅仅是在返回错误之前修改错误,您可以使用当前的if err != nil { ... }语法。

=?运算符也有点令人困惑。 那里发生的事情并不是很明显。

像这样:
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")
或简写:
file := os.Open("/some/file") or raise
和延期:
defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)
有点罗嗦,单词的选择可以减少最初的混淆(即人们可能会立即将raise与来自其他语言(如 python)的类似关键字联系起来,或者只是推断引发错误/最后一个非-默认值向上堆栈到调用者)。

这也是一个很好的命令式解决方案,它不会试图解决所有可能的晦涩错误处理。 到目前为止,最大的错误处理块是上述性质的。 对于后者,当前的语法也有帮助。

编辑:
如果我们想减少一点“魔法”,前面的例子也可能是这样的:
file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err
defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)
我个人认为前面的例子更好,因为它们将完整的错误处理向右移动,而不是像这里的情况那样拆分它。 不过这可能更清楚。

我想建议我们的问题陈述是,......

我不同意问题陈述。 我想建议一个替代方案:


从语言的角度来看,不存在错误处理。 Go 提供的唯一内容是预先声明的错误类型,即使这样也只是为了方便,因为它并没有启用任何真正新的东西。 错误只是值。 错误处理只是普通的用户代码。 从语言 POV 来看,它没有什么特别之处,也不应该有什么特别之处。 错误处理的唯一问题是,有些人认为必须不惜一切代价消除这种宝贵而美丽的简单性。

就像 Cznic 所说的那样,如果有一个不仅仅用于错误处理的解决方案,那就太好了。

使错误处理更通用的一种方法是根据联合类型/总和类型和展开来考虑它。 Swift 和 Rust 都有解决方案? ! 语法,虽然我认为 Rust 的有点不稳定。

如果我们不想让 sum-types 成为一个高级概念,我们可以将它作为多返回的一部分,就像元组不是 Go 真正的一部分一样,但你仍然可以做多返回。

受 Swift 启发的语法:

func Failable() (*Thingie | error) {
    ...
}

guard thingie, err := Failable() else { 
    return wrap(err, "Could not make thingie)
}
// err is not in scope here

您也可以将其用于其他用途,例如:

guard val := myMap[key] else { val = "default" }

@bcmills@jba提出的解决方案=?不仅针对错误,而且概念针对非零。 这个例子会正常工作。

func Foo()(Bar, Recover){}
bar := Foo() =? recover { log.Println("[Info] Recovered:", recover)}

这个提议的主要思想是旁注,把代码的主要目的分开,把次要的情况放在一边,以便于阅读。
对我来说,Go 代码的阅读,在某些情况下,不是连续的,很多时候你用if err!= nil {return err}停止这个想法,所以旁注的想法对我来说似乎很有趣,就像我们读的一本书一样主要思想不断,然后阅读附注。 ( @jba谈话
在极少数情况下,错误是函数的主要目的,可能是在恢复中。 通常当我们遇到错误时,我们会添加一些上下文、日志和返回值,在这些情况下,边注可以使您的代码更具可读性。
不知道是不是最好的语法,尤其不喜欢第二部分的块,旁注要小一点,一行就够了

bar := Foo() =? recover: log.Println("[Info] Recovered:", recover)

@billyh

  1. 正如其他人所说,错误值检查和分配是不透明和不寻常的; 和

请更具体:“不透明和不寻常”是非常主观的。 您能否举出一些您认为提案会令人困惑的代码示例?

  1. =? 语法也很不寻常。 […]

IMO 这是一个功能。 如果有人看到一个不寻常的操作员,我怀疑他们更倾向于查看它的作用,而不是仅仅假设某些可能准确或不准确的东西。

  1. 返回一个有效值和错误是很常见的,任何新的解决方案也应该处理这种情况。

是吗?

仔细阅读提案: =?在评估Block之前执行分配,因此它也可以用于这种情况:

n := r.Read(buf) =? err {
  if err == io.EOF {
    […]
  }
  return err
}

正如@nigeltao指出的那样,您始终可以使用现有的 'n, err := r.Read(buf)` 模式。 为常见情况添加一个功能来帮助确定范围和样板并不意味着我们也必须将它用于不常见的情况。

也许与其争论各种语法建议,不如开始挖掘支持数据?

请参阅 Ian 在原始帖子中链接的众多问题(及其示例)。
另请参阅https://github.com/golang/go/wiki/ExperienceReports#error -handling。

如果您从这些报告中获得了具体的见解,请务必分享。

@urandom

我个人不认为错误部分应该在一个块中。 不可避免地,这将导致它分布在 3 行而不是 1 行中。

该块的目的有两个:

  1. 在产生错误的表达式与其处理程序之间提供清晰的视觉和语法中断,以及
  2. 允许更广泛的错误处理(根据@ianlancetaylor在原始帖子中的既定目标)。

3 行 vs. 1 甚至不是语言变化:如果行数是您最关心的问题,我们可以通过对gofmt的简单更改来解决这个问题。

file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err

我们已经有了returnpanic ; 在这些之上添加raise似乎增加了太多退出函数的方法以获得太少的收益。

defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)

errors.ReplaceIfNil(err, err2)需要一些非常不寻常的传递引用语义。
你可以通过指针传递err ,我想:

defer err2 := f.Close() or errors.ReplaceIfNil(&err, err2)

但这对我来说仍然很奇怪。 or令牌是否构造了表达式、语句或其他内容? (更具体的建议会有所帮助。)

@卡姆约翰逊

你的guard … else语句的具体语法和语义是什么? 对我来说,这看起来很像=?::的代币和可变位置交换。 (同样,一个更具体的建议会有所帮助:您想到的实际语法和语义是什么?)

@bcmills
假设的 ReplaceIfNil 将是一个简单的:

func ReplaceIfNil(original, replacement error) error {
   if original == nil {
       return replacement
   }
   return original
}

没有什么不寻常的。 也许名字...

or将是一个二元运算符,其中左操作数将是一个 IdentifierList 或一个 PrimaryExpr。 在前者的情况下,它被简化为最右边的标识符。 然后,如果左侧操作数不是默认值,则允许执行右侧操作数。

这就是为什么我之后需要另一个令牌来执行返回默认值的魔力,对于除函数 Result 中的最后一个参数之外的所有参数,该参数随后将采用表达式的值。
IIRC,不久前还有另一个提案,该提案将让语言添加“...”或其他内容,以取代繁琐的默认值初始化。 出于这个原因,整个事情可能看起来像这样:

f, err := os.Open("/some/file") or return ..., errors.Wrap(err, "more context")

至于块,我知道它允许更广泛的处理。 我个人不确定该提案的范围是否应该尝试满足所有可能的情况,而不是假设的 80%。 而且我个人认为结果需要多少行很重要(尽管我从未说过这是我最关心的问题,实际上是在使用诸如 =? 之类的晦涩标记时的可读性或缺乏可读性)。 如果这个新提案在一般情况下跨越多行,我个人认为它没有以下优点:

if f, err := os.Open("/some/file"); err != nil {
     return errors.Wrap(err, "more context")
}
  • 如果上面定义的变量在if范围之外可用。
    由于这些错误处理块的视觉噪音,这仍然会使只有几个这样的语句的函数更难阅读。 这是人们在讨论 go 中的错误处理时的抱怨之一。

@urandom

or将是一个二元运算符,其中左操作数将是一个 IdentifierList 或一个 PrimaryExpr。 [...] 然后,如果左侧操作数不是默认值,则允许执行右侧操作数。

Go 的二元运算符是表达式,而不是语句,因此将or变成二元运算符会引发很多问题。 (作为更大表达式的一部分, or的语义是什么,这与您使用:=发布的示例有何相似之处?)

假设它实际上是一个语句,那么右边的操作数是什么? 如果是表达式,它的类型是什么, raise可以作为其他上下文中的表达式吗? 如果它是一个语句,除了raise之外,它的语义是什么? 或者您是否建议or raise本质上是一个单一的语句(例如or raise作为::=?的语法替代方案)?

我可以写吗

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

?

我可以写吗

f(r.Read(buf) or raise err)

?

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

不,这将是无效的,因为第二个raise 。 如果那不存在,那么整个转换链应该通过并且最终结果应该返回给调用者。 虽然这样的语义作为一个整体可能是不需要的,因为你可以只写:

defer f.Close() or raise(err2) Transform(errors.ReplaceIfNil(err, err2)


f(r.Read(buf) or raise err)

如果我们假设我的原始评论 - where or 将采用左侧的最后一个值,那么如果它是默认值,则最终表达式将计算结果列表的其余部分; 那么是的,这应该是有效的。 在这种情况下,如果r.Read返回错误,则该错误将返回给调用者。 否则, n将被传递给f

编辑:

除非我对这些术语感到困惑,否则我认为or是一个二元运算符,其操作数必须是相同的类型(但有点神奇,如果左操作数是一个列表,并且在这种情况下,它将采用所述事物列表的最后一个元素)。 raise将是一个一元运算符,它接受其操作数,并从函数返回,使用该操作数的值作为最后一个返回参数的值,前面的具有默认值。 然后,您可以在技术上在独立语句中使用raise ,以便从函数返回,也就是return ..., err

这将是理想的情况,但我也认为or raise只是=?的语法替代,只要它也接受一个简单的语句而不是一个块,以便以不那么冗长的方式涵盖了大多数用例。 或者我们也可以使用类似 defer 的语法,它接受一个表达式。 这将涵盖大多数情况,例如:

f := os.Open("/some/file") or raise(err) errors.Wrap(err, "with context")

和复杂的情况:

f := os.Open or raise(err) func() {
     if err == io.EOF {
         […]
     }
  return err
}()

再考虑一下我的建议,我放弃了关于联合/总和类型的一点。 我提议的语法是

guard [ ASSIGNMENT || EXPRESSION ] else { [ BLOCK ] }

在表达式的情况下,表达式被计算,如果布尔表达式的结果不等于true或其他表达式的空白值,则执行 BLOCK。 在赋值中,最后一个赋值的值为!= true / != nil 。 在guard语句之后,所做的任何分配都将在范围内(它不会创建新的块范围[除了最后一个变量?])。

在 Swift 中, guard语句的 BLOCK 必须包含returnbreakcontinuethrow 。 我还没有决定我是否喜欢它。 它似乎确实增加了一些价值,因为读者可以从guard这个词知道接下来会发生什么。

有没有人非常关注 Swift,可以说guard是否受到该社区的好评?

例子:

guard f, err := os.Open("/some/file") else { return errors.Wrap(err, "could not open:") }

guard data, err := ioutil.ReadAll(f) else { return errors.Wrap(err, "could not read:") }

var obj interface{}

guard err = json.Unmarshal(data, &obj) else { return errors.Wrap(err, "could not unmarshal:") }

guard m, _ := obj.(map[string]interface{}) else { return errors.New("unexpected data format") }

guard val, _ := m["key"] else { return errors.New("missing key") }

恕我直言,大家一下子在这里讨论的问题范围太广,但现实中最常见的模式是“按原样返回错误”。 那么为什么不使用 smth 来解决最大的问题,例如:

code, err ?= fn()

这意味着函数应该在 err != nil 时返回?

对于 := 运算符,我们可以引入 ?:=

code, err ?:= fn()

由于阴影,使用 ?:= 的情况似乎更糟,因为编译器必须将变量“err”传递给同名的 err 返回值。

我真的很兴奋,有些人专注于让编写正确的代码变得更容易,而不仅仅是缩短不正确的代码。

一些注意事项:

微软 Midori 的一位设计师关于错误模型的有趣“经验报告”

我认为这份文档和Swift 中的一些想法可以很好地应用于 Go2。

引入一个新的保留的throws关键字,函数可以定义为:

func Get() []byte throws {
  if (...) {
    raise errors.New("oops")
  }

  return []byte{...}
}

由于未处理的可抛出错误,尝试从另一个非抛出函数调用此函数将导致编译错误。
相反,我们应该能够传播错误,每个人都同意这是一个常见的情况,或者处理它。

func ScrapeDate() time.Time throws {
  body := Get() // compilation error, unhandled throwable
  body := try Get() // we've been explicit about potential throwable

  // ...
}

对于我们知道某个方法不会失败的情况,或者在测试中,我们可以引入类似于 swift 的try!

func GetWillNotFail() time.Time {
  body := Get() // compilation error, throwable not handled
  body := try Get() // compilation error, throwable can not be propagated, because `GetWillNotFail` is not annotated with throws
  body := try! Get() // works, but will panic on throws != nil

  // ...
}

虽然不确定这些(类似于swift):

func main() {
  // 1:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err { // err contains caught throwable
    // ...
  }

  // 2:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err.(type) { // similar to a switch statement
    case error:
      // ...
    case io.EOF
      // ...
  }
}

ps1。 多个返回值func ReadRune() (ch Rune, size int) throws { ... }
ps2。 我们可以用return try Get()return try! Get()
ps3。 我们现在可以像buffer.NewBuffer(try Get())buffer.NewBuffer(try! Get())这样的链接调用
ps4。 不确定注释(编写errors.Wrap(err, "context")简单方法)
ps5。 这些实际上是例外
ps6。 最大的胜利是忽略异常的编译时错误

您写的建议在 Midori 链接中与所有不好的内容完全相同
它的另一面......“投掷”的一个明显后果将是“人们
讨厌它”。为什么要在大多数情况下每次都写“throws”
职能?

顺便说一句,您可以强制检查错误而不是忽略错误
也适用于非错误类型,恕我直言,最好有更多
广义形式(例如 gcc __attribute__((warn_unused_result)))。

至于操作员的形式,我建议使用简短的形式或
关键字形式如下:

?= fn() 或检查 fn() -- 将错误传播给调用者
!= fn() OR nofail fn() -- 出错时恐慌

2017 年 8 月 26 日星期六下午 12:15,nvartolomei [email protected]
写道:

一些注意事项:

有趣的体验报告
http://joeduffyblog.com/2016/02/07/the-error-model/来自其中之一
微软 Midori 的设计师关于错误模型。

我认为这个文档和 Swift 的一些想法
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
可以很好地应用于Go2。

引入一个新的 reseved throws 关键字,函数可以定义为:

func Get() []byte 抛出 {
如果 (...) {
引发错误。新(“哎呀”)
}

返回 []byte{...}
}

尝试从另一个非抛出函数调用此函数将
由于未处理的可抛出错误,导致编译错误。
相反,我们应该能够传播错误,每个人都同意这是一个
常见情况,或处理它。

func ScrapeDate() time.Time 抛出 {
body := Get() // 编译错误,未处理的 throwable
body := try Get() // 我们已经明确了潜在的 throwable

// ...
}

对于我们知道某个方法不会失败的情况,或者在测试中,我们可以
介绍试试! 类似于迅捷。

func GetWillNotFail() time.Time {
body := Get() // 编译错误,throwable 未处理
body := try Get() // 编译错误,throwable无法传播,因为GetWillNotFail没有用throws注解
身体:=试试! Get() // 有效,但会在抛出时恐慌 != nil

// ...
}

虽然不确定这些(类似于swift):

功能主(){
// 1:
做 {
fmt.Printf("%v", 试试 ScrapeDate())
} catch err { // err 包含被捕获的 throwable
// ...
}

// 2:
做 {
fmt.Printf("%v", 试试 ScrapeDate())
} catch err.(type) { // 类似于switch语句
案例错误:
// ...
案例io.EOF
// ...
}
}

ps1。 多个返回值 func ReadRune() (ch Rune, size int) throws {
... }
ps2。 我们可以用 return try Get() 或 return try! 返回! 得到()
ps3。 我们现在可以像 buffer.NewBuffer(try Get()) 或 buffer.NewBuffer(try!
得到())
ps4。 不确定注释


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

我认为@jba@bcmills提出的操作符是一个非常好的主意,尽管它读起来更好拼写为“??” 而不是“=?” 海事组织。

看这个例子:

func doStuff() (int,error) {
    x, err := f() 
    if err != nil {
        return 0, wrapError("f failed", err)
    }

    y, err := g(x)
    if err != nil {
        return 0, wrapError("g failed", err)
    }

    return y, nil
}

func doStuff2() (int,error) {
    x := f()  ?? (err error) { return 0, wrapError("f failed", err) }
    y := g(x) ?? (err error) { return 0, wrapError("g failed", err) }
    return y, nil
}

我认为 doStuff2 相当容易和快速阅读,因为它:

  1. 浪费更少的垂直空间
  2. 很容易快速阅读左侧的快乐路径
  3. 很容易快速读取右侧的错误情况
  4. 没有 err 变量污染函数的本地命名空间

对我来说,仅此提议就显得不完整,而且具有太多魔力。 ??运算符将如何定义? “如果非零则捕获最后一个返回值”? “如果匹配方法类型,则捕获最后一个错误值?”

添加用于根据位置和类型处理返回值的新运算符看起来像一个黑客。

在2017年8月29日,13:03 +0300,的Mikael Gustavsson的[email protected] ,写道:

我认为@jba@bcmills提出的操作符是一个非常好的主意,尽管它读起来更好拼写为“??” 而不是“=?” 海事组织。
看这个例子:
func doStuff() (int,error) {
x, 错误 := f()
如果错误!= nil {
return 0, wrapError("f 失败", err)
}

   y, err := g(x)
   if err != nil {
           return 0, wrapError("g failed", err)
   }

   return y, nil

}

func doStuff2() (int,error) {
x := f() ?? (err 错误) { return 0, wrapError("f failed", err) }
y := g(x) ?? (err 错误) { return 0, wrapError("g failed", err) }
返回 y,无
}
我认为 doStuff2 相当容易和快速阅读,因为它:

  1. 浪费更少的垂直空间
  2. 很容易快速阅读左侧的快乐路径
  3. 很容易快速读取右侧的错误情况
  4. 没有 err 变量污染函数的本地命名空间


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

@nvartolomei

??运算符将如何定义?

参见https://github.com/golang/go/issues/21161#issuecomment -319434101 和https://github.com/golang/go/issues/21161#issuecomment -320758279。

由于@bcmills建议复活一个静止线程,如果我们要考虑从其他语言中@slvmnd的例子为例,用语句修饰符重做:

func doStuff() (int, err) {
        x, err := f()
        return 0, wrapError("f failed", err)     if err != nil

    y, err := g(x)
        return 0, wrapError("g failed", err)     if err != nil

        return y, nil
}

不像在一行中包含语句和错误检查那么简洁,但它读起来相当不错。 (我建议在 if 表达式中禁止 := 形式的赋值,否则范围问题可能会使人们感到困惑,即使他们在语法上很清楚)允许“除非”作为“if”的否定版本有点语法糖,但它很好读,值得考虑。

不过,我不建议从 Perl 那里抄袭。 (Basic Plus 2 很好)这种方式存在循环语句修饰符,虽然有时很有用,但会带来另一组相当复杂的问题。

较短的版本:
如果 err != nil 则返回
到时候也应该支持。

有了这样的语法,问题就出现了——非返回语句也应该是
支持这样的“if”语句,如下所示:
func(args) 如果条件

也许不是发明后行动 - 如果值得引入单个
行如果?

如果 err!=nil 返回
如果 err!=nil 返回 0, wrapError("failed", err)
如果 err!=nil do_smth()

它似乎比特殊形式的语法更自然,不是吗? 虽然我猜
它在解析中引入了很多痛苦:/

但是......这只是一些小的调整,而不是对错误的特殊语言支持
处理/传播。

在星期一,2017年9月18日在下午4点十四分,dsugalski [email protected]写道:

由于@bcmills https://github.com/bcmills建议复活一个
静止线程,如果我们要考虑抄袭其他语言,
似乎语句修饰符将为所有人提供合理的解决方案
这。 以@slvmnd https://github.com/slvmnd的例子为例,重做
语句修饰符:

func doStuff() (int, err) {
x, 错误 := f()
返回 0, wrapError("f failed", err) 如果 err != nil

  y, err := g(x)
    return 0, wrapError("g failed", err)     if err != nil

    return y, nil

}

不像将语句和错误检查放在一起那么简洁
行,但读起来相当不错。 (我建议禁止 := 形式的
在 if 表达式中赋值,否则可能会出现作用域问题
混淆人们,即使他们在语法上很清楚)允许“除非”作为
“if”的否定版本有点语法糖,但它可以很好地
阅读并值得考虑。

不过,我不建议从 Perl 那里抄袭。 (基本加 2 是
很好)这种方式在于循环语句修饰符,而有时
有用的,带来另一组相当复杂的问题。


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

仔细考虑@jba和其他人所要求的属性,即非错误代码与错误代码明显不同。 如果它对非错误代码路径也有显着的好处,那么它仍然是一个有趣的想法,但我想得越多,与提议的替代方案相比,它的吸引力就越小。

我不确定纯文本有多少视觉差异是合理的。 在某些时候,将其放到 IDE 或文本编辑器的代码着色层似乎更合适。

但是对于基于文本的可见区分,当我很久以前第一次开始使用它时,我们制定的格式标准是 IF/UNLESS 语句修饰符必须右对齐,这使它们非常突出。 (虽然在 VT-220 终端上授予的标准比在具有更灵活窗口大小的编辑器中更容易执行并且可能在视觉上更明显)

至少对我来说,我确实发现语句修饰符 case 很容易区分并且比当前的 if-block 方案读起来更好。 当然,其他人可能不是这种情况——我阅读源代码的方式与阅读英文文本的方式相同,因此它映射到现有的舒适模式中,并不是每个人都这样做。

return 0, wrapError("f failed", err) if err != nil可以写成if err != nil { return 0, wrapError("f failed", err) }

if err != nil return 0, wrapError("f failed", err)可以写成相同的。

也许这里所需要的只是让 gofmt 将if写在一行上的一行而不是将它们扩展到三行?

还有一种可能性让我印象深刻。 我在尝试快速编写一次性 Go 代码时遇到的很多摩擦是因为我必须对每个调用进行错误检查,所以我不能很好地嵌套调用。

例如,如果不先将 http.NewRequest 结果分配给临时变量,然后在其上调用 Do,我就无法在新的请求对象上调用 http.Client.Do。

我想知道我们是否可以允许:

f(y())

即使y返回(T, error)元组也能工作。 当 y 返回错误时,编译器可以中止表达式计算并导致该错误从 f 返回。 如果 f 没有返回错误,则可以给出一个错误。

然后我可以这样做:

n, err := http.DefaultClient.Do(http.NewRequest("DELETE", "/foo", nil))

如果 NewRequest 或 Do 失败,则错误结果将为非零。

然而,这有一个重大问题——如果 f 接受两个参数或可变参数,则上述表达式已经有效。 此外,执行此操作的确切规则可能非常复杂。

所以总的来说,我认为我不喜欢它(我也不热衷于这个线程中的任何其他提案),但我认为无论如何我都会考虑这个想法。

@rogpeppe或者你可以只使用json.NewEncoder

@gbbr哈是的,不好的例子。

一个更好的例子可能是 http.Request。 我已经更改了评论以使用它。

哇。 许多想法使代码可读性变得更糟。
我没问题

if val, err := DoMethod(); err != nil {
   // val is accessible only here
   // some code
}

只有一件事真的很烦人是返回变量的范围。
在这种情况下,您必须使用val但它在if范围内。
所以你必须使用else但 linter 会反对它(我也是),唯一的方法是

val, err := DoMethod()
if err != nil {
   // some code
}
// some code with val

能够访问if块之外的变量会很好:

if val, err := DoMethod(); err != nil {
   // some code
}
// some code with val

@dmbreaker这基本上就是 Swift 的保护条款的用途。 如果它通过某个条件,它会在当前范围内分配一个变量。 见我之前的评论

我完全赞成简化 Go 中的错误处理(尽管我个人并不那么介意),但我认为这为原本简单且极易阅读的语言增添了一点魔力。

@gbbr
你在这里指的“这个”是什么? 关于如何处理事情有很多不同的建议。

也许是一个两部分的解决方案?

try定义为“剥离返回元组中最右边的值;如果它不是其类型的零值,则将其作为该函数的最右边值返回,其他值设置为零”。 这使得常见的情况

 a := try ErrorableFunction(b)

并启用链接

 a := try ErrorableFunction(try SomeOther(b, c))

(可选地,为了效率,使其非零而不是非零。)如果错误函数返回非零/非零,则函数“中止一个值”。 try ed 函数的最右边的值必须可分配给调用函数的最右边的值,否则它是编译时类型检查错误。 (因此,这不是硬编码以仅处理error ,尽管社区可能不鼓励将其用于任何其他“聪明”代码。)

然后,允许使用类似 defer 的关键字捕获 try 返回,或者:

catch func(e error) {
    // whatever this function returns will be returned instead
}

或者,也许更详细但更符合 Go 的工作方式:

defer func() {
    if err := catch(); err != nil {
        set_catch(ErrorWrapper{a, "while posting request to server"})
    }
}()

catch情况下,函数的参数必须与返回的值完全匹配。 如果提供了多个函数,则该值将以相反的顺序通过所有函数。 您当然可以在其中放置一个解析为正确类型函数的值。 在基于defer的示例中,如果一个defer func 调用set_catch下一个 defer func 将获得它作为catch() 。 (如果你愚蠢到在这个过程中将它设置回 nil,你会得到一个令人困惑的返回值。不要那样做。)传递给 set_catch 的值必须可以分配给返回的类型。 在这两种情况下,我都希望它像defer ,因为它是一个语句,而不是声明,并且仅适用于语句执行后的代码。

从简单的角度来看,我倾向于更喜欢基于延迟的解决方案(基本上没有引入新概念,它是第二种类型的recover()而不是新事物),但承认它可能存在一些性能问题。 拥有一个单独的 catch 关键字可以通过在正常返回发生时更容易完全跳过来提高效率,如果希望获得最大效率,也许可以将它们绑定到作用域,以便每个作用域或函数只允许一个处于活动状态,我认为这几乎是零成本。 (可能源代码文件名和行号也应该从 catch 函数返回?在编译时这样做很便宜,并且可以避免人们现在要求完整堆栈跟踪的一些原因。)

也可以允许在函数内的一个地方有效地处理重复的错误处理,并允许将错误处理轻松地作为库函数提供,恕我直言,这是当前情况下最糟糕的方面之一,根据上面 rsc 的评论; 错误处理的费力倾向于鼓励“返回错误”而不是正确处理。 我知道我自己也在为此挣扎。

@thejerf Ian 对此提案的部分观点是探索解决错误样板文件的方法,同时又不妨碍函数添加上下文或以其他方式操纵它们返回的错误。

将错误处理分成trycatch似乎会违背这个目标,尽管我认为这取决于程序通常想要添加的细节类型。

至少,我想看看它是如何通过更现实的例子进行的。

我的提议的全部要点是允许添加上下文或操纵错误,我认为比这里的大多数提议在编程上更正确,这些提议涉及一遍又一遍地重复该上下文,这本身抑制了将附加上下文放入的愿望.

要重写原始示例,

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

出来作为

func Chdir(dir string) error {
    catch func(e error) {
        return &PathError{"chdir", dir, e}
    }

    try syscall.Chdir(dir)
    return nil
}

除了这个例子对于这些提议中的任何一个来说都太琐碎了,实际上,我想说在这种情况下,我们只是不理会原始函数。

就我个人而言,我首先不认为原始的 Chdir 函数有问题。 我正在专门对此进行调整,以解决函数因冗长的重复错误处理而被干扰的情况,而不是针对单一错误的函数。 我还要说的是,如果你有一个函数,你实际上为每个可能的用例做不同的事情,那么正确的答案可能是继续写我们已经得到的东西。 但是,我怀疑这对大多数人来说是迄今为止罕见的情况,因为如果那是常见情况,那么首先就不会有人投诉。 检查错误的噪音之所以重要,正是因为人们希望在一个函数中一遍又一遍地做“几乎相同的事情”。

我也怀疑人们想要的大部分东西都会得到满足

func SomethingBigger(dir string) (interface{}, error) {
     catch func (e error, filename string, lineno int) {
         return PackageSpecificError{e, filename, lineno, dir}
     }

     x := try Something()

     if x == true {
         try SomethingElse()
     } else {
         a, b = try AThirdThing()
     }

     return whatever, nil
}

如果我们消除了试图使单个 if 语句看起来不错的问题,因为它太小而无法关心,并且消除了一个函数的问题,该函数真正为每个错误返回做一些独特的事情,理由是 A : 这实际上是一个相当罕见的情况,而 B: 在这种情况下,样板开销实际上与独特处理代码的复杂性相比并不那么重要,也许问题可以简化为有解决方案的问题。

我也很想看

func packageSpecificHandler(f string) func (err error, filename string, lineno int) {
    return func (err error, filename string, lineno int) {
        return &PackageSpecificError{"In function " + f, err, filename, lineno}
    }
}

 func SomethingBigger(dir string) (interface{}, error) {
     catch packageSpecificHandler("SomethingBigger")

     ...
 }

或者一些等价物是可能的,因为当它起作用时。

而且,在页面上的所有提案中……这看起来不像 Go 吗? 它看起来比当前的 Go 更像 Go。

老实说,我的大部分专业工程经验都是使用 PHP(我知道),但 Go 的主要吸引力始终是可读性。 虽然我确实喜欢 PHP 的某些方面,但我最鄙视的是“最终的”“抽象的”“静态”的废话以及将过于复杂的概念应用到只做一件事的一段代码中。

看到这个提议让我立即闪回了看一段代码的感觉,不得不反复思考并真正“思考”那段代码在说什么/做什么。 我不认为这段代码是可读的,也没有真正添加到语言中。 我的第一直觉是向左看,我认为这总是返回nil 。 然而,有了这个改变,我现在必须左看右看,以确定代码的行为,这意味着更多的时间阅读和更多的心智模型。

然而,这并不意味着 Go 中的错误处理没有改进的余地。

很抱歉我还没有(还)阅读整个线程(它超长)但我看到人们抛出替代语法,所以我想分享我的想法:

a, err := helloWorld(); err? {
  return fmt.Errorf("helloWorld failed with %s", err)
}

希望我没有错过上面的东西使这无效。 我保证有一天我会看完所有评论:)

我相信,必须只允许在error类型上使用运算符,以避免类型转换语义混乱。

有趣, @buchanae ,但它是否让我们

if a, err := helloWorld(); err != nil {
  return fmt.Errorf("helloWorld failed with %s", err)
}

我确实看到它允许a转义,而在当前状态下,它的范围仅限于 then 和 else 块。

@object88你是对的,这种变化是微妙的、审美的和主观的。 就我个人而言,我希望 Go 2 在这个主题上有一个微妙的可读性变化。

就我个人而言,我发现它更具可读性,因为该行不以if开头并且不需要!=nil 。 变量位于左边缘,它们位于(大多数?)其他行上。

关于a范围的重点,我没有考虑过。

考虑到这种语法的其他可能性,这似乎是可能的。

err := helloWorld(); err? {
  return fmt.Errorf("error: %s", err)
}

并且可能

helloWorld()? {
  return fmt.Errorf("hello world failed")
}

这可能是它崩溃的地方。

也许返回错误应该是 Go 中每个函数调用的一部分,所以你可以想象:
``
a := helloWorld(); 呃? {
return fmt.Errorf("helloWorld 失败:%s", err)
}

真正的异常处理怎么样? 我的意思是尝试,抓住,最后而不是像许多现代语言一样?

不,它使代码隐含和不清楚(虽然确实短了一点)

2017 年 11 月 23 日星期四 07:27,Kamyar Miremadi通知@github.com
写道:

真正的异常处理怎么样? 我的意思是尝试,抓住,最后
而不是像许多现代语言?


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

回到@mpvl的 WriteToGCS 示例up-thread ,我想(再次)建议提交/回滚模式不够常见,不足以保证 Go 的错误处理发生重大变化。 在函数中捕捉模式并不难( playground link ):

func runWithCommit(f, commit func() error, rollback func(error)) (err error) {
    defer func() {
        if r := recover(); r != nil {
            rollback(fmt.Errorf("panic: %v", r))
            panic(r)
        }
    }()
    if err := f(); err != nil {
        rollback(err)
        return err
    }
    return commit()
}

然后我们可以把例子写成

func writeToGCS(ctx context.Context, bucket, dst string, r io.Reader) error {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    return runWithCommit(
        func() error { _, err := io.Copy(w, r); return err },
        func() error { return w.Close() },
        func(err error) { _ = w.CloseWithError(err) })
}

我建议更简单的解决方案:

func someFunc() error {
    ^err := someAction()
    ....
}

对于多个多重函数返回:

func someFunc() error {
    result, ^err := someAction()
    ....
}

对于多个返回参数:

func someFunc() (result Result, err error) {
    var result Result
    params, ^err := someAction()
    ....
}

^ 符号表示如果参数不为零则返回。
基本上“如果发生错误将错误移到堆栈上”

这种方法有什么缺点吗?

@gladkikhartem
如何在错误返回之前修改错误?

@urandom
包装错误是一个重要的动作,在我看来应该明确地完成。
Go 代码是关于可读性的,而不是魔法。
我想让错误包装更清晰

但同时我想摆脱那些不携带大量信息而只占用空间的代码。

if err != nil {
    return err
}

这就像 Go 的陈词滥调 - 您不想阅读它,只想跳过它。

到目前为止,我在本次讨论中看到的是以下内容的组合:

  1. 减少语法冗长
  2. 通过添加上下文来改善错误

这与@ianlancetaylor提到的两个方面的原始问题描述

1. 语法冗长减少

我喜欢@gladkikhartem的想法,即使是我在此处报告的原始形式,因为它已被编辑/扩展:

 result, ^ := someAction()

在 func 的上下文中:

func getOddResult() (int, error) {
    result, ^ := someResult()
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

这种简短的语法——或者采用@gladkikhartem提出的带有err^ ——将解决问题 (1) 的语法冗长部分。

2. 错误上下文

对于第二部分,添加更多上下文,我们现在甚至可以完全忘记它,以后如果使用特殊的contextError类型,建议自动为每个错误添加堆栈跟踪。 这种新的本机错误类型可以完整或短堆栈跟踪(想象一个GO_CONTEXT_ERROR=full )并且与error接口兼容,同时提供至少从顶部调用堆栈中提取函数和文件名的可能性入口。

当使用contextError ,Go 应该以某种方式将调用堆栈跟踪附加到创建错误的位置。

再次使用 func 示例:

func getOddResult() (int, contextError) {
    result, ^ := someResult() // here a 'contextError' is created; if the error received from 'someResult()' is also a `contextError`, the two are nested
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

只有类型从error更改contextError ,可以定义为:

type contextError interface {
    error
    Stack() []StackEntry
    Cause() contextError
}

(注意Stack()与 https://golang.org/pkg/runtime/debug/#Stack 的不同之处,因为我们希望这里有一个非字节版本的 goroutine 调用堆栈)

作为嵌套的结果, Cause()方法将返回 nil 或之前的contextError

我非常清楚携带这样的堆栈对内存的潜在影响,因此我暗示可能有一个默认的短堆栈,其中只包含 1 个或几个条目。 开发人员通常会在开发/调试版本中启用完整的 stracktraces,否则保留默认值(短堆栈跟踪)。

现有技术:

只是深思熟虑。

@gladkikhartem @gdm85

我认为你没有抓住这个提议的要点。 Per Ian 的原始帖子:

忽略错误已经很容易(也许太容易了)(参见 #20803)。 许多现有的错误处理建议使返回未修改的错误更容易(例如,#16225、#18721、#21146、#21155)。 很少有人可以更轻松地返回带有附加信息的错误。

返回未经修改的错误通常是错误的,而且通常至少是无益的。 我们希望鼓励谨慎的错误处理:只解决“返回未修改”用例会使激励偏向错误的方向。

@bcmills如果正在添加上下文(以堆栈跟踪的形式),则返回错误和附加信息。 是否会附加人类可读的消息,例如“插入记录时出错”被视为“谨慎的错误处理”? 如何决定应在调用堆栈中的哪个点添加此类消息(在每个函数中,顶部/底部等)? 这些都是编码错误处理改进时的常见问题。

默认情况下,“未修改的返回”可以通过“返回未修改的堆栈跟踪”来抵消,并且(以反应式的方式)根据需要添加人类可读的消息。 我没有具体说明如何添加这样的人类可读的消息,但是可以看到一些想法在pkg/errors包装是如何工作的。

“返回未修改的错误通常是错误的”:因此,我提出了惰性用例的升级路径,这与当前指出的有害用例相同。

@bcmills
我 100% 同意 #20803 错误应该始终被处理或明确忽略(我不知道为什么以前没有这样做......)
是的,我没有解决提案的问题,我也没有必要。 我关心提出的实际解决方案,而不是背后的意图,因为意图与结果不符。 当我看到 || 这样|| 被提议的东西 - 这让我很伤心。

如果将错误代码和错误消息等信息嵌入到错误中会很容易且透明——你不需要鼓励仔细的错误处理——人们会自己做。
例如,只需将错误设为别名。 我们可以返回任何类型的东西并在函数之外使用它而无需强制转换。 会让生活变得更轻松。

我喜欢 Go 提醒我处理错误,但我讨厌设计鼓励我去做一些有问题的事情。

@gdm85
自动将堆栈跟踪添加到错误是一个糟糕的主意,只需查看 Java 堆栈跟踪即可。
当您自己包装错误时 - 导航和了解出了什么问题要容易得多。 这就是包装它的全部意义所在。

@gladkikhartem我不同意一种“自动包装”的形式在导航和帮助理解出了什么问题方面会更糟糕。 我也没有完全理解您在 Java 堆栈跟踪中所指的内容(我猜是异常?美学上丑陋?什么具体问题?),而是朝着建设性的方向进行讨论:什么是“精心处理的错误”的好定义?

我要求两者都增强我对 Go 最佳实践的理解(它们可能或多或少是规范的),因为我觉得这样的定义可能是提出一些改进当前情况的建议的关键。

@gladkikhartem我知道这个提议已经if err != nil { return err }不同建议,这些是讨论仅改进特定情况的语法的地方。 谢谢。

@ianlancetaylor
对不起,如果我把讨论移开了。

如果您想将上下文信息添加到错误中,我建议使用以下语法:
(并强制人们对一个函数只使用一种错误类型,以便于上下文提取)

type MyError struct {
    Type int
    Message string
    Context string
    Err error
}

func VeryLongFunc() error {
    var err MyError
    err.Context = "general function context"


   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
   }

    // in case we need to make a cleanup after error

   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       file.Close()
   }

   // another variant with different symbol and return statement

   result, ?err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }

   // using original approach

   result, err.Err := someAction()
   if err != nil {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }
}

func main() {
    err := VeryLongFunc()
    if err != nil {
        e := err.(MyError)
        log.Print(e.Error(), " in ", e.Dir)
    }
}

^ 符号用于表示错误参数,以及区分函数定义和“someAction() { }”的错误处理
{ } 如果未修改返回错误,则可以省略

添加更多资源来回复我自己的邀请以更好地定义“谨慎的错误处理”:

尽管目前的方法很乏味,但我认为它比替代方法更容易混淆,尽管一行 if 语句可能有效? 或许?

blah, err := doSomething()
if err != nil: return err

...甚至...

blah, err := doSomething()
if err != nil: return &BlahError{"Something",err}

有人可能已经提出过这个问题,但是有很多很多帖子,我读过很多但不是全部。 也就是说,我个人认为显式比隐式更好。

我一直是面向铁路的编程的粉丝,这个想法来自 Elixir 的with语句。
else块将在e == nil短路后执行。

这是我的伪代码建议:

func Chdir(dir string) (e error) {
    with e == nil {
            e = syscall.Chdir(dir)
            e, val := foo()
            val = val + 1
            // something else
       } else {
           printf("e is not nil")
           return
       }
       return nil
}

@ardhitama这不就像 Try catch 一样,除了“With”就像“Try”语句,而“Else”就像“Catch”?
为什么不去实现像 Java 或 C# 这样的异常处理?
现在,如果程序员不想在该函数中处理异常,它会将其作为该函数的结果返回。 仍然没有办法强迫程序员处理他们不想处理的异常,而且很多时候你真的不需要,但我们在这里得到的是很多 if err!=nil 语句,这使代码变得丑陋并且不可读(很多噪音)。 难道这不是在其他编程语言中首先发明 Try Catch finally 语句的原因吗?

所以,我觉得 Go 的作者“试试”还是不要固执比较好!! 并在下一个版本中引入“Try Catch finally”语句。 谢谢你。

@KamyarM
你不能在 go 中引入异常处理,因为在 Go 中没有异常。
在 Go 中引入 try{} catch{} 就像在 C 中引入 try{} catch{} -这是完全错误的

@ianlancetaylor
如果根本不改变 Go 错误处理,而是改变这样的gofmt工具来处理单行错误呢?

err := syscall.Chdir(dir)
    if err != nil {return &PathError{"chdir", dir, err}}
err = syscall.Chdir(dir2)
    if err != nil {return err}

它向后兼容,您可以将其应用到您当前的项目中

异常是装饰性的 goto 语句,它们将您的调用堆栈转换为调用图,并且有充分的理由禁止或限制大多数严肃的非学术项目的使用。 一个有状态的对象调用一个方法,该方法将控制任意向上堆栈转移,然后继续执行指令......听起来是个坏主意,因为它是。

@KamyarM本质上是这样,但实际上并非如此。 在我看来,因为我们在这里是明确的,并且没有破坏任何 Go 习语。

为什么?

  1. with语句中的表达式不能声明新的 var,因此它明确指出其意图是评估块外的 var。
  2. with语句的行为类似于trycatch块中的语句。 实际上,在最坏的情况下,它需要在每条下一条指令评估with的条件时会变慢。
  3. 设计意图是删除过多的if而不是创建异常处理程序,因为处理程序将始终是本地的( with的表达式和else块)。
  4. 由于throw不需要堆栈展开

附: 如果我错了,请纠正我。

@ardhitama
KamyarM 在某种意义上是正确的, with语句看起来和try catch一样丑陋,它还为正常代码流引入了缩进级别。
甚至没有提到单独修改每个错误的原始提案的想法。 它不会与try catchwith或任何其他将语句组合在一起的方法优雅地工作。

@gladkikhartem
是的,因此我建议采用“面向铁路的编程”,并尽量不删除显式。 这只是解决问题的另一个角度,其他解决方案希望通过不让编译器自动为您编写if err != nil来解决它。

with不仅用于错误处理,它还可以用于任何其他控制流。

@gladkikhartem
让我说清楚我认为Try Catch Finally块很漂亮。 If err!=nil ...实际上是丑陋的代码。

Go 只是一种编程语言。 还有很多其他语言。 我发现 Go 社区中的许多人将其视为他们的宗教,并且不愿意改变或承认错误。 这是错误的。

@gladkikhartem

如果 Go 作者称它为 Go++ 或 Go# 或 GoJava 并在那里介绍Try Catch Finally就可以了;)

@KamyarM

避免不必要的更改对于任何工程工作来说都是必要的——至关重要的。 当人们在这种情况下说改变时,他们的真正意思是_改变得更好_,他们通过_参数_有效地传达了这一点,指导前提到预期的结论。

_just open your mind man!_ 上诉并不令人信服。 具有讽刺意味的是,它试图说出大多数程序员认为古老而笨拙的东西是_新的和改进的_。

Go 社区也有许多提案和讨论,其中讨论了以前的错误。 但是当你说 Go 只是一种编程语言时,我同意你的看法。 它确实在 Go 网站和其他地方这么说,我也和一些人谈过,他们也证实了这一点。

我发现 Go 社区中的许多人将其视为他们的宗教,并且不愿意改变或承认错误。

围棋以学术研究为基础; 个人意见不重要。

甚至 Microsoft 的 C# 编译器的主要开发人员也公开承认 _exceptions_ 是一种糟糕的错误管理方式,同时将 Go/Rust 模型视为更好的选择: http :

当然,Go 的错误模型有改进的余地,但不能采用类似异常的解决方案,因为它们只会增加巨大的复杂性以换取一些可疑的好处。

@Dr-Terrible 谢谢你的文章。

但我没有找到任何地方提到 GoLang 作为一种学术语言。

顺便说一句,为了说明我的观点,在这个例子中

func Execute() error {
    err := Operation1()
    if err!=nil{
        return err
    }

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

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

    err = Operation4()
    return err
}

类似于在 C# 中实现异常处理,如下所示:

         public void Execute()
        {

            try
            {
                Operation1();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation2();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation3();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation4();
            }
            catch (Exception)
            {
                throw;
            }
        }

在 C# 中,这不是一种糟糕的异常处理方式吗? 我的答案是肯定的,我不知道你的! 在围棋中,我别无选择。 这是可怕的选择或高速公路。 这就是 GO 中的情况,我别无选择。

顺便说一句,正如您分享的文章中所提到的,任何语言都可以像 Go 一样实现错误处理,而无需任何额外的语法,因此 Go 实际上并没有实现任何革命性的错误处理方式。 它只是没有任何错误处理方式,因此您只能使用 If 语句进行错误处理。

顺便说一句,我知道 GO 有一个非推荐的Panic, Recover , Defer ,它有点类似于Try Catch Finally但在我个人看来Try Catch Finally语法更清晰,更有条理的异常处理方式。

@可怕的博士

也请检查一下:
https://github.com/manucorporat/try

@KamyarM ,他没有说 Go 是一种学术语言,他说它是基于学术研究的。 这篇文章也不是关于 Go 的,但它调查了 Go 所采用的错误处理范式。

如果您发现manucorporat/try适合您,请务必在您的代码中使用它。 但是为语言本身添加try/catch的成本(性能、语言复杂性等)不值得权衡。

@KamyarM
你举的例子不准确。 替代

    err := Operation1()
    if err!=nil {
        return err
    }
    err = Operation2()
    if err!=nil{
        return err
    }
    err = Operation3()
    if err!=nil{
        return err
    }
    return Operation4()

将会

            Operation1();
            Operation2();
            Operation3();
            Operation4();

在这个例子中,异常处理似乎是更好的选择。 理论上应该不错,但实际上
您必须针对端点中发生的每个错误回复准确的错误消息。
Go 中的整个应用程序通常是 50% 的错误处理。

         err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

如果人们将拥有像 try catch 这样强大的工具,我 100% 肯定他们会过度使用它以支持谨慎的错误处理。

提到学术界很有趣,但 Go 是从实践经验中吸取的教训的集合。 如果目标是编写返回错误错误消息的糟糕 API,则异常处理是可行的方法。

但是,当我的请求包含格式错误的JSON request body时,我不希望出现invalid HTTP header错误,异常处理是神奇的即发即弃按钮,它在使用的 C++ 和 C# API 中实现了这一点他们。

对于大的 API 覆盖范围,不可能提供足够的错误上下文来实现有意义的错误处理。 那是因为任何好的应用程序在 Go 中都会有 50% 的错误处理,而在需要非本地控制传输来处理错误的语言中应该是 90%。

@gladkikhartem

您提到的替代方法是用 C# 编写代码的正确方法。 它只有 4 行代码,显示了愉快的执行路径。 它没有那些if err!=nil噪音。 如果发生异常,关心这些异常的函数可以使用Try Catch Finally处理它(它可以是同一个函数本身,也可以是同一个函数本身,也可以是调用者的调用者,也可以是调用者的调用者的调用者的调用者...或者只是一个处理应用程序中所有未处理错误的事件处理程序。程序员有不同的选择。)

err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

看起来很简单,但这是一个棘手的问题。 我想,您可以将一个自定义错误类型放在一起,该类型包含系统错误、用户错误(不会将内部状态泄露给可能没有最好意图的用户)和 HTTP 代码。

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

但是试一试

func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err}:nil;
}
func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err};
}



md5-9bcd2745464e8d9597cba6d80c3dcf40



```go
func Chdir(dir string) error {
    n , _ := syscall.Chdir(dir):
               // something to do
               fmt.Println(n)
}

所有这些都包含某种不明显的魔法,这并不能为读者简化事情。 在前两个示例中, err变成了某种伪关键字或自发变量。 在后两个示例中,完全不清楚:运算符应该做什么——是否会自动返回错误? 运算符的 RHS 是单个语句还是块?

FWIW,我会编写一个包装函数,以便您可以执行return newPathErr("chdir", dir, syscall.Chdir(dir))并且如果第三个参数为零,它将自动返回一个零错误。 :-)

IMO,我见过的最好的建议是实现“简化 Go 中的错误处理”和“返回带有附加上下文信息的错误”的目标,来自@mkaspa的 #21732:

a, b, err? := f1()

扩展到这个:

if err != nil {
   return nil, errors.Wrap(err, "failed")
}

我可以强迫它恐慌:

a, b, err! := f1()

扩展到这个:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

这将保持向后兼容性,并修复 go 中错误处理的所有痛点

这不处理像 bufio 函数返回非零值和错误的情况,但我认为在您关心其他返回值的情况下进行显式错误处理是可以的。 当然,非错误返回值需要是该类型的适当 nil 值。

这 ? 修饰符将减少传递函数的样板错误和 ! 修饰符将在其他语言中使用断言的地方做同样的事情,例如在一些主要功能中。

这个解决方案的优点是非常简单,不会做太多,但我认为符合本提案声明中提出的要求。

如果您有...

func foo() (int, int, error) {
    a, b, err? := f1()
    return a, b, nil
}
func bar() (int, error) {
    a, b, err? := foo()
    return a+b, nil
}

如果foo ,那么在bar的调用站点,错误会用相同的文本进行双重包装,而不会增加任何含义。 至少,我会反对建议中的errors.Wrap部分。

但进一步扩大,这样的预期结果是什么?

func baz() (a, b int, err error) {
  a = 1
  b = 2
  a, b, err? = f1()
  return

ab重新分配为 nil 值? 如果是这样,那就是魔法,我觉得我们应该避免。 他们执行先前分配的值吗? (我自己并不关心命名的返回值,但出于本提案的目的,它们仍然应该被考虑。)

@dup2X是的,让我们删除语言,应该

@object88很自然地期望在发生错误的情况下,其他所有内容都归零。 这很容易理解并且没有任何魔法,几乎已经是 Go 代码的约定,几乎不需要记住并且没有特殊情况。 如果我们允许保留值,那么事情就会变得复杂。 如果您忘记检查错误,返回值可能会被意外使用,在这种情况下任何事情都可能发生。 就像在部分分配的结构上调用方法而不是在 nil 上恐慌一样。 程序员甚至可能开始期望在出现错误时返回某些值。 在我看来,这将是一团糟,不会从中获得任何好处。

至于包装,我认为默认消息没有提供任何有用的信息。 将错误链接在一起就可以了。 就像异常中有内部异常一样。 对于调试库深处的错误非常有用。

恕我直言,我不同意,@creker。 我们在 Go stdlib 中有这种情况的例子,即使在非 nil 错误的情况下也有非 nil 返回值,并且实际上是函数式的,例如bufio.Reader struct中的几个函数。 我们积极鼓励 Go 程序员检查/处理所有错误; 忽略错误比获得非零返回值和错误更令人震惊。 在您引用的情况下,如果您返回 nils 并且不检查错误,您可能仍然对无效值进行操作。

但是把它放在一边,让我们进一步研究一下。 ?运算符的语义是什么? 是否只能应用于实现error接口的类型? 它可以应用于其他类型或返回参数吗? 如果它可以应用于不实现错误的类型,它是否由任何非 nil 值/指针触发? ?运算符可以应用于多个返回值,还是编译器错误?

@erwbgy
如果你只想返回错误而不附加任何有用的东西 - 告诉编译器将所有未处理的错误视为“if err != nil return ...”会简单得多,例如:

func doStuff() error {
    doAnotherStuff() // returns error
}

func doStuff() error {
    res := doAnotherStuff() // returns string, error
}

而且不需要额外的疯狂? 在这种情况下的符号。

@object88
我已经尝试在实际代码中应用此处显示的大多数错误包装建议,但遇到了一个主要问题 - 代码变得过于密集且不可读。
它所做的只是牺牲代码宽度来支持代码高度。
用通常的 if err != nil 包装错误实际上允许传播代码以获得更好的可读性,所以我认为我们根本不应该改变任何错误包装。

@object88

在您站点的情况下,如果您返回一个 nils 并且不检查错误,您可能仍然对无效值进行操作。

但这会产生明显且易于发现的错误,例如对 nil 的恐慌。 如果您确实需要在错误时返回有意义的值,您应该明确地这样做并准确记录在这种情况下哪个值可用。 在出错时仅返回碰巧在变量中的随机内容是危险的,并且会导致细微的错误。 再一次,没有任何好处。

@gladkikhartem if err != nil 的问题是实际逻辑完全丢失在其中,如果您想了解代码在其成功路径上做了什么并且不关心所有错误处理,则必须积极搜索它. 这就像阅读大量 C 代码,其中有几行实际代码,而其他一切都只是错误检查。 人们甚至诉诸宏来包装所有这些并转到函数的末尾。

我不明白在正确编写的代码中逻辑如何变得过于密集。 这是逻辑。 您的每一行代码都包含您关心的实际代码,这就是您想要的。 你不想要的是通过一行又一行的样板。 如果有帮助,请使用注释并将代码拆分为块。 但这听起来更像是实际代码的问题,而不是语言的问题。

如果你不重新格式化它,这在操场上是有效的:

a, b, err := Frob("one string"); if err != nil { return a, b, fmt.Errorf("couldn't frob: %v", err) }
// continue doing stuff with a and b

所以在我看来,最初的提议,以及上面提到的许多其他提议,都试图为这 100 个字符提出一个清晰的速记语法,并阻止 gofmt 坚持添加换行符和重新格式化跨 3 行的块。

所以让我们想象一下,我们改变 gofmt 不再坚持多行块,从上面的行开始,并尝试想出办法让它更短更清晰。

我认为分号(赋值)之前的部分不应该改变,所以我们可能会减少剩下的 69 个字符。 其中,49 个是 return 语句、要返回的值和错误包装,我认为更改其语法没有太大价值(例如,通过将 return 语句设为可选,这会使用户感到困惑)。

这样就可以找到; if err != nil { _ }的简写,其中下划线代表一段代码。 我认为任何简写都应该明确包含err以保持清晰,即使它使 nil 比较有些不可见,所以我们留下了; if _ != nil { _ }的简写。

想象一下我们使用单个字符。 我将选择 § 作为该角色的占位符。 代码行将是:

a, b, err := Frob("one string") § err return a, b, fmt.Errorf("couldn't frob: %v", err)

我不知道如何在不更改现有赋值语法或返回语法或不发生隐形魔法的情况下做得比这更好。 (仍然有一些神奇之处,因为我们将 err 与 nil 进行比较的事实并不明显。)

这是 88 个字符,在 100 个字符的行中总共节省了 12 个字符。

所以我的问题是:这真的值得做吗?

编辑:我想我的观点是,当人们看着 Go 的if err != nil块并说“我希望我们能摆脱那些垃圾”时,他们谈论的 80-90% 是你天生拥有的 _stuff为了处理错误而要做的事情_。 Go 的语法造成的实际开销是最小的。

@lpar ,您主要遵循我上面应用的相同逻辑,所以我自然同意您的推理。 但是我认为您低估了将所有错误内容放在正确位置的视觉吸引力:

a, b := Frob("one string")  § err { return ... }

比仅仅减少字符的因素更具可读性。

@lpar如果您删除几乎无用的fmt.Errorf ,您可以保存更多字符,将 return 更改为某些特殊语法并将调用堆栈引入错误,以便它们具有实际上下文,而不仅仅是美化的字符串。 那会让你得到这样的东西

a, b, err? := Frob("one string")

对我来说,Go 错误的问题总是缺乏上下文。 返回和包装字符串对于确定错误实际发生的位置根本没有用。 这就是为什么例如github.com/pkg/errors对我来说是必须的。 有了这样的错误,我得到了 Go 错误处理简单性的好处,以及完美捕获上下文并允许您找到确切故障位置的异常的好处。

而且,即使我们以你的例子为例,错误处理在右边的事实也是一个重要的可读性升级。 您不再需要跳过几行样板才能了解代码的实际含义。 关于错误处理的重要性,你可以说你想要什么,但是当我阅读代码以理解它时,我并不关心错误。 我所需要的只是成功的道路。 当我确实需要错误时,我会专门寻找它们。 错误,就其性质而言,属于例外情况,应尽可能少占用空间。

我认为fmt.Errorferrors.Wrap相比是否“无用”的问题与这个问题是正交的,因为它们都同样冗长。 (在实际应用程序中,我也不使用,我使用其他东西也记录错误和发生错误的代码行。)

所以我想这归结为一些人真的很喜欢正确的错误处理。 我只是不太相信,即使来自 Perl 和 Ruby 背景。

@lpar我使用errors.Wrap因为它会自动捕获调用堆栈 - 我真的不需要所有这些错误消息。 我更关心它发生的地方,也许,哪些参数被传递给产生错误的函数。 你甚至说你在做类似的事情 - 记录代码行以提供一些错误消息的上下文。 鉴于我们可以想出减少样板文件的方法,同时为错误提供更多上下文(这几乎是这里的建议)。

至于错误在右边。 对我来说,这不仅仅是关于位置,而是关于减少阅读充满错误处理的代码所需的认知负担。 我不相信错误如此重要以至于您希望它们占用尽可能多的空间的论点。 我实际上更希望他们尽可能多地离开。 他们不是主要故事。

@creker

这更可能是描述一个微不足道的开发人员错误,而不是生产系统中由于错误的用户输入而产生错误的错误。 如果您只需要确定错误的行号和文件路径,那么您很可能只是编写了代码并且已经知道出了什么问题。

@因为它类似于异常。 在大多数情况下,调用堆栈和异常消息足以确定发生错误的位置和原因。 在更复杂的情况下,您至少知道错误发生的地方。 默认情况下,例外为您提供此好处。 使用 Go,您要么必须将错误链接在一起,基本上模拟调用堆栈,要么包含实际的调用堆栈。

大多数情况下,在正确编写的代码中,您可以从行号和文件路径中知道确切原因,因为错误是意料之中的。 您实际上编写了一些代码以准备它可能发生。 如果发生了意想不到的事情,那么是的,调用堆栈不会为您提供原因,但会显着减少搜索空间。

@作为

根据我的经验,用户输入错误几乎可以立即处理。 真正有问题的生产错误发生在代码深处(例如,一个服务关闭,导致另一个服务抛出错误),获得正确的堆栈跟踪非常有用。 就其价值而言,Java 堆栈跟踪在调试生产问题时非常有用,而不是消息。

@creker
错误只是值,它们是函数输入和输出的一部分。 他们不可能是“出乎意料的”。
如果您想找出函数给您错误的原因 - 使用测试、日志记录等...

@gladkikhartem在现实世界中并不是那么简单。 是的,从某种意义上说,您期望错误,因为函数签名包含一个错误作为其返回值。 但是我所期望的是确切知道它发生的原因以及导致它的原因,因此您实际上知道如何修复它或根本不修复它。 错误的用户输入通常很容易通过查看错误消息来修复。 如果您使用 profocol 缓冲区并且未设置某些必填字段,那么如果您正确验证在线上收到的所有内容,那么这是预期的并且非常容易修复。

在这一点上,我不再理解我们在争论什么。 如果正确实施,堆栈跟踪或错误消息链非常相似。 它们减少了搜索空间,并为您提供有用的上下文来重现和修复错误。 我们需要的是考虑在提供足够上下文的同时简化错误处理的方法。 我绝不提倡简单比适当的上下文更重要。

这就是 Java 的论点——将所有错误代码移到其他地方,这样您就不必查看它了。 我认为这是误导; 不仅因为 Java 的机制在很大程度上失败了,还因为当我查看代码时,它在出现错误时的行为对我来说与一切正常时的行为一样重要。

没有人提出这样的论点。 我们不要将这里讨论的内容与所有错误处理都集中在一个地方的异常处理混淆。 称其为“很大程度上失败”只是一种意见,但我认为 Go 无论如何都不会再回到那种状态。 Go 错误处理只是不同的,可以改进。

@creker我试图提出同样的观点并要求澄清什么被认为是有意义/有用的错误消息。

事实是,我会在任何一天赠送质量不定的错误消息文本(开发人员在当时并具有这种知识的偏见)以换取调用堆栈和函数参数。 使用错误消息文本( fmt.Errorferrors.New ),您最终会在源代码中搜索文本,同时阅读调用堆栈/回溯(这似乎很讨厌,我希望不是出于审美原因)对应于直接通过文件/行号( errors.Wrap和类似的)查找。

两种不同的风格,但目的是一样的:试图在你的脑海中重现那些条件下运行时发生的事情。

在这个主题上,问题 #19991 可能是对定义有意义错误的第二种风格的方法的有效总结。

将所有错误代码移到其他地方,这样您就不必查看它

@lpar ,如果您回应我关于将错误处理移到右侧的观点:脚注/尾注(Java)和旁注(我的提议)之间存在很大差异。 旁注只需要很小的视线转移,而不会丢失上下文。

@gdm85

你最终会在源代码中搜索文本

正是我所说的堆栈跟踪和链接错误消息相似的意思。 他们都记录了错误的路径。 只有在消息的情况下,如果您编写它们时不够小心,您最终可能会得到完全无用的消息,这些消息可能来自程序中的任何地方。 链式错误的唯一好处是能够记录变量的值。 即使在函数参数甚至变量的情况下也可以自动化,至少对我来说,几乎可以涵盖我需要的所有错误。 它们仍然是值,如果需要,您仍然可以键入 switch 它们。 但是在某些时候,您可能会记录它们并且能够看到堆栈跟踪非常有用。

看看 Go 对恐慌做了什么。 您可以获得每个 goroutine 的完整堆栈跟踪。 我不记得有多少次它帮助我确定错误的原因并立即修复它。 它经常让我惊讶它是多么容易。 它非常流畅,整个语言非常可预测,您甚至不需要调试器。

与 Java 相关的一切似乎都有污名,人们通常不会提出任何论点。 这很糟糕,只是因为。 我不喜欢 Java,但这种推理对任何人都没有帮助。

同样,错误不是由开发人员修复错误的。 这是错误处理的好处之一。 Java 方式告诉开发人员这就是错误处理,而不是。 错误可能存在于应用层和流层之外。 Go 中的错误通常用于控制系统采用的恢复策略——在运行时,而不是编译时。

当语言通过解开堆栈并丢失他们在错误发生之前所做的一切的记忆,由于错误而削弱了他们的流程控制时,这可能是不可理解的。 在 Go 中,错误在运行时实际上很有用; 我不明白为什么他们应该携带行号之类的东西——运行代码几乎不关心这个。

@作为

同样,错误不是由开发人员修复错误的

这是完全错误的。 错误正是出于这个原因。 它们不仅限于此,但它是主要用途之一。 错误表明系统有问题,您应该对此采取措施。 对于预期和简单的错误,您可以尝试恢复,例如 TCP 超时。 对于更严重的事情,您将其转储到日志中,然后再调试问题。

这是错误处理的好处之一。 Java 方式告诉开发人员这就是错误处理,而不是。

我不知道 Java 教了你什么,但我出于同样的原因使用异常 - 控制系统在运行时采用的恢复策略。 Go 在错误处理方面没有什么特别之处。

当语言通过解开堆栈并丢失他们在错误发生之前所做的一切的记忆,由于错误而削弱了他们的流程控制时,这可能是不可理解的

可能是给某人的,不是给我的。

在 Go 中,错误在运行时实际上很有用; 我不明白为什么他们应该携带行号之类的东西——运行代码几乎不关心这个。

如果您关心修复代码中的错误,那么行号就是这样做的方法。 不是 Java 教会了我们这个,C 正是因为这个原因有__LINE____FUNCTION__ 。 你想记录你的错误并记录它发生的确切位置。 当出现问题时,您至少有一些事情可以开始。 不是由不可恢复的错误引起的随机错误消息。 如果您不需要此类信息,请忽略它。 它不会伤害你。 但至少它在那里并且可以在需要时使用。

我不明白为什么这里的人们总是将对话转移到异常与错误值之间。 没有人进行这种比较。 唯一讨论的是堆栈跟踪非常有用并且带有很多上下文信息。 如果这是不可理解的,那么您可能生活在一个完全不同的宇宙中,其中不存在追踪。

这是完全错误的。

但是我所指的生产系统仍在运行,它使用错误进行流控制,用 Go 编写,并用一种​​使用堆栈跟踪进行错误传播的语言替换了较慢的实现。

如果这是不可理解的,那么您可能生活在一个完全不同的宇宙中,其中不存在追踪。

要为每个返回错误类型的函数链接调用堆栈信息,请自行决定。 堆栈跟踪速度较慢,出于安全原因不适合在玩具项目之外使用。 让他们成为一流的围棋公民只是为了帮助粗心的错误传播策略,这是一种技术犯规。

如果您不需要此类信息,请忽略它。 它不会伤害你。

软件膨胀是服务器被 Go 重写的原因。 您看不到的内容仍然会降低管道的吞吐量。

我更喜欢从拥有此功能中受益的实际软件示例,而不是有关处理 TCP 超时和日志转储的不太相关的课程。

堆栈跟踪较慢

鉴于堆栈跟踪是在错误路径中生成的,没有人关心它们有多慢。 软件的正常运行已经中断。

出于安全原因,不适合在玩具项目之外使用

到目前为止,我还没有看到单个生产系统由于“安全原因”而关闭堆栈跟踪,或者根本没有。 另一方面,能够快速识别代码产生错误的路径非常有用。 这适用于大型项目,有许多不同的团队在代码库上工作,但没有人对整个系统有充分的了解。

您看不到的内容仍然会降低管道的吞吐量。

不,它真的没有。 正如我之前所说,堆栈跟踪是在错误时生成的。 除非您的软件经常遇到它们,否则吞吐量不会受到一点影响。

鉴于堆栈跟踪是在错误路径中生成的,没有人关心它们有多慢。 软件的正常运行已经中断。

不真实。

  • 错误可能是正常操作的一部分。
  • 错误可以恢复,程序可以继续,所以性能仍然存在问题。
  • 减慢一个例程会消耗_are_在快乐路径中运行的其他例程的资源。

@object88想象真实的生产代码。 您预计它会产生多少错误? 我想不多。 至少在正确编写的应用程序中。 如果 goroutine 处于繁忙循环中并且在每次迭代中不断抛出错误,则代码有问题。 但即使是这样,鉴于大多数 Go 应用程序都是 IO 绑定的,即使这也不是一个严重的问题。

@作为

但是我所指的生产系统仍在运行,它使用错误进行流控制,用 Go 编写,并用一种​​使用堆栈跟踪进行错误传播的语言替换了较慢的实现。

我很抱歉,但这是一个无意义的句子,与我所说的无关。 不打算回答了

堆栈跟踪较慢

慢,但多少? 有关系吗? 我不这么认为。 Go 应用程序通常是 IO 绑定的。 在这种情况下,追逐 CPU 周期是愚蠢的。 你在 Go 运行时遇到了更大的问题,它会占用 CPU。 扔掉有助于修复错误的有用功能并不是一个论据。

出于安全原因,不适合在玩具项目之外使用。

我不会费心去讨论不存在的“安全原因”。 但我想提醒您,通常应用程序跟踪存储在内部,只有开发人员才能访问它们。 无论如何,试图隐藏您的函数名称都是浪费时间。 这不是安全。 我希望我不需要详细说明。

如果您坚持出于安全原因,我希望您考虑 macOS/iOS,例如。 他们可以毫无问题地抛出包含所有线程的堆栈和所有 CPU 寄存器的值的恐慌和崩溃转储。 不要看到他们受到这些“安全原因”的影响。

让他们成为一流的围棋公民只是为了帮助粗心的错误传播策略,这是一种技术犯规。

你能不能再主观一点? “粗心的错误传播策略”你在哪里看到的?

软件膨胀是服务器被 Go 重写的原因。 您看不到的内容仍然会降低管道的吞吐量。

再次,多少钱?

我更喜欢从拥有此功能中受益的实际软件示例,而不是有关处理 TCP 超时和日志转储的不太相关的课程。

在这一点上,我似乎正在与除程序员之外的任何人交谈。 跟踪有益于任何和所有软件。 它是所有语言和所有类型软件中的通用技术,可帮助修复错误。 如果您想了解更多信息,可以阅读维基百科。

在没有任何共识的情况下进行如此多的徒劳讨论意味着没有优雅的方法来解决这个问题。

@object88
如果您想跟踪所有 goroutine,堆栈跟踪可能会很慢,因为 Go 应该等待其他 goroutine 解除阻塞。
如果您只是跟踪当前正在运行的 goroutine - 它并没有那么慢。

@creker
跟踪的好处所有的软件,但它取决于你在什么样的跟踪。 在我参与的大多数 Go 项目中,跟踪堆栈不是一个好主意,因为涉及并发。 数据来回移动,很多东西相互通信,一些 goroutines 只是几行代码。 在这种情况下进行堆栈跟踪无济于事。
这就是为什么我使用包含写入日志的上下文信息的错误来重新创建相同的堆栈跟踪,但它不绑定到实际的 goroutine 堆栈,而是绑定到应用程序逻辑本身。
这样我就可以只做 cat *.log | grep "orderID=xxx" 并获取导致错误的实际操作序列的堆栈跟踪。
由于 Go 上下文丰富的错误的并发特性比堆栈跟踪更有价值。

@gladkikhartem感谢您花时间撰写正确的论点。 我开始对那次谈话感到沮丧。

我理解你的论点并部分同意它。 尽管如此,我发现自己必须处理至少 5 个函数的堆栈。 这已经足够大,可以了解正在发生的事情以及您应该从哪里开始寻找。 但是在具有大量非常小的 goroutine 的高并发应用程序中,堆栈跟踪失去了它们的好处。 我同意。

@creker

想象一下真实的生产代码。 您预计它会产生多少错误? [...],鉴于大多数 Go 应用程序都是 IO 绑定的,即使这不是一个严重的问题。

很好,你提到了 IO 绑定操作。 io.Reader Read 方法在 EOF 返回一个正常的错误。 所以这将在快乐的道路上发生很多。

@urandom

堆栈跟踪不自觉地暴露了对系统分析很有价值的信息。

  • 用户名
  • 文件系统路径
  • 后端数据库类型/版本
  • 交易流程
  • 对象结构
  • 加密算法

我不知道一般的应用程序是否会注意到在错误类型中收集堆栈帧的开销,但我可以告诉你,对于性能关键的应用程序,由于当前的函数调用开销,许多小的 Go 函数是手动内联的。 跟踪会使情况变得更糟。

我相信 Go 的目标是拥有简单快速的软件,而追踪则是一种退步。 我们应该能够编写小函数并从这些函数返回错误,而不会导致性能下降,从而鼓励非常规错误类型和手动内联。

@creker

我将避免给你造成进一步不和谐的例子。 我很抱歉让你失望了。

我建议使用一个新的关键字“returnif”,它的名称立即显示了它的功能。 此外,它足够灵活,可以用于比错误处理更多的用例。

示例 1(使用命名返回):

a, 错误 = 某事(b)
如果错误!= nil {
返回
}

会成为:

a, 错误 = 某事(b)
returnif err != nil

示例 2(不使用命名返回):

a,错误:=某事(b)
如果错误!= nil {
返回一个,错误
}

会成为:

a,错误:=某事(b)
returnif err != nil { a, err }

关于您命名的返回示例,您的意思是...

a, err = something(b)
returnif err != nil

@安伯纳迪诺
为什么不直接更新 fmt 工具,而不必更新语言语法并添加新的无用关键字

a, err := something(b)
if err != nil { return a, err }

或者

a, err := something(b)
    if err != nil { return a, err }

@gladkikhartem这个想法不是每次你想传播错误时都输入,我宁愿这样做,应该以同样的方式工作

a, err? := something(b)

@mkaspa
这个想法是使代码更具可读性。 输入代码不是问题,阅读才是。

@gladkikhartem rust 使用这种方法,我认为它不会降低可读性

@gladkikhartem我不认为?会降低它的可读性。 我会说它完全消除了噪音。 对我来说,问题在于噪声也消除了提供有用上下文的可能性。 我只是不知道您可以在哪里插入通常的错误消息或包装错误。 调用堆栈是一个显而易见的解决方案,但正如已经提到的,并不适用于所有人。

@mkaspa
而且我认为它降低了可读性,下一步是什么? 我们正在努力寻找最佳解决方案还是只是分享意见?

@creker
'? 字符增加了读者的认知负担,因为返回什么并不那么明显,当然人们应该知道它在做什么。 而且当然 ? 标志在读者的脑海中提出了问题。

正如我之前所说,如果你想摆脱 err != nil 编译器可以检测未使用的错误参数并自己转发它们。

a, err? := doStuff(a,b)
err? := doAnotherStuff(b,z,d,g)
a, b, err? := doVeryComplexStuff(b)

将变得更具可读性

a := doStuff(a,b)
doAnotherStuff(b,z,d,g)
a, b := doVeryComplexStuff(b)

同样的魔法,只需要打字和思考的东西就少了

@gladkikhartem好吧,我认为没有一种解决方案不需要读者学习新东西。 这就是改变语言的结果。 我们必须权衡——要么我们在你的面部语法中使用冗长的语法,以准确地显示原始术语所做的事情,要么我们引入可以隐藏冗长的新语法,添加一些语法糖等。没有其他方法。 仅仅拒绝任何增加读者学习内容的东西都会适得其反。 我们不妨关闭所有 Go2 问题并收工。

至于您的示例,它引入了更多神奇的东西并隐藏了任何注入点以引入允许开发人员为错误提供上下文的语法。 而且,最重要的是,它完全隐藏了有关哪个函数调用可能引发错误的任何信息。 这闻起来越来越像例外。 如果我们只是认真地重新抛出错误,那么堆栈跟踪就成为必须的,因为这是在这种情况下您可以保留上下文的唯一方法。

最初的提案实际上已经很好地涵盖了所有这些。 它足够冗长,并为您提供了一个包装错误和提供有用上下文的好地方。 但主要问题之一是这种神奇的“错误”。 我认为它很丑,因为它不够神奇,也不够冗长。 它有点在中间。 可能让它变得更好的是引入更多的魔法。

如果||会产生一个自动包装原始错误的新错误怎么办。 所以这个例子变成了这样

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir}
    return nil
}

err消失了,所有的包装都是隐式处理的。 现在我们需要一些方法来访问这些内部错误。 我认为向error接口添加另一种方法,如Inner() error是行不通的。 一种方法是引入像unwrap(error) []error这样的内置函数。 它所做的是按照它们被包装的顺序返回所有内部错误的一部分。 这样你就可以访问任何内部错误或范围。

鉴于error只是一个接口,并且您需要一个地方来放置任何自定义error类型的包装错误,因此这种实现是有问题的。

对我来说,这符合所有条件,但可能有点太神奇了。 但是,鉴于 error 接口的定义非常特殊,将其更接近一等公民可能不是一个坏主意。 错误处理很冗长,因为它只是一个普通的 Go 代码,没有什么特别之处。 这在纸面上可能很好,并且可以制作华而不实的头条新闻,但错误太特殊了,不值得这样对待。 他们需要特殊的外壳。

最初的提议是关于减少错误检查的数量还是每个单独检查的长度?

如果是后者,那么以只有一个条件陈述和一个重复陈述为由驳斥提案的必要性是微不足道的。 有些人不喜欢 for 循环,我们是否也应该提供一个隐式循环结构?

到目前为止,提议的语法更改是一个有趣的思想实验,但没有一个像原始的那样清晰、可发音或简单。 Go 不是 bash,错误不应该是“魔法”。

我越来越多地阅读这些提案,越来越多的人看到人们的论点不过是“它增加了一些新的东西,所以它很糟糕,不可读,让一切保持原样”。

@as提案给出了它试图实现的目标的摘要。 正在做什么是非常明确的。

与原文一样清晰、清晰或简单

任何提案都会引入新的语法,对于某些人来说,新的语法听起来与“不可读、复杂等”相同。 仅仅因为它是新的,并不会使它变得不那么清晰、易读或简单。 “||” 和 ”?” 一旦您知道它的作用,示例就会像现有语法一样清晰和简单。 或者我们应该开始抱怨“->”和“<-”太神奇了,读者必须知道它们的意思? 让我们用方法调用替换它们。

Go 不是 bash,错误不应该是“魔法”。

这是完全没有根据的,也不能作为任何理由。 我无法理解这与 Bash 有什么关系。

@creker
是的,我完全同意你介绍的事件更多的魔法。 我的例子只是 ? 操作员输入更少内容的想法。

我同意我们需要牺牲一些东西并引入一些改变,当然还有一些魔法。 这只是可用性的优点和这种魔法的缺点的平衡。

原文 || 提案非常好,经过实际测试,但我认为格式很难看,我建议将格式更改为

syscal.Chdir(dir)
    || return &PathError{"chdir", dir}

PS,您如何看待这种魔术语法的变体?

syscal.Chdir(dir) {
    return &PathError{"chdir", dir}
}

从可读性的角度来看, @gladkikhartem两者看起来都不错,但我对后者有一种不好的感觉。 它引入了这个我不太确定的奇怪块作用域。

我鼓励您不要孤立地查看语法,而应在函数的上下文中查看。 此方法有几个不同的错误处理块。

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath, err := p.preparePath()
        if err != nil {
                return nil, err
        }
    fis, err := l.context.ReadDir(absPath)
    if err != nil {
        return nil, err
    } else if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

提议的更改如何清理此功能?

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath, err? := p.preparePath()
    fis, err? := l.context.ReadDir(absPath)
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
         if _, ok := err.(*build.NoGoError); ok {
             // There isn't any Go code here.
             return nil, nil
         }
         return nil, err
    }

    return buildPkg, nil
}

@bcmills提议更合适。

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath := p.preparePath()        =? err { return nil, err }
    fis := l.context.ReadDir(absPath) =? err { return nil, err }
    if len(fis) == 0 {
        return nil, nil
    }
    buildPkg := l.context.Import(".", absPath, 0) =? err {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }
    return buildPkg, nil
}

@object88

func (l *Loader) processDirectory(p *Package) (p *build.Package, err error) {
        absPath, err := p.preparePath() {
        return nil, fmt.Errorf("prepare path: %v", err)
    }
    fis, err := l.context.ReadDir(absPath) {
        return nil, fmt.Errorf("read dir: %v", err)
    }
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0) {
        err, ok := err.(*build.NoGoError)
                if !ok {
            return nil, fmt.Errorf("buildpkg: %v",err)
        }
        return nil, nil
    }
    return buildPkg, nil
}

继续戳这个。 也许我们可以想出更完整的用法示例。

@erwbgy ,这个对我来说看起来最好,但我不相信支出会那么好。

  • 什么是非错误返回值? 它们总是零值吗? 如果 _was_ 先前为命名返回分配了值,它会被覆盖吗? 如果命名值被用来存储错误函数的结果,它会返回吗?
  • ?运算符可以应用于非错误值吗? 我可以做一些类似(!ok)?ok!?事情吗(这有点奇怪,因为你将赋值和操作捆绑在一起)? 还是这种语法仅适用于error

@rodcorsi ,这个让我很ReadDir而是ReadBuildTargetDirectoryForFileInfo或类似的愚蠢的东西怎么办。 或者你有大量的论据。 preparePath的错误处理也会被推离屏幕。 在水平屏幕尺寸有限(或视口不那么宽,如 Github)的设备上,您可能会丢失=?部分。 我们非常擅长垂直滚动; 在水平上没有那么多。

@gladkikhartem ,它似乎与实现error接口的某些(仅是最后一个?)参数有关。 它看起来很像一个函数声明,只是……_感觉_很奇怪。 有什么方法可以将它绑定到ok样式的返回值吗? 总的来说,您只购买了 1 条线。

@object88
自动换行解决了非常广泛的代码问题。 不是广泛使用吗?

@object88关于很长的函数调用。 让我们处理这里的主要问题。 问题不在于屏幕外推出的错误处理。 问题是函数名很长和/或参数列表很大。 在就屏幕外的错误处理提出任何论点之前,需要先解决这个问题。

我还没有看到默认情况下设置为自动换行的 IDE 或代码友好的文本编辑器。 除了在页面加载后手动修改 CSS 之外,我还没有找到任何方法来使用 Github。

我确实认为代码宽度是一个重要因素——它与_可读性_有关,这是本提案的推动力。 声称围绕错误的“代码太多”。 并不是说功能不存在,或者错误需要以其他方式实现,而是代码读起来不好。

@object88
是的,此代码适用于任何返回错误接口作为最后一个参数的函数。

关于行保存,您不能在更少的行中放入更多信息。 代码应该均匀分布,不要太密集,也不要在每个语句之后留出空间。
我同意它看起来像一个函数声明,但同时它与现有的 if ...; 非常相似。 err != nil { 声明,所以人们不会太困惑。

代码宽度是一个重要因素。 如果我有 80 行编辑器和 80 行代码是一个函数调用,然后我有||

为了完整起见,我将抛出一个带有||语法的示例,我的自动错误包装和非错误返回值的自动归零

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath := p.preparePath() || errors.New("prepare path")
    fis := l.context.ReadDir(absPath) || errors.New("ReadDir")
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

关于您关于其他返回值的问题。 如果出现错误,它们在所有情况下都将为零值。 我已经介绍了为什么我认为这很重要。

问题是您的示例一开始并不涉及。 但它仍然显示了这个提议,至少对我来说,代表了什么。 我想让它解决的是最常见和最常用的成语

err := func()
if err != nil {
    return err
}

我们都同意这种代码是一般错误处理的重要部分(如果不是最大的)。 所以解决这个案子是合乎逻辑的。 如果你想做一些与错误有关的事情,应用一些逻辑 - 去做吧。 这就是冗长应该是程序员阅读和理解的实际逻辑的地方。 我们不需要浪费空间和时间阅读无意识的样板。 它是无意识的,但仍然是 Go 代码的重要组成部分。

关于早先关于隐式返回零值的讨论。 如果您需要在错误时返回有意义的值 - 去吧。 同样,这里的详细程度很好,有助于理解代码。 如果你需要做一些更复杂的事情,放弃语法糖并没有错。 ||足够灵活,可以解决这两种情况。 您可以省略非错误值,它们将被隐式归零。 或者,您可以根据需要明确指定它们。 我记得甚至有一个单独的提案也涉及您想要返回错误并将其他所有内容归零的情况。

@object88

声称围绕错误的“代码太多”。

这不仅仅是任何代码。 主要问题是关于错误和非常常见的错误处理案例有太多无意义的样板。 当有一些值得阅读的东西时,冗长很重要。 if err == nil then return err没有任何价值,只是您想重新抛出错误。 对于这样一个原始逻辑,它需要很多空间。 并且您拥有的逻辑、库调用、包装器等都可以很好地返回错误,这个样板越多地开始支配重要的东西 - 代码的实际逻辑。 该逻辑实际上可以包含一些重要的错误处理逻辑。 但是它在它周围的大多数样板的这种重复性中迷失了。 这可以解决,Go 与之竞争的其他现代语言尝试解决这个问题。 因为错误处理是如此重要,它不仅仅是一个常规代码。

@creker
我同意,如果err != nil return err是太多样板,我们担心的是,如果我们将创建简单的方法将错误向上转发 - 统计程序员,尤其是初级程序员将使用最简单的方法,而不是这样做在某种情况下什么是合适的。
这与 Go 的错误处理是一样的——它迫使你做一件体面的事情。
因此,在此提案中,我们希望鼓励其他人慎重处理和包装错误。

我会说我们应该让简单的错误处理看起来丑陋且难以实现,但优雅的错误处理与包装或堆栈跟踪看起来不错且易于实现。

@gladkikhartem我总是觉得这种关于古代编辑的论点很愚蠢。 谁在乎这个,为什么语言会受到这个影响? 现在是 2018 年,几乎每个人都有大显示器和一些不错的编辑器。 极少数人不应该影响其他所有人。 应该反过来——少数人应该自己处理。 滚动,自动换行,任何东西。

@gladkikhartem Go 已经

Go 实际上做的不是强迫任何事情。 Go 强迫你处理错误的概念是误导性的,而且一直都是。 Go 作者强迫你在他们的博客文章和会议演讲中这样做。 实际的语言让你做任何你想做的事。 这里的主要问题是 Go 默认选择什么——默认错误会被默默忽略。 甚至有人提议改变这一点。 如果 Go 是要强迫你做一件体面的事情,那么它应该做以下事情。 如果未正确处理返回的错误,则在编译时返回错误或在运行时发生恐慌。 据我了解,Rust 是这样做的 - 默认情况下错误会恐慌。 这就是我所说的强迫做正确的事情。

真正迫使我在 Go 中处理错误的是我的开发人员良心,没有别的。 但它总是很容易让步。现在如果我不明确地去阅读函数签名,没有人会告诉我它返回一个错误的任何事情。 有一个实际例子。 很长一段时间我都不知道fmt.Println返回了一个错误。 我没有用它的返回值,我只想打印东西。 所以我没有动力去看看它的回报。 这与 C 的问题相同。 错误是值,您可以忽略它们,直到您的代码在运行时中断为止,并且您对此一无所知,因为不会因有用的恐慌而崩溃,例如,未处理的异常。

@gladkikhartem从我对这个提案的理解

我写这个提案主要是为了鼓励那些想要简化 Go 错误处理的人思考如何轻松地围绕错误包装上下文,而不仅仅是返回未修改的错误。

@creker
我的编辑器宽度为 100 个字符,因为我有文件资源管理器、git 控制台等......,在我们的团队中没有人编写超过 100 个字符长度的代码,这很愚蠢(除了少数例外)

Go 不会强制错误处理,linters 会。 (也许我们应该为此编写一个 linter?)

好吧,如果我们不能想出解决方案,每个人都以自己的方式理解提案——为什么不为我们需要的东西指定一些要求? 首先就需求达成一致,然后开发解决方案会容易得多。

例如:

  1. 新提案的语法应该在文本中包含return语句,否则读者不清楚发生了什么。 ( 同意不同意 )
  2. 新提案应支持返回多个值的函数(同意/不同意)
  3. 新提案应该占用更少的空间(1 行,2 行,不同意)
  4. 新提案应该能够处理很长的表达(同意/不同意)
  5. 新提案应允许出现错误时的多个声明(同意/不同意)
  6. .....

@creker ,我大约 75% 的开发是在 VSCode 中的 15" 笔记本电脑上完成的。我最大化了我的水平空间,但仍然有一个限制,尤其是当我并排编辑时。我敢打赌在学生中,笔记本电脑远多于台式机。我不想限制语言的平易性,因为我们预计每个人都拥有大尺寸显示器。

不幸的是,无论你有多大的屏幕,github 仍然限制了视口。

@gladkikhartem

新手懒惰在这里适用,但在其中一些示例中随意使用errors.New也表明缺乏对语言的理解。 除非它们是动态的,否则不应在返回值中分配错误,并且如果将这些错误放入可比较的包范围变量中,则页面上的语法会更短,并且在生产代码中实际上也可以接受。 那些在 Go 错误处理“样板”中受害最深的人走的捷径最多,并且没有足够的经验来正确处理错误。

simplifying error handling构成并不明显,但先例是less runes != simple 。 我认为为了简单起见,有一些限定词可以以可量化的方式衡量结构:

  • 构造表达方式的数量
  • 该构造与其他构造的相似性,以及这些构造之间的内聚力
  • 构造总结的逻辑操作数
  • 构造与自然语言的相似性(即,没有否定等)

例如,原始提案将传播错误的方式从 2 种增加到 3 种。它类似于逻辑 OR,但具有不同的语义。 它总结了低复杂度的条件回报(与copyappend>> )。 新方法不如旧方法自然,如果大声说出来可能是abs, err := path(foo) || return err -> if theres an error, it's returning err在这种情况下,如果您可以使用竖线,这将是一个谜可以像在代码审查中大声说出来的一样编写它。

@作为
完全同意less runes != simple
简单我的意思是可读和可理解的。
因此,任何不熟悉 go 的人都应该阅读它并了解它的作用。
它应该像一个笑话——你不必解释它。

当前的错误处理实际上是可以理解的,但如果你有太多的if err != nil return.不是完全可读的

@object88没关系。 我说得比较笼统,因为这个论点经常出现。 比如,让我们想象一些可用于编写围棋的可笑的古老终端屏幕。 这是一种什么样的论调? 可笑的极限在哪里? 如果我们认真对待它,那么我们应该观察确凿的事实——最流行的屏幕尺寸和分辨率是多少。 只有这样我们才能画出一些东西。 但是争论通常只是想象一些没有人使用的屏幕尺寸,但有人可以使用的可能性很小。

@gladkikhartem不,他们建议,linters 不会强迫您。 这里有很大的不同。 它们是可选的,不是 Go 工具链的一部分,只提供建议。 强制只能意味着两件事 - 编译或运行时错误。 其他一切都是建议和选择。

我同意,我们应该更好地制定我们想要的内容,因为该提案并未完全涵盖所有方面。

@作为

新方法不如旧方法自然,如果大声说出来可能是 abs, err := path(foo) || return err -> 如果出现错误,则返回 err,在这种情况下,如果您可以像在代码审查中大声说出来的那样编写它,为什么可以使用竖线,这将是一个谜。

新方法不太自然的原因只有一个——它现在不是语言的一部分。 没有其他原因。 想象一下 Go 已经使用了这种语法——这很自然,因为你很熟悉它。 就像您熟悉->selectgo和其他语言中不存在的其他东西一样。 为什么可以使用竖线代替回车? 我用一个问题来回答。 当你可以用循环做同样的事情时,为什么有一种方法可以在一次调用中附加切片? 当您可以对循环执行相同操作时,为什么有一种方法可以在一次调用中将内容从读取器接口复制到写入器接口? etc etc etc 因为您希望您的代码更紧凑且更具可读性。 当 Go 已经用大量示例与它们相矛盾时,您正在提出这些论点。 再一次,让我们更加开放,不要仅仅因为它是新的并且尚未在语言中就否定任何东西。 我们不会因此而取得任何成就。 有一个问题,很多人都在呼吁解决,让我们来处理它。 Go 不是一种神圣的理想语言,它不会被任何添加到其中的东西亵渎。

当你可以用循环做同样的事情时,为什么有一种方法可以在一次调用中附加切片?

编写 if 语句错误检查是微不足道的,我很想看看您对append

当您可以对循环执行相同操作时,为什么有一种方法可以在一次调用中将内容从读取器接口复制到写入器接口?

读取器和写入器抽象出复制操作的源和目标、缓冲策略,有时甚至是循环中的标记值。 你不能用循环和切片来表达这种抽象。

当 Go 已经用大量示例与它们相矛盾时,您正在提出这些论点。

我不相信是这样,至少在那些例子中不是。

再一次,让我们更加开放,不要仅仅因为它是新的并且尚未在语言中就否定任何东西。

鉴于 Go 有兼容性保证,您应该最仔细地检查新功能,因为如果它们很糟糕,您将不得不永远处理它们。 到目前为止,这里没有人做的是创建了一个实际的概念证明,并在一个小型开发团队中使用它。

如果您查看一些提案(例如泛型)的历史,您会发现,在完成之后,实现往往是:“哇,这实际上不是一个好的解决方案,让我们暂时不要进行任何更改”。 另一种选择是一种充满建议的语言,并且没有简单的方法可以追溯驱逐它们。

关于宽屏与薄屏的事情,另一件要考虑的事情是多任务处理

您可能有多个并排的窗口,以便在您将一些代码放在一起时偶尔跟踪其他内容,而不是仅仅盯着编辑器,将上下文完全切换到另一个窗口以查找函数,也许是 StackOverflow,并一路跳回编辑器。

@作为
完全同意大多数提议的功能是不切实际的,我开始认为 || 和 ? 事情可能是这样。

@creker
copy() 和 append()不是要实现的微不足道的任务

我在 CI/CD 上有 linters,它们确实迫使我处理所有错误。 它们不是语言的一部分,但它不在乎——我只需要结果。
(顺便说一句,我有强烈的意见 - 如果有人不在 Go 中使用短绒 - 他只是......)

关于屏幕尺寸 - 真的一点都不好笑。 请停止这种无关紧要的讨论。 您的屏幕可以任意宽 - 您总是有可能无法看到|| return &PathError{Err:err}部分代码。 只需谷歌单词“ide”,看看什么样的空间可用于代码。

并且请仔细阅读其他人的文字,我没有说 Go 强制您处理所有错误

这与 Go 的错误处理是一样的——它迫使你做一件体面的事情。

@gladkikhartem Go 在错误处理方面不强制执行任何操作,这就是问题所在。 体面与否,没关系,那只是挑剔。 尽管对我来说这意味着在所有情况下处理所有错误,除了像fmt.Println

如果有人不在 Go 中使用短绒 - 他只是

也许是。 但是,如果某些东西不是真的被迫,那么它就不会飞。 有些人会使用它,有些人不会。

关于屏幕尺寸 - 真的一点都不好笑。 请停止这种无关紧要的讨论。

我不是开始抛出应该以某种方式影响决策的随机数的人。 我明确表示我理解问题,但它应该是客观的。 不是“我有 80 个符号宽的 IDE,Go 应该考虑到这一点并忽略其他所有人”。

如果我们谈论的是我的屏幕尺寸。 Visual Studio 代码为我提供了 270 个水平空间符号。 我不会主张占用那么多空间是正常的。 但是当您考虑带有注释的结构和特别长的命名字段类型时,我的代码很容易超过 120 个符号。 如果我要使用||语法,那么在 3-5 个参数函数调用和带有自定义消息的包装错误的情况下,它很容易适应 100-120。

以太方式如果要实现||的东西,那么 gofmt 可能不应该强迫您将它写在一行上。 在某些情况下,它可能会占用太多空间。

@erwbgy ,这个对我来说看起来最好,但我不相信支出会那么好。

@object88对我来说,

val, err := func()
if err != nil {
    return nil, errors.WithStack(err)
}

更简单:

val, err? := func()

没有什么能阻止更复杂的错误处理以当前方式完成。

什么是非错误返回值? 它们总是零值吗? 如果之前为命名返回分配了一个值,它会被覆盖吗? 如果命名值被用来存储错误函数的结果,它会返回吗?

所有其他返回参数都是适当的 nil 值。 对于命名参数,我希望它们保留任何先前分配的值,因为它们已经保证已经分配了一些值。

可以吗? 运算符应用于非错误值? 我可以做类似 (!ok) 的事情吗? 或者好吗!? (这有点奇怪,因为您将分配和操作捆绑在一起)? 还是这种语法仅适用于错误?

不,我认为将此语法用于错误值以外的任何其他内容没有任何意义。

我认为出于对更具可读性的代码的绝望,“必须”函数正在激增。

sqlx

db.MustExec(schema)

html模板

var t = template.Must(template.New("name").Parse("html"))

我建议使用恐慌运算符(不确定是否应该将其称为“运算符”)

a,  😱 := someFunc(b)

相同,但可能比

a, err := someFunc(b)
if err != nil {
  panic(err)
}

😱 可能太难打字了,我们可以使用诸如 !, or !!, or

a,  !! := someFunc(b)
!! = maybeReturnsError()

或许 !! 恐慌和! 回报

我的 2 美分的时间。 为什么我们不能只使用标准库的debug.PrintStack()进行堆栈跟踪? 这个想法是仅在发生错误的最深级别打印堆栈跟踪。

为什么我们不能只使用标准库的debug.PrintStack()进行堆栈跟踪?

错误可能会跨越许多堆栈。 它们可以跨通道发送,存储在变量中等。了解这些转换点通常比了解第一次产生错误的片段更有帮助。

此外,堆栈跟踪本身通常包括内部(未导出的)辅助函数。 当您尝试调试意外崩溃时,这很方便,但对正常操作过程中发生的错误没有帮助。

对于完整的编程新手来说,最用户友好的方法是什么?

我发现了更简单的版本。 只需要一个if !err
没什么特别的,直观的,没有额外的标点符号,代码更小

```去
绝对路径,错误:= p.preparePath()
返回 nil,如果出错则出错

错误 := doSomethingWith(absPath) 如果 !err
doSomethingElse() 如果 !err

doSomethingRegardlessOfErr()

// 在一处处理错误; 如果需要的话; 类似捕捉而不缩进
如果错误{
返回“没有代码污染的错误”,错误
}
``

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

欢迎回来,好老的 MUMPS 发布条件 ;-)

谢谢,但不用谢。

@dmajkic这对“返回带有附加上下文信息的错误”没有任何帮助。

@erwbgy这个问题的标题是 _proposal:Go 2:用 || 简化错误处理 err suffix_ 我的评论就是在这种情况下。 抱歉,如果我参与了之前的讨论。

@cznic 是的。 后置条件不是 Go-way,但前置条件看起来也被污染了:

if !err; err := doSomethingWith(absPath)
if !err; doSomethingElse()

@dmajkic提案不仅仅是标题——ianlancetaylor 描述了三种处理错误的方法,并特别指出很少有提案可以更容易地返回带有附加信息的错误。

@erwbgy我确实经历了@ianlancetaylor指定的所有问题,它们都依赖于添加新关键字(如try() )或使用特殊的非字母数字字符。 就个人而言 - 我不喜欢那样,因为用 !"#$%& 重载的代码看起来很冒犯,就像骂人一样。

我确实同意并感觉到这个问题的前几行所说的:太多的 Go 代码进行错误处理。 我提出的建议符合这种情绪,建议非常接近 Go 现在的感觉,不需要额外的关键字或关键字符。

有条件的延迟怎么样

func something() (int, error) {
    var error err
    var oth err

    defer err != nil {
        return 0, mycustomerror("More Info", err)
    }
    defer oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }

    _, err = a()
    _, err = b()
    _, err = c()
    _, oth = d()
    _, err = e()

    return 2, nil
}


func something() (int, error) {
    var error err
    var oth err

    _, err = a()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = b()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = c()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, oth = d()
    if oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }
    _, err = e()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }

    return 2, nil
}

这将显着改变defer的含义——它只是在作用域末尾运行的东西,而不是导致作用域提前退出的东西。

如果他们在这种语言中引入 Try Catch,所有这些问题都会以非常简单的方式解决。

他们应该介绍这样的东西。 如果将 error 的值设置为 nil 以外的值,则可能会中断当前工作流程并自动触发 catch 部分,然后 finally 部分和当前库也可以正常工作而无需更改。 问题解决了!

try (var err error){
     i, err:=DoSomething1()
     i, err=DoSomething2()
     i, err=DoSomething3()
} catch (err error){
   HandleError(err)
   // return err  // similar to throw err
} finally{
  // Do something
}

image

@sbinet这总比没有好,但如果他们只是简单地使用每个人都熟悉的相同 try-catch 范式,那就更好了。

@KamyarM您似乎建议添加一种机制,以便在将变量设置为非零值时抛出异常。 那不是每个人都熟悉的“范式”。 我不知道任何以这种方式工作的语言。

看起来类似于 Swift,它也有“异常”,不像异常那样工作。

不同的语言表明 try catch 确实是二等解决方案,而我猜 Go 将无法像使用 Maybe monad 等那样解决这个问题。

@ianlancetaylor我只是在其他编程语言(如 C++、Java、C# 等)中提到了 Try-Catch,而不是我在这里的解决方案。 如果 GoLang 从第一天开始就使用 Try-Catch 会更好,这样我们就不需要处理这种错误处理方式(这实际上并不新鲜。如果需要,您可以使用任何其他编程语言编写相同的 GoLang 错误处理)像那样编码)但我建议的是一种与当前可以返回错误对象的库向后兼容的方法。

Java 异常是一列火车残骸,所以我必须在这里坚决反对@KamyarM。 仅仅因为熟悉某物,并不意味着它是一个好的选择。

我的意思是。

@KamyarM感谢您的澄清。 我们明确考虑并拒绝了例外情况。 错误并不例外; 它们的发生是出于各种完全正常的原因。 https://blog.golang.org/errors-are-values

例外与否,但它们确实解决了由于错误处理样板而导致的代码膨胀问题。 同样的问题使 Objective-C 瘫痪,它的工作方式与 Go 非常相似。 错误只是 NSError 类型的值,它们没有什么特别之处。 它在大量 ifs 和错误包装方面也有同样的问题。 这就是为什么 Swift 改变了一切。 他们最终混合了两种 - 它像异常一样工作,这意味着它终止执行并且您应该捕获异常。 但它不会展开堆栈并且像常规返回一样工作。 因此,反对在控制流中使用异常的技术论点在那里并不适用——这些“异常”与常规返回一样快。 它更像是一种语法糖。 但是 Swift 有一个独特的问题。 许多 Cocoa API 是异步的(回调和 GCD),并且与那种错误处理不兼容 - 没有像 await 这样的异常就毫无用处。 但几乎所有 Go 代码都是同步的,这些“异常”实际上可以工作。

@urandom
Java 中的异常也不错。 问题在于不知道如何使用它的糟糕程序员。

如果您的语言具有糟糕的功能,那么最终会有人使用该功能。 如果您的语言没有这样的功能,则可能性为 0%。 这是简单的数学。

@as我不同意你的看法,即 try-catch 是一个糟糕的功能。 这是非常有用的功能,让我们的生活更轻松,这就是我们在这里发表评论的原因,因此可能是 Google GoLang 团队添加了类似的功能。 我个人讨厌 GoLang 中的 if-elses 错误处理代码,我不太喜欢延迟恐慌恢复概念(它类似于 try-catch 但不像 Try-Catch-Finally 块那样有条理) . 它在代码中增加了太多的噪音,使代码在许多情况下不可读。

该语言中已经存在无需样板即可处理错误的功能。 添加更多功能来满足来自基于异常的语言的初学者似乎不是一个好主意。

那么谁来自 C/C++、Objective-C,我们在样板方面有同样的问题? 看到像 Go 这样的现代语言遇到完全相同的问题令人沮丧。 这就是为什么围绕错误作为价值的整个炒作感觉如此虚假和愚蠢的原因 - 这已经做了很多年了,几十年了。 感觉 Go 没有从那次经历中学到任何东西。 特别是看看 Swift/Rust,他们实际上正在努力寻找更好的方法。 Go 解决了现有的解决方案,如 Java/C# 解决了异常,但至少那些是更老的语言。

@KamyarM你有没有使用过面向铁路的编程? 梁?

恕我直言,如果你会使用这些,你就不会那么夸奖异常了。

@ShalokShalom不多。 但这不只是一个状态机吗? 在失败的情况下这样做,在成功的情况下呢? 好吧,我认为并非所有类型的错误都应该像异常一样处理。 当只需要用户输入验证时,可以简单地返回一个带有验证错误详细信息的布尔值。 异常应该仅限于 IO 或网络访问或错误的功能输入,并且在错误非常严重并且您想不惜一切代价停止愉快的执行路径的情况下。

有人说 Try-Catch 不好的原因之一是因为它的性能。 可能这是由于对每个可能发生异常的地方使用处理程序映射表引起的。 我在某处读到,即使异常也更快(没有异常发生时成本为零,但实际发生时成本更高)将其与 If Error 检查(无论是否有错误,它总是被检查)进行比较。 除此之外,我认为 Try-Catch 语法没有任何问题。 只有编译器实现它的方式使它不同,而不是它的语法。

来自 C/C++ 的人们非常称赞 Go 没有异常并且
做出明智的选择,抵制那些声称它是“现代的”和
感谢上帝关于可读的工作流程(尤其是在 C++ 之后)。

2018 年 4 月 17 日星期二 03:46,Antonenko Artem通知@github.com
写道:

那么谁来自 C/C++、Objective-C,我们有相同的
样板的确切问题? 看到现代语言令人沮丧
就像 Go 遇到完全相同的问题。 这就是为什么整个炒作
围绕错误,因为价值观感觉如此虚假和愚蠢 - 这已经完成了
多年来,几十年。 感觉 Go 没有从中学到任何东西
经验。 尤其是看着 Swift/Rust 谁实际上试图找到
更好的方法。 Go 解决了现有的解决方案,如 Java/C# 解决了
例外,但至少那些是更古老的语言。


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

@kirillx我从来没有说过我想要像 C++ 这样的例外。 请再次阅读我的评论。 这与错误处理更可怕的 C 有什么关系? 您不仅有大量的样板文件,而且还缺少 defer 和多个返回值,这迫使您使用指针参数返回值并使用 goto 来组织您的清理逻辑。 Go 使用相同的错误概念,但解决了延迟和多个返回值的一些问题。 但是样板文件仍然存在。 其他现代语言也不想要例外,但也不想因为 C 风格的冗长而适应它。 这就是为什么我们有这个提案并对这个问题如此感兴趣。

提倡例外的人应该阅读这篇文章: https :

Java/C++ 样式异常本质上不好的原因与特定实现的性能无关。 异常是不好的,因为它们是 BASIC 的“出错时跳转”,而跳转在它们可能生效的上下文中是不可见的。 异常将错误处理隐藏在您很容易忘记的地方。 Java 的检查异常应该解决这个问题,但实际上他们没有解决,因为人们只是捕获并处理异常或到处转储堆栈跟踪。

我大部分时间都在写 Java,我坚决不希望在 Go 中看到 Java 风格的异常,无论它们的性能有多高。

@lpar不是所有的 for 循环、while 循环、 if elses 、 switch case 、breaks 和 continue 之类的 GoTo 。 那么,编程语言还剩下什么?

虽然,for 和 if/else 不涉及执行流无形地跳转到其他地方,没有标记来表明它会这样做。

如果有人只是将错误传递给 GoLang 中的前一个调用者,而该调用者只是将错误返回给前一个调用者,依此类推(除了大量代码噪音),有什么不同? 我们需要查看和遍历多少代码才能看到谁来处理错误? try-catch 也是如此。

什么可以阻止程序员? 有时函数真的不需要处理错误。 我们只想将错误传递给 UI,以便用户或系统管理员可以解决它或找到解决方法。

如果一个函数不想处理异常,它就不会使用 try-catch 块,因此前一个调用者可以处理它。 我认为语法没有任何问题。 它也干净得多。 但是,性能及其在语言中的实现方式是不同的。

正如你在下面看到的,我们需要添加 4 行代码,只是为了不处理错误:

func myFunc1() error{
  // ...
  if (err){
      return err
  }
  return nil
}

如果您想将错误传递回调用者进行处理,那很好。 关键是在错误返回给您时,您正在这样做是可见的。

考虑:

x, err := lib.SomeFunc(100, 4)
if err != nil {
  // A
}
// B

看代码就知道调用函数时可能会出错。 您知道如果发生错误,代码流将在 A 点结束。您知道代码流将在 B 点结束的唯一其他地方。还有一个隐式约定,如果 err 为 nil,则 x 是某个有效值,零或其他。

与Java对比:

x = SomeFunc(100, 4)

从代码看,你不知道调用函数时是否会发生错误。 如果发生错误并表示为异常,则发生goto ,您可能会在周围代码底部的某个位置结束……或者如果未捕获到异常,您可能会结束在完全不同的代码段底部的某处。 或者你可能会在别人的代码中结束。 事实上,由于默认的异常处理程序可以被替换,你可能会根据其他人的代码所做的事情最终在任何地方结束。

此外,没有隐式约定 x 有效——函数返回 null 以指示错误或缺失值是很常见的。

对于 Java,每次调用都会出现这些问题 —— 不仅是糟糕的代码,您还必须为 _all_ Java 代码担心。 这就是为什么 Java 开发环境有弹出式帮助来显示您指向的函数是否可能导致异常,以及它可能导致哪些异常。 这就是 Java 添加检查异常的原因,因此对于常见错误,您至少必须有一些警告,即函数调用可能会引发异常并转移程序流程。 同时,返回的空值和NullPointerException的未经检查的性质是一个问题,他们将Optional类添加到 Java 8 以尝试改进它,即使成本是必须显式包装每个返回对象的函数的返回值。

我的经验是,来自我收到的意外空值的NullPointerException是我的 Java 代码最终崩溃的最常见方式,而且我通常会得到一个几乎完全无用的大回溯,并且未指明原因的错误消息,因为它是在远离故障代码的地方生成的。 在 Go 中,老实说,我没有发现 nil 取消引用恐慌是一个重大问题,尽管我对 Go 的经验要少得多。 对我来说,这表明 Java 应该向 Go 学习,而不是相反。

我认为语法没有任何问题。

我认为没有人说语法是 Java 风格异常的问题。

@lpar ,为什么 Go 中的 nil 取消引用恐慌比 Java 中的 NullPointerException 更好? “恐慌”和“投掷”有什么区别? 它们的语义有什么区别?

恐慌只是可以恢复而抛出是可以捕捉的? 对?

我只记得一个区别,恐慌你可以恐慌一个错误对象或一个字符串对象,或者可能是任何其他类型的对象(如果我错了,请纠正我)但是使用 throw 你可以抛出一个异常类型的对象或一个子类仅例外。

为什么 Go 中的 nil dereference panics 比 Java 中的 NullPointerException 更好?

因为根据我的经验,前者几乎从未发生过,而后者一直发生,原因我已经解释过了。

@lpar嗯,我最近没有用 Java 编程,我想这是一个新事物(过去 5 年),但 C# 有安全导航操作符来避免空引用来创建异常,但是 Go 有什么? 我不确定,但我想它没有任何东西可以处理这些情况。 因此,如果您想避免恐慌,您仍然需要将那些丑陋的嵌套 if-not-nil-else 语句添加到代码中。

您通常不需要检查返回值以查看它们在 Go 中是否为 nil,只要您检查错误返回值即可。 所以没有丑陋的嵌套 if 语句。

空取消引用是一个不好的例子。 如果你没有抓住它,Go 和 Java 的工作方式完全一样——你会因堆栈跟踪而崩溃。 堆栈跟踪怎么可能没用,我现在没有。 你知道它发生的确切地点。 在我的 C# 和 Go 中,修复这种崩溃通常是微不足道的,因为根据我的经验,空引用是由于一个简单的程序员错误造成的。 在这种特殊情况下,没有什么可以向任何人学习的。

@lpar

因为根据我的经验,前者几乎从未发生过,而后者一直发生,原因我已经解释过了。

这是偶然的,我在您的评论中没有看到任何理由表明 Java 在 nil/null 上比 Go 更糟糕。 我在 Go 代码中观察到许多 nil 取消引用崩溃。 它们与 C#/Java 中的空取消引用完全相同。 你可能碰巧在 Go 中使用了更多的值类型,这有帮助(C# 也有)但不会改变任何东西。

至于例外,我们来看看 Swift。 对于可能引发错误的函数,您有一个关键字throws 。 没有它的函数不能抛出。 在实现方面,它的工作原理类似于 return - 可能一些寄存器是为返回错误而保留的,每次抛出函数时,它都会正常返回但带有一个错误值。 这样意外错误的问题就解决了。 你确切地知道哪个函数可能抛出,你知道它可能发生的确切位置。 错误是值,不需要堆栈展开。 他们只是返回,直到你抓住它。

或者类似于 Rust 的东西,你有特殊的 Result 类型,它携带结果和错误。 错误可以在没有任何显式条件语句的情况下传播。 加上大量的模式匹配优势,但这可能不适用于 Go。

这两种语言都采用两种解决方案(C 和 Java)并将它们组合成更好的东西。 来自异常的错误传播 + 错误值和来自 C 的明显代码流 + 没有无用的丑陋样板代码。 所以我认为查看这些特定的实现是明智的,不要仅仅因为它们在某些方面类似于异常而完全拒绝它们。 在如此多的语言中使用异常是有原因的,因为它们确实有积极的一面。 否则语言会忽略它们。 尤其是在 C++ 之后。

堆栈跟踪怎么可能没用,我现在没有。

我说“几乎完全没用”。 就像,我只需要一行信息,但它有几十行长。

这是偶然的,我在您的评论中没有看到任何理由表明 Java 在 nil/null 上比 Go 更糟糕。

那你就不听了。 返回并阅读有关隐式合同的部分。

错误可以在没有任何显式条件语句的情况下传播。

这正是问题所在——错误传播和控制流发生变化,但没有任何明确的迹象表明它会发生。 显然你不认为这是一个问题,但其他人不同意。

我不知道 Rust 或 Swift 实现的异常是否遭受与 Java 相同的问题,我将把它留给有相关语言经验的人。

@KamyarM您基本上使 nil 变得多余并为其获得完整的类型安全性:

https://fsharpforfunandprofit.com/posts/the-option-type/

这正是问题所在——错误传播和控制流发生变化,但没有任何明确的迹象表明它会发生。

这对我来说是真实的。 如果我开发了一些使用另一个包的包,并且该包抛出异常,现在 _I_ 也必须意识到这一点,无论我是否想使用该功能。 这是提议的语言功能中一个不常见的方面; 大多数是程序员可以选择使用的东西,或者只是不使用。 例外,就其意图而言,跨越了各种边界,无论是预期的还是其他的。

我说“几乎完全没用”。 就像,我只需要一行信息,但它有几十行长。

带有数百个 goroutine 的巨大 Go 跟踪在某种程度上更有用吗? 我不明白你要去哪里。 Java 和 Go 在这里完全相同。 有时您确实会发现观察完整堆栈以了解您的代码如何最终崩溃是很有用的。 C# 和 Go 跟踪在这方面帮助了我多次。

那你就不听了。 返回并阅读有关隐式合同的部分。

我读了它,没有任何改变。 根据我的经验,这不是问题。 这就是两种语言的文档(例如net.ParseIP )。 如果您忘记检查您的值是否为 nil/null,那么您在两种语言中都会遇到完全相同的问题。 在大多数情况下,Go 会返回一个错误,而 C# 会抛出一个异常,所以你甚至不需要担心 nil。 好的 API 不只是返回 null 而不抛出异常或其他东西来告诉你出了什么问题。 在其他情况下,您明确检查它。 根据我的经验,最常见的 null 错误类型是当您有协议缓冲区时,其中每个字段都是一个指针/对象,或者您有内部逻辑,其中类/结构字段可能为零,具体取决于内部状态,而您之前忘记检查它使用权。 这对我来说是最常见的模式,Go 中没有任何东西可以显着缓解这个问题。 我可以说出两件有点帮助的事情 - 有用的空值和值类型。 但这更多是为了简化编程,因为您不需要在使用前构造每个变量。

这正是问题所在——错误传播和控制流发生变化,但没有任何明确的迹象表明它会发生。 显然你不认为这是一个问题,但其他人不同意。

这是一个问题,我从来没有说过别的,但是这里的人非常关注 Java/C#/C++ 异常,以至于他们忽略了任何与它们稍微相似的东西。 究竟为什么 Swift 要求您用throws标记函数,以便您可以准确地看到您应该从函数中得到什么以及控制流可能在哪里中断以及您在 Rust 中使用的内容? 使用各种辅助方法显式传播错误以提供更多上下文。 它们都使用相同的错误概念作为值,但将其包装在语法糖中以减少样板。

带有数百个 goroutine 的巨大 Go 跟踪在某种程度上更有用吗?

使用 Go,您可以通过将错误与检测到的位置一起记录下来来处理错误。 除非您选择添加一个,否则没有回溯。 我只有一次需要这样做。

根据我的经验,这不是问题。

嗯,我的经验不同,我认为大多数人的经验不同,作为证据,我引用了 Java 8 添加了 Optional 类型的事实。

这里的这个线程讨论了 Go 及其错误处理系统的许多优点和缺点,包括关于异常与否的讨论,我强烈建议阅读它:

https://elixirforum.com/t/discussing-go-split-thread/13006/2

我的错误处理 2 美分(对不起,如果上面提到了这样的想法)。

在大多数情况下,我们希望重新抛出错误。 这导致了这样的片段:

a, err := fn()
if err != nil {
    return err
}
use(a)
return nil

如果没有分配给变量(没有任何额外的语法),让我们自动重新抛出非 nil 错误。 上面的代码会变成:

a := fn()
use(a)

// or just

use(fn())

编译器将err保存到隐式(不可见)变量中,检查它是否存在nil并继续(如果 err == nil)或返回它(如果 err == nil)并在最后返回 nil如果在函数执行过程中没有错误发生,通常但自动和隐式地发生。

如果必须处理err必须将其分配给显式变量并使用:

a, err := fn()
if err != nil {
    doSomething(err)
} else {
    use(a)
}
return nil

可以通过以下方式抑制错误:

a, _ := fn()
use(a)

在返回多个错误的罕见(极好)情况下,显式错误处理将是强制性的(就像现在一样):

err1, err2 := fn2()
if err1 != nil || err2 != nil {
    return err1, err2
}
return nil, nil

这也是我的论点——我们希望在大多数情况下重新抛出错误,这通常是默认情况。 也许给它一些背景。 除了异常,上下文是由堆栈跟踪自动添加的。 对于 Go 中的错误,我们通过添加错误消息手动完成。 为什么不让它更简单。 这正是其他语言在平衡清晰度问题的同时试图做的事情。

所以我同意“如果没有分配给变量(没有任何额外的语法),让我们自动重新抛出非零错误”,但后一部分让我感到烦恼。 这就是异常问题的根源所在,我认为,为什么人们如此反对谈论与它们略有关联的任何事情。 它们无需任何额外语法即可更改控制流。 这是一件坏事。

例如,如果您查看 Swift,这段代码将无法编译

func a() throws {}
func b() throws {
  a()
}

a可能会引发错误,因此您必须编写try a()才能传播错误。 如果您从b删除throws ,那么即使使用try a()也不会编译。 您必须处理b内的错误。 这是一种更好的错误处理方式,既解决了异常控制流不明确的问题,也解决了 Objective-C 错误的冗长问题。 后者非常类似于 Go 中的错误以及 Swift 旨在替换的内容。 我不喜欢 Swift 也使用的try, catch东西。 我更愿意将错误作为返回值的一部分。

所以我建议实际上有额外的语法。 以便调用站点自己告诉它它是控制流可能发生变化的潜在位置。 我还要建议的是,不编写这个额外的语法会产生编译错误。 与 Go 现在的工作方式不同,这将迫使您处理错误。 您可以使用_类的东西来消除错误,因为在某些情况下,处理每个小错误都会非常令人沮丧。 就像, printf 。 我不在乎它是否无法记录某些内容。 Go 已经有了这些烦人的导入。 但这至少通过工具解决了。

我现在能想到的编译时错误有两种替代方法。 就像现在的 Go 一样,让错误被默默地忽略。 我不喜欢那样,这一直是我在 Go 错误处理方面的问题。 它不强制执行任何操作,默认行为是静默忽略错误。 这很糟糕,这不是您编写健壮且易于调试的程序的方式。 我在 Objective-C 中有太多案例,当我懒惰或超时并忽略错误只是为了在同一代码中遇到错误但没有任何关于它发生的原因的诊断信息。 至少记录它可以让我在许多情况下解决问题。

缺点是人们可能会开始忽略错误,可以说到处都是try, catch(...) 。 这是一种可能性,但与此同时,默认情况下会忽略错误,这样做会更容易。 我认为关于例外的争论在这里不适用。 除了例外,有些人试图实现的是他们的程序更稳定的错觉。 未处理的异常使程序崩溃的事实是这里的问题。

另一种选择是恐慌。 但这只是令人沮丧并带来了异常的记忆。 这肯定会导致人们进行“防御性”编码,以免他们的程序崩溃。 对我来说,现代语言应该在编译时做尽可能多的事情,并在运行时尽可能少做决定。 恐慌可能适合的地方是调用堆栈的顶部。 例如,不处理主函数中的错误会自动产生恐慌。 这也适用于 goroutines 吗? 应该不会吧

为什么要考虑妥协?

@nick-korsakov 最初的提案(本期)想为错误添加更多上下文:

忽略错误已经很容易(也许太容易了)(参见 #20803)。 许多现有的错误处理建议使返回未修改的错误更容易(例如,#16225、#18721、#21146、#21155)。 很少有人可以更轻松地返回带有附加信息的错误。

另请参阅此评论

此评论中,我建议为了在此讨论中取得进展(而不是在循环中运行),我们应该更好地定义目标,例如什么是仔细处理的错误消息。 整件事读起来很有趣,但它似乎受到了三秒金鱼记忆问题的影响(没有太多的专注/前进,重复漂亮的创造性语法更改和关于异常/恐慌等的争论)。

另一个自行车棚:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    try err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    try errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    try errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

try表示“如果不是空值则返回”。 在此,我假设errors.Errorf将在 err 为零时返回nil 。 我认为在坚持轻松包装的目标的同时,这与我们预期的节省一样多。

标准库中的扫描器类型将错误状态存储在一个结构中,该结构的方法可以在继续之前负责地检查错误的存在。

type Scanner struct{
    err error
}
func (s *Scanner) Scan() bool{
   if s.err != nil{
       return false
   }
   // scanning logic
}
func (s *Scanner) Err() error{ return s.err }

通过使用类型来存储错误状态,可以使使用这种类型的代码免于冗余错误检查。

它还不需要语言中的创造性和奇怪的语法更改或意外的控制转移。

我还必须建议像 try/catch 这样的东西,其中 err 在 try{} 中定义,如果 err 设置为非 nil 值 - 从 try{} 到 err 处理程序块(如果有的话)的流程中断。

内部没有例外,但整个事情应该更接近
在可以分配 err 的每一行之后执行if err != nil break检查的语法。
例如:

...
try(err) {
   err = doSomethig()
   err, value := doSomethingElse()
   doSomethingObliviousToErr()
   err = thirdErrorProneThing()
} 
catch(err SomeErrorType) {
   handleSomeSpecificErr(err)
}
catch(err Error) {
  panic(err)
}

我知道它看起来像 C++,但它在每一行之后也比手动if err != nil {...}众所周知和干净。

@作为

扫描仪类型之所以有效,是因为它正在完成所有工作,因此可以在此过程中跟踪自己的错误。 请不要自欺欺人地认为这是一个通用的解决方案。

@卡姆约翰逊

如果我们想要对简单错误进行一次线性处理,我们可以更改语法以允许 return 语句成为一行代码块的开始。
它将允许人们写:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    if err != nil return err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    if err != nil return errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    if err != nil return errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

我认为应该将规范更改为类似的内容(这可能很幼稚:))

Block = "{" StatementList "}" | "return" Expression .

如果 err 检查一行而不是三行,我认为特殊的大小写返回并不比仅仅更改 gofmt 以使其变得简单更好。

@urandom

错误合并超出一种可装箱类型及其操作不应该被鼓励。 对我来说,这表明在源自不同不相关操作的错误之间缺乏包装或添加错误上下文的努力。

扫描仪方法是我在整个“错误就是价值”的口头禅中读到的最糟糕的事情之一:

  1. 在几乎所有需要大量错误处理样板的用例中,它都是无用的。 调用多个外部包的函数不会从中受益。
  2. 这是一个复杂而陌生的概念。 引入它只会让未来的读者感到困惑,并使你的代码比它需要的更复杂,这样你就可以解决语言设计的缺陷。
  3. 它隐藏了逻辑,并试图通过从中取最坏的(复杂的控制流)而不获取任何好处来与异常相似。
  4. 在某些情况下,它会浪费计算资源。 每次调用都必须在很久以前发生的无用错误检查上浪费时间。
  5. 它隐藏了错误发生的确切位置。 想象一下您解析或序列化某些文件格式的情况。 您将有一系列读/写调用。 想象一下,第一个失败了。 您如何判断错误发生的确切位置? 它正在解析或序列化哪个字段? “IO 错误”、“超时”——这些错误在这种情况下是无用的。 您可以为每个读/写提供上下文(例如字段名称)。 但此时最好放弃整个方法,因为它对你不利。

在某些情况下,它会浪费计算资源。

基准? 究竟什么是“计算资源”?

它隐藏了错误发生的确切位置。

不,它不会,因为非零错误不会被覆盖

调用多个外部包的函数不会从中受益。
这是一个复杂而陌生的概念
扫描仪方法是我在整个“错误就是价值”的背景下读到的最糟糕的事情之一

我的印象是你不理解这种方法。 它在逻辑上等同于自包含类型中的常规错误检查,我建议您仔细研究该示例,因此它可能是您理解的最糟糕的事情,而不仅仅是您_阅读过的最糟糕的事情。

抱歉,我要把我自己的建议添加到堆栈中。 我已经通读了这里的大部分内容,虽然我喜欢其中的一些建议,但我觉得他们试图做的太多了。 问题是错误样板。 我的建议只是在语法级别消除该样板文件,并保留错误传递的方式。

提议

通过启用_!标记作为语法糖来减少错误样板,从而在分配非零error值时引起恐慌

val, err := something.MayError()
if err != nil {
    panic(err)
}

可以成为

val, _! := something.MayError()

if err := something.MayError(); err != nil {
    panic(err)
}

可以成为

_! = something.MayError()

当然,特定的符号有待讨论。 我还考虑了_^_*@等。 我选择_!作为事实上的建议,因为我认为它会是最熟悉的。

从语法上讲, _! (或选定的标记)将是error类型的符号,可在使用它的范围内使用。 它以nil开头,任何时候分配给它,都会执行nil检查。 如果它被设置为一个非零的error值,就会启动一个恐慌。 因为_! (或者再次选择的令牌)在 go 中不是语法上有效的标识符,所以名称冲突不会成为问题。 这个以太变量只会在使用它的范围内引入,类似于命名返回值。 如果需要语法上有效的标识符,也许可以使用占位符,在编译时将其重写为唯一名称。

理由

我在 Go 中看到的最常见的批评之一是错误处理的冗长。 API 边界处的错误并不是一件坏事。 但是,必须将错误传递到 API 边界可能会很痛苦,尤其是对于深度递归算法。 为了避免引入递归代码的冗长错误传播,可以使用恐慌。 我觉得这是一种非常常用的技术。 我已经在我自己的代码中使用过它,我也看到它在野外使用过,包括在 go 的解析器中。 有时,您已经在程序的其他地方进行了验证,并期望错误为零。 如果接收到非零错误,这将违反您的不变量。 当不变量被违反时,恐慌是可以接受的。 在复杂的初始化代码中,有时将错误转换为恐慌并恢复它们以在具有更多上下文知识的地方返回是有意义的。 在所有这些场景中,都有机会减少错误样板。

我意识到尽可能避免恐慌是 go 的哲学。 它们不是跨 API 边界传播错误的工具。 但是,它们是语言的一个特性,并且具有合法的用例,例如上面描述的那些。 恐慌是简化私有代码中错误传播的绝妙方法,语法的简化将大大有助于使代码更清晰,可以说,更清晰。 我觉得一目了然地识别_! (或@或 `_^ 等...)比“if-error-panic”形式更容易。 令牌可以显着减少必须编写/读取以传达/理解的代码量:

  1. 可能有错误
  2. 如果有错误,我们不期望它
  3. 如果有错误,它可能正在链上处理

与任何语法功能一样,存在滥用的可能性。 在这种情况下,go 社区已经有了一套处理恐慌的最佳实践。 由于此语法添加是恐慌的语法糖,因此可以将这组最佳实践应用于其使用。

除了简化可接受的恐慌用例之外,这也使快速原型设计变得更容易。 如果我有一个想法我想在代码中记下,并且只想在我玩弄时出现错误使程序崩溃,我可以使用这种语法添加而不是“if-error-panic”形式。 如果我能在开发的早期阶段用更少的行表达自己,我就可以更快地将我的想法变成代码。 一旦我对代码有了一个完整的想法,我就会回去重构我的代码以在适当的边界返回错误。 我不会在生产代码中留下免费的恐慌,但它们可以成为一个强大的开发工具。

恐慌只是另一个名字的例外,我喜欢 Go 的一件事是例外是例外。 我不想通过给它们语法糖来鼓励更多的例外。

@carlmjohnson必须满足以下

  1. 恐慌是具有合法用例的语言的一部分,或者
  2. 恐慌没有合法的用例,因此应该从语言中删除

我怀疑答案是 1。
我也不同意“恐慌只是另一个名字的例外”。 我认为这种挥手阻止了真正的讨论。 正如大多数其他语言中所见,恐慌和异常之间存在关键差异。

我理解下意识的“恐慌是不好的”反应,但个人对使用恐慌的感受并没有改变恐慌被使用的事实,而且实际上是有用的。 go 编译器使用恐慌来避免解析器和类型检查阶段(我最后一次查看)中的深度递归过程。
使用它们通过深度递归代码传播错误似乎不仅是一种可接受的用途,而且是 Go 开发人员认可的用途。

恐慌传达了一些特定的东西:

这里出了点这里没有准备好处理

代码中总会有一些地方是正确的。 尤其是在开发初期。 之前对 Go 进行了修改以改善重构体验:添加类型别名。 能够在恐慌中传播不需要的错误,直到您可以在更接近源的级别充实是否以及如何处理它们,可以使编写和逐步重构代码的繁琐程度大大降低。

我觉得这里的大多数提案都在提议对语言进行重大更改。 这是我能想到的最透明的方法。 它允许 go 中错误处理的整个当前认知模型保持完整,同时为特定但常见的情况启用语法简化。 目前的最佳实践要求“go 代码不应跨越 API 边界恐慌”。 如果我在包中有公共方法,如果出现问题,它们应该返回错误,除非在错误不可恢复的极少数情况下(例如,不变量违反)。 对语言的这一补充不会取代该最佳实践。 这只是一种减少内部代码样板,并使草图思路更加清晰的方法。 它确实使代码更易于线性阅读。

var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

除了语法和缺少对象层次结构(这是有道理的,因为 Go 没有继承)之外,Go 中的恐慌和 Java 或 Python 等中的异常之间确实没有根本区别。 它们的工作方式和使用方式是相同的。

当然,恐慌在语言中占有一席之地。 恐慌是为了处理那些应该只因程序员错误而发生的错误,否则这些错误是不可恢复的。 例如,如果您在整数上下文中除以零,则没有可能的返回值,而且您没有先检查零是您自己的错,因此它会发生恐慌。 类似地,如果您读取的切片越界,请尝试使用 nil 作为值等。这些事情是由程序员错误引起的——而不是由预期的情况引起的,比如网络关闭或文件权限错误——所以他们只是恐慌并炸毁堆栈。 Go 提供了一些类似于 template.Must 的辅助函数,因为预计这些函数将与硬编码字符串一起使用,其中任何错误都必须由程序员错误引起。 内存不足本身并不是程序员的错误,但它也是不可恢复的并且可能发生在任何地方,所以这不是错误而是恐慌。

人们有时也使用恐慌作为一种使堆栈短路的方式,但出于可读性和性能原因,这通常是不受欢迎的,而且我认为 Go 没有任何机会改变以鼓励其使用。

Go panics 和 Java 的 unchecked 异常几乎相同,它们存在的原因和处理相同的用例。 不要鼓励人们在其他情况下使用恐慌,因为这些情况与其他语言中的异常存在相同的问题。

人们有时也使用恐慌作为使堆栈短路的一种方式,但出于可读性和性能原因,通常不赞成这样做

首先,可读性问题是此语法更改直接解决的问题:

// clearly, linearly shows that these steps must occur in order,
// and any errors returned cause a panic, because this piece of
// code isn't responsible for reporting or handling possible failures:
// - IO Error: either network or disk read/write failed
// - External service error: some unexpected response from the external service
// - etc...
// It's not this code's responsibility to be aware of or handle those scenarios.
// That's perhaps the parent process's job.
var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

对比

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

暂时将可读性放在一边,给出的另一个原因是性能。
是的,使用 panic 和 defer 语句确实会导致性能损失,但在许多情况下,这种差异对于正在执行的操作来说可以忽略不计。 平均而言,磁盘和网络 IO 将比管理延迟/恐慌的任何潜在堆栈魔法花费更长的时间。

我在讨论恐慌时听到很多人重复这一点,我认为说恐慌是性能下降是不诚实的。 他们当然可以,但他们不一定是。 就像语言中的许多其他事物一样。 如果您在性能下降真的很重要的紧密循环中感到恐慌,那么您也不应该在该循环内推迟。 事实上,任何自己选择恐慌的函数通常都不应该捕捉到它自己的恐慌。 同样,今天编写的 go 函数不会同时返回错误恐慌。 这是不清楚的,愚蠢的,而不是最佳实践。 也许这就是我们可能习惯于在 Java、Python、Javascript 等中看到异常的方式,但这并不是 go 代码中通常使用的恐慌的方式,我不相信专门为传播错误的情况添加运算符通过恐慌向上调用堆栈将改变人们使用恐慌的方式。 无论如何,他们都在使用恐慌。 这个语法扩展的重点是承认开发人员使用恐慌的事实,它具有完全合法的用途,并减少围绕它的样板。

你能给我一些你认为这个语法特性会启用的有问题的代码的例子,这些代码目前不可能/违反最佳实践吗? 如果有人通过 panic/recover 向他们的代码用户传达错误,那么这目前是不受欢迎的,而且显然仍然会继续,即使添加了这样的语法。 如果可以,请回答以下问题:

  1. 您认为这样的语法扩展会引起哪些滥用?
  2. var1, err := trySomeTask1(); if err != nil { panic(err) }表达了var1, _! := trySomeTask1()没有表达的什么意思? 为什么?

在我看来,你的论点的关键是“恐慌是不好的,我们不应该使用它们”。
如果不共享,我将无法打开包装并讨论其背后的原因。

这些事情是由程序员错误引起的——而不是由预期的情况引起的,比如网络关闭或文件权限错误——所以他们只会恐慌并炸毁堆栈。

我和大多数地鼠一样,喜欢将错误作为值的想法。 我相信它有助于清楚地传达 API 的哪些部分可以保证结果,哪些部分可能会失败,而无需查看文档。

它允许收集错误和用更多信息增加错误。 这在 API 边界处非常重要,您的代码与用户代码在此处交叉。 但是,在这些 API 边界内,通常没有必要对您的错误执行所有这些操作。 尤其是如果您期待快乐路径,并且如果该路径失败,则有其他代码负责处理错误。

有时,处理错误不是您的代码的工作。
如果我正在编写一个库,我不在乎网络堆栈是否关闭 - 作为库开发人员,这超出了我的控制范围。 我会将这些错误返回给用户代码。

即使在我自己的代码中,有时我也会编写一段代码,其唯一的工作就是将错误返回给父函数。

例如,假设您有一个 http.HandlerFunc 从磁盘读取文件作为响应 - 这几乎总是有效的,如果失败,则可能是程序未正确编写(程序员错误)或文件系统有问题在程序的责任范围之外。 一旦 http.HandlerFunc 发生恐慌,它就会退出,一些基本处理程序将捕获该恐慌并向客户端写入 500。 如果在将来的任何时候我想以不同的方式处理该错误,我可以将_!替换err并使用错误值执行我想做的任何操作。 问题是,在程序的整个生命周期中,我可能不需要这样做。 如果我遇到这样的问题,处理程序不是负责处理该错误的代码部分。

我可以并且通常会在处理程序中写入if err != nil { panic(err) }if err != nil { return ..., err }以处理 IO 故障、网络故障等。当我确实需要检查错误时,我仍然可以这样做。 不过,大多数时候,我只是在写if err != nil { panic(err) }

或者,另一个例子,如果我递归搜索一个特里树(比如在 http 路由器实现中),我将声明一个函数func (root *Node) Find(path string) (found Value, err error) 。 该函数将推迟一个函数来恢复树下产生的任何恐慌。 如果程序正在创建格式错误的尝试怎么办? 如果由于程序没有以具有正确权限的用户身份运行而导致某些 IO 失败怎么办? 这些问题不是我的特里搜索算法的问题 - 除非我稍后明确提出 - 但它们可能是我可能遇到的错误。 将它们一直返回到堆栈会导致很多额外的冗长,包括在堆栈中保存理想情况下的几个 nil 错误值。 相反,我可以选择在该公共 API 函数之前恐慌一个错误并将其返回给用户。 目前,这仍然会导致额外的冗长,但不是必须的。

其他提案正在讨论如何将一个返回值视为特殊值。 这基本上是相同的想法,但他们没有使用语言中已经内置的功能,而是希望在某些情况下修改语言的行为。 在易于实施方面,这种类型的建议(已支持的东西的语法糖)将是最简单的。

编辑添加:
我并不认同我提出的建议,但我确实认为从新的角度看待错误处理问题很重要。 没有人提出任何新颖的建议,我想看看我们是否可以重新定义我们对这个问题的理解。 问题是有太多地方在不需要时显式处理错误,开发人员希望有一种方法可以在没有额外样板代码的情况下将它们传播到堆栈上。 事实证明,Go 已经具有该功能,但没有很好的语法。 这是关于用更简洁的语法包装现有功能的讨论,以便在不改变行为的情况下使语言更符合人体工程学。 如果我们能做到,那不是胜利吗?

@mccolljr谢谢,但该提案的目标之一是鼓励人们开发新方法来处理所有三种错误处理情况:忽略错误、返回未修改的错误、返回带有附加上下文信息的错误。 你的恐慌提议没有解决第三种情况。 这是一个重要的。

@mccolljr我认为 API 边界比你想象的要普遍得多。 我不认为 API 内调用是常见情况。 如果有的话,它可能是相反的(这里的一些数据会很有趣)。 所以我不确定为 API 内调用开发特殊语法是正确的方向。 此外,在 API 中使用return ed 错误而不是panic ed 错误通常是一个不错的方法(特别是如果我们针对这个问题提出了一个计划)。 panic ed 错误有其用途,但它们明显更好的情况似乎很少见。

我不相信专门为通过恐慌将错误传播到调用堆栈的情况添加运算符会改变人们使用恐慌的方式。

我认为你错了。 人们会使用您的速记运算符,因为它非常方便,然后他们最终会比以前更多地使用恐慌。

恐慌是有时有用还是很少有用,以及它们是否在 API 边界内或在 API 边界内有用,都是红鲱鱼。 人们可能会对错误采取很多行动。 我们正在寻找一种方法来缩短错误处理代码,同时又不让一个操作优先于其他操作。

但在许多情况下,这种差异对于正在执行的操作可以忽略不计

虽然是真的,但我认为这是一条危险的道路。 一开始看起来可以忽略不计,它会堆积起来,最终在已经很晚的时候造成瓶颈。 我认为我们应该从一开始就牢记性能并尝试找到更好的解决方案。 已经提到 Swift 和 Rust 确实有错误传播,但将其实现为,基本上,包装在语法糖中的简单返回。 是的,重用现有的解决方案很容易,但我更愿意让一切保持原样,而不是简化和鼓励人们使用隐藏在不熟悉的语法糖后面的恐​​慌,试图隐藏它基本上是例外的事实。

一开始看起来可以忽略不计,它会堆积起来,最终在已经很晚的时候造成瓶颈。

不用了,谢谢。 想象中的性能瓶颈是几何上可以忽略的性能瓶颈。

不用了,谢谢。 想象中的性能瓶颈是几何上可以忽略的性能瓶颈。

请把你的个人感受放在这个话题之外。 你显然对我有一些问题,不想带来任何有用的东西,所以请忽略我的评论,并像之前对几乎所有评论一样投反对票。 无需继续发布这些无意义的答案。

我对你没有意见,你只是在声称没有任何数据支持的性能瓶颈,我用文字和拇指指出了这一点。

伙计们,请保持谈话的尊重和切题。 这个问题是关于处理 Go 错误的。

https://golang.org/conduct

我想再次重新审视“返回错误/附加上下文”部分,因为我认为忽略错误已经被现有的_所覆盖。

我提出了一个两个词的关键字,后面可以跟一个字符串(可选)。 它是一个双词关键字的原因是双重的。 首先,与本质上神秘的运算符不同,无需太多先验知识就更容易掌握它的作用。 我选择了“或气泡”,因为我希望没有指定错误的or一词会向用户表明该错误正在此处处理,如果它不是零的话。 一些用户已经将or与处理来自其他语言(perl、python)的虚假值联系起来,阅读data := Foo() or ...可能会潜意识地告诉他们data是不可用的,如果or到达语句的bubble关键字虽然相对较短,但可能向用户表示某些事情正在上升(堆栈)。 up这个词也可能是合适的,但我不确定整个or up是否足够好理解。 最后,整个事情是一个关键字,首先是因为它更具可读性,其次是因为该行为不能由函数本身编写(您可能可以调用 panic 来逃避您所在的函数,但随后您就不能了不要阻止自己,其他人将不得不恢复)。

以下仅用于错误传播,因此只能用于返回错误和任何其他返回参数的零值的函数中:

返回错误而不以任何方式修改它:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble

    return data;
}

返回带有附加消息的错误:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble fmt.Sprintf("reading file %s", path)

    modified := modifyData(data) or bubble "modifying the data"

    return data;
}

最后,引入一个全局适配器机制,用于自定义错误修改:

// Default Bubble Processor
errors.BubbleProcessor(func(msg string, err error) error {
    return fmt.Errorf("%s: %v", msg, err)
})

// Some program might register the following:
errors.BubbleProcessor(func(msg string, err error) error {
    return errors.WithMessage(err, msg)
})

最后,对于少数需要真正复杂处理的地方,已经存在的冗长方式已经是最好的方式。

有趣的。 拥有全局气泡处理程序为想要堆栈跟踪的人提供了一个放置跟踪调用的地方,这是该方法的一个很好的好处。 OTOH,如果它具有签名func(string, error) error ,那么这意味着必须使用内置错误类型而不是任何其他类型来完成冒泡,例如实现error的具体类型。

此外, or bubble存在表明or dieor panic的可能性。 我不确定这是功能还是错误。

与本质上神秘的运算符不同,无需太多先验知识就更容易掌握它的作用

当您第一次遇到它时,这可能很好。 但是一遍又一遍地阅读和编写它 - 它似乎太冗长并且需要太多空间来传达一个非常简单的事情 - 未处理的错误会导致堆栈冒泡。 运算符起初很神秘,但它们简洁且与所有其他代码形成鲜明对比。 他们清楚地将主要逻辑与错误处理分开,因为它实际上是一个分隔符。 在我看来,一行中有这么多单词会损害可读性。 至少将它们合并为orbubble或删除其中之一。 我认为在那里有两个关键字没有意义。 它把 Go 变成了一种口语,我们知道这是怎么回事(例如 VB)

我不太喜欢全局适配器。 如果我的包设置了自定义处理器,而您的包也设置了自定义处理器,谁会赢?

@object88
我认为它类似于默认记录器。 您只设置一次输出(在您的程序中),这会影响您使用的所有包。

错误处理与日志记录非常不同; 一个描述程序的信息输出,另一个管理程序的流程。 如果我将适配器设置为在我的包中做一件事,我需要正确管理逻辑流,而另一个包或程序改变了这一点,那么你就处于一个糟糕的地方。

请带回 Try Catch 最后,我们不再需要打架了。 它让每个人都开心。 从其他编程语言中借用功能和语法并没有错。 Java 做到了,C# 也做到了,它们都是非常成功的编程语言。 GO 社区(或作者)请在需要时接受更改。

@KamyarM ,我恭敬地不同意; try/catch _not_ 让每个人都开心。 即使您想在代码中实现它,抛出的异常也意味着使用您代码的每个人都需要处理异常。 这不是可以本地化到您的代码的语言更改。

@object88
实际上,在我看来,气泡处理器描述了程序的信息性错误输出,使其与记录器没有什么不同。 而且我想您希望在整个应用程序中都有一个单一的错误表示,并且不会因包而异。

虽然也许你可以提供一个简短的例子,但也许我遗漏了一些东西。

非常感谢您的赞许。 这正是我正在谈论的问题。 GO 社区对变化不开放,我觉得它并且我真的不喜欢那样。

这可能与这种情况无关,但我正在寻找另一天等效于 C++ 的三元运算符的 Go 并且我遇到了这种替代方法:

v := map[bool]int{true: first_expression, false: second_expression} [条件]
而不是简单地
v= 条件 ? 第一个表达式:第二个表达式;

你们更喜欢这两种形式中的哪一种? 上面不可读的代码 (Go My Way) 可能有很多性能问题或 C++ (Highway) 中的第二个简单语法? 我更喜欢高速公路的家伙。 我不知道你。

所以总而言之,请带来新的语法,从其他编程语言中借用它们。 没有什么不妥。

最好的祝福,

GO 社区对变化不开放,我觉得它并且我真的不喜欢那样。

我认为这错误地描述了你所经历的潜在态度。 是的,当有人提出 try/catch 或 ?: 时,社区会产生很多阻力。 但原因并不是我们抵制新想法。 我们几乎都有使用具有这些功能的语言的经验。 我们对它们非常熟悉,我们中的某些人多年来每天都在使用它们。 我们的抵制是基于这样一个事实,即这些是_旧想法_,而不是新想法。 我们已经接受了一个改变:改变了 try/catch 和改变了使用 ?:。 我们抵制的是将_back_更改为使用我们已经使用过但不喜欢的这些东西。

实际上,在我看来,气泡处理器描述了程序的信息性错误输出,使其与记录器没有什么不同。 而且我想您希望在整个应用程序中都有一个单一的错误表示,并且不会因包而异。

如果有人想使用冒泡来传递堆栈跟踪,然后使用它来做出决定,该怎么办? 例如,如果错误源自文件操作,则失败,但如果错误源自网络,则等待并重试。 我可以看到为此构建一些逻辑到错误处理程序中,但如果每个运行时只有一个错误处理程序,那将是冲突的一个秘诀。

@urandom ,也许这是一个微不足道的例子,但假设我的适配器返回另一个实现error ,我希望在我的代码中的其他地方使用它。 如果另一个适配器出现并替换了我的适配器,那么我的代码将停止正常工作。

@KamyarM语言和它的习语是

Try-catch-finally 将是一种非常具有侵入性的更改:它将从根本上改变 Go 程序的结构方式。 相比之下,您在此处看到的大多数其他建议对于每个函数都是本地的:错误仍然是显式返回的值,控制流避免非本地跳转等。

使用您的三元运算符示例:是的,您今天可以使用地图伪造一个,但我希望您实际上不会在生产代码中找到它。 它不遵循习语。 相反,您通常会看到类似以下内容的内容:

    var v int
    if condition {
        v = first_expression
    } else {
        v = second_expression
    }

并不是我们不想借用语法,而是我们必须考虑它如何与语言的其余部分和今天已经存在的代码的其余部分相适应。

@KamyarM我同时使用 Go 和 Java,我强调_不_希望 Go 从 Java 复制异常处理。 如果您需要 Java,请使用 Java。 并且请将三元运算符的讨论带到适当的问题,例如#23248。

@lpar因此,如果我在一家公司工作并且出于某种未知原因他们选择 GoLang 作为他们的编程语言,我只需要辞掉工作并申请 Java!? 来人!

@bcmills您可以计算您在那里建议的代码。 我认为那是 6 行代码而不是 1 行,可能你会得到几个代码圈复杂度点(你们使用 Linter。对吗?)。

@carlmjohnson@bcmills任何古老而成熟的语法并不意味着它不好。 实际上,我认为 if else 语法比三元运算符语法要古老得多。

很好,你带来了这个 GO 成语的东西。 我认为这只是这种语言的问题之一。 每当有人要求更改时,都会说哦,不,这违反了 Go 的习惯用法。 我认为这只是抵制变化和阻止任何新想法的借口。

@KamyarM请保持礼貌。 如果你想阅读更多关于保持语言小规模的想法,我推荐https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html。

此外,与最近讨论的 try/catch 无关的一般性评论。

在这个线程中有很多提议。 就我自己而言,我仍然不觉得我对要解决的问题有很强的把握。 我很想听到更多关于他们的信息。

如果有人想承担维护已讨论过的问题的有组织和汇总的列表的不令人羡慕但重要的任务,我也会感到兴奋。

@josharian我只是在那里坦率地说。 我想展示语言或社区中的确切问题。 认为这是更多的批评。 GoLang 容易受到批评,对吗?

@KamyarM如果你为一家选择 Rust 作为其编程语言的公司工作,你会去 Rust Github 并开始要求垃圾收集内存管理和 C++ 风格的指针,这样你就不必处理借用检查器了吗?

Go 程序员不想要 Java 风格的异常的原因与不熟悉它们无关。 我第一次遇到异常是在 1988 年通过 Lisp,我相信这个帖子中还有其他人更早遇到它们——这个想法可以追溯到 1970 年代初期。

三元表达式更是如此。 阅读 Go 的历史——Go 的创造者之一 Ken Thompson 于 1969 年在贝尔实验室在 B 语言(C 的前身)中实现了三元运算符。我认为可以肯定地说,他在考虑时意识到了它的好处和缺陷是否将其包含在 Go 中。

Go 容易受到批评,但我们要求 Go 论坛中的讨论要有礼貌。 坦率不等于不礼貌。 请参阅https://golang.org/conduct的“Gopher Values”部分

@lpar是的,如果 Rust 有这样一个论坛,我会那样做 ;-) 说真的,我会那样做。 因为我想让我的声音被听到。

@ianlancetaylor我是否使用了任何粗俗的词语或语言? 我是否使用了歧视性语言或任何对某人的欺凌或任何不受欢迎的性挑逗? 我不这么认为。
来吧,我们只是在这里谈论 Go 编程语言。 这与宗教或政治或类似的事情无关。
我是坦率的。 我想让我的声音被听到。 我想这就是为什么有这个论坛。 为了能听到声音。 你可能不喜欢我的建议或我的批评。 没关系。 但我想你需要让我谈谈和讨论,否则我们都可以得出结论,一切都是完美的,没有问题,所以不需要进一步讨论。

@josharian谢谢你的文章,我会看看。

好吧,我回头看了看我的评论,看看那里是否有什么不好的地方。 我可能侮辱的唯一一件事(顺便说一句,我仍然称之为批评)是 GoLang 编程语言习语! 哈哈!

回到我们的话题,如果你听到我的声音,请 Go 作者考虑带回 Try catch 块。 让程序员决定是否在正确的地方使用它(你已经有了类似的东西,我的意思是恐慌延迟恢复,那么为什么不试试程序员更熟悉的 Try Catch 呢?)。
我为当前的 Go 错误处理建议了一种解决方法,以实现向后兼容性。 我并不是说这是最好的选择,但我认为这是可行的。

我将不再讨论这个话题。

谢谢你给我的机会。

@KamyarM您混淆了我们的要求,即您对我们不同意您的论点保持礼貌。 当人们不同意你的观点时,你会以个人的方式回应,比如“非常感谢你的反对。这正是我正在谈论的问题。GO 社区不会接受改变,我觉得这一点,我真的不同意”不喜欢那样。”

再说一遍:请客气点。 坚持技术论点。 避免攻击人而不是思想的人身攻击。 如果你真的不明白我的意思,我愿意线下讨论; 给我发邮件。 谢谢。

我会把我的 2c 扔进去,希望它不会在其他 N 百条评论中重复某些内容(或踩到对 urandom 提案的讨论)。

我喜欢发布的原始想法,但有两个主要调整:

  • 句法自行车棚:我坚信任何具有隐式控制流的东西都应该是自己的操作符,而不是现有操作符的过载。 我会把?!放在那里,但我很高兴任何不容易与 Go 中现有的运算符混淆的东西。

  • 这个运算符的 RHS 应该是一个函数,而不是一个带有任意注入值的表达式。 这将使开发人员编写非常简洁的错误处理代码,同时仍然清楚他们的意图,并且可以灵活地做他们可以做的事情,例如

func returnErrorf(s string, args ...interface{}) func(error) error {
  return func(err error) error {
    return errors.New(fmt.Sprintf(s, args...) + ": " + err.Error())
  }
}

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) ?! returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() ?! returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() ?! func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

如果没有发生错误,则永远不应评估 RHS,因此此代码不会在快乐路径上分配任何闭包或任何内容。

“重载”此模式以在更有趣的情况下工作也非常简单。 我想到了三个例子。

首先,如果 RHS 是func(error) (error, bool) ,我们可以让return是有条件的,就像这样(如果我们允许这样做,我认为我们应该使用与无条件返回不同的运算符。我会使用?? ,但我的“我不在乎,只要它是不同的”声明仍然适用):

func maybeReturnError(err error) (error, bool) {
  if err == io.EOF {
    return nil, false
  }
  return err, true
}

func id(err error) error { return err }

func ignoreError(err error) (error, bool) { return nil, false }

func foo(n int) error {
  // Does nothing
  id(io.EOF) ?? ignoreError
  // Still does nothing
  id(io.EOF) ?? maybeReturnError
  // Returns the given error
  id(errors.New("oh no")) ?? maybeReturnError
  return nil
}

或者,我们可以接受返回类型与外部函数匹配的 RHS 函数,如下所示:

func foo(r io.Reader) ([]int, error) {
  returnError := func(err error) ([]int, error) { return []int{0}, err }
  // returns `[]int{0}, err` on a Read failure
  n := r.Read(make([]byte, 4)) ?! returnError
  return []int{n}, nil
}

最后,如果我们真的想要,我们可以通过更改参数类型将其概括为不仅可以处理错误:

func returnOpFailed(name string) func(bool) error {
  return func(_ bool) error {
    return errors.New(name + " failed")
  }
}

func returnErrOpFailed(name string) func(error) error {
  return func(err error) error {
    return errors.New(name + " failed: " + err.Error())
  }
}

func foo(c chan int, readInt func() (int, error), d map[int]string) (string, error) {
  n := <-c ?! returnOpFailed("receiving from channel")
  m := readInt() ?! returnErrOpFailed("reading an int")
  result := d[n + m] ?! returnOpFailed("looking up the number")
  return result, nil
}

...当我必须做一些可怕的事情时,我个人会觉得这非常有用,比如手动解码map[string]interface{}

需要明确的是,我主要将扩展作为示例展示。 我不确定它们中的哪一个(如果有的话)在简单性、清晰性和通用性之间取得了良好的平衡。

我想再次重温“返回错误/附加上下文”部分,因为我假设忽略错误已经被现有的 _ 覆盖了。

我提出了一个两个词的关键字,后面可以跟一个字符串(可选)。

@urandom你的提案的第一部分是令人满意的,你总是可以BubbleProcessor进行第二次修订。 @object88提出的问题是有效的 IMO; 我最近看到了诸如“你不应该覆盖http的默认客户端/传输”之类的建议,这将成为其中的另一个。

在这个线程中有很多提议。 就我自己而言,我仍然不觉得我对要解决的问题有很强的把握。 我很想听到更多关于他们的信息。

如果有人想承担维护已讨论过的问题的有组织和汇总的列表的不令人羡慕但重要的任务,我也会感到兴奋。

如果@ianlancetaylor任命你,那可能是你@josharian ? :blush: 我不知道其他问题是如何计划/讨论的,但也许这个讨论只是被用作“建议箱”?

@KamyarM

@bcmills您可以计算您在那里建议的代码。 我认为那是 6 行代码而不是 1 行,可能你会得到几个代码圈复杂度点(你们使用 Linter。对吗?)。

隐藏圈复杂度使其更难被看到,但它并没有将其删除(还记得strlen吗?)。 正如使错误处理“缩短”使错误处理语义更容易被忽略一样——但更难看到。

源中重新路由流控制的任何语句或表达式都应该是明显和简洁的,但如果要在明显或简洁之间做出决定,在这种情况下应该首选明显的。

很好,你带来了这个 GO 成语的东西。 我认为这只是这种语言的问题之一。 每当有人要求更改时,都会说哦,不,这违反了 Go 的习惯用法。 我认为这只是抵制变化和阻止任何新想法的借口。

新的和有益的是有区别的。 你相信因为你有一个想法,它的存在就值得认可吗? 作为练习,请查看问题跟踪器并尝试想象今天的 Go,如果每个想法都被批准而不管社区的想法如何。

也许你相信你的想法比其他人更好。 这就是讨论的切入点。与其将对话退化为谈论整个系统如何因习语而被破坏,不如直接、逐点解决批评,或在您和同行之间找到中间立场。

@gdm85
我添加了处理器以对返回的错误部分进行某种自定义。 虽然我确实相信它有点像使用默认记录器,因为您可以在大部分时间使用它,但我确实说过我愿意接受建议。 作为记录,我不相信默认记录器和默认 http 客户端甚至远程在同一类别中。

我也喜欢@gburgessiv的提议,尽管我不是神秘运算符本身的忠实粉丝(也许至少像 Rust 一样选择? ,尽管我仍然认为这很神秘)。 这看起来是否更具可读性:

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) or returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() or returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() or func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

并且希望他的提议也将包含一个类似于他的 returnErrorf 函数的默认实现,位于errors包中的某处。 也许errors.Returnf()

@KamyarM
您已经在这里表达了您的意见,并且没有得到任何对异常原因表示同情的评论或反应。 除了扰乱其他讨论之外,我看不出重复同样的事情会取得什么成果,tbh。 如果那是你的目标,那就不酷了。

@josharian ,我将尝试简要总结讨论。 这将是有偏见的,因为我有一个提案,并且不完整,因为我无法重读整个线程。

我们试图解决的问题是由 Go 错误处理引起的视觉混乱。 这是一个很好的例子(来源):

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    return dirs, nil
}

该线程中的一些评论者认为这不需要修复; 他们很高兴错误处理是侵入性的,因为处理错误与处理非错误情况一样重要。 对他们来说,这里的任何提议都不值得。

尝试简化这样的代码的提案分为几组。

有些人提出了一种异常处理的形式。 鉴于 Go 一开始可以选择异常处理并选择不这样做,这些似乎不太可能被接受。

这里的许多提议选择了一个默认动作,比如从函数返回(原始提议)或恐慌,并建议一些语法,使该动作易于表达。 在我看来,所有这些建议都失败了,因为它们以牺牲其他行动为代价来优先采取一项行动。 我经常使用 return、 t.Fatallog.Fatal来处理错误,有时都是在同一天。

其他提议没有提供增加或包装原始错误的方法,或者使包装比不包装更难。 这些也是不够的,因为包装是为错误添加上下文的唯一方法,如果我们让它太容易跳过,它会比现在更频繁地执行。

其余的大多数提案都添加了一些糖,有时还添加了一些魔法来简化事情,而不限制可能的操作或包装的能力。 @bcmills建议添加了最少量的糖和零魔法,以稍微提高可读性,并防止出现令人讨厌的错误

其他一些提议添加了某种受约束的非本地控制流,例如函数开头或结尾的错误处理部分。

最后但并非最不重要的一点是, @mpvl认识到在出现恐慌时错误处理会变得非常棘手。 他建议对 Go 错误处理进行更彻底的改变,以提高正确性和可读性。 他有一个令人信服的论点,但最终我认为他的案件不需要大刀阔斧的改变,可以通过现有机制处理。

向这里没有代表的想法的任何人道歉。

我有一种感觉,有人会问我糖和魔法的区别。 (这是我自己问的。)

Sugar 是一种语法,可以在不从根本上改变语言规则的情况下缩短代码。 短赋值运算符:=是糖。 C 的三元运算符?:

魔术是对语言的更猛烈的破坏,比如在不声明的情况下将变量引入作用域,或者执行非本地控制转移。

这条线肯定是模糊的。

谢谢你这样做,@jba。 很有帮助。 简单说一下重点,目前发现的问题是:

Go 错误处理导致的视觉混乱

在出现恐慌的情况下,错误处理会变得非常棘手

如果@jba和我遗漏了任何其他根本不同的问题(不是解决方案),请(任何人)加入。 FWIW,我会考虑人体工程学、代码混乱、圈复杂度、口吃、样板等作为“视觉混乱”问题(或问题集群)的变体。

@josharian您想将范围界定问题 (https://github.com/golang/go/issues/21161#issuecomment-319277657) 视为“视觉混乱”问题的一个变体,还是一个单独的问题?

@bcmills对我来说似乎很不同,因为它是关于微妙的正确性问题,而不是美学/人体工程学(或至多涉及代码批量的正确性问题)。 谢谢! 想要编辑我的评论并添加它的单行概要吗?

我有一种感觉,有人会问我糖和魔法的区别。 (这是我自己问的。)

我使用魔术的这个定义:查看一些源代码,如果您能通过以下算法的某些变体弄清楚它应该做什么:

  1. 查找该行或函数中存在的所有标识符、关键字和语法结构。
  2. 对于语法结构和关键字,请参阅官方语言文档。
  3. 对于标识符,应该有一个明确的机制来使用您正在查看的代码中的信息来定位它们,使用代码当前所在的范围,如语言定义的那样,您可以从中获取标识符的定义,这将在运行时准确。

如果这个算法 _reliably_ 正确理解代码将要做什么,它并不神奇。 如果没有,那么其中就有一定的魔力。 当您尝试遵循文档引用和标识符定义到其他标识符定义时,您必须如何递归地应用它会影响所讨论的构造/代码的_复杂性_,但不影响_神奇性_。

魔法的例子包括: 没有明确的路径回到它们的起源的标识符,因为你在没有命名空间的情况下导入了它们(Go 中的点导入,尤其是当你有多个时)。 语言可能具有的非本地定义某些运算符将解析为的任何能力,例如在动态语言中,代码可以非本地完全重新定义函数引用,或重新定义语言对不存在的标识符的作用。 由在运行时从数据库加载的模式构造的对象,因此在代码时人们或多或少盲目地希望它们会在那里。

这样做的好处是它几乎排除了所有的主观性。

回到手头的话题,似乎已经提出了很多建议,而其他人用另一个建议解决这个问题的可能性让每个人都感到“是的!就是这样!” 接近零。

在我看来,对话可能应该朝着对这里提出的提案的各个方面进行分类的方向发展,并了解优先顺序。 我特别希望看到这一点,以揭示这里人们模糊地应用的相互矛盾的要求。

例如,我看到一些关于在控制流中添加额外跳转的抱怨。 但就我自己而言,按照最初提案的说法,我认为不必在函数中添加八次|| &PathError{"chdir", dir, err}如果它们很常见。 (我知道 Go 不像其他一些语言那样对重复代码过敏,但是,重复代码仍然有很高的发散错误风险。)但几乎根据定义,如果有一种机制来排除这种错误处理,代码不能从上到下,从左到右,没有跳转。 一般认为哪个更重要? 我怀疑仔细检查人们对代码隐含的要求会发现其他相互矛盾的要求。

但总的来说,我觉得如果社区能够在所有这些分析之后就需求达成一致,那么正确的解决方案很可能会很明显地从它们中消失,或者至少,正确的解决方案集将受到如此明显的限制,以至于问题变得容易处理。

(我还要指出,由于这是一个提案,当前的行为通常应该与新提案进行相同的分析。目标是显着改进,而不是完美;拒绝两三个显着改进,因为它们都不是完美是通往瘫痪的道路。无论如何,所有提案都是向后兼容的,所以在那些无论如何当前方法已经是最好的情况下(恕我直言,每个错误都以不同的方式合法处理的情况,根据我的经验,这种情况很少发生,但会发生) ,当前方法仍然可用。)

自从我第二次在函数中编写 if err !=nil 以来,我一直在思考这个问题,让我觉得一个相当简单的解决方案是允许条件返回看起来像三元的第一部分,理解为 if条件失败我们不返回。

我不确定这在解析/编译方面的效果如何,但似乎应该很容易解释为 if 语句,其中 '?' 可以在不破坏兼容性的情况下看到它,所以我想我会把它作为一个选择扔在那里。

此外,除了错误处理之外,还有其他用途。

所以你可能会做这样的事情:

func example1() error {
    err := doSomething()
    return err != nil ? err
    //more code
}

func example2() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, err
    //more code
}

当我们有一些清理代码时,我们也可以做这样的事情,假设 handleErr 返回一个错误:

func example3() error {
    err := doSomething()
    return err !=nil ? handleErr(err)
    //more code
}

func example4() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, handleErr(err)
    //more code
}

也许接下来,如果您愿意,您可以将其减少为一个班轮:

func example5() error {
    return err := doSomething(); err !=nil ? handleErr(err)
    //more code
}

func example6() (*Mything, error) {
    return err := doSomething(); err !=nil ? nil, handleErr(err)
    //more code
}

之前来自@jba 的fetch 示例可能如下所示:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return err := createFolderIfNotExist(to); err != nil ? nil, err
    return err := clearFolder(to); err != nil ? nil, err
    return err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

会对对此建议的反应感兴趣,也许在节省样板方面不是一个巨大的胜利,但保持非常明确,希望只需要一个小的向后兼容的更改(可能在这方面有一些非常不准确的假设)。

您也许可以通过单独的回报将其分开? 关键字可能会增加清晰度并使生活更简单,而不必担心与 return 的兼容性(考虑所有工具),然后这些可以在内部重写为 if/return 语句,给我们这样的:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return? err := createFolderIfNotExist(to); err != nil ? nil, err
    return? err := clearFolder(to); err != nil ? nil, err
    return? err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

之间似乎没有太大区别

return err != nil ? err

 if err != nil { return err }

此外,有时您可能想要做一些除了 return 之外的事情,比如 call paniclog.Fatal

自从我上周提交提案以来,我一直在讨论这个问题,我得出的结论是我同意@thejerf :我们一直在讨论一个又一个提案,而没有真正退后一步并检查我们喜欢什么关于每个,不喜欢每个,以及“正确”解决方案的优先级。

最常见的要求是在一天结束时,Go 需要能够处理 4 种错误处理情况:

  1. 忽略错误
  2. 返回未修改的错误
  3. 返回添加了上下文的错误
  4. 恐慌(或杀死程序)

提案似乎属于以下三类之一:

  1. 恢复到try-catch-finally错误处理风格。
  2. 添加新的语法/内置函数来处理上面列出的所有 4 种情况
  3. 断言 go 可以很好地处理某些情况,并提出语法/内置函数来帮助处理其他情况。

对给定提案的批评似乎分为对代码可读性、非明显跳转、向作用域隐式添加变量和简洁性的关注。 就我个人而言,我认为在对提案的批评中,有很多个人意见。 我并不是说这是一件坏事,但在我看来,并没有真正客观的标准来评估提案。

我可能不是试图创建该标准列表的人,但我认为如果有人将它们放在一起会非常有帮助。 到目前为止,我试图概述我对辩论的理解,以此作为分解 1. 我们所看到的,2. 它有什么问题,3.为什么这些事情是错误的,以及 4. 我们会做什么的起点喜欢看。 我认为我捕获了前 3 项的相当数量,但我无法在不诉诸“Go 当前拥有的内容”的情况下为第 4 项找到答案。

@jba 上面有另一个很好的总结评论,以获取更多上下文。 他说了很多我在这里说的,用不同的词。

@ianlancetaylor或任何比我更密切参与该项目的人,您是否愿意添加一组需要满足的“正式”(全部在一个评论中,有条理,有点全面但绝不具有约束力)标准? 也许如果我们讨论这些标准并确定提案需要满足的 4-6 个要点,我们可以在更多背景下重新启动讨论?

我不认为我可以写出一套全面的正式标准。 我能做的最好的事情就是列出一份不完整的重要事项清单,只有在这样做有重大好处时才应该忽略这些事项。

  • 对 1) 忽略错误的良好支持; 2) 返回未修改的错误; 3) 用附加上下文包装错误。
  • 虽然错误处理代码应该是清晰的,但它不应该支配功能。 阅读非错误处理代码应该很容易。
  • 现有的 Go 1 代码应该继续工作,或者至少必须可以机械地将 Go 1 转换为完全可靠的新方法。
  • 新方法应该鼓励程序员正确处理错误。 理想情况下,做正确的事情应该很容易,无论在任何情况下都是正确的。
  • 任何新方法都应该比当前方法更短和/或更少重复,同时保持清晰。
  • 该语言今天确实有效,每次更改都会带来成本。 改变带来的好处显然值得付出代价。 它不应该只是一个洗涤,它应该明显更好。

我希望自己在某个时候收集笔记,但我想解决恕我直言是本次讨论中的另一个主要障碍,即示例的琐碎性。

我已经从我的一个真实项目中提取了它,并对其进行了清理以供外部发布。 (我认为 stdlib 不是最好的来源,因为它缺少日志记录等问题。)

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    if err != nil {
        return nil, err
    }

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    if err != nil {
        return nil, err
    }

    session, err := communicationProtocol.FinalProtocol(conn)
    if err != nil {
        return nil, err
    }
    client.session = session

    return client, nil
}

(让我们不要过多地修改代码。我当然不能阻止你,但请记住,这不是我真正的代码,并且已经被破坏了一点。我不能阻止你发布自己的示例代码。)

观察:

  1. 这不是完美的代码; 我经常返回裸错误,因为它非常简单,正是我们遇到问题的那种代码。 提案应该通过简洁性和他们演示修复此代码的难易程度来评分。
  2. if forwardPort == 0子句 _deliberately_ 继续出错,是的,这是真正的行为,而不是我为此示例添加的内容。
  3. 这段代码要么返回一个有效的、连接的客户端,要么返回一个错误并且没有资源泄漏,所以围绕 .Close() 的处理(仅当函数出错时)是有意的。 还要注意 Close 的错误消失了,这在真正的 Go 中是很典型的。
  4. 端口号在其他地方受到限制,因此 url.Parse 不会失败(通过检查)。

我不会声称这展示了所有可能的错误行为,但它确实涵盖了相当多的范围。 (我经常为 Go on HN 等辩护,指出当我的代码完成烹饪时,在我的网络服务器中经常会出现_各种_错误行为;检查我自己的生产代码,从 1/3完全 1/2 的错误做了一些事情而不是简单地返回。)

我还将(重新)发布我自己的(更新的)提案以应用于此代码(除非有人说服我他们在那之前有更好的东西),但为了不垄断对话,我将等待至少在周末。 (这比看起来要少,因为它是一大块源代码,但仍然......)

使用try其中try只是 if != nil return 的快捷方式,将代码减少了 59 行中的 6 行,大约是 10%。

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    try err

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    try err

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    try err

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    try err

    session, err := communicationProtocol.FinalProtocol(conn)
    try err

    client.session = session

    return client, nil
}

值得注意的是,在几个地方我想写try x()但我不能,因为我需要设置 err 才能让延迟正常工作。

多一个。 如果我们有try可以在赋值行上发生的事情,它会减少到 47 行。

func NewClient(...) (*Client, error) {
    try listener, err := net.Listen("tcp4", listenAddr)

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    try conn, err := ConnectionManager{}.connect(server, tlsConfig)

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    try session, err := communicationProtocol.FinalProtocol(conn)

    client.session = session

    return client, nil
}
import "github.com/pkg/errors"

func Func3() (T1, T2, error) {...}

type PathError {
    err Error
    x   T3
    y   T4
}

type MiscError {
    x   T5
    y   T6
    err Error
}


func Foo() (T1, T2, error) {
    // Old school
    a, b, err := Func(3)
    if err != nil {
        return nil
    }

    // Simplest form.
    // If last unhandled arg's type is same 
    // as last param of func,
    // then use anon variable,
    // check and return
    a, b := Func3()
    /*    
    a, b, err := Func3()
    if err != nil {
         return T1{}, T2{}, err
    }
    */

    // Simple wrapper
    // If wrappers 1st param TypeOf Error - then pass last and only unhandled arg from Func3() there
    a, b, errors.WithStack() := Func3() 
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithStack(err)
    }
    */

    // Bit more complex wrapper
    a, b, errors.WithMessage("unable to get a and b") := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithMessage(err, "unable to get a and b")
    }
    */

    // More complex wrapper
    // If wrappers 1nd param TypeOf is not Error - then pass last and only unhandled arg from Func3() as last
    a, b, fmt.Errorf("at %v Func3() return error %v", time.Now()) := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, fmt.Errorf("at %v Func3() return error %v", time.Now(), err)
    }
    */

    // Wrapping with error types
    a, b, &PathError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &PathError{err, x, y}
    }
    */
    a, b, &MiscError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &MiscError{x, y, err}
    }
    */

    return a, b, nil
}

有点神奇(随意-1)但支持机械翻译

这就是(有些更新)我的提案的样子:

func NewClient(...) (*Client, error) {
    defer annotateError("client couldn't be created")

    listener := pop net.Listen("tcp4", listenAddr)
    defer closeOnErr(listener)
    conn := pop ConnectionManager{}.connect(server, tlsConfig)
    defer closeOnErr(conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    pop toServer.Send(&client.serverConfig)
    pop toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    session := pop communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

func closeOnErr(c io.Closer) {
    if err := erroring(); err != nil {
        closeErr := c.Close()
        if err != nil {
            seterr(multierror.Append(err, closeErr))
        }
    }
}

func annotateError(annotation string) {
    if err := erroring(); err != nil {
        log.Printf("%s: %v", annotation, err)
        seterr(errwrap.Wrapf(annotation +": {{err}}", err))
    }
}

定义:

pop对于最右边的值是错误的函数表达式是合法的。 它被定义为“如果错误不是零,则返回结果中所有其他值的零值和该错误的函数,否则产生一组没有错误的值”。 poperroring()没有特权交互; erroring()仍然可以看到错误的正常返回。 这也意味着您可以为其他返回值返回非零值,并且仍然使用延迟错误处理。 比喻是从返回值的“列表”中弹出最右边的元素。

erroring()被定义为在栈上向上到正在运行的延迟函数,然后是前一个栈元素(在这个例子中是延迟运行的函数,NewClient),访问当前返回的错误的值进行中。 如果函数没有这样的参数,要么panic 要么返回nil(以更有意义的为准)。 这个错误值不一定来自pop ; 它是任何从目标函数返回错误的东西。

seterr(error)允许您更改返回的错误值。 然后它将是任何未来erroring()调用看到的错误,如此处所示允许现在可以完成的相同的基于延迟的链接。

我在这里使用 hashicorp 包装和多错误; 根据需要插入您自己的巧妙包装。

即使定义了额外的函数,总和也更短。 我希望在额外的用途中摊销这两个功能,所以这些应该只计算部分。

请注意,我只是不理会 forwardPort 处理,而不是尝试围绕它添加更多语法。 作为一个例外情况,这可以更详细一些。

恕我直言,这个提议最有趣的地方只有在你想象尝试用传统的例外来编写它时才能看到。 它最终得到相当深的嵌套,并且处理可能发生的错误的 _collection_ 异常处理非常乏味。 (就像在真正的 Go 代码中一样,.Close 错误往往会被忽略,在异常处理程序中发生的错误在基于异常的代码中往往会被忽略。)

这扩展了现有的 Go 模式,例如defer并使用错误作为值来制作容易纠正的错误处理模式,这些模式在某些情况下很难用当前的 Go 或异常来表达,不需要彻底的手术到运行时(我不认为),而且实际上也不需要 Go 2.0。

缺点包括将erroringpopseterr作为关键字,导致这些功能的defer开销,因子错误处理的事实确实跳过了一些处理功能,并且它没有做任何事情来“强制”正确处理。 尽管我不确定最后一个是否可行,因为根据向后兼容的(正确)要求,您始终可以执行当前的操作。

这里的讨论非常有趣。

我想将错误变量保留在左侧,因此不会引入神奇的变量。 像最初的提案一样,我希望在同一行上处理错误内容。 我不会使用|| -operator,因为它对我来说看起来“太布尔”并且以某种方式隐藏了“返回”。

因此,我会通过使用扩展关键字“return?”使其更具可读性。 在 C# 中,有些地方使用问号来创建快捷方式。 例如而不是写:

if(foo != null)
{ foo.Bar(); }

你可以写:
foo?.Bar();

所以对于 Go 2,我想提出这个解决方案:

func foobar() error {
    return fmt.Errorf("Some error happened")
}

// Implicitly return err (there must be exactly one error variable on the left side)
err := foobar() return?
// Explicitly return err
err := foobar() return? err
// Return extended error info
err := foobar() return? &PathError{"chdir", dir, err}
// Return tuple
err := foobar() return? -1, err
// Return result of function (e. g. for logging the error)
err := foobar() return? handleError(err)
// This doesn't compile as you ignore the error intentionally
foobar() return?

只是一个想法:

foo, 错误 := myFunc()
错误!= 零? 返回包装(错误)

或者

如果错误!= nil ? 返回包装(错误)

如果你愿意在它周围放一些花括号,我们不需要改变任何东西!

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

您可以拥有 _all_ 您想要的自定义处理(或根本没有),您已经从典型案例中节省了两行代码,它 100% 向后兼容(因为没有更改语言),它更紧凑,而且简单。 它击中了伊恩的许多关键gofmt工具?

我在阅读 carlmjohnson 的建议之前写了这个,这是相似的......

只是一个#在错误之前。

但是在现实世界的应用程序中,您仍然必须编写正常的if err != nil { ... }以便您可以记录错误,这使得简单的错误处理无用,除非您可以通过名为after注释添加返回中间件,它在函数返回之后运行...(类似于defer但带有 args)。

@after(func (data string, err error) {
  if err != nil {
    log.Error("error", data, " - ", err)
  }
})
func alo() (string, error) {
  // this is the equivalent of
  // data, err := db.Find()
  // if err != nil { 
  //   return "", err 
  // }
  str, #err := db.Find()

  // ...

  #err = db.Create()

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  // this is the equivalent of
  // data, data2, err, errNotFound := db.Find()
  // if err != nil { 
  //   return nil, nil, err, nil
  // } else if errNotFound != nil {
  //   return nil, nil, nil, errNotFound
  // }
  data, data2, #err, #errNotFound := db.Find()

  // ...

  return data, data2, nil, nil
}

比:

func alo() (string, error) {
  str, err := db.Find()
  if err != nil {
    log.Error("error on find in database", err)
    return "", err
  }

  // ...

  if err := db.Create(); err != nil {
    log.Error("error on create", err)
    return "", err
  }

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  data, data2, err, errNotFound := db.Find()
  if err != nil { 
    return nil, nil, err, nil
  } else if errNotFound != nil {
    return nil, nil, nil, errNotFound
  }

  // ...

  return data, data2, nil, nil
}

guard这样的 Swift 语句怎么样,除了guard...else不是guard...return

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

我喜欢 go 错误处理的明确性。 唯一的麻烦,恕我直言,是它需要多少空间。 我建议进行 2 次调整:

  1. 允许在布尔上下文中使用 nil-able 项,其中nil相当于false ,非nil相当于true
  2. 支持单行条件语句运算符,如&&||

所以

file, err := os.Open("fails.txt")
if err != nil {
    return &FooError{"Couldn't foo fails.txt", err}
}

可以变成

file, err := os.Open("fails.txt")
if err {
    return &FooError{"Couldn't foo fails.txt", err}
}

甚至更短

file, err := os.Open("fails.txt")
err && return &FooError{"Couldn't foo fails.txt", err}

而且,我们可以做到

i,ok := v.(int)
ok || return fmt.Errorf("not a number")

也许

i,ok := v.(int)
ok && s *= i

如果重载&&||会造成太多歧义,也许可以选择其他一些字符(不超过 2 个),例如?#??## ,或??!! ,等等。 重点是支持具有最少“嘈杂”字符(不需要括号、大括号等)的单行条件语句。 运算符&&||很好,因为这种用法在其他语言中已有先例。

不是支持复杂单行条件表达式的提议,只是支持单行条件语句。

此外,这不是支持其他一些语言的全方位“真实性”的建议。 这些条件将支持 nil/非 nil 或布尔值。

对于这些条件运算符,它甚至可能适合限制为单个变量而不支持表达式。 任何更复杂的或带有else子句的内容都可以使用标准的if ...结构来处理。

为什么不像@mattn之前所说的那样发明轮子并使用已知的try..catch形式? )

try {
    a := foo() // func foo(string, error)
    b := bar() // func bar(string, error)
} catch (err) {
    // handle error
}

似乎没有理由区分捕获的错误源,因为如果您真的需要它,您可以始终使用旧形式的if err != nil而不使用try..catch

另外,我不太确定,但如果不处理,可能会添加“抛出”错误的能力?

func foo() (string, error) {
    f := bar() // similar to if err != nil { return "", err }
}

func baz() string {
    // Compilation error.
    // bar's error must be handled because baz() does not return error.
    return bar()
}

@gobwas从可读性的角度来看,完全理解控制流非常重要。 看看你的例子,不知道哪一行可能会导致跳转到 catch 块。 这就像一个隐藏的goto语句。 难怪现代语言试图对其进行明确并要求程序员明确标记控制流可能因抛出错误而发散的地方。 非常像returngoto但语法更好。

@creker是的,我完全同意你的看法。 我在上面的例子中考虑了流量控制,但没有意识到如何以简单的形式做到这一点。

也许是这样的:

try {
    a ::= foo() // func foo(string, error)
    b ::= bar() // func bar(string, error)
} catch (err) {
    // handle error
}

或之前的其他建议,例如try a := foo() ..?

@gobwas

当我进入 catch 块时,我如何知道 try 块中的哪个函数导致了错误?

@urandom如果您需要知道它,您可能想要在没有try..catch情况下执行if err != nil try..catch

@robert-wallis:我在线程前面提到了 Swift 的保护语句

@pdk

允许在布尔上下文中使用 nil-able 项,其中 nil 相当于 false,非 nil 相当于 true

我看到这会导致使用 flag 包的很多错误,人们会写if myflag { ... }但意思是写if *myflag { ... }并且它不会被编译器捕获。

当您背靠背尝试多个事物时,try/catch 仅比 if/else 短,由于控制流问题等,每个人或多或少都同意这是不好的。

FWIW,Swift 的 try/catch 至少解决了不知道哪些语句可能会抛出的视觉问题:

do {
    let dragon = try summonDefaultDragon() 
    try dragon.breathFire()
} catch DragonError.dragonIsMissing {
    // ...
} catch DragonError.halatosis {
    // ...
}

@robert-wallis ,你有一个例子:

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

在第一次使用guard ,它看起来非常像if err != nil { return &FooError{"Couldn't foo fails.txt", err}} ,所以我不确定这是否是一个巨大的胜利。

在第二次使用时,不清楚err来自哪里。 它几乎看起来像是从os.Open返回的内容,我猜这不是您的意图? 这会更准确吗?

guard err = os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

在这种情况下,它看起来像......

if err = os.Remove("fails.txt"); err != nil { return &FooError{"Couldn't remove fails.txt", err}}

但它仍然有较少的视觉混乱。 if err = , ; err != nil { - 即使它是单班轮,对于这么简单的事情来说仍然有太多事情要做

同意有更少的混乱。 但为了保证添加到语言中而明显减少? 我不确定我是否同意。

我认为 Java/C#/... 中 try-catch 块的可读性非常好,因为您可以遵循“快乐路径”序列而不会被错误处理中断。 缺点是你基本上有一个隐藏的转到机制。

在 Go 中,我开始在错误处理程序之后插入空行,以使“快乐路径”逻辑的延续更加明显。 所以从 golang.org 的这个样本中(9 行)

record := new(Record)
err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}
err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

我经常这样做(11行)

record := new(Record)

err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}

err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

现在回到提案,因为我已经发布了这样的内容会很好(3 行)

record := new(Record)
err := datastore.Get(c, key, record) return? &appError{err, "Record not found", 404}
err := viewTemplate.Execute(w, record) return? &appError{err, "Can't display record", 500}

现在我清楚地看到了幸福之路。 我的眼睛仍然知道右侧有用于错误处理的代码,但我只需要在真正需要时对其进行“眼睛解析”。

对所有人的一个问题:这段代码应该编译吗?

func foobar() error {
    return fmt.Errorf("Some error")
}
func main() {
    foobar()
}

恕我直言,用户应该被迫说他故意忽略了以下错误:

func main() {
    _ := foobar()
}

添加与@ianlancetaylor原帖的第 3 点相关的迷你体验报告,返回带有附加上下文信息的错误

在为 Go 开发flac 库时,我们希望使用@davecheney pkg/errors包 (https://github.com/mewkiz/flac/issues/22) 将上下文信息添加到错误中。 更具体地说,我们使用errors.WithStack包装返回的错误,它使用堆栈跟踪信息注释错误。

由于错误被注解,所以需要创建一个新的底层类型来存储这个附加信息,在errors.WithStack的情况下,类型是errors.withStack

type withStack struct {
    error
    *stack
}

现在,要检索原始错误,约定是使用errors.Cause 。 这使您可以将原始错误与例如io.EOF

然后,库的用户可以按照https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78使用errors.Cause检查原始错误价值:

frame, err := stream.ParseNext()
if err != nil {
    if errors.Cause(err) == io.EOF {
        break
    }
    return errors.WithStack(err)
}

这几乎适用于所有情况。

在重构我们的错误处理以始终一致地使用 pkg/errors 来添加上下文信息时,我们遇到了一个相当严重的问题。 为了验证零填充,我们实现了一个 io.Reader开始失败

问题是zeros.Read返回的错误的底层类型现在是errors.withStack,而不是io.EOF。 因此,当我们将该阅读器与io.Copy结合使用时,随后会导致问题,它专门检查io.EOF ,并且不知道使用errors.Cause来“解开”注释为的错误上下文信息。 由于我们无法更新标准库,解决方案是返回没有注释信息的错误(https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be)。

虽然丢失返回具体值的接口的注释信息是一种损失,但可以忍受。

从我们的经验中得出的结论是我们很幸运,因为我们的测试用例抓住了这一点。 编译器没有产生任何错误,因为zeros类型仍然实现了io.Reader接口。 我们也没有想到我们会遇到问题,因为添加的错误注释是机器生成的重写,简单地向错误添加上下文信息应该不会影响正常状态下的程序行为。

但确实如此,因此,我们希望贡献我们的经验报告以供考虑; 在考虑如何将上下文信息的添加集成到 Go 2 的错误处理中时,这样错误比较(如接口合同中使用的)仍然无缝地保持。

亲切地,
罗宾

@mewmew ,让我们将这个问题保留在错误处理的控制流方面。 如何最好地包装和解开错误应该在别处讨论,因为它在很大程度上与控制流正交。

我不熟悉你的代码库,我知道你说这是一个自动重构,但为什么你需要在 EOF 中包含上下文信息? 尽管类型系统将其视为错误,但 EOF 实际上更像是一个信号值,而不是实际错误。 特别是在io.Reader实现中,大多数时候它是一个预期值。 如果错误不是io.EOF那么只包装错误不是更好的解决方法吗?

是的,我建议我们保持现状。 我的印象是 Go 的错误系统是故意这样设计的,以阻止开发人员将错误推上调用堆栈。 该错误旨在在它们发生的地方解决,并知道什么时候使用恐慌更合适。

我的意思是 try-catch-throw 本质上不是 panic() 和 recovery() 的相同行为吗?

叹息,如果我们真的要开始尝试沿着这条路走下去。 为什么我们不能做类似的事情

_, ? := foo()
x?, err? := bar()

或者甚至是类似的东西

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

在哪里? 成为 if var != nil{ return var } 的简写别名。

我们甚至可以定义另一个满足该方法的特殊内置接口

func ?() bool //looks funky but avoids breakage.

我们可以用来基本上覆盖新的和改进的条件运算符的默认行为。

@mortdeus

我想我同意。
如果问题是有一种很好的方式来呈现快乐路径,IDE 的插件可以使用快捷方式折叠/展开if err != nil { return [...] }每个实例?

我觉得现在每个部分都很重要。 err != nil很重要。 return ...很重要。
写起来有点麻烦,但还是得写。 它真的会减慢人们的速度吗? 需要时间的是思考错误和返回什么,而不是写出来。

我对允许限制err变量范围的提案更感兴趣。

我认为我的有条件的想法是解决这个问题的最巧妙的方法。 我只是想到了一些其他的东西,可以使这个功能足够值得包含在 Go 中。 我将在单独的提案中写下我的想法。

我不明白这是如何工作的:

x, y, 错误:= baz(); 返回 ( x? && y? ) || err?

在哪里? 成为 if var == nil{ return var } 的简写别名。

x, y, 错误:= baz(); 返回 ( if x == nil{ return x} && if y== nil{ return y} ) || if err == nil{ return err}

x, y, 错误:= baz(); 返回 (x? && y?) || 呃?

变成

x, y, 错误:= baz();
if ((x != nil && y !=nil) || err !=nil)){
返回 x,y, 错误
}

当你看到 x? &&你? || 呃? 你应该在想“x 和 y 是否有效?错误呢?”

如果不是,则返回函数不执行。 我刚刚写了一个关于这个想法的新提案,它通过一个新的特殊内置接口类型进一步扩展了这个想法

我建议 Go 在 Go 版本 2 中添加默认的错误处理。

如果用户不处理错误,编译器返回 err 如果它不是 nil,所以如果用户写:

func Func() error {
    func1()
    func2()
    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

编译将其转换为:

func Func() error {
    err := func1()
    if err != nil {
        return err
    }

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

    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

如果用户使用 _ 处理错误或忽略它,编译器将不执行任何操作:

_ = func1()

或者

err := func1()

对于多个返回值,类似:

func Func() (*YYY, error) {
    ch, x := func1()
    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

编译器将转换为:

func Func() (*YYY, error) {
    ch, x, err := func1()
    if err != nil {
        return nil, err
    }

    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

如果 Func() 的签名不返回错误而是调用返回错误的函数,编译器将报告错误:“请在 Func() 中处理您的错误”
然后用户可以在 Func() 中记录错误

如果用户想将一些信息包装到错误中:

func Func() (*YYY, error) {
    ch, x := func1() ? Wrap(err, "xxxxx", ch, "abc", ...)
    return yyy, nil
}

或者

func Func() (*YYY, error) {
    ch, x := func1() ? errors.New("another error")
    return yyy, nil
}

好处是,

  1. 程序只是在发生错误的时候失败,用户不能隐式忽略错误。
  2. 可以显着减少代码行数。

这并不容易,因为 Go 可以有多个返回值,并且语言不应该在开发人员没有明确知道发生了什么的情况下分配本质上等同于返回参数的默认值。

我认为将错误处理放在赋值语法中并不能解决问题的根源,即“错误处理是重复的”。

在多行代码(这样做有意义)之后使用if (err != nil) { return nil } (或类似的)违反 DRY(不要重复自己)原则。 我相信这就是我们不喜欢这个的原因。

try ... catch也存在问题。 您不需要在发生错误的同一函数中显式处理错误。 我相信这是我们不喜欢try...catch的一个显着原因。

我不相信这些是相互排斥的。 我们可以有一个排序的try...catch没有throws

我个人不喜欢try...catch另一件事是try关键字的任意必要性。 就工作语法而言,没有理由不能在任何范围限制器之后使用catch 。 (如果我错了,有人会说出来)

这是我的建议:

  • 使用?作为返回错误的占位符,其中_将用于忽略它
  • 代替catch在我下面的示例中,可以使用error?代替以实现完全向后兼容性

^ 如果我认为这些向后兼容的假设是不正确的,请指出。

func example() {
    {
        // The following line will invoke the catch block
        data, ? := foopkg.buyIt()
        // The next two lines handle an error differently
        otherData, err := foopkg.useIt()
        if err != nil {
            // Here we eliminated deeper indentation
            otherData, ? = foopkg.breakIt()
        }
        if data == "" || otherData == "" {
        }
    } catch (err) {
        return errors.Label("fix it", err)
        // Aside: why not add Label() to the error package?
    }
}

我想到了一个反对这一点的论点:如果你这样写,改变那个catch块可能会对更深层次的代码产生意想不到的影响。 这与我们在try...catch中遇到的问题相同。

我认为,如果您只能在单个函数的范围内执行此操作,则风险是可控的——可能与当前在您打算更改其中许多错误处理代码时忘记更改一行错误处理代码的风险相同。 我认为这是代码重用的后果与不遵循 DRY 的后果(即没有免费午餐,正如他们所说)之间的相同区别

编辑:我忘了为我的例子指定一个重要的行为。 如果?在没有任何catch的范围内使用,我认为这应该是编译器错误(而不是引发恐慌,这无疑是我想到的第一件事)

编辑 2:疯狂的想法:也许catch块不会影响控制流......它实际上就像将catch { ... }的代码复制并粘贴到错误后的行中? ed(不完全是 - 它仍然有自己的范围)。 这看起来很奇怪,因为我们都不习惯它,所以catch如果这样做的话绝对不应该是关键字,否则......为什么不呢?

@mewmew ,让我们将这个问题保留在错误处理的控制流方面。 如何最好地包装和解开错误应该在别处讨论,因为它在很大程度上与控制流正交。

好的,让我们保留这个线程来控制流量。 我添加它只是因为它是一个与第 3 点的具体用法相关的问题,返回带有附加上下文信息的错误

@jba您是否知道任何专门用于包装/

我不熟悉你的代码库,我知道你说这是一个自动重构,但为什么你需要在 EOF 中包含上下文信息? 尽管类型系统将其视为错误,但 EOF 实际上更像是一个信号值,而不是实际错误。 特别是在 io.Reader 实现中,大多数时候它是一个预期值。 如果错误不是 io.EOF,那么只包装错误不是更好的解决方法吗?

@DeedleFake我可以详细说明一下,但为了保持主题,将在前面提到的专门用于包装/解开错误上下文信息的问题中这样做。

我阅读的所有提案(包括我的)越多,我就越不认为我们在 go 中的错误处理真的有问题。

我想要的是一些强制措施,不要意外忽略错误返回值,但至少要强制执行
_ := returnsError()

我知道有工具可以找到这些问题,但是对语言的第一级支持可能会捕获一些错误。 根本不处理错误就像拥有一个未使用的变量 - 这已经是一个错误。 当您向函数引入错误返回类型时,它还有助于重构,因为您必须在所有地方处理它。

大多数人在这里尝试解决的主要问题似乎是“打字量”或“行数”。 我同意任何减少行数的语法,但这主要是一个 gofmt 问题。 只允许内联“单行范围”,我们很好。

另一个保存一些输入的建议是隐式nil像布尔值一样检查:

err := returnsError()
if err { return err }

甚至

if err := returnsError(); err { return err }

这将适用于所有指针类型的原因。

我的感觉是,将函数调用 + 错误处理减少到一行的一切都会导致代码可读性降低和语法更复杂。

可读性较差的代码和更复杂的语法。

由于冗长的错误处理,我们已经有了可读性较差的代码。 添加已经提到的 Scanner API 技巧,它应该隐藏冗长,使情况变得更糟。 添加更复杂的语法可能有助于提高可读性,即语法糖的最终用途。 否则这个讨论就没有意义了。 在我看来,冒泡错误并为其他所有内容返回零值的模式很常见,足以保证语言更改。

冒泡错误并为所有内容返回零值的模式

19642 将使这更容易。


另外,感谢@mewmew的体验报告。 它肯定与这个线程有关,因为它与特定类型的错误处理设计中的危险有关。 我很想看到更多这些。

我觉得我的想法解释得不是很好,所以我创建了一个要点(并修改了我刚刚注意到的许多缺点)

https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390

这个要点中有不止一个想法,所以我希望它们可以分开讨论

暂时搁置这里的提案必须明确关于错误处理的想法,如果 Go 引入了类似collect语句怎么办?

collect语句的格式collect [IDENT] [BLOCK STMT] ,其中 ident 必须是nil类型的范围内变量。 在collect语句中,特殊变量_!可用作收集到的变量的别名。 _!不能在任何地方使用,只能作为赋值使用,与_ 。 每当分配_! ,都会执行隐式nil检查,如果_!不为零,则块停止执行并继续执行其余代码。

从理论上讲,这看起来像这样:

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    collect err {
        intermediate1, _! := Step1()
        intermediate2, _! := Step2(intermediate1, "something")
        // assign to result from the outer scope
        result, _! = Step3(intermediate2, 12)
    }
    return result, err
}

这相当于

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    {
        var intermediate1 SomeType
        intermediate1, err = Step1()
        if err != nil { goto collectEnd }

        var intermediate2 SomeOtherType
        intermediate2, err = Step2(intermediate1, "something")
        if err != nil { goto collectEnd }

        result, err = Step3(intermediate2, 12)
        // if err != nil { goto collectEnd }, but since we're at the end already we can omit this
    }

collectEnd:
    return result, err
}

像这样的语法功能还可以实现一些不错的其他功能:

// try several approaches for acquiring a value
func GetSomething() (s *Something) {
    collect s {
        _! = fetchOrNil1()
        _! = fetchOrNil2()
        _! = new(Something)
    }
    return s
}

需要新的语法特性:

  1. 关键字collect
  2. 特殊标识_! (我已经在解析器中使用过它,在不破坏其他任何东西的情况下将此匹配作为标识并不难)

我之所以提出这样的建议是因为“错误处理过于重复”的论点可以归结为“nil 检查过于重复”。 Go 已经有很多错误处理功能可以按原样工作。 您可以使用_忽略错误(或者只是不捕获返回值),您可以使用if err != nil { return err }返回未修改的错误,或者添加上下文并使用if err != nil { return wrap(err) } 。 这些方法本身都不是太重复。 重复性(显然)来自必须在整个代码中重复这些或类似的语法语句。 我认为引入一种在遇到非 nil 值之前执行语句的方法是保持错误处理相同的好方法,但减少了这样做所需的样板数量。

  • 对 1) 忽略错误的良好支持; 3) 用附加上下文包装错误。

检查,因为它保持不变(大部分)

  • 虽然错误处理代码应该是清晰的,但它不应该支配功能。

检查,因为如果需要,错误处理代码现在可以放在一个地方,而函数的内容可以以线性可读的方式发生

  • 现有的 Go 1 代码应该继续工作,或者至少必须可以机械地将 Go 1 转换为完全可靠的新方法。

检查,这是添加而不是更改

  • 新方法应该鼓励程序员正确处理错误。

检查一下,我认为,因为错误处理的机制没有什么不同 - 我们只有一个语法来从一系列执行和赋值中“收集”第一个非 nil 值,它可用于限制数量我们必须在函数中编写错误处理代码的地方

  • 任何新方法都应该比当前方法更短和/或更少重复,同时保持清晰。

我不确定这在这里是否适用,因为建议的功能不仅仅适用于错误处理。 我确实认为它可以缩短和澄清可能产生错误的代码,而不会在 nil 检查和早期返回中出现混乱

  • 该语言今天确实有效,每次更改都会带来成本。 它不应该只是一个洗涤,它应该明显更好。

同意,因此似乎范围超出错误处理范围的更改可能是合适的。 我相信潜在的问题是nil检查变得重复和冗长,而且恰好error是一个nil -able 类型。

@KernelDeimos我们基本上只是想出了同样的事情。 然而,我更进一步,并解释了为什么x, ? := doSomething()方式在实践中并没有真正奏效。 虽然很高兴看到我不是唯一一个考虑添加 ? 运算符以一种有趣的方式融入语言。

https://github.com/golang/go/issues/25582

这不是基本上只是陷阱吗?

这是一个吐痰球:

func NewClient(...) (*Client, error) {
    trap(err error) {
        return nil, err
    }

    listener, err? := net.Listen("tcp4", listenAddr)
    trap(_ error) {
        listener.Close()
    }

    conn, err? := ConnectionManager{}.connect(server, tlsConfig)
    trap(_ error) {
        conn.Close()
    }

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?

    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?

    session, err? := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

59 行 → 44

trap表示“如果标有指定类型的?的变量不是零值,则按堆栈顺序运行此代码。” 它就像defer但它会影响控制流。

我有点喜欢trap想法,但语法让我有点不舒服。 如果它是一种声明呢? 例如, trap err error {}声明一个trap称为err类型error其中,当分配给运行给定的代码。 代码甚至不必返回; 它只是被允许这样做。 这也打破了对nil特殊性的依赖。

编辑:现在我不在电话上,展开并举一个例子。

func Example(r io.Reader) error {
  trap err error {
    if err != nil {
      return err
    }
  }

  n, err? := io.Copy(ioutil.Discard, r)
  fmt.Printf("Read %v bytes.\n", n)
}

本质上, trap功能与var类似,不同之处在于,只要将它分配给?运算符,就会执行代码块。 ?运算符还可以防止它与:=一起使用时被遮蔽。 重新声明一个trap在同一范围内,不同于var ,是允许的,但它必须是相同的类型的现有的一个; 这允许您更改关联的代码块。 因为正在运行的块不一定会返回,所以它还允许您为特定的东西设置单独的路径,例如检查是否err == io.EOF

我喜欢这种方法的地方在于它看起来类似于Errors are Values 中errWriter示例,但在更通用的设置中不需要声明新类型。

@carlmjohnson你在回复谁?
无论如何,这个trap概念似乎只是编写defer语句的另一种方式,不是吗? 如果您panic遇到非 nil 错误,然后使用延迟闭包来设置命名返回值并执行清理,那么编写的代码基本上是相同的。 我认为这与我之前使用_!自动恐慌的提议遇到了相同的问题,因为它将一种错误处理方法置于另一种之上。 FWIW 我还觉得编写的代码比原始代码更难推理。 这个trap概念今天可以用 go 来模仿吗,即使它没有语法那么清晰? 我觉得它可以并且if err != nil { panic (err) }defer来捕获和处理它。

它似乎类似于我上面建议的collect块的概念,我个人认为它提供了一种更清晰的方式来表达相同的想法(“如果这个值不为零,我想捕获它并做一些事情用它”)。 Go 喜欢线性和明确的。 trap感觉像是panic / defer新语法,但控制流程不太清晰。

@mccolljr ,这似乎是对我recover -类似错误处理的函数。

我还观察到“陷阱”重写删除了我的提案的许多功能(出现了_非常_不同的错误),此外,我不清楚如何使用陷阱语句排除错误处理。 我的提案中的很多减少都以降低错误处理正确性的形式出现,并且我相信,回到让直接返回错误比做其他任何事情更容易。

我在上面给出的修改后的trap示例允许继续流动的能力。 我后来编辑了它,所以我不知道你是否看到了。 它与collect非常相似,但我认为它提供了更多的控制权。 根据作用域规则的工作方式,它可能有点像意大利面条,但我认为可以找到一个很好的平衡。

@thejerf啊,这更有意义。 我没有意识到这是对你提议的答复。 但是,我不清楚erroring()recover()之间的区别是什么,除了recover响应panic的事实。 似乎我们只是在需要返回错误时隐式地进行某种形式的恐慌。 延迟也是一个有点昂贵的操作,所以我不确定在所有可能产生错误的函数中使用它的感觉如何。

@DeedleFake同样适用于trap ,因为在我看来trap本质上是一个宏,它在使用?运算符时插入代码,该运算符呈现出自己的一系列问题和注意事项,或者它被实现为goto ... ,如果用户没有在trap块中返回,或者它只是一个语法不同的defer ,该怎么办。 另外,如果我在一个函数中声明几个陷阱块怎么办? 允许吗? 如果是这样,哪一个被执行? 这增加了实现的复杂性。 Go 喜欢自以为是,我喜欢这一点。 我认为collect或类似的线性结构比trap更符合 Go 的意识形态,正如我在我的第一个提案后向我指出的那样,它似乎是一个try-catch以服装建造。

如果用户没有在陷阱块中返回怎么办

如果trap不返回或以其他方式修改控制流( gotocontinuebreak等),则控制流返回到代码所在的位置块被“调用”。 块本身的工作方式与调用闭包类似,不同之处在于它可以访问控制流机制。 这些机制将在块被声明的地方工作,而不是在它被调用的地方,所以

for {
  trap err error {
    break
  }

  err? = errors.New("Example")
}

会工作。

另外,如果我在一个函数中声明几个陷阱块怎么办? 允许吗? 如果是这样,哪一个被执行?

是的,这是允许的。 这些块由陷阱命名,因此确定应该调用哪个块是相当简单的。 例如,在

trap err error {
  // Block 1.
}

trap n int {
  // Block 2.
}

n? = 3

块 2 被调用。 在这种情况下,最大的问题可能是在n?, err? = 3, errors.New("Example")的情况下会发生什么,这可能需要指定分配的顺序,如#25609 中所述。

我认为 collect 或类似的线性构造比 trap 更符合 Go 的意识形态,正如我在我的第一个提议后向我指出的那样,它似乎是一种装扮的 try-catch 构造。

我认为collecttrap本质上都是相反的try-catch 。 标准的try-catch是默认失败策略,需要您检查否则它会爆炸。 这是一个默认成功的系统,本质上允许您指定失败路径。

使整个事情复杂化的一件事是,错误本身并不被视为失败,而某些错误,例如io.EOF ,根本没有真正指定失败。 我认为这就是为什么与错误无关的系统,例如collecttrap ,是要走的路。

“啊,这更有意义。我没有意识到这是对你提议的回复。但是,我不清楚 erroring() 和 recovery() 之间的区别,除了recover 响应的事实之外恐慌。”

没有太大的区别是重点。 我正在尽量减少创建的新概念的数量,同时从它们中获得尽可能多的力量。 我认为在现有功能的基础上构建是一个特性,而不是一个错误。

我的提议的一个要点是探索不仅仅是“如果我们修复这个重复出现的三行代码块,我们return err并用?替换它会怎样”到“它如何影响语言的其余部分?它启用了哪些新模式?它创造了哪些新的“最佳实践”?哪些旧的“最佳实践”不再是最佳实践?” 我不是说我完成了那份工作。 即使判断这个想法实际上对 Go 的口味来说有太多的力量(因为 Go 不是一种功率最大化的语言,即使设计选择将其限制为error它仍然可能是最强大的在这个线程中提出的建议,我完全是指“强大”的好坏两方面),我认为我们可以继续探索新结构将对整个程序产生什么影响的问题,而不是它会做什么将执行七行示例函数,这就是为什么我尝试至少将示例提高到“真实代码”范围的 ~50-100 行。 5 行中的所有内容看起来都一样,其中包括 Go 1.0 错误处理,这可能是我们都从自己的经验中知道这里存在真正问题的部分原因,但是如果我们谈论的话,谈话只是在循环它的规模太小,直到有些人开始说服自己也许毕竟没有问题。 (相信您的真实编码经验,而不是 5 行示例!)

“当需要返回错误时,我们似乎只是隐含地做某种形式的恐慌。”

这不是隐含的。 这是明确的。 当pop操作符执行您想要的操作时,您可以使用它。 当它没有做你想做的事时,你就不要使用它。 它所做的事情很简单,可以用一个简单的句子来表达,尽管规范可能需要整整一段,因为这就是这些事情的工作方式。 没有隐含的。 此外,它不是恐慌,因为它只展开堆栈的一层,就像返回一样; 它与回归一样令人恐慌,但根本不是。

我也不在乎你是否将pop拼写为 ? 管他呢。 我个人认为一个词看起来更像 Go,因为 Go 目前不是一种符号丰富的语言,但不能否认符号具有不与任何现有源代码冲突的优势。 我对语义以及我们可以在它们的基础上构建什么以及新语义为新的和有经验的程序员提供的行为比拼写更感兴趣。

“延迟也是一个有点昂贵的操作,所以我不确定在所有可能产生错误的函数中使用它的感觉如何。”

我已经承认了。 虽然我建议一般来说它并没有那么昂贵,而且如果您有一个热门函数,为了优化目的,我并不会觉得太糟糕,请按照当前方式编写它。 尝试修改所有错误处理函数的 100% 显然不是我的目标,而是让其中的 80% 更简单、更正确,并让 20% 的情况(老实说,可能更像 98/2)保持原样是。 大量的 Go 代码对使用 defer 并不敏感,毕竟这就是defer存在的首要原因。

事实上,你可以简单地修改提议,不使用 defer 并使用像trap这样的关键字作为声明,无论它出现在哪里都只运行一次,而不是 defer 实际上是一个推送处理程序到延迟函数的堆栈上。 我故意选择重用defer以避免向语言添加新概念......甚至理解可能由循环中的延迟导致意外咬人的陷阱。 但这仍然只是需要理解的defer概念。

只是为了明确说明,向语言添加新关键字是一项重大更改。

package main

import (
    "fmt"
)

func return(i int)int{
    return i
}

func main() {
    return(1)
}

结果是

prog.go:7:6: syntax error: unexpected select, expecting name or (

这意味着,如果我们尝试在语言中添加trytrapassert任何关键字,我们都会冒着破坏大量代码的风险。 可能需要更长时间维护的代码。

这就是为什么我最初提议添加一个特殊的? go 运算符,它可以应用于语句上下文中的变量。 目前的?字符被指定为变量名的非法字符。 这意味着它当前没有在任何当前的 Go 代码中使用,因此我们可以在不引起任何破坏性更改的情况下引入它。

现在在赋值的左侧使用它的问题是它没有考虑 Go 允许多个返回参数。

例如考虑这个功能

func getCoord() x int, y int, z int, err error{
    x, err = getX()
    if err != nil{
        return 
    }

    y, err = getY()
    if err != nil{
        return 
    }

    z, err = getZ()
        if err != nil{
        return 
    }
    return
}

如果我们使用? 或者尝试在赋值的 lhs 中摆脱 if err != nil 块,我们是否自动假定错误意味着所有其他值现在都是垃圾? 如果我们这样做会怎样

func GetCoord() (x, y, z int, err error) {
    err = try GetX(&x) // or err? = GetX(&x) 
    err = try GetY(&y) // or err? = GetY(&x) 
    err = try GetZ(&z) // or err? = GetZ(&x) 
}

我们在这里做了什么假设? 仅仅假设可以扔掉价值应该不会有害吗? 如果错误更像是一个警告并且值 x 很好怎么办? 如果抛出错误的唯一函数是调用 GetZ() 并且 x、y 值实际上很好怎么办? 我们是否假定归还它们。 如果我们不使用命名返回参数怎么办? 如果返回 args 是像 map 或 channel 这样的引用类型,我们是否应该假设将 nil 返回给调用者是安全的?

TLDR; 添加? 或try分配给以消除

if err != nil{
    return err
}

比津贴引入了太多的混乱。

并添加类似trap建议的内容引入了破损的可能性。

这就是为什么在一个单独的问题中func ?() bool的能力,这样当你打电话时说

x, err := doSomething; return x, err?    

你可以让陷阱副作用以适用于任何类型的方式发生。

并应用 ? 只处理像我展示的那样的语句允许语句的可编程性。 在我的提案中,我建议允许一个特殊的 switch 语句,允许某人切换关键字 + ?

switch {
    case select?:
    //side effect/trap code specific to select
    case return?:
    //side effect/trap code specific to returns
    case for?: 
    //side effect/trap code specific to for? 

    //etc...
}  

如果我们使用 ? 在没有显式的类型上? 函数声明或内置类型,然后检查是否 var == nil || 的默认行为零值 {execute the statement} 是假定的意图。

Idk,我不是编程语言设计方面的专家,但这不是

例如, os.Chdir 函数当前是

func Chdir(dir string) error {
  if e := syscall.Chdir(dir); e != nil {
      return &PathError{"chdir", dir, e}
  }
  return nil
}

在这个提议下,它可以写成

func Chdir(dir string) error {
  syscall.Chdir(dir) || &PathError{"chdir", dir, err}
  return nil
}

本质上与 javascript 的箭头函数或 Dart 定义的“粗箭头语法”相同

例如

func Chdir(dir string) error {
    syscall.Chdir(dir) => &PathError{"chdir", dir, err}
    return nil
}

飞镖游

对于只包含一个表达式的函数,您可以使用简写语法:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
=> expr 语法是 { return expr; 的简写; }. => 符号有时被称为粗箭头语法。

@mortdeus ,Dart 箭头的左侧是一个函数签名,而syscall.Chdir(dir)是一个表达式。 它们似乎或多或少无关。

@mortdeus我之前忘了澄清,但我在这里评论的想法与您标记的提案几乎没有相似之处。 我确实喜欢?作为占位符的想法,所以我复制了它,但我的想法强调重新使用单个代码块来处理错误,同时避免try...catch一些已知问题。 我非常小心地提出了一些以前没有讨论过的东西,所以我可以贡献一个新的想法。

新的条件return (或returnIf )语句怎么样?

return(bool expression) ...

IE。

err := syscall.Chdir(dir)
return(err != nil) &PathError{"chdir", dir, e}

a, b, err := Foo()    // signature: func Foo() (string, string, error)
return(err != nil) "", "", err

或者让fmt在一行而不是三行上格式化一个仅返回函数的行:

err := syscall.Chdir(dir)
if err != nil { return &PathError{"chdir", dir, e} }

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil { return "", "", err }

我突然想到,如果我将它与命名参数结合起来,我可以通过添加一些运算符来获得我想要的一切,如果最右边的错误不是零,则该运算符会提前返回:

func NewClient(...) (c *Client, err error) {
    defer annotateError(&err, "client couldn't be created")

    listener := net.Listen("tcp4", listenAddr)?
    defer closeOnErr(&err, listener)
    conn := ConnectionManager{}.connect(server, tlsConfig)?
    defer closeOnErr(&err, conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?
    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?
    session := communicationProtocol.FinalProtocol(conn)?
    client.session = session

    return client, nil
}

func closeOnErr(err *error, c io.Closer) {
    if *err != nil {
        closeErr := c.Close()
        if err != nil {
            *err = multierror.Append(*err, closeErr)
        }
    }
}

func annotateError(err *error, annotation string) {
    if *err != nil {
        log.Printf("%s: %v", annotation, *err)
        *err = errwrap.Wrapf(annotation +": {{err}}", err)
    }
}

目前我对 Go 共识的理解是反对命名参数的,但如果名称参数的可供性发生变化,那么共识也会发生变化。 当然,如果反对它的共识足够强烈,则可以选择包括访问者。

这种方法让我得到了我正在寻找的东西(更容易在函数顶部系统地处理错误,分解此类代码的能力,以及减少行数的能力)以及这里的几乎所有其他建议,包括原始建议. 即使 Go 社区认为它不喜欢它,我也不必在意,因为它在我的代码中是逐个函数的,并且在任一方向都没有阻抗失配。

虽然我会表达对允许签名函数func GetInt() (x int, err error)在带有OtherFunc(GetInt()?, "...") (或任何最终结果)的代码中使用的提案的偏好,而不是无法组合成的一种表达。 虽然对不断重复的简单错误处理子句的烦恼较少,但我的代码量解包了 arity 2 的函数以使其能够获得第一个结果仍然令人讨厌,并且并没有真正增加任何内容的清晰度结果代码。

@thejerf ,我觉得这里有很多奇怪的行为。 您正在调用net.Listen ,它返回一个错误,但它没有被分配。 然后你推迟,传入err 。 每个新的defer会覆盖最后一个,以便永远不会调用annotateError ? 或者它们是否堆叠,以便如果从toServer.Send返回错误,则closeOnErr被调用两次,然后annotateError被调用? 仅当前一个调用具有匹配的签名时才调用closeOnErr吗? 这个案子呢?

conn := ConnectionManager{}.connect(server, tlsConfig)?
fmt.Printf("Attempted to connect to server %#v", server)
defer closeOnErr(&err, conn)

阅读代码,它也混淆了事情,比如为什么我不能说

client.session = communicationProtocol.FinalProtocol(conn)?

大概是因为FinalProtocol返回错误? 但这对读者是隐藏的。

最后,当我想在函数内报告错误并从中恢复时会发生什么? 看起来你的例子会阻止这种情况?

_附录_

好的,我认为当您想从错误中恢复时,通过您的示例,您可以分配它,如下行所示:

env, err := environment.GetRuntimeEnvironment()

这很好,因为 err 被隐藏了,但是如果我改变了......

forwardPort, err = env.PortToForward()
if err != nil {
    log.Printf("env couldn't provide forward port: %v", err)
}

只是

forwardPort = env.PortToForward()

那么您处理的延迟错误将无法捕获它,因为您使用的是在范围内创建的err 。 或者我错过了什么?

我相信对表示函数可能失败的语法的补充是一个好的开始。 我提出了一些类似的建议:

func (r Reader) Read(b []byte) (n int) fails {
    if somethingFailed {
        fail errors.New("something failed")
    }

    return 0
}

如果函数失败(通过使用fail关键字而不是return ,它将为每个返回参数返回零值。

func (c EmailClient) SendEmail(to, content string) fails {
    if !c.connected() {
        fail errors.New("could not connect")
    }

    // You can handle it and execution will continue if you don't fail or return
    n := r.Read(b) handle (err) {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    n := r.Read(b)
}

这种方法的好处是允许当前代码工作,同时启用一种新机制来更好地处理错误(至少在语法上)。

对关键词的评论:

  • fails可能不是最好的选择,但它是我目前能想到的最好的选择。 我想过使用err (或errs ),但是由于当前的预期,它们目前的使用方式可能会使这是一个糟糕的选择( err很可能是一个变量名,并且errs可能被假定为切片或数组或错误)。
  • handle可能有点误导。 我想使用recover ,但它用于panic s ...

编辑:更改 r.Read 调用以匹配io.Reader.Read()

该建议的部分原因是因为 Go 中当前的方法无法帮助工具理解返回的error值是否表示函数失败,或者它正在返回错误值作为其函数的一部分(例如github.com/pkg/errors )。

我相信使函数能够明确表达失败是改进错误处理的第一步。

@ibrasho ,您的示例与...有何不同

func (c EmailClient) SendEmail(to, content string) error {
    // ...

    // You can handle it and execution will continue if you don't fail or return
    _, _, err := r.Read()
        if err != nil {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    _, _, err := r.Read()
}

...如果我们为error未处理实例发出编译器警告或 lint ? 无需更改语言。

两件事情:

  • 我认为建议的语法阅读和看起来更好。 😁
  • 我的版本要求让函数能够明确声明它们失败。 这是目前 Go 中缺少的东西,可以使工具做更多的事情。 我们总是可以将返回error值的函数视为失败,但这是一个假设。 如果函数返回 2 error值怎么办?

我的建议有一些我删除的东西,那就是自动传播:

func (c EmailClient) SendEmail(to, content string) fails {
    n := r.Read(b)

    // Would automaticaly propgate the error, so it will be equivlent to this:
    // n := r.Read(b) handle (err) {
    //  fail err
    // }
}

我删除了它,因为我认为错误处理应该是明确的。

编辑:更改 r.Read 调用以匹配io.Reader.Read()

那么,这将是一个有效的签名或原型吗?

func (r *MyFileReader) Read(b []byte) (n int, err error) fails

(考虑到io.Reader实现在没有更多内容可读取时分配io.EOF并且在失败条件下出现其他一些错误。)

是的。 但是没有人应该期望err表示函数执行失败。 应将读取失败错误传递给句柄块。 错误是 Go 中的值,这个函数返回的错误除了是这个函数返回的东西(出于某种奇怪的原因)之外,应该没有特殊的意义。

我提议失败将导致返回值作为零值返回。 当前的 Reader.Read 已经做出了一些使用这种新方法可能无法实现的承诺。

当 Read 在成功读取 n > 0 个字节后遇到错误或文件结束条件时,它返回读取的字节数。 它可能会从同一个调用中返回(非零)错误或从后续调用中返回错误(和 n == 0)。 这种一般情况的一个实例是,在输入流末尾返回非零字节数的 Reader 可能返回 err == EOF 或 err == nil。 下一个 Read 应该返回 0,EOF。

在考虑错误 err 之前,调用者应始终处理返回的 n > 0 个字节。 这样做可以正确处理在读取一些字节后发生的 I/O 错误以及允许的 EOF 行为。

不鼓励 Read 的实现返回带有 nil 错误的零字节计数,除非 len(p) == 0。调用者应该将返回 0 和 nil 视为表示没有发生任何事情; 特别是它不表示EOF。

在当前提出的方法中,并非所有这些行为都是可能的。 从目前的 Read 接口契约来看,我看到了一些不足,比如如何处理部分读取。

一般来说,一个函数在它失败时部分完成时应该如何表现? 老实说,我还没有考虑过这个问题。

io.EOF 的情况很简单:

func DoSomething(r io.Reader) fails {
    // I'm using rerr so that I don't shadow the err returned from the function
    n, err := r.Read(b) handle (rerr) {
        if rerr != io.EOF {
            fail err
        }
        // Else do nothing?
    }
}

@thejerf ,我觉得这里有很多奇怪的行为。 你正在调用 net.Listen,它返回一个错误,但它没有被分配。

我正在使用由多人提出的通用?运算符来指示如果错误不为零,则返回其他值为零的错误。 我稍微喜欢一个简短的词而不是运算符,因为我不认为 Go 是一种运算符重的语言,但是如果?进入该语言,我仍然会计算我的奖金而不是踩我的脚一怒。

每个新的 defer 是否覆盖最后一个,以便永远不会调用 annotateError ? 或者它们是否堆叠,以便如果从例如 toServer.Send 返回错误,则 closeOnErr 被调用两次,然后 annotateError 被调用?

它现在像 defer 一样工作: https :

我认为可能令人困惑的一个方面是,在当前的 Go 中,命名参数最终将成为“实际返回的任何内容”,因此您可以执行我所做的操作并获取指向它的指针并传递将它传递给延迟函数并对其进行操作,无论您是否直接返回return errors.New(...)类的值,这在直觉上可能看起来像是一个不是命名变量的“新变量”,但实际上 Go 最终会在延迟运行时将其分配给命名变量。 现在很容易忽略当前 Go 的这个特殊细节。 我认为,虽然现在可能会令人困惑,但如果您甚至在使用此习语的代码库上工作(即,我并不是说这甚至必须成为“Go 最佳实践”,只需几次曝光就可以了) d 很快就搞定了。 因为,再多说一次是为了非常清楚,这就是 Go 已经工作的方式,而不是提议的更改。

这是我认为以前没有人提出过的提案。 使用示例:

 r, !handleError := something()

这个的意思和这个一样:

 r, _xyzzy := something()
 if ok, R := handleError(_xyzzy); !ok { return R }

(其中_xyzzy是一个新变量,其作用域仅扩展到这两行代码,而R可能是多个值)。

这个提议的优点不是特定于错误,不特别处理零值,并且很容易简洁地指定如何在任何特定代码块中包装错误。 语法变化很小。 鉴于直截了当的翻译,很容易理解此功能的工作原理。

缺点是它引入了隐式返回,不能编写一个只返回错误的通用处理程序(因为它的返回值需要基于调用它的函数),并且传递给处理程序的值是在调用代码中不可用。

以下是您可以如何使用它:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

或者,如果您的 init 代码失败,您可以在测试中使用它来调用 t.Fatal:

func TestSomething(t *testing.T) {
  must := func(err error) bool { t.Fatalf("init code failed: %s", err); return true }
  !must := setupTest()
  !must := clearDatabase()
  ...
}

我建议将函数签名更改为func(error) error 。 它简化了大多数情况,如果您需要进一步分析错误,只需使用当前机制。

语法问题:你能定义内联函数吗?

func Read(filename string) error {
    f, !func(err error) error {
        if err != nil { return true, fmt.Errorf("... %s", err) }
        return false, nil
    } := OpenFile(filename)
    /...

我对“不要那样做”很满意,但语法应该允许它减少特殊情况的数量。 这也将允许:

func failed(s string) func(error) error {
    return func(err error) {
       // returns a decorated error with the given string
   }
}

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

这让我觉得这是对这类事情更好的建议之一,至少在将这些东西简洁地放在那里方面。 这是另一种情况,恕我直言,您此时会发出比基于异常的代码更好的错误,这通常无法详细了解错误的性质,因为很容易让异常传播。

出于性能原因,我还建议将 bang 错误函数定义为不在零值上调用。 这使得它们的性能影响相当小; 在我刚刚展示的情况下,如果 Read 正常成功,它并不比当前的读取实现更昂贵,该实现已经在每个错误上执行if并且if子句失败。 如果我们一直在nil上调用一个函数,那么当它不能被内联时,它就会变得非常昂贵,这最终会成为非常重要的时间。 (如果错误正在积极发生,我们几乎可以在任何情况下证明并负担函数调用的合理性(如果您不能回退到当前方法),但对于非错误并不真正想要它。)也意味着 bang 函数可以在它们的实现中假设一个非零值,这也简化了它们。

@thejerf不错,但快乐的道路发生了很大变化。
许多消息之前有人建议使用类似于“或”sintax 的 Ruby - f := OpenFile(filename) or failed("couldn't open file")

额外的问题 - 是针对任何类型的参数还是仅针对错误? 如果仅用于错误 - 那么类型错误必须对编译器具有特殊意义。

@thejerf不错,但快乐的道路发生了很大变化。

我建议区分原始提案的可能通用路径,它看起来像 paulhankin 的原始建议:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

也许即使herr在某处分解,以及我对完全指定它需要什么的探索,这是本次对话的必要条件,以及我自己对如何在我的个人代码中使用它的思考,这只是对建议所启用和提供的其他内容的探索。 我已经说过,内联函数毕竟可能是一个坏主意,但语法应该允许它保持语法简单。 我已经可以编写一个包含三个函数的 Go 函数,并将它们全部内联到调用中。 这并不意味着 Go 被破坏了,或者 Go 需要做一些事情来阻止它; 这意味着如果我重视代码的清晰度,我就不应该这样做。 我喜欢 Go 使代码清晰,但开发人员仍然有一定程度的不可减少的责任来保持代码清晰。

如果你要告诉我“幸福之路”

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

很难阅读,但快乐的道路很容易阅读

func Read(filename string) error {
  f, err := OpenFile(filename)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  b, err := ReadBytes(f)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  err = ProcessBytes(b)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  return nil
}

那么我提交第二个更容易阅读的唯一可能方式是你已经习惯了阅读它并且你的眼睛被训练为完全跳过正确的路径以看到快乐的路径。 我可以猜到,因为我的也是。 但是一旦您习惯了替代语法,您也将接受相关培训。 你应该根据你已经习惯它时的感受来分析语法,而不是你现在的感受。

我还注意到,第二个示例中添加的换行符代表了我的真实代码会发生什么。 当前的 Go 错误处理倾向于添加到代码中的不仅仅是“行”代码,它还添加了许多“段落”,否则应该是一个非常简单的函数。 我想打开文件,读取一些字节并处理它们。 我不想

打开一个文件。

然后读取一些字节,如果可以的话。

然后处理它们,如果可以的话。

我觉得有很多反对票相当于“这不是我习惯的”,而不是对这些将如何在实际代码中发挥作用的实际分析,一旦你习惯了它们并流利地使用它们.

我不喜欢隐藏 return 语句的想法,我更喜欢:

f := OpenFile(filename) or return failed("couldn't open file")
....
func failed(msg string, err error) error { ... } 

在这种情况下or条件转发运算符,
如果它非零,则转发最后一个返回。
在 C# 中有一个类似的提议,使用运算符?>

f := OpenFile(filename) ?> return failed("couldn't open file")

@thejerf在您的案例中通过调用 failed(...) 来预先

使用“shift”键输入的@rodcorsi字符不好恕我直言-(如果您有多个键盘布局)

请不要让这种方式比现在更复杂。 真正在一行(而不是 3 个或更多)中移动相同的代码并不是真正的解决方案。 我个人认为这些提议中的任何一个都不那么可行。 伙计们,数学很简单。 要么接受“Try-catch”的想法,要么保持现状,这意味着很多“if then else”和代码噪音,并不真正适合在像 Fluent Interface 这样的 OO 模式中使用。

非常感谢您提供的所有帮助,也许很少有帮助;-)(开个玩笑)

@KamyarM IMO,“使用最著名的替代方案或根本不做任何更改”并不是一个很有成效的声明。 它阻止了创新并助长了循环论证。

@KernelDeimos我同意你的看法,但我在这个线程上看到很多评论,这些评论本质上是提倡将精确的 4 5 行移动到一行中,我认为这不是真正的解决方案,而且 Go 社区中的许多人非常虔诚地拒绝使用Try-Catch 关闭了任何其他意见的大门。 我个人认为发明这个try-catch概念的人真的考虑过它,虽然它可能有一些缺陷,但那些缺陷只是由不良的编程习惯造成的,即使你删除或限制也没有办法强迫程序员写出好的代码编程语言可能具有的所有优点或某些可能会说不好的功能。
我之前提出过类似的建议,这不完全是 java 或 C# try-catch,我认为它可以支持当前的错误处理和库,我使用了上面的示例之一。 所以基本上编译器会检查每一行之后的错误,如果 err 值设置为非零,则跳转到 catch 块:

try (var err error){ 
    f, err := OpenFile(filename)
    b, err := ReadBytes(f)
    err = ProcessBytes(b)
    return nil
} catch (err error){ //Required
   return err
} finally{ // Optional
    // Do something else like close the file or connection to DB. Not necessary in this example since we  return earlier.
}

@KamyarM
在您的示例中,我怎么知道(在编写代码时)哪个方法返回了错误? 如何实现处理错误的第三种方式(“返回带有附加上下文信息的错误”)?

@urandom
一种方法是使用 Go 开关并在 catch 中查找异常类型。 假设您有 pathError 异常,您知道这是由 OpenFile() 这种方式引起的。 另一种与 GoLang 中的 if err!=nil 错误处理没有太大区别的方法是:

try (var err error){ 
    f, err := OpenFile(filename)
} catch (err error){ //Required
   return err
}
try (var err error){ 
    b, err := ReadBytes(f)
catch (err error){ //Required
   return err
}
try (var err error){ 
    err = ProcessBytes(b)
catch (err error){ //Required
   return err
}
return nil

因此,您可以通过这种方式进行选择,但不受限制。 如果您真的想确切知道哪一行导致了问题,您可以将每一行都放入 try catch 中,就像您现在编写大量 if-then-else 一样。 如果错误对您来说并不重要,并且您想将其传递给调用方方法,该方法在本线程中讨论的示例中实际上是关于该方法的,我认为我提出的代码可以完成这项工作。

@KamyarM我知道你现在来自哪里。 我会说如果有这么多人反对 try...catch,我认为这是 try...catch 并不完美并且有缺陷的证据。 这是一个简单的问题解决方案,但如果 Go2 可以使错误处理比我们在其他语言中看到的更好,我认为那将非常酷。

我认为可以采用 try...catch 的优点而不采用我之前提出的 try...catch 的缺点。 我同意将三行变成 1 或 2 行解决不了任何问题。

在我看来,根本问题是如果逻辑的一部分是“返回给调用者”,函数中的错误处理代码就会重复。 如果您想随时更改逻辑,则必须更改if err != nil { return nil }每个实例。

也就是说,我真的很喜欢try...catch的想法,只要函数不能隐含地throw任何东西。

我认为有用的另一件事是catch {}的逻辑是否需要break关键字来中断控制流。 有时您希望在不破坏控制流的情况下处理错误。 (例如:“为这些数据的每一项做一些事情,向列表中添加一个非零错误并继续”)

@KernelDeimos我完全同意这一点。 我已经看到了这样的确切情况。 在破解代码之前,您需要尽可能多地捕获错误。 如果像 Channels 之类的东西可以用于 GoLang 中那些很好的情况,那么你可以将所有错误发送到 catch 期望的那个通道,然后 catch 可以一一处理它们。

我宁愿将“或返回”与 #19642、#21498 混合使用,而不是使用 try..catch(延迟/恐慌/恢复已经存在;在同一个函数中抛出就像有多个 goto 语句并且在 catch 中使用附加类型开关变得混乱;允许通过在堆栈中使用 try..catch 来忘记错误处理(或者如果范围 try..catch 在单个函数内,则编译器会显着复杂化)

@egorse
似乎 try-catch 语法@KamyarM暗示的是一些用于处理错误返回变量的语法糖,而不是对异常的介绍。 虽然出于各种原因我更喜欢“或返回”类型的语法,但它似乎是一个合法的建议。

话虽如此, @KamyarM ,为什么try有一个变量定义部分? 您正在定义一个err变量,但它被块本身内的其他err变量遮蔽。 它的目的是什么?

我认为是告诉它要关注什么变量,让它与error类型解耦。 它可能需要更改影子规则,除非您只需要人们非常小心。 不过,我不确定catch块中的声明。

@egorse @DeedleFake提到的正是它的目的。 这意味着 try 块关注该对象。 它也限制了它的范围。 它类似于 C# 中的 using 语句。 在 C# 中,一旦执行该块,就会自动处理通过 using 关键字定义的对象,并且这些对象的范围仅限于“Using”块。
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

使用 catch 是必要的,因为我们想强制程序员决定如何正确地处理错误。 在 C# 和 Java 中,catch 也是强制性的。 在 C# 中,如果您不想处理异常,则根本不会在该函数中使用 try-catch。 当异常发生时,调用层次结构中的任何方法都可以处理该异常,甚至可以再次抛出(或将其包装在另一个异常中)它。 我不认为你可以在 Java 中做同样的事情。 在 Java 中,可能抛出异常的方法需要在函数签名中声明。

我想强调的是,这个 try-catch 块不是准确的。 我使用这些关键字是因为这与它想要实现的目标相似,而且这也是许多程序员熟悉并在大多数编程概念课程中教授的内容。

可能有一个 _return on error_ 赋值,它只有在有一个 _named 错误返回参数_时才有效,如下所示:

func process(someInput string) (someOutput string, err error) {
    err ?= otherAction()
    return
}

如果err不是nil则返回。

我认为这个关于向 Rust 添加try糖的讨论将对本次讨论的参与者有所启发。

FWIW,关于简化错误处理的旧思想(如果这是废话,请道歉):

由插入符号^表示的加注标识符可以用作赋值左侧的操作数之一。 出于赋值的目的, raise 标识符是包含函数的最后一个返回值的别名,无论该值是否具有名称。 赋值完成后,该函数根据其类型的零值(nil、0、false、"")测试最后一个返回值。 如果它被认为是零,则函数继续执行,否则返回。

raise 标识符的主要目的是在给定的上下文中将错误从被调用函数简洁地传播回调用者,而不隐藏正在发生的事实。

例如,请考虑以下代码:

func Alpha() (string, error) {

    b, ^ := beta()
    g, ^ := gamma()
    return b + g, nil
}

这大致相当于:

func Alpha() (ret1 string, ret2 error) {

    b, ret2 := beta()
    if ret2 != nil {
        return
    }

    g, ret2 := gamma()
    if ret2 != nil {
        return
    }

    return b + g, nil
}

如果出现以下情况,则程序格式错误:

  • 在赋值中多次使用 raise 标识符
  • 该函数不返回值
  • 最后一个返回值的类型没有有意义且有效的零测试

这个建议与其他建议相似,因为它没有解决提供更多上下文信息的问题,这是值得的。

@gboyle这就是为什么应该命名 IMO 最后一个返回值并且类型error 。 这有两个重要意义:

1 - 其他返回值也被命名,因此
2 - 他们已经有有意义的零值。

@object88正如context包的历史向我们展示的那样,这需要核心团队的一些行动,比如定义一个内置的error类型(只是一个普通的 Go error )具有一些通用属性(消息?调用堆栈?等等)。

AFAIK 在 Go 中没有太多上下文语言结构。 除了godefer ,没有其他的,甚至这两个都非常明确和清晰(语法上的机器人 - 以及眼睛 - 和语义)。

像这样的事情怎么办?

(复制了一些我正在处理的真实代码):

func (g *Generator) GenerateDevices(w io.Writer) error {
    var err error
    catch err {
        _, err = io.WriteString(w, "package cc\n\nconst (") // if err != nil { goto Caught }
        for _, bd := range g.zwClasses.BasicDevices {
            _, err = w.Write([]byte{'\t'}) // if err != nil { goto Caught }
            _, err = io.WriteString(w, toGoName(bd.Name)) // if err != nil { goto Caught }
            _, err = io.WriteString(w, " BasicDeviceType = ") // if err != nil { goto Caught }
            _, err = io.WriteString(w, bd.Key) // if err != nil { goto Caught }
            _, err = w.Write([]byte{'\n'}) // if err != nil { goto Caught }
        }
        _, err = io.WriteString(w, ")\n\nvar BasicDeviceTypeNames = map[BasicDeviceType]string{\n") // if err != nil { goto Caught }
       // ...snip
    }
    // Caught:
    return err
}

err非零时,它会中断到“catch”语句的末尾。 您可以使用“catch”将通常返回相同类型错误的类似调用组合在一起。 即使调用不相关,您也可以事后检查错误类型并适当地包装它。

@lukescott阅读了@robpike https://blog.golang.org/errors-are-values的这篇博

@davecheney catch 的想法(没有尝试)保持了这种情绪的精神。 它将错误视为一个值。 当值不再为零时,它只是中断(在同一函数内)。 它不会以任何方式使程序崩溃。

@lukescott你今天可以使用 Rob 的技术,你不需要改变语言。

异常和错误之间有相当大的区别:

  • 错误是预期的(我们可以为它们编写测试),
  • 不希望出现异常(因此称为“异常”),

许多语言将两者都视为例外。

在泛型和更好的错误处理之间,我会选择更好的错误处理,因为 Go 中的大多数代码混乱都来自错误处理。 虽然可以说这种冗长是好的并且有利于简单,但 IMO 也将工作流的 _happy path_ 模糊到了模棱两可的程度。

我想以@thejerf的提议为

首先,不是! ,而是引入了or运算符,该操作符是左侧函数调用的最后一个返回参数,并调用右侧的 return 语句,其表达式为一个被调用的函数,如果移位的参数非零(错误类型为非零),则传递该参数。 如果人们认为也应该仅用于错误类型,那很好,尽管我觉得这个构造对于返回布尔值作为最后一个参数的函数也很有用(可以/不可以)。

Read 方法将如下所示:

func Read(filename string) error {
  f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)
  b := ReadBytes(f) or return errors.Contextf("reading file %s", filename)
  ProcessBytes(b) or return errors.Context("processing data")
  return nil
}

我假设错误包提供了如下方便的功能:

func Noop() func(error) error {
   return func(err error) {
       return err   
   }
}


func Context(msg string) func(error) error {
    return func(err error) {
        return fmt.Errorf("%s: %v", msg, err)
    }
}
...

这看起来完全可读,同时涵盖了所有必要的点,而且由于熟悉 return 语句,它看起来也不太陌生。

@urandom在这个语句中f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)怎么知道原因? 例如是缺少读取权限还是文件根本不存在?

@dc0d
好吧,即使在上面的例子中,原始错误也包括在内,因为用户给出的消息只是添加了上下文。 如前所述,并从原始提案派生而来, or return期望一个函数接收移位类型的单个参数。 这是关键,不仅允许使用适合大量人群的实用函数,而且如果您需要对特定值进行真正的自定义处理,您几乎可以编写自己的函数。

@urandom IMO 它隐藏了太多。

我的 2 美分在这里,我想提出一个简单的规则:

“函数的隐式结果错误参数”

对于任何函数,在结果参数列表的末尾隐含一个错误参数
如果没有明确定义。

为了讨论,假设我们有一个如下定义的函数:

func f() (int) {}
等同于: func f() (int, error) {}
根据我们的隐式结果错误规则。

对于赋值,您可以冒泡、忽略或捕获错误,如下所示:

1)冒泡

x := f()

如果 f 返回错误,当前函数将立即返回错误
(或创建一个新的错误堆栈?)
如果当前函数是 main,程序将停止。

它等效于以下代码片段:

x, 错误 := f()
如果错误!= nil {
返回...,错误
}

2)忽略

x, _ := f()

赋值表达式列表末尾的空白标识符,以明确表示丢弃错误。

3)抓

x, 错误 := f()

错误必须照常处理。

我相信这种惯用的代码约定更改只需要对编译器进行最少的更改
或者预处理器应该完成这项工作。

@dc0d你能举一个例子说明它隐藏了什么,以及如何隐藏?

@urandom这是导致问题“原始错误在哪里?”的原因,正如我在之前的评论中所问的那样。 它隐式地传递错误并且不清楚(例如)放置在此行中的原始错误在哪里: f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)OpenFile()返回的原始错误 - 可能是缺少读取权限或文件不存在,而不仅仅是“文件名有问题”。

@dc0d
我不同意。 它与处理 http.Handlers 一样清晰,稍后您将它们传递给某个多路复用器,然后突然您得到一个请求和一个响应编写器。 人们已经习惯了这种行为。 人们如何知道go语句的作用? 它在第一次遇到时显然不清楚,但非常普遍并且在语言中。

我不认为我们应该反对任何提案,理由是它是新的,没有人知道它是如何工作的,因为对大多数提案来说都是如此。

@urandom现在更有意义了(包括http.Handler示例)。

我们正在讨论事情。 我不反对或支持任何特定的想法。 但我确实支持简单和明确,同时传达关于开发人员体验的一些理智。

@dc0d

这可能是因为缺少读取权限或文件不存在

在这种情况下,您不仅要重新抛出错误,还要检查它的实际内容。 对我来说,这个问题是关于涵盖最流行的案例。 也就是说,使用添加的上下文重新抛出错误。 只有在更罕见的情况下,您才会将错误转换为某种具体类型并检查其实际内容。 并且对于当前的错误处理语法非常好,即使这里的一项提议被接受也不会去任何地方。

@creker错误不是例外(我之前的一些评论)。 错误是值,因此不可能抛出或重新抛出它们。 对于类似 try/catch 的场景,Go 有恐慌/恢复。

@dc0d我不是在谈论例外。 重新抛出是指将错误返回给调用者。 建议的or return errors.Contextf("opening file %s", filename)基本上包装并重新抛出错误。

@creker感谢您的解释。 它还添加了一些额外的函数调用,这些调用会影响调度程序,而调度程序在某些情况下可能不会产生所需的行为。

@dc0d这是一个实现细节,将来可能会改变。 它实际上可能会改变,非合作抢占现在正在进行中。

@creker
我认为您可以涵盖更多情况,而不仅仅是返回修改后的错误:

func retryReadErrHandler(filename string, count int) func(error) error {
     return func(err error) error {
          if os.IsTimeout(err) {
               count++
               return Read(filename, count)
          }
          if os.IsPermission(err) {
               log.Fatal("Permission")
          }

          return fmt.Errorf("opening file %s: %v", filename, err)
      }
}

func Read(filename string, count int) error {
  if count > 3 {
    return errors.New("max retries")
  }

  f := OpenFile(filename) or return retryReadErrHandler(filename, count)

  ...
}

@dc0d
额外的函数调用可能会被编译器内联

@urandom看起来很有趣。 隐式参数有点神奇,但这个参数实际上可以概括和简洁,足以涵盖所有内容。 只有在极少数情况下,您才不得不求助于常规的if err != nil

@urandom ,我对你的例子感到困惑。 为什么retryReadErrHandler返回一个函数?

@object88
这就是or return运算符背后的想法。 它需要一个函数,如果从左侧返回的最后一个非零参数将调用该函数。 在这方面,它的行为与 http.Handler 完全相同,将如何处理参数及其返回(或请求及其响应,在处理程序的情况下)的实际逻辑留给回调。 并且为了在回调中使用您自己的自定义数据,您创建了一个包装函数,该函数接收该数据作为参数并返回预期的内容。

或者用更熟悉的术语来说,它类似于我们通常对处理程序所做的事情:
```去
func nodesHandler(repo Repo) http.Handler {
返回 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
数据,_ := json.Marshal(repo.GetNodes())
w.Write(数据)
})
}

@urandom ,您可以通过将 LHS 保留为与今天相同并将or ... return更改returnif (cond)来避免一些魔法:

func Read(filename string) error {
   f, err := OpenFile(filename) returnif(err != nil) errors.Contextf(err, "opening file %s", filename)
   b, err := ReadBytes(f) returnif(err != nil) errors.Contextf(err, "reading file %s", filename)
   err = ProcessBytes(b) returnif(err != nil) errors.Context(err, "processing data")
   return nil
}

这提高了左侧错误值和右侧触发条件的通用性和透明度。

我看到这些不同的提议越多,我就越倾向于只进行政府变更。 语言已经拥有了强大的力量,让我们让它更易于扫描。 @billyh ,不要特别挑你的建议,但returnif(cond) ...只是重写if cond { return ...}一种方式。 为什么我们不能只写后者? 我们已经知道这意味着什么。

x, err := foo()
if err != nil { return fmt.Errorf(..., err) }

甚至

if x, err := foo(); err != nil { return fmt.Errorf(..., err) }

或者

x, err := foo(); if err != nil { return fmt.Errorf(..., err) }

没有新的魔法关键字或语法或运算符。

(如果我们还修复 #377 以增加使用:=灵活性,这可能会有所帮助。)

@randall77我也越来越倾向于这种方式。

@randall77该块在什么时候会被换行?

与此处提出的替代方案相比,上述解决方案更令人满意,但我不相信它比不采取任何行动要好。 Gofmt 应该尽可能具有确定性。

@as ,我还没有完全考虑if语句的主体包含单个return语句,则if语句的格式为一行。”

也许需要对if条件进行额外限制,例如它必须是布尔变量或两个变量或常量的二元运算符。

@billyh
我认为没有必要让它变得更冗长,因为我没有看到or中的那一点魔法带来的任何混乱。 我认为与@as不同的

@randall77
你的建议更符合代码风格的建议,这就是 go 高度自以为是的地方。 突然间有 2 种格式的 if 语句可能在整个社区中表现不佳。

更不用说像这样的衬垫更难阅读了。 if != ; { }甚至在几行中也太多了,因此这个提议。 该模式对于几乎所有情况都是固定的,并且可以变成易于阅读和理解的语法糖。

我对大多数这些建议的问题是不清楚发生了什么。 在开篇文章中,建议重新使用||来返回错误。 我不清楚那里正在发生回归。 我认为如果要发明新的语法,它需要符合大多数人的期望。 当我看到||我不希望有回报,甚至不会中断执行。 这对我来说很刺耳。

我确实喜欢 Go 的“错误就是价值”的观点,但我也同意if err := expression; err != nil { return err }过于冗长,主要是因为几乎每个调用都会返回错误。 这意味着您将拥有很多这些,并且很容易弄乱它,这取决于 err 声明(或隐藏)的位置。 它发生在我们的代码中。

由于 Go 不使用 try/catch 并在“异常”情况下使用 panic/defer,我们可能有机会重用 try 和/或 catch 关键字来缩短错误处理时间而不会使程序崩溃。

这是我的一个想法:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
}

这个想法是你在 LHS 上用关键字try前缀err try 。 如果 err 非零,则立即返回。 您不必在这里使用 catch ,除非返回不完全满意。 这更符合人们对“尝试中断执行”的期望,但它不会使程序崩溃,它只会返回。

如果返回不完全满足(编译时检查)或者我们想要包装错误,我们可以使用 catch 作为特殊的仅向前标签,如下所示:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
catch:
    return &CustomError{"some detail", err}
}

这也允许您检查和忽略某些错误:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, err = io.WriteString(w, "bar")
        if err == io.EOF {
            err = nil
        } else {
            goto catch
        }
    return
catch:
    return &CustomError{"some detail", err}
}

也许您甚至可以让try指定标签:

func WriteFooBar(w io.Writer) (err error) {
    _, try(handle1) err = io.WriteString(w, "foo")
    _, try(handle2) err = w.Write([]byte{','})
    _, try(handle3) err = io.WriteString(w, "bar")
    return
handle1:
    return &CustomError1{"...", err}
handle2:
    return &CustomError2{"...", err}
handle3:
    return &CustomError3{"...", err}
}

我意识到我的代码示例有点糟糕(foo/bar,ack)。 但希望我已经通过符合/反对现有期望来说明我的意思。 我也可以完全按照 Go 1 中的方式保留错误。但是如果发明了一种新语法,则需要仔细考虑该语法是如何被感知的,而不仅仅是在 Go 中。 很难发明一种新的语法而没有它已经意味着什么,所以通常最好遵循现有的期望而不是反对它们。

也许某种链接,比如你如何链接方法而不是错误? 我不确定那会是什么样子,或者它是否会起作用,只是一个疯狂的想法。
您现在可以通过在结构中维护某种错误值并在链的末尾提取它来对链进行排序以减少错误检查的数量。

这是一个非常奇怪的情况,因为虽然有一些样板,但我不太确定如何进一步简化它同时仍然有意义。

@thejerf的示例代码与@lukescott的提议类似:

func NewClient(...) (*Client, error) {
    listener, try err := net.Listen("tcp4", listenAddr)
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    return nil, err
}

它从 59 行减少到 47 行。

这是相同的长度,但我认为它比使用defer更清楚一点:

func NewClient(...) (*Client, error) {
    var openedListener, openedConn bool
    listener, try err := net.Listen("tcp4", listenAddr)
    openedListener = true

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    openedConn = true

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    if openedConn {
        conn.Close()
    }
    if openedListener {
        listener.Close()
    }
    return nil, err
}

使用deferifnotnil或其他东西可能更容易理解该示例。
但是,如果这些建议中的许多内容都涉及到这一点的话,那有点可以追溯到整条线。

稍微玩过示例代码后,我现在反对try(label) name变体。 我想如果你有很多花哨的事情要做,只需使用if err != nil { ... }的当前系统。 如果您正在做基本相同的事情,例如设置自定义错误消息,您可以这样做:

func WriteFooBar(w io.Writer) (err error) {
    msg := "thing1 went wrong"
    _, try err = io.WriteString(w, "foo")
    msg = "thing2 went wrong"
    _, try err = w.Write([]byte{','})
    msg = "thing3 went wrong"
    _, try err = io.WriteString(w, "bar")
    return nil

catch:
    return &CustomError{msg, err}
}

如果有人使用过 Ruby,这看起来很像他们的rescue语法,我认为它读起来相当不错。

可以做的一件事是使nil为假值,其他值评估为真,所以你最终得到:

err := doSomething()
if err { return err }

但我不确定这是否真的有效,它只会减少几个字符。
我打错了很多东西,但我想我从来没有打错过!= nil

之前已经提到过使接口为真/假,我说它会使带有标志的错误更常见:

verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
if verbose { ... } // should be *verbose!

@carlmjohnson ,在您上面提供的示例中,错误消息散布着happy-path 代码,这对我来说有点奇怪。 如果您需要格式化这些字符串,那么无论是否出错,您都需要做很多额外的工作:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    msg := fmt.Sprintf("While writing %s, thing1 went wrong", f.foo)
    _, try err = io.WriteString(w, f.foo)
    msg = fmt.Sprintf("While writing %s, thing2 went wrong", f.separator)
    _, try err = w.Write(f.separator)
    msg = fmt.Sprintf("While writing %s, thing3 went wrong", f.bar)
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    return &CustomError{msg, err}
}

@object88 ,我认为 SSA 分析应该能够确定是否未使用某些分配,并在不需要时将它们重新安排为不发生(太乐观了?)。 如果这是真的,这应该是有效的:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    var format string, args []interface{}

    msg = "While writing %s, thing1 went wrong", 
    args = []interface{f.foo}
    _, try err = io.WriteString(w, f.foo)

    format = "While writing %s, thing2 went wrong"
    args = []interface{f.separator}
    _, try err = w.Write(f.separator)

    format = "While writing %s, thing3 went wrong"
    args = []interface{f.bar}
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    msg := fmt.Sprintf(format, args...)
    return &CustomError{msg, err}
}

这会合法吗?

func Foo() error {
catch:
    try _ = doThing()
    return nil
}

我认为应该循环直到doThing()返回 nil,但我可以确信否则。

@卡姆约翰逊

稍微玩过示例代码后,我现在反对 try(label) 名称变体。

是的,我不确定语法。 我不喜欢它,因为它使 try 看起来像一个函数调用。 但我可以看到指定不同标签的价值。

这会合法吗?

我会说是的,因为 try 应该是前向的。 如果你想这样做,我会说你需要这样做:

func Foo() error {
tryAgain:
    if err := doThing(); err != nil {
        goto tryAgain
    }
    return nil
}

或者像这样:

func Foo() error {
    for doThing() != nil {}
    return nil
}

@Azareal

可以做的一件事是使 nil 为假值,而其他值的计算结果为真,因此您最终会得到: err := doSomething() if err { return err }

我认为缩短它是有价值的。 但是,我认为它不应该在所有情况下都适用于 nil。 也许会有这样的新界面:

interface Truthy {
  True() bool
}

然后可以按照您的建议使用任何实现此接口的值。

只要错误实现了接口,这就会起作用:

err := doSomething()
if err { return err }

但这行不通:

err := doSomething()
if err == true { return err } // err is not true

我对 golang 真的很陌生,但是您如何看待像下面这样引入条件委托人?

func someFunc() error {

    errorHandler := delegator(arg1 Arg1, err error) error if err != nil {
        // ...
        return err // or modifiedErr
    }

    ret, err := doSomething()
    delegate errorHandler(ret, err)

    ret, err := doAnotherThing()
    delegate errorHandler(ret, err)

    return nil
}

委托人的功能类似于东西,但是

  • 它的return表示return from its caller context 。 (返回类型必须与调用者相同)
  • 它可以在{ if之前选择{ ,在上面的例子中它是if err != nil
  • 它必须由调用者使用delegate关键字委托

它可能可以省略delegate来委托,但我认为它很难阅读函数的流程。

也许它不仅对错误处理有用,但我现在不确定。

添加check很漂亮,但你可以在返回之前做更多的事情:

result, err := openFile(f);
if err != nil {
        log.Println(..., err)
    return 0, err 
}

变成

result, err := openFile(f);
check err

```去
结果,错误:= openFile(f);
检查错误{
log.Println(..., 错误)
}

```Go
reslt, _ := check openFile(f)
// If err is not nil direct return, does not execute the next step.

```去
结果,错误:= 检查 openFile(f) {
log.Println(..., 错误)
}

It also attempts simplifying the error handling (#26712):
```Go
result, err := openFile(f);
check !err {
    // err is an interface with value nil or holds a nil pointer
    // it is unusable
    result.C...()
}

它还尝试简化(通过一些被认为是乏味的)错误处理 (#21161)。 它会变成:

result, err := openFile(f);
check err {
   // handle error and return
    log.Println(..., err)
}

当然,如果更清楚,您可以使用try和其他关键字代替check

reslt, _ := try openFile(f)
// If err is not nil direct return, does not execute the next step.

```去
结果,错误:= openFile(f);
尝试错误{
// 处理错误并返回
log.Println(..., 错误)
}

Reference:

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)
check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

If a function just returns an error, it can be used inline with check, so
```Go
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

变成

check UpdateDB()

对于具有多个返回值的函数,您需要进行赋值,就像我们现在所做的那样。

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

变成

a, b, err := Foo()
check err

// use a and b

第二种形式(检查 A、B)
检查 A,B 评估 A。如果为零,则不执行任何操作。 如果不是 nil,检查就像一个 return {}*, B.

这是为了错误修饰的需要。 我们仍然检查 A,但在隐式返回中使用的是 B。

例子

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

变成

a, err := Foo()
check err, &BarError{"Bar", err}

笔记
这是一个编译错误

对不评估为错误的事情使用 check 语句
使用 check in 返回值不是 { type }*, error 形式的函数
两个expr形式检查A,B是否短路。 如果 A 为零,则不评估 B。

实用性注意事项
有对修饰错误的支持,但只有当您确实需要修饰错误时,您才需要为笨拙的检查 A、B 语法付费。

对于if err != nil { return nil, nil, err }样板文件(这很常见),检查 err 尽可能简短,但不会牺牲清晰度(请参阅下面的语法注释)。

语法注意事项
我认为这种语法(check ..,在行的开头,类似于返回)是一种消除错误检查样板而不隐藏隐式返回引入的控制流中断的好方法。

像这样的想法的缺点||抓住上面,还是 a, b = foo()? 在另一个线程中提出,他们以一种使流程更难以遵循的方式隐藏控制流修改; 前者带有 ||机器附加在一个看起来很普通的行的末尾,后者带有一个小符号,可以出现在任何地方,包括在一行普通代码的中间和末尾,可能多次出现。

check 语句将始终是当前块中的顶级语句,与修改控制流的其他语句(例如,提前返回)具有相同的重要性。

这是另一个想法。

想象一个again语句,它定义了一个带有标签的宏。 它标记的语句语句可以通过稍后在函数中的文本替换再次扩展(让人想起 const/iota,带有 goto :-] 的阴影)。

例如:

func(foo int) (int, error) {
    err := f(foo)
again check:
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    check
    x, err := h()
    check
    return x, nil
}

将完全等同于:

func(foo int) (int, error) {
    err := f(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    x, err := h()
    if err != nil {
        return 0, errors.Wrap(err)
    }
    return x, nil
}

请注意,宏扩展没有参数 - 这意味着它是一个宏这一事实应该减少混淆,因为编译器不喜欢它们自己的符号

与 goto 语句一样,标签的作用域在当前函数内。

有趣的想法。 我喜欢 catch 标签的想法,但我认为它不适合 Go 范围(对于当前的 Go,你不能goto一个在其范围内定义了新变量的标签)。 again想法解决了这个问题,因为标签出现在引入新范围之前。

这里又是一个超级例子:

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
    catch {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, try err = net.Listen("tcp4", listenAddr)

    conn, try err = ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

这是一个更接近 Rog 提议的版本(我不太喜欢它):

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
again:
    if err != nil {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, err = net.Listen("tcp4", listenAddr)
    check

    conn, err = ConnectionManager{}.connect(server, tlsConfig)
    check

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    check

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    check

    session, err := communicationProtocol.FinalProtocol(conn)
    check
    client.session = session

    return client, nil
}

@carlmjohnson作为记录,这不是我的建议。 “check”标识符并不特殊——它需要通过把它放在“again”关键字之后来声明。

另外,我建议上面的例子并没有很好地说明它的用法 - 上面再次标记的语句中没有任何东西不能在 defer 语句中完成。 在 try/catch 示例中,该代码不能(例如)使用有关错误返回的源位置的信息来包装错误。 如果您在其中一个 if 语句中添加“try”(例如检查 GetRuntimeEnvironment 返回的错误),它也不会工作 AFAICS,因为 catch 语句引用的“err”与那个范围不同在块内声明。

我认为我对check关键字的唯一问题是,函数的所有退出都应该是return (或者至少有 _some_ 种“我要离开函数”的含义)。 我们_可能_得到become (对于 TCO),至少become有某种“我们正在成为一个不同的功能”......但“检查”这个词听起来真的不像这将是该功能的出口。

函数的退出点非常重要,我不确定check真的有那种“退出点”的感觉。 除此之外,我真的很喜欢check的想法,它允许更紧凑的错误处理,但仍然允许以不同的方式处理每个错误,或者按照您的意愿包装错误。

我也可以添加建议吗?
像这样的事情怎么样:

func Open(filename string) os.File onerror (string, error) {
       f, e := os.Open(filename)
       if e != nil { 
              fail "some reason", e // instead of return keyword to go on the onerror 
       }
      return f
}

f := Open(somefile) onerror reason, e {
      log.Prinln(reason)
      // try to recover from error and reasign 'f' on success
      nf = os.Create(somefile) onerror err {
             panic(err)
      }
      return nf // return here must return whatever Open returns
}

错误分配可以有任何形式,甚至是愚蠢的

f := Open(name) =: e

或者在错误的情况下返回一组不同的值,而不仅仅是错误,还有一个 try catch 块会很好。

try {
    f := Open("file1") // can fail here
    defer f.Close()
    f1 := Open("file2") // can fail here
    defer f1.Close()
    // do something with the files
} onerror err {
     log.Println(err)
}

@cthackers我个人认为 Go 中的错误没有特殊处理是非常好的。 它们只是价值观,我认为它们应该保持这种状态。

此外,try-catch(和类似的结构)只是围绕着鼓励不良实践的不良结构。 每个错误都应该单独处理,而不是由某些“捕获所有”错误处理程序处理。

https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
这太复杂了。

我的想法: |err|表示检查错误:if err!=nil {}

// common util func
func parseInt(s string) (i int64, err error){
    return strconv.ParseInt(s, 10, 64)
}

// expression check err 1 : check and use err variable
func parseAndSum(a string ,b string) (int64,error) {
    sum := parseInt(a) + parseInt(b)  |err| return 0,err
    return sum,nil
} 

// expression check err 2 : unuse variable 
func parseAndSum(a string , b string) (int64,error) {
    a,err := parseInt(a) |_| return 0, fmt.Errorf("parseInt error: %s", a)
    b,err := parseInt(b) |_| { println(b); return 0,fmt.Errorf("parseInt error: %s", b);}
    return a+b,nil
} 

// block check err 
func parseAndSum(a string , b string) (  int64,  error) {
    {
      a := parseInt(a)  
      b := parseInt(b)  
      return a+b,nil
    }|err| return 0,err
} 

@chen56和所有未来的评论者:请参阅https://go.googlesource.com/proposal/+/master/design/go2draft.md

我怀疑这现在已经过时了这个线程,在这里继续下去几乎没有意义。 Wiki 反馈页面是未来可能发生的事情。

使用 Go 2 提案的超级示例:

func NewClient(...) (*Client, error) {
    var (
        listener net.Listener
        conn     net.Conn
    )
    handle err {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener = check net.Listen("tcp4", listenAddr)

    conn = check ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    check toServer.Send(&client.serverConfig)

    check toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session := check communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

我认为这与我们所希望的一样干净。 handle块具有again标签或 Ruby 的rescue关键字的优良品质。 我脑子里剩下的唯一问题是是否使用标点符号或关键字(我认为是关键字)以及是否允许在不返回错误的情况下获取错误。

我试图理解这个提议——似乎每个函数只有一个句柄块,而不是能够在整个函数执行过程中针对不同的错误创建不同的响应。 这似乎是一个真正的弱点。

我还想知道我们是否也忽略了在我们的系统中开发测试工具的关键需求。 考虑我们将如何在测试期间执行错误路径应该是讨论的一部分,但我也没有看到,

@sdwarwick我认为这不是讨论https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md 中描述的设计草案的最佳场所。 更好的方法是在https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback的 wiki 页面上添加指向文章的链接。

也就是说,该设计草案确实允许在一个函数中使用多个句柄块。

这个问题最初是一个具体的提案。 我们不会采纳那个提议。 关于这个问题已经有很多很好的讨论,我希望人们将好的想法拿出来单独的提案和最近的设计草案的讨论。 我要结束这个问题。 谢谢大家的讨论。

如果要在这些例子中说话:

r, err := os.Open(src)
    if err != nil {
        return err
    }

我想在一行中写成大约这样:

r, err := os.Open(src) try ("blah-blah: %v", err)

而不是“尝试”,输入任何漂亮和合适的词。

使用这样的语法,错误将返回,其余的将是一些取决于类型的默认值。 如果我需要返回一个错误和其他特定的东西,而不是默认的,那么没有人会取消经典的 more multi-line 选项。

甚至更短(不添加某种错误处理):

r, err := os.Open(src) try

)
PS请原谅我的英语))

我的变种:

func CopyFile(src, dst string) string, error {
    r := check os.Open(src) // return nil, error
    defer r.Close()

    // if error: run 1 defer and retun error message
    w := check os.Create(dst) // return nil, error
    defer w.Close()

    // if error: run 2, 1 defer and retun error message
    if check io.Copy(w, r) // return nil, error

}

func StartCopyFile() error {
  res := check CopyFile("1.txt", "2.txt")

  return nil
}

func main() {
  err := StartCopyFile()
  if err!= nil{
    fmt.printLn(err)
  }
}

你好,

我有一个简单的想法,它松散地基于错误处理在 shell 中的工作方式,就像最初的提议一样。 在 shell 中,错误是通过不等于零的返回值来传达的。 最后一个命令/调用的返回值存储在 $? 在外壳中。 除了用户给定的变量名称之外,我们还可以自动将最新调用的错误值存储在预定义变量中,并使其可以通过预定义语法进行检查。 我选了? 作为引用最新错误值的语法,该值已从当前范围内的函数调用返回。 我选了! 作为 if 的简写? != 无 {}。 的选择? 受外壳的影响,也是因为它看起来有意义。 如果发生错误,您自然会对发生的事情感兴趣。 这是提出一个问题。 ? 是提出问题的常见标志,因此我们使用它来引用在同一范围内生成的最新错误值。
! 用作 if 的简写? != nil,因为它表示必须注意万一出现问题。 ! 意思是:如果出现问题,请执行此操作。 ? 引用最新的错误值。 像往常一样, ? 如果没有错误,则等于 nil。

val, err := someFunc(param)
! { return &SpecialError("someFunc", param, ?) }

为了使语法更吸引人,我允许放置 ! 直接在调用后面的行以及省略大括号。
有了这个提议,您还可以在不使用程序员定义的标识符的情况下处理错误。

这将被允许:

val, _ := someFunc(param)
! return &SpecialError("someFunc", param, ?)

这将被允许

val, _ := someFunc(param) ! return &SpecialError("someFunc", param, ?)

在这个提议下你不必在发生错误时从函数中返回
您可以尝试从错误中恢复。

val, _ := someFunc(param)
! {
val, _ := someFunc(paramAlternative)
  !{ return &SpecialError("someFunc alternative try failed too", paramAlternative, ?) }}

在这个建议下你可以使用! 在 for 循环中进行多次重试。

val, _ := someFunc(param)
for i :=0; ! && i <5; i++ {
  // Sleep or make a change or both
  val, _ := someFunc(param)
} ! { return &SpecialError("someFunc", param, ? }

我知道! 主要用于否定表达式,因此建议的语法可能会导致初学者的混淆。 这个想法是! 本身扩展到? != nil 在条件表达式中使用时,如上例所示,它不附加到任何特定表达式。 上面的 for 行不能用当前的 go 编译,因为没有上下文就没有任何意义。 根据这个提议! 本身就是真的,当最近的函数调用发生错误时,可以返回错误。

保留返回错误的 return 语句,因为正如其他人在此处评论的那样,需要一目了然地查看您的函数返回的位置。 您可以在错误不需要您离开函数的情况下使用此语法。

这个提议比其他一些提议更简单,因为没有努力创建类似于其他语言已知语法的 try 和 catch 块的变体。 它保持了 go 的当前理念,即在错误发生的地方直接处理错误,并使其更简洁。

@tobimensch请将新建议发布到要点,并在Go 2 错误处理反馈 wiki 中链接。 可能会忽略有关此已关闭问题的帖子。

如果您还没有看过它,您可能想阅读Go 2 错误处理草案设计

您可能对 Go 2 错误处理的需求考虑感兴趣。

现在指出可能有点晚了,但是任何感觉像 javascript 魔法的东西都会困扰我。 我说的是||运算符,它应该以某种方式神奇地与error接口一起工作。 我不喜欢。

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