更新:如果你看到这篇博文来自一篇标题为《糟糕的 Go 语言》的合辑文章,我想明确表示,我很惭愧被列在这样的列表中.Go绝对是我用过的最不烂的编程语言。当我写这篇文章时,我试图遏制我所看到的过度使用Go的一些更复杂部分的趋势。我仍然认为Channel可以更好,但总的来说Go很棒。就像在您最喜欢的工具箱中拥有此工具一样;它可以有一个目的(甚至更多),它仍然可以成为您最喜欢的工具箱!更新2:如果我没有指出这个对真实问题的出色调查,那我就是失职了:《理解 Go 中的实际并发错误》。这项调查的主要发现之一是……Go通道会导致很多错误。自2010年中后期以来,我一直断断续续地使用Google的Go编程语言,并且自2012年1月(Go1.0之前!)以来,我一直在使用Go为SpaceMonkey编写兼容的生产代码。我对Go的最初体验可以追溯到我在MattMight的UCombinator研究小组下对Hoare的通信顺序过程并发模型和π-演算的研究,作为我(现已重定向)博士工作的一部分,以更好地支持多核开发。那是Go发布的时候(多么巧合!),我立即开始学习和试验。它很快成为SpaceMonkey开发的核??心部分。目前,我们在SpaceMonkey的生产系统有超过425,000行纯Go代码(不包括我们所有供应库中的代码量,这将使它接近150万行),所以这不是你见过的最多的Gocode,但是对于一个比较年轻的语言,我们是重度用户。我们之前写过我们的Go用法。还开源了一些经常使用的库;许多似乎是我们的OpenSSL绑定(比crypto/tls更快,但请保持openssl本身是最新的!)、我们的错误处理库、日志库和收集库/zipkin客户端的度量范。我们使用Go,我们喜欢Go,我们认为它是迄今为止我们为满足需求而使用过的最不糟糕的编程语言。虽然我也不认为我可以说服自己不提我广泛避免使用goroutine-local-storage库(这是一个漂亮的hack,虽然它是你不应该使用的hack),但希望我的其他经历足以证明在解释我故意煽动性的帖子标题之前,我知道我在说什么。等等,什么?如果你在大街上问一位著名的程序员,Go有什么特别之处?她可能会告诉您Go最出名的是Channels和goroutines。Go的理论基础很大程度上基于Hoare的CSP(CommunicatingSequentialProcesses)模型,它本身就很吸引人和有趣,我坚信它产生的收益远远超过我们的预期。CSP(和π演算)都使用通信作为核心同步原语,因此Go拥有通道是有道理的。RobPike很长一段时间以来一直(有充分的理由)着迷于CSP。(过去和现在)。但从务实的角度(Go引以为豪的角度)来看,Go弄错了通道。在这一点上,通道实现在我的书中几乎是一个可靠的反模式。你为什么这么说?亲爱的读者,让我数一数。您可能最终不会只使用通道Hoare的“通信顺序过程”是一种计算模型,实际上,唯一的同步原语是在通道上发送或接收的。一旦使用互斥量、信号量或条件变量bam,您就不再处于纯CSP领域。Go程序员经常通过喊出“通过通信共享内存”的理念来宣扬这种模式和理念。那么,让我们尝试只使用CSP在Go中编写一个小程序!让我们成为高分接球手。我们所要做的就是跟踪我们看到的最大高分值。这就对了。首先,我们将创建一个Game结构。typeGamestruct{bestScoreintscoreschanint}bestScore将不受互斥量保护!这很好,因为我们只需要一个goroutine来管理它的状态并通过通道接收新分数。func(g*Game)run(){forscore:=rangeg.scores{ifg.bestScoreBenchmarkSimpleSet-83000000391ns/op>BenchmarkSimpleChannelSet-810000001699ns/o>无缓冲通道的情况是相似的,即使运行相同的测试也是争用而不是串行。也许Go调度程序会改进,但与此同时,好的旧互斥锁和条件变量非常好、高效和快速。如果您想提高性能,请使用久经考验的方法。通道不能与其他并发原语很好地混合好吧,我希望我已经让你相信,有时,你至少会与通道以外的原语进行交互。标准库似乎显然更喜欢传统的同步原语而不是通道。你猜怎么着,正确使用带有互斥量和条件变量的通道可能有点挑战性。关于通道的一个有趣的事情是通道发送是同步的,这在CSP中很有意义。channelsend和channelreceive的目的是成为一个同步掩码,发送和接收应该在同一虚拟时间发生。如果您在执行良好的CSP空间中,那就太好了。实际上,Go通道也以各种方式进行缓冲。您可以分配一个固定空间来考虑可能的缓冲,以便发送和接收是不同的事件,但缓冲区大小是有上限的。Go没有为您提供任意大小的缓冲区的方法-您必须提前分配缓冲区大小。没关系,我看到人们在邮件列表上争论不休,因为无论如何内存都是有限的。什么。这是一个糟糕的答案。使用任意缓冲的通道有多种原因。如果我们事先知道一切,为什么还要使用malloc?没有任意缓冲的通道意味着任何通道上的天真发送都可能随时阻塞。你想在一个通道上发送,并在互斥下更新一些其他簿记吗?小心!您的频道发送可能被屏蔽了!//...s.mtx.Lock()//...s.ch<-val//可能会阻塞!s.mtx.Unlock()//...这是哲学家的晚餐战斗食谱。如果使用锁,应该快速更新状态并释放,尽量不要在锁下做任何阻塞的事情。在Go中有一种方法可以在通道上进行非阻塞发送,但这不是默认行为。假设我们有一个通道ch:=make(chanint),我们希望在其上不阻塞地发送值1。这是您在不阻塞的情况下必须执行的最少键入操作:select{casech<-1://itsentdefault://itdidn't}这不是自然而然的事情。综上所述,由于一个channel上的很多操作都会阻塞,所以需要对哲学家及其用餐进行仔细的推理,才能在互斥量的保护下成功地使用与其并行的channel操作,而不会造成死锁。严格来说,回调更强大,不需要不必要的goroutines每当API使用通道,或者每当我指出通道使事情变得困难时,有人会指出我应该启动一个goroutine来读取通道,并进行任何转换或阅读该频道时需要维修。呃不。如果我的代码在热路径中怎么办?很少有需要通道的情况,如果你的API可以设计为使用互斥锁、信号量和回调而不使用额外的goroutines(因为所有事件边缘都由API事件触发),那么使用通道会迫使我添加另一个内存分配堆栈到资源使用。是的,goroutine比线程要轻很多,但是轻并不代表最轻。正如我之前在一篇关于使用通道(呵呵,互联网)的文章的评论中所论证的那样,如果您使用回调而不是通道,您的API总是可以更通用、更灵活,并且资源密集度会大大降低。“永远”是一个可怕的词,但我在这里是认真的。有证据级别的事情正在进行。如果有人为您提供了一个基于回调的API而您需要一个通道,您可以提供一个回调并将其发送到通道上,开销很小并且非常灵活。另一方面,如果有人给你一个基于通道的API,你需要一个回调,你必须启动一个goroutine来读取通道,你必须希望当你读完后,没有人试图在通道上发送任何其他导致阻塞的goroutines泄漏的东西。一个超级简单的实用例子,查看上下文接口(顺便说一句,这是一个非常有用的包,你应该使用它而不是goroutine本地存储)。typeContextinterface{...//Done返回一个通道,该通道在应该取消此工作单元时关闭。//Done返回一个通道,该通道在应该取消此工作单元时关闭。Done()<-chanstruct{}//当Done通道关闭时,Err返回一个非nil错误//当Done通道关闭时,Err返回一个非nil错误Err()error...}想象一下,你所有需要做的就是在Done()通道触发时记录相应的错误。你该怎么办?如果通道中没有好的选择位置,就得启动goroutine进行处理:gofunc(){<-ctx.Done()logger.Errorf("canceled:%v",ctx.Err())}()如果ctx在没有关闭返回Done()的通道的情况下得到垃圾收集怎么办?哎呀!这正是goroutine泄漏!现在假设我们更改Done的签名://Done在应该取消此工作单元时调用cb。Done(cbfunc())首先,日志记录现在非常容易。看看:ctx.Done(func(){log.Errorf("canceled:%v",ctx.Err())})。但是假设您确实需要一些选择行为。你可以这样称呼它:ch:=make(chanstruct{})ctx.Done(func(){close(ch)})瞧!通过使用回调,不会丢失任何表现力。ch就像一个返回Done()的通道,在日志记录的情况下我们不需要启动一个全新的堆栈。我必须保留堆栈跟踪(如果我们的日志包倾向于使用它们);我必须避免将其他堆栈分配和另一个goroutine分配给调度程序。下次你使用通道时,问问自己,如果你使用互斥锁和条件变量来代替,是否可以消除一些goroutines?如果答案是肯定的,那么修改这些代码会更有效。而且,如果您尝试使用频道只是为了在集合上使用range关键字,那么我将不得不请您放下键盘,或者回去写一本Python书。channelAPI不一致,就是cray-cray,当channel关闭的时候,执行close或者发送消息都会引起panic!为什么?如果你想关闭一个通道,你需要在外部同步它的关闭状态(使用互斥量等,这些互斥量的组合不是很好!),这样其他写入者就不会写入或关闭关闭的通道,或者只是向前冲,关闭或写入关闭的通道,并期望您必须恢复所有引发的恐慌。多么奇怪的行为。Go中几乎所有其他操作都有避免恐慌的方法(例如,类型断言具有,ok=模式),但是对于通道,您只能自己处理。好吧,所以当发送失败时通道会出现恐慌。我认为这有一定的道理。然而,与几乎所有其他具有nil值的东西不同,发送到nil通道不会panic。相反,它将永远阻塞!这是违反直觉的。这可能是有用的行为,例如将开罐器附加到除草机上,可能很有用(可在Skymall中找到),但肯定是出乎意料的。与nil映射(执行隐式指针解引用)、nil接口(执行隐式指针解引用)、未经检查的类型断言和所有其他类型交互不同,nil通道表现出实际的通道行为,就好像实例一样,就像创建一个全新的通道一样。接待好一点。在关闭的通道上执行接收时会发生什么?好吧,那将是有效的操作——您将得到一个零值。好吧,我想这是有道理的。奖!Receive允许您在收到值时执行ok=样式检查以确定通道是否打开。谢天谢地,我们在这里找到了,好的=。但是如果你从一个nil通道接收会发生什么?也永远封锁!是的!不要试图利用这样一个事实,即如果您关闭一个频道,您的频道就是零!渠道有什么好处?当然,渠道对某些事情很有用(毕竟它们是一个通用容器),而有些事情你只能用它们来做(比如选择)。它们是通用数据结构的另一个特例,Go程序员已经习惯于争论泛型,一提到这个词我就能感觉到PTSD(创伤后应激障碍)。我不是来这里谈论这个的,所以擦去你额头上的汗水,让我们继续前进。无论您对泛型有何看法,Go的映射、切片和通道都是支持泛型元素类型的数据结构,因为它们已被专门封装到语言中。在一种不允许您编写自己的通用容器的语言中,任何能让您更好地管理事物集合的东西都是有价值的。在这里,通道是支持任意值类型的线程安全数据结构。所以这很好用!我想这可以避免一些陈词滥调。我很难将此视为该频道的胜利。选择您可以对通道做的主要事情是选择语句。在这里,您可以等待固定数量的事件进入。它有点像epoll,但是你必须事先知道要等待多少套接字。这是一个非常有用的语言特性。如果未选中,通道将被完全冲洗。但是我的天告诉你,第一个decision可能需要在多个东西中进行选择,但是你又不知道有多少个,所以你要用reflect.Select。渠道如何才能更好?很难说Go语言团队可以为Go2.0做哪些最具战术性的事情(Go1.0兼容性保证很好,但很费力),但这并不能阻止我提出一些建议。选择条件变量!我们可以没有渠道!这是我建议我们摆脱一些“神牛”的地方,但让我问你,如果你可以选择任何自定义同步原语,那该有多棒?(答案:很好。)如果有,我们根本就不需要频道了。GC可以帮助我们吗?在第一个例子中,如果我们可以使用定向类型的通道垃圾收集(GC)来帮助我们清理,我们就可以轻松解决通道的高分服务器清理问题。如您所知,Go具有定向类型的通道。您可以使用只读通道类型(<-chan)和只写通道类型(chan<-)。这很棒!Go也有垃圾回收。显然,某些类型的簿记太麻烦了,我们不应该让程序员来处理它们。我们清理未使用的内存!垃圾收集非常有用和整洁。那么为什么不帮助清理未使用或死锁的通道读取呢?与其让make(chanWhatever)返回一个双向通道,不如让它返回两个单向通道(chanReader,chanWriter:=make(chanType))。让我们重新考虑一下我们原来的例子:通道的发送端将被保留。forscore:=rangescores{if*bestScore