写点什么

Go 语言很好很强大,但我有几个问题想吐槽

  • 2019-04-01
  • 本文字数:4122 字

    阅读完需:约 14 分钟

Go语言很好很强大,但我有几个问题想吐槽

Go 是一门非常不错的编程语言。然而,我在公司的 Slack 编程频道中对 Go 的抱怨却越来越多(猜到我是做啥了的吧?),因此我认为有必要把这些吐槽写下来并放在这里,这样当人们问我抱怨什么时,我给他们一个链接就行了。



先声明一下,在过去的一年里,我大量地使用 Go 语言开发命令行应用程序、scclc和 API。 其中既有供客户端调用的大规模 API,也有即将在https://searchcode.com/ 使用的语法高亮显示器


我这些批评全部是针对 Go 语言的。但是,我对使用过的每种语言都有不满。 我非常赞同下面的话:


“世界上只有两种语言:人们抱怨的语言和没人使用的语言。” —— Bjarne Stroustrup

1 不支持函数式编程

我并不是一个函数式编程狂热者。 说到 Lisp 语言,我首先想到的是语言障碍。


这可能是 Go 语言最大的痛点了。 与大部分人不同,我不希望 Go 支持泛型,因为它会为多数 Go 项目带来不必要的复杂性。 我希望 Go 语言支持适用于内置切片和 Map 的函数式方法。 切片和 Map 具有通用性,并且可以容纳任何类型,从这个意义上讲,它们已经非常神奇。在 Go 语言中只有利用接口才能实现类似效果,但这样一来将丧失安全性和速度。


例如,请考虑下面的问题。


给定两个字符串切片,找出二者都包含的字符串,并将其放入新的切片以备后用。


existsBoth := []string{}for _, first := range firstSlice {  for _, second := range secondSlice {    if first == second {      existsBoth = append(existsBoth, proxy)      break    }  }}
复制代码


上面是一个用 Go 语言实现的简单方案。当然还有其它方法,比如借助 Map 来减少运行时间。这里我们假设内存足够用或者切片都不太大,同时假设优化运行时间带来的复杂性远超收益,因此不值得优化。作为对比,使用 Java 流和函数式编程把相同的逻辑重写如下:


var existsBoth = firstList.stream()                .filter(x -> secondList.contains(x))                .collect(Collectors.toList());
复制代码


上面的代码隐藏了算法的复杂性,但是,你更容易理解它实际做的事情。


与 Go 代码相比,Java 代码的意图一目了然。 真正灵活之处在于,添加更多的过滤条件易如反掌。 如果使用 Go 语言添加下面例子中的过滤条件,我们需要在嵌套的 for 循环中再添加两个 if 条件。


var existsBoth = firstList.stream()                .filter(x -> secondList.contains(x))                .filter(x -> x.startsWith(needle))                .filter(x -> x.length() >= 5)                .collect(Collectors.toList());
复制代码


有些借助 go generate 命令的项目可以帮你实现上面的一些功能。但是,如果缺少良好的 IDE 支持,抽取循环中的语句作为单独的方法是一件低效又麻烦的事情 。

2 通道/并行切片处理

Go 通道通常都很好用。 但它并不能提供无限的并发能力。它确实存在一些会导致永久阻塞的问题,但这些问题用竞争检测器能很容易地解决。对于数量不确定或不知何时结束的流式数据,以及非 CPU 密集型的数据处理方法,Go 通道都是很好的选择。


Go 通道不太适合并行处理大小已知的切片。


多线程编程、理论和实践



几乎在其它任何语言中,当列表或切片很大时,为了充分利用所有 CPU 内核,通常都会使用并行流、并行 Linq、Rayon、多处理或其它语法来遍历列表。遍历后的返回值是一个包含已处理元素的列表。 如果元素足够多,或者处理元素的函数足够复杂,多核系统会更高效。


但是在 Go 语言中,实现高效处理所需要做的事情却并不显而易见。


一种可能的解决方案是为切片中的每个元素都创建一个 Go 例程。 由于 Go 例程的开销很低,因此从某种程度上来说这是一个有效的策略。


toProcess := []int{1,2,3,4,5,6,7,8,9}var wg sync.WaitGroup
for i, _ := range toProcess { wg.Add(1) go func(j int) { toProcess[j] = someSlowCalculation(toProcess[j]) wg.Done() }(i)}
wg.Wait()fmt.Println(toProcess)
复制代码


上面的代码会保持切片中元素的顺序,但我们假设不必保持元素顺序。


这段代码的第一个问题是增加了一个 WaitGroup,并且必须要记得调用它的 Add 和 Done 方法。这增加了开发人员的工作量。如果弄错了,这个程序不会产生正确的输出,结果是要么输出不确定,要么程序永不结束。此外,如果列表很长,你会为每个列表创建一个 Go 例程。正如我之前所说,这不是问题,因为 Go 能轻松搞定。问题在于,每个 Go 例程都会争抢 CPU 时间片。因此,这不是执行该任务的最有效方式。


你可能希望为每个 CPU 内核创建一个 Go 例程,并让这些例程选取列表并处理。创建 Go 例程的开销很小,但是在一个非常紧凑的循环中创建它们会使开销陡增。当我开发scc时就遇到了这种情况,因此我采用了每个 CPU 内核对应一个 Go 例程的策略。在 Go 语言中,要这样做的话,你首先要创建一个通道,然后遍历切片中的元素,使函数从该通道读取数据,之后从另一个通道读取。我们来看一下。


toProcess := []int{1,2,3,4,5,6,7,8,9}var input = make(chan int, len(toProcess))
for i, _ := range toProcess { input <- i}close(input)
var wg sync.WaitGroupfor i := 0; i < runtime.NumCPU(); i++ { wg.Add(1) go func(input chan int, output []int) { for j := range input { toProcess[j] = someSlowCalculation(toProcess[j]) } wg.Done() }(input, toProcess)}
wg.Wait()fmt.Println(toProcess)
复制代码


上面的代码创建了一个通道,然后遍历切片,将索引值放入通道。 接下来我们为每个 CPU 内核创建一个 Go 例程,操作系统会报告并处理相应的输入,然后等待,直到所有操作完成。这里有很多代码需要理解。


然而,这种实现有待商榷。如果切片非常大,通道的缓冲区长度和切片大小相同,你可能不希望创建一个有这么大缓冲区的通道。因此,你应该创建另一个 Go 例程来遍历切片,并将切片中的值放入通道,完成后关闭通道。 但这样一来代码会变得冗长,因此我把它去掉了。我希望可以大概地阐明基本思路。


使用 Java 语言大致这样实现:


var firstList = List.of(1,2,3,4,5,6,7,8,9);
firstList = firstList.parallelStream() .map(this::someSlowCalculation) .collect(Collectors.toList());
复制代码


通道和流并不等价。 使用队列去仿写 Go 代码的逻辑更好一些,因为它们更具有可比性,但我们的目的不是进行 1 对 1 的比较。 我们的目标是充分利用所有的 CPU 内核处理切片或列表。


如果 someSlowCalucation 方法调用了网络或其它非 CPU 密集型任务,这当然不是问题。 在这种情况下,通道和 Go 例程都会表现得很好。


这个问题与问题#1 有关。如果 Go 语言支持适用于切片/Map 对象的函数式方法,那么就能实现这个功能。 但是,如果 Go 语言支持泛型,有人就可以把上面的功能封装成像 Rust 的 Rayon 一样的库,让每个人都从中受益,这就很令人讨厌了(我不希望 Go 支持泛型)。


顺便说一下,我认为这个缺陷妨碍了 Go 语言在数据科学领域的成功,这也是为什么 Python 仍然是数据科学领域的王者。 Go 语言在数值操作方面缺乏表现力和能力,原因就是以上讨论的这些。

3 垃圾回收器

Go 的垃圾回收器做得非常不错。我开发的应用程序通常都会因为新版本的改进而变得更快。但是,它以低延迟为最高优先级。对于 API 和 UI 应用来说,这个选择完全可以接受。对于包含网络调用的应用,因为网络调用往往会是瓶颈,所以它也没问题。


我发现的问题是 Go 对 UI 应用来讲一点也不好(我不知道它有任何良好的支持)。如果你想要尽可能高的吞吐量,那这个选择会让你很受伤。这是我开发scc时遇到的一个主要问题。scc 是一个 CPU 密集型的命令行工具。为了解决这个问题,我不得不在代码里添加逻辑关闭 GC,直到达到某个阈值。但是我又不能简单的禁用它,因为有些任务会很快耗尽内存。


缺乏对 GC 的控制时常令人沮丧。你得学会适应它,但是,有时候如果能做到这样该有多好:“嘿,这些代码确实需要尽可能快地运行,所以如果你能在高吞吐模式运行一会,那就太好了。”



我认为这种情况在 Go 1.12 版本中有所改善,因为 GC 得到了进一步的改进。但仅仅是关闭和打开 GC 还不够,我期望更多的控制。 如果有时间我会再进行研究。

4 错误处理

我并不是唯一一个抱怨这个问题的人,但我不吐不快。


value, err := someFunc()if err != nil {  // Do something here}
err = someOtherFunc(value)if err != nil { // Do something here}
复制代码


上面的代码很乏味。 Go 甚至不会像有些人建议的那样强制你处理错误。 你可以使用“_”显式忽略它(这是否算作对它进行了处理呢?),你还可以完全忽略它。比如上面的代码可以重写为:


value, _ := someFunc()
someOtherFunc(value)
复制代码


很显然,我显式忽略了 someFunc 方法的返回。someOtherFunc(value)方法也可能返回错误值,但我完全忽略了它。 这里的错误都没有得到处理。


说实话,我不知道如何解决这个问题。 我喜欢 Rust 中的“?” 运算符,它可以帮助避免这种情况。V-Lang https://vlang.io/ 看起来也可能有一些有趣的解决方案。


另一个办法是使用可选类型(Optional types)并去掉 nil,但这不会发生在 Go 语言里,即使是 Go 2.0 版本,因为它会破坏向后兼容性。

结语

Go 仍然是一种非常不错的语言。如果你让我写一个 API,或者完成某个需要大量磁盘/网络调用的任务,它依然是我的首选。现在我会用 Go 而非 Python 去完成很多一次性任务,数据合并任务是例外,因为函数式编程的缺失使执行效率难以达到要求。


与 Java 不同,Go 语言尽量遵循“最小惊喜“原则。比如可以这样比较字两个符串是否相等:stringA == stringB。但如果你这样比较两个切片,那么会产生编译错误。这些都是很好的特性。


的确,二进制文件还可以变的更小(一些编译标志和upx可以解决这个问题),我希望它在某些方面变得更快,GOPATH 虽然不是很好,但也没有人们想得那么糟糕,默认的单元测试框架缺少很多功能,模拟(mocking)有点让人痛苦…


它仍然是我使用过的效率较高的语言之一。我会继续使用它,虽然我希望https://vlang.io/能最终发布,并解决我的很多抱怨。V 语言或 Go 2.0,Nim 或 Rust。现在有很多很酷的新语言可以使用,我们开发人员真的要被宠坏了。


查看英文原文:


https://boyter.org/posts/my-personal-complaints-about-golang/



2019-04-01 11:4510428

评论 1 条评论

发布
用户头像
不支持流处理的确很遗憾,go如果支持流处理我相信更加简洁。
2019-04-02 01:14
回复
没有更多了
发现更多内容

二十八分钟,带你用gitlab向企业微信发出灵魂拷问

📿

Java gitlab gitlab ci

Fluid — 云原生环境下的高效“数据物流系统”

阿里巴巴云原生

人工智能 云计算 容器 云原生 存储

项目优化-代码拆分

Darren

android 组件化 代码优化

跟单交易系统开发|跟单交易APP软件开发

系统开发

全网最全人工智能专业术语表(中英文对照)

澳鹏Appen

人工智能 大数据 数据 科技互联网 专业术语

apk优化,Android高级工程师必看系列,在线面试指南

欢喜学安卓

android 程序员 面试 移动开发

安全之路其修远兮,吾将上下而求索

Thrash

模块一课后作业

追随哆咪

架构实战营

架构实战营模块一作业

冷大大

作业 架构实战营 模块一

音频应用类开源 Demo 大盘点

anyRTC开发者

ios android 音视频 WebRTC RTC

数字货币期权交易系统开发|数字货币期权交易APP软件开发

系统开发

锁仓挖矿系统开发|锁仓挖矿APP软件开发

系统开发

秒合约交易系统开发|秒合约交易APP软件开发

系统开发

阿里的 RocketMQ 如何让双十一峰值之下 0 故障?

阿里巴巴云原生

容器 运维 云原生 k8s 消息中间件

架构实战营课程1作业

求索

学习 架构实战营

架构实战营模块1作业

半夏

学习 架构实战营

北京天源迪科上线迪科商旅App

DT极客

阿里巴巴开源容器镜像加速技术

阿里巴巴云原生

Serverless 容器 云原生 k8s 存储

七进七出,终获阿里32k*16offer,这就是我悲惨的面试经历~

Java架构师迁哥

Knative 基于流量的灰度发布和自动弹性实践

阿里巴巴云原生

Serverless 容器 开发者 云原生 k8s

Flink集成Iceberg在同程艺龙的实践

Apache Flink

flink

史上最全的Java面试题库宝典,Github上标星200k,太香了!

Java架构之路

Java 程序员 架构 面试 编程语言

双非本化学跨专业,投岗阿里/滴滴后端三面,最终拿下offer

Java 编程 程序员 架构 面试

给视频添加雪花飘落特效

老猿Python

OpenCV 音视频 图形图像处理 视频特效 引航计划

查漏补缺!驱动核心源码详解和Binder超系统学习资源,挥泪整理面经

欢喜学安卓

android 程序员 面试 移动开发

聪明人的训练(六)

Changing Lin

4月日更

翻译:《实用的Python编程》02_00_Overview

codists

Python

图解云原生应用设计模式

倪朋飞

Kubernetes 云原生

金三银四旗开得胜!春招字节正式批4面,顺利拿到offer

Java 编程 程序员 架构 面试

Linux ln 命令

一个大红包

4月日更

微信业务架构图

@oo?金樱子

Go语言很好很强大,但我有几个问题想吐槽_AI&大模型_Ben Boyter_InfoQ精选文章