Go2 设计草案介绍

2018-08-29 20:19:39 +08:00
 songtianyi

写下自己的理解。大家都在吐槽泛型,我觉得还行吧,contract 的表达能力蛮强的。

原文链接 https://github.com/songtianyi/songtianyi.github.io/blob/master/mds/techniques/go2-design-draft-introduction.md

go2 设计草案介绍

作者: songtianyi 2018-08-29

前言

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:

package main

import "fmt"

func Keys(m map[interface{}]interface{}) []interface{} {
	keys := make([]interface{}, 0)
	for k, _ := range m {
		keys = append(keys, k)
	}
	return keys
}

func main() {
	m := make(map[string]string, 1)
	m["demo"] = "data"
	fmt.Println(Keys(m))
}

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

package main

import "fmt"

func Keys<K, V>(m map[K]V) []K {
	keys := make([]K, 0)
	for k, _ := range m {
		keys = append(keys, k)
	}
	return keys
}

func main() {
	m := make(map[string]string, 1)
	m["demo"] = "data"
	fmt.Println(Keys(m))
}

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

// rust
fn print_g<T: Graph>(g : T) {
    println!("graph area {}", g.area());
}

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

// comparable contract
contract Equal(t T) {
	t == t
}
// addable contract
contract Addable(t T) {
	t + t
}

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

func Sum(type T Addable(T))(x []T) T {
	var total T
	for _, v := range x {
		total += v
	}
	return total
}

var x []int
total := Sum(int)(x)

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

total := Sum(x)

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

func Sum(type T Addable)(x []T) T {
	var total T
	for _, v := range x {
		total += v
	}
	return total
}

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

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

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

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

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

错误处理

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

先看下目前 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)
	}
}

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

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

r := check os.Open(src)

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

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

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

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

修改后的代码为:

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

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

反馈渠道

错误值

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

package main

import (
	"fmt"
	"io"
)

type RpcError struct {
	Line uint
}

func (s *RpcError) Error() string {
	return fmt.Sprintf("(%d): no route to the remote address", s.Line)
}

func fn3() error {
	return io.EOF
}

func fn2() error {
	if err := fn3(); err != nil {
		return &RpcError{Line: 12}
	}
	return nil
}
func fn1() error {
	if err := fn2(); err != nil {
		return fmt.Errorf("call fn2 failed, %s", err)
	}
	return nil
}
func main() {
	if err := fn1(); err != nil {
		fmt.Println(err)
	}
}

此程序的输出为:

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

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

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

或者进行类型断言:

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

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

error inspection

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

package errors

// A Wrapper is an error implementation
// wrapping context around another error.
type Wrapper interface {
	// Unwrap returns the next error in the error chain.
	// If there is no next error, Unwrap returns nil.
	Unwrap() error
}

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

// Is reports whether err or any of the errors in its chain is equal to target.
func Is(err, target error) bool

// As checks whether err or any of the errors in its chain is a value of type E.
// If so, it returns the discovered value of type E, with ok set to true.
// If not, it returns the zero value of type E, with ok set to false.
func As(type E)(err error) (e E, ok bool)
error formatting

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

package errors

type Formatter interface {
	Format(p Printer) (next error)
}

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

func (e *WriteError) Format(p errors.Printer) (next error) {
	p.Printf("write %s database", e.Database)
	if p.Detail() {
		p.Printf("more detail here")
	}
	return e.Err
}

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

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

反馈渠道

8107 次点击
所在节点    程序员
73 条回复
vigoss
2018-08-30 11:32:52 +08:00
这个泛型的写法也太复杂了吧。

ps 帮配音 @glues: "怎么了我胖虎说的有毛病吗?"
glues
2018-08-30 11:54:02 +08:00
@artandlol 我胖虎想 @ 谁就 @ 谁 (逃。。
Biebe
2018-08-30 12:09:39 +08:00
@loqixh gitlab 不只有一个 rails,其余中间件如 gitaly shell 都是 go 写的
ifaii
2018-08-30 12:28:26 +08:00
最大黑点就是错误处理 简直不能再烂
jlkm2010
2018-08-30 12:43:48 +08:00
还有人在这里杠,就足以说明 go 是主流语言之一了
rahuahua
2018-08-30 12:49:53 +08:00
@loqixh 可以找各个大公司的招聘看看,基本上都有 go 的职位,如果这都不算主流的话,那只有 c/c++/java 才能算主流了
simpx
2018-08-30 14:39:10 +08:00
go2 的风格,有从 c 变成 c++的趋势
cchange
2018-08-30 15:22:40 +08:00
看来把 C++中不复杂的内容学通了就可以了 语言这些太多包袱了
karllynn
2018-08-30 15:30:02 +08:00
屏蔽了某个杠精,神清气爽

我觉得这个改动可以啊,contract 给一个最简单的泛型,够用了
iRiven
2018-08-30 16:05:27 +08:00
泛型不太懂,看了官方的包各种类型判断,贼爽,至于错误处理还行,不是很理想,但有好过没有,后面的错误是扩展在 errors 包里还是直接在默认 error 类型上面扩展一个函数
rockyou12
2018-08-30 16:15:56 +08:00
泛型就学人家用尖括号定义真的不行吗……原版的 map 和 slice 的类型写着我都感觉挺难受了……

而且错误处理虽然没有 try/catch 的多层嵌套,但读的时候却是和业务顺序是反的,感觉好不适应,估计也只能多写一些菜知道好不好了……
songtianyi
2018-08-30 16:50:46 +08:00
@iRiven 可以说说哪里不懂嘛?
songtianyi
2018-08-30 17:00:04 +08:00
janxin
2018-08-30 17:09:14 +08:00
这次的 handle 明显就是一种 try catch 机制老的,就是嵌套关系十分不明显,后续程序代码理解时就难受了
psuwgipgf
2018-08-30 17:42:36 +08:00
@janxin 嗯,我也感觉不如 try catch 好用,可能是因为我是个 android 开发吧。不知道 其它流行语言是怎么处理这种情况的
songtianyi
2018-08-30 19:31:41 +08:00
@psuwgipgf try catch 是主流,go 的做法有点像 rust,但类型系统比不上 rust,所以看起来蹩脚
songtianyi
2018-08-30 19:38:23 +08:00
@janxin 的确,有一种 goto 的感觉,debug 和读起来比较费劲。
iceheart
2018-08-30 22:07:54 +08:00
check handle 一点也不简洁。
check 完全可以省掉。
比如这样
func service1()(string,error){}
func example() error {
handle error1 {
.....
}
res1, error1 := service1()//error != nil 时 触发 error1
...
}
janxin
2018-08-31 06:21:16 +08:00
@iceheart 最大的问题应该还是 go1 代码兼容问题。基于目前的方案,go1 的代码可以直接用在 go2 上,并且编译器无需大改。
janxin
2018-08-31 06:23:41 +08:00
@iceheart 而且还不需要考虑变量命名问题 error2 怎么办,比如可能出现的作用域问题等等

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/484373

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX