【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

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/



公众号推荐:

跳进 AI 的奇妙世界,一起探索未来工作的新风貌!想要深入了解 AI 如何成为产业创新的新引擎?好奇哪些城市正成为 AI 人才的新磁场?《中国生成式 AI 开发者洞察 2024》由 InfoQ 研究中心精心打造,为你深度解锁生成式 AI 领域的最新开发者动态。无论你是资深研发者,还是对生成式 AI 充满好奇的新手,这份报告都是你不可错过的知识宝典。欢迎大家扫码关注「AI前线」公众号,回复「开发者洞察」领取。

2019-04-01 11:4510250

评论 1 条评论

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

车联网通信安全之 SSL/TLS 协议|车联网系列专题 06

EMQ映云科技

车联网 物联网 IoT ssl emq

Java流程控制语句-分支结构(选择结构)

爱好编程进阶

Java 面试 后端开发

啃论文俱乐部的团队之道和成长之路

PaperResearch

开源 OpenHarmony 啃论文俱乐部 技术自由

Go能实现AOP吗?

捉虫大师

Java Go aop 4月月更

【二级等保】二级等保需要做日志审计吗?

行云管家

等保 日志审计 等保2.0 二级等保

JavaOOP面试题50题(含答案)

爱好编程进阶

Java 面试 后端开发

Java中IO字符流、File类

爱好编程进阶

Java 面试 后端开发

Java面试题总结(附答案)

爱好编程进阶

Java 面试 后端开发

FTP指的是什么协议?由什么组成?有什么作用?

行云管家

运维 网络协议 服务器 ftp

确定还不来看看?这样管理你的代码库既方便又省心!

Jianmu

开源 持续集成 工作流 代码管理 workflow

从安全和不安全两个角度,教你如何发布对象(含各种单例代码)

华为云开发者联盟

安全 线程 高并发 发布对象 对象溢出

Java基础 - Eclipse,API,Object常用方法

爱好编程进阶

Java 面试 后端开发

打基础丨Python图像处理入门知识详解

华为云开发者联盟

Python OpenCV 图像处理 图像 二值图像

Java中高级核心知识全面解析——常用框架(Spring中-Bean-的作用域与生命周期)

爱好编程进阶

Java 面试 后端开发

Java之Java特点

爱好编程进阶

Java 面试 后端开发

Java树结构实际应用(平衡二叉树-AVL树)

爱好编程进阶

Java 面试 后端开发

数字化赋能塑化产业,B2B电商模式引领企业增长新引擎

数商云

产业互联网 数字化转型

旺链周 | 我们的价值观,我们来代言

旺链科技

区块链 活动 价值观

你真的了解IT资产管理系统(ITAM)吗?

低代码小观

资产管理 企业管理系统 CRM系统 企业管理工具 资产安全

Java并发之Condition详解

爱好编程进阶

Java 面试 后端开发

蕉下招股书里提了26次的DTC,到底是啥?

易观分析

DTC

Java8的这些集合骚操作,你掌握了嘛?

爱好编程进阶

Java 面试 后端开发

Java基础知识点总结

爱好编程进阶

Java 面试 后端开发

JDK、JRE、JIT

爱好编程进阶

Java 面试 后端开发

在Redis中使用Pipelining提升查询速度

CRMEB

银行应构建主动式客户体验管理体系,助力客户价值增长

易观分析

银行 客户体验管理

服务可用性成险企智能运维关键破局能力,博睿数据APM下场助力

博睿数据

InfoQ 公开课开放报名,融云场景化低代码平台探究

融云 RongCloud

IndexedDB 代码封装、性能摸索以及多标签支持

网易云信

JavaScript 数据库

高层次人才一站式服务平台开发 人才综合服务平台系统

a13823115807

数据库性能优化方案

五分钟学大数据

4月月更

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