Go 2 错误处理提案:try 还是 check?
团队号 光谷码农 作者 chaishushan 原文链接
了解Go语言2.0的发展方向,请关注“光谷码农”公众号,我们带你了解最前沿的Go语言错误处理改进方向。
Go语言之父之一Robert Griesemer于2019-06-05新提交了一个try内置函数的提案,链接在这里:https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md
Go错误处理的问题
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) } return nil }
这里有太多重复的if err != nil错误处理代码。
if err != nil { return fmt.Errorf("copy %s %s: %v", src, dst, err) }
错误处理不仅仅是繁琐,它打断了表达式流程,⽆法链式操作:
v0, err := strconv.Atoi("123") if err != nil { return err } v1 := v0*2
在范型编程之后,错误处理是第二个被重点关注的改进方向。
曾经的提案:check & handle
在2018年,Go语言官方团队已经提交过一个错误改进提案:
func CopyFile(src, dst string) error { handle err { return fmt.Errorf("copy %s %s: %v", src, dst, err) } r := check os.Open(src) defer r.Close() w := check os.Create(dst) handle err { w.Close() os.Remove(dst) // (only if a check fails) } check io.Copy(w, r) check w.Close() return nil }
通过check & handle关键字来简化错误处理流程。
check关键字可以吃掉一个多余的err,这样可以进行链式操作:
func printSum(a, b string) error { handle err { return err } fmt.Println("result:", check strconv.Atoi(x) + check strconv.Atoi(y)) return nil }
但是前一个提案的最大风险是增加了新的关键词。Go语言目前有25个关键字,突然增加到27个,影响和谐社会。
因此,新的try内置函数的提案,主要是为了减少Go语言语法的变化,通过内置的特权函数来解决错误处理的问题。
try内置函数
先看看新提案的try函数签名:
func try(t1 T1, t1 T2, … tn Tn, te error) (T1, T2, … Tn)
输入的是n个参数加一个err返回值,返回的结果和前check关键字提案类似:检测err后返回前n个参数。这个特权函数是一个泛型函数。
有了try内置函数之后,之前的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) // only if a “try” fails } }() try(io.Copy(w, r)) try(w.Close()) return nil }
当try失败的时候,返回值中除了最后的err,其它的方针都是零值。需要注意的是,try函数无法处理复杂的错误(这个地方功能比之前的handle提案要弱一点)。
try函数也可以作为表达式使用(和check提案类似):
func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil }
另外,在单元测试中也可以使用try函数,它会被重写为t.Fatal(err)语句。
特殊的错误
对于特殊的错误类型,try函数的提案还在考虑是否可以传入额外的处理函数。类似这样:
f := try(os.Open(filename), func(err error) error { return err }, )
这样的话,可以对返回的错误,以及上下文状态进行补救处理。
比如CopyFile例子中,失败时可以这样删除临时文件:
func CopyFile(src, dst string) (err error) { ... w := try(os.Create(dst), func(err error) error) w.Close() os.Remove(dst) // only if a “try” fails return err ) defer w.Close() ... }
当然,这样的设计也会带来新的问题:try函数的参数具体是什么形式?
补充
最后,try是一个特殊的函数(类似的还有init函数等),不要把它当作普通函数使用,不然容易掉坑里去。
光谷码农
关注Go语言、WebAssembly等技术
发现 > 搜索 318517 即可

扫描或长按识别二维码 下载开发者头条客户端