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

总结


提出了一个新的try构造,该构造专门设计用于消除if表达式通常与Go中的错误处理相关联。 这是唯一的语言更改。 作者支持使用defer和标准库函数来丰富或包装错误。 这个小的扩展几乎适用于大多数情况,而无需使语言复杂化。


try构造易于解释,易于实现,该功能与其他语言构造正交且完全向后兼容。 如果将来需要,它也可以扩展。


本文档的其余部分安排如下:在简要介绍之后,我们给出了内置函数的定义并解释了其在实践中的用法。 讨论部分回顾了替代建议和当前设计。 最后,将给出结论和实施计划,并附有示例以及一部分问题和答案。


引言


在上一次在丹佛举行的Gophercon会议上,Go团队成员(Russ Cox,Marcel van Lohuizen)提出了一些有关如何减少Go( 设计草案 )中的手动错误处理的乏味的新想法。 从那以后,我们收到了大量反馈。


正如Russ Cox在对问题的评论中所解释的那样,我们的目标是通过减少专门用于错误检查的代码量来使错误处理更加轻巧。 我们还希望使编写错误处理代码更加方便,从而增加开发人员仍将时间花费在纠正错误处理上的可能性。 同时,我们希望使错误处理代码在程序代码中清晰可见。


草案中讨论的思想集中在新的一元check语句周围,该语句简化了对从某些表达式(通常是函数调用)中获得的错误值的显式验证,以及错误处理程序的声明和连接这两种新语言结构的一组规则。


我们收到的大多数反馈都集中在handle设计的细节和复杂性上,而check操作员的想法却变得更具吸引力。 实际上,社区中的一些成员采用了check操作员的想法并加以扩展。 以下是一些与我们提供的服务最相似的帖子:



当前的提案尽管在细节上有所不同,但是基于这三个方面,并且总体上基于对去年提出的设计草案的反馈。


为了使图片更完整,我们要注意的是,可以在此Wiki页面上找到更多错误处理建议。 还值得注意的是, 利亚姆·布雷克(Liam Breck)对错误处理机制提出了广泛的要求


最终,在该建议发布之后,我们了解到Ryan Hileman在五年前使用og rewriter工具实施了try并成功地在实际项目中使用了它。 请参阅( https://news.ycombinator.com/item?id=20101417 )。


内置try功能


提供


我们建议添加一个新的类似于函数的语言元素,称为try并使用签名进行调用


 func try(expr) (T1, T2, ... Tn) 

其中expr表示输入参数的表达式(通常是函数调用),该表达式返回n + 1个类型为T1, T2, ... Tn的值以及最后一个值的error 。 如果expr是单个值(n = 0),则该值必须为error类型,并且try不返回结果。 使用不返回类型error的最后一个值的表达式调用try导致编译错误。


try构造只能在至少返回一个值且最后一个返回值为error类型的error 。 在其他情况下调用try会导致编译错误。


如示例所示,使用f()函数调用try


 x1, x2, … xn = try(f()) 

导致以下代码:


 t1, … tn, te := f() // t1, … tn,  ()   if te != nil { err = te //  te    error return //     } x1, … xn = t1, … tn //     //     

换句话说,如果expr返回的最后一个error类型为nil ,则try简单地返回前n个值,并删除最后的nil


如果expr返回的最后一个值不是nil ,则:


  • 封闭函数的error返回值(在上面名为err的伪代码中,尽管它可以是任何标识符或未命名的返回值)接收从expr返回的错误值
  • 从封装功能中退出
  • 如果封闭函数具有其他返回参数,则这些参数将保留try调用之前其中包含的值。
  • 如果封闭函数具有其他未命名的返回参数,则为它们返回相应的零值(这与保存用于初始化的原始零值相同)。

如上例所示,如果在多个分配中使用了try ,并且检测到非零错误(以下称为non-nil-大约Per​​。),则不会执行分配(通过用户变量),并且分配左侧的任何变量都不会更改。 也就是说, try行为类似于函数调用:仅当try将控制权返回给调用者时(与从封闭函数返回的情况相反),其结果才可用。 因此,如果赋值左侧的变量是返回参数,则使用try会导致行为不同于现在遇到的典型代码。 例如,如果将a,b, err命名为封闭函数的返回参数,则此代码为:


 a, b, err = f() if err != nil { return } 

将始终为变量a, berr赋值,而不管对f()的调用是否返回错误。 相反的挑战


 a, b = try(f()) 

如果发生错误,请保持ab不变。 尽管这是一个细微的差别,但我们认为这种情况很少见。 如果需要无条件分配行为,则必须继续使用if表达式。


使用方法


try的定义明确地告诉您如何使用它:许多if检查错误返回的表达式可以用try代替。 例如:


 f, err := os.Open(filename) if err != nil { return …, err //       } 

可以简化为


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

如果调用函数没有返回错误,则不能使用try (请参见“讨论”部分)。 在这种情况下,无论如何都应在本地处理错误(因为没有错误返回),在这种情况下, if仍然存在适当的机制来检查错误。


一般而言,我们的目标不是用try替换所有可能的错误检查。 if表达式和显式变量带有错误值,则需要不同语义的代码可以并且应该继续使用。


测试并尝试


在我们较早编写规范的尝试之一(请参阅下面的设计迭代部分)中, try在没有返回错误的情况下在函数内使用时发生错误时, try会引起恐慌。 这允许基于标准库的testing包使用try单元测试。


作为选项之一,可以在testing包中使用带有签名的测试功能


 func TestXxx(*testing.T) error func BenchmarkXxx(*testing.B) error 

为了允许使用测试。 返回非零错误的测试函数将隐式调用t.Fatal(err)b.Fatal(err) 。 这是一个很小的库更改,根据上下文,避免了try不同行为(返回或恐慌)的需要。


这种方法的缺点之一是t.Fatalb.Fatal将无法返回测试所在的行号。 另一个缺点是我们也必须以某种方式更改子测验。 这个问题的解决方案是一个悬而未决的问题。 在本文档中,我们不建议对testing包进行特定更改。


另请参见#21111 ,它建议允许示例函数返回错误。


错误处理


原始设计草案主要涉及对包装错误或错误扩充的语言支持。 草案提出了新的关键字handle声明错误处理程序的新方法。 这种新的语言结构由于非平凡的语义而吸引了像苍蝇这样的问题,尤其是考虑到它对执行流程的影响时。 特别是, handle功能与defer函数痛苦地交叉,这使得新语言功能与其他所有功能都不正交。


该提议将原始草案设计简化为实质。 如果需要扩充或错误包装,则有两种方法: if err != nil { return err}附加,或在defer表达式中“声明”错误处理程序:


 defer func() { if err != nil { //      -   err = … // /  } }() 

在此示例中, err是封闭函数的类型为error的返回参数的名称。


在实践中,我们可以想象这样的助手功能:


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

或类似的东西。 fmt软件包可以成为此类帮助者的自然选择(它已经提供了fmt.Errorf )。 使用帮助程序,错误处理程序的定义在许多情况下将简化为一行。 例如,要丰富“复制”功能中的错误,可以编写


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

如果fmt.HandleErrorf隐式添加错误信息,则这种结构相当容易阅读,并且具有无需添加语言语法新元素即可实现的优点。


这种方法的主要缺点是必须命名返回的错误参数,这可能会导致API的准确性降低(请参阅本主题的FAQ)。 我们相信,一旦建立了适当的编写代码样式,我们就会习惯它。


效率延迟


使用defer作为错误处理程序时的重要考虑因素是效率。 defer表达被认为是缓慢的 。 我们不想在高效的代码和良好的错误处理之间进行选择。 不管提出什么建议,Go运行时团队和编译器团队都讨论了替代的实现方法,我们相信我们可以采用典型的方式来使用defer处理与现有“手动”代码效率相当的错误。 我们希望在Go 1.14中添加一个更快的defer实现(另请参见故障CL 171158 ,这是朝此方向迈出的第一步)。


特殊情况go try(f), defer try(f)


try构造看起来像一个函数,因此,可以在可以接受函数调用的任何地方使用它。 但是,如果在go语句中使用了try调用,事情将会变得复杂:


 go try(f()) 

这里的f()是在当前goroutine中执行go表达式时执行的,调用f的结果作为参数传递给trytry从新的goroutine开始。 如果f返回非零错误,则期望try从封闭函数返回;否则,返回false。 但是,没有功能(也没有类型为error返回参数),因为 该代码在单独的goroutine中执行。 因此,我们建议禁用go表达式中的try


情况与


 defer try(f()) 

看起来很相似,但是在这里defer的语义意味着try的执行将被延迟,直到它从封闭函数返回为止。 和以前一样,在defer时对f()进行求值,并将其结果传递给deferred try


try检查仅在从封闭函数返回之前的最后一刻返回的错误f() 。 在不更改try行为的情况下,此类错误可以覆盖封闭函数尝试返回的另一个错误值。 这充其量会混淆,最糟糕的是会引发错误。 因此,我们建议您也禁止在defer语句中调用try 。 如果可以合理地使用这种语义,我们总是可以重新考虑这个决定。


最后,像其余的内置结构一样, try只能用作调用。 不能将其用作值函数或变量赋值表达式,如f := try (就像f := printf := new一样)。


讨论区


设计迭代


以下是对导致当前最小提议的早期设计的简短讨论。 我们希望这将为选定的设计决策提供启发。


我们从“错误处理的关键部分”一文中得到的两个想法启发了这句话的第一次迭代即使用内置函数代替运算符,并使用通常的Go函数代替新的语言构造来处理错误。 与该出版物不同,我们的错误处理程序具有固定的签名func(error) error以简化事务。 如果在try退出封闭函数之前发生错误,则try函数将调用错误处理程序。 这是一个例子:


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

尽管此方法允许定义有效的用户定义错误处理程序,但它还提出了许多显然没有正确答案的问题:如果将nil传递给处理程序,该怎么办? 您是否应该感到恐慌或将其视为缺乏处理程序? 如果以非零错误调用处理程序,然后返回空结果怎么办? 这是否意味着错误已“消除”? 还是封闭函数返回一个空错误? 还有人怀疑错误处理程序的可选传输会鼓励开发人员忽略错误而不是纠正错误。 在各处进行正确的错误处理也很容易,但是跳过try一种用法。 等等。


在下一次迭代中,删除了传递自定义错误处理程序的功能,转而使用defer包裹错误。 这似乎是一种更好的方法,因为它使错误处理程序在源代码中更加引人注目。 此步骤还消除了与处理程序函数的可选传输有关的所有问题,但是,如果需要访问,则要求命名具有error类型的返回参数(我们认为这是正常的)。 此外,为了使try不仅在返回错误的函数中有用,还必须使try上下文的行为变得敏感:如果try在包级别使用的,或者如果它在不返回错误的函数中被调用,则在检测到错误时try自动惊慌。 (而且,副作用是,由于这个属性,所以must在该句子中调用该语言构造,而不是使用try 。) try (或must )的上下文相关行为似乎很自然并且也非常有用:它将消除在表达式中使用的许多用户定义函数初始化程序包变量。 这也为在testing包中使用单元测试中的可能性提供了可能性。


但是, try的上下文相关行为充满了错误:例如,当在函数签名中添加或删除返回错误时,使用try的函数的行为可能会悄悄更改(无论是否紧急)。 这似乎太危险了。 显而易见的解决方案是将try功能分为两个单独的musttry函数(非常类似于#31442中建议的方式)。 但是,这将需要两个内置函数,而仅try与更好地支持错误处理直接相关。


因此,在当前迭代中,我们决定删除try的双重语义,而不是包括第二个内置函数,因此,仅允许将其用于返回错误的函数中。


拟议设计的特点


这个建议很短,与去年的草案相比似乎有些退步。 我们认为所选解决方案是合理的:


  • 首先, try的语义与原始文档中提出的check语句完全相同,没有handle 。 这在重要方面之一中确认了原始草案的真实性。


  • 选择内置函数而不是运算符具有多个优点。 它不需要像check这样的新关键字,它会使设计与现有解析器不兼容。 也不需要使用新的运算符来扩展表达式的语法。 添加一个新的内置函数相对来说是微不足道的,并且与该语言的其他功能完全正交。


  • 使用内联函数而不是运算符需要使用括号。 我们应该写try(f())而不是try f() 。 这是与现有解析器向后兼容所必须付出的(小)价格。 但是,这也使设计与将来的版本兼容:如果我们一路认为以某种形式传递错误处理函数或为此目的添加附加参数来try是个好主意,则在try调用中添加附加参数将是微不足道的。


  • 事实证明,需要使用方括号有其优点。 在具有多个try调用的更复杂的表达式中,括号消除了对运算符优先级的处理,从而提高了可读性,如以下示例所示:



 info := try(try(os.Open(file)).Stat()) //   try info := try (try os.Open(file)).Stat() //  try   info := try (try (os.Open(file)).Stat()) //  try   

try , : try , .. try (receiver) .Stat ( os.Open ).


try , : os.Open(file) .. try ( , try os , , try try ).


, .. .


  • . , . , , , .

结论


. , . defer , .


Go - , . , Go append . append , . , . , try .


, , Go : panic recover . error try .


, try , , — — , . Go:


  • , try
  • -

, , . if -.


实作


:


  • Go.
  • try . , . .
  • go/types try . .
  • gccgo . ( , ).
  • .

- , . , . .


Robert Griesemer go/types , () cmd/compile . , Go 1.14, 1 2019.


, Ian Lance Taylor gccgo , .


"Go 2, !" , .


1 , , , Go 1.14 .



CopyFile :


 func CopyFile(src, dst string) (err error) { defer func() { if err != nil { err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r := try(os.Open(src)) defer r.Close() w := try(os.Create(dst)) defer func() { w.Close() if err != nil { os.Remove(dst) //    “try”    } }() try(io.Copy(w, r)) try(w.Close()) return nil } 

, " ", defer :


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

( defer -), defer , .


printSum


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

:


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

main :


 func localMain() error { hex := try(ioutil.ReadAll(os.Stdin)) data := try(parseHexdump(string(hex))) try(os.Stdout.Write(data)) return nil } func main() { if err := localMain(); err != nil { log.Fatal(err) } } 

- try , :


 n, err := src.Read(buf) if err == io.EOF { break } try(err) 


, .


: ?


: check handle , . , handle defer , handle .


: try ?


: try Go . - , . , . , " ". try , .. .


: try try?


: , check , must do . try , . try check (, ), - . . must ; try — . , Rust Swift try ( ). .


: ? Rust?


: Go ; , Go ( ; - ). , ? , . , , , (package, interface, if, append, recover, ...), , (struct, var, func, int, len, image, ..). Rust ? try — Go, , ( ) . , ? . , , (, ..) . . , .


: ( error) , defer , go doc. ?


: go doc , - ( _ ) , . , func f() (_ A, _ B, err error) go doc func f() (A, B, error) . , , , . , , . , , , -, (deferred) . Jonathan Geddes try() .


: defer ?


: defer . , , defer "" . . CL 171758 , defer 30%.


: ?


: , . , ( , ), . defer , . defer - https://golang.org/issue/29934 ( Go 2), .


: , try, error. , ?


: error ( ) , , nil . try . ( , . - ).


: Go , try ?


: try , try . super return -, try Go . try . .


: try , . ?


: try ; , . try ( ), . , if .


: , . try, defer . ?


: , . .


: try ( catch )?


: try — ("") , , ( ) . try ; . . "" . , . , try — . , , throw try-catch Go. , (, ), ( ) , . "" try-catch , . , , . Go . panic , .

Source: https://habr.com/ru/post/zh-CN472758/


All Articles