要学 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 。就像上面说的,当范型被大量运用以后,难度应该要提高一个量级。这是我的一点点经验,分享给大家,希望有帮助。

24289 次点击
所在节点    Go 编程语言
180 条回复
Jirajine
2020-10-03 18:44:53 +08:00
@reus 绝大多数使用场景这两个值就是应该有一个空,而 go 没有机制保证这一点。成功就是成功出错就是出错,不能说执行了一半错了一半。这个 io.EOF 场景恰好就是体现 go 错误处理糟糕的一个地方,你不但要检查 err!=nil,还得检查 err!=io.EOF ,读完了和读不了显然得不同处理。
rust 里 Read::read(&mut self,buf:&mut [u8])->Result<usize> 遇到 EOF 直接返回当前已经读取的长度,下次再调用立即返回 0,其他种类的错误既可以通过带有 exhausted check 的 enum match 确保所有情况都显式处理,也可以直接?语法糖轻松向上抛,并且得益于类型系统还带有 Java 中 checked exception 等价的效果。
12101111
2020-10-03 18:45:58 +08:00
@reus 不知道谁是半桶水,你可以看看`man 3 read`, EOF 是怎么确定的,
No data transfer shall occur past the current end-of-file. If the
starting position is at or after the end-of-file, 0 shall be returned.
If the file refers to a device special file, the result of subsequent
read() requests is implementation-defined.
所以 42,EOF 是怎么返回的?
go 的标准库设计的很没有品味, 很多东西都模模糊糊处理,在一些 corner case 就会出现很奇怪的行为.
go 缺少一种 rfc 机制,导致她的设计终究是跟着 Google 的需要而不是社区的需要走的,这也限制了她像 C 一样可以流行 50 年并且可以预料的再流行至少 20 年.
RickyC
2020-10-03 18:49:26 +08:00
我好像阅读障碍, 对于这么长的文章读不下去.
mangogeek
2020-10-03 18:53:25 +08:00
挺喜欢够浪(golang)
我们嵌入式网关里已经转 go 做业务了, 爽的一逼
AJQA
2020-10-03 18:54:48 +08:00
各位熟练 go 的
recover 只能用于 panic 对不对 总共有几种情况会产生 panic ?
比如调用 ioutil.WriteFile("a.txt",[]byte("sdfsd"),0644) 如果对当前目录没权限 会返回 err, printf 出来是 Permission denied
所以只要写 go 就无法避免检查返回值 漏检查了导致问题 会很难排查出来
reus
2020-10-03 19:06:06 +08:00
@12101111 我讨论的是 go 标准库定义的 io.Reader 接口,你拿 posix read 的文档来说个屁啊,根本就是两回事,看到都叫 EOF 就以为是一回事吗?不懂能不能别插嘴?
io.Reader 文档: https://pkg.go.dev/io#Reader
When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, it returns the number of bytes read. It may return the (non-nil) error from the same call or return the error (and n == 0) from a subsequent call.
定义得清清楚楚。
真的,不懂就闭嘴。王垠都还学习过一下,称得上半桶水,你是根本不懂,生搬硬套。
fpure
2020-10-03 19:12:26 +08:00
如果有一门支持 GC 的 Rust 语言就好了
reus
2020-10-03 19:17:53 +08:00
@Jirajine 难道 rust 可以保证你检查了返回值是 0 ? rust 只是多了?这个语法糖而已,难道就不是检查错误且要检查返回值是 0 ?
另外,我看你也不怎么熟悉 rust,io::ErrorKind 明明是标记为 non_exhaustive,文档里也明确写了 This list is intended to grow over time and it is not recommended to exhaustively match against it.
你说的是错的。
reus
2020-10-03 19:34:01 +08:00
@Jirajine Read::read 文档里还有一个约定:An error of the ErrorKind::Interrupted kind is non-fatal and the read operation should be retried if there is nothing else to do.
按照你的标准,这算不算糟糕?你要检查有没有错,如果有错,是不是 Interrupted,要不要重试,还要检查有没有读完,你真的可以随时用 ? 这个语法糖吗?
文档里的这个约定,是用语言机制保证的,还是仅仅由文档来约定的?不看文档,你知道要特别处理 Interrupted 吗?
coetzee
2020-10-03 19:38:25 +08:00
1:golang 粉丝太强势,楼上可以参看
2:大道至简,缺少大量语法糖,是优势也是劣势
3:gorotine 是最大的优点
4:go 的确是很重要,我觉得学一下总没坏处啊,学习这玩意儿别太偏激,跟随下潮流,Go,Rust,Java 谁更牛我不知道,反正都学一下呗,自己感兴趣哪个用哪个,小孩子才分对错,又不是以后不能切换。
5:golang 失去了现代语言很多特性,这点来看见仁见智
MarioLuo
2020-10-03 19:58:07 +08:00
@cmdOptionKana 异常影响太大,整个第三方库,而且 error 和异常混合使用容易造成混乱
Jirajine
2020-10-03 20:15:10 +08:00
@reus 所以 rust 就可以在一个循环里连续读,返回值为 0 为中止条件。

exhaustive 是编译器保证的,能确保当前情况确实是 exhausted,以后再加了编译会报错提醒你修改。
而 go 的错误类型就残废的多,没有办法保证你处理了每一种错误。
你当然可以不额外处理,有错就返回。go 你不看文档不也是直接 err!=nil 返回么。
额外处理的语法也比 go 要优雅:

loop {
match p.read(&mut buf){
Ok(0)=>break,
Ok(i)=>{//do something},
Err(ErrorKind::Interrupted)=>{//handle interrupt},
Err(e)=>{//handle other error}
}
}
clocktree
2020-10-03 20:17:47 +08:00
赞,才知道 import by 这个功能
EminemW
2020-10-03 20:38:04 +08:00
@mangogeek #64 其实 golang 念 go language
chihiro2014
2020-10-03 20:48:45 +08:00
我觉得还是 java 的 jdk 代码写的优雅
reus
2020-10-03 20:48:53 +08:00
@Jirajine 都说了 io::ErrorKind 标记了 #[non_exhaustive],编译器并不会检查。rust 也不能保证你处理了每一种情况。

go 这样写:
for {
n, err := r.Read(buf)
if n > 0 {
//handle data
}
if is(err, io.EOF) {
break
}
if err != nil {
// handle error
}
}

不是比你那样更简洁?
12101111
2020-10-03 21:01:19 +08:00
@reus It may return the (non-nil) error from the same call, 这个情况在底层是 posix IO 的情况下是不可能发生的,所以说 union type 完全可以表达正常的语义,不需要 int 和 err 同时出现.posix IO 是使用最广的 IO,因此 go 的 Reader 接口设计的有问题, 凭空出现了本应不存在的情况

另外 Rust 里标注#[non_exhaustive]的 enum 在 match 时没有`_`来匹配将来可能新增的字段是会编译错误的,因此不需要看到文档里写这句话也应该知道.用到了,编译不过,rustc 报错提示你 match 分支必须有_,自然就知道了.

如果你要读完那不应该用 read,应该用 read_to_end, 文档里写的很清楚,read_to_end 会忽略 ErrorKind::Interrupted.我不知道别人怎么写,但是我用写的命令行程序里 read_to_end 是直接用?抛到 main 然后直接输出错误退出程序的.磁盘 io 错误也没法处理,但是各种网络库里就要仔细处理了.

在 read 里暴露 ErrorKind::Interrupted 和 ErrorKind::WouldBlock 是因为异步运行时需要靠这个来进行异步 IO 操作.Rust 暴露很多细节是必要的,因为作为系统语言 Rust 允许程序员和最底层进行交互.go 搞有栈协程,因此这种 no blocking IO 的东西看起来很没有用.但是 go 程序员可不能在不改造编译器的情况下修改 go 的运行时.
reus
2020-10-03 21:26:10 +08:00
@12101111 posix 有没有,关 go 的 io.Reader 什么事? go 支持一堆 non-posix 的平台。而且实现了 io.Reader 的类型,又不限于系统 io 。它是个 byte stream 接口,和 posix io 毫无关系。这个设计允许少一次 Read 调用,难道和 posix 不同,就叫有问题?

没错啊,你的 match 里有 _ ,然后后面新增了其他 kind,编译器不会报错,你就不知道新增了一个 kind,你就不知道需要特别处理这个新增的 kind 。前面有人不是说 rust 会提示你处理所有情况吗?我就明确说了,不会。你只能用兜底的分支去处理,你不看文档,你就不知道多了。

read_to_end? 你内存只有 32G,要你处理一个 128G 的文件,你也用 read_to_end?

看看 tokio 从 go 的运行时学来多少东西吧,rust 只不过把运行时的实现扔出去了而已,你用了 async,你就撇不开 async executor,这就是和 go 类似的运行时,也离不开 M:N 式的调度器。
nnws2681521
2020-10-03 21:31:05 +08:00
就想知道,你英语为何这么厉害
WispZhan
2020-10-03 21:36:05 +08:00
就 go 这设计,早就翻车了。 泛型语法的设计直接劝退。

而且 go 本身就完全不适合写业务逻辑代码,就只适合写一些中间件或者 Cloud Native 应用。
一直不明白拿 go 写 web 的人是啥想法(特指写业务代码的)

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

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

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

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

© 2021 V2EX