TB一周萃选[第7期]

本文是首发于个人微信公众号的文章“TB一周萃选[第7期]”的归档。

img{512x368}

我看过小马哥(哈维尔·马斯切拉诺)踢球,
你看过小马哥踢球,
他看过小马哥踢球。
我们看过小马哥踢球,
你们看过小马哥踢球,
他们看过小马哥踢球!

— 改编自网络资料

都说三九天是一年中最冷的一段时间,但我们这里稍有偏差,就个人赶脚:四九、五九才是我们这里温度的最低点。这一周的感受用一句东北话来说就是嘎嘎冷!体感温度近零下30摄氏度:一开车门,好不容易凝聚在身体周遭的“热量”瞬间散失,似乎已经有10多年没有感觉到如此持续的寒冷了。

但巴萨新闻中的一则消息却让作为阿根廷和巴萨双重球迷的我感到了一丝温暖。北京时间本周五凌晨,在巴萨主场与西班牙人队的国王杯四分之一决赛前,梦三主力、巴萨后防中坚小马哥携着自己的家人在巴萨队友的列队欢迎下、在诺坎普主场球迷山呼海啸般的欢呼声中走入诺坎普,和大家做着最后的告别。对于一名职业球员来说,这已经算是在俱乐部层面能得到的最高荣誉了。

虽说梅球王是我的最爱,但小马哥也是我十分喜欢和尊敬的一名足球运动员,在他的身上你几乎能够看到一名职业运动员所有的“正能量”标签:高超的专业能力、职业、自律、低调、坚毅、领导力、热爱足球、热爱家庭、没有绯闻等。对于小马哥这样的功勋球员,以“不只是一家俱乐部(Mes que un club)”为使命的巴萨俱乐部也做出了最大的让步,为小马哥设定了较低的转会费,让他可以按照自己的意愿成功转会到中超的华夏幸福。

小马哥将自己职业生涯中最好的七年奉献给了巴萨,对巴萨的贡献可谓是居功至伟!看看小马哥为巴萨赢得的荣誉吧。

img{512x368}

感谢小马哥,祝福小马哥在后续的职业生涯中一切顺利!在中国生活的快乐!

一、一周文章精粹

1. Hello, 中国!

由于“众所周知”的原因,大陆地区的Gopher们在访问Go官方站点时十分困难。这一定程度上影响了Go在大陆地区的推广。但Go语言在大陆地区的发展势头让Go team看到了建立大陆地区mirror站的必要性。就在这一周,中国的Gopher们迎来了一个Go官方的好消息,那就是Go语言大陆地区官方网站上线了。网站的地址是https://golang.google.cn,这个网站目前就是Go官方站的mirror,很多深层的链接可能依然指向源站,不过迈出第一步总是好的。

文章链接:“Hello,中国!”

2. 尚未修复的逃逸分析缺陷(Escape-Analysis Flaws)

William Kennedy是著名的Go语言培训师,也是《Go in action》这本书的作者之一,他在Ardan Labs网站上撰写了许多篇关于Go语言的学习资料。其中最新的一篇“Escape Analysic Flaws”探讨了当前Go compiler(截至到Go 1.9)中依然存在的逃逸分析的缺陷,包括:

  • Indirect Assignment
  • Indirect Call
  • Slice and Map Assignments
  • Interfaces
  • Unknown

Go实际编码过程中减少在heap上的内存分配是提升性能,减少cost的好方法,通过William的分析,我们也期望能做到尽量避免逃逸的情况,但有些时候做起来很难。因此,让Go compiler自身变得更聪明才是终极解决方法。

文章链接:“Escape-Analysis Flaws”

3. Github用户使用的编程语言排名

国外友人Ben Fredericksont通过对2011以来github的public event数据的分析,得出了关于github上编程语言的使用变化趋势,包括:top ten活跃语言、主流语言的活跃程度变化趋势、2018值得学习的几个热门新语言、几门趋势下降很快的语言、科学计算语言的变化趋势、函数式语言的变化趋势等。

img{512x368}
图:2018值得学习的几个热门新语言

文章链接:“Ranking Programming Languages by GitHub Users”

4. Nonblocking I/O指南

Go语言的默认的网络I/O编程模型是阻塞I/O,这可以大幅降低应用开发者在处理网络I/O时的心智负担。但这也仅限于“用户层面”,研究过Go runtime调度的gopher都知道,在runtime内部,关于网络I/O的调度实际上是Nonblocking的。imgix的工程师Cindy Sridharan曾全面细致总结了对Nonblocking I/O的技术要点的理解,这里推荐给大家。

img{512x368}

文章链接:“Nonblocking I/O”

5. 预测:2018年的最佳Linux发行版

Linux内核已经成为这个星球上使用最为广泛的操作系统内核了,无论是云服务器,还是桌面机,从移动终端到Iot设备,现代人身边10米范围内,一般总能找出一台运行着Linux内核的设备。而对于用户而言,看到的更多是基于Linux内核的各种发行版,比如:Ubuntu、CentOS等。年初JACK WALLEN在linux.com博客上撰文预测了2018年各个领域的最佳Linux发行版,包括从sysadmin、桌面版、server版、便携版、iot版等多个方面。这些预测基于distrowatch.com上各个发行版的人气排名。

文章链接:“best linux distributions for 2018”

6. 如何使用Go语言创建基于AWS Lambda的serverless应用

AWS Lambda宣布支持Go不久,各路关于如何使用Go在AWS Lambda创建serverless应用的资料便接踵踏来。这里推荐的就是其中的一篇。对于想使用Go在AWS Lambda上“尝鲜”的Gopher们,这是个不错的入门文章。

img{512x368}

文章链接:“Serverless Golang API with AWS Lambda”

7. JavaScript框架终极指南

JavaScript这门语言虽然“颜值”不那么高,但这并不妨碍它抱上浏览器这一“大腿”,并还进军了服务端市场。在这一过程中,JavaScript领域诞生了诸多Framework,最出名的莫过于三巨头:AngularReactVue.js这三个框架了。除此之外,还有太多我甚至没有听过名字的框架。这里推荐的“JavaScript框架终极指南”一文就是对JavaScript目前的主流框架的状态、优劣势进行详细总结说明的一篇文章,希望能帮助你挑选出最适合你的Js框架。

img{512x368}

文章链接:“The Ultimate Guide to JavaScript Frameworks”

二、一周资料分享

1. ROSCon 2017资料

ROS作为世界上应用最为广泛、最具影响力的开源机器人操作系统,它从2012年开始举办的ROSCon大会就备受关注,2017年ROSCon大会在加拿大温哥华举行。在人工智能、智能驾驶如此“热”的今天,ROS作为很多智能驾驶平台(比如百度的ApollotierIVautoware等)的底层支撑组件自然吸引了自全世界范围内的学者和工程师的眼球和参与。这次大会的topic是干货满满,由于是ROS2发布正式版前的最后一次大会,因此涉及ROS2的topics十分多,算是为ROS2正式登场预热(注:ROS2在2017.12.10正式发布,代号:Ardent Apalone)。

img{512x368}

资料分享链接:“ROSCon 2017资料”

三、一周工具推荐

1. carbon:一款源码图片创建和分享的工具

在技术文章写作中,我们会有大量的代码截图的需求,但限于客观原因,截图的质量和风格难于把控。Carbon这个工具就是来帮助解决这个问题的。Carbon是一个在线服务,支持通过将源码文件拖拽到生成框中自动生成代码图片。Carbon支持几乎所有主流语言,并可以自动识别,并且Carbon支持多种风格的代码高亮样式,比如:Monokai、Solarized等。

img{512x368}
图:Carbon主页

img{512x368}
图:Carbon生成的Go源码图片

推荐工具链接:Carbon

四、一周图书推荐

1.《Hello World! Second Edition – Computer Programming for Kids and Other Beginners》

都说00后是互联网时代的原住民,那么伴着这轮AI热,我们是否可以大胆地说2020后或2025后是AI时代的原住民呢。这让我仿佛看到了“超能陆战队”中男主小宏所使用的IT装备和掌握的编程技能。也许在未来10年后,编程就会像数学、语文一样成为在AI时代的基本技能。而这一切都要从娃娃抓起,从编程基础抓起。Sande父子合作编写的这本《Hello World》图文并茂地将孩子带入二进制的程序世界,孩子将在轻松惬意的氛围中学习基础的编程概念:如内存、循环、输入和输出、数据结构和图形用户界面等。对于如今智力水平普遍较高的孩子们来说,这些内容就像小游戏般容易掌握。书中使用的教学语言是Python,别忘了目前的Python可是AI时代的top3语言,并是AI第一语言的强有力的竞争者。

很多人说:当前儿童编程的第一语言是MIT的Scratch,我不能否认这一点,Scratch就是为Kids们所创造的,它是MIT继Seymour Papert教授在创建LOGO语言、探索儿童编程教育后的又一杰作。全图形化的编程教学让孩子们很是喜欢。但我个人觉得如果能结合一些真实代码,尤其是对于中高年级的学生来说,将是大有裨益的。

作为Gopher,我一直在想足够简洁的Go语言也是可以作为儿童编程教学语言的,希望能早日出现一门以Go语言为第一教学语言的儿童编程图书。

img{512x368}

图书链接:
《父与子的编程之旅 – 与小卡特一起学Python》
《Hello World! Second Edition – Computer Programming for Kids and Other Beginners》


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

我的联系方式:

微博:http://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 Go语言编程指南
商务合作请联系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