Go语言的表达性、错误处理方法和泛型等讨论摘录 - 黑客新闻


可汗网络学院发布了50万行Go代码以后的两点心得:Go一般比Python更冗长;快速,工具扎实。黑客新闻网友又开展了大量讨论,总体观点分两派:挺Go派和认为Go已经如当年Java一样冗长:
 
Go语言表型力问题
paul_e_warner :我已经用Go编写了一些代码,而我遇到的主要问题是,对于一种“现代”语言而言,感觉真的已经过时了,每次使用它时,我都感觉像是在2005年与讨厌的Java打交道。
Go语言可以在语言中添加一些功能,使其在不损害核心语言的简单性的情况下,更具可读性,并且更不易出错。泛型是著名的例子,但真正令我关心的是缺少可为空的类型签名。这是避免出现一整类错误的好方法,而我所使用的几乎每种现代语言除Go以外都为此开发了解决方案。
我遇到的另一个问题是对反射。总的来说,我认为如果您必须依靠反射来做某事,那通常意味着您正在解决正常语言中的一些固有限制:所得到的代码通常比具有更高表现力的代码可读性低得多。很多Go库和框架在很多情况下都被迫使用它,因为没有它,就没有其他方法可以表达一些非常基本的东西。
我真的很想喜欢Go,我喜欢很多东西,它是“做某事的唯一方法”,这意味着代码总是感觉一致。将错误作为值是一种处理异常的优越方法。不久前,我不得不为求职面试项目写一些东西,这确实令人耳目一新。
dcow:如果您是以自己编写的原始代码量感到自豪的工程师类型,那么Go是适合您的。如果您想专注于创造性地表达问题,那么Go并不是您的工具。
在几种不同的情况下,Go是首选语言。就我个人而言,Go太冗长了,在编写确保代码正常运行所需的大量测试时,这尤其痛苦/明显,因为Go的类型系统/编译器不会帮助您确保代码正确性。
Go本质上是新的Java。
konart:我在处理js和ruby项目后的经验(ruby是几年前我的主要语言):那些表达性强的语言导致的代码行数是减少了,但是最后,您遇到的是一堆垃圾,没人愿意修改,因为一些开发人员非常有创造力,他们想要表达自己的想法超过解决问题的能力。如果您确实想解决问题并确保以后的人们可以快速了解解决问题的方式和原因,那么Go是一种工具。而不是对您的创作感到敬畏。
ratww:问题不在于表达性如何。Go的复杂功能很容易被滥用,并且在语言中没有其他选择。
Go中随处可见:它允许反射和空接口周围有很多“巧妙”之处,从而有效地将其转变为规格不明确的动态语言。模板也一样。这是与在Ruby中进行元编程的同一问题,也是困扰一些Java项目的复杂OOP体系结构的同一问题。当然,这都是可以避免的。
恰恰相反,这些功能不是“表达性的”,而是导致了一种“没有人希望接触的废话堆,因为某些开发人员如此有创造力并想表达自己”的问题。
与高表达语言相比,使用Golang(或任何其他“非表达语言”)避免出现这种情况需要团队付出更多的纪律。另一方面,许多新的ES6功能使它具有更高的表达能力,而不必增加复杂性,实际上使代码更易于阅读和更可靠。
Philip-J-Fry:强烈反对:‘Go中随处可见:它允许反射和空接口周围有很多“巧妙”之处’,你可以通过反射来编写聪明的代码。但是除非有解决问题的必要方法,否则我们不鼓励这样做,例如自动JSON编组/解组。没有专业的Go开发人员会立即接触空的接口或反射,而不用冗长的类型安全代码查看解决方案的外观。
dcow:当我谈论表现力时,我指的是Lisp,Scheme,Rust,Haskell,Scala,Kotlin等。持续的注意力持续到他们必须编写测试和部署代码为止。我的意思是,这正是Google喜欢Go的原因。但是我喜欢享受编写代码的乐趣,并且理解我必须对其进行测试并在其整个生命周期内进行维护,所以我倾向于使用有趣且易于维护的语言。对我来说,那些语言倾向于提供正式的宏/元编程,类型系统,支持高阶编程,并且最好执行内存安全性。我宁愿让大部分代码变得无聊,而更多地关注它是否在视觉上可读并且在逻辑上易于理解。
amw-zero:Rust须是有史以来表达能力最低的语言之一,因此与其他实际表达语言一起使用时,这是一个很奇怪的语言。这并不是对Rust的全面否定,因为表达能力不是它的主要目标。
同样,类型系统“健全”是一个非常空洞的愿望。创建一个形式上合理但不实用的类型系统很容易。这与“类型正确性”相同。在Java具有泛型之前,Java程序是“类型正确的”,并且类型系统极为有限。
所以类型本质上是没有意义的目标,因为类型可能意味着很多事情。
takeda:Ruby鼓励人变得聪明,并且我有接触过某人“聪明”代码的经验。我对js不太熟悉,但感觉也与我相似。但是,使事情变得更糟的恰恰是它们都是动态语言,但是有一个中间立场。举例来说,Rust感觉(我仍在计划学习它)看起来相比Java甚至C具有适当的功能。
candiodari:这其实就是ops运维和dev研发之间的传统冲突。软件研发人员想要一些小代码来表达很多东西,同时又要维护一些核心功能,除了不必显式编写代码外,研发人员实际上并不关心边缘情况。不用说,无论研发人员多么聪明,都会有一些极端的情况不是他们想要的。测试侧重于核心功能,仅此而已。
ops运维程序员关注的是:应该让一切正常运行。修改变成第二优先级。他们会讨厌任何他们不确切知道会发生什么情况的极端情况,因为这可能很糟糕,这是他们所有工作的动力。他们想手动编码每个边缘情况,并进行测试。
C / C ++(特别是“智能” C ++),Haskell,Lisp等适用于软件研发人员。
Go,Java,C#等适用于软件运维人员。
一家大型银行可能应该使用Java。尝试创办新银行的人可能更喜欢Haskell。
我认为这种冲突不会很快解决,在软件运维组织中引入变动,除了弄清楚软件系统应如何更改之外,通常是一个挑战。
noisy_boy :我认为当谈到Go的冗长性时,我们不应该指责Java。我可以简单地在Java中执行dict.contains(“ foo”),而不必经历冗长的循环:

    if val, ok := dict["foo"]; ok {
       
//do something here
    }

在Java中,诸如过滤/转换集合之类的常用操作非常简洁-我敢肯定,在Go中这样做至少需要5倍的行数:
   
collection.stream().filter(...).map(...).collect(...)

amw-zero:首先,Go的Map中可能存在或可能不存在值,这是一个普遍的问题。大多数语言仅返回nil或Option值,但是Go返回错误并强制您处理它。我没有评论这是好是坏,但这就是代码之所以这样的原因。
真正的问题是您刚刚弄清楚了代码就是那样的原因,而且原因也很疯狂。出于性能考虑,没有一种语言会故意使某些细节变得冗长,以阻止这样做。即使他们这样做了,key查找也是固定的复杂性,并且是您在所有编程中都可以执行的最有效的操作之一。
 dcow:Go的冗长性还存在某种可以帮助防止并发相关错误的内存安全系统(类似于Rust),那么它的合理性就更高了。冗长而不具有强表达性没有错,强表达性这不是我的风格。
  
Go语言错误处理方式
fpoling:让我为Go感到难过的是,冗长不是来自语言,而是来自其格式化程序。
如果如果err!= nil {return err},如果格式化程序允许在一行中写入,它将使代码行数减少三分之一。
不同意:将错误视为值是处理异常的一种非常优越的方法。我从未见过有力的理由解释。我可以告诉您为什么异常(用Java实现)很酷:您可以像成功执行每个函数一样编写代码,而不是在每个函数调用之后都添加一行(或更多)错误处理代码,这使操作更加困难遵循逻辑。
lolinder:我写Java,但是我更喜欢将错误作为值,因为您不能只考虑幸福的道路。它确实使您考虑对这种特定故障的适当响应。您可以使用exception来做到这一点,但实际上,这就像您说的那样:所有错误处理都委派给某个通用的包罗万象的统一模块,在Web应用程序中,它通常只给出通用的500 Internal Server Error。
如果我将错误编码为值(通常使用Either进行编码),则在我在获得成功的结果之前,必须决定如何优雅地回退是否发生了失败。也许我只是隐藏了需要该数据的视图部分;也许我显示了适当的错误消息;也许我通过电子邮件发送操作,让人们知道问题所在。无论我做什么,我都必须认真考虑,而不仅仅是假设错误会被某人捕获。当不可避免地发生故障时,结果通常会大大改善用户体验。
异常Exception往往使成本过高,以至于没有上下文可以做出适当的决定。当您仍然有足够的上下文时,值往往会迫使人们早日做出决定。
solatic:我不敢苟同:它迫使您考虑功能本身是否失败了但不是为什么)。“为什么”存在于错误(或异常Exception)本身的类型中,但是Go不会让您检查错误的类型。确实,纯粹的人工记忆在大多数情况下会迫使您一次又一次地输入err!= nil。
在Java代码中的每一行包装在try / catch中(这不是惯用的,但绝对有可能),可以实现优雅回退。
如果像Go那样返回错误“值”结果,那么您也不应该直接将生产错误值向您的运营团队发送电子邮件。它无法缩放。您应该记录该错误本身,并且您的监视装置应面向负责更改的相关团队(全面披露:我为这样的监视堆栈公司工作)。您实际上很少想从错误中恢复,这种情况很少见,因为通常这是导致静默故障并难以诊断问题的模式。
lolinder:是的,我不赞成Go的错误处理方法,而只是将错误视为价值的想法。我不能代表Go,但是其他语言非常明显地表明,要处理错误,您必须检查其类型,从而了解“为什么”。
缺乏强制处理(类型检查)异常的原因正是我不喜欢Java模型的原因。在检查错误状态之前,我想拥有一个可能包含或可能不包含我要查找的数据的Either(如果没有,请提供说明)。在真正异常的情况下,我可能会崩溃并给出500错误,但是根据定义,检查的异常应该可以恢复,并且在生产代码库中,我不想不从异常中恢复。
unscaled:在Java中,除了从RuntimeException派生的异常外,您必须捕获或转发所有异常,并且必须在类型签名中指定它们。RuntimeExceptions等同于发生紧急情况,因此您不必catch它们。
Java从不让您忽略非紧急的异常。与Go不同,您可以在其中忽略错误值。
除此之外,Java会强制您明确声明每个函数可以引发什么类型的异常。
Java异常比Go严格,反之亦然。
I/O异常子类之前都是来自于Exception异常处理的最佳实践(就像最近在Go中引入了fmt.Errorf(“%w “))。
我同意Java标准库(尤其是其中的较旧部分)是很糟糕的。Go标准库,不管它有什么问题,都还是很可靠的。
不幸的是,Java标准库和Java EE库的可疑质量在过去已经导致了我们今天在Java中看到的一些不良模式,而这种不良行为并不是该语言所必需的,例如严重滥用继承。
我仍然要在这里指出,返回错误值并不优于抛出异常Excepton,因为:
  1. Java异常处理也可以被强制为显式的。
  2. Go不强制处理错误。实际上,您始终可以忽略函数的返回值或将其错误部分分配。这远没有空白的catch块明确。

puzz:查看任何一段Java代码,并尝试猜测其循环复杂性。这并不简单。因为每一行和每个函数调用都可能失败。而这种失败是代码执行树分支的另一个地方。除非您检查代码中的每个方法调用,否则您将看不到它。
在Go中:每个错误都是显而易见的,并且只要快速地进行处理,您就可以感觉到任何代码的循环复杂性。
所以,仅此而已。代码的复杂性是可见的。
tacitusarc:我认为解释起来有些复杂,但主要归结为:错误是真实的。Java方法让您可以通过声明异常来忽略它们,这种想法是,其他人会处理它。Golang函数使错误更加严重。他们迫使您考虑如何处理错误,并质疑您是否应该在特定情况下出错。实际上,它帮助改变了我对功能的看法。在Java中,有很多行为情况我没有顾及到过,但在golang中,使它们成为幂等既容易,而且事实证明,它更健壮。
引入的用于处理golang的错误处理模式虽然很冗长,但是确实可以使人们确信,一旦编写了代码,就应该处理错误。当然,程序员可以显式地忽略错误,但是这样做与忘记捕获引发的异常不同,因为程序员必须不遗余力地编写代码来忽略错误。
fierro:Go的基本宗旨是应对每个错误。以下是戴夫·切尼(Dave Cheney)的摘录,澄清了这一点:
“对于真正异常的情况,那些代表不可恢复的编程错误(例如索引超出范围)或无法恢复的环境问题(例如堆栈用完)的情况,我们会感到恐慌。”对于所有其他情况,按照这个定义,您在Go程序中遇到的任何错误情况都不是异常的,您希望得到它们,因为无论返回布尔值、错误还是惊慌,它都是在您的测试中产生的结果代码。
lolinder:我认为关键是在Go中,您会期望失败。失败不是异常,它是逻辑的第一部分。
Java会限制您将失败视为规则的异常,并将成功路径视为实际代码。此后,我们很多人观察到,失败方法会导致错误,因为程序员会忽略失败,从而使它们可以由堆栈顶部的“包罗万象”来处理,这通常只是转储一个堆栈跟踪并每天调用它。
Go所支持的范例将错误视为另一种程序状态,而错误的含义与期望的行为同样重要。这迫使程序员考虑失败的所有含义,而不仅仅是添加另一个“ throws”子句。
 
Go vs. Java
ferdowsi :我率先在公司创建了一个新的Go服务。如果是Node或Python,我很容易想到一个团队陷入了数周的API框架,测试框架,整理工具,格式化工具,TLS堆栈,包装和发行研究高峰等问题。使用Go,所有这些问题基本上都可以通过标准库的强大功能来解决。我们再快乐不过了。人们常常低估了Go的联网能力。使用标准库中的服务器,路由器,多路复用器,身份验证等内容构建Web应用程序,然后仅在云中部署二进制Blob(与体系结构无关)即可使您的应用程序比竞争对手的编程语言/框架更轻松地运行。
takeda:我认为拥有选择权只是成熟语言的标志。在python中,您还可以实现API框架,使用标准库进行单元测试,TLS和打包。同样,Go也有3rd party框架。
loopz:可以肯定,总会有权衡取舍。许多人试图传达的是,Go的默认设置往往是更强大,更简单的选择。
但是,对于一个孤独的狼性项目,我将更多地依赖于gorm之类的东西,而不仅仅是手工制作并维护大量的sql。
另一个误解是,这些都是针对没有经验的程序员的。但是您需要数十年的经验才能欣赏Go中已经进行的所有考虑到的权衡取舍。
不过,它只是一个工具,与FP完全不同,它完全是另一种野兽。很明显,Haskell启发了Go和生态系统的许多功能。从另一端看,这可能是另一个很好的工具。
boyter:我之所以使用Java是因为它足够快,具有良好的库并且具有足够的生产力。Go的运行速度几乎一样快,标准库满足了您的大部分需求,并且它可以编译为单个二进制文件的功能使部署变得简单。
我也回头想想,我可以用Java完成所有这些工作吗?可能...但是我也觉得这需要更多的努力。
stickfigure:Java已经走了很长一段路。如今,以Java的函数风格进行编程非常容易。Go似乎竭尽全力使其变得更困难。也许泛型会有所帮助。Go更多地涉及显式命令样式,默认为良好/已知的性能特征。建议不要尝试使用FP,除非只是为了对其进行测试。
byronr:Go和Java是近亲。当然,区别在于JIT:使用Go单个二进制文件意味着您可以将其分解以找出其要执行的操作。JIT增加了一层神秘感。另外,使用Go可以在性能热点中避免堆分配。对于Java,我不太确定。
unscaled:通过JMH运行代码仍然很容易看到如何对代码进行JIT。像IntelliJ这样的JVM和IDE包含大量的工具来进行审查。根据我的亲身经历,让我们回到当前的问题上,如果您有许多小型变量,那么您的应用程序在Java中的性能将比Go更好。Java中的转义分析可能比Go中的转义分析更高级,并且在Java中,如果您更喜欢吞吐量而不是延迟,那么还可以使用分代generational  GC。
byronr:从本质上讲-jar文件并没有定义将要运行的程序。它是jar和运行它的JVM的结合。使用Go,二进制就是全部。因此,即使我可以预测特定的JVM将如何处理给定的jar,但是如果我的代码可能跨多个JVM部署,我仍然会陷入困境。
 
泛型的用处
strken :没有泛型的问题是您构建的系统问题。他们要么放弃类型安全性,要么使用反射,导致难以调试的问题,或者依靠代码生成,这又慢又难以调试。
我同意缺少泛型只会偶尔并不会像预期的那样对生产率产生太大的影响,但是我仍然很高兴看到它们被添加到语言中。
auxym:出于好奇,真诚的问题,因为我根本没有接触过Go。没有泛型的容器类型(如数组或哈希图)如何工作?您是否基本上必须为每种不同的包含类型声明和实现类型/方法?
SiVal:我几乎不需要直接使用泛型。如果我必须针对两种类型编写算法,则可以像处理抽象类型编程所带来的复杂性一样轻松地进行复制和调整。
但是,我需要泛型。数十年来,我一直使用多种语言进行工作,并且我对具有成熟/已调试/优化的算法和数据类型标准库(尤其是“容器”)的语言高度评价。
Go有一个贫乏的“内置”子集,仅此而已。
关于Go的最好的事情之一就是其标准库的高质量,它具有标准库。因此容器和算法库应该存在一个空白。这些算法的专家可以利用Go的并发性来编写库算法,这些库算法提供了性能和安全性的结合,而我根本无法快速创建。
当人们(反复地)声称Go并不需要真正的泛型,因为他们几乎不需要泛型,我不得不怀疑他们对DS和算法的了解是特别高还是特别低:或者他们是否甚至不知道他们本来应该使用的DS /算法。
catlifeonmars:就个人而言,我很少需要专门的算法。我不确定这是否主要是我从事的问题空间(提供用户身份和访问管理的服务),但我从来没有发现需要超越蛮力搜索或需要除数组或内置哈希以外的数据结构映射(有时是并发哈希映射)。我所做的大多数优化都涉及消除网络调用和实现缓存。
zeugmasyllepsis:Go支持一些内置的泛型类型,例如哈希图。Go目前缺少的是用户定义的泛型,尽管正在积极研究中。
 
Go vs. Python
zozbot234 :是的,Go当然比Python更容易阅读。这主要是因为Go是静态类型的,并且具有良好的IMO编辑器工具。在设计中不要原谅其他暴行。
skj:对于大型python项目,由于继承和混合,通常很难弄清楚下一步将执行什么代码。
使用Go,通常非常简单,是的,有接口,但是它们用于人们实际上希望代码选择是动态的地方,而不是人们只是以DRY的名义开心地建立疯狂的层次结构。
因此,有了Go,我发现它非常易于阅读,因为它具有较高的代码局部性,并且易于通过函数调用跟踪到正确的目标。
对于已经本地化的事物,“易于阅读”只是“熟悉”的代码。Go绝对更容易阅读,因为通过跟踪进行绝对更简单。其他任何事情都只是在拼写,只有很少的经验,这些障碍就消失了。
joshuamorton:例如,对于某些特定类型的python而言这是正确的,但对于我编写的大多数python代码而言,则并非如此。我会向您洗脑“鼓励不复杂的体系结构”是一件好事,但是Go在另一个方向上摇摆得太远了,例如python中的set(foo)-set(bar)表现得非常好(并且高效)的方法来给我foo中没有的东西。
在执行过程中,您可以将其作为嵌套的for循环(难于读取,而不是n ^ 2而不是n)来进行,也可以显式循环遍历每个迭代器并转换为映射(公平地讲,这正是python所做的也可以),然后自己编写map差异图,这很容易搞砸,应该进行彻底的测试以确保您在实现中没有犯任何错误。
在python中,它的一行清晰,有表现力的,已知正确的代码。当然,泛型提议是想在功能上的改善做很多工作,但是我不同意go的“严格意义上是容易做的”,因为通常会有更多的可追溯之处,因为该语言为您提供的抽象要少得多,因此与其要展开使用某些高级语言功能的单个复杂表达式,您必须展开本地项目中这些抽象的pseduo重新实现。
LandR :go太冗长而难以阅读。我认为它是目前表达能力最差的语言之一。
sangnoir:缺乏表达能力会提高可读性:在团队环境中,并非每个人都以易于理解的方式“表达”自己。代码只写一次,但是却读很多遍。
我曾经在一个维护大型Perl代码库的团队中工作:Perl既可以表现力简洁,但这会降低可读性,尤其是在同事感觉喜欢使用晦涩的语言功能以“精妙的”单行代码表达他们的聪明/个性的时候。当聪明的代码有错误时,您将不得不以更加“聪明”的方式修复边缘情况,或者将其展开为可读的函数。
corty :Go可以轻松替换Python或Ruby(因为它速度更快并且可以使用库来满足您的需要)。Go可以代替Java和C#,因为并发更容易并且性能相当。Go不能在任何功能中替换C ++,因为C的编译器太低级了,底层的东西是不可能的,并且Go库将永远无法与C和C ++相提并论。
但是总的来说,Go很不错,因为有足够的工具可以很好地完成大多数事情,并且该语言足够原始,可以使该工具在几乎所有情况下都能正常工作。这与C ++不同,因为那里的工具对于模板,宏,继承和指针总是太愚蠢。
而且它不同于Java,后者的工具过于复杂,缓慢且价格昂贵。
另外,Go还没有积累太多的“惊奇”错误特征,因此与父代命名的任何其他语言相比,可以更可靠地编写和检查代码。恐慌可以看作是这种功能失常的罕见情况,但通常仅用于表示通常无法恢复的问题,例如“内存不足”。因此,异常远非the脚。
cle:几年来,我维护了数十万行Go代码。我不会为此选择其他语言。
维护这样的代码库的重要属性是语言规律性(大多数代码以相同的方式编写,并且您不必对事物进行个人的“品味”决定),良好的文档和在线社区,维护良好的库,工具功能用于版本控制和命名空间,简化部署等。
我同意Python非常适合面试。除非有真正令人信服的理由,否则我不会将Python用于由一群人维护多年的大型代码库。
kuang_eleven
Python具有所有这些功能:
  • -语言规范;黑色是多年来的默认设置,如果您想强制使用其他样式,则可以使用其他工具
  • -文档和在线社区;甚至没有竞赛,在广泛的文档和在线支持中没有语言能与Python相提并论
  • -库;几乎一样,Python标准库是世界一流的
  • -版本控制和命名空间;根据您的需要,Python再次变得更强大,Go中的命名空间是一场灾难。
  • -部署简单;...好吧,你得到了这个。Python部署比预期的难。不过,我发现我的Docker映像在两者之间具有相同的复杂性。使用Python进行更复杂的部署,但无需编译

  
其他
baby:Golang的全部重点是易于阅读,有助于和维护。这是因为它的表现力较差,并且在编写代码时具有较少的自由度。反过来,这会使编写代码更加困难,但是如果您想使阅读代码更容易,那就是必须要权衡的问题。
我坚信,人们已经能够在没有泛型的情况下编写出惊人的大型应用程序,而泛型的添加只会扼杀Golang的最大功能之一:简单性。