<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; Signal</title>
	<atom:link href="http://tonybai.com/tag/signal/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 25 May 2026 23:53:54 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Go开发命令行程序指南</title>
		<link>https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go/</link>
		<comments>https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go/#comments</comments>
		<pubDate>Fri, 24 Mar 2023 22:35:58 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AIGC]]></category>
		<category><![CDATA[argument]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[brew]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[cli]]></category>
		<category><![CDATA[clig.dev]]></category>
		<category><![CDATA[cobra]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[coverage]]></category>
		<category><![CDATA[curl]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[errors]]></category>
		<category><![CDATA[flag]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-module]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[goinstall]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[goreleaser]]></category>
		<category><![CDATA[GPT-4]]></category>
		<category><![CDATA[grep]]></category>
		<category><![CDATA[help]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[kingpin]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[option]]></category>
		<category><![CDATA[pkg]]></category>
		<category><![CDATA[POSIX]]></category>
		<category><![CDATA[README]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[subcommand]]></category>
		<category><![CDATA[subtest]]></category>
		<category><![CDATA[tar]]></category>
		<category><![CDATA[testify]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[usage]]></category>
		<category><![CDATA[主命令]]></category>
		<category><![CDATA[优雅退出]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[参数]]></category>
		<category><![CDATA[命令行程序]]></category>
		<category><![CDATA[子命令]]></category>
		<category><![CDATA[标志]]></category>
		<category><![CDATA[表驱动测试]]></category>
		<category><![CDATA[选项]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3837</guid>
		<description><![CDATA[注：上面篇首配图的底图由百度文心一格生成。 本文永久链接 &#8211; https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go 近期在Twitter上看到一个名为“Command Line Interface Guidelines”的站点，这个站点汇聚了帮助大家编写出更好命令行程序的哲学与指南。这份指南基于传统的Unix编程原则，又结合现代的情况进行了“与时俱进”的更新。之前我还真未就如何编写命令行交互程序做系统的梳理，在这篇文章中，我们就来结合clig这份指南，(可能不会全面覆盖)整理出一份使用Go语言编写CLI程序的指南，供大家参考。 一. 命令行程序简介 命令行接口（Command Line Interface, 简称CLI）程序是一种允许用户使用文本命令和参数与计算机系统互动的软件。开发人员编写CLI程序通常用在自动化脚本、数据处理、系统管理和其他需要低级控制和灵活性的任务上。命令行程序也是Linux/Unix管理员以及后端开发人员的最爱。 2022年Q2 Go官方用户调查结果显示(如下图)：在使用Go开发的程序类别上，CLI类程序排行第二，得票率60%。 之所以这样，得益于Go语言为CLI开发提供的诸多便利，比如： Go语法简单而富有表现力； Go拥有一个强大的标准库，并内置的并发支持； Go拥有几乎最好的跨平台兼容性和快速的编译速度； Go还有一个丰富的第三方软件包和工具的生态系统。 这些都让开发者使用Go创建强大和用户友好的CLI程序变得容易。 容易归容易，但要用Go编写出优秀的CLI程序，我们还需要遵循一些原则，获得一些关于Go CLI程序开发的最佳实践和惯例。这些原则和惯例涉及交互界面设计、错误处理、文档、测试和发布等主题。此外，借助于一些流行的Go CLI程序开发库和框架，比如：cobra、Kingpin和Goreleaser等，我们可以又好又快地完成CLI程序的开发。在本文结束时，你将学会如何创建一个易于使用、可靠和可维护的Go CLI程序，你还将获得一些关于CLI开发的最佳实践和惯例的见解。 二. 建立Go开发环境 如果你读过《十分钟入门Go语言》或订阅学习过我的极客时间《Go语言第一课》专栏，你大可忽略这一节的内容。 在我们开始编写Go CLI程序之前，我们需要确保我们的系统中已经安装和配置了必要的Go工具和依赖。在本节中，我们将向你展示如何安装Go和设置你的工作空间，如何使用go mod进行依赖管理，以及如何使用go build和go install来编译和安装你的程序。 1. 安装Go 要在你的系统上安装Go，你可以遵循你所用操作系统的官方安装说明。你也可以使用软件包管理器，如homebrew（用于macOS）、chocolatey（用于Windows）或snap/apt（用于Linux）来更容易地安装Go。 一旦你安装了Go，你可以通过在终端运行以下命令来验证它是否可以正常工作。 $go version 如果安装成功，go version这个命令应该会打印出你所安装的Go的版本。比如说： go version go1.20 darwin/amd64 2. 设置你的工作区(workspace) Go以前有一个惯例，即在工作区目录中(\$GOPATH)组织你的代码和依赖关系。默认工作空间目录位于$HOME/go，但你可以通过设置GOPATH环境变量来改变它的路径。工作区目录包含三个子目录：src、pkg和bin。src目录包含了你的源代码文件和目录。pkg目录包含被你的代码导入的已编译好的包。bin目录包含由你的代码生成的可执行二进制文件。 Go 1.11引入Go module后，这种在\$GOPATH下组织代码和寻找依赖关系的要求被彻底取消。在这篇文章中，我依旧按照我的习惯在$HOME/go/src下放置我的代码示例。 为了给我们的CLI程序创建一个新的项目目录，我们可以在终端运行以下命令： $mkdir -p [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-developing-cli-program-in-go-1.png" alt="" /></p>
<blockquote>
<p>注：上面篇首配图的底图由百度<a href="https://yige.baidu.com">文心一格</a>生成。</p>
</blockquote>
<p><a href="https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go">本文永久链接</a> &#8211; https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go</p>
<p>近期在Twitter上看到一个名为<a href="https://clig.dev/">“Command Line Interface Guidelines”的站点</a>，这个站点汇聚了帮助大家编写出更好命令行程序的哲学与指南。这份指南<a href="https://book.douban.com/subject/1467587/">基于传统的Unix编程原则</a>，又结合现代的情况进行了“与时俱进”的更新。之前我还真未就如何编写命令行交互程序做系统的梳理，在这篇文章中，我们就来结合<a href="https://clig.dev/">clig这份指南</a>，(可能不会全面覆盖)整理出一份使用Go语言编写CLI程序的指南，供大家参考。</p>
<h2>一. 命令行程序简介</h2>
<p>命令行接口（Command Line Interface, 简称CLI）程序是一种允许用户使用文本命令和参数与计算机系统互动的软件。开发人员编写CLI程序通常用在自动化脚本、数据处理、系统管理和其他需要低级控制和灵活性的任务上。<strong>命令行程序也是Linux/Unix管理员以及后端开发人员的最爱</strong>。</p>
<p><a href="https://go.dev/blog/survey2022-q2-results">2022年Q2 Go官方用户调查结果</a>显示(如下图)：在使用Go开发的程序类别上，CLI类程序排行第二，得票率60%。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-developing-cli-program-in-go-2.png" alt="" /></p>
<p>之所以这样，得益于Go语言为CLI开发提供的诸多便利，比如：</p>
<ul>
<li>Go语法简单而富有表现力；</li>
<li>Go拥有一个强大的标准库，并内置的并发支持；</li>
<li>Go拥有几乎最好的跨平台兼容性和快速的编译速度；</li>
<li>Go还有一个丰富的第三方软件包和工具的生态系统。</li>
</ul>
<p>这些都让开发者使用Go创建强大和用户友好的CLI程序变得容易。</p>
<p>容易归容易，但要用Go编写出优秀的CLI程序，我们还需要遵循一些原则，获得一些关于Go CLI程序开发的最佳实践和惯例。这些原则和惯例涉及交互界面设计、错误处理、文档、测试和发布等主题。此外，借助于一些流行的Go CLI程序开发库和框架，比如：<a href="https://github.com/spf13/cobra">cobra</a>、<a href="https://github.com/alecthomas/kingpin">Kingpin</a>和<a href="https://github.com/goreleaser/goreleaser">Goreleaser</a>等，我们可以又好又快地完成CLI程序的开发。在本文结束时，你将学会如何创建一个易于使用、可靠和可维护的Go CLI程序，你还将获得一些关于CLI开发的最佳实践和惯例的见解。</p>
<h2>二. 建立Go开发环境</h2>
<blockquote>
<p>如果你读过<a href="https://tonybai.com/2023/02/23/learn-go-in-10-min">《十分钟入门Go语言》</a>或订阅学习过我的极客时间<a href="http://gk.link/a/10AVZ">《Go语言第一课》专栏</a>，你大可忽略这一节的内容。</p>
</blockquote>
<p>在我们开始编写Go CLI程序之前，我们需要确保我们的系统中已经安装和配置了必要的Go工具和依赖。在本节中，我们将向你展示如何安装Go和设置你的工作空间，如何<a href="https://tonybai.com/2022/03/12/dependency-hell-in-go/">使用go mod进行依赖管理</a>，以及如何使用go build和go install来编译和安装你的程序。</p>
<h3>1. 安装Go</h3>
<p>要在你的系统上安装Go，你可以遵循你所用操作系统的官方安装说明。你也可以使用软件包管理器，如<a href="https://brew.sh">homebrew</a>（用于macOS）、chocolatey（用于Windows）或snap/apt（用于Linux）来更容易地安装Go。</p>
<p>一旦你安装了Go，你可以通过在终端运行以下命令来验证它是否可以正常工作。</p>
<pre><code>$go version
</code></pre>
<p>如果安装成功，go version这个命令应该会打印出你所安装的Go的版本。比如说：</p>
<pre><code>go version go1.20 darwin/amd64
</code></pre>
<h3>2. 设置你的工作区(workspace)</h3>
<p>Go以前有一个惯例，即在工作区目录中(\$GOPATH)组织你的代码和依赖关系。默认工作空间目录位于$HOME/go，但你可以通过设置GOPATH环境变量来改变它的路径。工作区目录包含三个子目录：src、pkg和bin。src目录包含了你的源代码文件和目录。pkg目录包含被你的代码导入的已编译好的包。bin目录包含由你的代码生成的可执行二进制文件。</p>
<p><a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11">Go 1.11引入Go module</a>后，这种在\$GOPATH下组织代码和寻找依赖关系的要求被彻底取消。在这篇文章中，我依旧按照我的习惯在$HOME/go/src下放置我的代码示例。</p>
<p>为了给我们的CLI程序创建一个新的项目目录，我们可以在终端运行以下命令：</p>
<pre><code>$mkdir -p $HOME/go/src/github.com/your-username/your-li-program
$cd $HOME/go/src/github.com/your-username/your-cli-program
</code></pre>
<p>注意，我们的项目目录名使用的是github的URL格式。这在Go项目中是一种常见的做法，因为它使得使用go get导入和管理依赖关系更加容易。go module成为构建标准后，这种对项目目录名的要求已经取消，但很多Gopher依旧保留了这种作法。</p>
<h3>3. 使用go mod进行依赖管理</h3>
<p>1.11版本后Go推荐开发者使用module来管理包的依赖关系。一个module是共享一个共同版本号和导入路径前缀的相关包的集合。一个module是由一个叫做go.mod的文件定义的，它指定了模块的名称、版本和依赖关系。</p>
<p>为了给我们的CLI程序创建一个新的module，我们可以在我们的项目目录下运行以下命令。</p>
<pre><code>$go mod init github.com/your-username/your-cli-program
</code></pre>
<p>这将创建一个名为go.mod的文件，内容如下。</p>
<pre><code>module github.com/your-username/your-cli-program

go 1.20
</code></pre>
<p>第一行指定了我们的module名称，这与我们的项目目录名称相匹配。第二行指定了构建我们的module所需的Go的最低版本。</p>
<p>为了给我们的模块添加依赖项，我们可以使用go get命令，加上我们想使用的软件包的导入路径和可选的版本标签。例如，如果我们想使用<a href="https://github.com/spf13/cobra">cobra</a>作为我们的CLI框架，我们可以运行如下命令：</p>
<pre><code>$go get github.com/spf13/cobra@v1.3.0
</code></pre>
<p>go get将从github下载cobra，并在我们的go.mod文件中把它作为一个依赖项添加进去。它还将创建或更新一个名为go.sum的文件，记录所有下载的module的校验和，以供后续验证使用。</p>
<p>我们还可以使用其他命令，如go list、go mod tidy、go mod graph等，以更方便地检查和管理我们的依赖关系。</p>
<h3>4. 使用go build和go install来编译和安装你的程序</h3>
<p>Go有两个命令允许你编译和安装你的程序：go build和go install。这两个命令都以一个或多个包名或导入路径作为参数，并从中产生可执行的二进制文件。</p>
<p>它们之间的主要区别在于它们将生成的二进制文件存储在哪里。</p>
<ul>
<li>go build将它们存储在当前工作目录中。</li>
<li>go install将它们存储在\$GOPATH/bin或\$GOBIN（如果设置了）。</li>
</ul>
<p>例如，如果我们想把CLI程序的main包（应该位于github.com/your-username/your-cli-program/cmd/your-cli-program）编译成一个可执行的二进制文件，称为your-cli-program，我们可以运行下面命令：</p>
<pre><code>$go build github.com/your-username/your-cli-program/cmd/your-cli-program
</code></pre>
<p>或</p>
<pre><code>$go install github.com/your-username/your-cli-program/cmd/your-cli-program@latest
</code></pre>
<h2>三. 设计用户接口(interface)</h2>
<p>要编写出一个好的CLI程序，最重要的环节之一是<strong><a href="https://clig.dev/#human-first-design">设计一个用户友好的接口</a></strong>。好的命令行用户接口应该是<strong>一致的、直观的和富有表现力的</strong>。在本节中，我将说明如何为命令行程序命名和选择命令结构(command structure)，如何使用标志(flag)、参数(argument)、子命令(subcommand)和选项(option)作为输入参数，如何使用cobra或Kingpin等来解析和验证用户输入，以及如何遵循POSIX惯例和GNU扩展的CLI语法。</p>
<h3>1. 命令行程序命名和命令结构选择</h3>
<p>你的CLI程序的名字应该是<strong><a href="https://clig.dev/#naming">简短、易记、描述性的和易输入的</a></strong>。它应该避免与目标平台中现有的命令或关键字发生冲突。例如，如果你正在编写一个在不同格式之间转换图像的程序，你可以把它命名为imgconv、imago、picto等，但不能叫image、convert或format。</p>
<p>你的CLI程序的命令结构应该反映你想提供给用户的主要功能特性。你可以选择使用下面命令结构模式中的一种：</p>
<ul>
<li>一个带有多个标志(flag)和参数(argument)的单一命令（例如：curl、tar、grep等)</li>
<li>带有多个子命令(subcommand)的单一命令（例如：git、docker、kubectl等)</li>
<li>具有共同前缀的多个命令（例如：aws s3、gcloud compute、az vm等)</li>
</ul>
<p>命令结构模式的选择取决于你的程序的复杂性和使用范围，一般来说：</p>
<ul>
<li>如果你的程序只有一个主要功能或操作模式(operation mode)，你可以使用带有多个标志和参数的单一命令。</li>
<li>如果你的程序有多个相关但又不同的功能或操作模式，你可以使用一个带有多个子命令的单一命令。</li>
<li>如果你的程序有多个不相关或独立的功能或操作模式，你可以使用具有共同前缀的多个命令。</li>
</ul>
<p>例如，如果你正在编写一个对文件进行各种操作的程序（如复制、移动、删除），你可以任选下面命令结构模式中的一种：</p>
<ul>
<li>带有多个标志和参数的单一命令（例如，fileop -c src dst -m src dst -d src)</li>
<li>带有多个子命令的单个命令（例如，fileop copy src dst, fileop move src dst, fileop delete src)</li>
</ul>
<h3>2. 使用标志、参数、子命令和选项</h3>
<p><strong>标志(flag)</strong>是以一个或多个(通常是2个)中划线（-）开头的输入参数，它可以修改CLI程序的行为或输出。例如：</p>
<pre><code>$curl -s -o output.txt https://example.com
</code></pre>
<p>在这个例子中：</p>
<ul>
<li>“-s”是一个让curl沉默的标志，即不输出执行日志到控制台；</li>
<li>“-o”是另一个标志，用于指定输出文件的名称</li>
<li>“output.txt”则是一个参数，是为“-o”标志提供的值。</li>
</ul>
<p><strong>参数(argument)</strong>是不以中划线（-）开头的输入参数，为你的CLI程序提供额外的信息或数据。例如：</p>
<pre><code>$tar xvf archive.tar.gz
</code></pre>
<p>我们看在这个例子中：</p>
<ul>
<li>x是一个指定提取模式的参数</li>
<li>v是一个参数，指定的是输出内容的详细(verbose)程度</li>
<li>f是另一个参数，用于指定采用的是文件模式，即将压缩结果输出到一个文件或从一个压缩文件读取数据</li>
<li>archive.tar.gz是一个参数，提供文件名。</li>
</ul>
<p><strong>子命令(subcommand)</strong>是输入参数，作为主命令下的辅助命令。它们通常有自己的一组标志和参数。比如下面例子：</p>
<pre><code>$git commit -m "Initial commit"
</code></pre>
<p>我们看在这个例子中：</p>
<ul>
<li>git是主命令(primary command)</li>
<li>commit是一个子命令，用于从staged的修改中创建一个新的提交(commit)</li>
<li>“-m”是commit子命令的一个标志，用于指定提交信息</li>
<li>“Initial commit”是commit子命令的一个参数，为”-m”标志提供值。</li>
</ul>
<p><strong>选项(option)</strong>是输入参数，它可以使用等号（=）将标志和参数合并为一个参数。例如:</p>
<pre><code>$docker run --name=my-container ubuntu:latest
</code></pre>
<p>我们看在这个例子中“&#8211;name=my-container”是一个选项，它将容器的名称设为my-container。该选项前面的部分“&#8211;name”是一个标志，后面的部分“my-container”是参数。</p>
<h3>3. 使用cobra包等来解析和验证用户输入的信息</h3>
<p>如果手工来解析和验证用户输入的信息，既繁琐又容易出错。幸运的是，有许多库和框架可以帮助你在Go中解析和验证用户输入。其中最流行的是<a href="https://github.com/spf13/cobra">cobra</a>。</p>
<p>cobra是一个Go包，它提供了简单的接口来创建强大的CLI程序。它支持子命令、标志、参数、选项、环境变量和配置文件。它还能很好地与其他库集成，比如：<a href="https://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files/">viper</a>（用于配置管理）、<a href="https://github.com/spf13/pflag">pflag</a>（用于POSIX/GNU风格的标志）和<a href="https://github.com/docopt/docopt.go">Docopt</a>（用于生成文档）。</p>
<p>另一个不那么流行但却提供了一种声明式的方法来创建优雅的CLI程序的包是<a href="https://github.com/alecthomas/kingpin">Kingpin</a>，它支持标志、参数、选项、环境变量和配置文件。它还具有自动帮助生成、命令完成、错误处理和类型转换等功能。</p>
<p>cobra和Kingpin在其官方网站上都有大量的文档和例子，你可以根据你的偏好和需要选择任选其一。</p>
<h3>4. 遵循POSIX惯例和GNU扩展的CLI语法</h3>
<p><a href="http://get.posixcertified.ieee.org">POSIX（Portable Operating System Interface）</a>是一套标准，定义了软件应该如何与操作系统进行交互。其中一个标准定义了CLI程序的语法和语义。GNU（GNU&#8217;s Not Unix）是一个旨在创建一个与UNIX兼容的自由软件操作系统的项目。GNU下的一个子项目是<a href="https://www.gnu.org/software/coreutils/">GNU Coreutils</a>，它提供了许多常见的CLI程序，如ls、cp、mv等。</p>
<p>POSIX和GNU都为CLI语法建立了一些约定和扩展，许多CLI程序都采用了这些约定与扩展。下面列举了这些约定和扩展中的一些主要内容：</p>
<ul>
<li>单字母标志(single-letter flag)以一个中划线（-）开始，可以组合在一起（例如：-a -b -c 或 -abc )</li>
<li>长标志(long flag)以两个中划线（&#8211;）开头，但不能组合在一起（例如：&#8211;all、&#8211;backup、&#8211;color )</li>
<li>选项使用等号(=)来分隔标志名和参数值(例如：&#8211;name=my-container )</li>
<li>参数跟在标志或选项之后，没有任何分隔符（例如：curl -o output.txt https://example.com ）。</li>
<li>子命令跟在主命令之后，没有任何分隔符（例如：git commit -m “Initial commit” )</li>
<li>一个双中划线（&#8211;）表示标志或选项的结束和参数的开始（例如：rm &#8212; -f 表示要删除“-f”这个文件，由于双破折线的存在，这里的“-f”不再是标志)</li>
</ul>
<p>遵循这些约定和扩展可以使你的CLI程序更加一致、直观，并与其他CLI程序兼容。然而，它们并不是强制性的，如果你有充分的理由，你也大可不必完全遵守它们。例如，一些CLI程序使用斜线（/）而不是中划线（-）表示标志（例如， robocopy /S /E src dst ）。</p>
<h2>四. 处理错误和信号</h2>
<p>编写好的CLI程序的一个重要环节就是<strong><a href="https://clig.dev/#errors">优雅地处理错误和信号</a></strong>。</p>
<p>错误是指你的程序由于某些内部或外部因素而无法执行其预定功能的情况。信号是由操作系统或其他进程向你的程序发送的事件，以通知它一些变化或请求。在这一节中，我将说明一下如何使用log、fmt和errors包进行日志输出和错误处理，如何使用os.Exit和defer语句进行优雅的终止，如何使用os.Signal和context包进行中断和取消操作，以及如何遵循CLI程序的退出状态代码惯例。</p>
<h3>1. 使用log、fmt和errors包进行日志记录和错误处理</h3>
<p>Go标准库中有三个包log、fmt和errors可以帮助你进行日志和错误处理。log包提供了一个简单的接口，可以将格式化的信息写到标准输出或文件中。fmt包则提供了各种格式化字符串和值的函数。errors包提供了创建和操作错误值的函数。</p>
<p>要使用log包，你需要在你的代码中导入它：</p>
<pre><code>import "log"
</code></pre>
<p>然后你可以使用log.Println、log.Printf、log.Fatal和log.Fatalf等函数来输出不同严重程度的信息。比如说：</p>
<pre><code>log.Println("Starting the program...") // 打印带有时间戳的消息
log.Printf("Processing file %s...\n", filename) // 打印一个带时间戳的格式化信息
log.Fatal("Cannot open file: ", err) // 打印一个带有时间戳的错误信息并退出程序
log.Fatalf("Invalid input: %v\n", input) // 打印一个带时间戳的格式化错误信息，并退出程序。
</code></pre>
<p>为了使用fmt包，你需要先在你的代码中导入它：</p>
<pre><code>import "fmt"
</code></pre>
<p>然后你可以使用fmt.Println、fmt.Printf、fmt.Sprintln、fmt.Sprintf等函数以各种方式格式化字符串和值。比如说：</p>
<pre><code>fmt.Println("Hello world!") // 打印一条信息，后面加一个换行符
fmt.Printf("The answer is %d\n", 42) // 打印一条格式化的信息，后面是换行。
s := fmt.Sprintln("Hello world!") // 返回一个带有信息和换行符的字符串。
t := fmt.Sprintf("The answer is %d\n", 42) // 返回一个带有格式化信息和换行的字符串。
</code></pre>
<p>要使用错误包，你同样需要在你的代码中导入它：</p>
<pre><code>import "errors"
</code></pre>
<p>然后你可以使用 errors.New、errors.Unwrap、errors.Is等函数来创建和操作错误值。比如说：</p>
<pre><code>err := errors.New("Something went wrong") // 创建一个带有信息的错误值
cause := errors.Unwrap(err) // 返回错误值的基本原因（如果没有则为nil）。
match := errors.Is(err, io.EOF) // 如果一个错误值与另一个错误值匹配，则返回真（否则返回假）。
</code></pre>
<h3>2. 使用os.Exit和defer语句实现CLI程序的优雅终止</h3>
<p>Go有两个功能可以帮助你优雅地终止CLI程序：os.Exit和defer。os.Exit函数立即退出程序，并给出退出状态代码。defer语句则会在当前函数退出前执行一个函数调用，它常用来执行清理收尾动作，如关闭文件或释放资源。</p>
<p>要使用os.Exit函数，你需要在你的代码中导入os包：</p>
<pre><code>import "os"
</code></pre>
<p>然后你可以使用os.Exit函数，它的整数参数代表退出状态代码。比如说</p>
<pre><code>os.Exit(0) // 以成功的代码退出程序
os.Exit(1) // 以失败代码退出程序
</code></pre>
<p>要使用defer语句，你需要把它写在你想后续执行的函数调用之前。比如说</p>
<pre><code>file, err := os.Open(filename) // 打开一个文件供读取。
if err != nil {
    log.Fatal(err) // 发生错误时退出程序
}
defer file.Close() // 在函数结束时关闭文件。

// 对文件做一些处理...
</code></pre>
<h3>3. 使用os.signal和context包来实现中断和取消操作</h3>
<p>Go有两个包可以帮助你实现中断和取消长期运行的或阻塞的操作，它们是os.signal和context包。os.signal提供了一种从操作系统或其他进程接收信号的方法。context包提供了一种跨越API边界传递取消信号和deadline的方法。</p>
<p>要使用os.signal，你需要先在你的代码中导入它。</p>
<pre><code>import (
  "os"
  "os/signal"
)
</code></pre>
<p>然后你可以使用signal.Notify函数针对感兴趣的信号(如下面的os.Interrupt信号)注册一个接收channel(sig)。比如说：</p>
<pre><code>sig := make(chan os.Signal, 1) // 创建一个带缓冲的信号channel。
signal.Notify(sig, os.Interrupt) // 注册sig以接收中断信号（例如Ctrl-C）。

// 做一些事情...

select {
case &lt;-sig: // 等待来自sig channel的信号
    fmt.Println("被用户中断了")
    os.Exit(1) // 以失败代码退出程序。
default: //如果没有收到信号就执行
    fmt.Println("成功完成")
    os.Exit(0) // 以成功代码退出程序。
}
</code></pre>
<p>要使用上下文包，你需要在你的代码中导入它：</p>
<pre><code>import "context"
</code></pre>
<p>然后你可以使用它的函数，如context.Background、context.WithCancel、context.WithTimeout等来创建和管理Context。Context是一个携带取消信号和deadline的对象，可以跨越API边界。比如说：</p>
<pre><code>ctx := context.Background() // 创建一个空的背景上下文（从不取消）。
ctx, cancel := context.WithCancel(ctx) // 创建一个新的上下文，可以通过调用cancel函数来取消。
defer cancel() // 在函数结束前执行ctx的取消动作

// 将ctx传递给一些接受它作为参数的函数......

select {
case &lt;-ctx.Done(): // 等待来自ctx的取消信号
    fmt.Println("Canceled by parent")
    return ctx.Err() // 从ctx返回一个错误值
default: // 如果没有收到取消信号就执行
    fmt.Println("成功完成")
    return nil // 不返回错误值
}
</code></pre>
<h3>4. CLI程序的退出状态代码惯例</h3>
<p>退出状态代码是一个整数，表示CLI程序是否成功执行完成。CLI程序通过调用os.Exit或从main返回的方式返回退出状态值。其他CLI程序或脚本可以可以检查这些退出状态码，并根据状态码值的不同执行不同的处理操作。</p>
<p>业界有一些关于退出状态代码的约定和扩展，这些约定被许多CLI程序广泛采用。其中一些主要的约定和扩展如下：。</p>
<ul>
<li>退出状态代码为0表示程序执行成功（例如：os.Exit(0) )</li>
<li>非零的退出状态代码表示失败（例如：os.Exit(1) ）。</li>
<li>不同的非零退出状态代码可能表示不同的失败类型或原因（例如：os.Exit(2)表示使用错误，os.Exit(3)表示权限错误等等）。</li>
<li>大于125的退出状态代码可能表示被外部信号终止（例如，os.Exit(130)为被信号中断）。</li>
</ul>
<p>遵循这些约定和扩展可以使你的CLI程序表现的更加一致、可靠并与其他CLI程序兼容。然而，它们不是强制性的，你可以使用任何对你的程序有意义的退出状态代码。例如，一些CLI程序使用高于200的退出状态代码来表示自定义或特定应用的错误（例如，os.Exit(255)表示未知错误）。</p>
<h2>五. 编写文档</h2>
<p>编写优秀CLI程序的另一个重要环节是编写清晰简洁的文档，解释你的程序做什么以及如何使用它。文档可以采取各种形式，如README文件、usage信息、help flag等。在本节中，我们将告诉你如何为你的程序写一个README文件，如何为你的程序写一个有用的usage和help flag等。</p>
<h3>1. 为你的CLI程序写一个清晰简洁的README文件</h3>
<p>README文件是一个文本文件，它提供了关于你的程序的基本信息，如它的名称、描述、用法、安装、依赖性、许可证和联系细节等。它通常是用户或开发者在源代码库或软件包管理器上首次使用你的程序时会看到的内容。</p>
<p>如果你要为Go CLI程序编写一个优秀的README文件，你应该遵循一些最佳实践，比如：</p>
<ul>
<li>使用一个描述性的、醒目的标题，反映你的程序的目的和功能。</li>
<li>提供一个简短的介绍，解释你的程序是做什么的，为什么它是有用的或独特的。</li>
<li>包括一个usage部分，说明如何用不同的标志、参数、子命令和选项来调用你的程序。你可以使用代码块或屏幕截图来说明这些例子。</li>
<li>包括一个安装(install)部分，解释如何在不同的平台上下载和安装你的程序。你可以使用go install、go get、<a href="https://github.com/goreleaser/goreleaser">goreleaser</a>或其他工具来简化这一过程。</li>
<li>指定你的程序的发行许可，并提供一个许可全文的链接。你可以使用<a href="https://spdx.dev/">SPDX标识符</a>来表示许可证类型。</li>
<li>为想要报告问题、请求新功能、贡献代码或提问的用户或开发者提供联系信息。你可以使用github issue、pr、discussion、电子邮件或其他渠道来达到这个目的。</li>
</ul>
<p>以下是一个Go CLI程序的README文件的示例供参考：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-developing-cli-program-in-go-3.png" alt="" /></p>
<h3>2. 为你的CLI程序编写有用的usage和help标志</h3>
<p>usage信息是一段简短的文字，总结了如何使用你的程序及其可用的标志、参数、子命令和选项。它通常在你的程序在没有参数或输入无效的情况下运行时显示。</p>
<p>help标志是一个特殊的标志（通常是-h或&#8211;help），它可以触发显示使用信息和一些关于你的程序的额外信息。</p>
<p>为了给你的Go CLI程序写有用的usage信息和help标志，你应该遵循一些准则，比如说：</p>
<ul>
<li>使用一致而简洁的语法来描述标志、参数、子命令和选项。你可以用方括号“[ ]”表示可选元素，使用角括号“&lt; >”表示必需元素，使用省略号“&#8230;”表示重复元素，使用管道“|”表示备选，使用中划线“-”表示标志(flag)，使用等号“=”表示标志的值等等。</li>
<li>对标志、参数、子命令和选项应使用描述性的名称，以反映其含义和功能。避免使用单字母名称，除非它们非常常见或非常直观（如-v按惯例表示verbose模式）。</li>
<li>为每个标志、参数、子命令和选项提供简短而清晰的描述，解释它们的作用以及它们如何影响你的程序的行为。你可以用圆括号“（ ）”来表达额外的细节或例子。</li>
<li>使用标题或缩进将相关的标志、参数、子命令和选项组合在一起。你也可以用空行或水平线（&#8212;）来分隔usage的不同部分。</li>
<li>在每组中按名称的字母顺序排列标志。在每组中按重要性或逻辑顺序排列参数。在每组中按使用频率排列子命令。</li>
</ul>
<p>git的usage就是一个很好的例子：</p>
<pre><code>$git
usage: git [--version] [--help] [-C &lt;path&gt;] [-c &lt;name&gt;=&lt;value&gt;]
           [--exec-path[=&lt;path&gt;]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=&lt;path&gt;] [--work-tree=&lt;path&gt;] [--namespace=&lt;name&gt;]
           &lt;command&gt; [&lt;args&gt;]
</code></pre>
<p>结合上面的准则，大家可以细心体会一下。</p>
<h2>六. 测试和发布你的CLI程序</h2>
<p>编写优秀CLI程序的最后一个环节是测试和发布你的程序。测试确保你的程序可以按预期工作，并符合质量标准。发布可以使你的程序可供用户使用和访问。</p>
<p>在本节中，我将说明如何使用testing、testify/assert、mock包对你的代码进行单元测试，如何使用go test、coverage、benchmark工具来运行测试和测量程序性能以及如何使用goreleaser包来构建跨平台的二进制文件。</p>
<h3>1. 使用testing、testify的assert及mock包对你的代码进行单元测试</h3>
<p>单元测试是一种验证单个代码单元（如函数、方法或类型）的正确性和功能的技术。单元测试可以帮助你尽早发现错误，提高代码质量和可维护性，并促进重构和调试。</p>
<p>要为你的Go CLI程序编写单元测试，你应该遵循一些最佳实践：</p>
<ul>
<li>使用内置的测试包来创建测试函数，以Test开头，后面是被测试的函数或方法的名称。例如：func TestSum(t *testing.T) { &#8230; }；</li>
<li>使用&#42;testing.T类型的t参数，使用t.Error、t.Errorf、t.Fatal或t.Fatalf这样的方法报告测试失败。你也可以使用t.Log、t.Logf、t.Skip或t.Skipf这样的方法来提供额外的信息或有条件地跳过测试。</li>
<li>使用<a href="https://tonybai.com/2023/03/15/an-intro-of-go-subtest">Go子测试(sub test)</a>，通过t.Run方法将相关的测试分组。例如：</li>
</ul>
<pre><code>func TestSum(t *testing.T) {
    t.Run("positive numbers", func(t *testing.T) {
        // test sum with positive numbers
    })
    t.Run("negative numbers", func(t *testing.T) {
        // test sum with negative numbers
    })
}
</code></pre>
<ul>
<li>使用表格驱动(table-driven)的测试来运行多个测试用例，比如下面的例子：</li>
</ul>
<pre><code>func TestSum(t *testing.T) {
    tests := []struct{
        name string
        a int
        b int
        want int
    }{
        {"positive numbers", 1, 2, 3},
        {"negative numbers", -1, -2, -3},
        {"zero", 0, 0 ,0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Sum(tt.a , tt.b)
            if got != tt.want {
                t.Errorf("Sum(%d , %d) = %d; want %d", tt.a , tt.b , got , tt.want)
            }
        })
    }
}
</code></pre>
<ul>
<li>使用外部包，如testify/assert或mock来简化你的断言或对外部的依赖性。比如说：</li>
</ul>
<pre><code>import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

type Calculator interface {
    Sum(a int , b int) int
}

type MockCalculator struct {
    mock.Mock
}

func (m *MockCalculator) Sum(a int , b int) int {
    args := m.Called(a , b)
    return args.Int(0)
}
</code></pre>
<h3>2. 使用Go的测试、覆盖率、性能基准工具来运行测试和测量性能</h3>
<p>Go提供了一套工具来运行测试和测量你的代码的性能。你可以使用这些工具来确保你的代码按预期工作，检测错误或bug，并优化你的代码以提高速度和效率。</p>
<p>要使用go test、coverage、benchmark工具来运行测试和测量你的Go CLI程序的性能，你应该遵循一些步骤，比如说。</p>
<ul>
<li>将以&#95;test.go结尾的测试文件写在与被测试代码相同的包中。例如：sum_test.go用于测试sum.go。</li>
<li>使用go测试命令来运行一个包中的所有测试或某个特定的测试文件。你也可以使用一些标志，如-v，用于显示verbose的输出，-run用于按名字过滤测试用例，-cover用于显示代码覆盖率，等等。例如：go test -v -cover ./&#8230;</li>
<li>使用go工具cover命令来生成代码覆盖率的HTML报告，并高亮显示代码行。你也可以使用-func这样的标志来显示函数的代码覆盖率，用-html还可以在浏览器中打开覆盖率结果报告等等。例如：go tool cover -html=coverage.out</li>
<li>编写性能基准函数，以Benchmark开头，后面是被测试的函数或方法的名称。使用类型为&#42;testing.B的参数b来控制迭代次数，并使用b.N、b.ReportAllocs等方法控制报告结果的输出。比如说</li>
</ul>
<pre><code>func BenchmarkSum(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        Sum(1 , 2)
    }
}
</code></pre>
<ul>
<li>
<p>使用go test -bench命令来运行一个包中的所有性能基准测试或某个特定的基准文件。你也可以使用-benchmem这样的标志来显示内存分配的统计数据，-cpuprofile或-memprofile来生成CPU或内存profile文件等等。例如：go test -bench . -benchmem ./&#8230;</p>
</li>
<li>
<p>使用pprof或benchstat等工具来分析和比较CPU或内存profile文件或基准测试结果。比如说。</p>
</li>
</ul>
<pre><code># Generate CPU profile
go test -cpuprofile cpu.out ./...

# Analyze CPU profile using pprof
go tool pprof cpu.out

# Generate two sets of benchmark results
go test -bench . ./... &gt; old.txt
go test -bench . ./... &gt; new.txt

# Compare benchmark results using benchstat
benchstat old.txt new.txt
</code></pre>
<h3>3. 使用goreleaser包构建跨平台的二进制文件</h3>
<p>构建跨平台二进制文件意味着将你的代码编译成可执行文件，可以在不同的操作系统和架构上运行，如Windows、Linux、Mac OS、ARM等。这可以帮助你向更多的人分发你的程序，使用户更容易安装和运行你的程序而不需要任何依赖或配置。</p>
<p>为了给你的Go CLI程序建立跨平台的二进制文件，你可以使用外部软件包，比如goreleaser等 ，它们可以自动完成程序的构建、打包和发布过程。下面是使用goreleaser包构建程序的一些步骤。</p>
<ul>
<li>使用go get或go install命令安装goreleaser。例如： go install github.com/goreleaser/goreleaser@latest</li>
<li>创建一个配置文件（通常是.goreleaser.yml），指定如何构建和打包你的程序。你可以定制各种选项，如二进制名称、版本、主文件、输出格式、目标平台、压缩、校验和、签名等。例如。</li>
</ul>
<pre><code># .goreleaser.yml
project_name: mycli
builds:
  - main: ./cmd/mycli/main.go
    binary: mycli
    goos:
      - windows
      - darwin
      - linux
    goarch:
      - amd64
      - arm64
archives:
  - format: zip
    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    files:
      - LICENSE.txt
      - README.md
checksum:
  name_template: "{{ .ProjectName }}_checksums.txt"
  algorithm: sha256
</code></pre>
<p>运行goreleaser命令，根据配置文件构建和打包你的程序。你也可以使用-snapshot用于测试，-release-notes用于从提交信息中生成发布说明，-rm-dist用于删除之前的构建，等等。例如：goreleaser &#8211;snapshot &#8211;rm-dist。</p>
<p>检查输出文件夹（通常是dist）中生成的二进制文件和其他文件。你也可以使用goreleaser的发布功能将它们上传到源代码库或软件包管理器中。</p>
<h2>七. clig.dev指南要点</h2>
<p>通过上述的系统说明，你现在应该可以设计并使用Go实现出一个CLI程序了。不过本文并非覆盖了clig.dev指南的所有要点，因此，在结束本文之前，我们再来回顾一下clig.dev指南中的要点，大家再体会一下。</p>
<p>前面说过，clig.dev上的cli指南是一个开源指南，可以帮助你写出更好的命令行程序，它采用了传统的UNIX原则，并针对现代的情况进行了更新。</p>
<p>遵循cli准则的一些好处是：</p>
<ul>
<li>你可以创建易于使用、理解和记忆的CLI程序。</li>
<li>你可以设计出能与其他程序进行很好配合的CLI程序，并遵循共同的惯例。</li>
<li>你可以避免让用户和开发者感到沮丧的常见陷阱和错误。</li>
<li>你可以从其他CLI设计者和用户的经验和智慧中学习。</li>
</ul>
<p>下面是该指南的一些要点：</p>
<ul>
<li>理念</li>
</ul>
<p>这一部分解释了好的CLI设计背后的核心原则，如人本设计、可组合性、可发现性、对话性等。例如，以人为本的设计意味着CLI程序对人类来说应该易于使用和理解，而不仅仅是机器。可组合性意味着CLI程序应该通过遵循共同的惯例和标准与其他程序很好地协作。</p>
<ul>
<li>参数和标志</li>
</ul>
<p>这一部分讲述了如何在你的CLI程序中使用位置参数(positional arguments )和标志。它还解释了如何处理默认值、必传参数、布尔标志、多值等。例如，你应该对命令的主要对象或动作使用位置参数，对修改或可选参数使用标志。你还应该使用长短两种形式的标志（如-v或-verbose），并遵循常见的命名模式（如&#8211;help或&#8211;version）。</p>
<ul>
<li>配置</li>
</ul>
<p>这部分介绍了如何使用配置文件和环境变量来为你的CLI程序存储持久的设置。它还解释了如何处理配置选项的优先级、验证、文档等。例如，你应该使用配置文件来处理用户很少改变的设置，或者是针对某个项目或环境的设置。对于特定于环境或会话的设置（如凭证或路径），你也应该使用环境变量。</p>
<ul>
<li>输出</li>
</ul>
<p>这部分介绍了如何格式化和展示你的CLI程序的输出。它还解释了如何处理输出verbose级别、进度指示器、颜色、表格等。例如，你应该使用标准输出（stdout）进行正常的输出，这样输出的信息可以通过管道输送到其他程序或文件。你还应该使用标准错误（stderr）来处理不属于正常输出流的错误或警告。</p>
<ul>
<li>错误</li>
</ul>
<p>这部分介绍了如何在你的CLI程序中优雅地处理错误。它还解释了如何使用退出状态码、错误信息、堆栈跟踪等。例如，你应该使用表明错误类型的退出代码（如0代表成功，1代表一般错误）。你还应该使用简洁明了的错误信息，解释出错的原因以及如何解决。</p>
<ul>
<li>子命令</li>
</ul>
<p>这部分介绍了当CLI程序有多种操作或操作模式时，如何在CLI程序中使用子命令。它还解释了如何分层构建子命令，组织帮助文本，以及处理常见的子命令（如help或version）。例如，当你的程序有不同的功能，需要不同的参数或标志时（如git clone或git commit），你应该使用子命令。你还应该提供一个默认的子命令，或者在没有给出子命令时提供一个可用的子命令列表。</p>
<p>业界有许多精心设计的CLI工具的例子，它们都遵循cli准则，大家可以通过使用来深刻体会一下这些准则。下面是一些这样的CLI工具的例子：</p>
<ul>
<li>
<p>httpie：一个命令行HTTP客户端，具有直观的UI，支持JSON，语法高亮，类似wget的下载，插件等功能。例如，Httpie使用清晰简洁的语法进行HTTP请求，支持多种输出格式和颜色，优雅地处理错误并提供有用的文档。</p>
</li>
<li>
<p>git：一个分布式的版本控制系统，让你管理你的源代码并与其他开发者合作。例如，Git使用子命令进行不同的操作（如git clone或git commit），遵循通用的标志（如-v或-verbose），提供有用的反馈和建议（如git status或git help），并支持配置文件和环境变量。</p>
</li>
<li>
<p>npm：一个JavaScript的包管理器，让你为你的项目安装和管理依赖性。例如，NPM使用一个简单的命令结构（npm <command> [args]），提供一个简洁的初始帮助信息，有更详细的选项（npm help npm），支持标签完成和合理的默认值，并允许你通过配置文件（.npmrc）自定义设置。</p>
</li>
</ul>
<h2>八. 小结</h2>
<p>在这篇文章中，我们系统说明了如何编写出遵循命令行接口指南的Go CLI程序。</p>
<p>你学习了如何设置Go环境、设计命令行接口、处理错误和信号、编写文档、使用各种工具和软件包测试和发布程序。你还看到了一些代码和配置文件的例子。通过遵循这些准则和最佳实践，你可以创建一个用户友好、健壮和可靠的CLI程序。</p>
<p>最后我们回顾了clig.dev的指南要点，希望你能更深刻理解这些要点的含义。</p>
<p>我希望你喜欢这篇文章并认为它很有用。如果你有任何问题或反馈，请随时联系我。编码愉快！</p>
<blockquote>
<p>注：本文系与New Bing Chat联合完成，旨在验证如何基于AIGC能力构思和编写长篇文章。文章内容的正确性经过笔者全面审校，可放心阅读。</p>
</blockquote>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>写Go代码时遇到的那些问题[第1期]</title>
		<link>https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/</link>
		<comments>https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/#comments</comments>
		<pubDate>Sat, 13 Jan 2018 03:31:05 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CanonicalImportPath]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[flag]]></category>
		<category><![CDATA[gb]]></category>
		<category><![CDATA[glide]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.4]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[godep]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[kill]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[logrotate]]></category>
		<category><![CDATA[logrus]]></category>
		<category><![CDATA[lumberjack]]></category>
		<category><![CDATA[marshal]]></category>
		<category><![CDATA[microservice]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[unmarshal]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[包管理]]></category>
		<category><![CDATA[命令行程序]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[集中日志]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2521</guid>
		<description><![CDATA[程序员步入“大龄”，写代码的节奏也会受到影响。以前是长时间持续地写，现在写代码的节奏变成了“波浪形”：即写一段时间，歇一段时间。当然这里的“歇”并不是真的歇，而是做其他事情了，比如：回顾、整理与总结。 平时写Go代码，时不时就遇到一些问题，或是写出一些让自己还算满意的代码，这里全部列为“问题”行列。这些“问题”(以及其解决方法)往往比较“小”、比较“碎片”，不适合以自己“擅长”的“长篇”风格写出来分享，也不知道以什么样的“题目”去分享更好，但这样的“问题”在日常又总是会遇到。考量来考量去，赶脚还是用一系列的文章去分享比较合适，即每隔一段时间，积累了一些问题后，就写一篇文章分享一下。 这是第一篇，后续不确定时间地(注意：这不是weekly的哦)发布新篇，直到没啥可写了或不写Go代码了^0^。 一、Go包管理 首当其冲的是Go包管理。 1. vendor的“传染性”带来的问题 Go从1.5版本开始引入vendor机制以辅助Go的包管理。随着vendor机制的应用日益广泛，我们会发现：有些时候你要是不用vendor（在不借助第三方包管理工具的前提下），很多编译问题是解决不了的，或者说vendor机制有一定的传染性。比如下面这个例子： 如上图所示：app_c包直接调用lib_a包中函数，并使用了lib_b包(v0.2版本)中的类型，lib_a包vendor了lib_b包(v0.1版本)。在这样的情况下，当我们编译app_c包时，是否会出现什么问题呢？我们一起来看一下这个例子： 在$GOPATH/src路径下面我们查看当前示例的目录结构： $tree ├── app_c ├── c.go ├── lib_a ├── a.go └── vendor └── lib_b └── b.go ├── lib_b ├── b.go 各个源文件的示例代码如下： //lib_a/a.go package lib_a import "lib_b" func Foo(b lib_b.B) { b.Do() } //lib_a/vendor/lib_b/b.go package lib_b import "fmt" type B struct { } func (*B) Do() { [...]]]></description>
			<content:encoded><![CDATA[<p>程序员步入“大龄”，写代码的节奏也会受到影响。以前是长时间持续地写，现在写代码的节奏变成了“波浪形”：即写一段时间，歇一段时间。当然这里的“歇”并不是真的歇，而是做其他事情了，比如：回顾、整理与总结。</p>
<p>平时写<a href="http://tonybai.com/tag/go">Go</a>代码，时不时就遇到一些问题，或是写出一些让自己还算满意的代码，这里全部列为“问题”行列。这些“问题”(以及其解决方法)往往比较“小”、比较“碎片”，不适合以自己“擅长”的“长篇”风格写出来分享，也不知道以什么样的“题目”去分享更好，但这样的“问题”在日常又总是会遇到。考量来考量去，赶脚还是用一系列的文章去分享比较合适，即每隔一段时间，积累了一些问题后，就写一篇文章分享一下。</p>
<p>这是第一篇，后续不确定时间地(注意：这不是weekly的哦)发布新篇，直到没啥可写了或不写<a href="https://golang.org/">Go代码</a>了^0^。</p>
<h2>一、Go包管理</h2>
<p>首当其冲的是<a href="http://tonybai.com/2017/06/08/first-glimpse-of-dep/">Go包管理</a>。</p>
<h3>1. vendor的“传染性”带来的问题</h3>
<p>Go从<a href="http://tonybai.com/2015/07/10/some-changes-in-go-1-5/">1.5版本</a>开始引入<a href="http://tonybai.com/2015/07/31/understand-go15-vendor/">vendor机制</a>以辅助Go的包管理。随着<a href="http://tonybai.com/2015/07/31/understand-go15-vendor/">vendor</a>机制的应用日益广泛，我们会发现：有些时候你要是不用vendor（在不借助第三方包管理工具的前提下），很多编译问题是解决不了的，或者说<strong>vendor机制有一定的传染性</strong>。比如下面这个例子：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/1st-issue/writing-go-code-issue-1st-demo-1.png" alt="img{512x368}" /></p>
<p>如上图所示：app_c包直接调用lib_a包中函数，并使用了lib_b包(v0.2版本)中的类型，lib_a包vendor了lib_b包(v0.1版本)。在这样的情况下，当我们编译app_c包时，是否会出现什么问题呢？我们一起来看一下这个例子：</p>
<pre><code>在$GOPATH/src路径下面我们查看当前示例的目录结构：

$tree
├── app_c
    ├── c.go
├── lib_a
    ├── a.go
    └── vendor
        └── lib_b
            └── b.go
├── lib_b
    ├── b.go
</code></pre>
<p>各个源文件的示例代码如下：</p>
<pre><code>//lib_a/a.go
package lib_a

import "lib_b"

func Foo(b lib_b.B) {
    b.Do()
}

//lib_a/vendor/lib_b/b.go

package lib_b

import "fmt"

type B struct {
}

func (*B) Do() {
    fmt.Println("lib_b version:v0.1")
}

// lib_b/b.go
package lib_b

import "fmt"

type B struct {
}

func (*B) Do() {
    fmt.Println("lib_b version:v0.2")
}

// app_c/c.go
package app_c

import (
    "lib_a"
    "lib_b"
)

func main() {
    var b lib_b.B
    lib_a.Foo(b)
}

</code></pre>
<p>进入app_c目录，执行编译命令：</p>
<pre><code>$go build c.go
# command-line-arguments
./c.go:10:11: cannot use b (type "lib_b".B) as type "lib_a/vendor/lib_b".B in argument to lib_a.Foo
</code></pre>
<p>我们看到go compiler认为：<strong>app_c包main函数中定义的变量b的类型(lib_b.B)与lib_a.Foo的参数b的类型(lib_a/vendor/lib_b.B)是不同的类型，不能相互赋值</strong>。</p>
<h3>2. 通过手工vendor解决上述问题</h3>
<p>这个例子非常有代表性，那么怎么解决这个问题呢？<strong>我们需要在app_c中也使用vendor机制</strong>，即将app_c所需的lib_a和lib_b都vendor到app_c中。</p>
<pre><code>按照上述思路解决后的示例的目录结构：

$tree
├── app_c
    ├── c.go
    └── vendor
        ├── lib_a
        │   └── a.go
        └── lib_b
            └── b.go
├── lib_a
    ├── a.go
    └── vendor
        └── lib_b
            └── b.go
├── lib_b
    ├── b.go
</code></pre>
<p>不过要注意的是：app_c/vendor下面的库中的vendor目录要被删除掉的，我们只保留顶层vendor。现在我们再来编译c.go就可以顺利编译通过了。</p>
<h3>3. 使用dep</h3>
<p>对于demo或规模不大、依赖不多的小项目，手工进行vendor还是蛮有效的。一个可行的手工vendor步骤：</p>
<ul>
<li>在项目顶层创建vendor；</li>
<li>通过go list -json ./&#8230;查看项目依赖 “deps”;</li>
<li>逐一下载各个依赖，并确定要使用的版本(tag or branch)，将特定版本cp到顶层的vendor目录下，至少要做到vendor所有直接依赖包；</li>
<li>可以在顶层vendor下创建dependencies.list文件，手工记录vendor的依赖包列表以及版本信息。</li>
</ul>
<p>但是对于稍大一点的项目，手工vendor就会费时费力，有时仅能顾及到“直接依赖包”的vendor，“数不清”的间接依赖/传递依赖会让你头疼不已。这个时候我们会想到使用第三方的包管理工具。在现在这个时间点，如果你再和我提<a href="http://tonybai.com/2014/10/30/a-hole-of-godep/">godep</a>、<a href="https://github.com/Masterminds/glide">glide</a>等，那你就out了，<a href="https://github.com/golang/dep">dep</a>是首选。</p>
<p>在<a href="http://tonybai.com/2017/06/08/first-glimpse-of-dep/">《初窥dep》</a>一文中，我们对当时的dep进行了较为详细的工作机制分析，如今dep已经演化到0.3.2版本了，并且commandline交互接口已经稳定了。dep init默认采用network mode，即到各个依赖包的upstream上查找版本信息并下载；dep init也支持-gopath模式，即在本地$GOPATH下获取依赖包的元信息并分析。</p>
<p>不过，对于在国内的gopher，dep init的过程依然是一道很难逾越的“坎”。问题多出在：第三方包特别喜欢依赖的golang.org/x下的那些包，常见的包有：net、text、crypto等。golang.org/x/{package_name}仅仅是<a href="http://tonybai.com/2014/11/04/some-changes-in-go-1-4/">canonical import path</a>，真正的代码存储在go.googlesource.com上，而在国内get这些包，我们会得到如下错误：</p>
<pre><code>$go get -u golang.org/x/net
package golang.org/x/net: unrecognized import path "golang.org/x/net" (https fetch: Get https://golang.org/x/net?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
</code></pre>
<p>这将导致dep init命令长期阻塞，给国内gopher带来极为糟糕的体验。更为糟糕的是，即便是采用了一些fan qiang方式，有些时候go.googlesource.com依旧无法连接。因此，我一般的作法是在国外的主机上进行dep init，然后将vendor checkin到代码仓库中。这样其他人在得到你的代码后，也不需dep ensure(也要下载依赖包)即可实现reproducable build。</p>
<p>有些朋友可能会将从github.com/golang上下载的net包来代替golang.org/x/net，并使用dep init -v -gopath=true的模式。但这种替换会被dep分析出来，因为dep会尝试去读取代码库的元信息，结果依然会是失败。</p>
<h2>二. 非容器化应用的本地日志管理</h2>
<p>在<a href="http://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/">微服务</a>、容器化大行其道的今天，单个应用的日志处理变得简单化了，应用只需要将要输出的信息输出到stdout、stderr上即可。<a href="http://tonybai.com/2017/03/03/implement-kubernetes-cluster-level-logging-with-fluentd-and-elasticsearch-stack/">logging基础设施</a>会收集容器日志，并做后续归档、分析、过滤、查找、展示等处理。但是在非容器环境、在没有统一的logging基础设施的前提下，日志的管理就又交还给应用自身了。浅显的日志管理至少要包含日志的rotate(轮转)、压缩归档以及历史归档文件的处理吧。这里我们就来探讨一下这个问题的几种解决方法。</p>
<h3>1.  托管给logrotate</h3>
<p>在主流的Linux发行版上都有一个<a href="https://github.com/logrotate/logrotate">logrotate</a>工具程序，应用程序可以借助该工具对应用输出的日志进行rotate、压缩、归档和删除历史归档日志，这样可大幅简化应用的日志输出逻辑，应用仅需要将日志输出到一个具名文件中即可，其余都交给logrotate处理。</p>
<p>我们建立一个输出log的demo app:</p>
<pre><code>//testlogrotate.go

package main

import (
    "log"
    "os"
    "time"
)

func main() {
    file, err := os.OpenFile("./app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalln("Failed to open log file:", err)
    }
    defer file.Close()

    logger := log.New(file,
        "APP_LOG_PREFIX: ",
        log.Ldate|log.Ltime|log.Lshortfile)

    for {
        logger.Println("test log")
        time.Sleep(time.Second * 1)
    }
}

</code></pre>
<p>该程序每隔1s向app.log文件写入一行日志。</p>
<pre><code># tail -f app.log
APP_LOG_PREFIX: 2018/01/12 19:14:43 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:44 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:45 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:46 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:47 testlogrotate.go:22: test log
... ..
</code></pre>
<p>接下来，我们就要用logrotate对该app.log文件进行定期的rotate、压缩归档以及历史归档清理了，我们需要为app.log定制一个配置。logrotate读取配置的目录是/etc/logrotate.d，我们在/etc/logrotate.d下面建立applog文件(当然你也可以在任意其他目录下建立配置文件，不过其他目录下的配置文件无法被logrotate的cron任务感知到，不过这样的配置文件可以手工与logrotate程序结合使用)，文件内容如下：</p>
<pre><code># cat /etc/logrotate.d/applog

/data/tonybai/test/go/app.log {
  rotate 7
  daily
  size=10M
  compress
  dateext
  missingok
  copytruncate
}

</code></pre>
<p>这个配置的大致含义是：<br />
 * 每天rotate一次<br />
 * 日志保留7天(rotate=7, daily rotate)<br />
 * 归档日志采用压缩形式<br />
 * 归档日志带有时间戳<br />
 * 当当前日志size > 10M时，会进行一次rotate<br />
 * 最重要的是copytruncate这个配置，这个配置的含义是将app.log当前日志copy到一个归档文件后，对app.log进行truncate操作，这样app.log的open file fd并不改变，不会影响到原app继续写日志。当然这个copy的过程中可能会有少量日志lost。</p>
<p>如果你觉得logrotate在时间粒度和精确度上依旧无法满足你的要求，你可以结合crontab自己定时执行logrotate(crontab -e编辑crontab的配置)：</p>
<pre><code># logrotate -f /etc/logrotate.d/applog
</code></pre>
<p>下面是rotate时，tail -f中看到的情况：</p>
<pre><code>APP_LOG_PREFIX: 2018/01/12 20:25:59 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:00 testlogrotate.go:21: test log
tail: app.log: file truncated
APP_LOG_PREFIX: 2018/01/12 20:26:01 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:02 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:03 testlogrotate.go:21: test log
</code></pre>
<p>可以看到tail可以检测到file truncate事件。</p>
<h3>2.  使用自带rotate功能log包</h3>
<p>在go技术栈中<a href="https://github.com/avelino/awesome-go#logging">众多的logging包</a>中，<a href="https://github.com/sirupsen/logrus">logrus</a>是使用较为广泛的一个包，支持与std库 log API兼容的结构化日志、支持logging level设置、支持安全地并发写日志以及hook等。但logrus自身并不具备auto rotate功能，需要结合其他工具才能实现。这里用<a href="https://npf.io/">nate finch</a>的<a href="https://github.com/natefinch/lumberjack">lumberjack</a>，我们来看一个简单的例子：</p>
<pre><code>// testlogrusAndlumberjack.go

package main

import (
    "time"

    "github.com/natefinch/lumberjack"
    log "github.com/sirupsen/logrus"
)

func main() {
    logger := log.New()
    logger.SetLevel(log.DebugLevel)
    logger.Formatter = &amp;log.JSONFormatter{}

    logger.Out = &amp;lumberjack.Logger{
        Filename:   "./app.log",
        MaxSize:    1, // megabytes
        MaxBackups: 3,
        MaxAge:     1,    //days
        Compress:   true, // disabled by default
        LocalTime:  true,
    }

    for {
        logger.Debug("this is an app log")
        time.Sleep(2 * time.Millisecond)
    }
}

</code></pre>
<p>从代码里，我们看到：通过设置logger.Out为一个lumberjack.Logger的实例，将真正的Write交给了lumberjack.Logger，而后者实现了log的rotate功能，与logrotate的配置有些类似，这里也包括日志最大size设定、保留几个归档日志、是否压缩、最多保留几天的日志。不过当前lumberjack实现的rotate判断条件仅有一个：MaxSize，而没有定时rotate的功能。</p>
<p>我们执行一下该程序，等待一会，并停止程序。可以看到目录下的日志文件发生了变化：</p>
<pre><code>$ls -lh
-rw-r--r--  1 tony  staff   3.7K Jan 12 21:03 app-2018-01-12T21-03-42.844.log.gz
-rw-r--r--  1 tony  staff   3.7K Jan 12 21:04 app-2018-01-12T21-04-15.017.log.gz
-rw-r--r--  1 tony  staff   457K Jan 12 21:04 app.log
</code></pre>
<p>lumberjack每发现app.log大于MaxSize就会rotate一次，这里已经有了两个归档压缩文件，并被lumberjack赋予了时间戳和序号，便于检索和查看。</p>
<h3>3. 关于对日志level的支持以及loglevel的热更新</h3>
<p>对日志level的支持是logging包选项的一个重要参考要素。logrus支持设置六个log level：</p>
<pre><code>    PanicLevel
    FatalLevel
    ErrorLevel
    WarnLevel
    InfoLevel
    DebugLevel
</code></pre>
<p>并且对不同的leve的日志，logrus支持设定hook分别处理，比如：放到不同的日志文件中。通过logrus.Logger.SetLevel方法可以运行时更新logger实例的loglevel，这个特性可以让我们在生产环境上通过临时打开debuglevel日志对程序进行更细致的观察，以定位问题，快速定位bug，非常实用。</p>
<p>结合<a href="http://tonybai.com/2012/09/21/signal-handling-in-go/">系统Signal</a>机制，我们可以通过USR1和USR2两个signal来运行时调整程序的日志级别，我们来看一个示例：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/1st-issue/writing-go-code-issue-1st-demo-2.png" alt="img{512x368}" /></p>
<p>从上面图片可以看到，日志级别从高到低分别为：Panic, Fatal, Error, Warn，Info和Debug。如果要调高log level，我们向程序发送USR1来调高日志级别，相反，发送USR2来调低日志级别：</p>
<p>我们在testlogrusAndlumberjack.go上面做些修改：增加对signal: USR1和USR2的监听处理，同时循环打印各种级别日志，以后续验证日志级别的动态调整：</p>
<pre><code>// testloglevelupdate.go

import (
    log "github.com/sirupsen/logrus"
    ... ...
)

func main() {
    logger := log.New()
    logger.SetLevel(log.DebugLevel)
    logger.Formatter = &amp;log.JSONFormatter{}

    logger.Out = &amp;lumberjack.Logger{
        Filename:   "./app.log",
        MaxSize:    1, // megabytes
        MaxBackups: 3,
        MaxAge:     1,    //days
        Compress:   true, // disabled by default
        LocalTime:  true,
    }

    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2)
    go watchAndUpdateLoglevel(c, logger)

    for {
        logger.Debug("it is debug level log")
        logger.Info("it is info level log")
        logger.Warn("it is warning level log")
        logger.Error("it is warning level log")
        time.Sleep(5 * time.Second)
    }
}

</code></pre>
<p>watchAndUpdateLoglevel函数用于监听程序收到的系统信号，并根据信号类型调整日志级别：</p>
<pre><code>// testloglevelupdate.go
func watchAndUpdateLoglevel(c chan os.Signal, logger *log.Logger) {
    for {
        select {
        case sig := &lt;-c:
            if sig == syscall.SIGUSR1 {
                level := logger.Level
                if level == log.PanicLevel {
                    fmt.Println("Raise log level: It has been already the most top log level: panic level")
                } else {
                    logger.SetLevel(level - 1)
                    fmt.Println("Raise log level: the current level is", logger.Level)
                }

            } else if sig == syscall.SIGUSR2 {
                level := logger.Level
                if level == log.DebugLevel {
                    fmt.Println("Reduce log level: It has been already the lowest log level: debug level")
                } else {
                    logger.SetLevel(level + 1)
                    fmt.Println("Reduce log level: the current level is", logger.Level)
                }

            } else {
                fmt.Println("receive unknown signal:", sig)
            }
        }
    }
}

</code></pre>
<p>运行该程序后，你可以通过如下命令向程序发送信号：</p>
<pre><code>$ kill -s USR1|USR2 程序的进程号
</code></pre>
<p>通过日志的输出，可以判断出日志级别调整是否生效，这里就不细说了。</p>
<p>不过这里还要提一点的是logrus目前对于输出的日志中双引号内的一些字符（比如双引号自身）会做转义处理，即在前面加上“反斜杠”，比如：</p>
<pre><code>{"level":"debug","msg":"receive a msg: {\"id\":\"000002\",\"ip\":\"201.108.111.117\"}","time":"2018-01-11T20:42:31+08:00"}
</code></pre>
<p>这个问题让日志可读性大幅下降，但这个问题似乎尚处于无解状态</p>
<h2>三. json marshal json string时的转义问题</h2>
<p>之前写过这样一个function，用于统一marshal内部组件通信的应答消息：</p>
<pre><code>func marshalResponse(code int, msg string, result interface{}) (string, error) {
    m := map[string]interface{}{
        "code":   0,
        "msg":    "ok",
        "result": result,
    }

    b, err := json.Marshal(&amp;m)
    if err != nil {
        return "", err
    }

    return string(b), nil
}
</code></pre>
<p>不过当result类型为json string时，这个函数的输出带有转义反斜线：</p>
<pre><code>//testmarshaljsonstring.go
... ...
func main() {
    s, err := marshalResponse(0, "ok", `{"name": "tony", "city": "shenyang"}`)
    if err != nil {
        fmt.Println("marshal response error:", err)
        return
    }
    fmt.Println(s)
}
</code></pre>
<p>运行这个程序输出：</p>
<pre><code>{"code":0,"msg":"ok","result":"{\"name\": \"tony\", \"city\": \"shenyang\"}"}
</code></pre>
<p>怎么解决掉这个问题呢？json提供了一种RawMessage类型，本质上就是[]byte，我们将json string转换成RawMessage后再传给json.Marshal就可以解决掉这个问题了：</p>
<pre><code>//testmarshaljsonstring.go
func marshalResponse1(code int, msg string, result interface{}) (string, error) {
    s, ok := result.(string)
    var m = map[string]interface{}{
        "code": 0,
        "msg":  "ok",
    }

    if ok {
        rawData := json.RawMessage(s)
        m["result"] = rawData
    } else {
        m["result"] = result
    }

    b, err := json.Marshal(&amp;m)
    if err != nil {
        return "", err
    }

    return string(b), nil
}

func main() {
    s, err = marshalResponse1(0, "ok", `{"name": "tony", "city": "shenyang"}`)
    if err != nil {
        fmt.Println("marshal response1 error:", err)
        return
    }
    fmt.Println(s)
}
</code></pre>
<p>再运行这个程序的输出结果就变成了我们想要的结果了：</p>
<pre><code>{"code":0,"msg":"ok","result":{"name":"tony","city":"shenyang"}}
</code></pre>
<h2>四. 如何在main包之外使用flag.Parse后的命令行flag变量</h2>
<p>我们在使用Go开发交互界面不是很复杂的command-line应用时，一般都会使用std中的flag包进行命令行flag解析，并在main包中校验和使用flag.Parse后的flag变量。常见的套路是这样的：</p>
<pre><code>//testflag1.go
package main

import (
    "flag"
    "fmt"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&amp;endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&amp;user, "user", "", "etcdv3 client user")
    flag.StringVar(&amp;password, "password", "", "etcdv3 client password")
}

func usage() {
    fmt.Println("flagdemo-app is a daemon application which provides xxx service.\n")
    fmt.Println("Usage of flagdemo-app:\n")
    fmt.Println("\t flagdemo-app [options]\n")
    fmt.Println("The options are:\n")

    flag.PrintDefaults()
}

func main() {
    flag.Usage = usage
    flag.Parse()

   // ... ...
   // 这里我们可以使用endpoints、user、password等flag变量了
}

</code></pre>
<p>在这样的一个套路中，我们可以在main包中直接使用flag.Parse后的flag变量了。但有些时候，我们需要在main包之外使用这些flag vars(比如这里的：endpoints、user、password)，怎么做呢，有几种方法，我们逐一来看看。</p>
<h3>1. 全局变量法</h3>
<p>我想大部分gopher第一个想法就是使用全局变量，即建立一个config包，包中定义全局变量，并在main中将这些全局变量绑定到flag的Parse中：</p>
<pre><code>$tree globalvars
globalvars
├── config
│   └── config.go
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/globalvars/config/config.go

package config

var (
    Endpoints string
    User      string
    Password  string
)

// flag-demo/globalvars/etcd/etcd.go
package etcd

import (
    "fmt"

    "../config"
)

func EtcdProxy() {
    fmt.Println(config.Endpoints, config.User, config.Password)
    //... ....
}

// flag-demo/globalvars/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./config"
    "./etcd"
)

func init() {
    flag.StringVar(&amp;config.Endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&amp;config.User, "user", "", "etcdv3 client user")
    flag.StringVar(&amp;config.Password, "password", "", "etcdv3 client password")
}

.... ...

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy()

    time.Sleep(5 * time.Second)
}

</code></pre>
<p>可以看到，我们在绑定cmdline flag时使用的是config包中定义的全局变量。并且在另外一个etcd包中，使用了这些变量。</p>
<p>我们运行这个程序：</p>
<pre><code>./main -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123
192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123
</code></pre>
<p>不过这种方法要<strong>注意这些全局变量值在Go包初始化过程的顺序</strong>，比如：如果在etcd包的init函数中使用这些全局变量，那么你得到的各个变量值将为空值，因为etcd包的init函数在main.init和main.main之前执行，这个时候绑定和Parse都还未执行。</p>
<h3>2. 传参法</h3>
<p>第二种比较直接的想法就是将Parse后的flag变量以参数的形式、以某种init的方式传给其他要使用这些变量的包。</p>
<pre><code>$tree parampass
parampass
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/parampass/etcd/etcd.go
package etcd
... ...

func EtcdProxy(endpoints, user, password string) {
    fmt.Println(endpoints, user, password)
}

// flag-demo/parampass/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./etcd"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&amp;endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&amp;user, "user", "", "etcdv3 client user")
    flag.StringVar(&amp;password, "password", "", "etcdv3 client password")
}

... ...

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy(endpoints, user, password)

    time.Sleep(5 * time.Second)
}

</code></pre>
<p>这种方法非常直观，这里就不解释了。但注意：一旦使用这种方式，一定需要在main包与另外的包之间建立某种依赖关系，至少main包会import那些使用flag变量的包。</p>
<h3>3. 配置中心法</h3>
<p>全局变量法直观，而且一定程度上解除了其他包与main包的耦合。但是有一个问题，那就是一旦flag变量发生增减，config包就得相应添加或删除变量定义。是否有一种方案可以在flag变量发生变化时，config包不受影响呢？我们可以用配置中心法。所谓的配置中心法，就是实现一个与flag变量类型和值无关的通过配置存储结构，我们在main包中向该结构注入parse后的flag变量，在其他需要flag变量的包中，我们使用该结构得到flag变量的值。</p>
<pre><code>$tree configcenter
configcenter
├── config
│   └── config.go
└── main.go

//flag-demo/configcenter/config/config.go
package config

import (
    "log"
    "sync"
)

var (
    m  map[string]interface{}
    mu sync.RWMutex
)

func init() {
    m = make(map[string]interface{}, 10)
}

func SetString(k, v string) {
    mu.Lock()
    m[k] = v
    mu.Unlock()
}

func SetInt(k string, i int) {
    mu.Lock()
    m[k] = i
    mu.Unlock()
}

func GetString(key string) string {
    mu.RLock()
    defer mu.RUnlock()
    v, ok := m[key]
    if !ok {
        return ""
    }
    return v.(string)
}

func GetInt(key string) int {
    mu.RLock()
    defer mu.RUnlock()
    v, ok := m[key]
    if !ok {
        return 0
    }
    return v.(int)
}

func Dump() {
    log.Println(m)
}

// flag-demo/configcenter/main.go

package main

import (
    "flag"
    "fmt"
    "time"

    "./config"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&amp;endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&amp;user, "user", "", "etcdv3 client user")
    flag.StringVar(&amp;password, "password", "", "etcdv3 client password")
}
... ...
func main() {
    flag.Usage = usage
    flag.Parse()

    // inject flag vars to config center
    config.SetString("endpoints", endpoints)
    config.SetString("user", user)
    config.SetString("password", password)

    time.Sleep(5 * time.Second)
}

</code></pre>
<p>我们在main中使用config的SetString将flag vars注入配置中心。之后，我们在其他包中就可以使用：GetString、GetInt获取这些变量值了，这里就不举例了。</p>
<h3>4、“黑魔法”: flag.Lookup</h3>
<p>flag包中提供了一种类似上述的”配置中心”的机制，但这种机制不需要我们显示注入“flag vars”了，我们只需按照flag提供的方法在其他package中读取对应flag变量的值即可。</p>
<pre><code>$tree flaglookup
flaglookup
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/flaglookup/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./etcd"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&amp;endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&amp;user, "user", "", "etcdv3 client user")
    flag.StringVar(&amp;password, "password", "", "etcdv3 client password")
}

......

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy()

    time.Sleep(5 * time.Second)
}

// flag-demo/flaglookup/etcd/etcd.go
package etcd

import (
    "flag"
    "fmt"
)

func EtcdProxy() {
    endpoints := flag.Lookup("endpoints").Value.(flag.Getter).Get().(string)
    user := flag.Lookup("user").Value.(flag.Getter).Get().(string)
    password := flag.Lookup("password").Value.(flag.Getter).Get().(string)

    fmt.Println(endpoints, user, password)
}

</code></pre>
<p>运行该程序：</p>
<pre><code>$go run main.go -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123
192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123
</code></pre>
<p>输出与我们的预期是一致的。</p>
<h3>5、对比</h3>
<p>我们用一幅图来对上述几种方法进行对比：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/1st-issue/writing-go-code-issue-1st-demo-3.png" alt="img{512x368}" /></p>
<p>很显然，经过简单包装后，“黑魔法”flaglookup应该是比较优异的方案。main包、other packages只需import flag即可。</p>
<p>注意：<strong>在main包中定义exported的全局flag变量并被其他package import的方法是错误的，很容易造成import cycle问题。并且任何其他package import main包都是不合理的</strong>。</p>
<h2>五. 小结</h2>
<p>以上是这段时间遇到的、收集的一些Go问题以及solution。注意：<strong>这些solution不一定是最优方案哦！如果您有更好方案，欢迎批评指正和互动交流</strong>。</p>
<p>本文章中涉及到的所有源码和配置文件在<a href="https://github.com/bigwhite/experiments/tree/master/writing-go-code-issues/1st-issue">这里</a>可以下载到。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>docker容器内服务程序的优雅退出</title>
		<link>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/</link>
		<comments>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/#comments</comments>
		<pubDate>Thu, 09 Oct 2014 13:58:49 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BestPractice]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[Dockerfile]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[nsenter]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[supervisor]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[信号]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[命令行]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[映像]]></category>
		<category><![CDATA[最佳实践]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[虚拟化]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1555</guid>
		<description><![CDATA[近期在试验如何将我们的产品部署到docker容器中去，这其中涉及到一个技术环节，那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出，指的就是程序在退出前有清理资源（比如关闭文件描述符、关闭socket），保存必要中间状态，持久化内存数据 （比如将内存中的数据flush到文件中）的机会。docker作为目前最火的轻量级虚拟化技术，其在后台服务领域的应用是极其广泛的，其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。 一、优雅退出的原理 对于服务程序而言，一般都是以daemon形式运行在后台的。通知这些服务程序退出需要使用到系统的signal机制。一般服务程序都会监听某个 特定的退出signal，比如SIGINT、SIGTERM等（通过kill -l命令你可以查看到几十种signal）。当我们使用kill + 进程号时，系统会默认发送一个SIGTERM给相应的进程。该进程通过signal handler响应这一信号，并在这个handler中完成相应的&#8220;优雅退出&#8221;操作。 与&#8220;优雅退出&#8221;对立的是&#8220;暴力退出&#8221;，也就是我们常说的使用kill -9，也就是kill -s SIGKILL + 进程号，这个行为不会给目标进程任何时间空隙，而是直接将进程杀死，无论进程当前在做何种操作。这种操作常常导致&#8220;不一致&#8221;状态的出现。SIGKILL这 个信号比较特殊，进程无法有效监听该信号，无法有效针对该信号设置handler，无法改变其信号的默认处理行为。 二、测试用&#8220;服务程序&#8221; 为了测试docker容器对优雅退出的支持，我们编写如下&#8220;服务程序&#8221;用于放在docker容器中运行： //dockerapp1.go package main import &#34;fmt&#34; import &#34;time&#34; import &#34;os&#34; import &#34;os/signal&#34; import &#34;syscall&#34; type signalHandler func(s os.Signal, arg interface{}) type signalSet struct { &#160;&#160;&#160;&#160;&#160;&#160;&#160; m map[os.Signal]signalHandler } func signalSetNew() *signalSet { &#160;&#160;&#160;&#160;&#160;&#160;&#160; ss := new(signalSet) [...]]]></description>
			<content:encoded><![CDATA[<p><span style="line-height: 1.6em;">近期在试验如何将我们的产品部署到</span><a href="http://docker.com" style="line-height: 1.6em;">docker容器</a><span style="line-height: 1.6em;">中去，这其中涉及到一个技术环节，那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出，指的就是程序在退出前有清理资源（比如关闭文件描述符、关闭socket），保存必要中间状态，持久化内存数据 （比如将内存中的数据flush到文件中）的机会。docker作为目前最火的轻量级虚拟化技术，其在后台服务领域的应用是极其广泛的，其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。</span></p>
<p><b>一、优雅退出的原理</b></p>
<p>对于服务程序而言，一般都是以daemon形式运行在后台的。通知这些服务程序退出需要使用到系统的signal机制。一般服务程序都会监听某个 特定的退出signal，比如SIGINT、SIGTERM等（通过kill -l命令你可以查看到几十种signal）。当我们使用kill + 进程号时，系统会默认发送一个SIGTERM给相应的进程。该进程通过signal handler响应这一信号，并在这个handler中完成相应的&ldquo;优雅退出&rdquo;操作。</p>
<p>与&ldquo;优雅退出&rdquo;对立的是&ldquo;暴力退出&rdquo;，也就是我们常说的使用kill -9，也就是kill -s SIGKILL + 进程号，这个行为不会给目标进程任何时间空隙，而是直接将进程杀死，无论进程当前在做何种操作。这种操作常常导致&ldquo;不一致&rdquo;状态的出现。SIGKILL这 个信号比较特殊，进程无法有效监听该信号，无法有效针对该信号设置handler，无法改变其信号的默认处理行为。</p>
<p><b>二、</b><b>测试用&ldquo;服务程序&rdquo;</b></p>
<p>为了测试docker容器对优雅退出的支持，我们编写如下&ldquo;服务程序&rdquo;用于放在docker容器中运行：</p>
<p><font face="Courier New">//dockerapp1.go</font></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;<br />
	import &quot;time&quot;<br />
	import &quot;os&quot;<br />
	import &quot;os/signal&quot;<br />
	import &quot;syscall&quot;</font></p>
<p><font face="Courier New">type signalHandler func(s os.Signal, arg interface{})</font></p>
<p><font face="Courier New">type signalSet struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m map[os.Signal]signalHandler<br />
	}</font></p>
<p><font face="Courier New">func signalSetNew() *signalSet {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := new(signalSet)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.m = make(map[os.Signal]signalHandler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return ss<br />
	}</font></p>
<p><font face="Courier New">func (set *signalSet) register(s os.Signal, handler signalHandler) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if _, found := set.m[s]; !found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[s] = handler<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func (set *signalSet) handle(sig os.Signal, arg interface{}) (err error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if _, found := set.m[sig]; found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[sig](sig, arg)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return fmt.Errorf(&quot;No handler available for signal %v&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; panic(&quot;won&#39;t reach here&quot;)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go sysSignalHandleDemo()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the main goroutine wait!<br />
	}</font></p>
<p><font face="Courier New">func sysSignalHandleDemo() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := signalSetNew()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;handle signal: %v\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;signal termiate received, app exit normally\n&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGINT, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR1, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR2, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGTERM, handler)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan os.Signal)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var sigs []os.Signal<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for sig := range ss.m {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigs = append(sigs, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signal.Notify(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sig := &lt;-c</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := ss.handle(sig, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;unknown signal received: %v, app exit unexpectedly\n&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>关于<a href="http://tonybai.com/tag/golang">Go语言</a>对系统Signal的处理，可以参考《<a href="http://tonybai.com/2012/09/21/signal-handling-in-go/">Go中的系统Signal处理</a>》一文。</p>
<p><b>三、制作测试用docker image</b></p>
<p>在《 <a href="http://tonybai.com/2014/09/26/install-docker-on-ubuntu-server-1404/">Ubuntu Server 14.04安装docker</a>》一文中，我们完成了在ubuntu 14.04上安装docker的步骤。要制作测试用docker image，我们首先需要pull一个base image。我们以CentOS6.5为例：</p>
<p>在Ubuntu 14.04上执行：<br />
	&nbsp;&nbsp;&nbsp; <font face="Courier New">sudo&nbsp; docker pull centos:centos6</font></p>
<p>docker会自动从<a href="https://registry.hub.docker.com">官方仓库</a>下载一个制作好的docker image。下载成功后，我们可以run一下试试，像这样：</p>
<p><font face="Courier New">$&gt; sudo docker run -t -i centos:centos6 /bin/bash</font></p>
<p>我们查看一下CentOS6的小版本：<br />
	<font face="Courier New">$&gt; cat /etc/centos-release<br />
	CentOS release 6.5 (Final)</font></p>
<p>这是一个极其精简的CentOS，各种工具均未安装：<br />
	<font face="Courier New">bash-4.1# telnet<br />
	bash: telnet: command not found<br />
	bash-4.1# ssh<br />
	bash: ssh: command not found<br />
	bash-4.1# ftp<br />
	bash: ftp: command not found<br />
	bash-4.1# echo $PATH<br />
	/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</font></p>
<p>如果你要安装一些必要的工具，可以直接使用yum install，默认的base image已经将yum配置好了，可以直接使用。如果通过公司代理访问外部网络，别忘了先export http_proxy。另外docker直接使用宿主机的/etc/resolv.conf作为容器的DNS，我们也无需额外设置DNS。</p>
<p>接下来，我们就制作我们的第一个测试用image。安装官方推荐的Best Practice，我们使用Dockerfile来bulid一个测试用image。步骤如下：</p>
<p>- 建立~/ImagesFactory目录<br />
	- 将构建好的dockerapp1拷贝到~/ImagesFactory目录下<br />
	- 进入~/ImagesFactory目录，创建Dockerfile文件，Dockerfile内容如下：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	COPY ./dockerapp1 /bin<br />
	CMD /bin/dockerapp1</font></p>
<p>- 执行docker build，结果如下：</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v1&quot; ./<br />
	Sending build context to Docker daemon 7.496 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp1 /bin<br />
	2014/10/09 16:05:25 lchown /var/lib/docker/aufs/mnt/fb0e864d3f07ca17ef8b6b69f034728e1f1158fd3f9c83fa48243054b2f26958/bin/dockerapp1: not a directory</font></p>
<p>居然build失败，提示什么not a directory。于是各种Search，终于发现问题所在，原来是&ldquo;<font face="Courier New">COPY ./dockerapp1 /bin</font>&rdquo;这条命令错了，少了个&ldquo;/&rdquo;，将&quot; /bin&quot;改为&ldquo;/bin/&rdquo;就OK了，Docker真是奇怪啊，这块明显应该做得更兼容些。新的Dockerfile如下：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	COPY ./dockerapp1 /bin/<br />
	CMD /bin/dockerapp1</font></p>
<p>构建结果如下：</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v1&quot; ./<br />
	Sending build context to Docker daemon 7.496 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp1 /bin/<br />
	&nbsp;&#8212;&gt; 20c3783c42ab<br />
	Removing intermediate container cab639ab4321<br />
	Step 3 : CMD /bin/dockerapp1<br />
	&nbsp;&#8212;&gt; Running in 31875d3c37f9<br />
	&nbsp;&#8212;&gt; 21a720a808a7<br />
	Removing intermediate container 31875d3c37f9<br />
	Successfully built 21a720a808a7</font></p>
<p><font face="Courier New">$ sudo docker images<br />
	REPOSITORY&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TAG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; VIRTUAL SIZE<br />
	test&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; v1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 21a720a808a7&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 59 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 214.6 MB</font></p>
<p><b>四、第一个测试容器</b></p>
<p>我们基于image &quot;test:v1&quot;启动一个测试容器：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v1&quot;<br />
	daf3ae88fec23a31cde9f6b9a3f40057953c87b56cca982143616f738a84dcba</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	daf3ae88fec2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 17 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 16 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; condescending_sammet&nbsp;&nbsp; </font></p>
<p>通过docker run命令，我们基于image&quot;test:v1&quot;启动了一个容器。通过docker ps命令可以看到容器成功启动，容器id：<font face="Courier New">daf3ae88fec2，别名为：</font><font face="Courier New">condescending_sammet。</font></p>
<p><font face="Courier New">根据Dockerfile我们知道，容器启动后将执行&quot;/bin/dockerapp1&quot;这个程序，dockerapp1退出，容器即退出。 run命令的&quot;-d&quot;选项表示容器将以daemon的形式运行，我们在前台无法看到容器的输出。那么我们怎么查看容器的输出呢？我们可以通过 docker logs + 容器id的方式查看容器内应用的标准输出或标准错误。我们也可以进入容器来查看。</font></p>
<p><font face="Courier New">进入容器有多种方法，比如用sudo docker attach </font><font face="Courier New"><font face="Courier New">daf3ae88fec2</font>。attach后，就好比将daemon方式运行的容器 拿到了前台，你可以Ctrl + C一下，可以看到如下dockerapp1的输出:</font></p>
<p><font face="Courier New">^Chandle signal: interrupt</font></p>
<p><font face="Courier New">另外一种方式是利用nsenter工具进入我们容器的namespace空间。ubuntu 14.04下可以通过如下方式安装该工具：</font></p>
<p><font face="Courier New">$ wget <a class="moz-txt-link-freetext" href="https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz">https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz</a>; tar xzvf util-linux-2.24.tar.gz<br />
	$ cd util-linux-2.24<br />
	$ ./configure &#8211;without-ncurses &amp;&amp; make nsenter<br />
	$ sudo cp nsenter /usr/local/bin</font></p>
<p>安装后，我们通过如下方式即可进入上面的容器：</p>
<p><font face="Courier New">$ echo $(sudo docker inspect &#8211;format &quot;{{ .State.Pid }}&quot; daf3ae88fec2)<br />
	5494<br />
	$ sudo nsenter &#8211;target 5494 &#8211;mount &#8211;uts &#8211;ipc &#8211;net &#8211;pid<br />
	-bash-4.1# ps -ef<br />
	UID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PID&nbsp; PPID&nbsp; C STIME TTY&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TIME CMD<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp; 0 09:20 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 /bin/dockerapp1<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp; 0 09:32 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 -bash<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 27&nbsp;&nbsp;&nbsp; 16&nbsp; 0 09:32 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 ps -ef<br />
	-bash-4.1# </font></p>
<p>进入容器后通过ps命令可以看到正在运行的dockerapp1程序。在容器内，我们可以通过kill来测试dockerapp1的运行情况：</p>
<p><font face="Courier New">-bash-4.1# kill -s SIGINT 1</font></p>
<p>通过前面的attach窗口，我们可以看到dockerapp1输出:</p>
<p><font face="Courier New">handle signal: interrupt</font></p>
<p>如果你发送SIGTERM信号，那么dockerapp1将终止运行，容器也就停止了。</p>
<p><font face="Courier New">-bash-4.1# kill 1</font></p>
<p>attach窗口显示：</p>
<p><font face="Courier New">signal termiate received, app exit normally</font></p>
<p>我们可以看到容器启动后默认执行的时Dockerfile中的CMD命令，如果Dockerfile中有多行CMD命令，Docker在启动容器 时只会执行最后一条CMD命令。如果在docker run中指定了命令，docker则会执行命令行中的命令而不会执行dockerapp1，比如：</p>
<p><font face="Courier New">$ sudo docker run -t -i &quot;test:v1&quot; /bin/bash<br />
	bash-4.1# </font></p>
<p>这里我们看到直接执行的时bash，dockerapp1并未执行。</p>
<p><b>五、docker stop的行为</b></p>
<p>我们先来看看docker stop的manual：</p>
<p><font face="Courier New">$ sudo docker stop &#8211;help<br />
	Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]<br />
	Stop a running container by sending SIGTERM and then SIGKILL after a grace period<br />
	&nbsp; -t, &#8211;time=10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.</font></p>
<p>可以看出当我们执行docker stop时，docker会首先向容器内的当前主程序发送一个SIGTERM信号，用于容器内程序的退出。如果容器在收到SIGTERM后没有马上退出， 那么stop命令会在等待一段时间（默认是10s）后，再向容器发送SIGKILL信号，将容器杀死，变为退出状态。</p>
<p>我们来验证一下docker stop的行为。启动刚才那个容器：</p>
<p><font face="Courier New">$ sudo docker start daf3ae88fec2<br />
	daf3ae88fec2</font></p>
<p><font face="Courier New">attach到容器daf3ae88fec2<br />
	$ sudo docker attach daf3ae88fec2</font></p>
<p>新打开一个窗口，执行docker stop命令：<br />
	<font face="Courier New">$ sudo docker stop daf3ae88fec2<br />
	daf3ae88fec2</font></p>
<p>可以看到attach窗口输出：<br />
	<font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p>通过docker ps查看，发现容器已经退出。</p>
<p>也许通过上面的例子还不能直观的展示stop命令的<b>两阶段行为</b>，因为dockerapp1收到SIGTERM后直接就退出 了，stop命令无需等待容器慢慢退出，也无需发送SIGKILL。我们改造一下dockerapp1这个程序。</p>
<p>我们复制一下dockerapp1.go为dockerapp2.go，编辑dockerapp2.go，将handler中对SIGTERM的 处理注释掉，其他不变：</p>
<p><font face="Courier New">handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;handle signal: %v\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /*<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;signal termiate received, app exit normally\n&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p>我们使用dockerapp2来构建一个新image：test:v2，将Dockerfile中得dockerapp1换成 dockerapp2即可。</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v2&quot; ./<br />
	Sending build context to Docker daemon 9.369 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp2 /bin/<br />
	&nbsp;&#8212;&gt; 27cd613a9bd7<br />
	Removing intermediate container 07c760b6223b<br />
	Step 3 : CMD /bin/dockerapp2<br />
	&nbsp;&#8212;&gt; Running in 1aac086452a7<br />
	&nbsp;&#8212;&gt; 82eb876fefd2<br />
	Removing intermediate container 1aac086452a7<br />
	Successfully built 82eb876fefd2</font></p>
<p>利用image &quot;test:v2&quot;创建一个容器来测试stop。</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v2&quot;<br />
	29f3ec1af3c355458cbbd802a5e8a53da28e9f51a56ce822c7bba2a772edceac</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	29f3ec1af3c3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 7 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 6 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; romantic_feynman&nbsp;</font>&nbsp;&nbsp;</p>
<p>Attach到这个容器并观察，在另外一个窗口stop该container。我们在attach窗口只看到如下输出：</p>
<p><font face="Courier New">handle signal: terminated</font></p>
<p>stop命令的执行没有立即返回，而是等待容器退出。等待10s后，容器退出，stop命令执行结束。从这个例子我们可以明显看出stop的两阶 段行为。</p>
<p>如果我们以<font face="Courier New">sudo docker run -i -t &quot;test:v1&quot; /bin/bash</font>形式启动容器，那stop命令会将SIGTERM发送给bash这个程序，即使你通过nsenter进入容 器，启动了dockerapp1，dockerapp1也不会收到SIGTERM，dockerapp1会随着容器的退出而被强行终止，就像被 kill -9了一样。</p>
<p><b>六、多进程容器服务</b>程序</p>
<p>上面无论是dockerapp1还是dockerapp2，都是一个单进程服务程序。如果我们在容器内执行一个多进程程序，我们该如何优雅退出 呢？我们先来编写一个多进程的服务程序dockerapp3：</p>
<p>在dockerapp1.go的基础上对main和sysSignalHandleDemo进行修改形成dockerapp3.go，修改后这两 个函数的代码如下：</p>
<p><font face="Courier New">//dockerapp3.go<br />
	&#8230; &#8230;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go sysSignalHandleDemo()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pid, _, err := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;err fork process, err: %v\n&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if pid == 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;i am in child process, pid = %v\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the child process wait<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;i am parent process, pid = %v\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;fork ok, childpid = %v\n&quot;, pid)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the main goroutine wait!<br />
	}</font></p>
<p><font face="Courier New">func sysSignalHandleDemo() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := signalSetNew()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: handle signal: %v\n&quot;, syscall.Getpid(), s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: signal termiate received, app exit normally\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGINT, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR1, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR2, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGTERM, handler)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan os.Signal)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var sigs []os.Signal<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for sig := range ss.m {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigs = append(sigs, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signal.Notify(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sig := &lt;-c</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := ss.handle(sig, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: unknown signal received: %v, app exit unexpectedly\n&quot;, syscall.Getpid(), sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>dockerapp3利用fork创建了一个子进程，这样dockerapp3实际上是两个进程在运行，各自有自己的signal监听 goroutine，goroutine的处理逻辑是相同的。注意：由于Windows和Mac OS X不具备fork语义，因此在这两个平台上运行dockerapp3不会得到预期结果。</p>
<p>利用dockerapp3，我们创建image &quot;test:v3&quot;:</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v3&quot; ./<br />
	[sudo] password for tonybai:<br />
	Sending build context to Docker daemon 11.24 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp3 /bin/<br />
	&nbsp;&#8212;&gt; 6ccf97065853<br />
	Removing intermediate container 6d85fe241939<br />
	Step 3 : CMD /bin/dockerapp3<br />
	&nbsp;&#8212;&gt; Running in 75d76380992a<br />
	&nbsp;&#8212;&gt; c9e7bf361ed7<br />
	Removing intermediate container 75d76380992a<br />
	Successfully built c9e7bf361ed7</font></p>
<p>启动基于test:v3 image的容器：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v3&quot;<br />
	781cecb4b3628cb33e1b104ea57e506ad5cb4a44243256ebd1192af86834bae6<br />
	$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	781cecb4b362&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 5 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 4 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; insane_bohr&nbsp;&nbsp;&nbsp;</font>&nbsp;&nbsp;&nbsp;</p>
<p>通过docker logs查看dockerapp3的输出：</p>
<p><font face="Courier New">$ sudo docker logs 781cecb4b362<br />
	i am parent process, pid = 1<br />
	fork ok, childpid = 13<br />
	i am in child process, pid = 13</font></p>
<p>可以看出主进程pid为1，子进程pid为13。我们通过stop停止该容器：</p>
<p><font face="Courier New">$ sudo docker stop 781cecb4b362<br />
	781cecb4b362</font></p>
<p>再次通过docker logs查看：</p>
<p><font face="Courier New">$ sudo docker logs 781cecb4b362<br />
	i am parent process, pid = 1<br />
	fork ok, childpid = 13<br />
	i am in child process, pid = 13<br />
	1: handle signal: terminated<br />
	1: signal termiate received, app exit normally</font></p>
<p>我们可以看到主进程收到了stop发来的SIGTERM并退出，主进程的退出导致容器退出，导致子进程13也无法生存，并且没有优雅退出。而在非 容器状态下，子进程是可以被init进程接管的。</p>
<p>因此对于docker容器内运行的多进程程序，stop命令只会将SIGTERM发送给容器主进程，要想让其他进程也能优雅退出，需要在主进程与 其他进程间建立一种通信机制。在主进程退出前，等待其他子进程退出。待所有其他进程退出后，主进程再退出，容器停止。这样才能保证服务程序的优雅 退出。</p>
<p><b>七、容器内启动多个服务程序</b></p>
<p>虽说docker <a href="https://docs.docker.com/articles/dockerfile_best-practices/">best practice</a>建议一个container内只放置一个服务程序，但对已有的一些遗留系统，在架构没有做出重构之前，很可能会有在一个 container中部署两个以上服务程序的情况和需求。而docker Dockerfile只允许执行一个CMD，这种情况下，我们就需要借助类似supervisor这样的进程监控管理程序来启动和管理container 内的多个程序了。</p>
<p>下面我们来自制作一个基于centos:centos6的安装了supervisord以及两个服务程序的image。我们将dockerapp1拷贝一份，并将拷贝命名为dockerapp1-brother。下面是我们的Dockerfile：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	RUN yum install python-setuptools -y<br />
	RUN easy_install supervisor<br />
	RUN mkdir -p /var/log/supervisor<br />
	COPY ./supervisord.conf /etc/supervisord.conf<br />
	COPY ./dockerapp1 /bin/<br />
	COPY ./dockerapp1-brother /bin/<br />
	CMD ["/usr/bin/supervisord"]</font></p>
<p>supervisord的配置文件supervisord.conf内容如下：</p>
<p><font face="Courier New">; supervisor config file</font></p>
<p><font face="Courier New">[unix_http_server]<br />
	file=/var/run/supervisor.sock&nbsp;&nbsp; ; (the path to the socket file)<br />
	chmod=0700&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ; sockef file mode (default 0700)</font></p>
<p><font face="Courier New">[supervisord]<br />
	logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)<br />
	pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)<br />
	childlogdir=/var/log/supervisor&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ; (&#39;AUTO&#39; child log dir, default $TEMP)</font></p>
<p><font face="Courier New">[rpcinterface:supervisor]<br />
	supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface</font></p>
<p><font face="Courier New">[supervisorctl]<br />
	serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL&nbsp; for a unix socket</font></p>
<p><font face="Courier New">[supervisord]<br />
	nodaemon=false</font></p>
<p><font face="Courier New">[program:dockerapp1]<br />
	command=/bin/dockerapp1<br />
	stdout_logfile=/tmp/dockerapp1.log<br />
	stopsignal=TERM<br />
	stopwaitsecs=10</font></p>
<p><font face="Courier New">[program:dockerapp1-brother]<br />
	command=/bin/dockerapp1-brother<br />
	stdout_logfile=/tmp/dockerapp1-brother.log<br />
	stopsignal=QUIT<br />
	stopwaitsecs=10</font></p>
<p>开始build镜像：<br />
	&nbsp;&nbsp;&nbsp; <font face="Courier New">$&gt; sudo docker build -t=&quot;test:supervisor-v1&quot; ./<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; Successfully built d006b9ad10eb</font></p>
<p>基于该镜像，启动一个容器：<br />
	<font face="Courier New">$&gt; sudo docker run -d &quot;test:supervisor-v1&quot;<br />
	05ded2b898c90059d4c9b5c6ccc8603b6848ae767360c42bd9b36ff87fb4b9df</font></p>
<p>执行ps命令查看镜像id：<br />
	<font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES</font></p>
<p>怎么回事？Container没有启动起来？</p>
<p><font face="Courier New">$ sudo docker ps -a<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	05ded2b898c9&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:supervisor-v1&nbsp;&nbsp;&nbsp; &quot;/usr/bin/supervisor&nbsp;&nbsp; 22 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Exited (0) 21 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; hungry_engelbart</font></p>
<p>通过ps -a查看，container启动是成功了，但是成功退出了。于是尝试查看一下log：</p>
<p><font face="Courier New">sudo docker logs 05ded2b898c9<br />
	/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a &quot;-c&quot; argument specifying an absolute path to a configuration file for improved security.<br />
	&nbsp; &#39;Supervisord is running as root and it is searching &#39;</font></p>
<p>似乎是supervisord转为daemon程序，容器主进程退出了，容器随之终止了。</p>
<p>看来容器内的supervisord不能以daemon形式运行，应该以前台形式run。修改一下supervisord.conf中得配置：</p>
<p>将<br />
	<font face="Courier New">[supervisord]<br />
	nodaemon=false</font></p>
<p>改为</p>
<p><font face="Courier New">[supervisord]<br />
	nodaemon=true</font></p>
<p>重新制作镜像:</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:supervisor-v2&quot; ./<br />
	Sending build context to Docker daemon 13.12 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : RUN yum install python-setuptools -y<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; e09c66a1ea8c<br />
	Step 3 : RUN easy_install supervisor<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 9c8797e8c27e<br />
	Step 4 : RUN mkdir -p /var/log/supervisor<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 9bfc67f8517d<br />
	Step 5 : COPY ./supervisord.conf /etc/supervisord.conf<br />
	&nbsp;&#8212;&gt; 8c514f998363<br />
	Removing intermediate container 4a185856e6ed<br />
	Step 6 : COPY ./dockerapp1 /bin/<br />
	&nbsp;&#8212;&gt; 0317bd4914d3<br />
	Removing intermediate container ac5738380854<br />
	Step 7 : COPY ./dockerapp1-brother /bin/<br />
	&nbsp;&#8212;&gt; d89711888bdf<br />
	Removing intermediate container eadc9444e716<br />
	Step 8 : CMD ["/usr/bin/supervisord"]<br />
	&nbsp;&#8212;&gt; Running in aaa042ac3914<br />
	&nbsp;&#8212;&gt; 9655256bbfed<br />
	Removing intermediate container aaa042ac3914<br />
	Successfully built 9655256bbfed</font></p>
<p>有了前面的铺垫，这次build image瞬间完成。启动容器，查看容器启动状态，查看容器内supervisord的运行日志如下：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:supervisor-v2&quot;<br />
	61916f1c82338b28ced101b6bde119e4afb7c7fa349b4332ed51a43a4586b1b9</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	61916f1c8233&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:supervisor-v2&nbsp;&nbsp; &quot;/usr/bin/supervisor&nbsp;&nbsp; 16 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 16 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prickly_einstein</font></p>
<p><font face="Courier New">$ sudo docker logs 8eb3e9892e66</font></p>
<p><font face="Courier New">/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a &quot;-c&quot; argument specifying an absolute path to a configuration file for improved security.<br />
	&nbsp; &#39;Supervisord is running as root and it is searching &#39;<br />
	2014-10-09 14:36:02,334 CRIT Supervisor running as root (no user in config file)<br />
	2014-10-09 14:36:02,349 INFO RPC interface &#39;supervisor&#39; initialized<br />
	2014-10-09 14:36:02,349 CRIT Server &#39;unix_http_server&#39; running without any HTTP authentication checking<br />
	2014-10-09 14:36:02,349 INFO supervisord started with pid 1<br />
	2014-10-09 14:36:03,354 INFO spawned: &#39;dockerapp1&#39; with pid 14<br />
	2014-10-09 14:36:03,363 INFO spawned: &#39;dockerapp1-brother&#39; with pid 15<br />
	2014-10-09 14:36:04,368 INFO success: dockerapp1 entered RUNNING state, process has stayed up for &gt; than 1 seconds (startsecs)<br />
	2014-10-09 14:36:04,369 INFO success: dockerapp1-brother entered RUNNING state, process has stayed up for &gt; than 1 seconds (startsecs)</font></p>
<p>可以看到supervisord已经将dockerapp1和dockerapp1-brother启动起来了。</p>
<p>现在我们尝试停止容器，我们预期是supervisord在退出前通知dockerapp1和dockerapp1-brother先退出，我们可以通过 查看容器内的/tmp/dockerapp1.log和/tmp/dockerapp1-brother.log来确认supervisord是否做了通 知。</p>
<p><font face="Courier New">$ sudo docker stop 61916f1c8233<br />
	61916f1c8233</font></p>
<p><font face="Courier New">$ sudo docker logs 61916f1c8233<br />
	&#8230; &#8230;<br />
	2014-10-09 14:37:52,253 WARN received SIGTERM indicating exit request<br />
	2014-10-09 14:37:52,254 INFO waiting for dockerapp1, dockerapp1-brother to die<br />
	2014-10-09 14:37:52,254 INFO stopped: dockerapp1-brother (exit status 0)<br />
	2014-10-09 14:37:52,256 INFO stopped: dockerapp1 (exit status 0)</font></p>
<p>通过容器的log，我们看出supervisord是等待两个程序退出后才退出的，不过我们还是要看看两个程序的输出日志以最终确认。重新启动容器，通过nsenter进入到容器中。</p>
<p><font face="Courier New">-bash-4.1# vi /tmp/dockerapp1.log</font></p>
<p><font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p><font face="Courier New">-bash-4.1# vi /tmp/dockerapp1-brother.log</font></p>
<p><font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p>两个程序的标准输出日志证实了我们的预期。</p>
<p>BTW，在物理机上测试supervisord以daemon形式运行，当kill掉supervisord时，supervisord是不会通知其监控 和管理的程序退出的。只有在以non-daemon形式运行时，supervisord才会在退出前先通知下面的程序退出。如果在一段时间内下面程序没有 退出，supervisord在退出前会kill -9强制杀死这些程序的进程。</p>
<p>最后要说的时，在验证一些想法时，没有必要build image，我们可以直接将本地文件copy到容器中，下面是一个例子，我们将dockerapp1和dockerapp1-brother拷贝到镜像中：<br />
	<font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	4d8982bfccc7&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; centos:centos6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/bash&quot;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 26 minutes ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 26 minutes&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sharp_thompson&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	$ sudo docker inspect -f &#39;{{.Id}}&#39; 4d8982bfccc7<br />
	4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4<br />
	$ sudo cp dockerapp1&nbsp; /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1<br />
	$ sudo cp dockerapp1-brother&nbsp; /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1-brother</font></p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Go中的系统Signal处理</title>
		<link>https://tonybai.com/2012/09/21/signal-handling-in-go/</link>
		<comments>https://tonybai.com/2012/09/21/signal-handling-in-go/#comments</comments>
		<pubDate>Fri, 21 Sep 2012 08:56:41 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Daemon]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[信号]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[结构体]]></category>
		<category><![CDATA[编程，语言]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1046</guid>
		<description><![CDATA[我们在生产环境下运行的系统要求优雅退出，即程序接收退出通知后，会有机会先执行一段清理代码，将收尾工作做完后再真正退出。我们采用系统Signal来 通知系统退出，即kill pragram-pid。我们在程序中针对一些系统信号设置了处理函数，当收到信号后，会执行相关清理程序或通知各个子进程做自清理。kill -9强制杀掉程序是不能被接受的，那样会导致某些处理过程被强制中断，留下无法恢复的现场，导致消息被破坏，影响下次系统启动运行。 最近用Golang实现的一个代理程序也需要优雅退出，因此我尝试了解了一下Golang中对系统Signal的处理方式，这里和大家分享。Golang 的系统信号处理主要涉及os包、os.signal包以及syscall包。其中最主要的函数是signal包中的Notify函数： func Notify(c chan&#60;- os.Signal, sig &#8230;os.Signal) 该函数会将进程收到的系统Signal转发给channel c。转发哪些信号由该函数的可变参数决定，如果你没有传入sig参数，那么Notify会将系统收到的所有信号转发给c。如果你像下面这样调用Notify： signal.Notify(c, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2) 则Go只会关注你传入的Signal类型，其他Signal将会按照默认方式处理，大多都是进程退出。因此你需要在Notify中传入你要关注和处理的Signal类型，也就是拦截它们，提供自定义处理函数来改变它们的行为。 下面是一个较为完整的例子： //signal.go package main import &#34;fmt&#34; import &#34;time&#34; import &#34;os&#34; import &#34;os/signal&#34; import &#34;syscall&#34; type signalHandler func(s os.Signal, arg interface{}) type signalSet struct { &#160;&#160;&#160; m map[os.Signal]signalHandler } func signalSetNew()(*signalSet){ &#160;&#160;&#160; ss := new(signalSet) &#160;&#160;&#160; ss.m [...]]]></description>
			<content:encoded><![CDATA[<p>我们在生产环境下运行的系统要求优雅退出，即程序接收退出通知后，会有机会先执行一段清理代码，将收尾工作做完后再真正退出。我们采用系统Signal来 通知系统退出，即kill pragram-pid。我们在程序中针对一些系统信号设置了处理函数，当收到信号后，会执行相关清理程序或通知各个子进程做自清理。kill -9强制杀掉程序是不能被接受的，那样会导致某些处理过程被强制中断，留下无法恢复的现场，导致消息被破坏，影响下次系统启动运行。</p>
<p>	最近用<a href="http://golang.org">Golang</a>实现的一个代理程序也需要优雅退出，因此我尝试了解了一下<a href="http://tonybai.com/2012/08/17/hello-go/">Golang</a>中对系统Signal的处理方式，这里和大家分享。Golang 的系统信号处理主要涉及os包、os.signal包以及syscall包。其中最主要的函数是signal包中的Notify函数：</p>
<p>	<font face="Courier New">func Notify(c chan&lt;- os.Signal, sig &#8230;os.Signal)</font></p>
<p>	该函数会将进程收到的系统Signal转发给channel c。转发哪些信号由该函数的可变参数决定，如果你没有传入sig参数，那么Notify会将系统收到的所有信号转发给c。如果你像下面这样调用Notify：</p>
<p>	<font face="Courier New">signal.Notify(c, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2)</font></p>
<p>	则Go只会关注你传入的Signal类型，其他Signal将会按照默认方式处理，大多都是进程退出。因此你需要在Notify中传入你要关注和处理的Signal类型，也就是拦截它们，提供自定义处理函数来改变它们的行为。</p>
<p>	下面是一个较为完整的例子：</p>
<p>	<font face="Courier New">//signal.go</font></p>
<p>	<font face="Courier New">package main</p>
<p>	import &quot;fmt&quot;<br />
	import &quot;time&quot;<br />
	import &quot;os&quot;<br />
	import &quot;os/signal&quot;<br />
	import &quot;syscall&quot;</p>
<p>	type signalHandler func(s os.Signal, arg interface{})</p>
<p>	type signalSet struct {<br />
	&nbsp;&nbsp;&nbsp; m map[os.Signal]signalHandler<br />
	}</p>
<p>	func signalSetNew()(*signalSet){<br />
	&nbsp;&nbsp;&nbsp; ss := new(signalSet)<br />
	&nbsp;&nbsp;&nbsp; ss.m = make(map[os.Signal]signalHandler)<br />
	&nbsp;&nbsp;&nbsp; return ss<br />
	}</p>
<p>	func (set *signalSet) register(s os.Signal, handler signalHandler) {<br />
	&nbsp;&nbsp;&nbsp; if _, found := set.m[s]; !found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[s] =&nbsp; handler<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</p>
<p>	func (set *signalSet) handle(sig os.Signal, arg interface{})(err error) {<br />
	&nbsp;&nbsp;&nbsp; if _, found := set.m[sig]; found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[sig](sig, arg)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return fmt.Errorf(&quot;No handler available for signal %v&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>	&nbsp;&nbsp;&nbsp; panic(&quot;won&#39;t reach here&quot;)<br />
	}</p>
<p>	func main() {<br />
	&nbsp;&nbsp;&nbsp; go sysSignalHandleDemo()<br />
	&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the main goroutine wait!<br />
	}</p>
<p>	func sysSignalHandleDemo() {<br />
	&nbsp;&nbsp;&nbsp; ss := signalSetNew()<br />
	&nbsp;&nbsp;&nbsp; handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;handle signal: %v\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>	&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGINT, handler)<br />
	&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR1, handler)<br />
	&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR2, handler)</p>
<p>	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan os.Signal)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var sigs []os.Signal<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for sig := range ss.m {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigs = append(sigs, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signal.Notify(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sig := &lt;-c</p>
<p>	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := ss.handle(sig, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (err != nil) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;unknown signal received: %v\n&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>	上例中Notify函数只有一个参数，没有传入要关注的sig，因此程序会将收到的所有类型Signal都转发到channel c中。build该源文件并执行程序：</p>
<p>	<span style="font-family:courier new,courier,monospace;">$&gt; go build signal.go<br />
	$&gt; signal</span></p>
<p>	在另外一个窗口下执行如下命令：<br />
	<span style="font-family:courier new,courier,monospace;">$&gt; ps -ef|grep signal<br />
	tonybai&nbsp; 25271&nbsp; 1087&nbsp; 0 16:27 pts/1&nbsp;&nbsp;&nbsp; 00:00:00 signal<br />
	$&gt; kill -n 2 25271<br />
	$&gt; kill -n 12 25271<br />
	$&gt; kill 25271</span></p>
<p>	我们在第一个窗口会看到如下输出：<br />
	<span style="font-family:courier new,courier,monospace;">$&gt; signal<br />
	handle signal: interrupt<br />
	handle signal: user defined signal 2<br />
	unknown signal received: terminated</span></p>
<p>	在sysSignalHandleDemo中我们也可以为Notify传入我们所关注的Signal集合：</p>
<p>	<font face="Courier New">signal.Notify(c, sigs&#8230;)</font></p>
<p>	这样只有在该集合中的信号我们才能捕获，收到未在集合中的信号时，程序多直接退出。上面只是一个Demo，只是说明了我们可以捕捉到我们所关注的信号，并未体现程序如何优雅退出，不同程序的退出方式不同，这里没有通用方法，就不细说了，你的程序需要你专门的设计。</p>
<p>	另外我们生产环境下的程序多是以Daemon守护进程的形式运行的。我们用C实现的程序多参考&ldquo;<a href="http://book.douban.com/subject/1788421/">Unix高级编程</a>&rdquo;中的方法将程序转为Daemon Process，但在Go中目前尚提供相关方式，网上有一些实现，但据说都不理想。更多的Go开发者建议不要在代码中实现Daemon转换，建议直接利用 第三方工具。比如在<a href="http://ubuntu.com">Ubuntu</a>下我们可以使用start-stop-daemon这个小程序轻松将你的程序转换为Daemon：</p>
<p>	$&gt; start-stop-daemon &#8211;start &#8211;pidfile ./signal.pid &#8211;startas /home/tonybai/test/go/signal &#8211;background -m<br />
	$&gt; start-stop-daemon &#8211;stop &#8211;pidfile ./signal.pid &#8211;startas /home/tonybai/test/go/signal</p>
<p>	这里注意：只有加上-m选项，pidfile才能成功创建。</p>
<p>	start-stop-daemon在<a href="http://www.debian.org">Debian</a>系的Linux发行版中都是默认自带的。但在<a href="http://www.redhat.com">Redhat</a>系Linux发行版中却没有该工具，我们可以自行安装：</p>
<p>	<font face="Courier New">wget -c http://developer.axis.com/download/distribution/apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz<br />
	tar -xzf apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz<br />
	cd apps/sys-utils/start-stop-daemon-IR1_9_18-2<br />
	gcc start-stop-daemon.c -o start-stop-daemon</font></p>
<p>	切换到root下<br />
	<font face="Courier New">cp start-stop-daemon /sbin/<br />
	chmod +x /sbin/start-stop-daemon</font></p>
<p>	另外Go 1.0.2提供的二进制安装包直接在Redhat 5.6(Linux tonybai 2.6.18-238.el5 #1 SMP Sun Dec 19 14:22:44 EST 2010 x86_64 x86_64 x86_64 GNU/Linux)下面运行出错，提示无法找到GLIBC 2.7版本。目前解决这一问题的方法似乎只有从源码编译安装。进入到$GOROOT/src下，执行./all.bash即可。重现编译链接后的go可执 行程序则运行一切正常。</p>
<p style='text-align:left'>&copy; 2012, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/09/21/signal-handling-in-go/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>APR源代码分析-信号篇</title>
		<link>https://tonybai.com/2005/09/13/apr-signal/</link>
		<comments>https://tonybai.com/2005/09/13/apr-signal/#comments</comments>
		<pubDate>Tue, 13 Sep 2005 09:25:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[APR]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[IPC]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/2005/09/13/apr%e5%88%86%e6%9e%90-%e4%bf%a1%e5%8f%b7%e7%af%87/</guid>
		<description><![CDATA[<pre><p>U know 信号是Unix的重要系统机制。信号机制使用起来很简单，但是理解起来有并不是那么Easy。APR Signal的封装也并不繁琐，代码量很少，所以分析APR Signal的过程其实就是学习Signal机制的过程。</p></pre>]]></description>
			<content:encoded><![CDATA[<p>U know <a href="http://en.wikipedia.org/wiki/Unix_signal">信号</a>是<a href="http://en.wikipedia.org/wiki/Unix">Unix</a>的重要系统机制。信号机制使用起来很简单，但是理解起来有并不是那么Easy。<a href="http://apr.apache.org">APR</a> Signal的封装也并不繁琐，代码量很少，所以分析APR Signal的过程其实就是学习Signal机制的过程。</p>
<p>一、信号介绍<br />
	1、Signal&ldquo;历史久远&rdquo;，在最初的Unix系统上就能看到它&ldquo;伟岸&rdquo;的身影。它的引入用来进行User Mode进程间的交互，系统内核也可以利用它通知User Mode进程发生了哪些系统事件。从最开始引入到现在，信号只是做了很小的一些改动（不可靠信号模型到可靠信号模型）。</p>
<p>2、信号服务于两个目的：<br />
	&nbsp;1) 通知某进程某特定事件发生了；<br />
	&nbsp;2) 强制其通知进程执行相应的信号处理程序。</p>
<p>二、基础概念<br />
	1、信号的一个特性就是可以在任何时候发给某一进程，而无需知道该进程的状态。如果该进程当前并未处于执行态，则该信号被内核Save起来，直到该进程恢复执行才传递给它；如果一个信号被进程设置为阻塞，则该信号的传递被延迟，直到其阻塞被取消它才被传递给进程。</p>
<p>2、系统内核严格区分信号传送的两个阶段：<br />
	&nbsp;1) Signal Generation : 系统内核更新目标进程描述结构来表示一个信号已经被发送出去。<br />
	&nbsp;2) Signal Delivery : 内核强制目标进程对信号做出反应，或执行相关信号处理函数，或改变进程执行状态。<br />
	信号的诞生和传输我们可以这样理解：把信号作为&ldquo;消费品&rdquo;，其Generation状态就是&ldquo;消费品诞生&rdquo;，其Delivery状态就是理解为&ldquo;被消费了&rdquo;。这样势必存在这样的一个情况：&ldquo;消费品诞生了，但是还没有被消费掉&rdquo;，在信号模型中，这样的状态被称为&ldquo;pending&rdquo;(悬而未决)。</p>
<p>任何时候一个进程只能有一个这样的某类型的pending信号，同一进程的其他同类型的pending信号将不排队，将被简单的discard(丢弃)掉。</p>
<p>3、如何消费一个signal<br />
	&nbsp;1) 忽略该信号；[注1]<br />
	&nbsp;2) 响应该信号，执行一特定的信号处理函数；<br />
	&nbsp;3) 响应该信号，执行系统默认的处理函数。包括：Terminate、Dump、Ignore、Stop、Continue等。<br />
	这里有特殊：SIGKILL和SIGSTOP两个信号不能忽略、不能捕捉、不能阻塞，而只是执行系统默认处理函数。</p>
<p>三、APR Signal封装<br />
	APR Signal源代码的位置在$(APR_HOME)/\threadproc目录下，本篇blog着重分析unix子目录下的signals.c文件内容，其相应头文件为$(APR_HOME)/include/apr_signal.h。</p>
<p>1、apr_signal函数<br />
	Unix信号机制提供的最简单最常见的接口是signal函数，用来设置某特定信号的处理函数。但是由于早期版本和后期版本处理信号方式的不同，导致现在直接使用signal函数在不同的平台上可能得到不同的结果。<br />
	早期版本处理方式：进程每次处理信号后，随即将信号的处理动作重置为默认值。<br />
	后期版本处理方式：进程每次处理信号后，信号的处理动作不被重置为默认值。</p>
<p>我们举例测试一下：分别在Solaris 9 、Cygwin和RedHat Linux 9上。<br />
	例子：<br />
	E.G 1:<br />
	void siguser1_handler(int sig);</p>
<p>int main(void)<br />
	{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (signal(SIGUSR1, siguser1_handler) == SIG_ERR) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; perror(&quot;siguser1_handler error&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; exit(1);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; while (1) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pause();<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</p>
<p>void siguser1_handler(int sig)<br />
	{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;in siguser1_handler, %d\n&quot;, sig);<br />
	}</p>
<p>input:<br />
	kill -USR1 9122<br />
	kill -USR1 9122</p>
<p>output:(Solaris 9)<br />
	in siguser1_handler, 16<br />
	用户信号1 (程序终止)</p>
<p>output:(Cygwin and RH9)<br />
	in siguser1_handler, 30<br />
	in siguser1_handler, 30<br />
	&#8230;<br />
	..</p>
<p>E.G 1结果表示在Solaris 9上，信号的处理仍然按照早期版本的方式，而Cygwin和RH9则都按照后期版本的方式。<br />
	那么有什么替代signal函数的办法么？在最新的X/Open和UNIX specifications中都推荐使用一个新的信号接口sigaction，该接口采用后期版本的信号处理方式。在《Unix高级环境编程》中就有使用sigaction实现signal的方法，而APR恰恰也是使用了该方法实现了apr_signal。其代码如下：<br />
	APR_DECLARE(apr_sigfunc_t *) apr_signal(int signo, apr_sigfunc_t * func)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; struct sigaction act, oact;</p>
<p>&nbsp;&nbsp;&nbsp; act.sa_handler = func;<br />
	&nbsp;&nbsp;&nbsp; sigemptyset(&#038;act.sa_mask);&nbsp;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;(1)<br />
	&nbsp;&nbsp;&nbsp; act.sa_flags = 0;<br />
	#ifdef SA_INTERRUPT&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* SunOS */<br />
	&nbsp;&nbsp;&nbsp; act.sa_flags |= SA_INTERRUPT;<br />
	#endif<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;</p>
<p>&nbsp;&nbsp;&nbsp; if (sigaction(signo, &#038;act, &#038;oact) &lt; 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return SIG_ERR;<br />
	&nbsp;&nbsp;&nbsp; return oact.sa_handler;<br />
	}</p>
<p>(1) 这里有一个Signal Set(信号集)的概念，通过相关函数操作信号集以改变内核传递信号给进程时的行为。Unix用sigset_t结构来表示信号集。信号集总是和sigprocmask或sigaction一起使用。关于信号集和sigprocmask函数将在下面详述。</p>
<p>2、apr_signal_block和apr_signal_unblock<br />
	这两个函数分别负责阻塞和取消阻塞内核传递某信号给目标进程。其主要利用的就是sigprocmask函数来实现的。每个进程都有其对应的信号屏蔽字，它让目标进程能够通知内核&ldquo;哪些传给我的信号该阻塞，哪些畅通无阻&rdquo;。在《Unix高级环境编程》中作者有这么一段说明&ldquo;如果在调用sigprocmask后有任何未决的、不再阻塞的信号，则在sigprocmask返回前，至少将其中之一递送给该进程。&rdquo;能理解这句我想信号屏蔽字这块儿也就没什么问题了。在Unix高级环境编程》中作者举了一个很不错的例子，讲解的也很详细。这里想举例说明的是：如果多次调用SET_BLOCK的sigprocmask设置屏蔽字，结果是什么呢？</p>
<p>E.G 3<br />
	int main(void)<br />
	{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigset_t newmask, oldmask, pendmask;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 设置进程信号屏蔽字, 阻塞SIGQUIT */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigemptyset(&#038;newmask);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigaddset(&#038;newmask, SIGQUIT);</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigprocmask(SIG_BLOCK, &#038;newmask, &#038;oldmask) &lt; 0) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; perror(&quot;SIG_BLOCK error&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;1st to wait 30 seconds\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sleep(30);</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 第一次察看当前的处于pend状态的信号 */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigpending(&#038;pendmask) &lt; 0) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; perror(&quot;sigpending error&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigismember(&#038;pendmask, SIGQUIT)) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;SIGQUIT pending\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;SIGQUIT unpending\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigismember(&#038;pendmask, SIGUSR1)) {</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigismember(&#038;pendmask, SIGUSR1)) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;SIGUSR1 pending\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;SIGUSR1 unpending\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 重新设置屏蔽字, 阻塞SIGUSR1 */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigemptyset(&#038;newmask);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigaddset(&#038;newmask, SIGUSR1);</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigprocmask(SIG_BLOCK, &#038;newmask, &#038;oldmask) &lt; 0) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; perror(&quot;SIG_BLOCK error&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;2nd to wait 30 seconds\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sleep(30);</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 再次察看当前的处于pend状态的信号 */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigpending(&#038;pendmask) &lt; 0) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; perror(&quot;sigpending error&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigismember(&#038;pendmask, SIGQUIT)) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;SIGQUIT pending\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;SIGQUIT unpending\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (sigismember(&#038;pendmask, SIGUSR1)) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;SIGUSR1 pending\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;SIGUSR1 unpending\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; exit(0);<br />
	}</p>
<p>//output:<br />
	1st to wait 30 seconds<br />
	^\<br />
	SIGQUIT pending<br />
	SIGUSR1 unpending<br />
	2nd to wait 30 seconds &#8212; 这之后发送kill -USR1 28821<br />
	SIGQUIT pending<br />
	SIGUSR1 pending</p>
<p>第一次输出SIGUSR1 unpending是因为并未发送USR1信号，所以自然为unpending状态；我想说的是第二次重新sigprocmask时我们仅加入了SIGUSR1,并未显示加入SIGQUIT，之后察看pending信号中SIGQUIT仍然为pending状态，这说明两次SET_BLOCK的sigprocmask调用是&quot;或&quot;的关系，第二次SET_BLOCK的sigprocmask调用不会将第一次SET_BLOCK的sigprocmask调用设置的阻塞信号变为非阻塞的。</p>
<p>四、总结<br />
	信号简单而强大，如果想深入了解signal的实现，参考资料中的第二本书会给你满意的答案。</p>
<p>五、参考资料：<br />
	1、《<a href="http://book.douban.com/subject/1788421/">Unix高级环境编程</a>》<br />
	2、《<a href="http://book.douban.com/subject/1767120">深入理解Linux内核</a>》</p>
<p>[注1]<br />
	忽略信号和阻塞信号<br />
	前者相当于一个消费行为，该信号的状态为&ldquo;已消费&rdquo;，而后者只是将信号做缓存，等待阻塞打开，再交给进程消费，其状态为&ldquo;未消费&rdquo;，也相当于处于pending状态。</p>
<p style='text-align:left'>&copy; 2005, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2005/09/13/apr-signal/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
