\u003cp\u003eGo是一门非常不错的编程语言。然而,我在公司的Slack编程频道中对Go的抱怨却越来越多(猜到我是做啥了的吧?),因此我认为有必要把这些吐槽写下来并放在这里,这样当人们问我抱怨什么时,我给他们一个链接就行了。\u003c/p\u003e\n\u003cp\u003e\u003cimg src=\"https://static.geekbang.org/infoq/5c9f145801dd0.png?imageView2/0/w/800\" alt=\"image\" /\u003e\u003c/p\u003e\n\u003cp\u003e先声明一下,在过去的一年里,我大量地使用Go语言开发命令行应用程序、\u003ca href=\"https://github.com/boyter/scc/\"\u003escc\u003c/a\u003e、\u003ca href=\"https://github.com/boyter/lc/\"\u003elc\u003c/a\u003e和API。 其中既有供客户端调用的大规模API,也有即将在\u003ca href=\"https://searchcode.com/\"\u003ehttps://searchcode.com/\u003c/a\u003e 使用的\u003ca href=\"https://github.com/boyter/searchcode-server-highlighter\"\u003e语法高亮显示器\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e我这些批评全部是针对Go语言的。但是,我对使用过的每种语言都有不满。 我非常赞同下面的话:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“世界上只有两种语言:人们抱怨的语言和没人使用的语言。” —— Bjarne Stroustrup\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3\u003e1 不支持函数式编程\u003c/h3\u003e\n\u003cp\u003e我并不是一个函数式编程狂热者。 说到Lisp语言,我首先想到的是语言障碍。\u003c/p\u003e\n\u003cp\u003e这可能是Go语言最大的痛点了。 与大部分人不同,我不希望Go支持泛型,因为它会为多数Go项目带来不必要的复杂性。 我希望Go语言支持适用于内置切片和Map的函数式方法。 切片和Map具有通用性,并且可以容纳任何类型,从这个意义上讲,它们已经非常神奇。在Go语言中只有利用接口才能实现类似效果,但这样一来将丧失安全性和速度。\u003c/p\u003e\n\u003cp\u003e例如,请考虑下面的问题。\u003c/p\u003e\n\u003cp\u003e给定两个字符串切片,找出二者都包含的字符串,并将其放入新的切片以备后用。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eexistsBoth := []string{}\nfor _, first := range firstSlice {\n\tfor _, second := range secondSlice {\n\t\tif first == second {\n\t\t\texistsBoth = append(existsBoth, proxy)\n\t\t\tbreak\n\t\t}\n\t}\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e上面是一个用Go语言实现的简单方案。当然还有其它方法,比如借助Map来减少运行时间。这里我们假设内存足够用或者切片都不太大,同时假设优化运行时间带来的复杂性远超收益,因此不值得优化。作为对比,使用Java流和函数式编程把相同的逻辑重写如下:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003evar existsBoth = firstList.stream()\n .filter(x -\u0026gt; secondList.contains(x))\n .collect(Collectors.toList());\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e上面的代码隐藏了算法的复杂性,但是,你更容易理解它实际做的事情。\u003c/p\u003e\n\u003cp\u003e与Go代码相比,Java代码的意图一目了然。 真正灵活之处在于,添加更多的过滤条件易如反掌。 如果使用Go语言添加下面例子中的过滤条件,我们需要在嵌套的for循环中再添加两个if条件。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003evar existsBoth = firstList.stream()\n .filter(x -\u0026gt; secondList.contains(x))\n .filter(x -\u0026gt; x.startsWith(needle))\n .filter(x -\u0026gt; x.length() \u0026gt;= 5)\n .collect(Collectors.toList());\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e有些借助go generate命令的项目可以帮你实现上面的一些功能。但是,如果缺少良好的IDE支持,抽取循环中的语句作为单独的方法是一件低效又麻烦的事情 。\u003c/p\u003e\n\u003ch3\u003e2 通道/并行切片处理\u003c/h3\u003e\n\u003cp\u003eGo通道通常都很好用。 但它并不能提供无限的并发能力。它确实存在一些会导致永久阻塞的问题,但这些问题用竞争检测器能很容易地解决。对于数量不确定或不知何时结束的流式数据,以及非CPU密集型的数据处理方法,Go通道都是很好的选择。\u003c/p\u003e\n\u003cp\u003eGo通道不太适合并行处理大小已知的切片。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e多线程编程、理论和实践\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg src=\"https://static.geekbang.org/infoq/5c9f14588eb02.png?imageView2/0/w/800\" alt=\"image\" /\u003e\u003c/p\u003e\n\u003cp\u003e几乎在其它任何语言中,当列表或切片很大时,为了充分利用所有CPU内核,通常都会使用并行流、并行Linq、Rayon、多处理或其它语法来遍历列表。遍历后的返回值是一个包含已处理元素的列表。 如果元素足够多,或者处理元素的函数足够复杂,多核系统会更高效。\u003c/p\u003e\n\u003cp\u003e但是在Go语言中,实现高效处理所需要做的事情却并不显而易见。\u003c/p\u003e\n\u003cp\u003e一种可能的解决方案是为切片中的每个元素都创建一个Go例程。 由于Go例程的开销很低,因此从某种程度上来说这是一个有效的策略。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003etoProcess := []int{1,2,3,4,5,6,7,8,9}\nvar wg sync.WaitGroup\n\nfor i, _ := range toProcess {\n\twg.Add(1)\n\tgo func(j int) {\n\t\ttoProcess[j] = someSlowCalculation(toProcess[j])\n\t\twg.Done()\n\t}(i)\n}\n\nwg.Wait()\nfmt.Println(toProcess)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e上面的代码会保持切片中元素的顺序,但我们假设不必保持元素顺序。\u003c/p\u003e\n\u003cp\u003e这段代码的第一个问题是增加了一个WaitGroup,并且必须要记得调用它的Add和Done方法。这增加了开发人员的工作量。如果弄错了,这个程序不会产生正确的输出,结果是要么输出不确定,要么程序永不结束。此外,如果列表很长,你会为每个列表创建一个Go例程。正如我之前所说,这不是问题,因为Go能轻松搞定。问题在于,每个Go例程都会争抢CPU时间片。因此,这不是执行该任务的最有效方式。\u003c/p\u003e\n\u003cp\u003e你可能希望为每个CPU内核创建一个Go例程,并让这些例程选取列表并处理。创建Go例程的开销很小,但是在一个非常紧凑的循环中创建它们会使开销陡增。当我开发\u003ca href=\"https://github.com/boyter/scc/\"\u003escc\u003c/a\u003e时就遇到了这种情况,因此我采用了每个CPU内核对应一个Go例程的策略。在Go语言中,要这样做的话,你首先要创建一个通道,然后遍历切片中的元素,使函数从该通道读取数据,之后从另一个通道读取。我们来看一下。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003etoProcess := []int{1,2,3,4,5,6,7,8,9}\nvar input = make(chan int, len(toProcess))\n\nfor i, _ := range toProcess {\n\tinput \u0026lt;- i\n}\nclose(input)\n\nvar wg sync.WaitGroup\nfor i := 0; i \u0026lt; runtime.NumCPU(); i++ {\n\twg.Add(1)\n\tgo func(input chan int, output []int) {\n\t\tfor j := range input {\n\t\t\ttoProcess[j] = someSlowCalculation(toProcess[j])\n\t\t}\n\t\twg.Done()\n\t}(input, toProcess)\n}\n\nwg.Wait()\nfmt.Println(toProcess)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e上面的代码创建了一个通道,然后遍历切片,将索引值放入通道。 接下来我们为每个CPU内核创建一个Go例程,操作系统会报告并处理相应的输入,然后等待,直到所有操作完成。这里有很多代码需要理解。\u003c/p\u003e\n\u003cp\u003e然而,这种实现有待商榷。如果切片非常大,通道的缓冲区长度和切片大小相同,你可能不希望创建一个有这么大缓冲区的通道。因此,你应该创建另一个Go例程来遍历切片,并将切片中的值放入通道,完成后关闭通道。 但这样一来代码会变得冗长,因此我把它去掉了。我希望可以大概地阐明基本思路。\u003c/p\u003e\n\u003cp\u003e使用Java语言大致这样实现:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003evar firstList = List.of(1,2,3,4,5,6,7,8,9);\n\nfirstList = firstList.parallelStream()\n .map(this::someSlowCalculation)\n .collect(Collectors.toList());\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e通道和流并不等价。 使用队列去仿写Go代码的逻辑更好一些,因为它们更具有可比性,但我们的目的不是进行1对1的比较。 我们的目标是充分利用所有的CPU内核处理切片或列表。\u003c/p\u003e\n\u003cp\u003e如果someSlowCalucation方法调用了网络或其它非CPU密集型任务,这当然不是问题。 在这种情况下,通道和Go例程都会表现得很好。\u003c/p\u003e\n\u003cp\u003e这个问题与问题#1有关。如果Go语言支持适用于切片/Map对象的函数式方法,那么就能实现这个功能。 但是,如果Go语言支持泛型,有人就可以把上面的功能封装成像Rust的Rayon一样的库,让每个人都从中受益,这就很令人讨厌了(我不希望Go支持泛型)。\u003c/p\u003e\n\u003cp\u003e顺便说一下,我认为这个缺陷妨碍了Go语言在数据科学领域的成功,这也是为什么Python仍然是数据科学领域的王者。 Go语言在数值操作方面缺乏表现力和能力,原因就是以上讨论的这些。\u003c/p\u003e\n\u003ch3\u003e3 垃圾回收器\u003c/h3\u003e\n\u003cp\u003eGo的垃圾回收器做得非常不错。我开发的应用程序通常都会因为新版本的改进而变得更快。但是,它以低延迟为最高优先级。对于API和UI应用来说,这个选择完全可以接受。对于包含网络调用的应用,因为网络调用往往会是瓶颈,所以它也没问题。\u003c/p\u003e\n\u003cp\u003e我发现的问题是Go对UI应用来讲一点也不好(我不知道它有任何良好的支持)。如果你想要尽可能高的吞吐量,那这个选择会让你很受伤。这是我开发\u003ca href=\"https://github.com/boyter/scc/\"\u003escc\u003c/a\u003e时遇到的一个主要问题。scc是一个CPU密集型的命令行工具。为了解决这个问题,我不得不在代码里添加逻辑关闭GC,直到达到某个阈值。但是我又不能简单的禁用它,因为有些任务会很快耗尽内存。\u003c/p\u003e\n\u003cp\u003e缺乏对GC的控制时常令人沮丧。你得学会适应它,但是,有时候如果能做到这样该有多好:“嘿,这些代码确实需要尽可能快地运行,所以如果你能在高吞吐模式运行一会,那就太好了。”\u003c/p\u003e\n\u003cp\u003e\u003cimg src=\"https://static.geekbang.org/infoq/5c9f14592f362.png?imageView2/0/w/800\" alt=\"image\" /\u003e\u003c/p\u003e\n\u003cp\u003e我认为这种情况在Go 1.12版本中有所改善,因为GC得到了进一步的改进。但仅仅是关闭和打开GC还不够,我期望更多的控制。 如果有时间我会再进行研究。\u003c/p\u003e\n\u003ch3\u003e4 错误处理\u003c/h3\u003e\n\u003cp\u003e我并不是唯一一个抱怨这个问题的人,但我不吐不快。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003evalue, err := someFunc()\nif err != nil {\n\t// Do something here\n}\n\nerr = someOtherFunc(value)\nif err != nil {\n\t// Do something here\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e上面的代码很乏味。 Go甚至不会像有些人建议的那样强制你处理错误。 你可以使用“_”显式忽略它(这是否算作对它进行了处理呢?),你还可以完全忽略它。比如上面的代码可以重写为:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003evalue, _ := someFunc()\n\nsomeOtherFunc(value)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e很显然,我显式忽略了someFunc方法的返回。someOtherFunc(value)方法也可能返回错误值,但我完全忽略了它。 这里的错误都没有得到处理。\u003c/p\u003e\n\u003cp\u003e说实话,我不知道如何解决这个问题。 我喜欢Rust中的“?” 运算符,它可以帮助避免这种情况。V-Lang \u003ca href=\"https://vlang.io/\"\u003ehttps://vlang.io/\u003c/a\u003e 看起来也可能有一些有趣的解决方案。\u003c/p\u003e\n\u003cp\u003e另一个办法是使用可选类型(Optional types)并去掉nil,但这不会发生在Go语言里,即使是Go 2.0版本,因为它会破坏向后兼容性。\u003c/p\u003e\n\u003ch3\u003e结语\u003c/h3\u003e\n\u003cp\u003eGo仍然是一种非常不错的语言。如果你让我写一个API,或者完成某个需要大量磁盘/网络调用的任务,它依然是我的首选。现在我会用Go而非Python去完成很多一次性任务,数据合并任务是例外,因为函数式编程的缺失使执行效率难以达到要求。\u003c/p\u003e\n\u003cp\u003e与Java不同,Go语言尽量遵循“最小惊喜“原则。比如可以这样比较字两个符串是否相等:stringA == stringB。但如果你这样比较两个切片,那么会产生编译错误。这些都是很好的特性。\u003c/p\u003e\n\u003cp\u003e的确,二进制文件还可以变的更小(一些\u003ca href=\"https://boyter.org/posts/trimming-golang-binary-fat/\"\u003e编译标志和upx\u003c/a\u003e可以解决这个问题),我希望它在某些方面变得更快,GOPATH虽然不是很好,但也没有人们想得那么糟糕,默认的单元测试框架缺少很多功能,模拟(mocking)有点让人痛苦…\u003c/p\u003e\n\u003cp\u003e它仍然是我使用过的效率较高的语言之一。我会继续使用它,虽然我希望\u003ca href=\"https://vlang.io/\"\u003ehttps://vlang.io/\u003c/a\u003e能最终发布,并解决我的很多抱怨。V语言或Go 2.0,Nim或Rust。现在有很多很酷的新语言可以使用,我们开发人员真的要被宠坏了。\u003c/p\u003e\n\u003cp\u003e查看英文原文:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://boyter.org/posts/my-personal-complaints-about-golang/\"\u003ehttps://boyter.org/posts/my-personal-complaints-about-golang/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg src=\"https://static.geekbang.org/infoq/5c8325eb1892c.png?imageView2/0/w/800\" alt=\"image\" /\u003e\u003c/p\u003e\n