要学 Go 的赶紧上车

2020-10-03 12:59:17 +08:00
 kidlj

Go 预计在 2021 年开始支持范型,那个时候代码对新手的复杂度将提升一个量级。当前 Go 语言只有一种明显的抽象,就是 interface (接口),范型是另一种,而且比接口要更难以理解和编写、阅读。

2017 年我决定转 Go 的时候,调查了社区的成熟度,比如 kafka,redis,mongo,mysql,pg 等等中间件的库是否完善,那时候这些该有的都已经有了。现在又过了三年,Go 的社区更加成熟了,特别是 Docker 和 Kubernets 主导的云原生生态的崛起,Go 语言占了半壁江山还多。

前一阵坐我背后的一个写 Python 的同事在学习 Go,问我一个简单的问题。一个函数的返回值是 error 类型,被他当作是返回变量,因此代码看不懂。这可能是只写过动态类型语言( python, javascript, php 等)都要面对的一个思维转变。这个时候学习 Go,除了静态类型的思维转变以及 interface 这一层抽象,你会发现 Go 大部分时候像一个动态类型的语言,而且特性非常少(比 Python 少得多),Go 开源代码和标准库也非常 accessible 。

不过当范型推出来以后,虽然从 draft design 来看范型的设计已经非常简单,但对于没接触过静态类型语言的同学来说这是一个不小的挑战,甚至函数的签名都难以分辨。因此这是一个肉眼可见的复杂度提升。而且可以预计的是,当范型可用的时候,社区里大量的开源项目和库将迁移到范型的实现(因为代码更紧凑和通用),我觉得那个时候代码不会像当前一样 accessible 。

所以这个时候上车,用大约半年到一年时间熟悉 Go 的静态类型和 interface,等到范型推出的时候,可以比较轻松地过渡过去。

下面是一些学习资料的推荐:

  1. <The Go Programming Language> 是 Brian Kernighan 和一位 Go 开发组成员 Alan 合写的一本书。虽然这本书出来好几年了,但是 Go 从 1.0 以后就没怎么变化,所以还是很适用。推荐阅读英文版,中文版在大部分时候翻译得不错,但是在一些难以理解的部分,翻译并不能让事情更容易理解,甚至还会出现错误。
  2. Web 框架推荐 Echo 。写过 Node.js 的同学会发现,Echo(或 gin ) 就是 Express 或 Koa 的翻版,都是 middleware 和 c.Next 的执行方式,属于极简框架,很容易上手。
  3. Go 的并发很简单,只有 Goroutine 和 channel 一种方式。官方出的那本书里讲解得非常清晰,必读。不过一开始如果理解起来有困难的话,甚至可以跳过,因为很多时候不太用得着。比如用 Echo 框架来写业务,大部分时候不涉及并发,并发是在 Echo 框架的层面实现的(一个请求一个 goroutine ),所以业务代码就不需要并发了。
  4. Go modules 很好用。新推出不久的 pkg.go.dev 可以查询某个库被哪些开源项目 import,可以方便地学习这个库的使用方式。比如想看哪些开源项目使用了 Echo 框架,点开 Echo 的 import by tab 就看到了。这里是示例: https://pkg.go.dev/github.com/labstack/echo/v4?tab=importedby

我写 Go 有两年时间,肯定不算是一个 Go 的高手,但也不害怕去阅读一些大型项目的代码,比如 Kubernetes 。这就是 Go 的魔力,very accessible 。就像上面说的,当范型被大量运用以后,难度应该要提高一个量级。这是我的一点点经验,分享给大家,希望有帮助。

24606 次点击
所在节点    Go 编程语言
180 条回复
Nugine0
2020-10-04 11:20:42 +08:00
@reus 根据你的介绍,这个 io.Reader 显然是没有 union type 带来的设计失误。

按正常人的思维,调用一个接口要么成功,要么失败,应该用 union type 。但 (42, io.EOF) 算什么?叠加态?

你可以列很多理由说明这个设计合理,但改变不了它反人类的事实,它的语义和正常函数是不同的。这就是额外的心智负担,说好的大道至简呢?

go 阅读起来也不是那么容易。检查完 err 不检查空指针,直接挂了。调用栈追下去,一个 interface{}拍脸上,得,全动态了。

go 用看似简单的语法给人快速入门的错觉,真以为什么人都能看懂大项目?真以为换条赛道就能马上超车?路上的坑一个也逃不了,全要挨个踩过去。
mywaiting
2020-10-04 11:37:37 +08:00
世界上只有两种语言,一种没人用,另一种被喷成渣
Nugine0
2020-10-04 12:02:27 +08:00
@reus 再说 rust 。
non_exhaustive 归 lint 管,上游新增了字段会被 lint 查出来,可以自动化。
<https://rust-lang.github.io/rust-clippy/master/index.html#wildcard_enum_match_arm>
讲真的,不会有编程语言连这种程度的 lint 都做不到吧。

有异步运行时能把同步 IO 转异步 IO,依赖的就是非阻塞和 WouldBlock 。
Rust 异步运行时依靠社区,没 go 那么统一,某种程度上是弱点,但也给出了自由空间。
reus
2020-10-04 12:20:58 +08:00
@Nugine0 能不能不要双标?你不会看前面有人给出的 rust 代码和我给出的 go 代码?
rust 的 match 有四个 case,go 的 if 有三个。你要是说 go 这种设计反人类,那 rust 同样也反人类。
正常函数?你自己不适应就叫不正常?
要么成功要么失败才正常,那 Ok(0), Ok(n), Err(ErrorKind: Interrupted), Err(e),四种情况,符合你不反人类的标准吗?没有心智负担是吧?
说好的 union type 呢?怎么还一个套一个的?怎么不用同一个 union type 表达所有情况?
能不能不要双重标准?

lint ?前面有人说了是语言机制,到你这里倒成了需要 linter 来做了?
你要是认为 linter 也算是机制的一部分,那还有什么好谈论的? go 也有一堆 linter,前面说的什么忘记检查等等,全都可以提示。

明明就是狭隘偏执,非要说得自己代表全人类,和自己熟悉的设计不同,就是反人类了,好大的脸!
est
2020-10-04 12:44:01 +08:00
golang 创始人口味和大多数 crud boy 的不符。
reus
2020-10-04 12:51:58 +08:00
@Nugine0 一个函数可以返回多个值,你是缺乏这种思维。
你缺乏的思维,就是不正常的思维?
42, io.EOF 的意思是,读了 42 字节,并且已经到达字节流的末尾,不用再读,很难理解吗?
换成单返回值,就需要两次调用,一次返回 42,一次返回 0 。我前面已经说过,go 这种设计可以节省一次调用。
“要么成功,要么失败”,显然是错的,rust 的成功,也包含两种成功,一种表示 EOF,一种表示读了一些。rust 的失败,也不止一种失败,而且 Interrupted 是一种特殊的失败,它并不是真的失败,只是暂时的失败。
io 本来就有固有的复杂度,不要以为抬出 union type 就是银弹,明明就不是。
我不是说 rust 的设计有问题,rust 的设计反映了 io 本身的复杂。有问题的是你们这些人,无视事实,非要用不用标准,褒扬一个贬低另一个。
Jirajine
2020-10-04 13:36:02 +08:00
@reus 首先,这个 interrupt 是因为 rust 的标准库接口暴露出了更多的底层复杂度,和语言的设计没太大关系。
如果用 tokio::io::AsyncReadExt 代替 std::io::Read 的话,就和 go 一样不需要考虑底层细节了。

至于 union type,绝大多数使用情况下,当你调用完一个函数,应该成功时返回结果,失败时返回错误(原因)。当你检查完 if err!=nil 之后,一般情况是认为 既然 err 是 nil,那 val 就不是 nil,如果这个函数写的有问题的话,就可能同时返回(nil,nil),接下来你没有额外检查的话就会遭遇臭名昭著的"NullPointerException"。
要是同时检查 val 和 err 是否为 nil 的话,那就太过 verbose 了。
以及那个 check 语法糖也没实装,当嵌套调用函数 /方法的时候就得一遍遍的复制粘贴,这显然也是语言设计失败的地方。

而 union type 则从语言层面确保了这一点,而不是仅靠程序员的约定俗成,只要返回 Ok(val),那 val 就能够确保不会是空。
nullable 是 go 里很有问题的一个方面,使用 union type Option<T>也能够很优雅的解决这个问题。

再说 error,go 的 error 类型也是比较残废的,你只知道它返回 error,却不知道可能返回哪些 error 。rust 除了 non_exhaustive 这种刻意让你 fallback 处理的情况外,是能保证你显式的处理(或显式的忽略)了每一种可能的错误,不会出现 unexpected error,与 Java 的 checked exception 有异曲同工之处。

我这里说的糟粕,主要是指语言设计层面上。goroutine 则可以说是 go 最大的亮点,可惜 tokio 没法抄过来,async 带来了大量的复杂度,远没有 goroutine 写的爽快。
reus
2020-10-04 14:06:46 +08:00
@Jirajine 你在说啥?以 io.Reader 为例,返回类型是 (int, error),哪来什么 null pointer ?
就算返回的是指针,且函数有问题,那后面如果遇到解指针,那就会直接 panic,表明程序需要改,是不需要写代码做检查的。
rust 你以为就没有问题?你看看多少人直接用 unwrap 吧,编译器可不会禁止你用。

如果你认为需要一遍又一遍复制,那你不会写成函数吗?这么简单的减少代码的技巧都不会吗?

说了半天你都没明白,union type 没法保证什么吗?
明明 rust 返回的 Ok(val),就分成 val = 0 和 val > 0 两种情况。既然抬出 union type,怎么不定义一个类型,分开这两种情况? Ok(val) 分成 Ok(n) 和 Zero,这样编译器才知道你必须处理返回 0 的情况。问题就是,rust 不是这么设计的。明明就是需要约定俗成检查 0,明明就是约定俗成需要检查 Interrupted 。比 go 优雅在哪里?我看不到。
你不处理 Ok(0),编译器报错吗?你不处理 Interrupted,编译器报错吗?

go 里你不需要知道返回哪些 error,你只需要知道你能处理哪些 error,把你能处理的处理掉,不能处理掉的就直接返回,就行了。java 的 checked execption 不知道多少人批评,有什么好学的?

例如一个函数,调用另一个函数,且知道可以处理 ErrFoo 和 ErrBar 类错误,其他不管,是这样写的
err := fn()
if e := ErrFoo{}; as(err, &e); {
// 处理 ErrFoo 类错误
} else if e := ErrBar{}; as(err, &e); {
// 处理 ErrBar 类错误
} else {
// 不知道怎么处理,直接返回
return err
}

处处都想要处理任何错误,本身就是个伪需求。
chibupang
2020-10-04 14:36:04 +08:00
有什么好争的,语言就是工具而已,真不喜欢 xx 语言的某个特性你可以不用它,非要恶心自己干嘛呢?如果不得不用,那你就尝试去改变它呀。如果没本事决定自己用什么语言,又没能力去改变它,还恶心自己,那可能不是语言本身的问题了。( again,语言不过就是工具
Jirajine
2020-10-04 15:07:30 +08:00
@reus io 这里是比较特别的情况,你一般封装的函数不会出现正确和错误的叠加态。
有问题是指会在某些 rare case 同时返回 nil,这种运行时错误难以排查,能够在编译时就解决当然要好的多。

unwrap()也不是语言的问题吧,你用 go 的库代码要是出错不上抛无脑 panic()不也是坑么。

每次拿函数 /方法返回值使用的时候都得一遍遍的复制 if err!=nil {return nil,err} 这怎么解决?

当然一般情况不会处理每一种错误,但能和不能还是有区别的。像你这样把其他错误抛上去,那上层呢?你多抛几个来源不通的错误,上层没法知道有哪些错误他可以处理,再往上抛,这样层层叠加。最后你根本没办法区分,最终最外层只能包个 try catch 那样记录个日志然后退出。
如王垠讲的那样 https://www.yinwang.org/blog-cn/2017/05/23/kotlin,checked exception 好处是毫无疑问的,而类型系统保证的 Error type 得到同样好处的同时却不像 chacked exception 那样在实践中有那么多问题。
XIVN1987
2020-10-04 15:35:42 +08:00
我觉得 Go 相对于 Java 并没有不得了的优点
都是带 GC 的静态语言,唯一重大的区别是 Java 执行需要虚拟机,而 Go 直接执行机器码,,不过对于桌面应用和服务器来说,几十 M 的虚拟机并不造成严重的劣势,,而且虚拟机本身也会使得一些程序语法更容易实现

另外,说 Go 简单,,那是因为 Go 还年轻,,如果 Go 变得像 Java 一样主流,,那么它一定会添加越来越多的特性,,到时候就不简单了
比如争论了好几年的泛型,,现在不是马上要加了吗?至于异常处理,谁又敢说以后肯定不会加?
ntgeralt
2020-10-04 15:54:21 +08:00
其实我想问,github win 版用什么编译出来,感觉运行很流畅啊
reus
2020-10-04 16:21:57 +08:00
@Jirajine rust 不能解决空指针的问题,解一个空指针,和对 None 用 unwrap 是同样性质的事情。既然你认为 unwrap 不是语言的问题,怎么到 go 里解空指针,就成了语言的问题?另外,go 里空指针一样可以调用方法,只要没有解指针的操作,那就是没问题。

我不想和你谈 checked exception,别的地方已经有很多讨论,也远远说不上“毫无疑问”: https://www.zhihu.com/question/60240474
王垠很多观点都是错的。
cmdOptionKana
2020-10-04 16:55:55 +08:00
@ntgeralt 和 vscode 一样,基于 Electron 。
GBdG6clg2Jy17ua5
2020-10-04 18:23:48 +08:00
先不学了,包管理器已经出来了。我再等等泛型。然后再等等异常处理。估计,就差不多了
Jirajine
2020-10-04 18:37:42 +08:00
@reus Option<T>当然能解决空指针的问题,unwrap()是用户主动的,而 go 里指针全部可空,相当于在所有解引用操作时自动调用 unwrap(),当然是语言的问题。
rust 的类型系统就可以在编译时就确保你必须显式处理 None 的情况,无论是用 pattern match,组合器,还是调用 unwrap()告诉编译器我保证此值不空。不然你根本拿不出来里面的值。
两者最大的区别是,当你认为一个指针非空而对它进行解引用,它可能是空从而让你 nullpointerexception.
而 rust 只要你拿到 Option<T>里面的值,就可以确保它一定非空。如果你忘了处理,压根不会通过编译。
kotlin 里面也有区分 nullability 的类型系统,而 rust 直接通过 enum 就达到了同样的效果。

至于 checked exception,我上面说的是,它的好处是毫无疑问的,而它的问题(如导致接口无法向下兼容)使用 error type 也可以避免。
reus
2020-10-04 19:43:01 +08:00
@Jirajine 是吗?
这是 github 搜索 unwrap panic language:Rust 的结果: https://github.com/search?l=&q=unwrap+panic+language%3ARust&type=issues
上万个相关 issue,你确定 rust 可以阻止开发者不当使用 unwrap ?
no1xsyzy
2020-10-04 20:15:48 +08:00
@reus #113 没多少时间,看前两个回答其实也很半桶水。
一个是 Java 的 Function 接口定义没写完善的问题,只支持了输入侧的逆变没支持输出侧的协变,因为没有考虑到 throws 也是毫无疑问的返回值的一种,还是 Either 的一种不完整表达所致。
第二个也是实践上的错误,Exception 分类应当是嵌套封装而不是继承封装,否则永远只能支持协变(至于双变在语言层面上就不支持),对错误处理即调用方不友好,只是目前来说这个错误从 Java 开始一直持续到现在…… 封装一个 Error Code 进去就是正确途径的一种拟似。
话说说到底,错误处理把逆变和协变关系给调转了是问题所在,至于再深究下去,根本问题是范畴论与变量的变属性兼容比较诡异。

我的建议是把一些错误处理改换成 Algebraic effects
no1xsyzy
2020-10-04 20:20:54 +08:00
@no1xsyzy 没写完发出来了,我的建议是把一些错误处理改换成 Algebraic effects,一些错误处理以返回值论。
这样就可以通过注入一个 handler 来手动调整全局的 EOF 行为。
Jirajine
2020-10-04 20:23:50 +08:00
@reus #117 搜 nil pointer language:go 还有 4 万个 issues 呢,这样比较没多大意义。

这玩意就像防火墙,谁都不能阻止你手动关了它作死,但比起完全没有保护措施你不能说它没用。

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

https://yangjunhui.monster/t/712344

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

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

© 2021 V2EX