标签 编程 下的文章

让reviewdog支持gitlab-push-commit,守住代码质量下限

本文永久链接 – https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor

一. 代码质量保证的手段

从世界上首款计算机高级程序设计语言Fortran自上世纪50年代诞生以来,编程这个行当已经走过了近70年。虽然年头已不少,但不可否认的一点是:软件生产依然无法像硬件那样标准化,同一个小功能,N个程序员的有N种实现方法

那么如何保证生产出的软件的质量符合我们的要求呢?不同领域的程序员都在进行着努力,比如:做编译器的让编译器更加严格,努力将内存安全问题彻底消除(如Rust);做工具链的为程序员提供了内置于语言的各种单测、集成测试、接口测试、fuzzing test等工具(如Go工具链),让程序员可以更容易地对自己所写的代码进行全方位的测试,以期找出更多的代码中的潜在问题…

当然,还有一种主观的代码质量保证方法目前依旧是主流,它就是是同行的代码评审(code review, cr)

代码评审的方法主要有两种,一种是大家坐到一个会议室中,对某个人的某段代码“发表大论”;另外一种则是利用像gerrit这样的工具,在线对其他人的某次提交的代码或某PR的代码进行“评头论足”。

不过无论哪种,最初的时候大家都会细无巨细地从语法层面看到代码结构设计,再到业务逻辑层面,但这样做的弊端也是很显而易见,那就是效率低下,不聚焦(focus)

于是人们想到了:能否利用工具来尽可能地发现语法层面的问题,这样代码评审时,人类专家便可以聚焦代码结构设计与业务逻辑层面的问题,分工明确后,效率自然提升(如下图):

注:目前绝大多数工具链仅能自动帮助程序员解决语法层面的问题。将来,随着工具的日益强大,工具可以不断升级关注层次,逐渐进化到具备发现代码结构设计问题,甚至可以发现业务层面逻辑问题的能力。

于是就有了reviewdog这样的可以调用各种linter工具对代码进行自动扫描并将问题以comment的形式自动提交的代码仓库的工具。

到这里很多朋友会问,即便让工具来关注语法层面的问题,为何要用reviewdog这样的工具,git的pre-commit hook、git server hooks、利用Make等工具做开发阶段检查等手段也能检查代码中的语法问题,它们不再香了吗?

下面简单看看这些方法的“问题”(我们假设大家都已经在使用git作为代码版本管理工具):

  • git pre-commit-hook

git pre-commit hook是一个客户端的git hook,它是放在开发人员本地代码copy中的.git/hooks目录下的钩子,当开发人员在本地执行git commit时会被唤起执行。pre-commot hook的问题就在于我们没法在中心代码仓库对pre-commit hook的脚本内容做统一管理和维护。这个更适合开发人员根据自己的喜好、代码素养在自己的开发环境下部署。

此外,有些代码并不一定是在开发者自己的开发机上提交的,换环境后,pre-commit hook就不在生效。

  • 利用Make等工具做本地检查

利用make工具,我们可以在本地build代码之前对代码做lint等各种静态检查,但和pre-commit-hook一样,虽然Makefile可以提交代码仓库,但真正用于检查代码的工具依旧是在开发人员本地,难于对工具版本,设定的检查规则进行统一管理维护,可能导致不同开发人员环境有不一致的情况。另外同样的情况,有些代码并不一定是在开发者自己的开发机上提交的,换环境后,Make工具依赖的代码检查工具可能并不存在,检查环节就无法有效实施。

  • git server hooks

git支持server hooksgitlab自12.8版本也开始支持server hooks(替换之前的custom hooks)。

Git server支持以下钩子:

  • pre-receive
  • post-receive
  • update

我倒是没有深研究过这些server hooks是否能满足我们的功能要求,但就git server hooks的部署特点就决定了,它不适合,因为它要在gitlab的server上执行,这就意味着我们需要的所有静态代码检查工具都要部署和配置在与gitlab server同一个环境中,这耦合性太强,根本不便于我们对这些静态代码检查工具的管理与日常维护。

而像reviewdog这样的工具将与ci工具(比如gitlab-ci)集成,运行在slave/worker/runner的机器上,而这些机器上的环境便很容易统一的定制与管理。

好了,下面进入reviewdog时间!

注:我们以代码仓库为gitlab为例,我曾做过小调查,目前企业内部基本都在使用gitlab搭建私有git仓库,除了那些自实现code仓库平台的大厂。

二. reviewdog是什么

reviewdog是一个什么样的工具呢?我们来看看下面这幅示意图:

我们看到,这是一幅基于gitlab的ci执行流程图,在这个流程中,reviewdog运行在gitlab-runner节点,也就是负责真正执行ci job的节点上。每当开发人员执行一次git push,将commit同步到代码仓库,一次ci job将被触发,在承载该ci job的gitlab-runner节点上,reviewdog被唤起,它做了三件事:

  • 调用静态代码检查工具对最新pull下来的代码进行检查;
  • 将代码检查结果(第几行有问题)与commit diff的结果进行比对,得到交集(即commit diff中变更(add和update)的代码行与代码检查结果的行一致的,放入交集中);
  • 将交集中代码检查结果信息以gitlab commit comment的形式post到gitlab仓库中

这样开发人员就可以通过commit页面看到这些comments,并应对这些comment,必要情况下,会修复这些问题。

我们看到reviewdog和其他工具相比,最大的不同就是可以找出commit diff与lint结果中的交集,并与代码仓库交互,将这些交集中的结果以comments的形式放入commit页面,就像同行代码评审时,同行直接在你的commit页面添加comment一样

然而当前版本的reviewdog还不支持直接在gitlab-push-commit上做检查与提交comment,可能是这样的场景较为少见,因为目前开源项目更多采用基于pr(pull request)的工作流,所以reviewdog内置了诸如github-pr-check、github-pr-review、gitlab-mr-commit等工作流的代码review。而像我们使用的基于gitlab-push-commit可能并不多见(当然我们内部使用这种也是有特定上下文的)。

那么如何让reviewdog支持gitlab-push-commit,即对push动作中的commit进行静态代码检查并将结果以comment的形式放入commit页面呢?我们只能fork reviewdog项目,并在fork后的项目中自行添加对gitlab-push-commit模式的支持。

三. 改造reviewdog以支持gitlab-push-commit模式

reviewdog就是一个命令行工具,通常就是一次性执行,因此它的代码结构较为清晰。我们可以简单围绕它支持的几种reporter模式来搞清楚如何增加对gitlab-push-commit模式的支持。

这里说明一下gitlab-push-commit模式的含义,首先该模式适用于开发人员通过git push推送代码到gitlab时触发的ci job。在该ci job中,reviewdog会运行配置的静态代码分析工具(比如golangci-lint等)对最新的代码进行扫描,并得到问题集合;然后获取最新的commit的sha值(CI_COMMIT_SHA)以及push之前的latest commit的sha值(CI_COMMIT_BEFORE_SHA),并比较这两个版本间的diff。最后通过文件名与行号将问题集合与diff集合中的“交集”找出来,并将结果以comment形式通过gitlab client api提交到的此次push的最新的那个commit的页面。

目前该模式尚存在一个“瑕疵”,那就是如果一个push中有多个commit,那么gitlab-push-commit模式不会针对每个commit做diff和comment,而只是会用push中的latest commit与push之前的最新commit做比较。

定义清除gitlab-push-commit模式含义后,我们就可以“照葫芦画瓢”的为reviewdog增加该模式的支持了!

在main.go中,我们主要是在run函数中增加一个reporter case分支:

// https://github.com/bigwhite/reviewdog/blob/master/cmd/reviewdog/main.go
func run(r io.Reader, w io.Writer, opt *option) error {
... ...

case "gitlab-push-commit":
    build, cli, err := gitlabBuildWithClient(opt.reporter)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] gitlabBuildWithClient ok\n")

    gc, err := gitlabservice.NewGitLabPushCommitsCommenter(cli, build.Owner, build.Repo, build.SHA)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\n")

    cs = reviewdog.MultiCommentService(gc, cs)
    ds, err = gitlabservice.NewGitLabPushCommitsDiff(cli, build.Owner, build.Repo, build.SHA, build.BeforeSHA)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\n")
... ...

}

在这个case中,我们主要是为后面的project.Run或reviewdog.Run方法准备gitlab client对象、PushCommitsCommenter对象(位于service/gitlab/gitlab_push_commits.go中)、PushCommitsDiff对象(位于service/gitlab/gitlab_push_commits_diff.go中)等。

gitlab_push_commits.go和gitlab_push_commits_diff.go是新增的两个go源文件,也是参考了同目录下的gitlab_mr_commit.go和gitlab_mr_diff.go改写而成的。具体代码这里就不列出来了,大家有兴趣可以自行阅读。

四. 部署gitlab-runner验证新版reviewdog

下面我们就来验证一下上述改造后的reviewdog。

1. 安装gitlab-runner

我们先在gitlab上建立一个实验项目,然后为该项目配置ci。如果你的gitlab还没有注册gitlab-runner,可以按下面步骤安装和注册runner节点(可以在顶层group下面建立,这样runner可以在group内共享:settings => CI/CD => Runners => Show runner installation instructions 有部署runner的详细命令说明):

//假设我们有一个ubuntu 20.04的主机,我们可以按下面命令安装和注册一个gitlab-runner:

sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# Give it permissions to execute
sudo chmod +x /usr/local/bin/gitlab-runner

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

# 注册该runner
sudo gitlab-runner register --url http://{gitlab-server-ip-addr}/ --registration-token {registration token}

上面命令会在/etc/gitlab-runner下面建立一个runner自用配置文件:config.toml:

//  /etc/gitlab-runner/config.toml

concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "runner for ard group"
  url = "http://gitlab_ip_addr/"
  id = 1
  token = "{registration token}"
  token_obtained_at = 2022-09-01T11:03:43Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "shell"
  shell = "bash"
  environment = ["PATH=/home/tonybai/.bin/go1.18/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"]
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]

这里我选择了shell executor,即基于主机shell执行ci job中的命令。runners下的environment可以设置shell的环境变量,这里的设置将覆盖对应账号(比如gitlab-runner)下的环境变量值。

gitlab-runner部署成功后,我们在group的runners下面便可以看到下面的available runners:

注:在创建runner时,我为该runner设置了两个tag:ard和ci。

注:确保runner执行的命令在主机的PATH下面可以找到。

2. 创建personal access token

reviewdog需要通过gitlab client API访问gitlab仓库获取信息并提交comments,这就需要我们为runner执行的命令提供access token。

gitlab有多种access token,比如:personal access token、project access token等。我们创建personal access token,我也测试过project access token,使用project access token可以成功提交comment,但是notify mail十有八九无法发送出来。

access token要保存好,因为它只显示一次。

我们将personal access token配置到实验项目的variable中(Settings => CI/CD => variables),variable的key为REVIEWDOG_GITLAB_API_TOKEN,值为刚刚创建的token。

后续每次CI job执行,该variable会作为预定义的环境变量对job生效。我们的reviewdog便可以使用该token访问gitlab。

3. 配置实验项目的ci pipeline

我们可以通过代码的形式配置实验项目的ci pipeline,我们在项目根目录下建立.gitlab-ci.yml文件,其内容如下:

// .gitlab-ci.yml

build-job:
  tags:
      - ard
  stage: build
  script:
    - export CI_REPO_OWNER=ard/incubators
    - export CI_REPO_NAME=learn-gitlab
    - reviewdog -reporter=gitlab-push-commit
  only:
    - master
    - pushes

.gitlab-ci.yml的具体字段含义可以参考gitlab文档。在这个配置中,值得注意的有几点:

  • 使用tags关联runner(这里用ard这个tag);
  • script部分是job具体执行的命令列表,这里先设置CI_REPO_OWNER和CI_REPO_NAME两个环境变量,供reviewdog使用;然后执行reviewdog;
  • only部分描述仅针对master分支的push事件触发ci job。

4. 配置.reviewdog.yml

最后,我们来配置一下适合实验项目的reviewdog的配置文件。我们同样在项目根目录下建立.reviewdog.yml文件,其内容如下:

runner:
  golangci:
    cmd: golangci-lint run --max-same-issues=0 --out-format=line-number ./...
    errorformat:
      - '%E%f:%l:%c: %m'
      - '%E%f:%l: %m'
      - '%C%.%#'
    level: warning

在这里我们看到,我们使用golangci-lint这个静态检查工具对实验项目的代码进行检查。这里的–max-same-issues=0的含义是不限制相同错误的数量。至于.reviewdog.yml的具体格式,reviewdog项目自身的.reviewdog.yml很具参考价值,大家需要时可以仔细研究。

5. 推送代码并验证reviewdog的执行结果

我们可以故意在代码中写下有问题的一些代码,这些问题要保证可以被golangci-lint工具扫描出来,比如:

package main

type Foo struct {
    A int
    B string
    C bool
}

func Demo1() error {
    return nil
}

func Demo2() error {
    return nil
}

func Demo3() error {
    return nil
}

func main() {
    f := &Foo{1, "tony", false}
    _ = f
    Demo2()
    Demo1()
    Demo3()
}

这里并没有对Demo函数调用进行错误处理,golangci-lint中的errcheck可以检测出这个问题。提交并push这些代码到仓库,稍等片刻,我们便可收到notify mail,打开commit页面,便会看到下面这样的commit comments:

看到这样的结果,说明reviewdog按预期工作了!

五. 小结

本文介绍了如何基于reviewdog对push提交的commit进行静态代码检查并像一个“同行”一样在commit中提交评论的方法。

这样做的目的就是希望通过工具提升代码评审的效率,同时也守住代码质量的下限。

就像本文开始所说的那样,随着检查工具能力的增强,这样的基于reviewdog自动检查代码的方案在保证代码质量方面还可以继续提升。

Go开源了go/ast等工具链,有能力的童鞋可以基于go/ast自行开发具有“特定目的”的检查工具并集成到reviewdog中,这将使得检查更有针对性和有效性。

本文涉及源码在这里下载 – https://github.com/bigwhite/reviewdog/


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

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

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

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

Golang Channel用法简编

在进入正式内容前,我这里先顺便转发一则消息,那就是Golang 1.3.2已经正式发布了。国内的golangtc已经镜像了golang.org的安装包下载页面,国内go程序员与爱好者们可以到"Golang中 国",即golangtc.com去下载go 1.3.2版本。

Go这门语言也许你还不甚了解,甚至是完全不知道,这也有情可原,毕竟Go在TIOBE编程语言排行榜上位列30开外。但近期使用Golang 实现的一杀手级应用 Docker你却不该不知道。docker目前火得是一塌糊涂啊。你去国内外各大技术站点用眼轻瞥一下,如 果没有涉及到“docker”字样新闻的站点建 议你以后就不要再去访问了^_^。Docker是啥、怎么用以及基础实践可以参加国内一位仁兄的经验之作:《 Docker – 从入门到实践》。

据我了解,目前国内试水Go语言开发后台系统的大公司与初创公司日益增多,比如七牛、京东、小米,盛大,金山,东软,搜狗等,在这里我们可以看到一些公司的Go语言应用列表,并且目前这个列表似乎依旧在丰富中。国内Go语言的推广与布道也再稳步推进中,不过目前来看多以Go入 门与基础为主题,Go idioms、tips或Best Practice的Share并不多见,想必国内的先行者、布道师们还在韬光养晦,积攒经验,等到时机来临再厚积薄发。另外国内似乎还没有一个针对Go的 布道平台,比如Golang技术大会之类的的平台。

在国外,虽然Go也刚刚起步,但在Golang share的广度和深度方面显然更进一步。Go的国际会议目前还不多,除了Golang老东家Google在自己的各种大会上留给Golang展示自己的 机会外,由 Gopher Academy 发起的GopherCon 会议也于今年第一次举行,并放出诸多高质量资料,在这里可以下载。欧洲的Go语言大会.dotgo也即将开幕,估计后续这两个大会将撑起Golang技术分享 的旗帜。

言归正传,这里要写的东西并非原创,自己的Go仅仅算是入门级别,工程经验、Best Practice等还谈不上有多少,因此这里主要是针对GopherCon2014上的“舶来品”的学习心得。来自CloudFlare的工程师John Graham-Cumming谈了关于 Channel的实践经验,这里针对其分享的内容,记录一些学习体会和理解,并结合一些外延知识,也可以算是一种学习笔记吧,仅供参考。

一、Golang并发基础理论

Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样,多数Golang程序员或爱好者仅仅停留在“知道”这一层次,理解CSP理论的并不多,毕竟多数程序员是搞工程 的。不过要想系统学习CSP的人可以从这里下载到CSP论文的最新版本。

维基百科中概要罗列了CSP模型与另外一种并发模型Actor模型的区别:

Actor模型广义上讲与CSP模型很相似。但两种模型就提供的原语而言,又有一些根本上的不同之处:
    – CSP模型处理过程是匿名的,而Actor模型中的Actor则具有身份标识。
    – CSP模型的消息传递在收发消息进程间包含了一个交会点,即发送方只能在接收方准备好接收消息时才能发送消息。相反,actor模型中的消息传递是异步 的,即消息的发送和接收无需在同一时间进行,发送方可以在接收方准备好接收消息前将消息发送出去。这两种方案可以认为是彼此对偶的。在某种意义下,基于交 会点的系统可以通过构造带缓冲的通信的方式来模拟异步消息系统。而异步系统可以通过构造带消息/应答协议的方式来同步发送方和接收方来模拟交会点似的通信 方式。
    – CSP使用显式的Channel用于消息传递,而Actor模型则将消息发送给命名的目的Actor。这两种方法可以被认为是对偶的。某种意义下,进程可 以从一个实际上拥有身份标识的channel接收消息,而通过将actors构造成类Channel的行为模式也可以打破actors之间的名字耦合。

二、Go Channel基本操作语法

Go Channel的基本操作语法如下:

c := make(chan bool) //创建一个无缓冲的bool型Channel

c <- x        //向一个Channel发送一个值
<- c          //从一个Channel中接收一个值
x = <- c      //从Channel c接收一个值并将其存储到x中
x, ok = <- c  //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

不带缓冲的Channel兼具通信和同步两种特性,颇受青睐。

三、Channel用作信号(Signal)的场景

1、等待一个事件(Event)

等待一个事件,有时候通过close一个Channel就足够了。例如:

//testwaitevent1.go
package main

import "fmt"

func main() {
        fmt.Println("Begin doing something!")
        c := make(chan bool)
        go func() {
                fmt.Println("Doing something…")
                close(c)
        }()
        <-c
        fmt.Println("Done!")
}

这里main goroutine通过"<-c"来等待sub goroutine中的“完成事件”,sub goroutine通过close channel促发这一事件。当然也可以通过向Channel写入一个bool值的方式来作为事件通知。main goroutine在channel c上没有任何数据可读的情况下会阻塞等待。

关于输出结果:

根据《Go memory model》中关于close channel与recv from channel的order的定义:The closing of a channel happens before a receive that returns a zero value because the channel is closed.

我们可以很容易判断出上面程序的输出结果:

Begin doing something!
Doing something…
Done!

如果将close(c)换成c<-true,则根据《Go memory model》中的定义:A receive from an unbuffered channel happens before the send on that channel completes.
"<-c"要先于"c<-true"完成,但也不影响日志的输出顺序,输出结果仍为上面三行。

2、协同多个Goroutines

同上,close channel还可以用于协同多个Goroutines,比如下面这个例子,我们创建了100个Worker Goroutine,这些Goroutine在被创建出来后都阻塞在"<-start"上,直到我们在main goroutine中给出开工的信号:"close(start)",这些goroutines才开始真正的并发运行起来。

//testwaitevent2.go
package main

import "fmt"

func worker(start chan bool, index int) {
        <-start
        fmt.Println("This is Worker:", index)
}

func main() {
        start := make(chan bool)
        for i := 1; i <= 100; i++ {
                go worker(start, i)
        }
        close(start)
        select {} //deadlock we expected
}

3、Select

【select的基本操作】
select是Go语言特有的操作,使用select我们可以同时在多个channel上进行发送/接收操作。下面是select的基本操作。

select {
case x := <- somechan:
    // … 使用x进行一些操作

case y, ok := <- someOtherchan:
    // … 使用y进行一些操作,
    //
检查ok值判断someOtherchan是否已经关闭

case outputChan <- z:
    // … z值被成功发送到Channel上时

default:
    // … 上面case均无法通信时,执行此分支
}

【惯用法:for/select】

我们在使用select时很少只是对其进行一次evaluation,我们常常将其与for {}结合在一起使用,并选择适当时机从for{}中退出。

for {
        select {
        case x := <- somechan:
            // … 使用x进行一些操作

        case y, ok := <- someOtherchan:
            // … 使用y进行一些操作,
            // 检查ok值判断someOtherchan是否已经关闭

        case outputChan <- z:
            // … z值被成功发送到Channel上时

        default:
            // … 上面case均无法通信时,执行此分支
        }
}

【终结workers】

下面是一个常见的终结sub worker goroutines的方法,每个worker goroutine通过select监视一个die channel来及时获取main goroutine的退出通知。

//testterminateworker1.go
package main

import (
    "fmt"
    "time"
)

func worker(die chan bool, index int) {
    fmt.Println("Begin: This is Worker:", index)
    for {
        select {
        //case xx:
            //做事的分支
        case <-die:
            fmt.Println("Done: This is Worker:", index)
            return
        }
    }
}

func main() {
    die := make(chan bool)

    for i := 1; i <= 100; i++ {
        go worker(die, i)
    }

    time.Sleep(time.Second * 5)
    close(die)
    select {}
//deadlock we expected
}

【终结验证】

有时候终结一个worker后,main goroutine想确认worker routine是否真正退出了,可采用下面这种方法:

//testterminateworker2.go
package main

import (
    "fmt"
    //"time"
)

func worker(die chan bool) {
    fmt.Println("Begin: This is Worker")
    for {
        select {
        //case xx:
        //做事的分支
        case <-die:
            fmt.Println("Done: This is Worker")
            die <- true
            return
        }
    }
}

func main() {
    die := make(chan bool)

    go worker(die)

    die <- true
    <-die
    fmt.Println("Worker goroutine has been terminated")
}

【关闭的Channel永远不会阻塞】

下面演示在一个已经关闭了的channel上读写的结果:

//testoperateonclosedchannel.go
package main

import "fmt"

func main() {
        cb := make(chan bool)
        close(cb)
        x := <-cb
        fmt.Printf("%#v\n", x)

        x, ok := <-cb
        fmt.Printf("%#v %#v\n", x, ok)

        ci := make(chan int)
        close(ci)
        y := <-ci
        fmt.Printf("%#v\n", y)

        cb <- true
}

$go run testoperateonclosedchannel.go
false
false false
0
panic: runtime error: send on closed channel

可以看到在一个已经close的unbuffered channel上执行读操作,回返回channel对应类型的零值,比如bool型channel返回false,int型channel返回0。但向close的channel写则会触发panic。不过无论读写都不会导致阻塞。

【关闭带缓存的channel】

将unbuffered channel换成buffered channel会怎样?我们看下面例子:

//testclosedbufferedchannel.go
package main

import "fmt"

func main() {
        c := make(chan int, 3)
        c <- 15
        c <- 34
        c <- 65
        close(c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)

        c <- 1
}

$go run testclosedbufferedchannel.go
15
34
65
0
panic: runtime error: send on closed channel

可以看出带缓冲的channel略有不同。尽管已经close了,但我们依旧可以从中读出关闭前写入的3个值。第四次读取时,则会返回该channel类型的零值。向这类channel写入操作也会触发panic。

【range】

Golang中的range常常和channel并肩作战,它被用来从channel中读取所有值。下面是一个简单的实例:

//testrange.go
package main

import "fmt"

func generator(strings chan string) {
        strings <- "Five hour's New York jet lag"
        strings <- "and Cayce Pollard wakes in Camden Town"
        strings <- "to the dire and ever-decreasing circles"
        strings <- "of disrupted circadian rhythm."
        close(strings)
}

func main() {
        strings := make(chan string)
        go generator(strings)
        for s := range strings {
                fmt.Printf("%s\n", s)
        }
        fmt.Printf("\n")
}

四、隐藏状态

下面通过一个例子来演示一下channel如何用来隐藏状态:

1、例子:唯一的ID服务

//testuniqueid.go
package main

import "fmt"

func newUniqueIDService() <-chan string {
        id := make(chan string)
        go func() {
                var counter int64 = 0
                for {
                        id <- fmt.Sprintf("%x", counter)
                        counter += 1
                }
        }()
        return id
}
func main() {
        id := newUniqueIDService()
        for i := 0; i < 10; i++ {
                fmt.Println(<-id)
        }
}

$ go run testuniqueid.go
0
1
2
3
4
5
6
7
8
9

newUniqueIDService通过一个channel与main goroutine关联,main goroutine无需知道uniqueid实现的细节以及当前状态,只需通过channel获得最新id即可。

五、默认情况

我想这里John Graham-Cumming主要是想告诉我们select的default分支的实践用法。

1、select  for non-blocking receive

idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列

select {
case b = <-idle:
 //尝试从idle队列中读取
    …
default:  //队列空,分配一个新的buffer
        makes += 1
        b = make([]byte, size)
}

2、select for non-blocking send

idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列

select {
case idle <- b: //尝试向队列中插入一个buffer
        //…
default: //队列满?

}

六、Nil Channels

1、nil channels阻塞

对一个没有初始化的channel进行读写操作都将发生阻塞,例子如下:

package main

func main() {
        var c chan int
        <-c
}

$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!

package main

func main() {
        var c chan int
        c <- 1
}

$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!

2、nil channel在select中很有用

看下面这个例子:

//testnilchannel_bad.go
package main

import "fmt"
import "time"

func main() {
        var c1, c2 chan int = make(chan int), make(chan int)
        go func() {
                time.Sleep(time.Second * 5)
                c1 <- 5
                close(c1)
        }()

        go func() {
                time.Sleep(time.Second * 7)
                c2 <- 7
                close(c2)
        }()

        for {
                select {
                case x := <-c1:
                        fmt.Println(x)
                case x := <-c2:
                        fmt.Println(x)
                }
        }
        fmt.Println("over")
}

我们原本期望程序交替输出5和7两个数字,但实际的输出结果却是:

5
0
0
0
… … 0死循环

再仔细分析代码,原来select每次按case顺序evaluate:
    – 前5s,select一直阻塞;
    – 第5s,c1返回一个5后被close了,“case x := <-c1”这个分支返回,select输出5,并重新select
    – 下一轮select又从“case x := <-c1”这个分支开始evaluate,由于c1被close,按照前面的知识,close的channel不会阻塞,我们会读出这个 channel对应类型的零值,这里就是0;select再次输出0;这时即便c2有值返回,程序也不会走到c2这个分支
    – 依次类推,程序无限循环的输出0

我们利用nil channel来改进这个程序,以实现我们的意图,代码如下:

//testnilchannel.go
package main

import "fmt"
import "time"

func main() {
        var c1, c2 chan int = make(chan int), make(chan int)
        go func() {
                time.Sleep(time.Second * 5)
                c1 <- 5
                close(c1)
        }()

        go func() {
                time.Sleep(time.Second * 7)
                c2 <- 7
                close(c2)
        }()

        for {
                select {
                case x, ok := <-c1:
                        if !ok {
                                c1 = nil
                        } else {
                                fmt.Println(x)
                        }
                case x, ok := <-c2:
                        if !ok {
                                c2 = nil
                        } else {
                                fmt.Println(x)
                        }
                }
                if c1 == nil && c2 == nil {
                        break
                }
        }
        fmt.Println("over")
}

$go run testnilchannel.go
5
7
over

可以看出:通过将已经关闭的channel置为nil,下次select将会阻塞在该channel上,使得select继续下面的分支evaluation。

七、Timers

1、超时机制Timeout

带超时机制的select是常规的tip,下面是示例代码,实现30s的超时select:

func worker(start chan bool) {
        timeout := time.After(30 * time.Second)
        for {
                select {
                     // … do some stuff
                case <- timeout:
                    return
                }
        }
}

2、心跳HeartBeart

与timeout实现类似,下面是一个简单的心跳select实现:

func worker(start chan bool) {
        heartbeat := time.Tick(30 * time.Second)
        for {
                select {
                     // … do some stuff
                case <- heartbeat:
                    //… do heartbeat stuff
                }
        }
}

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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