禅去



在评估我的工作时,我最近对如何编写好的代码进行了很多思考。鉴于没有人对如何编写不良代码感兴趣,因此出现了一个问题:如何知道您是否在Go上编写了良好的代码如果好与坏之间存在某种尺度,那么如何理解尺度的哪一部分属于好?它的属性,属性,区别特征,模式和习惯用法是什么?

惯用语


这些考虑导致我习惯了Go语言。如果我们称某些东西为“惯用语”,则这对应于一段时间的某种风格。如果某些东西不是惯用语,那么它就不符合主导风格。那不是时尚。

更重要的是,当我们说某人的代码不是惯用语时,这并不能解释原因。为什么不习惯?答案由字典给出。

成语(n。):整体上使用的语音革命,不做进一步分解,通常不允许其内部进行置换。

习语是常见含义的标志。书籍不会教您惯用的Go;只有当您成为社区的一员时,它才会为人所知。

我担心惯用的Go咒语,因为它通常是限制性的。她说:“你不能和我们坐在一起。” 当我们批评别人的作品“不是惯用的”时,这不是我们的意思吗?他们做错了。这看起来不对。这与时代风格不符。

我认为惯用的Go不适合教如何编写好的代码,因为从本质上讲,这意味着告诉人们他们做错了什么。最好提供这样的建议,即在一个人最想接收该建议时不要将其推开。

俗语


让我们转移一些惯用的问题。Go程序员还固有哪些其他文化产物?转到美丽的Go Proverbs页面这些俗语是合适的学习工具吗?他们会告诉初学者如何编写好的Go代码吗?

我不这么认为。我不想贬低作者的工作。他撰写的谚语仅仅是观察,而不是意义的定义。字典再次解救了:

谚语(n。):具有文字或象征意义的简短陈述。

Go Proverbs的任务是展示语言体系结构的深层本质。但是,对于来自没有结构化类型的语言的初学者来说,空的界面什么也没说 ”这样的建议会有用吗?

在一个不断发展的社区中,重要的是要认识到围棋学生的数量远远大于能熟练使用这种语言的学生数量。也就是说,在这种情况下,俗语可能不是最好的学习方法。

设计价值


刘丹(Dan Liu)在Windows NT-Windows 2000 Windows开发团队中发现 Mark Lukowski关于设计文化的旧演讲,之所以提及是因为Lukowski将文化描述为评估架构和做出折衷的一种常见方式。


主要思想是在未知的架构内做出基于价值的决策NT团队具有以下价值观:可移植性,可靠性,安全性和可扩展性。简而言之,设计价值是解决问题的一种方法。

追求价值


Go的显式值是什么?决定Go程序员如何解释世界的关键概念或哲学是什么?他们如何宣布?他们怎么教?他们是如何遵循的?它们如何随着时间变化?

您如何转换Go程序员以获得Go设计的价值?还是经验丰富的Go-pro,您如何将自己的价值观传承给子孙后代?这样一来,知识转移的过程不是可选的吗?没有新参与者和新思想的涌入,我们的社区将变得近视和枯萎。

其他语言的价值观


为了准备我要说的方式,我们可以注意其他语言及其设计价值。

例如,在C ++和Rust中,人们认为程序员不应该为自己不使用的功能付费。如果程序未使用该语言的某些资源密集型功能,则不能强迫程序承担维护此功能的成本。该值从语言投影到标准库中,并用作评估用C ++编写的所有程序的体系结构的标准。

Java,Ruby和Smalltalk的主要价值- 一切都是对象该原则在消息传递,信息隐藏和多态性方面是程序设计的基础。在这些语言中,遵循过程或功能范式的体系结构被认为是错误的。或者,就像Go程序员会说的那样,不是惯用的。

让我们回到我们的社区。Go程序员自称有哪些设计价值?关于该主题的讨论通常是零散的,因此要表达一系列含义并不容易。达成协议势在必行,但随着讨论参与者的增加,达成协议的难度成倍增加。但是,如果有人为我们做了这项艰巨的工作呢?

Zen Python Go


几十年前,蒂姆·彼得斯(Tim Peters)坐下来写了PEP-20 - The Zen of Python他试图记录Guido Van Rossum作为慷慨的终生Python独裁者所坚持的设计价值。

让我们看一下Python的Zen,看看我们是否可以学习有关Go设计师的设计价值的知识。

一个好的包装始于一个好名字


让我们从一个敏锐的开始:

命名空间是个好主意,让我们把它们做大!

Python的禅,记录19。

足够明确:Python程序员应该使用名称空间。很多空间。

在Go术语中,名称空间是一个包。毫无疑问,捆绑有利于设计和重用。但是对于如何执行此操作可能会感到困惑,尤其是如果您有多年使用另一种语言进行编程的经验。

在Go中,每个程序包都必须针对某些内容进行设计。名称是了解此目的地的最佳方法。重新设计了Peteres的思想,Go中的每个程序包都应该针对一件事进行设计。

这个想法并不新鲜,我已经讨论过了。但是,为什么要使用这种方法而不是使用另一种方法,在这种方法中,为了满足详细分类的需要而使用软件包?一切都与变化有关。

— , .


变更是我们参与的游戏的名称。作为程序员,我们管理变更。如果做得好,我们称之为架构。如果情况不好,我们称之为技术债务或遗留代码。

如果您编写的程序能与一组固定的输入数据一起运行一次,那么该程序是否具有良好的代码就不会引起人们的兴趣,因为只有其工作结果才对业务很重要。

但这不会发生。程序,需求和输入数据更改中存在错误,只有极少数的程序具有单一的执行期望。也就是说,您的程序随着时间改变。也许这项任务会交给您,但很可能其他人会完成。有人需要随附此代码。

我们如何使更改程序变得更加容易?在各处添加接口?一切都适合创建存根吗?紧密部署依赖项?也许,对于某些类型的程序,这些技术适用,但不适用于许多程序。但是,对于大多数程序而言,创建灵活的体系结构不仅仅是设计。

如果不是代替扩展组件,我们将替换它们吗?如果该组件未按照说明进行操作,则该进行更改了。

一个好的包装始于选择一个好的名字。考虑一下它是一个简短的演示,它仅用一个词来描述软件包的功能。并且当名称不再符合要求时,请查找替代名称。

简单很重要


简单胜于复杂。

Python的禅宗,条目3。

PEP-20声称简单胜于复杂,我完全同意。几年前,我写道:


大多数编程语言一开始都尝试简单,但后来决定变得强大。

根据我的观察,至少在那时,我不记得一种我知道不会被认为那么简单的语言。作为证明和诱惑,每种新语言的作者都宣称简单。但是我发现简单性并不是与Go相同时代的许多语言的核心价值(Ruby,Swift,Elm,Go,NodeJS,Python,Rust)。也许这会碰到一个痛处,但是可能的原因是这些语言都不是简单的。否则他们的作者并不认为它们简单。简单性未包含在核心价值列表中。

您可以认为我过时,但是这种简单性何时过时?为什么商业软件行业会不断高兴地忘记这个基本事实?

创建软件体系结构的方法有两种:使软件体系结构变得如此简单,以至于缺乏缺陷是显而易见的;而使软件体系结构如此复杂以至于它没有明显的缺陷。第一种方法要困难得多。

查尔斯·霍尔(Charles Hoar),《皇帝的旧衣服》,图灵奖演讲,1980年

简单并不意味着容易,我们知道。通常需要花费更多的精力来确保易用性,而不是易于创建。

简单性是可靠性的关键。

Edsger Dijkstra,EWD498,1975年6月18日

为什么要追求简单?为什么Go程序变得简单很重要?简单意味着原始,意味着可读性和易于遵循。简单并不意味着没有技巧,而是意味着可靠,可理解和可理解。

编程的核心是复杂性管理。

Brian Kernigan,软件工具(1976)

Python是否遵循其简单性的主张是一个有争议的问题。但是,在Go上,简单性是核心价值。我想我们都同意,在Go语言中,简单代码比智能代码更可取。

避免包级状态


显式胜于隐式。

Python的禅,入门2

在我看来,彼得斯在这里宁愿做梦,也不愿坚持事实。在Python中,很多东西是不明确的:装饰器,dunder方法等。无疑,这些是功能强大的工具,它们的存在是有原因的。关于每个功能的实施,特别是复杂的操作,有人工作。但是,由于积极使用这些功能,因此在读取代码时很难评估操作成本。

幸运的是,我们Go程序员可以选择使代码明确。也许对您而言,表象可能是官僚主义和冗长的同义词,但这只是肤浅的解释。仅关注语法,照顾行的长度以及将DRY原理应用于表达式是错误的。在我看来,提供联系和状态方面的明确性更为重要。

连接性是衡量一个人对另一个人的依赖性的一种方法。如果一个与另一个紧密相关,则两者一起移动。影响一个的行为直接反映在另一个行为中。想象一下,一列火车将所有轿厢连接在一起,或者更确切地说,将它们连接在一起。蒸汽火车去的地方有车。

连通性也可以用术语内聚-内聚来描述。这是一个人属于另一个人的量度。在焊接团队中,所有参与者都非常适合彼此,就好像他们是特别创造的。

为什么连贯性很重要?与火车一样,当您需要更改一段代码时,您必须更改其余紧密相关的代码。例如,某人发布了他们的API的新版本,现在您的代码无法编译。

API是不可避免的绑定源。但是它可以以更隐蔽的形式呈现。每个人都知道,如果API的签名已更改,则往返于API的数据也会更改。全部与函数签名有关:我采用一种类型的值并返回其他类型的值。如果API开始以其他方式传输数据?如果每个API调用的结果都取决于上一个调用,即使您没有更改设置怎么办?

这称为状态,状态管理是计算机科学中的一个问题。

package counter

var count int

func Increment(n int) int {
        count += n
        return count
}

这里我们有一个简单的包装counter。要更改计数器,可以调用Increment,如果以零值递增,甚至可以取回该值。

假设您需要测试此代码。每次测试后如何重置计数器?如果要并行运行测试,该怎么做?并假设您要在程序中使用多个计数器,您会成功吗?

当然不是。显然,解决方案是将变量封装variable在类型中。

package counter

type Counter struct {
        count int
}

func (c *Counter) Increment(n int) int {
        c.count += n
        return c.count
}

现在想象一下,所描述的问题不仅限于计数器;它还影响了应用程序的主要业务逻辑。您可以单独测试吗?您可以并行测试吗?您可以同时使用多个实例吗?如果对所有问题的回答均为“否”,则原因是数据包级别的状态。

避免这些情况。通过为类型提供类型所需的依赖关系(而不是使用包变量)来减少连接性和噩梦远程操作的数量。

为失败而不是成功制定计划


切勿默默地传递错误。

Python的禅,入门10

这是关于鼓励武士式异常处理的语言的说法:胜利就回来还是根本不回来。在基于异常的语言中,函数仅返回有效结果。如果函数无法执行此操作,则控制流程将以完全不同的方式进行。

显然,未经检查的异常是不安全的编程模型。如果您不知道哪些表达式会引发异常,如何在出现错误的情况下编写可靠的代码? Java尝试通过检查异常的概念来降低风险。据我所知,在其他流行语言中没有这种解决方案的类似物。许多语言都有例外,并且除Java之外的所有地方都不会检查它们。

显然,Go走了一条不同的道路。Go程序员认为,可靠的程序由处理成功路径之前处理故障的部分组成鉴于该语言是为服务器开发,多线程程序以及处理通过网络输入的数据而创建的,因此程序员应集中精力处理意外和损坏的数据,超时和连接失败。当然,如果他们想制造可靠的产品。

我认为错误应该得到明确处理,这应该是语言的主要价值。

彼得· 伯根( Peter Burgon),GoTime#91

我赞同彼得的话,它们是本文写作的动力。我相信Go的成功归功于显式错误处理。程序员主要考虑可能发生的崩溃。首先,我们解决诸如“假设”之类的问题。结果是程序在编写代码的阶段处理故障,而不是在操作过程中发生故障。

该代码的详细程度

if err != nil {
    return err
}

超过在每个故障状态发生时刻意处理每个故障状态的重要性。关键在于显式处理每个错误的值。

早日回报比深度投资更好


同级优于嵌套

Python Zen,第5条

这种明智的建议来自缩进是控制流主要形式的语言。我们如何解释Go术语中的这一技巧? gofmt管理Go程序中的全部空白空间,因此我们在这里无事可做。

我在上面写过关于软件包名称的信息。最好避免使用复杂的程序包层次结构。以我的经验,程序员越努力尝试对基于Go的代码进行分离和分类,则周期性导入软件包的风险就越高。

我相信,《The Zen of Python》第五条的最佳用途是在函数内部创建控制流。换句话说,避免需要多级缩进的控制流。

直接可见性是一条直线,沿着该直线视图不会被任何东西遮挡。

May Ryer,代码:将快乐路径与左边缘对齐

May Ryer可以直接将这种想法描述为编程:

  • 如果不满足前提条件,请使用控制语句尽早返回。
  • 将成功返回的语句放在函数的末尾,而不是放在条件块中。
  • 通过提取函数和方法来降低总体嵌套级别。

尝试确保重要功能永远不会移出视线到屏幕的右边缘。这个原则有一个副作用:您将避免与团队就线路长度发生无意义的争执。

每次缩进时,您都会在程序员的脑袋中添加一个前提条件,即占用其7±2个短期内存插槽之一。而不是加深嵌套,请尝试使函数的成功路径尽可能靠近屏幕左侧。

如果您认为运行缓慢,请通过基准测试进行验证


面对模棱两可的想法,放弃诱惑去猜测。

Python 12的禅宗

编程基于数学和逻辑。这两个概念很少使用运气元素。但是,作为程序员,我们每天都会做出许多假设。这个变量有什么作用?此选项有什么作用?如果我在这里过零会怎样?如果我两次拨打寄存器会怎样?在现代编程中,您必须承担很多责任,尤其是在使用其他人的库时。

该API应该易于使用,并且难以错误使用。

乔什·布洛赫(Josh Bloch)

我知道帮助程序员避免在创建API时猜测的最好方法之一是关注标准使用方法。呼叫者应该能够尽可能容易地执行正常操作。但是,在我写很多东西并谈论设计API之前,这是我对记录12的解释:不要猜测性能主题

尽管您对Knut的建议持态度,但Go成功的原因之一就是执行的有效性。可以用这种语言编写有效的程序,而且,有了它,人们选择去。有许多与绩效有关的误解。因此,当您寻找提高代码性能的方法,或遵循诸如“搁置速度变慢”,“ CGO成本很高”或“始终使用原子运算而不是互斥量”之类的教条提示时,请不要算命。

由于教条过时,请勿使您的代码复杂化。如果您认为某些工作进展缓慢,请首先在基准测试的帮助下进行确认。Go具有出色的免费基准测试和性能分析工具。使用它们来发现代码性能的瓶颈。

在开始使用gorutin之前,先了解它何时会停止


我想我已经列出了PEP-20的有价值的物品,也许将它们的解释扩展到了好口味之外。很好,因为尽管这是一种有用的修辞手法,但我们仍在谈论两种不同的语言。

编写g,o,一个空格,然后编写一个函数调用。三按按钮,不能再短了。单击三个按钮,即可启动子流程。

罗伯·派克(Rob Pike),《复杂性》(Simplicity is Complicated),dotGo 2015

接下来的两个技巧将介绍给goroutine。 Gorutins是语言的特征,是我们对高水平竞争的回应。它们非常易于使用:go在运算符前加一个字,然后异步运行该函数。没有执行线程,没有池执行器,没有ID,没有完成状态跟踪。

Gorutins很便宜。由于运行时环境具有在少量执行线程(无需管理)中多路复用goroutine的能力,因此您可以轻松创建数十万或数百万个goroutine。这使您能够以执行线程或事件回调的形式创建在使用其他竞争模型时不切实际的架构。

但是,无论goroutine多么便宜,它们都不是免费的。它们的堆栈至少需要几千字节。当您拥有数百万个goroutine时,它就会变得引人注目。我并不是要说,如果架构将您推向了这一点,则您无需使用数百万个goroutine。但是,如果使用它,监视它们就非常重要,因为goroutine会消耗大量资源。

Goroutine是Go中所有权的主要来源。为了发挥作用,goroutine必须做一些事情。也就是说,几乎总是包含指向资源的链接,即所有权信息:锁,网络连接,发送通道末尾的数据缓冲区。在goroutine生存期间,将保持锁定状态,保持连接打开状态,保存缓冲区,通道接收者将等待新数据。

释放资源的最简单方法是将它们链接到goroutine生命周期。完成后,将释放资源。而且,由于运行goroutine非常容易,因此在编写“ go and space”之前,请确保您已回答以下问题:

  • goroutine在什么情况下会停止?Go不能告诉goroutine结束。由于特定原因,没有停止或中断功能。我们不能命令goroutine停止,但是我们可以礼貌地询问。这几乎总是与通道的操作有关。当它关闭时,范围会循环退出通道。关闭通道时,可以选择它。从一个goroutine到另一个goroutine的信号最好表示为一个封闭通道。
  • ? , , : ?
  • , ? , - . , . . , .


可能在您的任何认真的Go程序中都使用了并发。这通常会导致工作程序模式的问题-每个连接一个goroutine。

一个很好的例子是net / http。停止拥有监听套接字的服务器非常简单,但是由该套接字生成的goroutine呢? net / http在请求对象内部提供了一个上下文对象,该上下文对象可用于告知侦听器需要取消请求,从而中断goroutine。但是尚不清楚如何找出何时需要完成所有这些操作。打电话是一回事context.Cancel,知道取消已完成是另一回事

我经常发现net / http的错误,但不是因为它很糟糕。相反,它是Go代码库中最成功,最古老和最受欢迎的API。因此,需要仔细分析其架构,演变和缺陷。考虑这种奉承,而不是批评。

因此,我想将net / http作为良好实践的反例。由于每个连接都是由type内部创建的goroutin处理的net/http.Server,因此net / http包外部的程序无法控制由接收套接字创建的goroutin。

这个领域的架构仍在发展中。您可以回想一下Go开发团队run.Group的go-kit或ErrGroup,它提供了执行,取消和等待异步执行功能的框架。

对于每个编写可以异步执行的代码的人,创建体系结构的主要原理是将运行goroutine的责任转移给了调用者。让他选择他要如何运行,跟踪并等待您的功能完成。

编写测试以阻止您的包API的行为


您可能希望在本文中我不会提及测试。抱歉,再等一次。

您的测试是对程序执行和不执行的协议。单元测试应在包级别阻止其API的行为。测试以代码形式描述了程序包承诺要做的事情。如果每个输入转换都有一个单元测试,那么您以代码的形式(而不是文档的形式)定义了有关代码将执行的协议。

批准此协议就像编写测试一样简单。在任何阶段,你可以声明具有高度的信心的行为,人们依靠所做的更改将继续发挥作用的变化前后。

测试阻止API行为。添加,更改或删除公共API的任何更改都应包括测试中的更改。

节制是一种美德


Go是一种只有25个关键字的简单语言。在某种程度上,这突出了该语言内置的功能。这些是允许语言自我推广的功能:简单竞争,结构化打字等。

我认为我们所有人都试图一次使用Go的所有功能感到困惑。你们当中有多少人受到了渠道使用的启发,以至于尽可能使用它们?我发现生成的程序很难测试,它们脆弱且过于复杂。你呢?

我在goroutines上也有同样的经历。试图将作品分成小块,我创建了难以控制的goroutin黑暗,并完全忽略了这样一个事实,即大多数人总是由于前任的期望而被封锁。代码是完全一致的,为了获得较小的优势,我不得不大大增加复杂性。你们当中有多少人遇到过?

我在嵌入方面也一样。起初,我将其与继承相混淆。然后,他遇到了一个脆弱的基类的问题,将已经具有多个任务的几种复杂类型组合成甚至更加复杂的巨大类型。

这可能是最无效的建议,但我认为必须提一下。建议是一样的:保持适度,Go的功能也不例外。只要有可能,就不要使用goroutines,通道,嵌入结构,匿名函数,大量的程序包和接口。使用比智能解决方案更简单的解决方案。

易于维护


最后,我将给您提供PEP-20的另一个条目:

可读性很重要。

Python的禅,记录7

关于所有程序设计语言中代码可读性的重要性,已经有很多说法。那些推广Go的人使用诸如简单,可读性,清晰度,生产力之类的词。但是所有这些都是一个概念的同义词-维护方便。

真正的目标是创建易于维护的代码。超出了作者的代码。不仅可以作为时间的投资,而且可以作为获取未来价值的基础而存在的代码。这并不意味着可读性并不重要,只是维护的便利性更为重要

Go不是为单行程序优化的语言之一。而且不是为行数最少的程序而优化的语言之一。我们没有针对磁盘上源代码的大小或在编辑器中编写程序的速度进行优化。我们希望优化我们的代码,以使它对读者更易理解。因为是他们将不得不陪伴他。

如果您为自己编写一个程序,那么它可能只会启动一次,或者您是唯一看到其代码的人。在这种情况下,请执行任何操作。但是,如果有多个人使用该代码,或者长时间使用该代码,并且需求,功能或运行时可能发生变化,则该程序应该易于维护。如果软件无法维护,则无法重写。这可能是您的公司最后一次投资Go。

您出发后的辛勤工作会很方便吗?您如何为今天之后的那些人提供代码维护方面的便利?

All Articles