如此出色

Go 2中新的错误处理设计草案已于最近发布,令人欣喜的是,该语言并没有站在一起-它不断发展,并且每年都有突飞猛进的发展。


只有在这里,直到只有2走的是看到在地平线上,并等待它是非常痛苦和伤心。 因此,我们将事情掌握在自己手中。 一点点的代码生成,一点点的ast工作,随着手部恐慌的轻微移动,恐慌就变成了优雅的异常!



我想立即发表一个非常重要且绝对认真的声明。
该决定本质上是娱乐性和教学性的。
我的意思是只有4个乐趣。 实际上,这通常是概念证明。 我警告过:)

那么发生了什么


结果是一个很小的库代码生成器 。 众所周知,代码生成器内部包含善良和优雅。 实际上不是,但是在Go世界中,它们非常受欢迎。


我们在原始环境中设置了这样的代码生成器。 他在标准go/ast模块的帮助下进行了解析,做了一些 狡猾的转换,结果写在文件旁边,并添加后缀_jex.go 。 生成的文件需要一个很小的运行时。


通过这种简单的方式,我们向Go添加了异常。


我们用


我们将生成器连接到文件,在文件头(在package之前)中编写


 //+build jex //go:generate jex 

如果现在运行命令go generate -tags jex ,则将执行jex实用程序。 她从os.Getenv("GOFILE")获取文件名,将其吃掉,消化并写入{file}_jex.go 。 新生文件的头中已经有//+build !jex (标记是反向的),因此go build ,并在其隔室中使用其他命令(例如go testgo install仅考虑新的正确文件。 Lepota ...


现在点导入github.com/anjensan/jex
是的,虽然必须通过点导入。 将来计划保持不变。


 import . "github.com/anjensan/jex" 

太好了,现在您可以在代码中插入对存根函数TRYTHROWEX调用。 出于所有这些原因,该代码在语法上仍然有效,甚至可以未经处理的形式进行编译(这根本行不通),因此可以使用自动补全功能,而lint 也不会真正发誓。 如果这些功能只有一个,编辑者还将显示这些功能的文档。


引发异常


 THROW(errors.New("error name")) 

捕捉异常


 if TRY() { //   } else { fmt.Println(EX()) } 

在后台生成一个匿名函数。 并且defer 。 它还有一个功能。 并且在recover ...好吧,仍然有一些不可思议的事情来处理returndefer


是的,顺便说一下,它们得到了支持!


此外,还有一个特殊的宏变量ERR 。 如果您为其分配错误,则会引发异常。 调用仍旧返回error函数会更容易


 file, ERR := os.Open(filename) 

此外,有两个exmust的小工具袋,但没什么可谈的。


例子


这是正确的,惯用的Go代码的示例


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

这段代码不是那么优雅。 顺便说一句,这不仅是我的意见!
但是jex将帮助我们改进它。


 func CopyFile_(src, dst string) { defer ex.Logf("copy %s %s", src, dst) r, ERR := os.Open(src) defer r.Close() w, ERR := os.Create(dst) if TRY() { ERR := io.Copy(w, r) ERR := w.Close() } else { w.Close() os.Remove(dst) THROW() } } 

但是例如下面的程序


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

可以改写成


 func main() { if TRY() { hex, ERR := ioutil.ReadAll(os.Stdin) data, ERR := parseHexdump(string(hex)) os.Stdout.Write(data) } else { log.Fatal(EX()) } } 

这是另一个示例,目的是更好地理解所提出的想法。 原始码


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

可以改写成


 func printSum_(a, b string) { x, ERR := strconv.Atoi(a) y, ERR := strconv.Atoi(b) fmt.Println("result:", x + y) } 

甚至那个


 func printSum_(a, b string) { fmt.Println("result:", must.Int_(strconv.Atoi(a)) + must.Int_(strconv.Atoi(b))) } 

例外情况


底线是error实例上的简单包装器结构。


 type exception struct { //  ,   err error //  ^W , ,    log []interface{} //      ,    suppress []*exception } 

重要的一点是,普通的紧急攻击不会被视为例外。 因此,所有标准错误(例如runtime.TypeAssertionError也不例外。 这符合Go中公认的最佳实践-如果我们有nil-dereference,那么我们会乐于乐意放弃整个过程。 可靠且可预测。 尽管我不确定,但也许值得回顾一下并找出此类错误。 也许是可选的?


这是一个异常链的例子


 func one_() { THROW(errors.New("one")) } func two_() { THROW(errors.New("two") } func three() { if TRY() { one_() } else { two_() } } 

在这里,我们冷静地处理异常one ,因为突然坏了……并抛出了异常two 。 因此,源onesuppress字段中suppress附加到它。 什么都不会丢失,一切都会进入日志。 因此,不需要特别使用非常流行的fmt.Errorf("blabla: %v", err)模式fmt.Errorf("blabla: %v", err)将整个错误链直接推入消息文本中。 当然,如果您愿意,当然也没有人禁止在这里使用它。


什么时候忘了


啊,另一个很重要的一点。 为了提高可读性,还有一个额外的检查:如果一个函数可以引发异常,则其名称必须以_结尾。 一个故意歪曲的名字会告诉程序员“尊敬的先生,在您的程序中这可能会出问题,请小心谨慎!”


自动检查已转换的文件,也可以使用jex-check命令在项目中手动启动jex-check 。 将它与其他linter一起作为构建过程的一部分来运行也许是有意义的。


//jex:nocheck评论被//jex:nocheck 。 顺便说一下,这是从匿名函数引发异常的唯一方法。


当然,这并不是解决所有问题的灵丹妙药。 Checker会错过这个


 func bad_() { THROW(errors.New("ups")) } func worse() { f := bad_ f() } 

另一方面,它并不比err declared and not used的标准检查差很多,这很容易规避。


 func worse() { a, err := foo() if err != nil { return err } b, err := bar() //  ,    ok... go vet, ? } 

总的来说,这个问题是很哲学的,当您忘记处理错误时最好做什么-安静地忽略它,或者引发恐慌...顺便说一句,可以通过在编译器中实现异常支持来达到测试的最佳结果,但这远远超出了本文的范围。 。


有人可能会说,尽管这是一个很棒的解决方案,但它不再是一个例外,因为现在例外意味着非常具体的实现。 好吧,那里是因为堆栈跟踪没有附加到异常,或者有一个单独的linter用于检查函数名称,或者该函数可以以_结尾但不引发异常,或者在语法上没有直接支持,或者确实很慌张,并且恐慌一点也不例外,因为唐蒲……孢子可能像毫无价值的毫无意义的一样热。 因此,我将把它们留在本文的框架之外,并且我将继续将所描述的解决方案称为“例外”。


关于Stackrace


通常,为了简化调试,开发人员会将堆栈跟踪粘贴到自定义error实现上。 甚至有几个流行的库。 但是,幸运的是,由于Go的一项有趣功能,除例外情况外,此操作不需要任何其他操作-在出现紧急情况时,在引发紧急情况的代码的堆栈上下文defer执行defer块。 因此在这里


 func foo_() { THROW(errors.New("ups")) } func bar() { if TRY() { foo_() } else { debug.PrintStack() } } 

将显示完整的堆栈跟踪,尽管有些冗长(我剪切了文件名)


  runtime/debug.Stack runtime/debug.PrintStack main.bar.func2 github.com/anjensan/jex/runtime.TryCatch.func1 panic main.foo_ main.bar.func1 github.com/anjensan/jex/runtime.TryCatch main.bar main.main 

考虑到代理功能并隐藏它们以提高可读性,使您自己的帮助程序格式化/打印堆栈跟踪记录不会有什么坏处。 我认为这是个好主意。


或者,您可以使用ex.Log()抓住堆栈并将其附加到异常。 然后,将这样的异常转移到另一个horoutin中-strextrace不丢失。


 func foobar_() { e := make(chan error, 1) go func() { defer close(e) if TRY() { checkZero_() } else { EX().Log(debug.Stack()) //   e <- EX().Wrap() //     } }() ex.Must_(<-e) //  ,  ,  } 

不幸的是


嗯...当然,这样的东西看起来会好得多


  try { throw io.EOF, "some comment" } catch e { fmt.Printf("exception: %v", e) } 

但是,,Go的语法不可扩展。
[深思熟虑]尽管可能会变得更好...


无论如何,你都必须变态。 另一种想法是使


  TRY; { THROW(io.EOF, "some comment") }; CATCH; { fmt.Printf("exception: %v", EX) } 

但是在go fmt之后,这样的代码显得有些愚蠢。 并且当编译器在两个分支中都看到return时发誓。 if-TRY没有这样的问题。


MUST函数替换ERR宏是很酷的(不止于此)。 为了写


  return MUST(strconv.Atoi(a)) + MUST(strconv.Atoi(b)) 

原则上,这仍然可行,当分析as​​t时,您可以派生表达式的类型,因为所有类型的类型都会生成一个简单的包装函数,例如must包中声明的那些包装函数,然后将MUST替换为相应的替代函数的名称。 这不是完全无关紧要的,而是完全可能的。只有编辑器/ ide无法理解这样的代码。 毕竟, MUST存根函数的签名无法在Go类型系统中表达。 因此没有自动完成功能。


引擎盖下


新导入将添加到所有处理的文件中。


  import _jex "github.com/anjensan/jex/runtime" 

panic(_jex.NewException(...))调用THROW替换为panic(_jex.NewException(...))EX()也将替换为包含捕获的异常的局部变量的名称。


但是, if TRY() {..} else {..}处理要复杂一些。 首先,对所有returndefer进行特殊处理。 然后将处理后的if分支放置在匿名函数中。 然后将这些函数传递给_jex.TryCatch(..) 。 这是


 func test(a int) (int, string) { fmt.Println("before") if TRY() { if a == 0 { THROW(errors.New("a == 0")) } defer fmt.Printf("a = %d\n", a) return a + 1, "ok" } else { fmt.Println("fail") } return 0, "hmm" } 

变成这样(我删除了//line注释):


 func test(a int) (_jex_r0 int, _jex_r1 string) { var _jex_ret bool fmt.Println("before") var _jex_md2502 _jex.MultiDefer defer _jex_md2502.Run() _jex.TryCatch(func() { if a == 0 { panic(_jex.NewException(errors.New("a == 0"))) } { _f, _p0, _p1 := fmt.Printf, "a = %d\n", a _jex_md2502.Defer(func() { _f(_p0, _p1) }) } _jex_ret, _jex_r0, _jex_r1 = true, a+1, "ok" return }, func(_jex_ex _jex.Exception) { defer _jex.Suppress(_jex_ex) fmt.Println("fail") }) if _jex_ret { return } return 0, "hmm" } 

很多,不是很漂亮,但是很有效。 好吧,不是全部,而且并非总是如此。 例如,您不能在TRY内进行defer-recover ,因为函数调用会变成一个额外的lambda。


另外,在显示ast树时,将显示选项“保存注释”。 因此,从理论上讲, go/printer应该打印它们。...老实说,事实是非常非常歪曲=)我不会举任何例子,只是歪曲。 原则上,如果您仔细指定所有ast节点的位置(现在它们为空),则可以完全解决此问题,但这绝对不包括在原型必需的清单中。


试一下


出于好奇,我写了一个小基准


我们有一个木制的qsort实现,可以检查负载中的重复项。 发现-一个错误。 一个版本只是通过return err抛出它,另一个版本通过调用fmt.Errorf澄清错误。 还有一个使用异常。 我们对不同大小的切片进行排序,要么根本没有重复(没有错误,切片被完全排序),要么进行一次重复(排序中断大约一半,可以通过计时看到)。


结果
 ~ > cat /proc/cpuinfo | grep 'model name' | head -1 model name : Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz ~ > go version go version go1.11 linux/amd64 ~ > go test -bench=. github.com/anjensan/jex/demo goos: linux goarch: amd64 pkg: github.com/anjensan/jex/demo BenchmarkNoErrors/_____10/exception-8 10000000 236 ns/op BenchmarkNoErrors/_____10/return_err-8 5000000 255 ns/op BenchmarkNoErrors/_____10/fmt.errorf-8 5000000 287 ns/op BenchmarkNoErrors/____100/exception-8 500000 3119 ns/op BenchmarkNoErrors/____100/return_err-8 500000 3194 ns/op BenchmarkNoErrors/____100/fmt.errorf-8 500000 3533 ns/op BenchmarkNoErrors/___1000/exception-8 30000 42356 ns/op BenchmarkNoErrors/___1000/return_err-8 30000 42204 ns/op BenchmarkNoErrors/___1000/fmt.errorf-8 30000 44465 ns/op BenchmarkNoErrors/__10000/exception-8 3000 525864 ns/op BenchmarkNoErrors/__10000/return_err-8 3000 524781 ns/op BenchmarkNoErrors/__10000/fmt.errorf-8 3000 561256 ns/op BenchmarkNoErrors/_100000/exception-8 200 6309181 ns/op BenchmarkNoErrors/_100000/return_err-8 200 6335135 ns/op BenchmarkNoErrors/_100000/fmt.errorf-8 200 6687197 ns/op BenchmarkNoErrors/1000000/exception-8 20 76274341 ns/op BenchmarkNoErrors/1000000/return_err-8 20 77806506 ns/op BenchmarkNoErrors/1000000/fmt.errorf-8 20 78019041 ns/op BenchmarkOneError/_____10/exception-8 2000000 712 ns/op BenchmarkOneError/_____10/return_err-8 5000000 268 ns/op BenchmarkOneError/_____10/fmt.errorf-8 2000000 799 ns/op BenchmarkOneError/____100/exception-8 500000 2296 ns/op BenchmarkOneError/____100/return_err-8 1000000 1809 ns/op BenchmarkOneError/____100/fmt.errorf-8 500000 3529 ns/op BenchmarkOneError/___1000/exception-8 100000 21168 ns/op BenchmarkOneError/___1000/return_err-8 100000 20747 ns/op BenchmarkOneError/___1000/fmt.errorf-8 50000 24560 ns/op BenchmarkOneError/__10000/exception-8 10000 242077 ns/op BenchmarkOneError/__10000/return_err-8 5000 242376 ns/op BenchmarkOneError/__10000/fmt.errorf-8 5000 251043 ns/op BenchmarkOneError/_100000/exception-8 500 2753692 ns/op BenchmarkOneError/_100000/return_err-8 500 2824116 ns/op BenchmarkOneError/_100000/fmt.errorf-8 500 2845701 ns/op BenchmarkOneError/1000000/exception-8 50 33452819 ns/op BenchmarkOneError/1000000/return_err-8 50 33374000 ns/op BenchmarkOneError/1000000/fmt.errorf-8 50 33705994 ns/op PASS ok github.com/anjensan/jex/demo 64.008s 

如果尚未引发错误(代码稳定且经过具体处理),则带有异常引发的保证与return errfmt.Errorf大致相当。 有时会快一点。 但是,如果引发了错误,则异常将排在第二位。 但这全都取决于“有用的工作/错误”的比率和堆栈的深度。 对于小片, return err领先于差距;对于大片和大片,异常已经等同于手动转发。


简而言之,如果错误极少发生,则异常甚至可以稍微加快代码的速度。 如果像其他所有人一样,那将是类似的事情。 但是,如果经常出现……则缓慢的例外远非最重要的问题,这值得担心。


作为测试,我迁移了一个真正的gosh 来排除异常。


令我深感遗憾的是,重写1合1无效

更确切地说,它本来可以证明,但是必须打扰。


因此,例如, rpc2XML函数似乎返回error ……是的,它永远不会返回错误。 如果您尝试序列化不支持的数据类型-没有错误,则输出为空。 也许这就是原意吗?..不,良心不允许这样。 添加者


  default: THROW(fmt.Errorf("unsupported type %T", value)) 

但是事实证明,此功能是以特殊方式使用的


 func rpcParams2XML(rpc interface{}) (string, error) { var err error buffer := "<params>" for i := 0; i < reflect.ValueOf(rpc).Elem().NumField(); i++ { var xml string buffer += "<param>" xml, err = rpc2XML(reflect.ValueOf(rpc).Elem().Field(i).Interface()) buffer += xml buffer += "</param>" } buffer += "</params>" return buffer, err } 

在这里,我们遍历参数列表,将它们全部序列化,但针对后者返回错误。 其余错误将被忽略。 奇怪的行为变得更容易


 func rpcParams2XML_(rpc interface{}) string { buffer := "<params>" for i := 0; i < reflect.ValueOf(rpc).Elem().NumField(); i++ { buffer += "<param>" buffer += rpc2XML_(reflect.ValueOf(rpc).Elem().Field(i).Interface()) buffer += "</param>" } buffer += "</params>" return buffer } 

如果至少一个字段无法序列化-这是一个错误。 好吧,那更好。 但是事实证明,此功能也以特殊方式使用。


 xmlstr, _ = rpcResponse2XML(response) 

同样,对于源代码来说,它并不是那么重要,因为会忽略错误。 我开始猜测为什么有些程序员如此喜欢通过if err != nil进行显式错误处理……但是除了例外,转发或处理都比忽略起来更容易


 xmlstr = rpcResponse2XML_(response) 

而且我还没有开始删除“错误链”。 这是原始代码


 func DecodeClientResponse(r io.Reader, reply interface{}) error { rawxml, err := ioutil.ReadAll(r) if err != nil { return FaultSystemError } return xml2RPC(string(rawxml), reply) } 

这是改写的


 func DecodeClientResponse_(r io.Reader, reply interface{}) { var rawxml []byte if TRY() { rawxml, ERR = ioutil.ReadAll(r) } else { THROW(FaultSystemError) } xml2RPC_(string(rawxml), reply) } 

在这里,原始错误( ioutil.ReadAll返回的错误)将不会丢失,它将附加到suppress字段中的异常中。 同样,它可以像原始版本一样进行,但是必须特别混淆...


我重写了测试,将if err != nil { log.Error(..) }替换为简单的异常抛出。 不利的一面是-测试落在了第一个错误上,“至少在某种程度上”没有继续起作用。 根据思想,有必要将它们划分为子测试...总的来说,在任何情况下都值得做。 但是,获得正确的堆栈竞价非常容易


 func errorReporter(t testing.TB) func(error) { return func(e error) { t.Log(string(debug.Stack())) t.Fatal(e) } } func TestRPC2XMLConverter_(t *testing.T) { defer ex.Catch(errorReporter(t)) // ... xml := rpcRequest2XML_("Some.Method", req) } 

通常,错误很容易忽略。 在原始代码中


 func fault2XML(fault Fault) string { buffer := "<methodResponse><fault>" xml, _ := rpc2XML(fault) buffer += xml buffer += "</fault></methodResponse>" return buffer } 

在这里,来自rpc2XML的错误再次被rpc2XML忽略。 变成了这样


 func fault2XML(fault Fault) string { buffer := "<methodResponse><fault>" if TRY() { buffer += rpc2XML_(fault) } else { fmt.Printf("ERR: %v", EX()) buffer += "<nil/>" } buffer += "</fault></methodResponse>" return buffer } 

根据我个人的感觉,返回带有错误的“半成品”结果会更容易。
例如,半构建的响应。 异常更为复杂,因为该函数要么返回成功的结果,要么什么都不返回。 一种原子性。 另一方面,异常更难忽略或丢失异常链中的根本原因。 毕竟,您仍然必须专门尝试执行此操作。 出现错误时,这很容易自然发生。


而不是结论


撰写本文时,没有地鼠受伤。


感谢您提供的去夫酒的照片http://migranov.ru


我无法在“编程”和“异常编程”中心之间进行选择。
两者都增加了一个非常困难的选择。

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


All Articles