建议:Go 2:通过有限的模式匹配实现更干净的错误处理的快乐路径

woobm2wo  于 6个月前  发布在  Go
关注(0)|答案(5)|浏览(45)

Go编程经验

有经验的

其他语言经验

JavaScript, Elixir, Kotlin, Dart, Ruby

相关想法

  • 这个想法或者类似的想法之前有人提出过吗?
  • 这会影响错误处理吗?
  • 这是关于泛型的吗?
  • 这个改变是向后兼容的吗?打破Go 1兼容性保证是一个很大的代价,需要很大的好处

这个想法或者类似的想法之前有人提出过吗?

我认为没有,但考虑到这些年来有多少关于错误处理的建议,这是有可能的。

这会影响错误处理吗?

是的。它的区别在于它不尝试以神奇的方式处理错误,而是引入一种新的语法,可以用于几种不同类型的常见数据处理。

这是关于泛型的吗?

不是。

建议

先例

在Elixir中,模式匹配通常被用作处理错误的方法。能够返回错误值的函数通常会返回一个形式为 {:error, error_string} 的元组,当它们没有失败时,通常会返回 {:ok, result} 。这导致了这样的代码:

{:ok, result} = thing_that_can_fail()

这段代码检查第一个元素是否为 :ok ,并将第二个元素赋给一个新的变量 result 。如果模式不匹配,程序将崩溃。然而,当你想要处理错误时,你通常会使用类似 case 表达式的东西,这允许检查多种可能的模式:

case thing_that_can_fail() do
  {:ok, result} -> use_result(result)
  {:error, err} -> handle_error(err)
end

这没问题,直到你需要连续做很多事情,其中几个都可能会失败:

case thing_that_can_fail() do
  {:ok, result} -> case other_thing_that_can_fail() do
                              {:ok, result2} -> use_results(result, result2)
                              {:error, err} -> handle_error(err)
                            end
  {:error, err} -> handle_error(err)
end

为了帮助解决这个问题,Elixir有一个 with 表达式,它允许在一行内处理多个模式匹配。以下代码与之前的代码功能相同:

with {:ok, result} <- thing_that_can_fail(),
         {:ok, result2} <- other_thing_that_can_fail() do
           use_results(result, result2)
else
  {:error, err} -> handle_error(err)
end

建议

我建议采用上面第二种语法的变体 with ,使其更像Go语言,并用它来实现重复、近乎相同的错误处理的去重。这可以通过添加一个新的关键字来实现,虽然我不确定具体是什么。为了说明,我将使用 with 来匹配Elixir代码,但实际上并不需要这样。通常情况下,新关键字在向后兼容性方面会是个问题,但应该可以从上下文中判断出这里是否应该将其视为关键字。只有在紧接着 { 并作为语句的第一部分出现时才允许使用这个关键字。在其他所有情况下,该关键字都将被视为标识符。我不知道这本身是否太复杂了,但如果是的话,也许还有其他替代语法可以避免这些问题。我没有想过其他的替代方案,尽管如此。
在一个 with 块内部 在一个 with 块内部,非常有限的模式匹配是可能的。它将使用一个新的赋值运算符 ~= 进行演示(仅为了说明)。模式匹配只能对函数的结果进行操作,不能对值的内部进行操作,并且只会进行简单的 == 检查。
这是一个例子:

with {
  result, nil ~= thingThatCanFail() // This line fails the pattern match if the second return value is not nil.
  val, true ~= result.(T) // Works for all comparable types, not just errors, and even works with type assertions.
  return useValue(val) // Other lines of code are possible, too.
} else {
  case _, error(err):
    return nil, fmt.Errorf("failed to do thing: %w", err)
}

else 块将是一个类似于 switch 或者 select 的案例列表。每个案例都是一个逗号分隔的标识符列表,与上面的模式匹配相同。如果 with 块中的任何模式匹配失败,那么就会按照从上到下的顺序运行 else 块中的案例。如果有一个成功了,就用它代替整个块并退出。
模式匹配的功能将会非常有限。除了可以检查字面量和非遮蔽的预定义值(如 truenil 等)之外,它们还可以被 Package 成看起来像类型转换的形式。如果是这样的话,它们将受到特定类型的约束。这一点在上面的错误处理示例中得到了证明。如果 else 块中没有任何案例匹配成功,那么整个块就会发生恐慌。或者什么都不做。我不确定哪种情况更有意义。
一个是关于变量创建规则的具体定义。在上面的示例中,我只是假设 srcdst 是由 ~= 操作符创建的,但也许这个假设没有意义。它应该像 := 一样在变量创建、遮蔽等方面起作用吗?还是它永远不会创建新变量?左侧值和右侧值之间的规则是什么?它只应该是预先声明的值,还是有可能匹配存储在变量本身中的值?Elixir使用 pin 操作符来声明在匹配过程中不应声明变量,即 {^v, r} = something() 意味着 r 是一个新的变量,但 ^v 应该只是匹配已经存储在 v 中的值。也许这样才合理?我不确定,而且我可能随时改变主意。
同样,我也担心如何区分应该单独处理的相似情况。例如,如果我想要为上面的示例添加自定义错误消息怎么办?如果我只想要将其应用于 io.Copy() ,那么只需不对该调用使用模式匹配并以传统方式处理即可。另一个选择是添加第三个值来指示,例如

with {
  _, err := io.Copy(dst, src)
  nil, err ~= err, fmt.Errorf("copy failed: %w", err)
} else {
  case _, error(err):
    return err
}

最后,在 else 情况下应如何处理重叠类型?例如,如果我在上面的例子中使用了上述代码,则 _, error(err) 情况下 _ 可能具有两种不同的类型,即 *os.Fileint64 。这有问题吗?也许它可以类似于 #65031 的方式工作。但那可能会变得很混乱。

结论

正如您可能从担忧部分看到的那样,我并不完全相信我的提议,但我认为这是一个完全不同方法来清理错误处理代码的可能性。它并不能解决人们在使用错误处理时遇到的所有问题,但我认为它利用了多个返回作为处理错误的一部分,比大多数提案都要多,其中许多提案都在设法使具有多个返回包括错误的函数表现得好像它们不是这样。话虽如此,即使这个提议被拒绝了,也许它会激发出一个更好的方案。

语言规范更改

主要有两个更改:添加一个 with 块和添加 ~= 操作符及其相关规则。

非正式更改

  • 无响应*

此更改是否向后兼容?

我认为是这样,但应该可以创建一个替代方案(如果不是的话)。潜在的不向后兼容的部分基本上只是实现细节。

正交性:此更改如何与现有功能相互作用或重叠?

它实现了将重复的错误处理与代码的正常路径分离。

此更改是否会使 Go 更难学或更容易学习,以及为什么?

略微困难一些,但我认为它并不过于复杂。它添加了一种新的控制流类型,但我认为主要的复杂性可能来自于关于模式匹配如何工作的规则。

成本描述

一些额外的复杂性。可能存在运行时成本,但如果存在的话非常微小。主要是语法糖。大部分可能的成本可能来自一些不必要的类型切换,但在大多数情况下可以在编译时优化掉这些切换。

对Go工具链的更改

解析Go代码的所有内容都会受到影响。

性能成本

在两者中可能都非常小。

原型

  • 无响应*
avkwfej4

avkwfej41#

根据我的观察,人们在社区中对try/catch感到厌恶。

zf2sa74q

zf2sa74q2#

它有一些相似之处,不幸的是,是的。我自己对 try / catch 感到“厌恶”。不过,我不认为这有 try / catch 的主要问题,尤其是 try / catch 处理多个错误唯一的真正方法是将整个东西包裹在每一行上,这最终回到了 if err != nil 的位置,但更糟糕。我有一个每行没有额外缩进的备选方案,但我认为它离 if err != nil 太近了,所以没有必要提及。它还缺少一些关键部分,没有这些部分它就完全无法工作。它看起来像这样:

// Very backwards incompatible, too.
use result, nil := something() else {
  return ???
}

我提出这个建议的主要目标是尝试找出一种不特定于 error 的错误处理替代方法。不过,遗憾的是,我不确定我真的成功了。正如我所说,我也不喜欢它的结果,但我认为模式匹配作为一种更简洁地检查多个返回值的方法具有一定的调查价值。

afdcj2ne

afdcj2ne3#

这个建议的语法非常有趣。它更干净、更有用、更可预测,也更可控,在我看来,比可怕的try/catch要好得多。我希望所有使用try/catch的语言都能采用这样的语法。然而,我认为这种额外的golang语法违背了我们所珍视的简单性。有了这个建议,以及所有类似的多错误处理建议,我们最终有两种语法选择来完成同样的事情("我应该使用if-not-null链还是with块?")......对于甚至不知道go的人来说,if-not-null链更容易阅读,而这则更复杂(尽管比try/catch要干净得多)。
我们喜欢在这里处理错误的痛苦。

vktxenjb

vktxenjb4#

解:因为$x_{1m0n1}^{x}$可能太通用,无法关联到错误处理上,所以我们选择一些能描述性的英文单词。

答案为:$x_{1a0b1}^{x}$

q35jwt9p

q35jwt9p5#

快乐路径是一个有趣的概念。在with语句中缩进几乎每个函数的主体似乎不太可取。
result, nil ~= f()的使用非常微妙。这里result意味着要设置的变量,而nil意味着要匹配的模式。但在Go中,nil只是一个标识符,而nil := 0是有效的(如果不寻常)Go代码。我们不能在这种形式的语句中特别对待nil。模式匹配变量的作用域规则似乎也不明确。
总的来说,我们并不清楚从模式匹配中能获得多少好处。它确实允许val, true ~= f()。但这并没有太多的需求。我们能否专门为错误进行优化?我们会失去多少?

相关问题