0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Go语言的设计上存在四大“硬伤”

DPVg_AI_era 来源:lp 2019-03-29 08:50 次阅读

Go语言为何不受待见?事实上,Go仍然是一种相当不错的语言,并且逐渐取代Python成为很多人的首选语言。但是其却有一些问题,使得开发速度大受影响。本文就跟随作者一起解读下Go中那些“硬伤”设计。

Go作为一种编程语言来说是相当体面的,然而,我在公司Slack(译者注:一种团队协作工具)的编程频道上对它的抱怨却越来越多(猜到我是做啥了的吧?)。我想我还是把这些抱怨写下来放在这里,这样当人们问我到底抱怨什么时,我就可以给他们一个链接,让他们直接到这里来看。

过去一年左右的时间我都一直在大量地使用Go语言来编程,写一些命令行应用程序、scc(https://github.com/boyter/scc/)、lc(https://github.com/boyter/lc/)和一些API等等。

其中包括一个供客户端调用代码高亮插件(https://github.com/boyter/searchcode-server-highlighter)的大规模API,这个代码高亮插件很快将会在https://searchcode.com/网站中使用。

我在这里的批评是专门针对Go编程语言的。然而,我对我使用的每种编程语言都有抱怨。这里我引用一下C++编程语言之父Bjarne Stroustrup说过的一句话:

“世界上只有两种编程语言:一种是人们抱怨的语言,另一种是没人使用的语言。”

——Bjarne Stroustrup

缺乏函数式编程

我不是一个函数式编程的狂热分子。Lisp语言让我首先想到的是语言障碍,这可能是我使用Go语言编程时最痛苦的地方。

和大多数开发者不一样,我不想要泛型,我认为这只会给大多数Go项目增加不必要的复杂性。我想要的是一些可以应用于内置的Slice(切片)和Map类型之上的函数方法。Slice和Map这两种类型都可以容纳任何类型,而且是泛型的,这种在某种意义上很神奇。然而Go的泛型不使用接口的话,就无法实现自己,这样就损失了所有的安全性和性能。

举个例子,考虑下面的需求。

给定两个字符串片断,找出这两段字符串片断中都包含的相同的子字符串,并将其放入一个新的字符串片断中,以便我们稍后处理它。

existsBoth:=[]string{}for_,first:=rangefirstSlice{for_,second:=rangesecondSlice{iffirst==second{existsBoth=append(existsBoth,proxy)break}}}

上面的Go语言的解决方法很简单。但我们还有其他方法,如使用Map来解决这个问题,使用Map可以减少运行时间,但是如果我们的内存容量有限,或者我们没有很大的片断需要处理,那么额外的运行时间并不足以抵消它带来的复杂性。

让我们将其与Java中使用Stream和函数编程来实现相同逻辑的代码比较一下。

varexistsBoth=firstList.stream().filter(x->secondList.contains(x)).collect(Collectors.toList());

上面的代码确实将算法的复杂性隐藏起来,从代码上看清楚要实现的逻辑容易得多。

与实现同样功能的Go代码相比,上面的代码的意图是显而易见的,真正的简洁之处是添加额外的过滤器也变得非常简单。而如果用Go语言实现像下面的示例一样添加额外的过滤器,我们就必须在已经嵌套的for循环中再添加两个if条件。

varexistsBoth=firstList.stream().filter(x->secondList.contains(x)).filter(x->x.startsWith(needle)).filter(x->x.length()>=5).collect(Collectors.toList());

有一些使用Go Generate的项目可以为你做到上面所说的,但是如果没有好的IDE支持的话,将上面的循环提取到它自己的方法中会非常笨重,而且会带来更多的麻烦。

通道(channel)/并行切片(Slice)处理

Go的通道(channel)通常非常简洁。虽然它们存在一些问题会导致它永久阻塞,但它们并不打算提供安全的并发性,因为通过竞争检测机制可以很容易地摆脱这些问题。对于一个不知道有多少个值或何时结束的流,或者如果处理这些值的方法不受CPU的制约,那么通道是一个很好的选择。

通道不太擅长的是处理那些预先知道大小并希望并行处理的切片(Slice)。

并行处理在几乎所有其他语言中都很常见,通常发生在你有一个大的列表或切片,使用并行流、并行LINQ(语言集成查询)、Rayon(一种数据并行库)、多进程或其他一些语法,使用所有可用的CPU,对该列表/切片进行迭代处理时。你将它们应用到你的列表上,然后返回处理好的元素列表。如果你的列表有太多的元素,或者你正在使用的函数太复杂,使用一个多核系统应该也可以更快地完成。

然而,在Go语言中,你需要怎么实现它并不明确。

一种可能的解决方法是为切片中的每个元素生成一个goroutine。因为goroutine的开销很低,所以在某种程度上,这是一个有效的策略。

toProcess:=[]int{1,2,3,4,5,6,7,8,9}varwgsync.WaitGroupfori,_:=rangetoProcess{wg.Add(1)gofunc(jint){toProcess[j]=someSlowCalculation(toProcess[j])wg.Done()}(i)}wg.Wait()fmt.Println(toProcess)

上面的代码会保持切片中元素的顺序,但是我们的例子并不要求实现这点。

上面的问题首先是添加一个waitgroup,并且必须记住递增并调用它。这对开发人员开说是额外负担。如果弄错了,这个程序将不会产生正确的输出,可能是不确定的结果,也可能永远不会执行完成。

另外,如果你的列表很长,你要为列表中每个单独的元素生成一个goroutine。正如我之前所说,这本身不是一个问题,因为Go语言能毫无问题地做到这一点。但问题是,每一个goroutine都要为使用CPU的时间片而竞争。因此这不是执行此任务的最有效方法。

你可能想做的是为每个CPU生成一个goroutine,并让它们依次挑选处理它的列表。增加一个goroutine的开销很小,但是对于一个迭代次数很多的循环来说,这个开销并不算小。当我在为scc项目工作时,我遇到了这个问题,它在每个CPU的内核上创建了一个goroutine。如果要完全用Go语言的方式来解决这个问题,你就需要创建一个通道,然后循环你的每个切片元素,让你的函数从该通道读取,然后再从另一个通道读取。

让我们看看代码。

toProcess:=[]int{1,2,3,4,5,6,7,8,9}varinput=make(chanint,len(toProcess))fori,_:=rangetoProcess{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内核创建一个goroutine来处理输入的值,然后等待它全部完成。要消化的代码很多。

这不是一个你应该怎么做的问题,因为如果你的切片非常大,你可能不想有一个具有相同长度的缓冲区的通道,所以你实际上应该创建另一个goroutine来循环切片,并将这些值放入通道。当处理完成后,它关闭通道。我已经删除了这个代码,因为它使代码变得更长,而且我已经基本上知道怎么做了。

Java的做法和上面大致相同。

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

是的,Go语言的通道(channel)和Java中的流(stream)并不是一回事,通道更接近于Java中的队列(queue),但我们这里的目的不是1对1的对比。我们想要的是使用我们所有的CPU内核来处理一个切片/列表。

当然,如果某个slowcaluation实际上是一个在网络上调用的方法,或者是其他一些需要大量CPU的方法,那么这就不是一个问题。在这种情况下,通道和goroutine都很出色。

这一问题与缺乏函数式编程有关。如果Go语言在slice/map对象之上有函数方法,那么添加这个功能是可能的。这也很烦人,因为如果Go支持泛型的话,就会有人可以把上面谈到的写成一个函数库,就像Rust的Rayon一样,每个人都会受益。

顺便说一句,我认为这一点阻碍了Go语言在数据科学领域的任何成功,因此,为什么Python仍然是那里的王者。而Go语言在数字操作中缺乏表现力和力量——以上就是原因。

垃圾回收(GC)

Go语言的垃圾回收机制非常可靠。每次Go语言版本更新,我都发现我的应用程序变得更快了,原因通常是因为GC的改进。将延迟的优先级置于所有其它要求之上,对于API和UI来说,是一个完全可以接受的选择。同样地它也适用于任何有网络呼叫的情况,这些呼叫也会成为瓶颈。

关键是Go语言对UI功能的实现没有任何好处(据我所知没有合适的绑定),当你需要尽可能大的吞吐量时,这个选择确实会伤害你。我在处理scc项目时遇到了一个大问题,scc是一个命令行应用程序,对CPU的要求很高。这是个问题,我添加了一个逻辑来关闭内存回收机制,直到内存使用量达到阈值。但是,我不能禁用它,因为程序在某些情况下工作时很快就会耗尽内存。

对GC缺乏控制有时令人沮丧。你学会了接受它,但有时你会说“嘿,这里的代码真的需要尽可能快的运行,所以如果能切换到高吞吐量模式一段时间,那就太好了。”

我认为随着Go语言的1.12版本的发布,这一点变得越来越不可能了,在这个版本中,GC看起来再次得到了改进,但是仅仅关闭和打开GC并不是我想要的控制。有时间的话我会再次深入了解一下。

错误处理

我不是唯一一个对这点有抱怨的人,但我必须写出来。

value,err:=someFunc()iferr!=nil{//Dosomethinghere}err=someOtherFunc(value)iferr!=nil{//Dosomethinghere}

上面的代码看起来相当乏味。Go语言甚至不强制你处理大家建议的错误。你可以显式忽略它(这是否算作处理它?),你甚至可以完全忽略它。例如,我可以像这样重写上面的内容:

value,_:=someFunc()someOtherFunc(value)

你很容易发现我省略了somefunc返回的内容,但是someotherfunc(value)同时也可以返回错误,而我却完全忽略了这个错误,对它不作任何处理。

老实说,我不知道有这里有什么解决办法。不过我喜欢Rust言语的问号(?)操作符,它可以避免这个问题。另外V-Lang(https://vlang.io/)看起来也可能有一些有趣的解决方案。

另一个想法是可选类型(Optional Type)和删除nil,但是这些在Go语言的2.0版本中是永远不会出现的,因为它会破坏向后兼容性。

总结

总的来讲,Go仍然是一种相当不错的语言。如果你要我要写一个API,或者一些需要快速进行大量磁盘/网络调用的应用,它仍然是我的第一选择。事实上,我正处在这样一个阶段:Go已经取代Python,成为我要完成的大量的一次性任务的首选语言。数据合并任务除外,因为缺乏函数式编程仍然是一件痛苦的事情,这使得开发速度大受影响。

对诸如像字符串stringA == stringB和编译错误的比较,你会发现Go语言的切片用在这里非常合适。它不像我在上面用来比较的Java语言那样经常有出人意料的结果。

Go的二进制文件的大小可以更小(一些编译开关和upx(可执行文件压缩工具)可以解决这个问题),我希望它在某些方面运行得更快一些,GOPATH不是很好,但也没有每个人所说的那么糟糕,默认的单元测试框架缺少很多功能,Mocking有点痛苦等等。

Go仍然是我使用过的一种更有效的语言。我会继续使用它,尽管我希望https://vlang.io/最终能够发布并且解决我的许多投诉的问题。它可能Go 2.0版,可能是Nim,也可能是Rust。现在有很多很酷的新语言可以玩。我们的开发人员真的被宠坏了。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编程语言
    +关注

    关注

    9

    文章

    1877

    浏览量

    33011
  • 过滤器
    +关注

    关注

    1

    文章

    406

    浏览量

    18950
  • go语言
    +关注

    关注

    1

    文章

    156

    浏览量

    8919

原文标题:Go 语言为何不受待见?

文章出处:【微信号:AI_era,微信公众号:新智元】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Go语言简介和安装方法

    Go 又称 Golang ,是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言Go 语言语法与
    发表于 07-19 16:33 408次阅读

    EMC 四大设计技巧

    本帖最后由 eehome 于 2013-1-5 09:54 编辑 EMC 四大设计技巧
    发表于 08-17 16:09

    Go语言开发有什么优势?怎么学?

    的方式全面的阐述Go在高并发、大流量方面的应用,课程中采用的个项目全部来自大公司实实在在的线上案例,并不是通过简化的、阉割过的项目。可以学习到Go语言理论与应用的最佳实践,高并发服务
    发表于 12-19 16:08

    go语言能做什么工作?

    Go语言主要用作服务器端开发,其定位是用来开发“大型软件”的,适合于很多程序员一起开发大型软件,并且开发周期长,支持云计算的网络服务。Go语言能够让程序员快速开发,并且在软件不断的增长
    发表于 03-22 15:03

    Go开发语言的优势在哪里?

    进程。对于Go语言,一直存在着争议,很多人善于用Go语言进行开发,但不是所有人都喜欢Go
    发表于 03-22 15:04

    go语言开发的相关资料分享

    go语音被称作互联网时代的"c" 。简单和高效率,用在嵌入式也是非常合适的。应该做一个大胆的尝试......意义何在? 现在的设备都在朝着物联网,智能化方面发展,已不是传统
    发表于 11-05 08:41

    D语言,Go语言,Rust语言优势分析

    编者按】本文是D语言来呢后创始人、架构师Andrei Alexandrescu在问答Quora回答在取代C语言的道路上,D、Go和Rust谁的前途最光明?为什么?的答案,从自己的角度谈及了D、
    发表于 10-13 11:11 0次下载

    网易有道CEO周枫推荐Go语言并介绍Go语言的3个优点

    网易有道CEO周枫推荐Go语言。他认为Go很好地继承了C语言灵活、简单有效的思想;Go有很高的生产效率;
    的头像 发表于 01-31 14:11 4957次阅读

    详解GO语言的趋势与使用情况

    Go 语言简单易学、性能优良。JetBrains Blog 发布了Go 语言的调查报告,看看GO 语言
    的头像 发表于 03-17 11:05 2794次阅读

    go语言枚举类型怎么用

    go 语言枚举类型是这么用的?在什么场景下会用到枚举?本文对 go 语言枚举做了详细讲解。 枚举,是一种重要的数据类型,由一组键值对组成,通常用来在编程
    的头像 发表于 09-02 09:43 4876次阅读

    详细介绍go语言中的闭包的实现

    什么是闭包? 什么场景下会用闭包 ? 本文对 go 语言中的闭包做了详细介绍。 闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。 Go中的闭包 闭包是函数式语言
    的头像 发表于 10-20 16:18 1675次阅读

    Go语言创始人反对在Go 1.18的标准库中引入泛型支持

    近日,Go 语言创始人之一 Rob Pike 在 Github 上发表评论引发关注。据悉,他已在 Go 代码仓库提交了一个 issue (#48918),反对在 Go 1.18 的标准
    的头像 发表于 10-26 09:45 1646次阅读
    <b class='flag-5'>Go</b><b class='flag-5'>语言</b>创始人反对在<b class='flag-5'>Go</b> 1.18的标准库中引入泛型支持

    带你了解go语言中的闭包

      【 导读】什么是闭包? 什么场景下会用闭包 ? 本文对 go 语言中的闭包做了详细介绍。 闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。 Go中的闭包 闭包是函数式
    的头像 发表于 11-02 15:27 2178次阅读

    Go 语言在极小硬件上的运用(三) | Linux 中国

    Go 语言在极小硬件上的运用(三) | Linux 中国
    发表于 11-23 18:21 8次下载
    <b class='flag-5'>Go</b> <b class='flag-5'>语言</b>在极小硬件上的运用(三) | Linux 中国

    go语言中怎么使用HTTP代理

    go语言中怎么使用HTTP代理。
    的头像 发表于 09-01 14:41 2164次阅读