标签 错误处理 下的文章

Go正走在成为下一个企业级编程语言的轨道上

发展演化了十年的Go语言已经被证明了是云计算时代的首选编程语言,但Go的用武之地显然不局限于此。Kevin Goslar近期在Hacker Noon发表了一篇名为:《Go is on a Trajectory to Become the Next Enterprise Programming Language》的文章,阐述了Go可能成为下一个企业编程语言的理由,这里是那篇文章的中文译文,分享给大家。

img{512x368}

摘要

Go是一种专门为大规模软件开发而设计的编程语言。它提供了强大的开发体验并避免了现有编程语言存在的许多问题。这些因素使其成为最有可能在未来替代Java主导企业软件平台的候选者之一。对于那些寻求在未来几十年内构建大规模云基础架构的安全和前瞻性技术的公司和开源计划而言,我建议它们将Go视为其主要的编程语言。Go的优势如下:

  • 基于现实世界的经验
  • 专注于大型工程
  • 专注于可维护性
  • 保持简单明了
  • 使事情显式且明显
  • 很容易学习
  • 仅提供了一种做事方式
  • 支持简单地内置并发
  • 提供面向计算的语言原语
  • 使用OO – 好的部分
  • 拥有现代化的标准库
  • 强制执行标准化格式
  • 有一个非常快的编译器
  • 使交叉编译变得容易
  • 执行得非常快
  • 需要较小的内存占用
  • 部署规模小
  • 部署完全独立
  • 支持vendor依赖
  • 提供兼容性保证
  • 鼓励提供良好的文档
  • 商业支持的开源

请继续阅读有关上述每个优势点的更多详细信息。然而,在进入Go之前,你应该注意:

  • 不成熟的库
  • 即将到来的改变
  • 没有“硬实时”支持

简介

Go是Google开发的一种编程语言,在过去几年中取得了很大的成功。大部分现代云计算,网络和DevOps平台都是Go语言编写的,例如:DockerKubernetesTerraformETCDistio等。许多公司也将它用于通用软件开发。Go所具备的功能让这些项目吸引了大量用户,而Go的易用性也使得这些项目有了很多的贡献者。

Go的优势来自于简单和经过验证的想法的结合,同时避免了其他语言中出现的许多问题。这篇博客文章概述了Go背后的一些设计原则和工程智慧,并展示它们是如何结合在一起的 – 它们使Go成为下一代大型软件开发平台的优秀候选者。许多编程语言在个别领域都比较强大,但是在将所有领域都结合起来时,没有其他语言能够如此一致地“得分”,特别是在大型软件工程方面。

基于现实世界的经验

Go是由经验丰富的软件行业资深人士创建的,他们长期以来一直感受到现有语言的缺点带来的痛苦。几十年前,Rob PikeKen Thompson在Unix,C和Unicode的发明中发挥了重要作用。在实现了用于JavaScript和Java的V8和HotSpot虚拟机之后,Robert Griesemer在编译器和垃圾收集方面拥有着数十年的经验。在太多次的不得不等待他们的谷歌规模的C++/Java代码库的编译过程的推动下,他们开始着手创建一门新的编程语言,这门语言中凝聚了他们通过编写半个世纪代码过程中所学到的一切。

专注于大型工程

几乎任何编程语言都可以成功构建小型工程项目。当成千上万的开发人员在数十年的持续时间压力下在包含数千万行代码的大量代码库上进行协作时,真正痛苦的问题就会发生。这会导致以下问题:

  • 超长的编译时长会中断开发过程
  • 代码库由几个人/团队/部门/公司拥有,混合了不同的编程风格
  • 该公司雇佣了数千名工程师,架构师,测试人员,Ops专家,审计员,实习生等,他们需要了解代码库,但需要具有广泛的编码经验
  • 依赖于许多外部库或运行时,其中一些不再以其最初的形式存在
  • 每行代码在代码库的生命周期内平均被重写了10次,留下了疤痕,瑕疵和技术偏移
  • 文档不完整

Go专注于减轻这些大规模的工程难题,有时是以使小型工程变得更加繁琐为代价,例如在这里和那里需要一些额外的代码。

专注于可维护性

Go强调尽可能多地将工作转交到自动代码维护工具中。Go工具链提供了最常用的功能,如格式化代码和自动package导入、查找符号的定义和用法、简单的重构以及代码味道的识别。由于标准化的代码格式化和单一的惯用方式,机器生成的代码更改看起来非常接近Go中人为生成的更改。并而使用类似的模式,使得人和机器的协作更加无缝。

保持简单直接

初级程序员为简单问题创建简单的解决方案。高级程序员为复杂问题创建复杂的解决方案。伟大的程序员找到复杂问题的简单解决方案。-  查尔斯康奈尔

很多人都对Go不包含他们喜欢的其他语言概念感到惊讶。Go确实是一种非常小而简单的语言,只包含最少的正交和经过验证的概念。这鼓励开发人员以最少的认知开销编写最简单的代码,以便许多其他人可以理解并使用它。

使事情显式而明显

良好的代码是显而易见的,避免聪明,模糊的语言功能,扭曲的控制流和间接性。

许多语言都致力于使编写代码变得高效。然而,在其生命周期中,人们将花费大约(100倍)的时间阅读代码,而不是首先编写所需的代码。例如,审查,理解,调试,更改,重构或重用它。在查看代码时,通常只能看到并理解它的一小部分,通常没有对整个代码库的完整理解。为了解释这一点,Go将一切都显式化了。

一个例子是错误处理。让异常在各个点中断代码并使沿着调用链处理可能会更容易。Go需要手动处理或返回每个错误。这使得它可以准确地显示代码可以被中断的位置以及如何处理或包装错误。总的来说,这使得错误处理更容易编写,但更容易理解。

简单易学

Go非常小而且简单,可以在短短几天内研究整个语言及其基本概念。根据我们的经验,经过不超过一周的培训(与其他语言的以月为单位相比),初学者可以理解Go专家编写的代码,并为此做出贡献。为了方便大量人群,Go网站提供了所需的所有教程和深入的文章。这些教程在浏览器中运行,允许人们在将Go安装到本地计算机上之前学习和使用Go。

一种做事方式

Go语言通过个人自我表达赋予团队合作能力。

在Go(和Python)中,所有语言特征都是正交的并且彼此互补,通常做某事只有一种方法。如果您要求10位Python或Go程序员解决问题,您将获得10个相对类似的解决方案。不同的程序员在彼此的代码库中感觉更有家的感觉。在查看其他人的代码时,每分钟的WTF更少,而且人们的工作更好地融合在一起,从而形成一个人人都为之骄傲并且喜欢工作的一致性。这避免了大规模的工程问题,例如:

  • 开发人员将良好的工作代码视为“混乱”,并要求在他们可以使用之前重写它,因为他们不会像原作者那样思考。
  • 不同的团队成员在该语言的不同子集中编写相同代码库的部分内容。

img{512x368}
来源:https://www.osnews.com/story/19266/wtfsm

简单,内置并发

Go专为现代多核硬件而设计。

目前使用的大多数编程语言(Java,JavaScript,Python,Ruby,C,C ++)都是在20世纪80年代到2000年代设计的,当时大多数CPU只有一个计算核心。这就是为什么它们本质上是单线程的,并将并行化视为事后增加的边缘情况,通过诸如线程和同步点之类的附加组件实现,这些附加组件既麻烦又难以正确使用。第三方库提供了更简单的并发形式,如Actor模型,但总有多个选项可用,导致语言生态系统碎片化。今天的硬件拥有越来越多的计算内核,软件必须并行化才能在其上高效运行。Go是在多核CPU时代编写的,并且在语言中内置了简单,高级的CSP风格的并发特性。

面向计算的语言原语

在基础层面上,计算机系统接收数据,处理它(通常经过几个步骤),并输出结果数据。例如,Web服务器从客户端接收HTTP请求,并将其转换为一系列数据库或后端调用。一旦这些调用返回,它就会将接收到的数据转换为HTML或JSON并将其输出给调用者。Go的内置语言原语直接支持这种范例:

  • 结构体代表数据
  • reader和writer代表流式IO
  • 函数处理数据
  • goroutines提供(几乎无限制的)并发
  • 通道用于管理并发处理步骤之间的数据

由于所有计算原语都是由语言以直接的形式提供的,因此Go源代码可以更直接地表达服务器执行的操作。

OO – 好的部分

img{512x368}
在基类中改变某些东西的副作用

面向对象非常有用。这几十年OO的应用是富有成效的,并且让我们了解它的哪些部分比其他部分可以更好地扩展。基于这些认知,Go采用面向对象的新方法。它保留了封装和消息传递等优点。Go避免了继承,因为它现在被认为是有害的,Go为组合提供头等的支持

现代标准库

许多当前使用的编程语言(Java,JavaScript,Python,Ruby)是在互联网成为当今无处不在的计算平台之前设计的。因此,这些语言的标准库仅为未针对现代互联网优化的网络提供相对通用的支持。Go是十年前创建的,当时互联网已经全面展开。Go的标准库允许在没有第三方库的情况下创建更复杂的网络服务。这可以防止使用第三方库的常见问题:

  • 碎片化:实现相同功能的总有多种选择
  • 膨胀:库通常实现的不仅仅是它们的用途
  • 依赖地狱:库通常依赖于特定版本的其他库
  • 质量未知:第三方代码可能具有可疑的质量和安全性
  • 未知支持:第三方库的开发可以随时停止
  • 意外更改:第三方库通常不像标准库那样进行严格的版本管理

Russ Cox的更多背景信息。

标准化格式

Gofmt的风格是没有人喜欢的,但gofmt是每个人的最爱。 – Rob Pike

Gofmt是一种以标准化方式格式化Go代码的程序。它不是最漂亮的格式化方式,而是最简单,最不讨厌的方式。标准化的源代码格式化具有惊人的积极影响:

  • 重点讨论重要主题:它消除了围绕标签与空格,缩进深度,每行长度,空行,花括号放置等的一系列无意义的争论
  • 开发人员在彼此的代码库中感到宾至如归,因为其他代码看起来很像他们编写的代码。每个人都喜欢自由地按照自己喜欢的方式格式化代码,但如果其他人冒昧地按照他们自己喜欢的方式格式化>代码,那么每个人都讨厌它。
  • 自动代码更改不会弄乱手写代码的格式,例如通过引入意外的空白更改。

许多其他语言社区现在正在开发gofmt等价物。当构建为第三方解决方案时,通常会有几种竞争格式标准。例如,JavaScript世界提供PrettierStandardJS。可以一起使用其中之一或两者。许多JS项目都没有采用它们,因为这是一个额外的决定。Go的格式化程序内置于该语言的标准工具链中,因此只有一个标准,每个人都在使用它。

快速编译

img{512x368}
来源:https://xkcd.com/303

大型代码库的长编译时间是引发Go语言起源的一个微小的原因。Google主要使用C++和Java,与Haskell,Scala或Rust等更复杂的语言相比,它可以相对快速地编译。尽管如此,当编译大型代码库时,即使是少量的慢速也会把人激怒,编译工作流中断导致编译延迟。Go是从头开始设计的,以使编译更有效,因此编译器速度非常快,几乎没有编译延迟。这为Go开发人员提供了类似于脚本语言的即时反馈,并具有静态类型检查的额外好处。

交叉编译

由于语言运行时非常简单,因此它已被移植到许多平台,如macOS,Linux,Windows,BSD,ARM等。Go可以开箱即用于编译所有这些平台的二进制文件。这使得我们可以轻松地从一台机器来进行部署。

快速执行

Go有着接近C的速度。与JITed(即时编译)语言(Java,JavaScript,Python等)不同,Go二进制文件不需要启动或预热时间,因为它们作为已编译和完全优化的本机代码提供。Go垃圾收集器仅以微秒的指令引入可忽略的暂停。在其快速的单核性能上面,Go使得利用所有的CPU内核更容易

小内存占用

像JVM,Python或Node这样的运行时不仅仅在运行时加载程序代码。它们还会加载大型且高度复杂的基础架构,以便在每次运行时编译和优化程序。这使得它们的启动时间变慢并导致它们使用大量(数百MB)的RAM。Go进程的开销较小,因为它们已经完全编译和优化,只需要运行。Go还以非常节省内存的方式存储数据。这在内存有限且昂贵的云环境中以及在开发期间非常重要,在开发期间我们希望在单个机器上快速启动整个堆栈,同时为其他软件留下内存。

小部署规模

Go二进制文件的大小非常简洁。Go应用程序的Docker镜像通常比用Java或Node编写的等效文件小10倍,因为它不需要包含编译器,JIT,并且需要更少的运行时基础结构。这在部署大型应用程序时很重要。想象一下,将一个简单的应用程序部署到100个生产服务器上 使用Node / JVM时,我们的docker仓库必须提供100个docker镜像@ 200 MB = 20 GB(总共)。这需要镜像仓库耗费一些时间来服务。想象一下,我们希望每天部署100次。使用Go服务时,Docker镜像仓库只需提供100个Docker镜像@ 20 MB = 2 GB。可以更快,更频繁地部署大型Go应用程序,从而允许重要更新更快地实现生产。

自包含部署

Go应用程序部署为包含所有依赖项的单个可执行文件。不需要安装特定版本的JVM,Node或Python运行时。不必将库下载到生产服务器上。不需要对运行Go二进制文件的机器进行任何更改。甚至不需要将Go二进制文件包装到Docker中来共享它们。您只需将Go二进制文件拖放到服务器上,无论该服务器上运行的是什么,它都会在那里运行。上述描述的唯一例外是使用net和os/user包时的动态链接glibc库时。

vendor依赖关系

Go故意避免使用第三方库的中央存储库。Go应用程序直接链接到相应的Git存储库,并将所有相关代码下载(vendor保存)到他们自己的代码库中。这有很多好处:

  • 我们可以在使用之前查看,分析和测试第三方代码。此代码与我们自己的代码一样,是我们应用程序的一部分,应符合相同的质量,安全性和可靠性标准。
  • 无需永久访问存储依赖项的各个位置。可以一次性的从任何地方(包括私人Git仓库)获取您的第三方库,并永久拥有它们。
  • 在checkout后编译代码库不需要进一步下载依赖项。
  • 如果互联网上某处的代码存储库突然提供不同的代码,也不会造成surprises。
  • 即使软件包存储库服务性能变慢或托管软件包不再存在,部署也不会中断。

兼容性保证

Go团队承诺,现有的程序将继续适用于新版本语言。这使得即使是大型项目也可以轻松升级到更新编译器的版本,并从新版本带来的许多性能和安全性改进中受益。同时,由于Go二进制文件包含了他们需要的所有依赖项,因此可以在同一服务器计算机上并行运行使用不同版本的Go编译器编译的二进制文件,而无需进行复杂的设置多个版本的运行时或虚拟化。

文档

在大型工程中,文档对于使软件易于访问和维护非常重要。与其他功能类似,Go中的文档简单实用:

  • 它嵌入在源代码中,因此两者可以同时维护。
  • 它不需要特殊的语法 – 文档只是普通的源代码注释。
  • 可运行的单元测试通常是最好的文档形式,所以Go允许你将它们嵌入到文档中
  • 所有文档实用程序都内置在工具链中,因此每个人都使用它们。
  • Go linter需要导出元素的文档,以防止“文档债务”的积累。

商业支持的开源

当商业实体在公开场合发展时,一些最流行和最全面设计的软件就会发生。这种设置结合了商业软件开发的优势 – 一致性和优化,使系统健壮,可靠,高效 – 具有开放式开发的优势,如来自许多行业的广泛支持,来自多个大型实体和许多用户的支持,以及长期支持,即使商业支持停止。Go就是这样开发的。

缺点

当然,Go并不完美,每种技术选择总是有利有弊。在进入Go之前,这里有一小部分需要考虑的方面。

未成熟

虽然Go的标准库在支持HTTP/2服务器推送等许多新概念方面处于行业领先地位,但与JVM生态系统中存在的相比,用于外部API的第三方Go库可能还不那么成熟。

即将到来的变化

Go团队知道几乎不可能改变现有的语言元素,因此只有在完全开发后才会添加新功能。在经历了10年稳定的故意阶段后,Go团队正在考虑对语言进行一系列更大的改进,作为Go 2.0之旅的一部分。

没有硬实时

虽然Go的垃圾收集器只引入了非常短的中断,但支持硬实时需要没有垃圾收集的技术,例如Rust。

结论

这篇博客文章给出了一些明智的背景知识,但往往没有那么明显的选择进入Go的设计,以及当他们的代码库和团队成数量级增长时,他们将如何从许多痛苦中拯救大型工程项目。总的来说,他们将Go定位为寻求Java之外的现代编程语言的大型开发项目的绝佳选择。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

写Go代码时遇到的那些问题[第2期]

第1期的“写Go代码时遇到的那些问题”一经发布后得到了很多Gopher的支持和赞赏,这也是我继续写下去的动力!不过这里依然要强调的是这一系列文章反映的是笔者在实践中对代码编写的认知以及代码的演化过程。这里的代码也许只是“中间阶段”,并不是什么最优的结果,我记录的只是对问题、对代码的一个思考历程。不过,十分欢迎交流与批评指正。

一、dep的日常操作

虽然dep在国内使用依然有init失败率较高(因为一些qiang外的第三方package)的坎儿,但我和主流Gopher社区和项目一样,义无反顾地选择在代码库中使用dep。本周dep刚刚发布了0.4.1版本,与之前版本最大的不同在于dep发布了其官网以及相对完整的文档(以替代原先在github项目主页上的简陋的、格式较low的FAQ),这也是dep继续走向成熟的一个标志。不过关于dep何时能merge到go tools链当中,目前还是未知数。不过dep会在相当长的一段时期继续以独立工具的形式存在,直到merge到Go tools中并被广泛接受。

包依赖管理工具在日常开发中并不需要太多的存在感,我们需要的这类工具特征是功能强大但接口“小”,对开发者体验好,不太需要太关心其运行原理,dep基本符合。dep日常操作最主要的三个命令:dep init、dep ensure和dep status。在《初窥dep》一文中,我曾重点说过dep init原理,这里就不重点说了,我们用一个例子来说说使用dep的日常workflow。

1、dep init empty project

我们可以对一个empty project或一个初具框架雏形的project进行init,这里init一个empty project,作为后续的示例基础:

➜  $GOPATH/src/depdemo $dep init -v
Getting direct dependencies...
Checked 1 directories for packages.
Found 0 direct dependencies.
Root project is "depdemo"
 0 transitively valid internal packages
 0 external packages imported from 0 projects
(0)   ✓ select (root)
  ✓ found solution with 0 packages from 0 projects

Solver wall times by segment:
  select-root: 68.406µs
        other:  9.806µs

  TOTAL: 78.212µs

➜  $GOPATH/src/depdemo $ls
Gopkg.lock    Gopkg.toml    vendor/

➜  $GOPATH/src/depdemo $dep status
PROJECT  CONSTRAINT  VERSION  REVISION  LATEST  PKGS USED

dep init有三个输出:Gopkg.lock、Gopkg.toml和vendor目录,其中Gopkg.toml(包含example,但注释掉了)和vendor都是空的,Gopkg.lock中仅包含了一些给gps使用的metadata:

➜  $GOPATH/src/depdemo git:(a337d5b) $cat Gopkg.lock
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.

[solve-meta]
  analyzer-name = "dep"
  analyzer-version = 1
  inputs-digest = "ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7"
  solver-name = "gps-cdcl"
  solver-version = 1

2、常规操作循环:for { 填代码 -> dep ensure }

接下来的常规操作就是我们要为project添加代码了。我们先来为工程添加一个main.go文件,源码如下:

// main.go
package main

import "fmt"

func main() {
    fmt.Println("depdemo")
}

这份代码的依赖只是std库的fmt,并没有使用第三方的依赖,因此当我们通过dep status查看当前状态、使用ensure去做同步时,发现dep并没有什么要做的:

➜  $GOPATH/src/depdemo $dep status
PROJECT  CONSTRAINT  VERSION  REVISION  LATEST  PKGS USED
➜  $GOPATH/src/depdemo $dep ensure -v
Gopkg.lock was already in sync with imports and Gopkg.toml

好吧。我们再来为main.go添点“有用”的内容:一段读取toml配置文件的代码。

//data.toml
id = "12345678abcdefgh"
name = "tonybai"
city = "shenyang"

// main.go
package main

import (
    "fmt"
    "log"

    "github.com/BurntSushi/toml"
)

type Person struct {
    ID   string
    Name string
    City string
}

func main() {
    p := Person{}
    if _, err := toml.DecodeFile("./data.toml", &p); err != nil {
        log.Fatal(err)
    }

    fmt.Println(p)
}

之后,再来执行dep status:

➜  $GOPATH/src/depdemo $dep status
Lock inputs-digest mismatch due to the following packages missing from the lock:

PROJECT                     MISSING PACKAGES
github.com/BurntSushi/toml  [github.com/BurntSushi/toml]

This happens when a new import is added. Run `dep ensure` to install the missing packages.
input-digest mismatch

我们看到dep status检测到项目出现”不同步”的情况(代码中引用的toml包在Gopkg.lock中没有),并建议使用dep ensure命令去做一次sync。

img{512x368}

我们来ensure一下(ensure的输入输出见上图):

$GOPATH/src/depdemo git:(master) $dep ensure -v
Root project is "depdemo"
 1 transitively valid internal packages
 1 external packages imported from 1 projects
(0)   ✓ select (root)

(1)    ? attempt github.com/BurntSushi/toml with 1 pkgs; 7 versions to try
(1)        try github.com/BurntSushi/toml@v0.3.0
(1)    ✓ select github.com/BurntSushi/toml@v0.3.0 w/1 pkgs
  ✓ found solution with 1 packages from 1 projects

Solver wall times by segment:
     b-source-exists: 15.821158205s
... ...
  b-deduce-proj-root:       5.453µs

  TOTAL: 16.176846089s

(1/1) Wrote github.com/BurntSushi/toml@v0.3.0

我们来看看项目中的文件都发生了哪些变化:

$git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   Gopkg.lock

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    vendor/

可以看到Gopkg.lock文件和vendor目录下发生了变化:

$git diff

diff --git a/Gopkg.lock b/Gopkg.lock
index bef2d00..c5ae854 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -1,9 +1,15 @@
 # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.

+[[projects]]
+  name = "github.com/BurntSushi/toml"
+  packages = ["."]
+  revision = "b26d9c308763d68093482582cea63d69be07a0f0"
+  version = "v0.3.0"
+
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7"
+  inputs-digest = "25c744eb70aefb94032db749509fd34b2fb6e7c6041e8b8c405f7e97d10bdb8d"
   solver-name = "gps-cdcl"
   solver-version = 1

$tree -L 2 vendor
vendor
└── github.com
    └── BurntSushi

可以看到Gopkg.lock中增加了toml包的依赖条目(版本v0.3.0),input-digest这个元数据字段的值也发生了变更;并且vendor目录下多了toml包的源码,至此项目又到达了“同步”状态。

3、添加约束

大多数情况下,我们到这里就算完成了dep work flow的一次cycle,但如果你需要为第三方包的版本加上一些约束条件,那么dep ensure -add就会派上用场,比如说:我们要使用toml包的v0.2.x版本,而不是v0.3.0版本,我们需要为github.com/BurntSushi/toml添加一条约束:

$dep ensure -v -add github.com/BurntSushi/toml@v0.2.0
Fetching sources...
(1/1) github.com/BurntSushi/toml@v0.2.0

Root project is "depdemo"
 1 transitively valid internal packages
 1 external packages imported from 1 projects
(0)   ✓ select (root)
(1)    ? attempt github.com/BurntSushi/toml with 1 pkgs; at least 1 versions to try
(1)        try github.com/BurntSushi/toml@v0.3.0
(2)    ✗   github.com/BurntSushi/toml@v0.3.0 not allowed by constraint ^0.2.0:
(2)        ^0.2.0 from (root)
(1)        try github.com/BurntSushi/toml@v0.2.0
(1)    ✓ select github.com/BurntSushi/toml@v0.2.0 w/1 pkgs
  ✓ found solution with 1 packages from 1 projects

Solver wall times by segment:
... ...

  TOTAL: 599.252392ms

(1/1) Wrote github.com/BurntSushi/toml@v0.2.0

add约束后,Gopkg.toml中增加了一条记录:

// Gopkg.toml
[[constraint]]
  name = "github.com/BurntSushi/toml"
  version = "0.2.0"

Gopkg.lock中的toml条目的版本回退为v0.2.0:

diff --git a/Gopkg.lock b/Gopkg.lock
index c5ae854..a557251 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -4,12 +4,12 @@
 [[projects]]
   name = "github.com/BurntSushi/toml"
   packages = ["."]
-  revision = "b26d9c308763d68093482582cea63d69be07a0f0"
-  version = "v0.3.0"
+  revision = "bbd5bb678321a0d6e58f1099321dfa73391c1b6f"
+  version = "v0.2.0"

 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "25c744eb70aefb94032db749509fd34b2fb6e7c6041e8b8c405f7e97d10bdb8d"
+  inputs-digest = "9fd144de0cc448be93418c927b5ce2a70e03ec7f260fa7e0867f970ff121c7d7"
   solver-name = "gps-cdcl"
   solver-version = 1

$dep status
PROJECT                     CONSTRAINT  VERSION  REVISION  LATEST  PKGS USED
github.com/BurntSushi/toml  ^0.2.0      v0.2.0   bbd5bb6   v0.2.0  1

vendor目录下的toml包源码也回退到v0.2.0的源码。关于约束规则的构成语法,可以参考dep文档

4、revendor/update vendor

使用vendor机制后,由于第三方依赖包修正bug或引入你需要的功能,revendor第三方依赖包版本或者叫update vendor会成为一个周期性的工作。比如:toml包做了一些bugfix,并发布了v0.2.1版本。在我的depdemo中,为了一并fix掉这些bug,我需要重新vendor toml包。之前我们加的constraint是满足升级到v0.2.1版本的,因此我们不需要重新设置constraints,我们只需要单独revendor toml即可,可以使用dep ensure -update 命令:

$dep ensure -v -update github.com/BurntSushi/toml
Root project is "depdemo"
 1 transitively valid internal packages
 1 external packages imported from 1 projects
(0)   ✓ select (root)
(1)    ? attempt github.com/BurntSushi/toml with 1 pkgs; 7 versions to try
(1)        try github.com/BurntSushi/toml@v0.3.0
(2)    ✗   github.com/BurntSushi/toml@v0.3.0 not allowed by constraint ^0.2.0:
(2)        ^0.2.0 from (root)
(1)        try github.com/BurntSushi/toml@v0.2.0
(1)    ✓ select github.com/BurntSushi/toml@v0.2.0 w/1 pkgs
  ✓ found solution with 1 packages from 1 projects

Solver wall times by segment:
  b-list-versions: 1m18.267880815s
  .... ...
  TOTAL: 1m57.118656393s

由于真实的toml并没有v0.2.1版本且没有v0.2.x版本,因此我们的dep ensure -update并没有真正获取到数据。vendor和Gopkg.lock都没有变化。

5、dep日常操作小结

下面这幅图包含了上述三个dep日常操作,可以直观地看出不同操作后,对项目带来的改变:

img{512x368}

“工欲善其事,必先利其器”,熟练的掌握dep的日常操作流程对提升开发效率大有裨益。

二、“超时等待退出”框架的一种实现

很多时候,我们在程序中都要启动多个goroutine协作完成应用的业务逻辑,比如:

func main() {
    go producer.Start()
    go consumer.Start()
    go watcher.Start()
    ... ...
}

启动容易停止难!当程序要退出时,最粗暴的方法就是不管三七二十一,main goroutine直接退出;优雅些的方式,也是*nix系统通常的作法是:通知一下各个Goroutine要退出了,然后等待一段时间后再真正退出。粗暴地直接退出的方式可能会导致业务数据的损坏、不完整或丢失。等待超时的方式虽然不能完全避免“损失”,但是它给了各个goroutine一个“挽救数据”的机会,可以尽可能地减少损失的程度。

但这些goroutine形态很可能不同,有些是server,有些可能是client worker或其manager,因此似乎很难用一种统一的框架全面管理他们的启动、运行和退出,于是我们缩窄“交互面”,我们只做“超时等待退出”。我们定义一个interface:

type GracefullyShutdowner interface {
    Shutdown(waitTimeout time.Duration) error
}

这样,凡是实现了该interface的类型均可在程序退出时得到退出的通知,并有机会做退出前的最后清理工作。这里还提供了一个类似http.HandlerFunc的类型ShutdownerFunc ,用于将普通function转化为实现了GracefullyShutdowner interface的类型实例:

type ShutdownerFunc func(time.Duration) error

func (f ShutdownerFunc) Shutdown(waitTimeout time.Duration) error {
    return f(waitTimeout)
}

1、并发退出

退出也至少有两种类型,一种是并发退出,这种退出方式下各个goroutine的退出先后次序对数据处理无影响;另外一种则是顺序退出,即各个goroutine之间的退出是必须按照一定次序进行的。我们先来说并发退出。上代码!

// shutdown.go
func ConcurrencyShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error {
    c := make(chan struct{})

    go func() {
        var wg sync.WaitGroup
        for _, g := range shutdowners {
            wg.Add(1)
            go func(shutdowner GracefullyShutdowner) {
                shutdowner.Shutdown(waitTimeout)
                wg.Done()
            }(g)
        }
        wg.Wait()
        c <- struct{}{}
    }()

    select {
    case <-c:
        return nil
    case <-time.After(waitTimeout):
        return errors.New("wait timeout")
    }
}

我们将各个GracefullyShutdowner接口的实现以一个变长参数的形式传入ConcurrencyShutdown函数。ConcurrencyShutdown函数实现也很简单,通过:

  • 为每个shutdowner启动一个goroutine实现并发退出,并将timeout参数传入shutdowner的Shutdown方法中;
  • sync.WaitGroup在外层等待每个goroutine的退出;
  • 通过select一个退出指示channel和time.After返回的timer channel来决定到底是正常退出还是超时退出。

该函数的具体使用方法可以参考:shutdown_test.go。

//shutdown_test.go
func shutdownMaker(processTm int) func(time.Duration) error {
    return func(time.Duration) error {
        time.Sleep(time.Second * time.Duration(processTm))
        return nil
    }
}

func TestConcurrencyShutdown(t *testing.T) {
    f1 := shutdownMaker(2)
    f2 := shutdownMaker(6)

    err := ConcurrencyShutdown(time.Duration(10)*time.Second, ShutdownerFunc(f1), ShutdownerFunc(f2))
    if err != nil {
        t.Errorf("want nil, actual: %s", err)
        return
    }

    err = ConcurrencyShutdown(time.Duration(4)*time.Second, ShutdownerFunc(f1), ShutdownerFunc(f2))
    if err == nil {
        t.Error("want timeout, actual nil")
        return
    }
}

2、串行退出

有了并发退出作为基础,串行退出也很简单了!

//shutdown.go
func SequentialShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error {
    start := time.Now()
    var left time.Duration

    for _, g := range shutdowners {
        elapsed := time.Since(start)
        left = waitTimeout - elapsed

        c := make(chan struct{})
        go func(shutdowner GracefullyShutdowner) {
            shutdowner.Shutdown(left)
            c <- struct{}{}
        }(g)

        select {
        case <-c:
            //continue
        case <-time.After(left):
            return errors.New("wait timeout")
        }
    }

    return nil
}

串行退出的一个问题是waitTimeout的确定,因为这个超时时间是所有goroutine的退出时间之和。在上述代码里,我把每次的lefttime传入下一个要执行的goroutine的Shutdown方法中,外部select也同样使用这个left作为timeout的值。对照ConcurrencyShutdown,SequentialShutdown更简单,这里就不详细说了。

3、小结

这是一个可用的、抛砖引玉式的实现,但还有很多改进空间,比如:可以考虑一下获取每个shutdowner.Shutdown后的返回值(error),留给大家自行考量吧。

三、Testcase的setUp和tearDown

Go语言自带testing框架,事实证明这是Go语言的一个巨大优势之一,Gopher们也非常喜欢这个testing包。但Testing这个事情比较复杂,有些场景还需要我们自己动脑筋在标准testing框架下实现需要的功能,比如:当测试代码需要访问外部数据库、Redis或连接远端server时。遇到这种情况,很多人想到了Mock,没错。Mock技术在一定程度上可以解决这些问题,但如果使用mock技术,业务代码就得为了test而去做一层抽象,提升了代码理解的难度,在有些时候这还真不如直接访问真实的外部环境。

这里先不讨论这两种方式的好坏优劣,这里仅讨论如果在testing中访问真实环境我们该如何测试。在经典单元测试框架中,我们经常能看到setUp和tearDown两个方法,它们分别用于在testcase执行之前初始化testcase的执行环境以及在testcase执行后清理执行环境,以保证每两个testcase之间都是独立的、互不干扰的。在真实环境下进行测试,我们也可以利用setUp和tearDown来为每个testcase初始化和清理case依赖的真实环境。

setUp和tearDown也是有级别的,有全局级、testsuite级以及testcase级。在Go中,在标准testing框架下,我们接触到的是全局级和testcase级别。Go中对全局级的setUp和tearDown的支持还要追溯到Go 1.4Go 1.4引入了TestMain方法,支持在诸多testcase执行之前为测试代码添加自定义setUp,以及在testing执行之后进行tearDown操作,例如:

func TestMain(m *testing.M) {
    err := setup()
    if err != nil {
        fmt.Println(err)
        os.Exit(-1)
    }

    r := m.Run()
    teardown()

    os.Exit(r)
}

但在testcase级别,Go testing包并没有提供方法上的支持。在2017年的GopherCon大会上,Hashicorp的创始人Mitchell Hashimoto做了题为:“Advanced Testing in Go”的主题演讲,这份资料里提出了一种较为优雅的为testcase进行setUp和teawDown的方法:

//setup-teardown-demo/foo_test.go
package foo_test

import (
    "fmt"
    "testing"
)

func setUp(t *testing.T, args ...interface{}) func() {
    fmt.Println("testcase setUp")
    // use t and args

    return func() {
        // use t
        // use args
        fmt.Println("testcase tearDown")
    }
}

func TestXXX(t *testing.T) {
    defer setUp(t)()
    fmt.Println("invoke testXXX")
}

这个方案充分利用了函数这个first-class type以及闭包的作用,每个Testcase可以定制自己的setUp和tearDown,也可以使用通用的setUp和tearDown,执行的效果如下:

$go test -v .
=== RUN   TestXXX
testcase setUp
invoke testXXX
testcase tearDown
--- PASS: TestXXX (0.00s)
PASS
ok      github.com/bigwhite/experiments/writing-go-code-issues/2nd-issue/setup-teardown-demo    0.010s

四、错误处理

本来想码一些关于Go错误处理的文字,但发现自己在2015年就写过一篇旧文《Go语言错误处理》,对Go错误处理的方方面面总结的很全面了。即便到今天也不过时,这当然也得益于Go1兼容规范的存在。因此有兴趣于此的朋友们,请移步到《Go语言错误处理》这篇文章吧。

注:本文所涉及的示例代码,请到这里下载。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

微信赞赏:
img{512x368}

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats