Go2设计草案介绍

Go中国 · 2018-08-30 09:28:42 · 1233 次点击 · 预计阅读时间 6 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-08-30 09:28:42 的文章,其中的信息可能已经有所发展或是发生改变。

前言

Go,毫无疑问已经成为主流服务端开发语言之一,但它的类型特性却少的可怜,仅支持 structural subtyping。在 TIOBE 排名前二十的语言中,不管是上古语言 Java, 还是 2010 年之后出现的新语言 Rust/Julia 等,都支持至少三种类型特性,对此社区抱怨很多,另外还有它的错误处理方式,以及在 Go1.11 版本才解决的依赖管理等问题。在最近的 GopherCon2018 上,官方放出了解决这些问题的草案 (draft),这些内容还没有成为正式的提案 (proposal), 只是先发出来供大家讨论,最终会形成正式提案并被逐步引入到后续的版本中。此次放出的草案,集中讨论了三个问题,泛型 / 错误处理 / 错误值。

泛型

泛型是复用逻辑的一个有效手段,在 2016 和 2017 年的 Go 语言调查中,泛型都列在最迫切的需求之首,在 Go1.0 release 之后 Go team 就已经开始探索如何引入泛型,但同时要保持 Go 的简洁性 (开发者喜爱 Go 的主要原因之一),之前的几种实现方式都存在严重的问题,被废弃掉了,所以进展并不算快,甚至导致部分人误解为 Go team 并不打算引入泛型。现在,最新的草案经过半年的讨论和优化,已经确认可行 (could work),我们期待已久的泛型几乎是板上钉钉的事情了,那么 Go 的泛型大概长什么样?

在没有泛型的情况下,通过 interface{}是可以解决部分问题的,比如 ring的实现,但这种方法只适合用在数据容器里, 且需要做类型转换。当我们需要实现一个通用的函数时,就做不到了,例如实现一个函数,其返回传入的 map 的 key:

  1. package main

  2. import "fmt"

  3. func Keys(m map[interface{}]interface{}) []interface{} {

  4. keys := make([]interface{}, 0)

  5. for k, _ := range m {

  6. keys = append(keys, k)

  7. }

  8. return keys

  9. }

  10. func main() {

  11. m := make(map[string]string, 1)

  12. m["demo"] = "data"

  13. fmt.Println(Keys(m))

  14. }

这样写连编译都通过不了,因为类型不匹配。那么参考其他支持泛型的语言的语法,可以这样写:

  1. package main

  2. import "fmt"

  3. func Keys<K, V>(m map[K]V) []K {

  4. keys := make([]K, 0)

  5. for k, _ := range m {

  6. keys = append(keys, k)

  7. }

  8. return keys

  9. }

  10. func main() {

  11. m := make(map[string]string, 1)

  12. m["demo"] = "data"

  13. fmt.Println(Keys(m))

  14. }

但是这种写法是有缺陷的,假设 append 函数并不支持 string 类型,就可能会出现编译错误。我们可以看下其他语言的做法:

  1. // rust

  2. fn print_g<T: Graph>(g : T) {

  3.    println!("graph area {}", g.area());

  4. }

Rust 在声明 T 的时候,限定了入参的类型,即入参 g 必须是 Graph 的子类。和 Rust 的 nominal subtyping 不同,Go 属于 structural subtyping,没有显式的类型关系声明,因此不能使用此种方式。Go 在草案中引入了 contract来解决这个问题,语法类似于函数, 写法更复杂,但表达能力比 Rust 要更强:

  1. // comparable contract

  2. contract Equal(t T) {

  3. t == t

  4. }

  5. // addable contract

  6. contract Addable(t T) {

  7. t + t

  8. }

上述代码分别约束了 T 必须是可比较的 (comparable),必须是能做加法运算(addable) 的。使用方式很简单, 定义函数的时候加上约束即可:

  1. func Sum(type T Addable(T))(x []T) T {

  2. var total T

  3. for _, v := range x {

  4. total += v

  5. }

  6. return total

  7. }

  8. var x []int

  9. total := Sum(int)(x)

得益于类型推断,在调用 Sum 时可以简写成:

  1. total := Sum(x)

contract 在使用时,如果参数是一一对应的 (可推断), 也可以省略参数:

  1. func Sum(type T Addable)(x []T) T {

  2. var total T

  3. for _, v := range x {

  4. total += v

  5. }

  6. return total

  7. }

不可推断时就需要指明该 contract 是用来约束谁的:

  1. func Keys(type K, V Equal(K))(m map[K]V) []K {

  2. ...

  3. }

当然,下面的写法也可以推断,最终如何就看 Go team 的抉择了:

  1. func Keys(type K Equal, V)(m map[K]V) []K {

  2. ...

  3. }

关于实现方面的内容,这里不再讨论,留给高手吧。官方开通了反馈渠道,可以去提意见,对于我来说,唯一不满意的地方是显式的 type关键字, 可能是为了方便和后边的函数参数相区分吧。

错误处理

健壮的程序需要大量的错误处理逻辑,在极端情况下,错误处理逻辑甚至比业务逻辑还要多,那么更简洁有效的错误处理语法是我们所追求的。

先看下目前 Go 的错误处理方式,一个拷贝文件的例子:

  1. func CopyFile(src, dst string) error {

  2. r, err := os.Open(src)

  3. if err != nil {

  4. return fmt.Errorf("copy %s %s: %v", src, dst, err)

  5. }

  6. defer r.Close()

  7. w, err := os.Create(dst)

  8. if err != nil {

  9. return fmt.Errorf("copy %s %s: %v", src, dst, err)

  10. }

  11. if _, err := io.Copy(w, r); err != nil {

  12. w.Close()

  13. os.Remove(dst)

  14. return fmt.Errorf("copy %s %s: %v", src, dst, err)

  15. }

  16. if err := w.Close(); err != nil {

  17. os.Remove(dst)

  18. return fmt.Errorf("copy %s %s: %v", src, dst, err)

  19. }

  20. }

上述代码中,错误处理的代码占了总代码量的接近 50%!

Go 的 assignment-and-if-statement错误处理语句是罪魁祸首,草案引入了 check表达式来代替:

  1. r := check os.Open(src)

但这只代替了赋值表达式和 if 语句,从之前的例子中我们可以看到,有四行完全相同的代码:

  1. return fmt.Errorf("copy %s %s: %v", src, dst, err)

它是可以被统一处理的, 于是 Go 在引入 check的同时引入了 handle语句:

  1. handle err {

  2. return fmt.Errorf("copy %s %s: %v", src, dst, err)

  3. }

修改后的代码为:

  1. func CopyFile(src, dst string) error {

  2. handle err {

  3. return fmt.Errorf("copy %s %s: %v", src, dst, err)

  4. }

  5. r := check os.Open(src)

  6. defer r.Close()

  7. w := check os.Create(dst)

  8. handle err {

  9. w.Close()

  10. os.Remove(dst) // (only if a check fails)

  11. }

  12. check io.Copy(w, r)

  13. check w.Close()

  14. return nil

  15. }

check 失败后,先被执行最里层的 (inner most) 的 handler,接着被上一个(按照语法顺序)handler 处理,直到 handler 执行了 return语句。

Go team 对该草案的期望是能够减少错误处理的代码量, 且兼容之前的错误处理方式, 要求不算高,这个设计也算能接受吧。

反馈渠道

错误值

Go 的错误值目前存在两个问题。一,错误链 (栈) 没有被很好地表达;二,缺少更丰富的错误输出方式。在该草案之前,已经有不少第三方的 package 实现了这些功能,现在要进行标准化。目前,对于多调用层级的错误,我们使用 fmt.Errorf 或者自定义的 Error 来包裹它:

  1. package main

  2. import (

  3. "fmt"

  4. "io"

  5. )

  6. type RpcError struct {

  7. Line uint

  8. }

  9. func (s *RpcError) Error() string {

  10. return fmt.Sprintf("(%d): no route to the remote address", s.Line)

  11. }

  12. func fn3() error {

  13. return io.EOF

  14. }

  15. func fn2() error {

  16. if err := fn3(); err != nil {

  17. return &RpcError{Line: 12}

  18. }

  19. return nil

  20. }

  21. func fn1() error {

  22. if err := fn2(); err != nil {

  23. return fmt.Errorf("call fn2 failed, %s", err)

  24. }

  25. return nil

  26. }

  27. func main() {

  28. if err := fn1(); err != nil {

  29. fmt.Println(err)

  30. }

  31. }

此程序的输出为:

  1. call fn2 failed, (12): no route to the remote address

很明显的问题是,我们在 main 函数里对 error 进行处理的时候不能进行类型判断, 比如使用 if 语句判断:

  1. if err == io.EOF { ... }

或者进行类型断言:

  1. if pe, ok := err.(*os.PathError); ok { ... pe.Path ... }

它是一个 RpcError 还是 io.EOF? 无从知晓。一大串的错误信息,人类可以很好地理解,但对于程序代码来说就很困难。

error inspection

草案引入了一个 error wrapper 来包裹错误链, 它相当于一个指针,将错误栈链接起来:

  1. package errors

  2. // A Wrapper is an error implementation

  3. // wrapping context around another error.

  4. type Wrapper interface {

  5. // Unwrap returns the next error in the error chain.

  6. // If there is no next error, Unwrap returns nil.

  7. Unwrap() error

  8. }

每个层级的 error 都实现这个 wrapper,这样在 main 函数里,我们可以通过 err.Unwrap() 来获取下一个层级的 error。另外,草案引入了两个函数来简化这个过程:

  1. // Is reports whether err or any of the errors in its chain is equal to target.

  2. func Is(err, target error) bool

  3. // As checks whether err or any of the errors in its chain is a value of type E.

  4. // If so, it returns the discovered value of type E, with ok set to true.

  5. // If not, it returns the zero value of type E, with ok set to false.

  6. func As(type E)(err error) (e E, ok bool)

error formatting

有时候我们需要将错误信息分类,因为某些情况下你需要所有的信息,某些情况下只需要部分信息,因此草案引入了一个 interface:

  1. package errors

  2. type Formatter interface {

  3. Format(p Printer) (next error)

  4. }

error 类型可以实现 Format 函数来打印更详细的信息:

  1. func (e *WriteError) Format(p errors.Printer) (next error) {

  2. p.Printf("write %s database", e.Database)

  3. if p.Detail() {

  4. p.Printf("more detail here")

  5. }

  6. return e.Err

  7. }

  8. func (e *WriteError) Error() string { return fmt.Sprint(e) }

在你使用 fmt.Println("%+v", err)打印错误信息时,它会调用 Format 函数。

反馈渠道



有疑问加站长微信联系(非本文作者)

本文来自:微信公众平台

感谢作者:Go中国

查看原文:Go2设计草案介绍

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1233 次点击  
加入收藏 微博
下一篇:Go log 日志
5 回复  |  直到 2018-09-04 10:56:58
ddxx11223
ddxx11223 · #1 · 7年之前

看得眼睛太疼了。。。

freboat
freboat · #2 · 7年之前

re, 楼主重新排下版,这个斑马形式代码,看着好累

关于第一个contract是不是意味这,这括号列表。。

func (this Type) fuck(type T Addable) (foo input) (bar returnv) {...}

万一后面的返回的是一个类似的函数,我该如何读懂阿

go那帮人打算把它搞成lisp吗

polaris
polaris · #3 · 7年之前

@ddxx11223 @freboat 处理了

chuqq
chuqq · #4 · 7年之前
polarispolaris #3 回复

@ddxx11223 @freboat 处理了

image.png

还是乱的?

chuqq
chuqq · #5 · 7年之前

假设 append 函数并不支持 string 类型,就可能会出现编译错误。

我看c++和java好像都不要求模板声明时对类型有哪些依赖(contract),golang不能简单点么

就比如c++在模板里就直接用+操作,如果类重载了+就可以用,如果没重载,就编译失败呗。

golang的设计是不是太追求声明契约了,连模板类型都要声明契约。

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传