<?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; 命令行</title>
	<atom:link href="http://tonybai.com/tag/%e5%91%bd%e4%bb%a4%e8%a1%8c/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 20 Apr 2026 23:16:50 +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>为什么你的 AI Agent 总是像个智障？来自 Manus 大佬的 2 年血泪避坑指南</title>
		<link>https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide/</link>
		<comments>https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide/#comments</comments>
		<pubDate>Wed, 18 Mar 2026 12:21:04 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AIAgent]]></category>
		<category><![CDATA[BinaryGuard]]></category>
		<category><![CDATA[cli]]></category>
		<category><![CDATA[CodeGeneration]]></category>
		<category><![CDATA[CognitiveLoad]]></category>
		<category><![CDATA[ContextPollution]]></category>
		<category><![CDATA[ErrorGuidance]]></category>
		<category><![CDATA[FunctionCalling]]></category>
		<category><![CDATA[Hallucination]]></category>
		<category><![CDATA[Manus]]></category>
		<category><![CDATA[Minimalism]]></category>
		<category><![CDATA[Pipe]]></category>
		<category><![CDATA[Productivity]]></category>
		<category><![CDATA[ProgressiveDiscovery]]></category>
		<category><![CDATA[SoftwareEngineering]]></category>
		<category><![CDATA[ToolSelection]]></category>
		<category><![CDATA[UnixPhilosophy]]></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>
		<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=6064</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide 大家好，我是Tony Bai。 如果你在过去一年里跟风写过 AI Agent（智能体），你大概率经历过这样的绝望时刻： 你兴致勃勃地给大模型挂载了二三十个精心编写的 Function Calling（函数调用）工具，比如 read_file, search_web, execute_python……你期待它能像钢铁侠的贾维斯一样运筹帷幄。 结果呢？面对稍微复杂一点的任务，你的 Agent 瞬间退化成一个“智障”。 它要么在几十个工具里疯狂迷失，选错了参数导致系统报错；要么陷入无限死循环，把你的 Token 烧个精光，最后无辜地吐出一句：“抱歉，我无法完成该任务。” 我们总以为是自己的 Prompt 没写对，或者是大模型还不够聪明。 直到前些日子，一位名叫 MorroHsu 的顶级实战派大佬（在被 Meta 收购前，他是现象级 AI 产品 Manus 的后端技术负责人）在 Reddit 上抛出了一篇长文。 在过去两年里，他以后端负责人的身份参与构建了包括 Manus、agent-clip 等在内的多个顶尖 Agent。在被大模型的各种奇葩幻觉折磨了无数遍之后，他得出了一个极其震撼、甚至有些反直觉的血泪结论： 别再瞎折腾繁琐的 Typed Function Calls（类型化函数调用）了！给大模型一堆乱七八糟的 API，就是它变“智障”的罪魁祸首。大模型最需要的，仅仅是 50 年前的 Linux 命令行（CLI）。 今天，我们就来看看这位 Manus 前后端大佬的 2 年避坑心法。看看为什么最前沿的 AI，反而需要最古老的 Unix 哲学来拯救。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/why-ai-agents-act-stupid-manus-expert-pitfall-guide-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide">本文永久链接</a> &#8211; https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide</p>
<p>大家好，我是Tony Bai。</p>
<p>如果你在过去一年里跟风写过 AI Agent（智能体），你大概率经历过这样的绝望时刻：</p>
<p>你兴致勃勃地给大模型挂载了二三十个精心编写的 Function Calling（函数调用）工具，比如 read_file, search_web, execute_python……你期待它能像钢铁侠的贾维斯一样运筹帷幄。</p>
<p>结果呢？面对稍微复杂一点的任务，你的 Agent 瞬间退化成一个“智障”。</p>
<p>它要么在几十个工具里疯狂迷失，选错了参数导致系统报错；要么陷入无限死循环，把你的 Token 烧个精光，最后无辜地吐出一句：“抱歉，我无法完成该任务。”</p>
<p>我们总以为是自己的 Prompt 没写对，或者是大模型还不够聪明。</p>
<p>直到前些日子，一位名叫 MorroHsu 的顶级实战派大佬（在被 Meta 收购前，他是现象级 AI 产品 Manus 的后端技术负责人）在 Reddit 上<a href="https://www.reddit.com/r/LocalLLaMA/comments/1rrisqn/i_was_backend_lead_at_manus_after_building_agents">抛出了一篇长文</a>。</p>
<p>在过去两年里，他以后端负责人的身份参与构建了包括 Manus、agent-clip 等在内的多个顶尖 Agent。在被大模型的各种奇葩幻觉折磨了无数遍之后，他得出了一个极其震撼、甚至有些反直觉的血泪结论：</p>
<p><strong>别再瞎折腾繁琐的 Typed Function Calls（类型化函数调用）了！给大模型一堆乱七八糟的 API，就是它变“智障”的罪魁祸首。大模型最需要的，仅仅是 50 年前的 Linux 命令行（CLI）。</strong></p>
<p>今天，我们就来看看这位 Manus 前后端大佬的 2 年避坑心法。看看为什么最前沿的 AI，反而需要最古老的 Unix 哲学来拯救。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/building-industrial-grade-agent-skills-qr.png" alt="" /></p>
<h2>为什么给 AI 几百个工具，它反而成了“智障”？</h2>
<p>目前主流的 Agent 框架（如 LangChain），都在教我们怎么给大模型塞满工具箱。你塞的工具越多，系统看起来越庞大。</p>
<p>但 MorroHsu 指出了这背后的致命逻辑错误：<strong>工具选择的认知过载（Cognitive Load）。</strong></p>
<p>大模型每次行动前，都要在几十个有着不同数据结构（Schemas）的工具中艰难地做选择题：<em>“我到底该用哪一个？参数填什么？”</em> 上下文的注意力被极大地分散了，准确率直线断崖式下跌。</p>
<p>大佬的解法粗暴且优雅：<strong>废弃所有花里胡哨的工具，只给大模型提供唯一的一个函数：run(command=”&#8230;”)。</strong></p>
<p>为什么？因为大模型天生就是个 Linux 高手！</p>
<p>大模型的训练语料库里，充斥着 GitHub 上数十亿行的代码、README.md 中的安装指南、以及 Stack Overflow 上的报错日志。这些语料中，密密麻麻全是 CLI 命令行。</p>
<p>如果你让它去调用你发明的 read_log_file(path) API，它还要去猜测你的参数定义；但如果你让它去找日志里的错误，它会凭着肌肉记忆毫不犹豫地写出：</p>
<p>run(command=”cat /var/log/app.log | grep &#8216;ERROR&#8217; | tail -n 20&#8243;)</p>
<p>你看，CLI 本身就是大模型最熟悉的母语。<strong>不要发明新的轮子去教大模型做事，直接把它最熟悉的世界交给它。</strong></p>
<h2>50年前的“管道”魔法，完美解决了 Agent 编排难题</h2>
<p>如果只有一个 run 命令，AI 遇到复杂任务怎么办？</p>
<p>这就引出了 50 年前 Unix 操作系统的伟大设计哲学：<strong>一切皆文件。</strong></p>
<p>Unix 的先驱们设计了大量只做一件事的小工具（cat, grep, sort），然后通过<strong>管道（Pipe |）</strong>将它们串联成无比强大的工作流。</p>
<p>而这，完美契合了大模型的核心本质——大模型只能理解文本输入和文本输出！</p>
<p>在传统的 Function Calling 中，为了完成“下载数据 -> 过滤错误 -> 排序前 10 条”这个任务，你的 Agent 可能需要连续调用 3 个不同的自定义函数，经历 3 轮耗时极长的 LLM 推理，中间稍微错一步就满盘皆输。</p>
<p>但在 CLI 模式下，AI 只需要通过一次组合调用就能秒杀：</p>
<p>run(command=”curl -sL $URL | grep &#8217;500&#8242; | sort | head 10&#8243;)</p>
<p>这种强大的“组合编排能力（Composition）”，不是什么 AI 领域的最新黑科技，而是 Unix 管道原生自带的降维打击。</p>
<h2>把大模型当人看，设计“防智障”导航系统</h2>
<p>当然，光把命令行扔给大模型，它依然会因为瞎猜而犯错。MorroHsu 总结了三个极其硬核的实战设计技巧，教你如何打造一个“防智障”的 Agent 导航系统：</p>
<p><strong>绝招 1：渐进式发现（Progressive Discovery）</strong></p>
<p>不要一开始就把所有命令的长篇大论全塞给大模型，那会瞬间撑爆它的上下文窗口。</p>
<p>只要告诉大模型：<em>“你可以运行 run(“command”)。遇到不懂的，运行 command &#8211;help”</em>。</p>
<p>大模型其实非常懂得自我探索。当它发现报错时，它会自动去查阅说明书。这种“按需发现”的能力，极大地节省了宝贵的 Token。</p>
<p><strong>绝招 2：把报错变成“向导”</strong></p>
<p>这是最具启发性的一点！当大模型敲错命令时，千万别只返回一个冷冰冰的 exit code 127 或者 command not found。大模型无法像人类那样去 Google 搜索错误原因，它只会陷入瞎猜的死循环。</p>
<p>你必须在 stderr（标准错误输出）里加上向导信息。</p>
<p>传统报错：cat: photo.png: binary file</p>
<p>给 AI 的防智障报错：[Error] cat: photo.png is a binary image. Use &#8216;see photo.png&#8217; instead.</p>
<p>不要试图阻止大模型犯错，而是要让它的每一次犯错，都成为指向正确道路的路标。</p>
<p><strong>绝招 3：双层架构（物理隔离幻觉）</strong></p>
<p>大模型的上下文是极其脆弱的。MorroHsu 分享了一个惨痛的真实案例：</p>
<p>一个用户上传了一张系统架构图，Agent 试图用 cat 命令读取它。结果 182KB 的乱码二进制字节流瞬间冲入了大模型的上下文。大模型当场“失了智”，开始不停地胡言乱语、重试、陷入死循环……足足浪费了 20 次推理的钱。</p>
<p>为了解决这个问题，必须在底层 Unix 执行和大模型展示层之间，建立一道<strong>“二进制守卫（Binary Guard）”</strong>和<strong>“截断溢出守卫（Overflow Mode）”</strong>。</p>
<p>当探测到命令输出超过 200 行，或者包含二进制乱码时，系统绝不把原数据返回给大模型，而是强制拦截并返回提示：</p>
<p><em>“&#8212; 输出已截断。请使用 grep 或 tail 命令进行搜索。&#8212;”</em></p>
<p>这就像给大模型戴上了一副防护眼镜，彻底杜绝了上下文被垃圾数据污染、导致智力下降的可能。</p>
<h2>小结：化繁为简，才是架构的最高境界</h2>
<p>目前，全网依然在乐此不疲地比拼谁的 Agent 框架更庞大、谁支持的 Tool Call 种类更多。但 原 Manus 大佬的这套“返璞归真”的血泪总结，给我们狠狠敲响了警钟。</p>
<p><strong>最前沿的 AI，其实最需要最古老的系统智慧。</strong></p>
<p>将 Unix 哲学的精髓（文本流、组合管道、小而美）与大模型的文本处理能力完美结合，放弃给 AI 制造复杂的隔离层和几十个脆弱的 API 接口，这才是真正属于“顶级工程师”的架构审美。</p>
<p>正如他在文末所言：“CLI 并非银弹，对于强类型校验和高安全性要求极高的场景，Typed API 依然不可或缺。<strong>但在广袤的智能体自主探索宇宙中，命令行，就是大模型所需要的全部。</strong>”</p>
<p>资料链接：https://www.reddit.com/r/LocalLLaMA/comments/1rrisqn/i_was_backend_lead_at_manus_after_building_agents</p>
<hr />
<p><strong>今日互动探讨：</strong></p>
<p>你在写 Agent 时，是喜欢用框架提供的一大堆 Tool Calls，还是像这位大神一样，直接让大模型写代码/写命令去执行？在实战中你的 AI 发生过哪些最搞笑的“智障/幻觉”行为？</p>
<p>欢迎在评论区分享你的血泪避坑史！</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p><strong>原「Gopher部落」已重装升级为「Go &amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！</strong></p>
<p>我们致力于打造一个高品质的 <strong>Go 语言深度学习</strong> 与 <strong>AI 应用探索</strong> 平台。在这里，你将获得：</p>
<ul>
<li><strong>体系化 Go 核心进阶内容:</strong> 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。</li>
<li><strong>前沿 Go+AI 实战赋能:</strong> 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 </li>
<li><strong>星主 Tony Bai 亲自答疑:</strong> 遇到难题？星主第一时间为你深度解析，扫清学习障碍。</li>
<li><strong>高活跃 Gopher 交流圈:</strong> 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。</li>
<li><strong>独家资源与内容首发:</strong> 技术文章、课程更新、精选资源，第一时间触达。</li>
</ul>
<p>衷心希望「Go &amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go开发者必看！JetBrains 2024报告深度解读：Go语言现状、趋势与未来机遇</title>
		<link>https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis/</link>
		<comments>https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis/#comments</comments>
		<pubDate>Thu, 10 Apr 2025 00:08:33 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[cloudflare]]></category>
		<category><![CDATA[cmdline]]></category>
		<category><![CDATA[GenAI]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[JetBrains]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[ollama]]></category>
		<category><![CDATA[openai]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[SlashData]]></category>
		<category><![CDATA[Stackoverflow]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[云服务]]></category>
		<category><![CDATA[命令行]]></category>
		<category><![CDATA[大模型]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4550</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis 嘿，各位Gopher！ 你是否也在关心Go语言的最新动态？它还在快速增长吗？薪资水平如何？未来方向在哪？ 这是我看到的关于2024年Go语言发展趋势最全面、数据最翔实的一份报告解读。 JetBrains，这家开发者们都非常熟悉的工具公司，最近发布了《Is Golang Still Growing? Go Language Popularity Trends in 2024》的研究报告文章。如果你是Go开发者，或者正在关注Go生态，这篇文章就是为你准备的，强烈推荐阅读！ 在深入细节之前，先为你快速提炼报告的核心发现，让你高效把握重点： Go开发者规模依旧庞大且专业： 全球专业Go开发者估算超400万，且持续增长。 云原生主战场地位稳固： Web服务、云服务、IT基础设施是Go应用核心领域。 “钱景”诱人： Go开发者薪资普遍处于行业较高水平。 各大榜单表现亮眼： 在TIOBE、GitHub Octoverse等多个权威榜单中，Go排名稳定或显著上升。 与Rust互补而非替代： 两者定位不同，常被结合使用。 未来聚焦： 持续深耕云原生，并在GenAI基础设施领域崭露头角。 Go开发者画像：规模、角色与“钱景” 报告显示，全球使用Go的专业开发者规模可观。JetBrains估计近一年有410万专业人士使用Go，其中180万将其作为主要语言之一。SlashData的估算则更高，达到470万（包含学生和爱好者），而最新的Stack Overflow和SlashData数据推算更是达到了580万。 从上图中展示的开发者从事的软件类型来看： Web服务 (无GUI): 744,000 网站: 732,000 云服务: 681,000 开发者角色方面(如上图)，除了大量的软件工程师/程序员 (约160万)外，DevOps/基础设施工程师(约50万)的比例也相当高，这凸显了Go在云原生基础设施和运维领域的巨大需求。 更让Gopher们关心的是薪资。报告明确指出，Go开发者是业内薪资最高的人群之一。美国Go开发者的平均年薪约为$76,000，经验丰富者甚至可达$500,000。 Go的应用版图：核心场景与行业分布 Go最常见的两大用例依然是： API/RPC服务(75%) 命令行工具(62%) 哪些行业在重度使用Go呢？ 科技 (超过40%): Google, DataDog, K8s, HashiCorp, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/jetbrains-2024-go-report-analysis-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis">本文永久链接</a> &#8211; https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis</p>
<p>嘿，各位Gopher！</p>
<p>你是否也在关心Go语言的最新动态？它还在快速增长吗？薪资水平如何？未来方向在哪？</p>
<p><strong>这是我看到的关于2024年Go语言发展趋势最全面、数据最翔实的一份报告解读。</strong> JetBrains，这家开发者们都非常熟悉的工具公司，最近发布了《<a href="https://blog.jetbrains.com/research/2025/04/is-golang-still-growing-go-language-popularity-trends-in-2024/">Is Golang Still Growing? Go Language Popularity Trends in 2024</a>》的研究报告文章。如果你是Go开发者，或者正在关注Go生态，<strong>这篇文章就是为你准备的，强烈推荐阅读！</strong></p>
<p>在深入细节之前，先为你<strong>快速提炼报告的核心发现</strong>，让你高效把握重点：</p>
<ul>
<li><strong>Go开发者规模依旧庞大且专业：</strong> 全球专业Go开发者估算超<strong>400万</strong>，且持续增长。</li>
<li><strong>云原生主战场地位稳固：</strong> Web服务、云服务、IT基础设施是Go应用核心领域。</li>
<li><strong>“钱景”诱人：</strong> Go开发者薪资普遍处于<strong>行业较高水平</strong>。</li>
<li><strong>各大榜单表现亮眼：</strong> 在TIOBE、GitHub Octoverse等多个权威榜单中，Go排名<strong>稳定或显著上升</strong>。</li>
<li><strong>与Rust互补而非替代：</strong> 两者定位不同，常被结合使用。</li>
<li><strong>未来聚焦：</strong> 持续深耕<strong>云原生</strong>，并在<strong>GenAI基础设施</strong>领域崭露头角。</li>
</ul>
<hr />
<h2>Go开发者画像：规模、角色与“钱景”</h2>
<p>报告显示，全球使用Go的专业开发者规模可观。JetBrains估计近一年有<strong>410万</strong>专业人士使用Go，其中<strong>180万</strong>将其作为主要语言之一。<a href="https://dashboard-tool-report.cdn.prismic.io/dashboard-tool-report/ZmMmh5m069VX1jxc_-W.Kodluyoruz-Programminglanguagecommunities.pdf">SlashData的估算则更高</a>，达到<strong>470万</strong>（包含学生和爱好者），而最新的Stack Overflow和SlashData数据推算更是达到了<strong>580万</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/jetbrains-2024-go-report-analysis-2.jpg" alt="" /></p>
<p>从上图中展示的开发者从事的软件类型来看：</p>
<ul>
<li>Web服务 (无GUI): <strong>744,000</strong></li>
<li>网站: <strong>732,000</strong></li>
<li>云服务: <strong>681,000</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/jetbrains-2024-go-report-analysis-3.jpg" alt="" /></p>
<p>开发者角色方面(如上图)，除了大量的<strong>软件工程师/程序员 (约160万)</strong>外，<strong>DevOps/基础设施工程师(约50万)</strong>的比例也相当高，这凸显了Go在云原生基础设施和运维领域的巨大需求。</p>
<p>更让Gopher们关心的是薪资。报告明确指出，Go开发者是<strong>业内薪资最高的人群之一</strong>。美国Go开发者的平均年薪约为<strong>$76,000</strong>，经验丰富者甚至可达<strong>$500,000</strong>。</p>
<hr />
<h2>Go的应用版图：核心场景与行业分布</h2>
<p>Go最常见的两大用例依然是：</p>
<ol>
<li><strong>API/RPC服务(75%)</strong></li>
<li><strong>命令行工具(62%)</strong></li>
</ol>
<p>哪些行业在重度使用Go呢？</p>
<ul>
<li><strong>科技 (超过40%):</strong> Google, DataDog, K8s, HashiCorp, Dropbox, Salesforce, Apple&#8230;</li>
<li><strong>金融服务 (13%):</strong> Monzo, American Express, Mercado Libre&#8230;</li>
<li><strong>交通与零售 (10%):</strong> Amazon, Uber, DeliveryHero, HelloFresh&#8230;</li>
<li><strong>媒体/游戏 (7%):</strong> Netflix, Bytedance, Tencent, Reddit, Snap&#8230;</li>
</ul>
<hr />
<h2>多维数据透视：Go在各大榜单上的表现</h2>
<p>担心Go的热度？来看看它在各大权威榜单上的表现吧：</p>
<ul>
<li><strong>JetBrains语言潜力指数:</strong> Go排名 <strong>第4</strong>，仅次于TypeScript, Rust, Python，显示出强大的增长潜力和用户粘性。</li>
<li><strong>Stack Overflow开发者调查:</strong> 在“受喜爱和期望” (Admired and Desired) 榜单中，Go从去年的第9位<strong>跃升至第7位</strong>，超过了C#和Shell。</li>
<li><strong>GitHub Octoverse:</strong> 稳定保持在 <strong>Top 10</strong> 编程语言之列，并且是 <strong>Top 3增长最快的语言之一</strong> (开源项目活跃度)。</li>
<li><strong>Cloudflare Radar (API客户端语言):</strong> Go在2024年 <strong>超越Node.js</strong>，成为自动化API请求最常用的语言，占比约<strong>12%</strong> (去年为8.4%)。</li>
<li><strong>TIOBE指数:</strong> Go从2023年的第13位<strong>大幅攀升至第7位</strong>，达到自2009年以来的最高排名！**</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/jetbrains-2024-go-report-analysis-4.png" alt="" /><br />
<center>TIOBE 2025.04榜单</center></p>
<p><strong>这些数据有力地证明，Go语言不仅没有衰退，反而在多个维度上保持着强劲的势头。</strong></p>
<hr />
<h2>Go vs Rust：是对手还是队友？</h2>
<p>报告特别提到了Go与同样热门的Rust的关系。结论是：<strong>它们更多是互补，而非直接竞争</strong>。</p>
<ul>
<li><strong>Go:</strong> 更易上手，开发效率高，非常适合云服务、微服务、API、CLI开发，强调 <strong>快速开发和可伸缩性</strong>。</li>
<li><strong>Rust:</strong> 性能极致，适用于性能密集型、底层嵌入式开发，但<strong>复杂性更高，开发成本和时间也更高</strong>。</li>
</ul>
<p>许多公司会同时使用这两种语言，根据场景需求选择最合适的工具。对Rust感兴趣的Go开发者增多，并不意味着Go市场份额的下降。</p>
<hr />
<h2>Go的未来之路：聚焦云原生与拥抱GenAI</h2>
<p>展望未来，Go团队将继续<strong>聚焦云原生领域</strong>，满足其对<strong>开发效率 (time to value)、可靠性和可伸缩性</strong>的核心需求。</p>
<p>一个令人兴奋的新方向是<strong>生成式AI (GenAI) 基础设施</strong>。虽然Go在传统机器学习领域不如Python，但其在性能和可伸缩性上的优势，使其成为构建<strong>AI模型服务 (model serving)</strong>等生产级AI基础设施的理想选择。</p>
<ul>
<li>主流AI平台 (OpenAI, Google AI等) 已提供<strong>Go SDK</strong>。</li>
<li>Go的GenAI生态正在成长，涌现出如<a href="https://github.com/ollama/ollama/">Ollama</a>, <a href="https://github.com/tmc/langchaingo">LangChain Go</a>, <a href="https://github.com/kserve/kserve">kserve</a>等工具。</li>
<li><strong>GenAI基础设施本身，就像云基础设施一样，正在越来越多地用Go编写。</strong></li>
</ul>
<p>报告还提到，<a href="https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team/">Go项目领导层虽有变动</a>（Russ Cox卸任，Austin Clements和Cherry Mui接任），但新领导层对Go的理念和目标有深刻理解，确保了项目的连续性和稳定性。<strong><a href="https://tonybai.com/2025/02/16/some-changes-in-go-1-24/">Go 1.24</a>已于2025年2月发布，未来可期。</strong></p>
<hr />
<h2>总结：黄金时代，未来可期</h2>
<p>总而言之，JetBrains这份详尽的报告描绘了一个清晰的画面：</p>
<p><strong>2024年，Go语言不仅保持了稳定发展，更在云原生领域巩固了核心地位，并在GenAI基础设施等新兴领域展现出强劲潜力。它正步入一个成熟且充满机遇的“黄金时代”</strong>。</p>
<p>对于Gopher们来说，持续深耕云原生，关注Go在AI基础设施的应用，无疑是明智的选择。</p>
<p>那么，<strong>你认为Go语言的下一个增长点会在哪里？你对Go的未来有什么看法？</strong></p>
<p><strong>欢迎在评论区留下你的真知灼见，一起交流探讨！</strong></p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</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，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</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>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go工具链版本已不由你定：go和toolchain指令详解</title>
		<link>https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod/</link>
		<comments>https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod/#comments</comments>
		<pubDate>Mon, 13 Jan 2025 22:45:22 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[auto]]></category>
		<category><![CDATA[backward-compatibility]]></category>
		<category><![CDATA[forward-compatibility]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-mod-tidy]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[go1compat]]></category>
		<category><![CDATA[goget]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[gotoolchain]]></category>
		<category><![CDATA[goversion]]></category>
		<category><![CDATA[language-version]]></category>
		<category><![CDATA[local]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[tool]]></category>
		<category><![CDATA[toolchain]]></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">https://tonybai.com/?p=4461</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod Go语言自诞生以来，就一直将向后兼容性作为其核心理念之一。Go1兼容性承诺确保了为Go1.0编写的代码能够在后续的Go1.x版本中持续正确地编译和运行。这一承诺为Go的成功奠定了坚实的基础，它不仅保障了稳定性，也大大减轻了随着语言演进带来的代码维护负担。然而，兼容性的内涵并不仅限于向后兼容。向前兼容性，即旧版本的工具链能够优雅地处理针对新版本编写的代码，对于打造流畅的开发体验同样至关重要。 在Go 1.21版本之前，向前兼容性在某种程度上是一个被忽视的领域。尽管go.mod文件中的go指令可以标明模块预期的Go版本，但在实际中，它更像是一个指导性建议，而非强制性规则。旧版本的Go工具链会尝试编译那些需要较新版本的代码，这经常导致令人困惑的错误，更有甚者会出现“静默成功”的情况——代码虽然可以编译，但由于较新版本中的细微改动，其运行时行为可能并不正确。 Go 1.21的发布标志着这一现状的重大转变。该版本引入了健壮且自动化的工具链管理机制，将go指令转变为一项强制性要求，并简化了使用不同Go版本进行开发的工作流程。即将发布的Go 1.24版本在此基础上进一步增强，引入了tool指令，允许开发者指定对外部工具及其特定版本的依赖，从而进一步提升了代码的可重复性和项目的可维护性。 这些改进进一步明确和巩固了go命令作为全方位依赖管理器的角色定位，它不仅管理外部模块，还负责管理Go工具链版本，以及越来越多的外部开发工具（如下图）： 不过向前兼容性规则的明确以及toolchain指令的引入也给Go开发者带来一定的理解上的复杂性，并且在使用Go 1.21版本之后，我们可能遇到会遇到一些因Go工具链版本选择而导致的编译问题。 本文将通过一系列典型场景和详细的示例，帮助读者全面理解Go向前兼容性的规则，以及go指令以及toolchain指令对Go工具链选择的细节，从而让大家能更加自信地驾驭Go开发中不断演进的技术环境。 接下来，我们就从对向前兼容性的理解开始！ 1. 理解向前兼容性 向前兼容性，在编程语言的语境中，指的是旧版本的编译器或运行时环境能够处理针对该语言的新版本编写的代码。它与向后兼容性相对，后者确保的是新版本的语言能够处理为旧版本编写的代码。向后兼容性对于维护现有代码库至关重要，而向前兼容性则是在使用不断演进的语言和依赖项时获得流畅开发体验的关键所在。 向前兼容性的挑战源于新语言版本通常会引入新的特性、语法变更或对标准库的修改。如果旧的工具链遇到了依赖于这些新元素的代码，它可能无法正确地编译或解释这些代码。理想情况下，工具链应该能够识别出代码需要一个更新的版本，并提供清晰的错误提示，从而阻止编译或执行。 在Go 1.21之前的版本中，向前兼容性并没有得到严格的保证。让我们来看一个例子。我们用Go 1.18泛型语法编写一个泛型函数Print： // toolchain-directive/demo1/mymodule.go package mymodule func Print[T any](s T) { println(s) } // toolchain-directive/demo1/go.mod module mymodule go 1.18 如果你尝试使用Go 1.17版本来构建这个模块，你将会遇到类似以下的错误： $go version go version go1.17 darwin/amd64 $go build # mymodule ./mymodule.go:3:6: missing function body ./mymodule.go:3:11: [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-go-and-toolchain-in-go-dot-mod-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod">本文永久链接</a> &#8211; https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod</p>
<p>Go语言自诞生以来，就一直将向后兼容性作为其核心理念之一。<a href="https://go.dev/doc/go1compat">Go1兼容性承诺</a>确保了为Go1.0编写的代码能够在后续的Go1.x版本中持续正确地编译和运行。这一承诺为Go的成功奠定了坚实的基础，它不仅保障了稳定性，也大大减轻了随着语言演进带来的代码维护负担。然而，<strong>兼容性的内涵并不仅限于向后兼容</strong>。向前兼容性，即旧版本的工具链能够优雅地处理针对新版本编写的代码，对于打造流畅的开发体验同样至关重要。</p>
<p>在<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21">Go 1.21版本</a>之前，向前兼容性在某种程度上是一个被忽视的领域。尽管go.mod文件中的go指令可以标明模块预期的Go版本，但在实际中，它更像是一个指导性建议，而非强制性规则。旧版本的Go工具链会尝试编译那些需要较新版本的代码，这经常导致令人困惑的错误，更有甚者会出现“静默成功”的情况——代码虽然可以编译，但由于较新版本中的细微改动，其运行时行为可能并不正确。</p>
<p>Go 1.21的发布标志着这一现状的重大转变。该版本引入了健壮且自动化的工具链管理机制，将go指令转变为一项强制性要求，并简化了使用不同Go版本进行开发的工作流程。即将发布的Go 1.24版本在此基础上进一步增强，<a href="https://tonybai.com/2024/12/17/go-1-24-foresight-part2/">引入了tool指令</a>，允许开发者指定对外部工具及其特定版本的依赖，从而进一步提升了代码的可重复性和项目的可维护性。</p>
<p>这些改进进一步明确和巩固了go命令作为全方位依赖管理器的角色定位，它不仅管理外部模块，还负责管理Go工具链版本，以及越来越多的外部开发工具（如下图）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-and-toolchain-in-go-dot-mod-2.png" alt="" /></p>
<p>不过向前兼容性规则的明确以及toolchain指令的引入也给Go开发者带来一定的理解上的复杂性，并且在使用Go 1.21版本之后，我们可能遇到会遇到一些因Go工具链版本选择而导致的编译问题。</p>
<p>本文将通过一系列典型场景和详细的示例，帮助读者全面理解Go向前兼容性的规则，以及go指令以及toolchain指令对Go工具链选择的细节，从而让大家能更加自信地驾驭Go开发中不断演进的技术环境。</p>
<p>接下来，我们就从对向前兼容性的理解开始！</p>
<h2>1. 理解向前兼容性</h2>
<p>向前兼容性，在编程语言的语境中，指的是旧版本的编译器或运行时环境能够处理针对该语言的新版本编写的代码。它与向后兼容性相对，后者确保的是新版本的语言能够处理为旧版本编写的代码。向后兼容性对于维护现有代码库至关重要，而向前兼容性则是在使用不断演进的语言和依赖项时获得流畅开发体验的关键所在。</p>
<p>向前兼容性的挑战源于新语言版本通常会引入新的特性、语法变更或对标准库的修改。如果旧的工具链遇到了依赖于这些新元素的代码，它可能无法正确地编译或解释这些代码。理想情况下，工具链应该能够识别出代码需要一个更新的版本，并提供清晰的错误提示，从而阻止编译或执行。</p>
<p>在Go 1.21之前的版本中，向前兼容性并没有得到严格的保证。让我们来看一个例子。我们用Go 1.18泛型语法编写一个泛型函数Print：</p>
<pre><code>// toolchain-directive/demo1/mymodule.go
package mymodule

func Print[T any](s T) {
    println(s)
}

// toolchain-directive/demo1/go.mod
module mymodule

go 1.18
</code></pre>
<p>如果你尝试使用Go 1.17版本来构建这个模块，你将会遇到类似以下的错误：</p>
<pre><code>$go version
go version go1.17 darwin/amd64

$go build
# mymodule
./mymodule.go:3:6: missing function body
./mymodule.go:3:11: syntax error: unexpected [, expecting (
note: module requires Go 1.18
</code></pre>
<p>这些错误信息具有一定的误导性，它们指向的是语法错误，而不是问题的本质：这段代码使用了<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18版本中才引入的泛型特性</a>。虽然go命令确实打印了一条有用的提示(note: module requires Go 1.18)，但对于规模大一些的项目来说，在满屏的编译错误中，这条提示很容易被忽略。</p>
<p>而比上面这个示例更隐蔽的问题是所谓的“<strong>静默成功</strong>”。</p>
<p>设想这样一个场景：Go标准库中的某个bug在Go 1.19版本中被修复了。你编写了一段代码，并在不知情的情况下依赖于这个bug修复。如果你没有使用任何Go 1.19版本特有的语言特性，并且你的go.mod文件中指定的是go 1.19，那么旧版本的Go 1.18工具链将会毫无怨言地编译你的代码并获得成功。然而，在运行这段代码时，你的程序可能会表现出不正确的行为，因为那个bug在Go 1.18的标准库中依然存在。这就是“静默成功”——编译过程没有任何错误提示，但最终生成的程序却是有缺陷的。</p>
<p>在Go 1.21版本之前，go.mod文件中的go指令更多的是一种指导性意见。它表明了期望使用的Go版本，但旧的工具链并不会严格执行它。这种执行上的疏漏是导致Go开发者面临向前兼容性挑战的主要原因。</p>
<p>Go 1.21版本从根本上改变了go指令的处理方式。它不再是一个可有可无的建议，而是一个强制性的规则。下面我们就来看看Go 1.21及更高版本中是如何确保向前兼容性的。由于多数情况下，我们不会显式在go.mod显式指定toolchain指令，因此，我们<strong>先来看看没有显式指定toolchain指令时，go指令对向前兼容性的影响</strong>。</p>
<h2>2. 作为规则的go指令：确保向前兼容性（Go 1.21及更高版本）</h2>
<p>Go 1.21对Go version、language version、release version等做了更明确的定义，我们先来看一下，这对后续理解go.mod文件中go指令的作用很有帮助。下图形象的展示了各个version之间的关系：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-and-toolchain-in-go-dot-mod-3.png" alt="" /></p>
<p>Go版本(Go Version)，也是发布版本(Release Version)使用1.N.P的版本号形式，其中1.N称为语言版本(language version)，表示实现该版本Go语言和标准库的Go版本的整体系列。1.N.P是1.N语言版本的一个实现，初始实现是1.N.0，也是1.N的第一次发布！后续的1.N.P成为1.N的补丁发布。</p>
<p>任何两个Go版本(Go version)都可以进行比较，以判断一个是小于、大于还是等于另一个。</p>
<p>如果语言版本不同，则语言版本的比较结果决定Go版本的大小。比如：1.21.9 vs. 1.22，前者的语言版本是1.21，后者语言版本是1.22，因此1.21.9 &lt; 1.22。</p>
<p>如果语言版本相同，从小到大的排序为：语言版本本身、按R排序的候选版本(1.NrcR)，然后按P排序的发布版本，例如：</p>
<pre><code>1.21 &lt; 1.21rc1 &lt; 1.21rc2 &lt; 1.21.0 &lt; 1.21.1 &lt; 1.21.2。
</code></pre>
<p>在Go 1.21之前，Go初始发布版本为1.N，而不是1.N.0，因此对于N &lt; 21，排序被调整为将1.N放在候选版本(rc)之后，例如：</p>
<pre><code>1.20rc1 &lt; 1.20rc2 &lt; 1.20rc3 &lt; 1.20 &lt; 1.20.1。
</code></pre>
<p>更早期版本的Go有beta发布，例如1.18beta2。Beta发布在版本排序中被放置在候选版本之前，例如：</p>
<pre><code>1.18beta1 &lt; 1.18beta2 &lt; 1.18rc1 &lt; 1.18 &lt; 1.18.1。
</code></pre>
<p>有了上述对Go version等的理解，我们再来看看go.mod中go指令在向前兼容性规则中的作用。</p>
<p>Go 1.21及更高版本中，go.mod文件中的go指令声明了使用模块或工作空间(workspace)所需的最低Go版本。出于兼容性原因，如果go.mod文件中省略了go指令行(通常我们都不这么做)，则该模块被视为隐式使用go 1.16这个指令行；如果go.work文件中省略了go指令行，则该工作空间被视为隐式使用go 1.18这个指令行。</p>
<p>那么，Go 1.21及更高版本的Go工具链在遇到go.mod中go指令行中的Go版本高于自身时会怎么做呢？下面我们通过四个场景的示例来看一下。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-and-toolchain-in-go-dot-mod-4.png" alt="" /></p>
<ul>
<li>场景一</li>
</ul>
<p>当前本地工具链go 1.22.0，go.mod中go指令行为go 1.23.0：</p>
<pre><code>// toolchain-directive/demo2/scene1/go.mod
module scene1

go 1.23.0
</code></pre>
<p>执行构建：</p>
<pre><code>$go build
go: downloading go1.23.0 (darwin/amd64)
... ...
</code></pre>
<p>Go自动下载当前go module中go指令行中的Go工具链版本并对当前module进行构建。</p>
<ul>
<li>场景二</li>
</ul>
<p>当前本地工具链go 1.22.0，go.mod中go指令行为go 1.22.0，但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1：</p>
<pre><code>// toolchain-directive/demo2/scene2/go.mod
module scene2

go 1.22.0

require (
    github.com/bigwhite/a v1.0.0
) 

replace github.com/bigwhite/a =&gt; ../a
</code></pre>
<p>执行构建：</p>
<pre><code>$go build
go: module ../a requires go &gt;= 1.23.1 (running go 1.22.0)
</code></pre>
<p>Go发现当前go module依赖的go module中go指令行中的Go版本比当前module的更新，则会输出错误提示！</p>
<ul>
<li>场景三</li>
</ul>
<p>当前本地工具链go 1.22.0，go.mod中go指令行为go 1.22.0，但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1，而依赖的github.com/bigwhite/b的go.mod中go指令行为go 1.23.2：</p>
<pre><code>// toolchain-directive/demo2/scene3/go.mod
module scene3

go 1.22.0

require (
    github.com/bigwhite/a v1.0.0
    github.com/bigwhite/b v1.0.0
) 

replace github.com/bigwhite/a =&gt; ../a
replace github.com/bigwhite/b =&gt; ../b
</code></pre>
<p>执行构建：</p>
<pre><code>$go build
go: module ../b requires go &gt;= 1.23.2 (running go 1.22.0)
</code></pre>
<p>Go发现当前go module依赖的go module中go指令行中的Go版本比当前module的更新，则会输出错误提示！并且选择了满足依赖构建的最小的Go工具链版本。</p>
<ul>
<li>场景四</li>
</ul>
<p>当前本地工具链go 1.22.0，go.mod中go指令行为go 1.23.0，但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1，而依赖的github.com/bigwhite/b的go.mod中go指令行为go 1.23.2：</p>
<pre><code>// toolchain-directive/demo2/scene4/go.mod
module scene4

go 1.23.0

require (
    github.com/bigwhite/a v1.0.0
    github.com/bigwhite/b v1.0.0
) 

replace github.com/bigwhite/a =&gt; ../a
replace github.com/bigwhite/b =&gt; ../b
</code></pre>
<p>执行构建：</p>
<pre><code>$go build
go: downloading go1.23.0 (darwin/amd64)
... ..
</code></pre>
<p>Go发现当前go module依赖的go module中go指令行中的Go版本与当前module的兼容，但比本地Go工具链版本更新，则会下载当前go module中go指令行中的Go版本进行构建。</p>
<p>从以上场景的执行情况来看，只有选择了当前go module的工具链版本时，才会继续构建下去，如果本地找不到这个版本的工具链，go会自动下载该版本工具链再进行编译(前提是GOTOOLCHAIN=auto)。如果像场景2和场景3那样，依赖的module的最低Go version大于当前module的go version，那么Go会提示错误并结束编译！后续你需要显式指定要使用的工具链才能继续编译！以场景3为例，通过GOTOOLCHAIN显式指定工具链，我们可以看到下面结果：</p>
<pre><code>// demo2/scene3

$GOTOOLCHAIN=go1.22.2 go build
go: downloading go1.22.2 (darwin/amd64)
^C

$GOTOOLCHAIN=go1.23.3 go build
go: downloading go1.23.3 (darwin/amd64)
.. ...
</code></pre>
<p>我们看到，go完全相信我们显式指定的工具链版本，即使是不满足依赖module的最低go版本要求的！</p>
<p>想必大家已经感受到支持新向前兼容规则带来的复杂性了！这里我们还没有显式使用到toolchain指令行呢！但其实，在上述场景中，虽然我们没有在go.mod中显式使用toolchain指令行，但Go模块会使用隐式的toolchain指令行，其隐式的默认值为toolchain goV，其中V来自go指令行中的Go版本，比如go1.22.0等。</p>
<p>接下来我们就简单地看看toolchain指令行，我们的宗旨是尽量让事情变简单，而不是变复杂！</p>
<h2>3. toolchain指令行与GOTOOLCHAIN</h2>
<p><a href="https://go.dev/ref/mod#go-mod-file-toolchain">Go mod的参考手册</a>告诉我们：toolchain指令仅在模块为主模块且默认工具链的版本低于建议的工具链版本时才有效，并建议：Go toolchain指令行中的go工具链版本不能低于在go指令行中声明的所需Go版本。</p>
<p>也就是说如果对toolchain没有特殊需求，我们还是尽量隐式的使用toolchain，即保持toolchain与go指令行中的go版本一致。</p>
<p>另外一个影响go工具链版本选择的是GOTOOLCHAIN环境变量，它的值决定了go命令的行为，特别是当go.mod文件中指定的Go版本（通过go或toolchain指令）与当前运行的go命令的版本不同时，GOTOOLCHAIN的作用就体现出来了。</p>
<p>GOTOOLCHAIN可以设置为以下几种形式：</p>
<ul>
<li>
<p>local: 这是最简单的形式，它指示go命令始终使用其自带的捆绑工具链，不允许自动下载或切换到其他工具链版本。即使go.mod文件要求更高的版本，也不会切换。如果版本不满足，则会报错。</p>
</li>
<li>
<p>\&lt;name&#62; (例如go1.21.3): 这种形式指示go命令使用特定名称的Go工具链。如果系统中存在该名称的可执行文件（例如在PATH环境变量中找到了go1.21.3），则会执行该工具链。否则，go命令会尝试下载并使用名为\&lt;name&#62;的工具链。如果下载失败或找不到，则会报错。</p>
</li>
<li>
<p>auto(或local+auto): 这是默认设置。在这种模式下，go命令的行为最为智能。它首先检查当前使用的工具链版本是否满足go.mod文件中go和toolchain指令的要求。如果不满足，它会根据如下规则尝试切换工具链。</p>
</li>
</ul>
<pre><code>- 如果go.mod中有toolchain行且指定的工具链名称比当前默认的工具链更新，则切换到toolchain行指定的工具链。
- 如果go.mod中没有有效的toolchain行（例如toolchain default或没有toolchain行），但go指令行指定的版本比当前默认的工具链更新，则切换到与go指令行版本相对应的工具链（例如go 1.23.1对应go1.23.1工具链）。
- 在切换时，go命令会优先在本地路径（PATH环境变量）中寻找工具链的可执行文件，如果找不到，则会下载并使用。
</code></pre>
<ul>
<li>
<p>\&lt;name&#62;+auto: 这种形式与auto类似，但它指定了一个默认的工具链\&lt;name&#62;。go命令首先尝试使用\&lt;name&#62;工具链。如果该工具链不满足go.mod文件中的要求，它会按照与auto模式相同的规则尝试切换到更新的工具链。这种方式可以用来设定一个高于内置版本的最低版本要求，同时又允许根据需要自动升级。</p>
</li>
<li>
<p>\&lt;name&#62;+path (或local+path): 这种形式与\&lt;name&#62;+auto类似，也指定了一个默认的工具链\&lt;name&#62;。不同之处在于，它<strong>禁用</strong>了自动下载功能。go命令首先尝试使用\&lt;name&#62;工具链，如果不满足要求，它会在本地路径中搜索符合要求的工具链，但不会尝试下载。如果找不到合适的工具链，则会报错。</p>
</li>
</ul>
<p>大多数情况我们会使用GOTOOLCHAIN的默认值，即在auto模式下。但是如果在国内自动下载go版本不便的情况下，可以使用local模式，这样在本地工具链版本不满足的情况下，可以尽快得到错误。或是通过\&lt;name&#62;强制指定使用特定版本的工具链，这样可以实现对组织内采用的工具链版本的精准控制，避免因工具链版本不一致而导致的问题。</p>
<h2>4. 使用go get管理Go指令行和toolchain指令行</h2>
<p>自go module诞生以来，我们始终可以使用go get对go module的依赖进行管理，包括添加/删除依赖，升降依赖版本等。</p>
<p>就像本文开头的那个图中所示，go命令作为全方位依赖管理器的角色定位，它不仅管理外部模块，还负责管理Go工具链版本，以及越来越多的外部开发工具。因此我们也可以使用go get管理指令行和toolchain指令行。</p>
<p>例如，go get go@1.22.1 toolchain@1.24rc1将改变主模块的go.mod文件，将go指令行改为go 1.22.1，将toolchain指令行改为toolchain go1.24rc1。我们要保证toolchain指令行中的版本始终等于或高于go指令行中的版本。</p>
<p>当toolchain指令行与go指令行完全匹配时，可以省略和隐含，所以go get go@1.N.P时可能会删除toolchain行。</p>
<p>反过来也是这样，当go get toolchain@1.N.P时，如果1.N.P &lt; go指令行的版本，go指令行也会随之被降级为1.N.P，这样就和toolchain版本一致了，toolchain指令行可能会被删除。</p>
<p>我们也可以通过下面go get命令显式删除toolchain指令行：</p>
<pre><code>$go get toolchain@none
</code></pre>
<p>通过go get管理Go指令行和toolchain指令行还会对require中依赖的go module版本产生影响，反之使用go get管理require中依赖的go module版本时，也会对Go指令行和toolchain指令行的版本产生影响！不过这一切都是通过go get自动完成的！下面我们通过示例来具体说明一下。</p>
<p>我们首先通过示例看看go get管理go指令行对require中依赖的Go模块版本的影响。</p>
<p>当你使用go get升级或降级go.mod文件中的go指令行时，go get 会根据新的Go版本要求，自动调整require指令行中依赖模块的版本，以满足新的兼容性要求。比如下面这个升级go版本导致依赖模块升级的示例。</p>
<p>假设你的模块mymodule的go.mod文件内容如下：</p>
<pre><code>module example.com/mymodule

go 1.21.0

require (
    example.com/moduleA v1.1.0 // 兼容Go 1.21.0
    example.com/moduleB v1.2.0 // 兼容Go 1.21.0
)
</code></pre>
<p>example.com/moduleA和example.com/moduleB的v1.1.0和v1.2.0版本都只兼容到Go 1.21.0。</p>
<p>现在，你执行以下命令升级Go版本：</p>
<pre><code>$go get go@1.23.1
</code></pre>
<p>go get会将go.mod文件中的go指令行更新为go 1.23.1。同时，它会检查require指令行中的依赖模块，发现example.com/moduleA和example.com/moduleB的v1.1.0和v1.2.0版本可能不兼容Go1.23.1。</p>
<p>假设example.com/moduleA和example.com/moduleB都有更新的版本v1.3.0，且兼容Go 1.23.1，那么go get会自动将require指令行更新为：</p>
<pre><code>module example.com/mymodule

go 1.23.1

require (
    example.com/moduleA v1.3.0 // 兼容Go 1.23.1
    example.com/moduleB v1.3.0 // 兼容Go 1.23.1
)
</code></pre>
<p>如果找不到兼容Go 1.23.1 的版本，go get可能会报错，提示无法找到兼容新Go版本的依赖模块。</p>
<p>同理，降低go版本也可能触发require中依赖模块降级。我们来看下面示例：</p>
<p>假设你的模块mymodule的go.mod文件内容如下：</p>
<pre><code>module example.com/mymodule

go 1.23.1

require (
    example.com/moduleA v1.3.0 // 兼容 Go 1.22.0及以上
    example.com/moduleB v1.3.0 // 兼容 Go 1.22.0及以上
)
</code></pre>
<p>现在，你执行以下命令降低go版本：</p>
<pre><code>$go get go@1.22.0
</code></pre>
<p>执行以上命令后，go.mod文件内容变为：</p>
<pre><code>module example.com/mymodule

go 1.22.0

require (
    example.com/moduleA v1.1.0 // 兼容Go 1.21.0及以上
    example.com/moduleB v1.2.0 // 兼容Go 1.21.0及以上
)
</code></pre>
<p>在这个例子中, go get go@1.22.0命令会将go指令行降级为go 1.22.0, 同时, go get会自动检查所有依赖项, 并尝试将它们降级到与go 1.22.0兼容的最高版本。在这个例子中, example.com/moduleA和example.com/moduleB都被降级到了与go 1.22.0兼容的最高版本。</p>
<p>反过来，使用go get管理require中依赖的Go模块版本时，也会对go指令行产生影响，我们看一个添加依赖导致go指令行版本升级的示例。</p>
<p>假设你的模块mymodule的go.mod文件内容如下：</p>
<pre><code>module example.com/mymodule

go 1.21.0

require (
    example.com/moduleA v1.1.0 // 兼容 Go 1.21.0
)
</code></pre>
<p>现在，你需要添加一个新的依赖项example.com/moduleC，而example.com/moduleC的最新版本v1.2.0的go.mod文件中指定了go 1.22.0：</p>
<pre><code>// example.com/moduleC 的 go.mod
module example.com/moduleC

go 1.22.0

require (
    ...
)
</code></pre>
<p>你执行以下命令添加依赖：</p>
<pre><code>$go get example.com/moduleC@v1.2.0
</code></pre>
<p>go get会发现example.com/moduleC的版本v1.2.0需要 Go 1.22.0，而你的模块当前只兼容Go 1.21.0。因此，go get会自动将你的模块的go.mod文件更新为：</p>
<pre><code>module example.com/mymodule

go 1.22.0

require (
    example.com/moduleA v1.1.0 // 兼容Go 1.21.0
    example.com/moduleC v1.2.0 // 需要Go 1.22.0
)
</code></pre>
<p>go指令行被升级到了go 1.22.0，以满足新添加的依赖项的要求。</p>
<p>不过无论如何双向影响，我们只要记住一个原则就够了，那就是<strong>go get和go mod tidy命令使go指令行中的Go版本始终保持大于或等于任何所需依赖模块的go指令行中的Go版本</strong>。</p>
<h2>5. 小结</h2>
<p>本文深入探讨了Go语言在版本管理和工具链兼容性方面的重要变革，特别是Go 1.21及以后的版本如何强化向前兼容性。在文章里，我强调了向后兼容性和向前兼容性在开发体验中的重要性，以及如何通过go指令和新引入的toolchain指令来管理工具链版本。</p>
<p>通过文中的示例，我展示了如何在不同场景下处理Go模块的兼容性问题，并解释了GOTOOLCHAIN环境变量如何影响工具链选择。最后，我还举例说明了如何通过使用go get命令有效管理Go指令和依赖模块的版本，确保代码的可维护性和稳定性。</p>
<p>不过我们也看到了，为了实现精确的向前兼容，Go引入了不少复杂的规则，短时间内记住这些规则还是有门槛的，我们只能在实践中慢慢吸收和理解。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/toolchain-directive">这里</a>下载。</p>
<h2>6. 参考资料</h2>
<ul>
<li><a href="https://go.dev/doc/toolchain">Go Toolchains</a> &#8211; https://go.dev/doc/toolchain</li>
<li><a href="https://go.dev/blog/toolchain">Forward Compatibility and Toolchain Management in Go 1.21</a> &#8211; https://go.dev/blog/toolchain</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们>将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流<br />
和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾<br />
。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入</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，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</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>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用issue2md将Github issue转换为Markdown</title>
		<link>https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/</link>
		<comments>https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/#comments</comments>
		<pubDate>Mon, 23 Dec 2024 15:02:42 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Claude]]></category>
		<category><![CDATA[cli]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go测试]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[hub]]></category>
		<category><![CDATA[issue]]></category>
		<category><![CDATA[issue2md]]></category>
		<category><![CDATA[issue2mdweb]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[markitdown]]></category>
		<category><![CDATA[omit]]></category>
		<category><![CDATA[omitzero]]></category>
		<category><![CDATA[sonnet]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[vercel]]></category>
		<category><![CDATA[Wechat]]></category>
		<category><![CDATA[加餐]]></category>
		<category><![CDATA[命令行]]></category>
		<category><![CDATA[极客时间]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4447</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md 到2024年底，不论你是否承认，AI时代都已经到来！近两个月，三大顶级商业AI模型巨头：Claude Sonnet 3.5、Google Gemini 2.0 Flash Experimental以及ChatGPT o3你方唱罢我登场，好不热闹！ 作为走在AI应用前沿的程序员，利用AI辅助自己提高学习和工作实践的效率都是必不可少的。在使用AI的过程中，我们经常需要向其提供一些文档资料，对于文字资料，AI更偏爱TXT、Markdown、PDF等格式的文件。部署在Vercel上的MarkdownDown支持输入网页URL并将其转换为Markdown，而微软开源的MarkItdown则能将多种格式(pdf、ppt、word、html、zip等)转换为Markdown。这些工具在实践中帮助我们实现对AI的快速“投喂”。 然而，一些资料，如GitHub Issues，尚不能通过上述工具方便地转换为干净的、无额外干扰内容的Markdown或其他适合投喂给AI的格式。受到MarkdownDown的启发，我思考是否可以将GitHub Issues转换为Markdown，最终促成了issue2md这个想法。该工具旨在简化GitHub Issues与Markdown之间的转换过程，使得开发者可以更高效地利用AI理解Github issue中的内容，包括用户讨论中的一些观点和想法。 三个月前，我利用AI完成了issue2md这个小工具，我自己甚至没有写下一行代码。我仅仅对其提出一个小小的要求，那就是不要依赖任何第三方包，仅可以依赖Go标准库。在这三个月中，该工具给了我很大的帮助，将由它生成的Github Issue对应的Markdown文档投喂给AI后，可以让我快速理解Github issue的要点，尤其是那些历经几年讨论，积累了数百条comment的issue！ 这里我将issue2md放到github上供大家下载使用，也希望能给大家带去相同的帮助。 下面简单介绍一下issue2md的用法。 issue2md项目有两个工具，或者说两种使用模式，一种是命令行模式，使用issue2md这个命令行工具。另外一种则是Web模式，使用issue2mdweb这个工具。 如果你喜欢命令行模式，那么你只需要使用下面命令安装issue2md即可： $go install github.com/bigwhite/issue2md/cmd/issue2md@latest issue2md cli程序的使用方法非常简单： Usage: issue2md issue-url [markdown-file] Arguments: issue-url The URL of the GitHub issue to convert. markdown-file (optional) The output markdown file. 它的第一个参数是github issue的URL。以Go 1.24版本json包增加对omitzero的支持的issue为例，它的url是https://github.com/golang/go/issues/45669，我们原封不动的将其作为issue2md的第一个参数执行： $issue2md https://github.com/golang/go/issues/45669 Issue [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/convert-github-issue-to-markdown-with-issue2md-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md">本文永久链接</a> &#8211; https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md</p>
<p>到2024年底，不论你是否承认，<a href="https://tonybai.com/2024/10/14/programming-in-ai-era">AI时代都已经到来</a>！近两个月，三大顶级商业AI模型巨头：<a href="https://claude.ai/chats">Claude Sonnet 3.5</a>、<a href="https://aistudio.google.com/">Google Gemini 2.0 Flash Experimental</a>以及<a href="https://openai.com/12-days/">ChatGPT o3</a>你方唱罢我登场，好不热闹！</p>
<p>作为走在AI应用前沿的程序员，利用AI辅助自己提高学习和工作实践的效率都是必不可少的。在使用AI的过程中，我们经常需要向其提供一些文档资料，对于文字资料，AI更偏爱TXT、Markdown、PDF等格式的文件。<a href="https://markdowndown.vercel.app/">部署在Vercel上的MarkdownDown</a>支持输入网页URL并将其转换为Markdown，而<a href="https://github.com/microsoft/markitdown">微软开源的MarkItdown</a>则能将多种格式(pdf、ppt、word、html、zip等)转换为Markdown。这些工具在实践中帮助我们实现对AI的快速“投喂”。</p>
<p>然而，一些资料，如GitHub Issues，尚不能通过上述工具方便地转换为干净的、无额外干扰内容的Markdown或其他适合投喂给AI的格式。受到MarkdownDown的启发，我思考是否可以将GitHub Issues转换为Markdown，最终促成了issue2md这个想法。该工具旨在简化GitHub Issues与Markdown之间的转换过程，使得开发者可以更高效地利用AI理解Github issue中的内容，包括用户讨论中的一些观点和想法。</p>
<p>三个月前，我利用AI完成了issue2md这个小工具，我自己甚至没有写下一行代码。我仅仅对其提出一个小小的要求，那就是<strong>不要依赖任何第三方包，仅可以依赖Go标准库</strong>。在这三个月中，该工具给了我很大的帮助，将由它生成的Github Issue对应的Markdown文档投喂给AI后，可以让我快速理解Github issue的要点，尤其是那些历经几年讨论，积累了数百条comment的issue！</p>
<p>这里我将<a href="https://github.com/bigwhite/issue2md">issue2md放到github上</a>供大家下载使用，也希望能给大家带去相同的帮助。</p>
<p>下面简单介绍一下issue2md的用法。</p>
<p>issue2md项目有两个工具，或者说两种使用模式，一种是命令行模式，使用issue2md这个命令行工具。另外一种则是Web模式，使用issue2mdweb这个工具。</p>
<p>如果你喜欢命令行模式，那么你只需要使用下面命令安装issue2md即可：</p>
<pre><code>$go install github.com/bigwhite/issue2md/cmd/issue2md@latest
</code></pre>
<p>issue2md cli程序的使用方法非常简单：</p>
<pre><code>Usage: issue2md issue-url [markdown-file]
Arguments:
  issue-url      The URL of the GitHub issue to convert.
  markdown-file  (optional) The output markdown file.
</code></pre>
<p>它的第一个参数是github issue的URL。以<a href="https://tonybai.com/2024/12/17/go-1-24-foresight-part2/">Go 1.24版本</a>json包<a href="https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero/">增加对omitzero的支持</a>的issue为例，它的url是https://github.com/golang/go/issues/45669，我们原封不动的将其作为issue2md的第一个参数执行：</p>
<pre><code>$issue2md https://github.com/golang/go/issues/45669
Issue and comments saved as Markdown in file golang_go_issue_45669.md
</code></pre>
<p>issue2md cli默认会生成一个命名格式如下的文件：</p>
<pre><code>{owner}_{repo}_issue_number.md
</code></pre>
<p>其内容使用markdown编辑器打开并渲染后将呈现如下的效果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/convert-github-issue-to-markdown-with-issue2md-2.png" alt="" /></p>
<p>当然你也可以通过传入第二个命令行参数，作为最终生成的markdown的文件名！</p>
<p>如果你不喜欢命令行模式，你可以<strong>使用issue2mdweb提供的Web模式</strong>。最简单的启动一个issue2mdweb服务的方法就是利用我发布到Docker hub上的issue2md的公共镜像，你可以像下面这样在本地或你的私有云里运行一个issue2mdweb服务：</p>
<pre><code>$docker run -d -p 8080:8080 bigwhite/issue2mdweb
</code></pre>
<p>然后用你的浏览器打开http://{host}:8080这个地址，你将看到如下的页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/convert-github-issue-to-markdown-with-issue2md-3.png" alt="" /></p>
<p>在中间的文本框中输入你要转换的Github issue地址，比如前面的https://github.com/golang/go/issues/45669，点击“Convert”，你的浏览器就会自动将转换后的Markdown文件下载到你的本地，文件命名和issue2md cli的默认命名格式一致！</p>
<p>如果你不想使用Docker运行，你可以自行下载issue2md代码并编译，也可以使用scripts中的命令将issue2mdweb安装为一个Systemd unit服务！</p>
<p>这里要注意的是，issue2md使用了Go标准口实现了对Github API的访问且没有使用任何账号信息，它仅适合将Public仓库的issue转换为Markdown，并且由于Github对API调用的限速，你在使用issue2md时不能过于频繁！此外，你若发现issue2md的bug或者你有什么新的想法，欢迎<a href="https://github.com/bigwhite/issue2md/issues">在issue2md仓库中提出你宝贵的issue</a>。</p>
<p>最后打个“广告”，根据极客时间的专栏推广计划，我在春节前会为“<a href="http://gk.link/a/10AVZ">Go语言第一课</a>”专栏<a href="https://time.geekbang.org/column/article/835197">续写五篇文章</a>，其中的第一篇“<a href="https://time.geekbang.org/column/article/835208">Go测试的5个使用建议</a>”已经上线。</p>
<p>无论你是“Go语言第一课”的学员，还是首次听说这门专栏的小伙伴，我都欢迎你阅读这些文章，希望这些专栏文章能你带去新的收获！也欢迎你将阅读后的感受在评论区分享出来！</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</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://gopherdaily.tonybai.com</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>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</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; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go官方发布的go.dev给gopher们带来了什么</title>
		<link>https://tonybai.com/2019/11/14/what-the-godev-website-bring-to-gophers/</link>
		<comments>https://tonybai.com/2019/11/14/what-the-godev-website-bring-to-gophers/#comments</comments>
		<pubDate>Thu, 14 Nov 2019 02:44:42 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[cloud-native]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-nuts]]></category>
		<category><![CDATA[go-tour]]></category>
		<category><![CDATA[go.dev]]></category>
		<category><![CDATA[gobridge]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[hello-world]]></category>
		<category><![CDATA[logrus]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[SRE]]></category>
		<category><![CDATA[tgpl]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[命令行]]></category>
		<category><![CDATA[地鼠]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2824</guid>
		<description><![CDATA[众所周知，Go是一个诞生于Google内部的编程语言，它在2009年11月份开源，在开源后立即受到了来自全世界开发人员的关注与贡献。但初期的Go语言的发展依旧是由Go核心团队的若干leader决定的，这种类“民主集中制”的方法延续了若干年。直到Go核心团队逐渐意识到Go应该更多倾听社区的声音，并让更多的gopher参与到Go项目的开发和贡献中来，甚至影响和决定一些语言特定的演化。于是Go团队开始特意为Go社区发展招兵买马。像Steve Francia、Francesc Campoy（后已经从google离职加入Dgraph）等都是在这个阶段加入Go team的。 Go团队在很长一段时间里尤其重视与社区的互动，比如连续多年发起Go user调查、Gophercon大会后的Go team与社区的见面会和分组讨论、去GOPATH降低Go入门学习曲线、发布Go新品牌标识、添加Go module机制、改善官网等。 在今天Go官博发文：“Go.dev: a new hub for Go developers”，正式发布go.dev站点，该站点被Go核心团队寄望于成为全世界Gopher开发人员的中心。它将告诉gopher（无论新手还是老油条）：谁在使用Go、用Go做什么、怎么学习Go(Go的各种学习资源、受欢迎的Go package都有哪些以及这些package的详细信息）。 go.dev发布之后，golang.org官网将更加聚焦go开源项目本身的开发、语言演化以及Go版本发布。而go.dev将成为gopher日常使用go语言的中心，包括go学习、go方案、go应用案例等。在这里我们简单探索一下go.dev这个站点究竟给gopher们带来了什么(这仅仅是go.dev的最小功能发布，后续go.dev可能会演化出更多特性、并根据社区反馈更好满足gopher需求)。 一. 学习资源聚合 go.dev的一个重要功能就是帮助首次进入Go世界的开发人员学习Go。 在go.dev的”learn”栏目下，我们在第一屏就看到了Go新手入门的三个步骤：安装、”Hello World”、Go tour以及更为详尽文档的入口： 接下来，go.dev提供了这些年口碑较好、受到gopher欢迎的一些初级在线学习资源： 像gobyexample.com、gophercises.com都在推荐行列。 Go技术类书籍以及培训资源是gopher学习Go过程中不可缺少的： Go.dev在learn栏目下推荐了一些口碑不错的Go书籍，比如：Alan A. A. Donovan和Brian W. Kernighan合著的Go圣经：《The Go Programming Language》被在首位推荐。知名Go培训师William Kennedy的培训也被推荐给了大家。不过口碑不错的书籍《Go in action》我觉得也应该列入推荐行列。 在Learn栏目最后，是全世界各地近期有关Go的meetup活动的schedule，Gopher可以得到最及时的meetup信息，并选择参加。 二. 成熟解决方案参考 go.dev开辟的”solution”栏目旨在提升go的开发过程。栏目从“云原生和网络服务开发”、“命令行程序开发”、“web开发”以及Devops/Site Reliability四个方面提供聚合化的资料。以“云原生和网络服务开发”为例，Go.dev提供了这方面的典型项目和用户、使用方法、关键方案（一些书籍、成熟框架、客户端库以及其他资源）。 go.dev solution栏目还提供了一些Go的典型客户以及这些客户使用Go的典型案例： 三、Package信息聚合中心 在go.dev的“explore”栏目下，我们看到的是Go package的信息中心： 就如上图所示，这里提供了受欢迎的package和特色package的推荐列表，以及package信息的搜索功能。 以logrus为例： 在logrus包的主页，我们看到了有关logrus的各种信息，项目repo地址、最新版本号、module名字、开源许可证信息、文档（应该是集成了godoc返回的结果）、它的依赖、以及以它为依赖的项目(见下图)： 四. 小结 go.dev目前处于最小产品状态(mvp)，从目前已经提供的栏目来看，go.dev能为gopher提供的帮助已经很全面了。后续go.dev站点的运营好坏（比如：信息更新是否及时等）将决定go.dev是否能达到其预期的期望。 go.dev目前似乎还缺少论坛功能。不过已有的golang-nuts、gobridge已经承担了这个角色，但如果能有一个官方论坛（一站式）就再好不过了。 [...]]]></description>
			<content:encoded><![CDATA[<p>众所周知，<a href="https://tonybai.com/2017/09/24/go-ten-years-and-climbing/">Go是一个诞生于Google内部的编程语言</a>，它在<a href="https://tonybai.com/2019/11/09/go-opensource-10-years/">2009年11月份开源</a>，在开源后立即受到了来自全世界开发人员的关注与贡献。但初期的Go语言的发展依旧是由Go核心团队的若干leader决定的，这种类“民主集中制”的方法延续了若干年。直到Go核心团队逐渐意识到Go应该更多倾听社区的声音，并让更多的gopher参与到Go项目的开发和贡献中来，甚至影响和决定一些语言特定的演化。于是Go团队开始特意为Go社区发展<strong>招兵买马</strong>。像<a href="https://spf13.com/">Steve Francia</a>、<a href="https://campoy.cat/">Francesc Campoy</a>（后已经从google离职加入<a href="https://dgraph.io/">Dgraph</a>）等都是在这个阶段加入Go team的。</p>
<p>Go团队在很长一段时间里尤其重视与社区的互动，比如连续多年发起<a href="https://blog.golang.org/survey2018-results">Go user调查</a>、<a href="https://www.gophercon.com/">Gophercon大会</a>后的Go team<a href="https://blog.golang.org/contributor-workshop">与社区的见面会和分组讨论</a>、<a href="https://tip.golang.org/doc/go1.8#gopath">去GOPATH降低Go入门学习曲线</a>、<a href="https://blog.golang.org/go-brand">发布Go新品牌标识</a>、添加<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go module机制</a>、<a href="https://golang.org/">改善官网</a>等。</p>
<p>在今天Go官博发文：<a href="https://blog.golang.org/go.dev">“Go.dev: a new hub for Go developers”</a>，正式发布<a href="https://go.dev/">go.dev站点</a>，该站点被Go核心团队寄望于成为全世界Gopher开发人员的中心。它将告诉gopher（无论新手还是老油条）：谁在使用Go、用Go做什么、怎么学习Go(Go的各种学习资源、受欢迎的Go package都有哪些以及这些package的详细信息）。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/homepage.png" alt="img{512x368}" /></p>
<p>go.dev发布之后，golang.org官网将更加聚焦go开源项目本身的开发、语言演化以及Go版本发布。而go.dev将成为gopher日常使用go语言的中心，包括go学习、go方案、go应用案例等。在这里我们简单探索一下go.dev这个站点究竟给gopher们带来了什么(这仅仅是go.dev的最小功能发布，后续go.dev可能会演化出更多特性、并根据社区反馈更好满足gopher需求)。</p>
<h2>一. <a href="https://learn.go.dev/">学习资源</a>聚合</h2>
<p>go.dev的一个重要功能就是<strong>帮助首次进入Go世界的开发人员学习Go</strong>。</p>
<p>在go.dev的”learn”栏目下，我们在第一屏就看到了Go新手入门的三个步骤：安装、”Hello World”、Go tour以及<a href="https://golang.org/doc/">更为详尽文档的入口</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/learn-1.png" alt="img{512x368}" /></p>
<p>接下来，go.dev提供了这些年口碑较好、受到gopher欢迎的一些初级在线学习资源：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/learn-2.png" alt="img{512x368}" /></p>
<p>像gobyexample.com、gophercises.com都在推荐行列。</p>
<p>Go技术类书籍以及培训资源是gopher学习Go过程中不可缺少的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/learn-3.png" alt="img{512x368}" /></p>
<p>Go.dev在learn栏目下推荐了一些口碑不错的Go书籍，比如：Alan A. A. Donovan和<a href="https://www.cs.princeton.edu/~bwk/">Brian W. Kernighan</a>合著的<a href="http://www.gopl.io/">Go圣经：《The Go Programming Language》</a>被在首位推荐。知名Go培训师William Kennedy的<a href="https://www.ardanlabs.com/">培训</a>也被推荐给了大家。不过口碑不错的书籍<a href="https://book.douban.com/subject/25858023/">《Go in action》</a>我觉得也应该列入推荐行列。</p>
<p>在Learn栏目最后，是全世界各地近期有关Go的meetup活动的schedule，Gopher可以得到最及时的meetup信息，并选择参加。</p>
<h2>二. 成熟解决方案参考</h2>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/solution-1.png" alt="img{512x368}" /></p>
<p>go.dev开辟的”solution”栏目旨在提升go的开发过程。栏目从“云原生和网络服务开发”、“命令行程序开发”、“web开发”以及Devops/Site Reliability四个方面提供聚合化的资料。以“云原生和网络服务开发”为例，Go.dev提供了这方面的典型项目和用户、使用方法、关键方案（一些书籍、成熟框架、客户端库以及其他资源）。</p>
<p>go.dev solution栏目还提供了一些Go的典型客户以及这些客户使用Go的典型案例：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/solution-2.png" alt="img{512x368}" /></p>
<h2>三、Package信息聚合中心</h2>
<p>在go.dev的“explore”栏目下，我们看到的是Go package的信息中心：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/explore-1.png" alt="img{512x368}" /></p>
<p>就如上图所示，这里提供了受欢迎的package和特色package的推荐列表，以及package信息的搜索功能。</p>
<p>以<a href="https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/">logrus为例</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/explore-2.png" alt="img{512x368}" /></p>
<p>在<a href="https://github.com/sirupsen/logrus">logrus包</a>的主页，我们看到了有关logrus的各种信息，项目repo地址、最新版本号、module名字、开源许可证信息、文档（应该是集成了godoc返回的结果）、它的依赖、以及以它为依赖的项目(见下图)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go.dev/explore-3.png" alt="img{512x368}" /></p>
<h2>四. 小结</h2>
<p>go.dev目前处于最小产品状态(mvp)，从目前已经提供的栏目来看，go.dev能为gopher提供的帮助已经很全面了。后续go.dev站点的运营好坏（比如：信息更新是否及时等）将决定go.dev是否能达到其预期的期望。</p>
<p>go.dev目前似乎还缺少论坛功能。不过已有的<a href="https://groups.google.com/forum/#!topic/golang-nuts/">golang-nuts</a>、<a href="https://forum.golangbridge.org/">gobridge</a>已经承担了这个角色，但如果能有一个官方论坛（一站式）就再好不过了。</p>
<p>go.dev在国内可以访问，就是速度有些慢（可能因地区而异）。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</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>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/11/14/what-the-godev-website-bring-to-gophers/feed/</wfw:commentRss>
		<slash:comments>4</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>使用命令行方式开发Android应用</title>
		<link>https://tonybai.com/2011/05/24/develop-android-app-in-command-line-method/</link>
		<comments>https://tonybai.com/2011/05/24/develop-android-app-in-command-line-method/#comments</comments>
		<pubDate>Tue, 24 May 2011 06:57:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[命令行]]></category>
		<category><![CDATA[安卓]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/2011/05/24/%e4%bd%bf%e7%94%a8%e5%91%bd%e4%bb%a4%e8%a1%8c%e6%96%b9%e5%bc%8f%e5%bc%80%e5%8f%91android%e5%ba%94%e7%94%a8/</guid>
		<description><![CDATA[<p>这两天参加了一个Android开发入门培训，讲师的水平不敢恭维，课讲的基本上也是一塌糊涂，不过通过这次培训，我算是达到了Android开发快速入门的预期目标。<br /><br />一般来说Android应用开发的标准工具组合是JDK + Android SDK + ADT (Android Development Tools) + Eclipse...</p>]]></description>
			<content:encoded><![CDATA[<p>这两天参加了一个<a href="http://en.wikipedia.org/wiki/Android_(operating_system)" target="_blank">Android</a>开发入门培训，讲师的水平不敢恭维，课讲的基本上也是一塌糊涂，不过通过这次培训，我算是达到了Android开发快速入门的预期目标。</p>
<p>一般来说Android应用开发的标准工具组合是JDK + Android SDK + ADT (Android Development Tools) + <a href="http://www.eclipse.org" target="_blank">Eclipse</a>，大家基本上是通过IDE GUI进行开发操作的。不过我个人更喜欢<a href="http://tonybai.com/2011/03/16/know-how-to-use-command-line-tool/" target="_blank">命令行</a>，所以这次我也尝试探索了一下使用命令行方式开发Android应用的方法。</p>
<p>入门的第一步就是搭建开发环境。关于Android开发环境搭建的资料早已汗牛充栋，不过我也看了一下这些资料多是关于如何在Windows下使用Eclipse搭建环境的，而在Linux环境下不用Eclipse的手工搭建环境的资料甚少。而我用的是<a href="http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/" target="_blank">Ubuntu 10.04</a>，所以在这里我想说说Ubuntu下搭建Android开发环境的过程，以及在此过程中遇到的诸多问题的解决。</p>
<p>Android应用主要用Java语言开发，所以JDK是不可缺少的，这是一个前提条件。关于JDK的安装以及环境变量配置，这里就不赘述了。我在Ubuntu下安装的是Oracle（原Sun）提供的JDK 1.6版本。</p>
<p>Android开发环境搭建的核心就是SDK。不过大陆的程序员们真的很悲哀，原因你懂的。为了下载一个SDK，到处翻山越岭，跋山涉水啊，好不痛苦。不过还好，领导们还给我们留下了一线生机。那就是<a href="http://dl-ssl.google.com/android/repository">http://dl-ssl.google.com/android/repository</a>，这里可以下载到Android SDK相关组件包。</p>
<p>首先你可以下载这个库的导航文件repository.xml(wget -c <a href="http://dl-ssl.google.com/android/repository/repository.xml">http://dl-ssl.google.com/android/repository/repository.xml</a>)。打开这个文件，通过里面的注释你会看到这个文件大约包含了四个部分：<br />
	. PLATFORMS<br />
	. PLATFORM-TOOLS<br />
	. TOOLS<br />
	. DOCS</p>
<p>这恰恰是Android SDK包的几个主要组成部分：<br />
	. 其中TOOLS对应的就是Android SDK Tools，主要用于SDK自身组件安装、卸载管理，提供模拟器工具以及其他开发所需的第三方工具。<br />
	. 其中PLATFORMS对应的是Android SDK Platform，这些包为Android应用开发提供了各个版本的虚拟设备（AVD）。比如Android 2.2、Android 2.3.3等虚拟设备。<br />
	. 其中PLATFORM-TOOLS对应的是Android SDK Platform-tools，这些包提供了与虚拟设备管理和调试相关的工具，如ADB。</p>
<p>我们如何通过这些组件包来组装成一个完整的Android SDK包呢？步骤大致如下：<br />
	. 下载Android SDK Tools包，也就是Repository中对应的TOOLS部分。我这里找到的是tools_r11-linux.zip(wget -c <a href="http://dl-ssl.google.com/android/repository/tools_r11-linux.zip">http://dl-ssl.google.com/android/repository/tools_r11-linux.zip</a>)。<br />
	. 在本地建立android-sdk-linux_86目录，将下载的tools_r11-linux.zip放到该目录下，解压，我们得到tools_r11-linux目录。<br />
	. 将android-sdk-linux_86目录下的tools_r11-linux目录改名为tools。<br />
	. 在android-sdk-linux_86目录下建立两个新目录：add-ons和platforms。（如果没有这两个目录，下一步中的android启动会失败）<br />
	. 进入android-sdk-linux_86/tools下，执行./android，启动Android SDK and AVD Manager。<br />
	. 在启动的Android SDK and AVD Manager对话框的&quot;Installed Packages&quot;里你会看到我们已经安装了&ldquo;Android SDK Tools, revision 11&rdquo;。</p>
<p>到这里，我们算是迈出了坚实的第一步。接下来，我们有两种方式继续我们的安装过程：<br />
	一种是通过SDK/AVD Manager在线安装SDK其余组件。在&quot;Installed Packages&quot;里点击&quot;Update All&quot;按钮，等待一会，你会看到可以安装的组件。这里我们至少需要一个Platform包（比如Android 2.3.3 API 10, revision 1）以及Platform-tools包（比如Android SDK Platform-tools, revision 4）。选择你要的组件包后，就可以install了。安装后，一个完整的Android SDK就呈现在你的眼前了。这种方式也是最快捷、最方便的方式了。</p>
<p>另外一种是离线安装方式。如果你和我一样，使用的是公司的代理网络，那么你很可能无法在线安装，即使SDK/AVD Manager支持配置网络代理。这样你就需要进行离线安装了，也就是需要你手工下载各个组件包，然后安装到指定的目录下。我这里就做了如下操作：<br />
	. 执行下面命令下载各组件包：<br />
	&nbsp; wget -c <a href="http://dl-ssl.google.com/android/repository/android-2.2_r02-linux.zip">http://dl-ssl.google.com/android/repository/android-2.2_r02-linux.zip</a><br />
	&nbsp; wget -c <a href="http://dl-ssl.google.com/android/repository/android-2.3.3_r01-linux.zip">http://dl-ssl.google.com/android/repository/android-2.3.3_r01-linux.zip</a><br />
	&nbsp; wget -c <a href="http://dl-ssl.google.com/android/repository/platform-tools_r04-linux.zip">http://dl-ssl.google.com/android/repository/platform-tools_r04-linux.zip</a><br />
	. 将android-2.2_r02-linux.zip拷贝到android-sdk-linux_86/platforms目录下，并解压。<br />
	. 将android-2.3.3_r01-linux.zip拷贝到android-sdk-linux_86/platforms目录下，并解压。<br />
	. 将platform-tools_r04-linux.zip拷贝到android-sdk-linux_86目录下，解压，并改名为platform-tools。</p>
<p>至此，SDK各组件安装完毕。执行tools/android，在&quot;Installed Packages&quot;下，你就会看到上述已经安装的组件包了。(笔者最后又发现了一个可以下载Android SDK的地方：<a href="http://dl.google.com/android[/android-sdk_r08-linux_86.tgz">http://dl.google.com/android[/android-sdk_r08-linux_86.tgz</a>]，在这里你下载到的SDK包内platforms和add-ons目录都已经建立完毕了，SDK tools在tools目录下，其余组件的安装方法和上面一致。)</p>
<p>为了方便后续使用，我们可将SDK目录下的platform-tools和tools两个路径添加到PATH环境变量中。接下来，我们就可以创建一个虚拟设备了。Android虚拟设备其实是一组配置，tools下的emulator使用这些配置启动一个特定版本的Android模拟程序，用来部署、运行和测试你开发的Android应用。</p>
<p>我们可以通过&quot;android list targets&quot;命令来查看当前系统中可以创建哪些平台的虚拟设备，在我的系统下，这条命令的执行结果如下：</p>
<p>Available Android targets:<br />
	id: 1 or &quot;android-8&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp; Name: Android 2.2<br />
	&nbsp;&nbsp;&nbsp;&nbsp; Type: Platform<br />
	&nbsp;&nbsp;&nbsp;&nbsp; API level: 8<br />
	&nbsp;&nbsp;&nbsp;&nbsp; Revision: 2<br />
	&nbsp;&nbsp;&nbsp;&nbsp; Skins: WVGA854, QVGA, WVGA800 (default), WQVGA400, WQVGA432, HVGA<br />
	id: 2 or &quot;android-10&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp; Name: Android 2.3.3<br />
	&nbsp;&nbsp;&nbsp;&nbsp; Type: Platform<br />
	&nbsp;&nbsp;&nbsp;&nbsp; API level: 10<br />
	&nbsp;&nbsp;&nbsp;&nbsp; Revision: 1<br />
	&nbsp;&nbsp;&nbsp;&nbsp; Skins: WVGA854, QVGA, WVGA800 (default), WQVGA400, WQVGA432, HVGA</p>
<p>我们有两个Platform可选，这里我们创建一个Android 2.3.3的虚拟设备。创建的命令如下：</p>
<p>$&gt; android create avd -n helloandroid -t 2<br />
	Android 2.3.3 is a basic Android platform.<br />
	Do you wish to create a custom hardware profile [no]</p>
<p>Created AVD &#039;helloandroid&#039; based on Android 2.3.3,<br />
	with the following hardware config:<br />
	hw.lcd.density=240<br />
	vm.heapSize=24<br />
	hw.ramSize=256</p>
<p>其中-n 用于指定avd的名字，-t则用于指定platform，也就是target，之前我们已经列出系统中的Targets，我们只需选择一个，并使用target的id即可。</p>
<p>创建后，我们可以通过android list avd来查看系统中都创建了哪些avd：<br />
	$&gt; android list avd<br />
	Available Android Virtual Devices:<br />
	&nbsp;&nbsp;&nbsp; Name: helloandroid<br />
	&nbsp;&nbsp;&nbsp; Path: /media/winD/tonybai/android-sdk-linux_86/.android/avd/helloandroid.avd<br />
	&nbsp; Target: Android 2.3.3 (API level 10)<br />
	&nbsp;&nbsp;&nbsp; Skin: WVGA800</p>
<p>有了avd，我们就可以启动emulator了。执行emulator -avd helloandroid，我们得到了如下错误信息：<br />
	&ldquo;emulator: ERROR: the user data image is used by another emulator. aborting&rdquo;</p>
<p>这条错误信息的字面意思是有另外一个emulator使用了这个avd，但是我找了半天，发现我并未启动任务其他emulator，系统进程列表中也没有其他emulator的信息。又到网上找了一些资料，都说是因emulator异常退出，导致没有解锁avd配置目录下的.lock文件导致的。但我到avd配置目录下，根本没有找到什么.lock文件。</p>
<p>我又通过调试模式执行了一遍：emulator -avd helloandroid -verbose -debug-all，这回我得到的信息如下：<br />
	&#8230; 这里省略了几百行日志&#8230;.<br />
	emulator: found system.img in search dir: /media/winD/tonybai/android-sdk-linux_86/platforms/android-2.3.3_r01-linux/images/<br />
	emulator: found userdata-qemu.img in content directory<br />
	emulator:&nbsp;&nbsp;&nbsp;&nbsp; locking user data image at /media/winD/tonybai/android-sdk-linux_86/.android/avd/helloandroid.avd/userdata-qemu.img<br />
	emulator: ERROR: the user data image is used by another emulator. aborting</p>
<p>从上面的错误日志来看，似乎emulator在对userdata-qemu.img加锁时出现了问题。这个问题古怪了些。我的SDK部署在FAT32分区，难道是跨分区文件锁有问题。无奈下把SDK搬移到我的HOME路径下，并修改PATH环境变量。重新启动emulator，这回emulator启动成功了。不过第一次启动emulator可真是够慢的，大约有5、6分钟之多，才看到Android的界面。不过还有一个问题，那就是emulator启动的模拟器画面太大，出了屏幕边界（我的本子是12寸屏幕的）。我们来修改一下avd的配置，调整屏幕属性：</p>
<p>在android-sdk-linux_86/.android/avd/helloandroid.avd目录下，我们打开config.ini，将下面三项配置：<br />
	hw.lcd.density=240<br />
	skin.name=WVGA800<br />
	skin.path=platforms/android-2.3.3_r01-linux/skins/WVGA800<br />
	修改为：<br />
	hw.lcd.density=160<br />
	skin.name=HVGA<br />
	skin.path=platforms/android-2.3.3_r01-linux/skins/HVGA</p>
<p>重新启动emulator，这回整个模拟器的画面都在屏幕以内了。</p>
<p>万事俱备，只欠东风！下面我们就可以开始创建我们第一个HelloAndroid工程了。在~/proj/android下建立helloandroid目录，进入helloandroid目录，执行下面命令：</p>
<p>$&gt; android create project &#8211;name helloandroid &#8211;activity HelloAndroid &#8211;path ./ &#8211;package com.examples.helloandroid &#8211;target 2</p>
<p>Created directory /home/tonybai/proj/android/helloandroid/src/com/examples/helloandroid<br />
	Added file ./src/com/examples/helloandroid/HelloAndroid.java<br />
	Created directory /home/tonybai/proj/android/helloandroid/res<br />
	Created directory /home/tonybai/proj/android/helloandroid/bin<br />
	Created directory /home/tonybai/proj/android/helloandroid/libs<br />
	Created directory /home/tonybai/proj/android/helloandroid/res/values<br />
	Added file ./res/values/strings.xml<br />
	Created directory /home/tonybai/proj/android/helloandroid/res/layout<br />
	Added file ./res/layout/main.xml<br />
	Created directory /home/tonybai/proj/android/helloandroid/res/drawable-hdpi<br />
	Created directory /home/tonybai/proj/android/helloandroid/res/drawable-mdpi<br />
	Created directory /home/tonybai/proj/android/helloandroid/res/drawable-ldpi<br />
	Added file ./AndroidManifest.xml<br />
	Added file ./build.xml<br />
	Added file ./proguard.cfg</p>
<p>Build该工程： ant release（注意对于2.3的SDK，ant要使用1.8以上版本）。一切很顺利，Build成功后，在bin下面出现了&quot;helloandroid-unsigned.apk&quot;文件。</p>
<p>那么如何将apk文件部署到模拟器中运行呢？如果系统内仅有一个device在运行（可通过adb devices命令查看），那么我们可以直接执行ant install，这样我们的apk就会自动被部署到emulator中了（这期间使用的是调试版的数字签名）。</p>
<p>部署后，你就会在emulator的界面上看到一个绿机器人图标且名字为&ldquo;HelloAndroid&rdquo;的程序了。点击其执行，我们得到一行文字：Hello World, HelloAndroid。这个文字是工程被创建时默认自带的，你当然也可以修改它了。</p>
<p>另外如果要卸载这个应用也很简单，执行ant uninstall就是了。</p>
<p>如果系统有多个AVD在运行，那么我们同样可以通过adb命令来选择一个device安装我们的应用，如果一个device的名字是emulator-5554(通过adb devices查看)，那么我们可以先执行ant debug，生成bin/helloandroid-debug.apk，然后通过&quot;adb -s emulator-5554 install bin/helloandroid-debug.apk&quot;将应用安装到emulator-5554上去。</p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/05/24/develop-android-app-in-command-line-method/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>借开源实现你的雄心壮志</title>
		<link>https://tonybai.com/2011/03/26/fulfill-your-ambitions-with-opensource/</link>
		<comments>https://tonybai.com/2011/03/26/fulfill-your-ambitions-with-opensource/#comments</comments>
		<pubDate>Sat, 26 Mar 2011 02:39:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></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/2011/03/26/%e5%80%9f%e5%bc%80%e6%ba%90%e5%ae%9e%e7%8e%b0%e4%bd%a0%e7%9a%84%e9%9b%84%e5%bf%83%e5%a3%ae%e5%bf%97/</guid>
		<description><![CDATA[<br />
本文翻译自"Fulfill Your Ambitions with Open Source"，来自于《程序员应该知道的97件事》一书中的某个章节。<br />
<br />
如果你在工作中没能开发那些可以实现你雄心壮志的软件，那你将有很不错的机会。也许你正在为一家庞大的保险公司开发软件，然而你实际上却宁愿供职于Goo...]]></description>
			<content:encoded><![CDATA[<p>本文翻译自&quot;<a href="http://programmer.97things.oreilly.com/wiki/index.php/Fulfill_Your_Ambitions_with_Open_Source" target="_blank">Fulfill Your Ambitions with Open Source</a>&quot;，来自于《<a href="http://book.douban.com/subject/5263681/" target="_blank">程序员应该知道的97件事</a>》一书中的某个章节。</p>
<p>如果你在工作中没能开发那些可以实现你雄心壮志的软件，那你将有很不错的机会。也许你正在为一家庞大的保险公司开发软件，然而你实际上却宁愿供职于Google、Apple、Microsoft或是你自己初创的公司去开发下一个对世界影响巨大的软件。如果你去为你根本不关心的系统开发软件，那你永远也实现不了你心中的抱负。</p>
<p>幸运的是，你的问题有一个答案：<a href="http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/" target="_blank">开源</a>。那里有成千上万的开源项目。其中许多项目的开发都十分活跃，可以提供你想要的各种软件开发经验。如果你有了开发操作系统的想法，那就从十多个操作系统项目中选择一个加入吧。如果你想从事音乐软件、动画制作软件、加密技术、机器人技术、电脑游戏、大型网络在线游戏、移动电话或任何一类软件的开发，你肯定可以找到至少一个开源项目可以满足你的兴趣。</p>
<p>当然，这世上没有免费的午餐。你必须愿意牺牲一些你个人的自由时间，因为你在日常工作中可能无法从事一个开源视频游戏的开发 &#8211; 你还要为你的雇主负责。此外，只有极少数人可以从对开源项目的贡献中获得收入 &#8211; 有些人得到了，不过大多数人没有。你应该放弃一些你的自由时间（少玩些视频游戏，少看些电视也没啥大不了的）。你在开源项目上的工作越努力，你就会越快地实现你作为程序员的抱负。考虑你的雇佣合同也同样重要 &#8211; 一些雇主可能会限制你可以贡献的内容，即使是在你自己的自由时间里。此外，当你的工作涉及到版权，专利，商标及贸易机密时，当心侵犯知识产权法。</p>
<p>开源为那些激情十足的程序员们提供了巨大的机会。首先，你可以看到其他人是如何实现一个让你感兴趣的解决方案的 &#8211; 你可以通过阅读其他人的源代码学到很多东西。第二，你向这个项目贡献你的代码以及想法 &#8211; 不是所有你的好想法都将被接受的，不过其中的一些可能被接受。通过开发解决方案以及贡献代码你就可以学到一些新东西。第三，你会遇到那些对此类软件拥有和你同样热情的<a href="http://tonybai.com/2011/02/24/the-professional-programmer/" target="_blank">卓越的程序员</a> &#8211; 这些因开源项目合作而形成的友谊可能会持续一生。第四，假设你是一个称职的贡献者，你将在这门让你感兴趣的技术上积累许多实际经验。</p>
<p>开始参加开源项目十分容易。有很多有关你需要的工具（例如：<a href="http://tonybai.com/2011/02/18/put-everything-under-version-control/" target="_blank">源码管理工具</a>，编辑器，编程语言，构建系统等）的文档资料。找到第一个你要参与的项目，学习这个项目所使用的工具。多数情况下有关这些项目自身的文档很少，不过这也无关紧要，因为学习开源项目的最好的方法就是自己研究这些代码。如果你想加入，你应该帮忙编写文档。或者你可以自愿编写测试代码。虽然这听起来也许不那么让人兴奋，但事实是为其他人的程序编写测试代码比几乎任何其它方式都能更快速地了解这个软件。<a href="http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/" target="_blank">编写测试代码</a>，优秀的测试代码。找Bug，提交修正建议，结交朋友，参加到那些你喜欢的软件开发工作中，实现你软件开发的雄心壮志。</p>
<p>by <a href="http://programmer.97things.oreilly.com/wiki/index.php/Richard_Monson-Haefel" target="_blank">Richard Monson-Haefel</a></p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/03/26/fulfill-your-ambitions-with-opensource/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>你应该关心你的代码</title>
		<link>https://tonybai.com/2011/03/22/you-gotta-care-about-the-code/</link>
		<comments>https://tonybai.com/2011/03/22/you-gotta-care-about-the-code/#comments</comments>
		<pubDate>Tue, 22 Mar 2011 12:48:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[Programmer]]></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/2011/03/22/%e4%bd%a0%e5%ba%94%e8%af%a5%e5%85%b3%e5%bf%83%e4%bd%a0%e7%9a%84%e4%bb%a3%e7%a0%81/</guid>
		<description><![CDATA[<br />
本文翻译自"You Gotta Care about the Code"，来自于《程序员应该知道的97件事》一书中的某个章节。<br />
<br />
即使不用大侦探福尔摩斯，我们也能知道优秀的程序员能写出好代码。糟糕的程序员...则不能。他们生产出代码巨兽，而其他人则不得不去清理。你想写出好代码，对不对...]]></description>
			<content:encoded><![CDATA[<p><p>本文翻译自”<a href="http://programmer.97things.oreilly.com/wiki/index.php/You_Gotta_Care_about_the_Code" target="_blank">You Gotta Care about the Code</a>“，来自于《<a href="http://book.douban.com/subject/5263681/" target="_blank">程序员应该知道的97件事</a>》一书中的某个章节。</p>
<p>即使不用大侦探福尔摩斯，我们也能知道优秀的程序员能写出好代码。糟糕的程序员&#8230;则不能。他们生产出代码巨兽，而其他人则不得不去清理。你想写出好代码，对不对？你渴望成为一名优秀的程序员。</p>
<p>好代码不会凭空冒出来。它也不是什么需要各大行星排成一列时靠运气才发生的事情。为了写出好代码，你必须在代码上下足功夫。这的确很难。并且如果你真正地关心好代码，你也只是得到这些好代码，仅此而已。</p>
<p>优秀的编程能力不单纯来自于技术能力。我曾经看到过很多智力超群的程序员，他们可以实现出精细的且令人印象深刻的算法，他们对语言标准烂熟于心，但他们编写出的代码却是最糟糕的。这些代码难于阅读，难于使用，并且难于修改。我还曾见过一些谦卑的程序员，他们坚持编写简单精炼的代码，不过他们却可以写出风格优雅且极具表现力的代码，能在工作中使用这些代码不失为一件乐事。</p>
<p>基于我在软件行业多年的经验，我得出这样的结论：那些可胜任工作的程序员与伟大程序员之间的真正差别在于态度。优秀的编程在于能在真实世界的约束以及软件行业的巨大压力下采用一些专业的方法，并且由衷地渴望编写出最好的软件。</p>
<p>通往地狱的代码是用良好的意愿铺成的。要成为一名优秀的程序员，你必须克服良好意愿的影响，并且真实地去关心你的代码 &#8211;培养积极的观点，形成健康的态度。伟大的代码是由工匠大师们精心制作的，而不是由粗心程序员草率编写的或者由某些自称编程大师的人故弄玄虚地创建的。</p>
<p>你想编写出好代码。你想成为一名优秀的程序员。因此，你就应该关心你的代码：</p>
<p>&middot;在任何编码的情况下，你都应该拒绝在那些只是看似能运行的代码上工作。你应该力求精心制作一份优雅且完全正确的代码（并有良好的测试用例可以证明你的代码是正确的）。</p>
<p>&middot;你编写的代码应该是可发现的（其它程序员可以很容易地学会和理解），可维护的（即你或者其它程序员在将来可以很容易的修改这些代码），以及正确的（你需要采取一切可能的措施确保你已经解决了这个问题，而不只是让程序看似是可以工作的）。</p>
<p>&middot;你和其它程序员一起工作。没有程序员是孤立的。很少有程序员独自工作；程序员团队所从事的大多数工作要么是在一个公司环境中，要么是一个开源项目。你要顾及其它程序员，并且构建其它人可以读懂的代码。你希望团队能编写出尽可能最好的软件，而不是使自己看起来很聪明。</p>
<p>&middot;任何时候你遇到一段代码，你都应该力求将它改造得比之前更好（要么结构更优，要么更易测试，要么更易理解）。</p>
<p>&middot;你关心代码和编程，所以你持续不断的学习新的语言，惯用法以及技术。不过你只能在适当的时候应用它们。</p>
<p>幸运的是，你正在阅读这些建议，那是因为你确实关心你的代码。你感兴趣。这是你的激情。快乐地编程。享受制作代码解决棘手的问题的乐趣吧。生产出让你感到骄傲的软件！</p>
<p>By <a href="http://programmer.97things.oreilly.com/wiki/index.php/Pete_Goodliffe" target="_blank">Pete Goodliffe</a></p>
</p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/03/22/you-gotta-care-about-the-code/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>知道如何使用命令行工具</title>
		<link>https://tonybai.com/2011/03/16/know-how-to-use-command-line-tool/</link>
		<comments>https://tonybai.com/2011/03/16/know-how-to-use-command-line-tool/#comments</comments>
		<pubDate>Wed, 16 Mar 2011 04:35:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[Programmer]]></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/2011/03/16/%e7%9f%a5%e9%81%93%e5%a6%82%e4%bd%95%e4%bd%bf%e7%94%a8%e5%91%bd%e4%bb%a4%e8%a1%8c%e5%b7%a5%e5%85%b7/</guid>
		<description><![CDATA[本文翻译自"Know How to Use Command-line Tool"，来自于《程序员应该知道的97件事》一书中的某个章节。<br />
<br />
现今，很多软件开发工具被打包成集成开发环境（Integrated Development Environments，IDE）提供给开发者。微软的Visual Studio和开源的Eclipse就是两个颇受欢...]]></description>
			<content:encoded><![CDATA[<p>本文翻译自”<a href="http://programmer.97things.oreilly.com/wiki/index.php/Know_How_to_Use_Command-line_Tools" target="_blank">Know How to Use Command-line Tool</a>“，来自于《<a href="http://book.douban.com/subject/5263681/" target="_blank">程序员应该知道的97件事</a>》一书中的某个<a href="http://programmer.97things.oreilly.com/wiki/index.php/Know_How_to_Use_Command-line_Tools" target="_blank">章节</a>。</p>
<p>现今，很多软件开发工具被打包成集成开发环境（Integrated Development Environments，IDE）提供给开发者。微软的Visual Studio和开源的Eclipse就是两个颇受欢迎的IDE，当然还有很多其他类似的工具。很多程序员喜欢使用IDE，这不仅是因为IDE容易使用，而且IDE还可以让程序员无需过多考虑一些过程中的微小细节，特别是构建过程。</p>
<p>不过，易用也有其负面因素。通常，一个工具容易使用，是因为这个工具在幕后替你作出了决定并自动地做了很多事情。因此，如果你只用IDE作为唯一的开发环境，你可能永远无法知道你的工具实际上究竟做了哪些事情。你点击一个按钮，一些奇妙的事情发生了，一个可执行文件就会出现在你的工程目录下。</p>
<p>使用命令行构建工具，你会了解到更多有关这些工具在工程构建过程中的行为细节。编写你自己的Makefile文件可以帮助你理解构建一个可执行文件过程中的每一步（编译，汇编，链接等）。用这些工具的命令行参数做些试验也是一个有价值的学习体验。最初开始使用命令行构建工具时，你可以使用一些开源的命令行工具，比如GCC。或者你也可以使用IDE自带的命令行工具。毕竟，一个设计精美的IDE只是一组命令行工具的图形前端而已。</p>
<p>与IDE相比，命令行工具除了可以帮助你增进对构建过程的理解之外，还能更容易更高效地完成某些任务。例如，grep和sed两个实用程序提供的查找和替换能力往往比IDE中提供的工具更为强大。命令行工具原生地支持脚本，支持自动化运行一些任务，诸如按预定时间制作日构建版本，为一个工程制作多个版本以及运行测试用例。在IDE中，这类自动化工作做起来可能非常困难（如果不是不可能的话），因为构建参数通常是通过图形界面的对话框设置的，并且构建过程是通过鼠标点击启动的。如果你一直没有脱离过IDE的襁褓，那你可能都无法意识到这类自动化的任务是可行的。</p>
<p>不过请等一下。难道IDE的使用没有使开发工作更简单，没有提高程序员的生产力吗？噢，不是这样的。这里提出的建议不是让你停止使用IDE。而是建议你&ldquo;应该深入到幕后&ldquo;，弄明白你的IDE到底为你做了哪些事情。而这么做的最好的方式就是学习使用命令行工具。接下来，当你回过头来使用IDE时，你就会更透彻地理解IDE为你做了哪些事情，并且知道如何控制构建过程了。另一方面，一旦你掌握了命令行工具的用法，体验到了这些工具提供强大功能和灵活性后，你也许会发现：与IDE比起来，你更喜欢命令行工具。</p>
<p>By <a href="http://programmer.97things.oreilly.com/wiki/index.php/Carroll_Robinson" target="_blank">Carroll Robinson</a></p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/03/16/know-how-to-use-command-line-tool/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>
