<?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; Build</title>
	<atom:link href="http://tonybai.com/tag/build/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sat, 04 Apr 2026 00:51:31 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道</title>
		<link>https://tonybai.com/2025/05/31/six-smells-in-go/</link>
		<comments>https://tonybai.com/2025/05/31/six-smells-in-go/#comments</comments>
		<pubDate>Sat, 31 May 2025 02:36:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[closure]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomod]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[XML]]></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=4769</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/31/six-smells-in-go 大家好，我是Tony Bai。 在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。 今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。 坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？” 在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。 典型场景：先异步通知，后更新状态 想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。 package main import ( "fmt" "sync" "time" ) type Order struct { ID string Status string // "pending", "paid", "notified" } func updateOrderStatusInDB(order *Order, status string) { fmt.Printf("数据库：订单 %s 状态更新为 %s\n", order.ID, status) [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/six-smells-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/31/six-smells-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/05/31/six-smells-in-go</p>
<p>大家好，我是Tony Bai。</p>
<p>在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。</p>
<p>今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<h2>坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？”</h2>
<p>在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。</p>
<p><strong>典型场景：先异步通知，后更新状态</strong></p>
<p>想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。</p>
<pre><code class="go">package main

import (
    "fmt"
    "sync"
    "time"
)

type Order struct {
    ID     string
    Status string // "pending", "paid", "notified"
}

func updateOrderStatusInDB(order *Order, status string) {
    fmt.Printf("数据库：订单 %s 状态更新为 %s\n", order.ID, status)
    order.Status = status // 模拟数据库更新
}

func asyncSendNotification(order *Order) {
    fmt.Printf("营销系统：收到订单 %s 通知，当前状态：%s。准备发送优惠券...\n", order.ID, order.Status)
    // 模拟耗时操作
    time.Sleep(50 * time.Millisecond)
    fmt.Printf("营销系统：订单 %s 优惠券已发送 (基于状态：%s)\n", order.ID, order.Status)
}

func main() {
    order := &amp;Order{ID: "123", Status: "pending"}
    var wg sync.WaitGroup

    fmt.Printf("主流程：订单 %s 支付成功，准备处理...\n", order.ID)

    // 坏味道：先启动异步通知，再更新数据库状态
    wg.Add(1)
    go func(o *Order) { // 注意这里传递了指针
        defer wg.Done()
        asyncSendNotification(o)
    }(order) // goroutine 捕获的是 order 指针

    // 模拟主流程的其他操作，或者数据库更新前的延时
    time.Sleep(500 * time.Millisecond) 

    updateOrderStatusInDB(order, "paid") // 更新数据库状态

    wg.Wait()
    fmt.Printf("主流程：订单 %s 处理完毕，最终状态：%s\n", order.ID, order.Status)
}
</code></pre>
<p>该示例的可能输出：</p>
<pre><code>主流程：订单 123 支付成功，准备处理...
营销系统：收到订单 123 通知，当前状态：pending。准备发送优惠券...
营销系统：订单 123 优惠券已发送 (基于状态：pending)
数据库：订单 123 状态更新为 paid
主流程：订单 123 处理完毕，最终状态：paid
</code></pre>
<p>我们看到营销系统拿到的优惠券居然是基于“pending”状态。</p>
<p><strong>问题分析：</strong></p>
<p>在上面的代码中，asyncSendNotification goroutine 和 updateOrderStatusInDB 是并发执行的。由于 asyncSendNotification 启动在先，并且捕获的是 order 指针，它很可能在 updateOrderStatusInDB 将订单状态更新为 “paid” <strong>之前</strong> 就读取了 order.Status。这就导致营销系统基于一个过时的状态（”pending”）发送了通知或优惠券，引发业务逻辑错误。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>确保关键操作的同步性或顺序性：</strong> 对于有严格先后顺序要求的操作，不要轻易异步化。如果必须异步，确保依赖的操作完成后再执行。</li>
<li><strong>使用同步原语：</strong> 利用 sync.WaitGroup、channel 等确保操作的正确顺序。例如，可以先更新数据库，再启动异步通知。</li>
<li><strong>传递值而非指针（如果适用）：</strong> 如果异步操作仅需快照数据，考虑传递值的副本，而不是指针。但在很多场景下，我们确实需要操作同一个对象。</li>
<li><strong>在异步回调中重新获取最新状态：</strong> 如果异步回调依赖最新状态，应在回调函数内部重新从可靠数据源（如数据库）获取，而不是依赖启动时捕获的状态。</li>
</ol>
<p><strong>修正示例思路：</strong></p>
<pre><code class="go">// ... (Order, updateOrderStatusInDB, asyncSendNotification 定义不变) ...
func main() {
    order := &amp;Order{ID: "123", Status: "pending"}
    var wg sync.WaitGroup

    fmt.Printf("主流程：订单 %s 支付成功，准备处理...\n", order.ID)

    updateOrderStatusInDB(order, "paid") // 先更新数据库状态

    // 再启动异步通知
    wg.Add(1)
    go func(o Order) { // 传递结构体副本，或者在异步函数内部重新获取
        defer wg.Done()
        // 实际场景中，如果 asyncSendNotification 依赖的是更新后的状态，
        // 它应该有能力从某个地方（比如参数，或者内部重新查询）获取到 "paid" 这个状态。
        // 这里简化为直接使用传入时的状态，但强调其应为 "paid"。
        // 或者，更好的方式是 asyncSendNotification 接受一个 status 参数。
        clonedOrderForNotification := o // 假设我们传递的是更新后的状态的副本
        asyncSendNotification(&amp;clonedOrderForNotification)
    }(*order) // 传递 order 的副本，此时 order.Status 已经是 "paid"

    wg.Wait()
    fmt.Printf("主流程：订单 %s 处理完毕，最终状态：%s\n", order.ID, order.Status)
}
</code></pre>
<h2>坏味道二：指针与闭包的“爱恨情仇”——“我以为它没变，结果它却跑了！”</h2>
<p>闭包是 Go 语言中一个强大的特性，它能够捕获其词法作用域内的变量。然而，当闭包捕获的是指针，并且这个指针指向的数据在 goroutine 启动后可能被外部修改，或者指针本身被重新赋值时，就可能导致并发问题和难以预料的行为。虽然 Go 1.22+ 通过实验性的 GOEXPERIMENT=loopvar 改变了 for 循环变量的捕获语义，解决了经典的循环变量闭包陷阱，但指针与闭包结合时对共享可变状态的考量依然重要。</p>
<p><strong>典型场景：闭包捕获指针，外部修改指针或其指向内容</strong></p>
<p>我们来看一个不涉及循环变量，但同样能体现指针与闭包问题的场景：</p>
<pre><code class="go">package main

import (
    "fmt"
    "sync"
    "time"
)

type Config struct {
    Version string
    Timeout time.Duration
}

func watchConfig(cfg *Config, wg *sync.WaitGroup) {
    defer wg.Done()
    // 这个 goroutine 期望在其生命周期内使用 cfg 指向的配置
    // 但如果外部在它执行期间修改了 cfg 指向的内容，或者 cfg 本身被重新赋值，
    // 那么这个 goroutine 看到的内容就可能不是启动时的那个了。
    fmt.Printf("Watcher: 开始监控配置 (Version: %s, Timeout: %v)\n", cfg.Version, cfg.Timeout)
    time.Sleep(100 * time.Millisecond) // 模拟监控工作
    fmt.Printf("Watcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\n", cfg.Version, cfg.Timeout)
}

func main() {
    currentConfig := &amp;Config{Version: "v1.0", Timeout: 5 * time.Second}
    var wg sync.WaitGroup

    fmt.Printf("主流程：初始配置 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)

    // 启动一个 watcher goroutine，它捕获了 currentConfig 指针
    wg.Add(1)
    go watchConfig(currentConfig, &amp;wg) // currentConfig 指针被传递

    // 主流程在 watcher goroutine 执行期间，修改了 currentConfig 指向的内容
    time.Sleep(10 * time.Millisecond) // 确保 watcher goroutine 已经启动并打印了初始配置
    fmt.Println("主流程：检测到配置更新，准备在线修改...")
    currentConfig.Version = "v2.0" // 直接修改了指针指向的内存内容
    currentConfig.Timeout = 10 * time.Second
    fmt.Printf("主流程：配置已修改为 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)

    // 或者更极端的情况，主流程让 currentConfig 指向了一个全新的 Config 对象
    // time.Sleep(10 * time.Millisecond)
    // fmt.Println("主流程：检测到配置需要完全替换...")
    // currentConfig = &amp;Config{Version: "v3.0", Timeout: 15 * time.Second} // currentConfig 指向了新的内存地址
    // fmt.Printf("主流程：配置已替换为 (Version: %s, Timeout: %v)\n", currentConfig.Version, currentConfig.Timeout)
    // 注意：如果 currentConfig 被重新赋值指向新对象，原 watchConfig goroutine 仍然持有旧对象的指针。
    // 但如果原意是让 watchConfig 感知到“最新的配置”，那么这种方式是错误的。

    wg.Wait()
    fmt.Println("主流程：所有处理完毕。")

    fmt.Println("\n--- 更安全的做法：传递副本或不可变快照 ---")
    // 更安全的做法：如果 goroutine 需要的是启动时刻的配置快照
    stableConfig := &amp;Config{Version: "v1.0-stable", Timeout: 5 * time.Second}
    configSnapshot := *stableConfig // 创建一个副本

    wg.Add(1)
    go func(cfgSnapshot Config, wg *sync.WaitGroup) { // 传递的是 Config 值的副本
        defer wg.Done()
        fmt.Printf("SafeWatcher: 开始监控配置 (Version: %s, Timeout: %v)\n", cfgSnapshot.Version, cfgSnapshot.Timeout)
        time.Sleep(100 * time.Millisecond)
        // 即使外部修改了 stableConfig，cfgSnapshot 依然是启动时的值
        fmt.Printf("SafeWatcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\n", cfgSnapshot.Version, cfgSnapshot.Timeout)
    }(configSnapshot, &amp;wg)

    time.Sleep(10 * time.Millisecond)
    stableConfig.Version = "v2.0-stable" // 修改原始配置
    stableConfig.Timeout = 10 * time.Second
    fmt.Printf("主流程：stableConfig 已修改为 (Version: %s, Timeout: %v)\n", stableConfig.Version, stableConfig.Timeout)

    wg.Wait()
    fmt.Println("主流程：所有安全处理完毕。")
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>在第一个示例中，watchConfig goroutine 通过闭包（函数参数也是一种闭包形式）捕获了 currentConfig 指针。这意味着 watchConfig 内部对 cfg 的访问，实际上是访问 main goroutine 中 currentConfig 指针所指向的那块内存。</p>
<ul>
<li><strong>当外部修改指针指向的内容时：</strong> 如代码中 currentConfig.Version = “v2.0&#8243;，watchConfig goroutine 在后续访问 cfg.Version 时，会看到这个被修改后的新值，这可能不是它启动时期望的行为。</li>
<li><strong>当外部修改指针本身时 (注释掉的极端情况)：</strong> 如果 currentConfig = &amp;Config{Version: “v3.0&#8243;, &#8230;}，那么 watchConfig 捕获的 cfg 仍然指向<strong>原始的 Config 对象</strong>（即 “v1.0&#8243; 那个）。如果此时的业务逻辑期望 watchConfig 使用“最新的配置对象”，那么这种捕获指针的方式就会导致错误。</li>
</ul>
<p>这些问题的根源在于对<strong>共享可变状态</strong>的并发访问缺乏控制，以及对指针生命周期和闭包捕获机制的理解不够深入。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li>
<p><strong>明确 goroutine 需要的数据快照还是共享状态：</strong></p>
<ul>
<li>如果 goroutine 只需要启动时刻的数据快照，并且不希望受外部修改影响，那么应该<strong>传递值的副本</strong>给 goroutine（或者在闭包内部创建副本）。如第二个示例中的 configSnapshot。</li>
<li>如果 goroutine 需要与外部共享并感知状态变化，那么必须使用<strong>同步机制</strong>（如 mutex、channel、atomic 操作）来保护对共享状态的访问，确保数据一致性和避免竞态条件。</li>
</ul>
</li>
<li>
<p><strong>谨慎捕获指针，特别是那些可能在 goroutine 执行期间被修改的指针：</strong></p>
<ul>
<li>如果捕获了指针，要清楚地知道这个指针的生命周期，以及它指向的数据是否会被其他 goroutine 修改。</li>
<li>如果指针指向的数据是可变的，并且多个 goroutine 会并发读写，<strong>必须加锁保护</strong>。</li>
</ul>
</li>
<li>
<p><strong>考虑数据的不可变性：</strong> 如果可能，尽量使用不可变的数据结构。将不可变的数据传递给 goroutine 是最安全的并发方式之一。</p>
</li>
<li>
<p><strong>对于经典的 for 循环启动 goroutine 捕获循环变量的问题：</strong></p>
<ul>
<li><strong>Go 1.22+ (启用 GOEXPERIMENT=loopvar) 或未来版本：</strong> 语言层面已经解决了每次迭代共享同一个循环变量的问题，每次迭代会创建新的变量实例。此时，直接在闭包中捕获循环变量是安全的。</li>
<li><strong>Go 1.21 及更早版本 (或未启用 loopvar 实验特性)：</strong> 仍然需要通过<strong>函数参数传递</strong>的方式来确保每个 goroutine 捕获到正确的循环变量值。例如：</li>
</ul>
</li>
</ol>
<pre><code class="go">for i, v := range values {
    valCopy := v // 如果 v 是复杂类型，可能需要更深的拷贝
    indexCopy := i
    go func() {
        // 使用 valCopy 和 indexCopy
    }()
}
// 或者更推荐的方式：
for i, v := range values {
    go func(idx int, valType ValueType) { // ValueType 是 v 的类型
        // 使用 idx 和 valType
    }(i, v)
}
</code></pre>
<p>虽然 Go 语言在 for 循环变量捕获方面做出了改进，但指针与闭包结合时对共享状态和生命周期的审慎思考，仍然是编写健壮并发程序的关键。</p>
<h2>坏味道三：错误处理的哲学——“是Bug就让它崩！”真的好吗？</h2>
<p>Go 语言通过返回 error 值来处理可预期的错误，而 panic 则用于表示真正意外的、程序无法继续正常运行的严重错误，通常由运行时错误（如数组越界、空指针解引用）或显式调用 panic() 引发。当 panic 发生且未被 recover 时，程序会崩溃并打印堆栈信息。</p>
<p>一种常见的观点是：“如果是 Bug，就应该让它尽快崩溃 (Fail Fast)”，以便问题能被及时发现和修复。这种观点在很多情况下是合理的。然而，在某些 <strong>mission-critical（关键任务）系统</strong>中，例如金融交易系统、空中交通管制系统、重要的基础设施服务等，一次意外的宕机重启可能导致不可估量的损失或严重后果。在这些场景下，即使因为一个未捕获的 Bug 导致了 panic，我们也可能期望系统能有一定的“韧性”，而不是轻易“放弃治疗”。</p>
<p><strong>典型场景：一个关键服务在处理请求时因 Bug 发生 Panic</strong></p>
<pre><code class="go">package main

import (
    "fmt"
    "net/http"
    "runtime/debug"
    "time"
)

// 模拟一个关键数据处理器
type CriticalDataProcessor struct {
    // 假设有一些内部状态
    activeConnections int
    lastProcessedID   string
}

// 处理数据的方法，这里故意引入一个可能导致 panic 的 bug
func (p *CriticalDataProcessor) Process(dataID string, payload map[string]interface{}) error {
    fmt.Printf("Processor: 开始处理数据 %s\n", dataID)
    p.activeConnections++
    defer func() { p.activeConnections-- }() // 确保连接数正确管理

    // 模拟一些复杂逻辑
    time.Sleep(50 * time.Millisecond)

    // ！！！潜在的 Bug ！！！
    // 假设 payload 中 "user" 字段应该是一个结构体指针，但有时可能是 nil
    // 或者，某个深层嵌套的访问可能导致空指针解引用
    // 为了演示，我们简单模拟一个 nil map 访问导致的 panic
    var userDetails map[string]string
    // userDetails = payload["user"].(map[string]string) // 这本身也可能 panic 如果类型断言失败
    // 为了稳定复现 panic，我们直接让 userDetails 为 nil
    if dataID == "buggy-data-001" { // 特定条件下触发 bug
        fmt.Printf("Processor: 触发 Bug，尝试访问 nil map '%s'\n", userDetails["name"]) // 这里会 panic
    }

    p.lastProcessedID = dataID
    fmt.Printf("Processor: 数据 %s 处理成功\n", dataID)
    return nil
}

// HTTP Handler - 版本1: 不做任何 recover
func handleRequestVersion1(processor *CriticalDataProcessor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        dataID := r.URL.Query().Get("id")
        if dataID == "" {
            http.Error(w, "缺少 id 参数", http.StatusBadRequest)
            return
        }

        // 模拟从请求中获取 payload
        payload := make(map[string]interface{})
        // if dataID == "buggy-data-001" {
        //  // payload["user"] 可能是 nil 或错误类型，导致 Process 方法 panic
        // }

        err := processor.Process(dataID, payload) // 如果 Process 发生 panic，整个 HTTP server goroutine 会崩溃
        if err != nil {
            http.Error(w, fmt.Sprintf("处理失败: %v", err), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "请求 %s 处理成功\n", dataID)
    }
}

// HTTP Handler - 版本2: 在每个请求处理的 goroutine 顶层 recover
func handleRequestVersion2(processor *CriticalDataProcessor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Fprintf(os.Stderr, "!!!!!!!!!!!!!! PANIC 捕获 !!!!!!!!!!!!!!\n")
                fmt.Fprintf(os.Stderr, "错误: %v\n", err)
                fmt.Fprintf(os.Stderr, "堆栈信息:\n%s\n", debug.Stack())
                fmt.Fprintf(os.Stderr, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")

                // 向客户端返回一个通用的服务器错误
                http.Error(w, "服务器内部错误，请稍后重试", http.StatusInternalServerError)

                // 可以在这里记录更详细的错误到日志系统、发送告警等
                // 例如：log.Errorf("Panic recovered: %v, Stack: %s", err, debug.Stack())
                // metrics.Increment("panic_recovered_total")

                // 重要：根据系统的 mission-critical 程度和业务逻辑，
                // 这里可能还需要做一些清理工作，或者尝试让系统保持在一种“安全降级”的状态。
                // 但要注意，recover 后的状态可能是不确定的，需要非常谨慎。
            }
        }()

        dataID := r.URL.Query().Get("id")
        if dataID == "" {
            http.Error(w, "缺少 id 参数", http.StatusBadRequest)
            return
        }
        payload := make(map[string]interface{})

        err := processor.Process(dataID, payload)
        if err != nil {
            // 正常错误处理
            http.Error(w, fmt.Sprintf("处理失败: %v", err), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "请求 %s 处理成功\n", dataID)
    }
}

func main() {
    processor := &amp;CriticalDataProcessor{}

    // mux1 使用 Version1 handler (不 recover)
    // mux2 使用 Version2 handler (recover)

    // 启动 HTTP 服务器 (这里为了演示，只启动一个，实际中会选择一个)
    // 你可以注释掉一个，运行另一个来观察效果

    // http.HandleFunc("/v1/process", handleRequestVersion1(processor))
    // fmt.Println("V1 Server (不 recover) 启动在 :8080/v1/process")
    // go http.ListenAndServe(":8080", nil)

    http.DefaultServeMux.HandleFunc("/v2/process", handleRequestVersion2(processor))
    fmt.Println("V2 Server (recover) 启动在 :8081/v2/process")
    go http.ListenAndServe(":8081", nil)

    fmt.Println("\n请在浏览器或使用 curl 测试:")
    fmt.Println("  正常请求: curl 'http://localhost:8081/v2/process?id=normal-data-002'")
    fmt.Println("  触发Bug的请求: curl 'http://localhost:8081/v2/process?id=buggy-data-001'")
    fmt.Println("  (如果启动V1服务，触发Bug的请求会导致服务崩溃)")

    select {} // 阻塞 main goroutine，保持服务器运行
}
</code></pre>
<p><strong>问题分析：</strong></p>
<ul>
<li><strong>不 Recover (handleRequestVersion1)：</strong> 当 processor.Process 方法因为 Bug（例如访问 nil map userDetails["name"]）而发生 panic 时，如果这个 panic 没有在当前 goroutine 的调用栈中被 recover，它会一直向上传播。对于由 net/http 包为每个请求创建的 goroutine，如果 panic 未被处理，将导致该 goroutine 崩溃。在某些情况下（取决于 Go 版本和 HTTP server 实现的细节），这可能导致整个 HTTP 服务器进程终止，或者至少是该连接的处理异常中断，影响服务可用性。</li>
<li><strong>Recover (handleRequestVersion2)：</strong> 通过在每个请求处理的 goroutine 顶层使用 defer func() { recover() }()，我们可以捕获这个由 Bug 引发的 panic。捕获后，我们可以：
<ul>
<li>记录详细的错误信息和堆栈跟踪，便于事后分析和修复 Bug。</li>
<li>向当前请求的客户端返回一个通用的错误响应（例如 HTTP 500），而不是让连接直接断开或无响应。</li>
<li><strong>关键在于：</strong> 阻止了单个请求处理中的 Bug 导致的 panic 扩散到导致整个服务不可用的地步。服务本身仍然可以继续处理其他正常的请求。</li>
</ul>
</li>
</ul>
<p><strong>“是Bug就让它崩！”的观点在很多开发和测试环境中是值得提倡的，因为它能让我们更快地发现和定位问题。然而，在线上，特别是对于 mission-critical 系统：</strong></p>
<ul>
<li><strong>可用性是第一要务：</strong> 一次意外的全面宕机，可能比单个请求处理失败带来的损失大得多。</li>
<li><strong>数据一致性风险：</strong> 如果 panic 发生在关键数据操作的中间状态，直接崩溃可能导致数据不一致或损坏。recover 之后虽然也需要谨慎处理状态，但至少给了我们一个尝试回滚或记录问题的机会。</li>
<li><strong>用户体验：</strong> 对用户而言，遇到一个“服务器内部错误”然后重试，通常比整个服务长时间无法访问要好一些。</li>
</ul>
<p><strong>避坑与决策指南：</strong></p>
<ol>
<li><strong>在关键服务的请求处理入口或 goroutine 顶层设置 recover 机制：</strong> 这是构建健壮服务的推荐做法。
<ul>
<li>recover 应该与 defer 配合使用。</li>
<li>在 recover 逻辑中，务必记录详细的错误信息、堆栈跟踪，并考虑集成到告警系统。</li>
</ul>
</li>
<li><strong>recover 之后做什么？——视情况而定，但要极其谨慎：</strong>
<ul>
<li><strong>对于单个请求处理 goroutine：</strong> 通常的做法是记录错误，向当前客户端返回错误响应，然后让该 goroutine 正常结束。避免让这个 panic 影响其他请求。</li>
<li><strong>对于核心的、管理全局状态的 goroutine：</strong> 如果发生 panic，表明系统可能处于一种非常不稳定的状态。recover 后，可能需要执行一些清理操作，尝试将系统恢复到一个已知的安全状态，或者进行优雅关闭并重启。<strong>绝对不应该假装什么都没发生，继续使用可能已损坏的状态。</strong></li>
<li><strong>“苟活”的度：</strong> “苟活”不代表对 Bug 视而不见。recover 的目的是保障服务的整体可用性，同时为我们争取定位和修复 Bug 的时间。捕获到的 panic 必须被视为高优先级事件进行处理。</li>
</ul>
</li>
<li><strong>库代码应极度克制 panic：</strong> 库不应该替应用程序做“是否崩溃”的决策。</li>
<li><strong>测试，测试，再测试：</strong> 通过充分的单元测试、集成测试和压力测试，尽可能在上线前发现和消除潜在的 Bug，减少线上发生 panic 的概率。可以使用 Go 的 race detector 来检测并发代码中的竞态条件。</li>
<li><strong>不要滥用 panic/recover 作为正常的错误处理机制：</strong> panic/recover 主要用于处理不可预料的、灾难性的运行时错误或程序缺陷，而不是替代 error 返回值来处理业务逻辑中的预期错误。</li>
</ol>
<p>“是Bug就让它崩！”在开发阶段有助于快速发现问题，但在生产环境，特别是 mission-critical 系统中，<strong>“有控制地恢复，详细记录，并保障整体服务可用性”</strong> 往往是更明智的选择。这并不意味着容忍 Bug，而是采用一种更成熟、更负责任的方式来应对突发状况，确保系统在面对未知错误时仍能表现出足够的韧性。</p>
<h2>坏味道四：http.Client 的“一次性”误区——“每次都新建，省心又省事？”</h2>
<p>Go 标准库的 net/http 包提供了强大的 HTTP客户端功能。但有些开发者（尤其是初学者）在使用 http.Client 时，会为每一个 HTTP 请求都创建一个新的 http.Client 实例。</p>
<p><strong>典型场景：函数内部频繁创建 http.Client</strong></p>
<pre><code class="go">package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

// 坏味道：每次调用都创建一个新的 http.Client
func fetchDataFromAPI(url string) (string, error) {
    client := &amp;http.Client{ // 每次都新建 Client
        Timeout: 10 * time.Second,
    }
    resp, err := client.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

// 正确的方式：复用 http.Client
var sharedClient = &amp;http.Client{ // 全局或适当范围复用的 Client
    Timeout: 10 * time.Second,
    // 可以配置 Transport 以控制连接池等
    // Transport: &amp;http.Transport{
    //  MaxIdleConns:        100,
    //  MaxIdleConnsPerHost: 10,
    //  IdleConnTimeout:     90 * time.Second,
    // },
}

func fetchDataFromAPIReusable(url string) (string, error) {
    resp, err := sharedClient.Get(url) // 复用 Client
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

func main() {
    // 模拟多次调用
    // 如果使用 fetchDataFromAPI，每次都会创建新的 TCP 连接
    // _,_ = fetchDataFromAPI("https://www.example.com")
    // _,_ = fetchDataFromAPI("https://www.example.com")

    // 使用 fetchDataFromAPIReusable，会复用连接
    data, err := fetchDataFromAPIReusable("https://httpbin.org/get")
    if err != nil {
        fmt.Printf("请求错误: %v\n", err)
        return
    }
    fmt.Printf("获取到数据 (部分): %s...\n", data[:50])

    data, err = fetchDataFromAPIReusable("https://httpbin.org/get")
    if err != nil {
        fmt.Printf("请求错误: %v\n", err)
        return
    }
    fmt.Printf("再次获取到数据 (部分): %s...\n", data[:50])
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>http.Client 的零值或通过 &amp;http.Client{} 创建的实例，其内部的 Transport 字段（通常是 *http.Transport）会维护一个 TCP 连接池，并处理 HTTP keep-alive 等机制以复用连接。<strong>如果为每个请求都创建一个新的 http.Client，那么每次请求都会经历完整的 TCP 连接建立过程（三次握手），并在请求结束后关闭连接。</strong></p>
<p><strong>危害：</strong></p>
<ol>
<li><strong>性能下降：</strong> 频繁的 TCP 连接建立和关闭开销巨大。</li>
<li><strong>资源消耗增加：</strong> 短时间内大量创建连接可能导致客户端耗尽可用端口，或者服务器端累积大量 TIME_WAIT 状态的连接，最终影响整个系统的吞吐量和稳定性。</li>
</ol>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>复用 http.Client 实例：</strong> 这是官方推荐的最佳实践。可以在全局范围创建一个 http.Client 实例（如 http.DefaultClient，或者一个自定义配置的实例），并在所有需要发起 HTTP 请求的地方复用它。</li>
<li><strong>http.Client 是并发安全的：</strong> 你可以放心地在多个 goroutine 中共享和使用同一个 http.Client 实例。</li>
<li><strong>自定义 Transport：</strong> 如果需要更细致地控制连接池大小、超时时间、TLS 配置等，可以创建一个自定义的 http.Transport 并将其赋给 http.Client 的 Transport 字段。</li>
</ol>
<h2>坏味道五：API 设计的“文档缺失”——“这参数啥意思？猜猜看！”</h2>
<p>良好的 API 设计是软件质量的基石，而清晰、准确的文档则是 API 可用性的关键。然而，在实际项目中，我们常常会遇到一些 API，其参数、返回值、错误码、甚至行为语义都缺乏明确的文档说明，导致用户（调用方）在集成时只能靠“猜”或者阅读源码，极易产生误用。</p>
<p><strong>典型场景：一个“凭感觉”调用的服务发现 API</strong></p>
<p>假设我们有一个类似 Nacos Naming 的服务发现客户端，其 GetInstance API 的文档非常简略，或者干脆没有文档，只暴露了函数签名：</p>
<pre><code class="go">package main

import (
    "errors"
    "fmt"
    "math/rand"
    "time"
)

// 假设这是 Nacos Naming 客户端的一个简化接口
type NamingClient interface {
    // GetInstance 获取服务实例。
    // 关键问题：
    // 1. serviceName 需要包含 namespace/group 信息吗？格式是什么？
    // 2. clusters 是可选的吗？如果提供多个，是随机选一个还是有特定策略？
    // 3. healthyOnly 如果为 true，是否会过滤掉不健康的实例？如果不健康实例是唯一选择呢？
    // 4. 返回的 instance 是什么结构？如果找不到实例，是返回 nil, error 还是空对象？
    // 5. error 可能有哪些类型？调用方需要如何区分处理？
    // 6. 这个调用是阻塞的吗？超时机制是怎样的？
    // 7. 是否有本地缓存机制？缓存刷新策略是？
    GetInstance(serviceName string, clusters []string, healthyOnly bool) (instance interface{}, err error)
}

// 一个非常简化的模拟实现 (坏味道的 API 设计，文档缺失)
type MockNamingClient struct{}

func (c *MockNamingClient) GetInstance(serviceName string, clusters []string, healthyOnly bool) (interface{}, error) {
    fmt.Printf("尝试获取服务: %s, 集群: %v, 只获取健康实例: %t\n", serviceName, clusters, healthyOnly)

    // 模拟一些内部逻辑和不确定性
    if serviceName == "" {
        return nil, errors.New("服务名不能为空 (错误码: Naming-1001)") // 文档里有这个错误码说明吗？
    }

    // 假设我们内部有一些实例数据
    instances := map[string][]string{
        "OrderService":   {"10.0.0.1:8080", "10.0.0.2:8080"},
        "PaymentService": {"10.0.1.1:9090"},
    }

    // 模拟集群选择逻辑 (文档缺失，用户只能猜)
    selectedCluster := ""
    if len(clusters) &gt; 0 {
        selectedCluster = clusters[rand.Intn(len(clusters))] // 随机选一个？
        fmt.Printf("选择了集群: %s\n", selectedCluster)
    }

    // 模拟健康检查和实例返回 (文档缺失)
    if healthyOnly &amp;&amp; rand.Float32() &lt; 0.3 { // 30% 概率找不到健康实例
        return nil, fmt.Errorf("在集群 %s 中未找到 %s 的健康实例 (错误码: Naming-2003)", selectedCluster, serviceName)
    }

    if insts, ok := instances[serviceName]; ok &amp;&amp; len(insts) &gt; 0 {
        return insts[rand.Intn(len(insts))], nil // 返回一个实例地址
    }

    return nil, fmt.Errorf("服务 %s 未找到 (错误码: Naming-4004)", serviceName)
}

func main() {
    client := &amp;MockNamingClient{}

    // 用户A的调用 (基于猜测)
    fmt.Println("用户A 调用:")
    instA, errA := client.GetInstance("OrderService", []string{"clusterA", "clusterB"}, true)
    if errA != nil {
        fmt.Printf("用户A 获取实例失败: %v\n", errA)
    } else {
        fmt.Printf("用户A 获取到实例: %v\n", instA)
    }

    fmt.Println("\n用户B 的调用 (换一种猜测):")
    // 用户B 可能不知道 serviceName 需要什么格式，或者 clusters 参数的意义
    instB, errB := client.GetInstance("com.example.PaymentService", nil, false) // serviceName 格式？clusters 为 nil 会怎样？
    if errB != nil {
        fmt.Printf("用户B 获取实例失败: %v\n", errB)
    } else {
        fmt.Printf("用户B 获取到实例: %v\n", instB)
    }
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>当 API 的设计者没有提供清晰、详尽的文档来说明每个参数的含义、取值范围、默认行为、边界条件、错误类型以及API的整体行为和副作用时，API 的使用者就只能依赖猜测、尝试，甚至阅读源码（如果开源的话）来理解如何正确调用。</p>
<p><strong>危害：</strong></p>
<ol>
<li><strong>极易误用：</strong> 用户可能以 API 设计者未预期的方式调用接口，导致程序行为不符合预期，甚至引发错误。</li>
<li><strong>集成成本高：</strong> 理解和调试一个文档不清晰的 API 非常耗时。</li>
<li><strong>脆弱的依赖：</strong> 当 API 的内部实现或未明确定义的行为发生变化时，依赖这些隐性行为的调用方代码很可能会中断。</li>
<li><strong>难以排查问题：</strong> 出现问题时，很难判断是调用方使用不当，还是 API 本身的缺陷。</li>
</ol>
<p><strong>避坑指南 (针对 API 设计者)：</strong></p>
<ol>
<li><strong>编写清晰、准确、详尽的文档是 API 设计不可或缺的一部分！</strong> 这不仅仅是注释，可能还包括独立的 API 参考手册、用户指南和最佳实践。</li>
<li><strong>参数和返回值要有明确的语义：</strong> 名称应自解释，复杂类型应有结构和字段说明。
<ul>
<li>例如，serviceName 是否需要包含命名空间或分组信息？格式是什么？</li>
<li>clusters 参数是可选的吗？如果提供多个，选择策略是什么？是轮询、随机还是有特定优先级？</li>
<li>healthyOnly 的确切行为是什么？如果没有健康的实例，是返回错误还是有其他回退逻辑？</li>
</ul>
</li>
<li><strong>明确约定边界条件和错误情况：</strong>
<ul>
<li>哪些参数是必需的，哪些是可选的？可选参数的默认值是什么？</li>
<li>对于无效输入，API 会如何响应？返回哪些具体的错误码或错误信息？（例如，示例中的 Naming-1001, Naming-2003, Naming-4004 是否有统一的文档说明其含义和建议处理方式？）</li>
<li>API 调用可能产生的副作用是什么？</li>
</ul>
</li>
<li><strong>提供清晰的调用示例：</strong> 针对常见的用例，提供可运行的代码示例。</li>
<li><strong>考虑 API 的易用性和健壮性：</strong>
<ul>
<li>是否需要版本化？</li>
<li>是否需要幂等性保证？</li>
<li>认证和授权机制是否清晰？</li>
<li>超时和重试策略是怎样的？</li>
</ul>
</li>
<li><strong>将 API 的使用者视为首要客户：</strong> 站在使用者的角度思考，他们需要哪些信息才能轻松、正确地使用你的 API。</li>
</ol>
<p><strong>对于 API 的使用者：</strong> 当遇到文档不清晰的 API 时，除了“猜测”，更积极的做法是向 API 提供方寻求澄清，或者在有条件的情况下，参与到 API 文档的改进和完善中。</p>
<p>在之前《<a href="https://tonybai.com/2025/05/23/go-api-design-mcp-sdk/">API设计的“Go境界”：Go团队设计MCP SDK过程中的取舍与思考</a>》一文中，我们了见识了Go团队的API设计艺术，大家可以认知阅读和参考。</p>
<h2>坏味道六：匿名函数类型签名的“笨拙感”——“这函数参数看着眼花缭乱！”</h2>
<p>Go 语言的函数是一等公民，可以作为参数传递，也可以作为返回值。这为编写高阶函数和实现某些设计模式提供了极大的灵活性。然而，当匿名函数的类型签名（特别是嵌套或包含多个复杂函数类型参数时）直接写在函数定义中时，代码的可读性会大大降低，显得冗余和笨拙。</p>
<p><strong>典型场景：复杂的函数签名</strong></p>
<pre><code class="go">package main

import (
    "errors"
    "fmt"
    "strings"
)

// 坏味道：函数签名中直接嵌入复杂的匿名函数类型
func processData(
    data []string,
    filterFunc func(string) bool, // 参数1：一个过滤函数
    transformFunc func(string) (string, error), // 参数2：一个转换函数
    aggregatorFunc func([]string) string, // 参数3：一个聚合函数
) (string, error) {
    var filteredData []string
    for _, d := range data {
        if filterFunc(d) {
            transformed, err := transformFunc(d)
            if err != nil {
                // 注意：这里为了简化，直接返回了第一个遇到的错误
                // 实际应用中可能需要更复杂的错误处理逻辑，比如收集所有错误
                return "", fmt.Errorf("转换 '%s' 失败: %w", d, err)
            }
            filteredData = append(filteredData, transformed)
        }
    }
    if len(filteredData) == 0 {
        return "", errors.New("没有数据需要聚合")
    }
    return aggregatorFunc(filteredData), nil
}

// 使用 type 定义函数类型别名，代码更清晰
type StringFilter func(string) bool
type StringTransformer func(string) (string, error)
type StringAggregator func([]string) string

func processDataWithTypeAlias(
    data []string,
    filter StringFilter,
    transform StringTransformer,
    aggregate StringAggregator,
) (string, error) {
    // 函数体与 processData 相同
    var filteredData []string
    for _, d := range data {
        if filter(d) {
            transformed, err := transform(d)
            if err != nil {
                return "", fmt.Errorf("转换 '%s' 失败: %w", d, err)
            }
            filteredData = append(filteredData, transformed)
        }
    }
    if len(filteredData) == 0 {
        return "", errors.New("没有数据需要聚合")
    }
    return aggregate(filteredData), nil
}

func main() {
    sampleData := []string{"  apple  ", "Banana", "  CHERRY  ", "date"}

    // 使用原始的 processData，函数调用时也可能显得冗长
    result, err := processData(
        sampleData,
        func(s string) bool { return len(strings.TrimSpace(s)) &gt; 0 },
        func(s string) (string, error) {
            trimmed := strings.TrimSpace(s)
            if strings.ToLower(trimmed) == "banana" { // 假设banana是不允许的
                return "", errors.New("包含非法水果banana")
            }
            return strings.ToUpper(trimmed), nil
        },
        func(s []string) string { return strings.Join(s, ", ") },
    )

    if err != nil {
        fmt.Printf("处理错误 (原始方式): %v\n", err)
    } else {
        fmt.Printf("处理结果 (原始方式): %s\n", result)
    }

    // 使用 processDataWithTypeAlias，定义和调用都更清晰
    filter := func(s string) bool { return len(strings.TrimSpace(s)) &gt; 0 }
    transformer := func(s string) (string, error) {
        trimmed := strings.TrimSpace(s)
        if strings.ToLower(trimmed) == "banana" {
            return "", errors.New("包含非法水果banana")
        }
        return strings.ToUpper(trimmed), nil
    }
    aggregator := func(s []string) string { return strings.Join(s, ", ") }

    resultTyped, errTyped := processDataWithTypeAlias(sampleData, filter, transformer, aggregator)
    if errTyped != nil {
        fmt.Printf("处理错误 (类型别名方式): %v\n", errTyped)
    } else {
        fmt.Printf("处理结果 (类型别名方式): %s\n", resultTyped)
    }
}
</code></pre>
<p><strong>问题分析：</strong></p>
<p>Go 语言的类型系统是强类型且显式的。函数类型本身也是一种类型。当我们将一个函数类型（特别是具有多个参数和返回值的复杂函数类型）直接作为另一个函数的参数类型或返回值类型时，会导致函数签名变得非常长，难以阅读和理解。这与 Go 追求简洁和可读性的哲学在观感上有所冲突。</p>
<p><strong>避坑指南：</strong></p>
<ol>
<li><strong>使用 type 关键字定义函数类型别名：</strong> 这是解决此类问题的最推荐、最地道也是最常见的方法。通过为复杂的函数签名定义一个有意义的类型名称，可以极大地提高代码的可读性和可维护性。如示例中的 StringFilter, StringTransformer, StringAggregator。</li>
<li><strong>何时可以不使用类型别名：</strong>
<ul>
<li>当函数签名非常简单（例如 func() 或 func(int) int）且该函数类型只在局部、极少数地方使用时，直接写出可能问题不大。</li>
<li>但一旦函数签名变复杂，或者该函数类型需要在多个地方使用（作为不同函数的参数或返回值，或者作为结构体字段类型），就应该毫不犹豫地使用类型别名。</li>
</ul>
</li>
<li><strong>理解背后的设计考量：</strong> Go 语言强调类型的明确性。虽然直接写出函数类型显得“笨拙”，但也保证了类型信息在代码中的完全显露，避免了某些动态语言中因类型不明确可能导致的困惑。类型别名则是在这种明确性的基础上，提供了提升可读性的手段。</li>
</ol>
<p>为了更好地简化匿名函数，Go团队也提出了关于引入轻量级匿名函数语法的提案（Issue #21498），该提案一直是社区讨论的焦点，它旨在提供一种更简洁的方式来定义匿名函数，尤其是当函数类型可以从上下文推断时，从而减少样板代码，提升代码的可读性和编写效率。</p>
<h2>小结：于细微处见真章，持续打磨代码品质</h2>
<p>今天我们复盘的这六个 Go 编码“坏味道”——异步时序混乱、指针闭包陷阱、不当的错误处理、http.Client 误用、文档缺失的 API 以及冗长的函数签名——可能只是我们日常开发中遇到问题的冰山一角。</p>
<p>它们中的每一个，看似都是细节问题，但“千里之堤，溃于蚁穴”。正是这些细节的累积，最终决定了我们软件产品的质量、系统的稳定性和团队的开发效率。</p>
<p>识别并规避这些“坏味道”，需要我们：</p>
<ul>
<li><strong>深入理解 Go 语言的特性和设计哲学。</strong></li>
<li><strong>培养严谨的工程思维和对细节的关注。</strong></li>
<li><strong>重视代码审查，从他人的错误和经验中学习。</strong></li>
<li><strong>持续学习，不断反思和总结自己的编码实践。</strong></li>
</ul>
<p>希望今天的分享能给大家带来一些启发。让我们一起努力，写出更少“坑”、更高质量的 Go 代码！</p>
<hr />
<p><strong>聊一聊，也帮个忙：</strong></p>
<ul>
<li><strong>在你日常的 Go 开发或 Code Review 中，还遇到过哪些让你印象深刻的“编码坏味道”？</strong></li>
<li><strong>对于今天提到的这些问题，你是否有自己独特的解决技巧或更深刻的理解？</strong></li>
<li><strong>你认为在团队中推广良好的编码规范和实践，最有效的方法是什么？</strong></li>
</ul>
<p>欢迎在<strong>评论区</strong>留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助，也请<strong>转发给你身边的 Gopher 朋友们</strong>，让我们一起在 Go 的道路上精进！</p>
<p><strong>想与我进行更深入的 Go 语言、编码实践与 AI 技术交流吗？</strong> 欢迎加入我的<strong>“Go &amp; AI 精进营”知识星球</strong>。</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p>我们星球见！</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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/31/six-smells-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性</title>
		<link>https://tonybai.com/2025/05/30/gopher-girlfriend/</link>
		<comments>https://tonybai.com/2025/05/30/gopher-girlfriend/#comments</comments>
		<pubDate>Fri, 30 May 2025 11:33:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomod]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[recover]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[sqlx]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[XML]]></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=4765</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/30/gopher-girlfriend 大家好，我是Tony Bai。 最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。 那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。 “Gopher 女友”的可爱特质大揭秘！ 让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照： 1. “cute” (可爱) Meme 解读： 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。 Go语言真相： 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的语法简洁、核心概念少、没有过多的“语法糖”，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。 2. “low-maintenance” (低维护) Meme 解读： 她不“作”，好相处，不需要你花太多心思去“伺候”。 Go语言真相： 这简直是 Go 语言的真实写照！ gofmt 强制统一代码风格，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。 强大的工具链 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-girlfriend-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/30/gopher-girlfriend">本文永久链接</a> &#8211; https://tonybai.com/2025/05/30/gopher-girlfriend</p>
<p>大家好，我是Tony Bai。</p>
<p>最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-girlfriend-2.jpg" alt="" /></p>
<p>那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。</p>
<h2>“Gopher 女友”的可爱特质大揭秘！</h2>
<p>让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照：</p>
<p><strong>1. “cute” (可爱)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。</li>
<li><strong>Go语言真相：</strong> 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的<strong>语法简洁、核心概念少、没有过多的“语法糖”</strong>，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。</li>
</ul>
<p><strong>2. “low-maintenance” (低维护)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她不“作”，好相处，不需要你花太多心思去“伺候”。</li>
<li><strong>Go语言真相：</strong> 这简直是 Go 语言的真实写照！
<ul>
<li><strong>gofmt 强制统一代码风格</strong>，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。</li>
<li><strong>强大的工具链</strong> (go build, go test, go mod 等) 让构建、测试、依赖管理变得异常简单。</li>
<li><strong>静态编译生成单个可执行文件</strong>，部署过程干净利落，没有复杂的运行时依赖和“DLL地狱”。</li>
<li><strong>内置垃圾回收 (GC)</strong> 机制，虽然不是“银弹”，但也极大地减轻了开发者的内存管理负担。</li>
</ul>
</li>
</ul>
<p>这些特性使得Go项目的<strong>维护成本相对较低</strong>，开发者可以将更多精力聚焦在业务逻辑上。</p>
<p><strong>3. “leaves you love letters in go.mod” (在 go.mod 里给你留情书)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 多么浪漫的表达！她把对你的“心意”（依赖）都清清楚楚地写在了 go.mod 这封“情书”里。</li>
<li><strong>Go语言真相：</strong> 自从 Go Modules 成为官方推荐的依赖管理方案后，go.mod 文件就成了每个 Go 项目的“标准配置”。它清晰、明确地记录了项目的模块路径、Go 版本以及所有直接和间接依赖及其版本号。这种<strong>依赖关系的透明化和可追溯性</strong>，就像一封真挚的“情书”，让你对项目的“家底”一目了然，极大地方便了依赖管理和构建复现。</li>
</ul>
<p><strong>4. “panics but quickly recovers” (会panic但能快速恢复)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她偶尔也会有小情绪（panic），但总能很快调整过来（recover），不至于让关系彻底崩溃。</li>
<li><strong>Go语言真相：</strong> Go 语言通过 panic 来表示严重的、通常是程序缺陷导致的运行时错误。但与其他一些语言遇到类似情况直接崩溃不同，Go 提供了 recover 机制。通过在 defer 函数中调用 recover()，我们可以捕获 panic，记录错误信息，执行一些清理操作，甚至尝试让程序从一个可控的状态恢复或优雅降级，而不是让整个服务“一蹶不振”。这种设计赋予了 Go 程序更强的<strong>韧性</strong>。</li>
</ul>
<p><strong>5. “shares her emotions by communicating” (通过沟通分享她的情感)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她乐于沟通，而不是让你猜她的心思。</li>
<li><strong>Go 语言真相：</strong> 这无疑是在致敬 Go 并发编程的核心原语——<strong>channel</strong>！Go 语言信奉“不要通过共享内存来通信，而要通过通信来共享内存” (Don&#8217;t communicate by sharing memory, share memory by communicating) 的并发哲学。Channel 正是 goroutine 之间进行<strong>数据传递和状态同步</strong>的主要桥梁，它使得并发逻辑的表达更加清晰和安全。</li>
</ul>
<p><strong>6. “thinks mutexes are romantic” (认为互斥锁是浪漫的)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 这个有点“硬核”的浪漫！她认为互斥锁 (mutex) 这种保护共享资源、确保“二人世界”不被打扰的机制，是充满“安全感”的浪漫。</li>
<li><strong>Go语言真相：</strong> sync.Mutex 是 Go 中最常用的并发同步原语之一，用于在并发访问共享资源时避免竞态条件。虽然 Go 推崇通过 channel 进行通信，但在某些场景下，使用互斥锁保护共享数据仍然是必要且高效的。这个梗幽默地反映了 Gopher 对<strong>并发安全</strong>的极致追求和对底层同步机制的熟悉。</li>
</ul>
<p><strong>7. “doesn&#8217;t cry when invalid memory address or nil pointer dereference” (当无效内存地址或空指针解引用时不会哭)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 遇到问题，她不“哭哭啼啼”（难以追踪的错误），而是直接“告诉你”（panic）。</li>
<li><strong>Go 语言真相：</strong> 当 Go 程序遇到空指针解引用、数组越界等严重的运行时错误时，它会立即 panic，并打印出清晰的错误信息和堆栈跟踪。这与某些语言可能产生的段错误 (segmentation fault) 或未定义行为，导致问题难以定位和复现相比，无疑是一种更“直接”和有助于<strong>快速暴露和定位 Bug</strong> 的行为。</li>
</ul>
<p><strong>8. “thinks ORM is astrology for devs” (认为 ORM 对开发者来说是占星术)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她对那些过度封装、隐藏细节、让人感觉像“玄学”的 ORM 框架持保留态度。</li>
<li><strong>Go语言真相：</strong> 这是 Go 社区一个广为人知的“文化梗”。许多 Gopher 更倾向于使用标准库的 database/sql 包配合轻量级的 SQL 构建库（如 sqlx等），或者直接编写原生 SQL。这背后是对<strong>数据层掌控力、性能透明度以及避免不必要的“魔法”和复杂抽象</strong>的追求。他们认为，SQL 本身就是一种强大的 DSL，过度封装反而可能引入新的问题。</li>
</ul>
<p><strong>9. “cooks you meals from scratch” (从零开始为你做饭)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她心灵手巧，能用最新鲜的食材（标准库）为你烹制美味佳肴，而不是依赖各种半成品（重型框架或过多第三方库）。</li>
<li><strong>Go 语言真相：</strong> Go 拥有一个异常<strong>强大且设计精良的标准库</strong>。无论是网络编程 (net/http, net)、JSON/XML 处理 (encoding/json, encoding/xml)、文件操作 (os, io)、加密解密 (crypto/*)，还是并发原语 (sync, sync/atomic)，标准库都提供了高质量的实现。这使得 Go 开发者在很多场景下可以“自给自足”，减少对外部依赖，构建出更轻量、更可控的系统。</li>
</ul>
<p><strong>10. “reviews your code every night” (每晚都审查你的代码)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她非常关心你的代码质量，时刻帮你把关。</li>
<li><strong>Go 语言真相：</strong> 这可以从几个层面理解：
<ul>
<li><strong>静态类型检查：</strong> Go 是一门静态类型语言，编译器在编译阶段就能帮你发现大量的类型错误和低级 Bug，就像一位尽职的“审查员”。</li>
<li><strong>go vet 等工具：</strong> Go 工具链内置了 go vet 等静态分析工具，可以帮助检查代码中潜在的错误或可疑构造。</li>
<li><strong>社区文化：</strong> Go 社区非常重视 Code Review 的实践，鼓励通过同行评审来提升代码质量。</li>
<li><strong>语言设计本身：</strong> Go 语言的简洁性和一些强制性规范（如未使用变量的编译错误），也在某种程度上“迫使”开发者写出更清晰、更规范的代码，更易于审查。</li>
</ul>
</li>
</ul>
<p><strong>11. “compiles fast” (编译快)</strong></p>
<ul>
<li><strong>Meme 解读：</strong> 她做事麻利，从不拖沓。</li>
<li><strong>Go 语言真相：</strong> 这绝对是 Go 语言最令人称道的特性之一！Go 的<strong>编译速度极快</strong>，即使是中大型项目，编译过程通常也只需要十几秒钟。这极大地提升了开发者的工作效率和迭代速度，减少了漫长的等待时间，让开发体验如丝般顺滑。快速编译使得“编码-编译-测试”的循环非常高效。</li>
</ul>
<h2>小结：“Go语言女友”，为何如此理想？</h2>
<p>看完了对 “gopher gf” Meme 图的逐条解读，我们不难发现，这位“理想女友”的每一个“可爱特质”，都精准地映射了 Go 语言在现实世界中的核心优势：</p>
<ul>
<li><strong>简洁易学 (cute)</strong></li>
<li><strong>维护成本低 (low-maintenance)</strong></li>
<li><strong>依赖管理清晰 (leaves you love letters in go.mod)</strong></li>
<li><strong>具备韧性的错误处理 (panics but quickly recovers)</strong></li>
<li><strong>推崇通信共享内存的并发模型 (shares her emotions by communicating)</strong></li>
<li><strong>重视并发安全 (thinks mutexes are romantic)</strong></li>
<li><strong>明确的运行时错误反馈 (doesn&#8217;t cry when invalid memory address or nil pointer dereference)</strong></li>
<li><strong>崇尚直接、避免过度抽象 (thinks ORM is astrology for devs)</strong></li>
<li><strong>强大的标准库 (cooks you meals from scratch)</strong></li>
<li><strong>利于代码质量保障的特性与文化 (reviews your code every night)</strong></li>
<li><strong>闪电般的编译速度 (compiles fast)</strong></li>
</ul>
<p>正是这些特性，使得 Go 语言在云原生、微服务、分布式系统、网络编程、命令行工具等众多领域大放异彩，成为越来越多开发者和企业的首选。它就像一位可靠、高效、易于相处且不乏生活情趣的“伴侣”，帮助我们更轻松、更愉快地构建出色的软件系统。</p>
<p>当然，Meme 终归是 Meme，它用一种轻松幽默的方式，概括了 Go 语言的诸多美好。现实中的 Go 语言也并非完美无缺，它依然在不断发展和进化。但不可否认的是，这些“可爱”的特质，正是 Go 语言独特魅力和强大生命力的源泉。</p>
<p>那么，你心中的“Go 语言女友”又是怎样的呢？或者，你最欣赏 Go 语言的哪个“可爱”特质？</p>
<p>欢迎在<strong>评论区</strong>分享你的看法和脑洞！如果你觉得这篇文章有趣且让你对 Go 语言有了更深的（或者说更“萌”的）理解，也请<strong>转发给你身边的 Gopher 朋友们</strong>，一起感受这份来自代码世界的“浪漫”与“可爱”！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<blockquote>
<p>注：本文部分内容经过AI润色和优化，以提升读者阅读体验。</p>
</blockquote>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/30/gopher-girlfriend/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.20中值得关注的几个变化</title>
		<link>https://tonybai.com/2023/02/08/some-changes-in-go-1-20/</link>
		<comments>https://tonybai.com/2023/02/08/some-changes-in-go-1-20/#comments</comments>
		<pubDate>Wed, 08 Feb 2023 15:21:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[arena]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[benchstat]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[comparable]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[cover]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[ecdh]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.17]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[go1.19]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[GOCOVERDIR]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[loccount]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PGO]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[port]]></category>
		<category><![CDATA[riscv64]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[stdlib]]></category>
		<category><![CDATA[unit-test]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[vet]]></category>
		<category><![CDATA[wasi]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[WebAssembly]]></category>
		<category><![CDATA[wrap]]></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=3794</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/02/08/some-changes-in-go-1-20 美国时间2023年2月1日，唯一尚未退休的Go语言之父Robert Griesemer代表Go核心开发团队在Go官博撰文正式发布了Go 1.20版本。就像Russ Cox在2022 GopherCon大会所说的那样：Go2永不会到来，Go 1.x.y将无限延续！ 注：似乎新兴编程语言都喜欢停留在1.x.y上无限延续，譬如已经演化到1.67版本的Rust^_^。 在《Go，13周年》之后，Go 1.20新特性在开发主干冻结(2022.11)之前，我曾写过一篇《Go 1.20新特性前瞻》，对照着Go 1.20 milestone中内容，把我认为的主要特性和大家简单过了一遍，不过那时Go 1.20毕竟没有正式发布，前瞻肯定不够全面，某些具体的点与正式版本可能也有差异！现在Go 1.20版本正式发布了，其Release Notes也补充完整了，在这一篇中，我再来系统说说Go 1.20版本中值得关注的那些变化。对于在前瞻一文中详细介绍过的特性，这里不会再重复讲解了，大家参考前瞻一文中的内容即可。而对于其他一些特性，或是前瞻一文中着墨不多的特性，这里会挑重点展开说说。 按照惯例，我们依旧首先来看看Go语法层面都有哪些变化，这可能也是多数Gopher们最为关注的变化点。 一. 语法变化 Go秉持“大道至简”的理念，对Go语法特性向来是“不与时俱进”的。自从Go 1.18大刀阔斧的加入了泛型特性后，Go语法特性就又恢复到了之前的“新三年旧三年，缝缝补补又三年”的节奏。Go 1.20亦是如此啊！Release Notes说Go 1.20版本在语言方面包含了四点变化，但看了变化的内容后，我觉得真正的变化只有一个，其他的都是修修补补。 1. 切片到数组的转换 唯一算是真语法变化的特性是支持切片类型到数组类型(或数组类型的指针)的类型转换，这个特性在前瞻一文中系统讲过，这里就不赘述了，放个例子大家直观认知一下就可以了： // https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/slice2arr.go func slice2arrOK() { var sl = []int{1, 2, 3, 4, 5, 6, 7} var arr = [7]int(sl) var parr = (*[7]int)(sl) fmt.Println(sl) // [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-20-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20">本文永久链接</a> &#8211; https://tonybai.com/2023/02/08/some-changes-in-go-1-20</p>
<p>美国时间2023年2月1日，唯一尚未退休的Go语言之父<a href="https://github.com/griesemer">Robert Griesemer</a>代表Go核心开发团队在<a href="https://go.dev/blog/go1.20">Go官博撰文正式发布了Go 1.20版本</a>。就像<a href="https://www.youtube.com/watch?v=v24wrd3RwGo">Russ Cox在2022 GopherCon大会</a>所说的那样：<strong><a href="https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language">Go2永不会到来，Go 1.x.y将无限延续</a></strong>！</p>
<blockquote>
<p>注：似乎新兴编程语言都喜欢停留在1.x.y上无限延续，譬如已经<a href="https://www.rust-lang.org">演化到1.67版本的Rust</a>^_^。</p>
</blockquote>
<p>在<a href="https://tonybai.com/2022/11/11/go-opensource-13-years/">《Go，13周年》</a>之后，Go 1.20新特性在开发主干冻结(2022.11)之前，我曾写过一篇《<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">Go 1.20新特性前瞻</a>》，对照着<a href="https://github.com/golang/go/milestone/250">Go 1.20 milestone</a>中内容，把我认为的主要特性和大家简单过了一遍，不过那时Go 1.20毕竟没有正式发布，前瞻肯定不够全面，某些具体的点与正式版本可能也有差异！现在Go 1.20版本正式发布了，其<a href="https://go.dev/blog/go1.20">Release Notes</a>也补充完整了，在这一篇中，我再来系统说说Go 1.20版本中值得关注的那些变化。对于在<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">前瞻一文</a>中详细介绍过的特性，这里不会再重复讲解了，大家参考前瞻一文中的内容即可。而对于其他一些特性，或是前瞻一文中着墨不多的特性，这里会挑重点展开说说。</p>
<p>按照惯例，我们依旧首先来看看Go语法层面都有哪些变化，这可能也是多数Gopher们最为关注的变化点。</p>
<h2>一. 语法变化</h2>
<p>Go秉持“大道至简”的理念，对Go语法特性向来是“不与时俱进”的。自从<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18大刀阔斧的加入了泛型特性</a>后，Go语法特性就又恢复到了之前的“新三年旧三年，缝缝补补又三年”的节奏。Go 1.20亦是如此啊！Release Notes说Go 1.20版本在语言方面包含了四点变化，但看了变化的内容后，我觉得真正的变化只有一个，其他的都是修修补补。</p>
<h3>1. 切片到数组的转换</h3>
<p>唯一算是真语法变化的特性是支持<strong>切片类型到数组类型(或数组类型的指针)的类型转换</strong>，这个特性在<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">前瞻一文</a>中系统讲过，这里就不赘述了，放个例子大家直观认知一下就可以了：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/slice2arr.go

func slice2arrOK() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var arr = [7]int(sl)
    var parr = (*[7]int)(sl)
    fmt.Println(sl)  // [1 2 3 4 5 6 7]
    fmt.Println(arr) // [1 2 3 4 5 6 7]
    sl[0] = 11
    fmt.Println(arr)  // [1 2 3 4 5 6 7]
    fmt.Println(parr) // &amp;[11 2 3 4 5 6 7]
}

func slice2arrPanic() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    fmt.Println(sl)
    var arr = [8]int(sl) // panic: runtime error: cannot convert slice with length 7 to array or pointer to array with leng  th 8
    fmt.Println(arr)     // &amp;[11 2 3 4 5 6 7]

}

func main() {
    slice2arrOK()
    slice2arrPanic()
}
</code></pre>
<p>有两点注意一下就好：</p>
<ul>
<li>切片转换为数组类型的指针，那么该指针将指向切片的底层数组，就如同上面例子中slice2arrOK的parr变量那样；</li>
<li>转换的数组类型的长度不能大于原切片的长度(注意是长度而不是切片的容量哦)，否则在运行时会抛出panic。</li>
</ul>
<h3>2. 其他的修修补补</h3>
<ul>
<li>comparable“放宽”了对泛型实参的限制</li>
</ul>
<p>下面代码在Go 1.20版本之前，比如Go 1.19版本中会无法通过编译：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable.go

func doSth[T comparable](t T) {
}

func main() {
    n := 2
    var i interface{} = n // 编译错误：interface{} does not implement comparable
    doSth(i)
}
</code></pre>
<p>之前，comparable约束下的泛型形参需要支持严格可比较(strictly comparable)的类型作为泛型实参，哪些是严格可比较的类型呢？Go 1.20的语法规范做出了进一步澄清：如果一个类型是可比较的，且不是接口类型或由接口类型组成的类型，那么这个类型就是<strong>严格可比较的类型</strong>，包括：</p>
<pre><code>- 布尔型、数值类型、字符串类型、指针类型和channel是严格可比较的。
- 如果结构体类型的所有字段的类型都是严格可比较的，那么该结构体类型就是严格可比较的。
- 如果数组元素的类型是严格可比较的，那么该数组类型就是严格可比较的。
- 如果类型形参的类型集合中的所有类型都是严格可比较的，那么该类型形参就是严格可比较的。
</code></pre>
<p>我们看到：例外的就是接口类型了。接口类型不是“严格可比较的(strictly comparable)”，但未作为类型形参的接口类型是可比较的(comparable)，如果两个接口类型的动态类型相同且值相等，那么这两个接口类型就相等，或两个接口类型的值均为nil，它们也相等，否则不等。</p>
<p>Go 1.19版本及之前，作为非严格比较类型的接口类型是不能作为comparable约束的类型形参的类型实参的，就像上面comparable.go中示例代码那样，但Go 1.20版本开始，这一要求被防控，接口类型被允许作为类型实参赋值给comparable约束的类型形参了！不过这么做之前，你也要明确一点，如果像下面这样两个接口类型底层类型相同且是不可比较的类型（比如切片)，那么代码将在运行时抛panic：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable1.go

func doSth[T comparable](t1, t2 T) {
    if t1 != t2 {
        println("unequal")
        return
    }
    println("equal")
}

func main() {
    n1 := []byte{2}
    n2 := []byte{3}
    var i interface{} = n1
    var j interface{} = n2
    doSth(i, j) // panic: runtime error: comparing uncomparable type []uint8
}
</code></pre>
<p>Go 1.20语言规范借此机会还进一步澄清了结构体和数组两种类型比较实现的规范：对于结构体类型，Go会按照结构体字段的声明顺序，逐一字段进行比较，直到遇到第一个不相等的字段为止。如果没有不相等字段，则两个结构体字段相等；对于数组类型，Go会按数组元素的顺序，逐一元素进行比较，直到遇到第一个不相等的元素为止。如果没有不相等的元素，则两个数组相等。</p>
<ul>
<li>unsafe包继续添加“语法糖”</li>
</ul>
<p>继<a href="https://tonybai.com/2021/08/17/some-changes-in-go-1-17">Go 1.17版本</a>在unsafe包增加Slice函数后，Go 1.20版本又增加三个语法糖函数：SliceData、String和StringData：</p>
<pre><code>// $GOROOT/src/unsafe/unsafe.go
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte
</code></pre>
<p>值得注意的是由于string的不可更改性，String函数的参数ptr指向的内容以及StringData返回的指针指向的内容在String调用和StringData调用后不允许修改，但实际情况是怎么样的呢？</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/unsafe.go

func main() {
    var arr = [6]byte{'h', 'e', 'l', 'l', 'o', '!'}
    s := unsafe.String(&amp;arr[0], 6)
    fmt.Println(s) // hello!
    arr[0] = 'j'
    fmt.Println(s) // jello!

    b := unsafe.StringData(s)
    *b = 'k'
    fmt.Println(s) // kello!

    s1 := "golang"
    fmt.Println(s1) // golang
    b = unsafe.StringData(s1)
    *b = 'h' // fatal error: fault, unexpected fault address 0x10a67e5
    fmt.Println(s1)
}
</code></pre>
<p>我们看到：unsafe.String函数调用后，如果我们修改了传入的指针指向的内容，那么该改动会影响到后续返回的string内容！而StringData返回<br />
的指针所指向的内容一旦被修改，其结果要根据字符串的来源而定了。对于由可修改的底层数组“创建”的字符串(如s)，通过StringData返回的指<br />
针可以“修改”字符串的内容；而对于由字符串字面值初始化的字符串变量(如s1)，其内容是不可修改的(编译器将字符串底层存储分配在了只读数据区)，尝试通过指针修改指向内容，会导致运行时的段错误。</p>
<h2>二. 工具链</h2>
<h3>1. Go安装包“瘦身”</h3>
<p>这些年，Go发布版的安装包“体格”是越来越壮了，动辄100多MB的压缩包，以go.dev/dl页面上的go1.xy.linux-amd64.tar.gz为例，我们看看从Go 1.15版本到Go 1.19版本的“体格”变化趋势：</p>
<pre><code>Go 1.15 - 116MB
Go 1.16 - 123MB
Go 1.17 - 129MB
Go 1.18 - 135MB
Go 1.19 - 142MB
</code></pre>
<p>如果按此趋势，Go 1.20势必要上到150MB以上。但Go团队找到了“瘦身”方法，那就是：<a href="https://github.com/golang/go/issues/47257">从Go 1.20开始发行版的安装包不再为GOROOT中的软件包提供预编译的.a文件了</a>，这样我们得到的瘦身后的Go 1.20版本的size为<strong>95MB</strong>！相较于Go 1.19，Go 1.20的安装包“瘦”了三分之一。安装包解压后这种体现更为明显：</p>
<pre><code>➜  /Users/tonybai/.bin/go1.19 git:(master) ✗ $du -sh
495M    .
➜  /Users/tonybai/.bin/go1.20 git:(master) ✗ $du -sh
265M    .
</code></pre>
<p>我们看到：Go 1.20占用的磁盘空间仅为Go 1.19版本的一半多一点而已。 并且，Go 1.20版本中，GOROOT下的源码将像其他用户包那样在构建后被缓存到本机cache中。此外，go install也不会为GOROOT下的软件包安装.a文件。</p>
<h3>2. 编译器</h3>
<h4>1) PGO(profile-guided optimization)</h4>
<p>Go 1.20编译器的一个最大的变更点是引入了PGO优化技术预览版，这个在前瞻一文中也有<a href="https://tonybai.com/2022/11/17/go-1-20-foresight">对PGO技术的简单介绍</a>。说白了点，PGO技术就是在原有compiler优化技术的基础上，针对程序在生产环境运行中的热点关键路径再进行一轮优化，并且针对热点代码执行路径，编译器会放开一些限制，比如<a href="https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example">Go决定是否对函数进行内联优化的复杂度上限默认值是80</a>，但对于PGO指示的关键热点路径，即便函数复杂性超过80很多，也可能会被inline优化掉。</p>
<p>之前持续性能剖析工具开发商Polar Signals曾发布一篇文章<a href="https://www.polarsignals.com/blog/posts/2022/09/exploring-go-profile-guided-optimizations/">《Exploring Go&#8217;s Profile-Guided Optimizations》</a>，专门探讨了PGO技术可能带来的优化效果，文章中借助了Go项目中自带的测试示例，这里也基于这个示例带大家重现一下。</p>
<p>我们使用的例子在Go 1.20源码/安装包的\$GOROOT/src/cmd/compile/internal/test/testdata/pgo/inline路径下：</p>
<pre><code>$ls -l
total 3156
-rw-r--r-- 1 tonybai tonybai    1698 Jan 31 05:46 inline_hot.go
-rw-r--r-- 1 tonybai tonybai     843 Jan 31 05:46 inline_hot_test.go
</code></pre>
<p>我们首先执行一下inline目录下的测试，并生成用于测试的可执行文件以及对应的cpu profile文件供后续PGO优化使用：</p>
<pre><code>$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof
goos: linux
goarch: amd64
pkg: cmd/compile/internal/test/testdata/pgo/inline
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkA-8        1348        870005 ns/op
PASS
ok      cmd/compile/internal/test/testdata/pgo/inline   1.413s
</code></pre>
<p>接下来，我们对比一下不使用PGO和使用PGO优化，Go编译器在内联优化上的区别：</p>
<pre><code>$diff &lt;(go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m" 2&gt;&amp;1 | grep "can inline") &lt;(go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m -pgoprofile inline_hot.pprof" 2&gt;&amp;1 | grep "can inline")
4a5,6
&gt; ./inline_hot.go:53:6: can inline (*BS).NS with cost 106 as: method(*BS) func(uint) (uint, bool) { x := int(i &gt;&gt; lWSize); if x &gt;= len(b.s) { return 0, false }; w := b.s[x]; w = w &gt;&gt; (i &amp; (wSize - 1)); if w != 0 { return i + T(w), true }; x = x + 1; for loop; return 0, false }
&gt; ./inline_hot.go:74:6: can inline A with cost 312 as: func() { s := N(100000); for loop; for loop }
</code></pre>
<blockquote>
<p>上面diff命令中为Go test命令传入-run=none -tags=”" -gcflags=”-m -m”是为了仅编译源文件，而不执行任何测试。</p>
</blockquote>
<p>我们看到，相较于未使用PGO优化的结果，PGO优化后的结果多了两个inline函数，这两个可以被inline的函数，一个的复杂度开销为106，一个是312，都超出了默认的80，但仍然可以被inline。</p>
<p>我们来看看PGO的实际优化效果，我们分为在无PGO优化与有PGO优化下执行100次benchmark，再用benchstat工具对比两次的结果：</p>
<pre><code>$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof -count=100 &gt; without_pgo.txt
$go test -o inline_hot.test -bench=. -gcflags="-pgoprofile inline_hot.pprof" -count=100 &gt; with_pgo.txt

$benchstat without_pgo.txt with_pgo.txt
goos: linux
goarch: amd64
pkg: cmd/compile/internal/test/testdata/pgo/inline
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
    │ without_pgo.txt │            with_pgo.txt             │
    │     sec/op      │   sec/op     vs base                │
A-8       874.7µ ± 0%   872.6µ ± 0%  -0.24% (p=0.024 n=100)
</code></pre>
<blockquote>
<p>注：benchstat的安装方法：\$go install golang.org/x/perf/cmd/benchstat@latest</p>
</blockquote>
<p>我们看到，在我的机器上(ubuntu 20.04 linux kerenel 5.4.0-132)，PGO针对这个测试的优化效果并不明显(仅仅有0.24%的提升)，Polar Signals原文中的提升幅度也不大，仅为1.05%。</p>
<p>Go官方Release Notes中提到benchmark提升效果为3%~4%，同时官方也提到了，这个仅仅是PGO初始技术预览版，后续会加强对PGO优化的投入，直至对多数程序产生较为明显的优化效果。个人觉得目前PGO尚处于早期，不建议在生产中使用。</p>
<p>Go官方也增加针对<a href="https://go.dev/doc/pgo">PGO的ref页面</a>，大家重点看看其中的FAQ，你会有更多收获！</p>
<h4>2) 构建速度</h4>
<p>Go 1.18泛型落地后，Go编译器的编译速度出现了回退(幅度15%)，Go 1.19编译速度也没有提升。虽然编译速度回退后依然可以“秒杀”竞争对手，但对于以编译速度快著称的Go来说，这个问题必须修复。Go 1.20做到了这一点，让Go编译器的编译速度重新回归到了Go 1.17的水准！相对Go 1.19提升10%左右。</p>
<p>我使用github.com/reviewdog/reviewdog这个库实测了一下，分别使用go 1.17.1、go 1.18.6、go 1.19.1和Go 1.20对这个module进行go build -a构建(之前将依赖包都下载本地，排除掉go get环节的影响)，结果如下：</p>
<pre><code>go 1.20：
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  48.01s user 7.96s system 536% cpu 10.433 total

go 1.19.1：
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  54.40s user 10.20s system 506% cpu 12.757 total

go 1.18.6：
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  53.78s user 9.85s system 545% cpu 11.654 total

go 1.17.1：
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  50.30s user 9.76s system 580% cpu 10.338 total
</code></pre>
<p>虽然不能十分精确，但总体上反映出各个版本的编译速度水准以及Go 1.20相对于Go 1.18和Go 1.19版本的提升。我们看到Go 1.20与Go 1.17版本在一个水平线上，甚至要超过Go 1.17(但可能仅限于我这个个例)。</p>
<h4>3) 允许在泛型函数/方法中进行类型声明</h4>
<p>Go 1.20版本之前下面代码是无法通过Go编译器的编译的：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/tools/compiler/local_type_decl.go
package main

func F[T1 any]() {
    type x struct{} // 编译错误：type declarations inside generic functions are not currently supported
    type y = x      // 编译错误：type declarations inside generic functions are not currently supported
}

func main() {
    F[int]()
}
</code></pre>
<p><a href="https://github.com/golang/go/issues/47631">Go 1.20改进了语言前端的实现</a>，通过unified IR实现了对在泛型函数/方法中进行类型声明(包括定义type alias)的支持。</p>
<p>同时，Go 1.20在<a href="https://go.dev/ref/spec#Type_parameter_declarations">spec</a>中还明确了<a href="https://github.com/golang/go/issues/40882">哪些使用了递归方式声明的类型形参列表是不合法的</a>：</p>
<pre><code>type T1[P T1[P]] …                    // 不合法: 形参列表中作为约束的T1引用了自己
type T2[P interface{ T2[int] }] …     // 不合法: 形参列表中作为约束的T2引用了自己
type T3[P interface{ m(T3[int])}] …   // 不合法: 形参列表中作为约束的T3引用了自己

type T4[P T5[P]] …                    // 不合法: 形参列表中，T4引用了T5 并且
type T5[P T4[P]] …                    //          T5引用了T4

type T6[P int] struct{ f *T6[P] }     // 正确: 虽然引用了T6，但这个引用发生在结构体定义中而不是形参列表中
</code></pre>
<h4>4) 构建自举源码的Go编译器的版本选择</h4>
<p>Go从Go 1.5版本开始实现自举，即使用Go实现Go，那么自举后的Go项目是谁来编译的呢？最初对应编译Go 1.5版本的Go编译器版本为Go 1.4。</p>
<p>以前从源码构建Go发行版，当未设置GOROOT_BOOTSTRAP时，编译脚本会默认使用Go 1.4，但如果有更高版本的Go编译器存在，会使用更高版本的编译器。</p>
<p>Go 1.18和Go 1.19会首先寻找是否有go 1.17版本，如果没有再使用go 1.4。</p>
<p>Go 1.20会寻找当前Go 1.17的最后一个版本Go 1.17.13，如果没有，则使用Go 1.4。</p>
<p>将来，Go核心团队计划一年升级一次构建自举源码的Go编译器的版本，例如：Go 1.22版本将使用Go 1.20版本的编译器。</p>
<h4>5) cgo</h4>
<p>Go命令现在在没有C工具链的系统上会默认禁用了cgo。更具体来说，当CGO_ENABLED环境变量未设置，CC环境变量未设置以及PATH环境变量中没有找到默认的C编译器（通常是clang或gcc）时，CGO_ENABLED会被默认设置为0。</p>
<h3>3. 其他工具</h3>
<h4>1) 支持采集应用执行的代码盖率</h4>
<p>在前瞻一文中，我提到过Go 1.20将对代码覆盖率的支持扩展到了应用整体层面，而不再仅仅是unit test。这里使用一个例子来看一下，究竟如何采集应用代码的执行覆盖率。我们以gitlab.com/esr/loccount这个代码统计工具为例，先修改一下Makefile，在go build后面加上-cover选项，然后编译loccount，并对其自身进行代码统计：</p>
<pre><code>// /home/tonybai/go/src/gitlab.com/loccount
$make
$mkdir mycovdata
$GOCOVERDIR=./mycovdata loccount .
all          SLOC=4279    (100.00%) LLOC=1213    in 110 files
Go           SLOC=1724    (40.29%)  LLOC=835     in 3 files
asciidoc     SLOC=752     (17.57%)  LLOC=0       in 5 files
C            SLOC=278     (6.50%)   LLOC=8       in 2 files
Python       SLOC=156     (3.65%)   LLOC=0       in 2 files
... ...
</code></pre>
<p>上面执行loccount之前，我们建立了一个mycovdata目录，并设置GOCOVERDIR的值为mycovdata目录的路径。在这样的上下文下，执行loccount后，mycovdata目录下会生成一些覆盖率统计数据文件：</p>
<pre><code>$ls mycovdata
covcounters.4ec45ce64f965e77563ecf011e110d4f.926594.1675678144659536943  covmeta.4ec45ce64f965e77563ecf011e110d4f
</code></pre>
<p>怎么查看loccount的执行覆盖率呢？我们使用go tool covdata来查看：</p>
<pre><code>$go tool covdata percent -i=mycovdata
    loccount    coverage: 69.6% of statements
</code></pre>
<p>当然, covdata子命令还支持其他一些功能，大家可以自行查看manual挖掘。</p>
<h4>2) vet</h4>
<p>Go 1.20版本中，go工具链的vet子命令增加了两个十分实用的检测：</p>
<ul>
<li>对loopclosure这一检测策略进行了增强</li>
</ul>
<p>具体可参见https://github.com/golang/tools/tree/master/go/analysis/passes/loopclosure代码</p>
<ul>
<li>增加对2006-02-01的时间格式的检查</li>
</ul>
<p>注意我们使用time.Format或Parse时，最常使用的是2006-01-02这样的格式，即ISO 8601标准的时间格式，但一些代码中总是出现2006-02-01，十分容易导致错误。这个版本中，go vet将会对此种情况进行检查。</p>
<h2>三. 运行时与标准库</h2>
<h3>1. 运行时(runtime)</h3>
<p>Go 1.20运行时的调整并不大，仅对GC的内部数据结构进行了微调，这个调整可以获得最多2%的内存开销下降以及cpu性能提升。</p>
<h3>2. 标准库</h3>
<p>标准库肯定是变化最多的那部分。前瞻一文中对下面变化也做了详细介绍，这里不赘述了，大家可以翻看那篇文章细读：</p>
<ul>
<li>支持wrap multiple errors</li>
<li>time包新增DateTime、DateOnly和TimeOnly三个layout格式常量</li>
<li>新增arena包<br />
&#8230; &#8230;</li>
</ul>
<p>标准库变化很多，这里不能一一罗列，再补充一些我认为重要的，其他的变化大家可以到<a href="https://go.dev/doc/go1.20">Go 1.20 Release Notes</a>去看：</p>
<h4>1) arena包</h4>
<p>前瞻一文已经对arena包做了简要描述，对于arena包的使用以及最佳适用场合的探索还在进行中。著名持续性能剖析工具<a href="https://pyroscope.io/">pyroscope</a>的官方博客文章<a href="https://pyroscope.io/blog/go-1-20-memory-arenas/">《Go 1.20 arenas实践：arena vs. 传统内存管理》</a>对于arena实验特性的使用给出了几点好的建议，比如：</p>
<ul>
<li>只在关键的代码路径中使用arena，不要到处使用它们</li>
<li>在使用arena之前和之后对你的代码进行profiling，以确保你在能提供最大好处的地方添加arena。</li>
<li>密切关注arena上创建的对象的生命周期。确保你不会把它们泄露给你程序中的其他组件，因为那里的对象可能会超过arena的寿命。</li>
<li>使用defer a.Free()来确保你不会忘记释放内存。</li>
<li>如果你想在arena被释放后使用对象，使用arena.Clone()将其克隆回heap中。</li>
</ul>
<p>pyroscope的开发人员认为arena是一个强大的工具，也支持标准库中保留arena这个特性，但也建议将arena和reflect、unsafe、cgo等一样纳入“不推荐”使用的包行列。这点我也是赞同的。我也在考虑如何基于arena改进我们产品的协议解析器的性能，有成果后，我也会将实践过程分享出来的。</p>
<h4>2) 新增crypto/ecdh包</h4>
<p>密码学包(crypto)的主要maintainer <a href="https://filippo.io/">Filippo Valsorda</a>从google离职后，<a href="https://words.filippo.io/full-time-maintainer/">成为了一名专职开源项目维护者</a>。这似乎让其更有精力和动力对crypto包进行更好的规划、设计和实现了。<a href="https://github.com/golang/go/issues/52221">crypto/ecdh包就是在他的提议下加入到Go标准库中的</a>。</p>
<p>相对于标准库之前存在的crypto/elliptic等包，crypto/ecdh包的API更为高级，Go官方推荐使用ecdh的高级API，这样大家以后可以不必再与低级的密码学函数斗争了。</p>
<h4>3) HTTP ResponseController</h4>
<p>以前HTTP handler的超时都是http服务器全局指定一个的：包括ReadTimeout和WriteTimeout。但有些时候，如果能在某个请求范围内支持这些超时（以及可能的其他选项）将非常有用。Damien Neil就创建了这个<a href="https://github.com/golang/go/issues/54136">增加ResponseController的提案</a>，下面是一个在HandlerFunc中使用ResponseController的例子：</p>
<pre><code>http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
  ctl := http.NewResponseController(w, r)
  ctl.SetWriteDeadline(time.Now().Add(1 * time.Minute)) // 仅为这个请求设置deadline
  fmt.Fprintln(w, "Hello, world.") // 这个写入的timeout为1-minute
})
</code></pre>
<h4>4) context包增加WithCancelCause函数</h4>
<p>context包新增了一个WithCancelCause函数，与WithCancel不同，通过WithCancelCause返回的Context，我们可以得到cancel的原因，比如下面示例：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/library/context.go

func main() {
    myError := fmt.Errorf("%s", "myError")
    ctx, cancel := context.WithCancelCause(context.Background())
    cancel(myError)
    fmt.Println(ctx.Err())          // context.Canceled
    fmt.Println(context.Cause(ctx)) // myError
}
</code></pre>
<p>我们看到通过context.Cause可以得到Context在cancel时传入的错误原因。</p>
<h2>四. 移植性</h2>
<p>Go对新cpu体系结构和OS的支持向来是走在前面的。Go 1.20还新增了对freebsd在risc-v上的实验性支持，其环境变量为GOOS=freebsd, GOARCH=riscv64。但Go 1.20也将成为对下面平台提供支持的最后一个Go版本：</p>
<ul>
<li>Windows 7, 8, Server 2008和Server 2012</li>
<li>MacOS 10.13 High Sierra和10.14 (我的安装了10.14的mac os又要在go 1.21不被支持了^_^) </li>
</ul>
<p>近期Go团队又有了新提案：<a href="https://github.com/golang/go/issues/58141">支持WASI(GOOS=wasi GOARCH=wasm)</a>，WASI是啥，它是WebAssembly一套与引擎无关(engine-indepent)的、面向非Web系统的WASM API标准，是WebAssembly脱离浏览器的必经之路！一旦生成满足WASI的WASM程序，该程序就可以在任何支持WASI或兼容的runtime上运行。不出意外，该提案将在Go 1.21或Go 1.22版本落地。</p>
<p>本文中的示例代码可以在<a href="https://github.com/bigwhite/experiments/blob/master/go1.20-examples">这里</a>下载。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/02/08/some-changes-in-go-1-20/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言数据竞争检测与数据竞争模式</title>
		<link>https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go/</link>
		<comments>https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go/#comments</comments>
		<pubDate>Tue, 21 Jun 2022 14:42:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AddressSanitizer]]></category>
		<category><![CDATA[arxiv]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[closure]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[data-race]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[DmitryVyukov]]></category>
		<category><![CDATA[err]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[function-literal]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.1]]></category>
		<category><![CDATA[go1.19]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gorun]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[gotools]]></category>
		<category><![CDATA[LeakSanitizer]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[MemorySanitizer]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[paper]]></category>
		<category><![CDATA[race]]></category>
		<category><![CDATA[race-detector]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[ShadowCell]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[Thread]]></category>
		<category><![CDATA[ThreadSanitizer]]></category>
		<category><![CDATA[TSan]]></category>
		<category><![CDATA[uber]]></category>
		<category><![CDATA[Valgrind]]></category>
		<category><![CDATA[waitgroup]]></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=3597</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go uber，就是那个早早退出中国打车市场的优步，是Go语言早期接纳者，也是Go技术栈的“重度用户”。uber内部的Go代码仓库有5000w+行Go代码，有2100个Go实现的独立服务，这样的Go应用规模在世界范围内估计也是Top3了吧。 uber不仅用Go，还经常输出它们使用Go的经验与教训，uber工程博客就是这些高质量Go文章的载体，这些文章都值得想“深造”的gopher们反复阅读和体会。 近期该博客发布了两篇有关Go并发数据竞争的文章，一篇为《Dynamic Data Race Detection in Go Code》，另一篇为《Data Race Patterns in Go》。这两篇文章也源于uber工程师发表在arxiv上的预印版论文《A Study of Real-World Data Races in Golang》。 感慨一下：不得不佩服国外工程师的这种“下得了厨房，还上得了厅堂”的研发能力，这也是我在团队中为大家树立的目标。 这里和大家过一下这两篇精简版的博客文章，希望我们都能有收获。 一. Go内置data race detector 我们知道：并发程序不好开发，更难于调试。并发是问题的滋生地，即便Go内置并发并提供了基于CSP并发模型的并发原语(goroutine、channel和select)，实际证明，现实世界中，Go程序带来的并发问题并没有因此减少(手动允悲)。“没有银弹”再一次应验！ 不过Go核心团队早已意识到了这一点，在Go 1.1版本中就为Go工具增加了race detector，通过在执行go工具命令时加入-race，该detector可以发现程序中因对同一变量的并发访问(至少一个访问是写操作)而引发潜在并发错误的地方。Go标准库也是引入race detector后的受益者。race detector曾帮助Go标准库检测出42个数据竞争问题。 race detector基于Google一个团队开发的工具Thread Sanitizer(TSan)(除了thread sanitizer，google还有一堆sanitizer，比如：AddressSanitizer, LeakSanitizer, MemorySanitizer等)。第一版TSan的实现发布于2009年，其使用的检测算法“源于”老牌工具Valgrind。出世后，TSan就帮助Chromium浏览器团队找出近200个潜在的并发问题，不过第一版TSan有一个最大的问题，那就是慢！。 因为有了成绩，开发团队决定重写TSan，于是就有了v2版本。与V1版本相比，v2版本有几个主要变化： 编译期注入代码(instrumentation)； 重新实现运行时库，并内置到编译器(LLVM和GCC)中； 除了可以做数据竞争(data race)检测外，还可以检测死锁、加锁状态下的锁释放等问题； 与V1版本相比，v2版本性能提升约20倍； 支持Go语言。 那么TSan v2究竟是怎么工作的呢？我们继续往下看。 二. ThreadSanitizer v2版本工作原理 根据Thread Sanitizer [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go">本文永久链接</a> &#8211; https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go</p>
<p>uber，就是那个早早退出中国打车市场的优步，是Go语言早期接纳者，也是Go技术栈的“重度用户”。<a href="https://eng.uber.com/data-race-patterns-in-go/">uber内部的Go代码仓库有5000w+行Go代码</a>，有2100个Go实现的独立服务，这样的Go应用规模在世界范围内估计也是Top3了吧。</p>
<p>uber不仅用Go，还经常输出它们使用Go的经验与教训，<a href="https://eng.uber.com/">uber工程博客</a>就是这些高质量Go文章的载体，这些文章都值得想“深造”的gopher们反复阅读和体会。</p>
<p>近期该博客发布了两篇有关Go并发数据竞争的文章，一篇为<a href="https://eng.uber.com/dynamic-data-race-detection-in-go-code/">《Dynamic Data Race Detection in Go Code》</a>，另一篇为<a href="https://eng.uber.com/data-race-patterns-in-go/">《Data Race Patterns in Go》</a>。这两篇文章也源于uber工程师发表在arxiv上的预印版论文<a href="https://arxiv.org/pdf/2204.00764.pdf">《A Study of Real-World Data Races in Golang》</a>。</p>
<blockquote>
<p>感慨一下：不得不佩服国外工程师的这种“下得了厨房，还上得了厅堂”的研发能力，这也是我在团队中为大家树立的目标。</p>
</blockquote>
<p>这里和大家过一下这两篇精简版的博客文章，希望我们都能有收获。</p>
<hr />
<h3>一. Go内置data race detector</h3>
<p>我们知道：并发程序不好开发，更难于调试。并发是问题的滋生地，即便Go内置并发并提供了基于CSP并发模型的并发原语(goroutine、channel和select)，实际证明，<a href="https://songlh.github.io/paper/go-study.pdf">现实世界中，Go程序带来的并发问题并没有因此减少</a>(手动允悲)。<strong>“没有银弹”再一次应验</strong>！</p>
<p>不过Go核心团队早已意识到了这一点，在<a href="https://go.dev/doc/go1.1#race">Go 1.1版本</a>中就为Go工具增加了race detector，通过在执行go工具命令时加入-race，该detector可以发现程序中因对同一变量的并发访问(至少一个访问是写操作)而引发潜在并发错误的地方。Go标准库也是引入race detector后的受益者。race detector曾<a href="https://go.dev/blog/race-detector">帮助Go标准库检测出42个数据竞争问题</a>。</p>
<p>race detector基于Google一个团队开发的工具<a href="https://github.com/google/sanitizers">Thread Sanitizer(TSan)</a>(除了thread sanitizer，google还有一堆sanitizer，比如：AddressSanitizer, LeakSanitizer, MemorySanitizer等)。第一版TSan的实现发布于2009年，其使用的检测算法“源于”老牌工具Valgrind。出世后，TSan就帮助Chromium浏览器团队找出近200个潜在的并发问题，不过第一版TSan有一个最大的问题，那就是<strong>慢！</strong>。</p>
<p>因为有了成绩，开发团队决定重写TSan，于是就有了v2版本。与V1版本相比，v2版本有几个主要变化：</p>
<ul>
<li>编译期注入代码(instrumentation)；</li>
<li>重新实现运行时库，并内置到编译器(LLVM和GCC)中；</li>
<li>除了可以做数据竞争(data race)检测外，还可以检测死锁、加锁状态下的锁释放等问题；</li>
<li>与V1版本相比，v2版本性能提升约20倍；</li>
<li>支持Go语言。</li>
</ul>
<p>那么TSan v2究竟是怎么工作的呢？我们继续往下看。</p>
<h3>二. ThreadSanitizer v2版本工作原理</h3>
<p>根据<a href="https://github.com/google/sanitizers/wiki/ThreadSanitizerAlgorithm">Thread Sanitizer wiki上对v2版算法的描述</a>，Thread Sanitizer分为两部分：<strong>注入代码与运行时库</strong>。</p>
<h4>1. 注入代码</h4>
<p>第一部分是在编译阶段配合编译器在源码中注入代码。那么<strong>在什么位置注入什么代码呢</strong>？前面说过Thread Sanitizer会跟踪程序中的每次内存访问，因此TSan会在每次内存访问的地方注入代码，当然下面的情况除外：</p>
<ul>
<li>肯定不会出现数据竞争的内存访问</li>
</ul>
<p>比如：全局常量的读访问、函数中对已被证明不会<a href="https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example/">逃逸到堆</a>上的内存的访问；</p>
<ul>
<li>冗余访问：写入某个内存位置之前发生的读操作</li>
<li>&#8230; &#8230;</li>
</ul>
<p>那么注入的什么代码呢？下面是一个在函数foo内写内存操作的例子：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-2.png" alt="" /></p>
<p>我们看到对地址p的写操作前注入了&#95;&#95;tsan_write4函数，函数foo的入口和出口分别注入了&#95;&#95;tsan_func_entry和 &#95;&#95;tsan_func_exit。而对于需要注入代码的内存读操作，注入代码则是&#95;&#95;tsan_read4；原子内存操作使用&#95;&#95;tsan_atomic进行注入&#8230;。</p>
<h4>2. TSan运行时库</h4>
<p>一旦在编译期注入代码完毕，构建出带有TSan的Go程序，那么在Go程序运行阶段，起到数据竞争检测作用的就是Tsan运行时库了。TSan是如何检测到有数据竞争的呢？</p>
<p>TSan的检测借助了一个称为<strong>Shadow Cell</strong>的概念。什么是Shadow Cell呢？一个Shadow Cell本身是一个8字节的内存单元，它代表一个对某个内存地址的读/写操作的<strong>事件</strong>，即每次对某内存块的写或读操作都会生成一个Shadow Cell。显然Shadow Cell作为内存读写事件的记录者，其本身存储了与此事件相关的信息，如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-3.png" alt="" /></p>
<p>我们看到，每个Shadow Cell记录了线程ID、时钟时间、操作访问内存的位置(偏移)和长度以及该内存访问事件的操作属性(是否是写操作)。<strong>针对每个应用程序的8字节内存，TSan都会对应有一组(N个)Shadow Cell</strong>，如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-4.png" alt="" /></p>
<p>N可以取2、4和8。N的取值直接影响TSan带来的开销以及data race检测的“精度”。</p>
<h4>3. 检测算法</h4>
<p>有了代码注入，也有了记录内存访问事件的Shadow Cell，那么TSan是通过什么逻辑检测data race的呢？我们结合<a href="http://gcc.gnu.org/wiki/cauldron2012?action=AttachFile&amp;do=get&amp;target=kcc.pdf">Google大神Dmitry Vyukov在一次speak中举的例子</a>来看一下检测算法是怎么运作的：</p>
<p>我们以N=8为例(即8个Shadow Cell用于跟踪和校验一个应用的8字节内存块)，下面是初始情况，假设此时尚没有对该8字节应用内存块的读写操作：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-5.png" alt="" /></p>
<p>现在，一个线程T1向该块内存的前两个字节进行了写操作，写操作会生成第一个Shadow Cell，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-6.png" alt="" /></p>
<p>这里我们结合图中的Shadow Cell说说Pos字段。Pos字段描述的是写/读操作访问的8字节内存单元的起始偏移与长度，比如这里的<strong>0:2</strong>代表的就是起始字节为第一个字节，长度为2个字节。此时Shadow Cell窗口只有一个Shadow Cell，不存在race的可能。</p>
<p>接下来，一个线程T2又针对该块内存的后四个字节进行了一次读操作，读操作会生成第二个Shadow Cell，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-7.png" alt="" /></p>
<p>此次读操作涉及的字节与第一个Shadow Cell没有交集，不存在data race的可能。</p>
<p>再接下来，一个线程T3针对该块内存的前四个字节进行了一次写操作，写操作会生成第三个Shadow Cell，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-8.png" alt="" /></p>
<p>我们看到T1和T3两个线程对该内存块的访问有重叠区域，且T1为写操作，那么这种情况就有可能存在data race。而TSan的race检测算法本质上就是一个状态机，每当发生一次内存访问，都会走一遍状态机。状态机的逻辑也很简单，就是遍历这块内存对应的Shadow Cell窗口中的所有Cell，用最新的Cell与已存在的Cell逐一比对，如果存在race，则给出warning。</p>
<p>像这个例子中T1的write与T3的read区域重叠，如果Shallow Cell1的时钟E1没有happens-before Shadow Cell的时钟E3，那么就存在data race的情况。happens-before如何判定，我们可以从tsan的实现中找到端倪：</p>
<pre><code>https://code.woboq.org/gcc/libsanitizer/tsan/tsan_rtl.cc.html

static inline bool HappensBefore(Shadow old, ThreadState *thr) {
    return thr-&gt;clock.get(old.TidWithIgnore()) &gt;= old.epoch();
}
</code></pre>
<p>在这个例子中，对应一个8字节应用内存的一组Shadow Cell的数量为N=8，但内存访问是高频事件，因此很快Shadow Cell窗口就会写满，那么新的Shadow Cell存储在哪里呢？在这种情况下，TSan算法会随机删除一个old Shadow Cell，并将新Shadow Cell写入。这也印证了前面提到的：N值的选取会在一定程度上影响到TSan的检测精度。</p>
<p>好了，初步了解了TSan v2的检测原理后，我们再回到uber的文章，看看uber是在何时部署race检测的。</p>
<h3>三. 何时部署一个动态的Go数据竞争检测器</h3>
<p>通过前面对TSan原理的简单描述我们也可以看出，-race带来的数据竞争检测对程序运行性能和开销的影响还是蛮大的。Go官方文档<a href="https://go.dev/doc/articles/race_detector">《Data Race Detector》</a>一文中给出使用-race构建的Go程序相较于正常构建的Go程序，运行时其内存开销是后者的5-10倍，执行时间是2-20倍。但我们知道race detector只能在程序运行时才能实施数据竞争问题的检测。因此，Gopher在使用-race都会比较慎重，尤其是在生产环境中。 2013年，Dmitry Vyukov和Andrew Gerrand联合撰写的介绍Go race detector的文章<a href="https://go.dev/blog/race-detector">“introducing the go race detector”</a>中也直言：<strong>在生产环境一直开着race detector是不实际的</strong>。他们推荐两个使用race detector的时机：一个是在测试执行中开启race detector，尤其是集成测试和压力测试场景下；另外一个则是在生产环境下开启race detector，但具体操作是：仅在众多服务实例中保留一个带有race detector的服务实例，但有多少流量打到这个实例上，你自己看着办^_^。</p>
<p>那么，uber内部是怎么做的呢？前面提到过：uber内部有一个包含5000w+行代码的单一仓库，在这个仓库中有10w+的单元测试用例。uber在部署race detector的时机上遇到两个问题：</p>
<ul>
<li>由于-race探测结果的不确定性，使得针对每个pr进行race detect的效果不好。</li>
</ul>
<p>比如：某个pr存在数据竞争，但race detector执行时没有检测到；后来的没有data race的PR在执行race detect时可能会因前面的pr中的data race而被检测出问题，这就可能影响该pr的顺利合入，影响相关开发人员的效率。</p>
<p>同时，将已有的5000w+代码中的所有data race情况都找出来本身也是不可能的事情。</p>
<ul>
<li>race detector的开销会影响到SLA(我理解是uber内部的CI流水线也有时间上的SLA(给开发者的承诺)，每个PR跑race detect，可能无法按时跑完)，并且提升硬件成本</li>
</ul>
<p>针对上述这两个问题，给出的部署策略是“事后检测”，即每隔一段时间，取出一版代码仓库的快照，然后在-race开启的情况下，把所有单元测试用例跑一遍。好吧，似乎没有什么新鲜玩意。很多公司可能都是这么做的。</p>
<p>发现data race问题，就发报告给相应开发者。这块uber工程师做了一些工作，通过data race检测结果信息找出最可能引入该bug的作者，并将报告发给他。</p>
<p>不过有一个数据值得大家参考：在没有data race检测的情况下，uber内部跑完所有单元测试的时间p95位数是25分钟，而在启用data race后，这个时间增加了4倍，约为100分钟。</p>
<p>uber工程师在2021年中旬实施的上述实验，在这一实验过程中，他们找到了产生data race的主要代码模式，后续他们可能会针对这些模式制作静态代码分析工具，以更早、更有效地帮助开发人员捕捉代码中的data race问题。接下来，我们就来看看这些代码模式。</p>
<h3>四. 常见的数据竞争模式都有哪些</h3>
<p><a href="https://eng.uber.com/data-race-patterns-in-go/">uber工程师总结了7类数据竞争模式</a>，我们逐一看一下。</p>
<h4>1. 闭包的“锅”</h4>
<p><a href="https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go">Go语言原生提供了对闭包(closure)的支持</a>。在Go语言中，闭包就是<a href="https://tip.golang.org/ref/spec#Function_literals">函数字面值</a>。闭包可以引用其包裹函数(surrounding function)中定义的变量。然后，这些变量在包裹函数和函数字面值之间共享，只要它们可以被访问，这些变量就会继续存在。</p>
<p>不过不知道大家是否意识到了Go闭包对其包裹函数中的变量的捕捉方式都是通过引用的方式。而不像C++等语言那样可以选择通过值方式(by value)还是引用方式(by reference)进行捕捉。引用的捕捉方式意味着一旦闭包在一个新的goroutine中执行，那么两个goroutine对被捕捉的变量的访问就很大可能形成数据竞争。“不巧的”的是在Go中闭包常被用来作为一个goroutine的执行函数。</p>
<p>uber文章中给出了三个与这种无差别的通过引用方式对变量的捕捉方式导致的数据竞争模式的例子：</p>
<ul>
<li>例子1</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-9.png" alt="" /></p>
<p>这第一个例子中，每次循环都基于一个闭包函数创建一个新的goroutine，这些goroutine都捕捉了外面的循环变量job，这就在多个goroutine之间建立起对job的竞争态势。</p>
<ul>
<li>例子2</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-10.png" alt="" /></p>
<p>例子2中闭包与变量声明作用域的结合共同造就了新goroutine中的err变量就是外部Foo函数的返回值err。这就会造成err值成为两个goroutine竞争的“焦点”。</p>
<ul>
<li>例子3</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-11.png" alt="" /></p>
<p>例子3中，<a href="https://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values">具名返回值变量</a>result被作为新goroutine执行函数的闭包所捕获，导致了两个goroutine在result这个变量上产生数据竞争。</p>
<h4>2. 切片的“锅”</h4>
<p>切片是Go内置的复合数据类型，与传统数组相比，切片具备动态扩容的能力，并且在传递时传递的是“切片描述符”，开销小且固定，这让其在Go语言中得到了广泛的应用。但灵活的同时，切片也是Go语言中“挖坑”最多的数据类型之一，大家在使用切片时务必认真细致，稍不留神就可能犯错。</p>
<p>下面是一个在切片变量上形成数据竞争的例子：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-12.png" alt="" /></p>
<p>从这份代码来看，开发人员虽然对被捕捉的切片变量myResults通过mutex做了同步，但在后面创建新goroutine时，在传入切片时却因没有使用mutex保护。不过例子代码似乎有问题，传入的myResults似乎没有额外的使用。</p>
<h4>3. map的“锅”</h4>
<p>map是Go另外一个最常用的内置复合数据类型， 对于go入学者而言，由map导致的问题可能仅次于切片。go map并非goroutine-safe的，go禁止对map变量的并发读写。但由于是内置hash表类型，map在go编程中得到了十分广泛的应用。</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-13.png" alt="" /></p>
<p>上面例子就是一个并发读写map的例子，不过与slice不同，go在map实现中内置了对并发读写的检测，即便不加入-race，一旦发现也会抛出panic。</p>
<h4>4. 误传值惹的祸</h4>
<p>Go推荐使用传值语义，因为它简化了逃逸分析，并使变量有更好的机会被分配到栈中，从而减少GC的压力。但有些类型是不能通过传值方式传递的，比如下面例子中的sync.Mutex：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-14.png" alt="" /></p>
<p>sync.Mutex是一个零值可用的类型，我们无需做任何初始赋值即可使用Mutex实例。但Mutex类型有内部状态的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-15.png" alt="" /></p>
<p>通过传值方式会导致状态拷贝，失去了在多个goroutine间同步数据访问的作用，就像上面例子中的Mutex类型变量m那样。</p>
<h4>5. 误用消息传递(channel)与共享内存</h4>
<p>Go采用CSP的并发模型，而channel类型充当goroutine间的通信机制。虽然相对于共享内存，CSP并发模型更为高级，但从实际来看，在对CSP模型理解不到位的情况下，使用channel时也十分易错。</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-16.png" alt="" /></p>
<p>这个例子中的问题在于Start函数启动的goroutine可能阻塞在f.ch的send操作上。因为，一旦ctx cancel了，Wait就会退出，此时没有goroutine再在f.ch上阻塞读，这将导致Start函数启动的新goroutine可能阻塞在“f.ch &lt;- 1”这一行上。</p>
<p>大家也可以看到，像这样的问题很细微，如果不细致分析，很难肉眼识别出来。</p>
<h4>6. sync.WaitGroup误用导致data race问题</h4>
<p>sync.WaitGroup是Go并发程序常用的用于等待一组goroutine退出的机制。它通过Add和Done方法实现内部计数的调整。而Wait方法用于等待，直到内部计数器为0才会返回。不过像下面例子中的对WaitGroup的误用会导致data race问题：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-17.png" alt="" /></p>
<p>我们看到例子中的代码将wg.Add(1)放在了goroutine执行的函数中了，而没有像正确方法那样，将Add(1)放在goroutine创建启动之前，这就导致了对WaitGroup内部计数器形成了数据竞争，很可能因goroutine调度问题，是的Add(1)在未来得及调用，从而导致Wait提前返回。</p>
<p>下面这个例子则是由于defer函数在函数返回时的执行顺序问题，导致两个goroutine在locationErr这个变量上形成数据竞争：</p>
<p><img src="https://tonybai.com/wp-content/uploads/data-race-detection-and-pattern-in-go-18.png" alt="" /></p>
<p>main goroutine在判断locationErr是否为nil的时候，另一个goroutine中的doCleanup可能执行，也可能没有执行。</p>
<h4>7. 并行的表驱动测试可能引发数据竞争</h4>
<p>Go内置单测框架，并支持并行测试(testing.T.Parallel())。但如若使用并行测试，则极其容易导致数据竞争问题，原文没有给出例子，这个大家自行体会吧。</p>
<h3>五. 小结</h3>
<p>关于data race的代码模式，在uber发布这两篇文章之前，也有一些资料对数据竞争问题的代码模式进行了分类整理，比如下面两个资源，大家可以参照着看。</p>
<ul>
<li>《Data Race Detector》- https://go.dev/doc/articles/race_detector</li>
<li>《ThreadSanitizer Popular Data Races》- https://github.com/google/sanitizers/wiki/ThreadSanitizerPopularDataRaces中的模式</li>
</ul>
<p>在刚刚发布的<a href="https://tonybai.com/2022/06/12/go-1-19-foresight">Go 1.19beta1版本</a>中提到，最新的-race升级到了TSan v3版本，race检测性能相对于上一版将提升1.5倍-2倍，内存开销减半，并且没有对goroutine的数量的上限限制。</p>
<blockquote>
<p>注：Go要使用-race，则必须启用CGO。</p>
</blockquote>
<pre><code>// runtime/race.go

//go:nosplit
func raceinit() (gctx, pctx uintptr) {
    // cgo is required to initialize libc, which is used by race runtime
    if !iscgo {
        throw("raceinit: race build must use cgo")
    }
    ... ...
}
</code></pre>
<h3>六. 参考资料</h3>
<ul>
<li>“Finding races and memory errors with compiler instrumentation” &#8211; http://gcc.gnu.org/wiki/cauldron2012?action=AttachFile&amp;do=get&amp;target=kcc.pdf</li>
<li>《Race detection and more with ThreadSanitizer 2》 &#8211; https://lwn.net/Articles/598486/</li>
<li>《Google ThreadSanitizer &#8212; 排查多线程问题data race的大杀器》- https://zhuanlan.zhihu.com/p/139000777</li>
<li>《Introducing the Go Race Detector》- https://go.dev/blog/race-detector</li>
<li>ThreadSanitizer Algorithm V2 &#8211; https://github.com/google/sanitizers/wiki/ThreadSanitizerAlgorithm</li>
<li>paper: FastTrack: Efficient and Precise Dynamic Race Detection &#8211; https://users.soe.ucsc.edu/~cormac/papers/pldi09.pdf</li>
<li>paper: Eraser: A Dynamic Data Race Detector for Multithreaded Programs &#8211; https://homes.cs.washington.edu/~tom/pubs/eraser.pdf</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go程序员拥抱C语言简明指南</title>
		<link>https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/</link>
		<comments>https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/#comments</comments>
		<pubDate>Sun, 15 May 2022 23:11:16 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ANSI-C]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[break]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C11]]></category>
		<category><![CDATA[C18]]></category>
		<category><![CDATA[C99]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[clang-format]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[Configure]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[C标准库]]></category>
		<category><![CDATA[C语言]]></category>
		<category><![CDATA[DennisRitchie]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[fallthrough]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[iso]]></category>
		<category><![CDATA[K&R]]></category>
		<category><![CDATA[KenThompson]]></category>
		<category><![CDATA[LeetCode]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[Lint]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[loccount]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[soname]]></category>
		<category><![CDATA[switch-case]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[utf-8]]></category>
		<category><![CDATA[Windows]]></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=3535</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher 本文是为于航老师的极客时间专栏《深入C语言和程序运行原理》写的加餐文章《Tony Bai：Go程序员拥抱C语言简明指南》，这里分享给大家，尤其是那些想学习C语言的Gopher们。 你好，我是Tony Bai。 也许有同学对我比较熟悉，看过我在极客时间上的专栏《Tony Bai ·Go语言第一课》，或者是关注了我的博客。那么，作为一个Gopher，我怎么跑到这个C语言专栏做分享了呢？其实，在学习Go语言并成为一名Go程序员之前，我也曾是一名地地道道的C语言程序员。 大学毕业后，我就开始从事C语言后端服务开发工作，在电信增值领域摸爬滚打了十多年。不信的话，你可以去翻翻我的博客，数一数我发的C语言相关文章是不是比关于Go的还多。一直到近几年，我才将工作中的主力语言从C切换到了Go。不过这并不是C语言的问题，主要原因是我转换赛道了。我目前在智能网联汽车领域从事面向云原生平台的先行研发，而在云原生方面，新生代的Go语言有着更好的生态。 不过作为资深C程序员，C语言已经在我身上打下了深深的烙印。虽然Go是我现在工作中的主力语言，但我仍然会每天阅读一些C开源项目的源码，每周还会写下数百行的C代码。在一些工作场景中，特别是在我参与先行研发一些车端中间件时，C语言有着资源占用小、性能高的优势，这一点是Go目前还无法匹敌的。 正因为我有着C程序员和Go程序员的双重身份，接到这个加餐邀请时，我就想到了一个很适合聊的话题——在 Gopher（泛指Go程序员）与C语言之间“牵线搭桥”。在这门课的评论区里，我看到一些同学说，“正是因为学了Go，所以我想学好C”。如果你也对Go比较熟悉，那么恭喜你，这篇加餐简直是为你量身定制的：一个熟悉Go的程序员在学习C时需要注意的问题，还有可能会遇到的坑，我都替你总结好了。 当然，我知道还有一些对Go了解不多的同学，看到这里也别急着退出去。因为C和Go这两门语言的比较，本身就是一个很有意思的话题。今天的加餐，会涉及这两门语言的异同点，通过对C与Go语言特性的比较，你就能更好地理解“C 语言为什么设计成现在这样”。 一. C语言是现代IT工业的根基 在比较C和Go之前，先说说我推荐Gopher学C的最重要原因吧：用一句话总结，C语言在IT工业中的根基地位，是Go和其他语言目前都无法动摇的。 C语言是由美国贝尔实验室的丹尼斯·里奇（Dennis Ritchie）以Unix发明人肯·汤普森（Ken Thompson）设计的B语言为基础而创建的高级编程语言。诞生于上个世纪（精确来说是1972年）的它，到今年（2022年）已到了“知天命”的半百年纪。 年纪大、设计久远一直是“C语言过时论”兴起的根源，但如果你相信这一论断，那就大错特错了。下面，我来为你分析下个中缘由。 首先，我们说说C语言本身：C语言一直在演进，从未停下过脚步。 虽然C语言之父丹尼斯·里奇不幸于2011年永远地离开了我们，但C语言早已成为ANSI（美国国家标准学会）标准以及ISO/IEC（国际标准化组织和国际电工委员会）标准，因此其演进也早已由标准委员会负责。我们来简单回顾一下C语言标准的演进过程： 1989年，ANSI发布了首个C语言标准，被称为C89，又称ANSI C。次年，ISO和IEC把ANSI C89标准定为C语言的国际标准（ISO/IEC 9899:1990），又称C90，它也是C语言的第一个官方版本； 1999年，ISO和IEC发布了C99标准(ISO/IEC 9899:1999)，它是C语言的第二个官方版本； 2011年，ISO和IEC发布了C11标准(ISO/IEC 9899:2011)，它是C语言的第三个官方版本； 2018年，ISO和IEC发布了C18标准(ISO/IEC 9899:2018)，它是C语言的第四个官方版本。 目前，ISO/IEC标准化委员会正在致力于C2x标准的改进与制定，预计它会在2023年发布。 其次，时至今日，C语言的流行度仍然非常高。 著名编程语言排行榜TIOBE的数据显示，各大编程语言年度平均排名的总位次，C语言多年来高居第一，如下图（图片来自TIOBE）所示： 这说明，无论是在过去还是现在，C语言都是一门被广泛应用的工业级编程语言。 最后，也是最重要的一点是：C语言是现代IT工业的根基，我们说C永远不会退出IT行业舞台也不为过。 如今，无论是普通消费者端的Windows、macOS、Android、苹果iOS，还是服务器端的Linux、Unix等操作系统，亦或是各个工业嵌入式领域的操作系统，其内核实现语言都是C语言。互联网时代所使用的主流Web服务器，比如 Nginx、Apache，以及主流数据库，比如MySQL、Oracle、PostgreSQL等，也都是使用C语言开发的杰作。可以说，现代人类每天都在跟由C语言实现的系统亲密接触，并且已经离不开这些系统了。回到我们程序员的日常，Git、SVN等我们时刻在用的源码版本控制软件也都是由C语言实现的。 可以说，C语言在IT工业中的根基地位，不光Go语言替代不了，C++、Rust等系统编程语言也无法动摇，而且不仅短期如此，长期来看也是如此。 总之，C语言具有紧凑、高效、移植性好、对内存的精细控制等优秀特性，这使得我们在任何时候学习它都不会过时。不过，我在这里推荐Gopher去了解和系统学习C语言，其实还有另一个原因。我们继续往下看。 二. C与Go的相通之处：Gopher拥抱C语言的“先天优势” 众所周知，Go 是在C语言的基础上衍生而来的，二者之间有很多相通之处，因此 Gopher 在学习C语言时是有“先天优势”的。接下来，我们具体看看C和Go的相通之处有哪些。 1. 简单且语法同源 Go语言以简单著称，而作为Go先祖的C语言，入门门槛同样不高：Go有25个关键字，C有32个关键字（C89标准），简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言，这与C的简单也不无关系。 和Go不同的是，C语言是一个小内核、大外延的编程语言，其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库，我们可以快速掌握它。但需要注意的是，与Go语言“开箱即用、内容丰富”的标准库不同，C标准库非常小（在C11标准之前甚至连thread库都不包含），所以掌握“小内核”后，在LeetCode平台上刷题是没有任何问题的，但要写出某一领域的工业级生产程序，我们还有很多外延知识技能要学习，比如并发原语、操作系统的系统调用，以及进程间通信等。 C语言的这种简单很容易获得Gopher们的认同感。当年Go语言之父们在设计Go语言时，也是主要借鉴了C语言的语法。当然，这与他们深厚的C语言背景不无关系：肯·汤普森（Ken [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher">本文永久链接</a> &#8211; https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher</p>
<p>本文是为于航老师的极客时间专栏<a href="http://gk.link/a/11osT">《深入C语言和程序运行原理》</a>写的加餐文章<a href="https://time.geekbang.org/column/article/500145">《Tony Bai：Go程序员拥抱C语言简明指南》</a>，这里分享给大家，尤其是那些想学习C语言的Gopher们。</p>
<hr />
<p>你好，我是Tony Bai。</p>
<p>也许有同学对我比较熟悉，看过我在极客时间上的专栏<a href="http://gk.link/a/10AVZ">《Tony Bai ·Go语言第一课》</a>，或者是关注了<a href="https://tonybai.com">我的博客</a>。那么，作为一个Gopher，我怎么跑到这个C语言专栏做分享了呢？其实，在学习Go语言并成为一名Go程序员之前，我也曾是一名地地道道的C语言程序员。</p>
<p>大学毕业后，我就开始从事C语言后端服务开发工作，在电信增值领域摸爬滚打了十多年。不信的话，你可以去翻翻<a href="https://tonybai.com/tag/c">我的博客</a>，数一数我发的C语言相关文章是不是比关于Go的还多。一直到近几年，我才将工作中的主力语言从C切换到了Go。不过这并不是C语言的问题，主要原因是我转换赛道了。我目前在智能网联汽车领域从事面向云原生平台的先行研发，而在云原生方面，新生代的Go语言有着更好的生态。</p>
<p>不过作为资深C程序员，C语言已经在我身上打下了深深的烙印。虽然Go是我现在工作中的主力语言，但我仍然会每天阅读一些C开源项目的源码，每周还会写下数百行的C代码。在一些工作场景中，特别是在我参与先行研发一些车端中间件时，C语言有着资源占用小、性能高的优势，这一点是Go目前还无法匹敌的。</p>
<p>正因为我有着C程序员和Go程序员的双重身份，接到这个加餐邀请时，我就想到了一个很适合聊的话题——在 Gopher（泛指Go程序员）与C语言之间“牵线搭桥”。在这门课的评论区里，我看到一些同学说，“正是因为学了Go，所以我想学好C”。如果你也对Go比较熟悉，那么恭喜你，这篇加餐简直是为你量身定制的：一个熟悉Go的程序员在学习C时需要注意的问题，还有可能会遇到的坑，我都替你总结好了。</p>
<p><strong>当然，我知道还有一些对Go了解不多的同学，看到这里也别急着退出去。</strong>因为C和Go这两门语言的比较，本身就是一个很有意思的话题。今天的加餐，会涉及这两门语言的异同点，通过对C与Go语言特性的比较，你就能更好地理解“C 语言为什么设计成现在这样”。</p>
<h2>一. C语言是现代IT工业的根基</h2>
<p>在比较C和Go之前，先说说我推荐Gopher学C的最重要原因吧：用一句话总结，<strong>C语言在IT工业中的根基地位，是Go和其他语言目前都无法动摇的</strong>。</p>
<p>C语言是由美国贝尔实验室的丹尼斯·里奇（Dennis Ritchie）以Unix发明人肯·汤普森（Ken Thompson）设计的B语言为基础而创建的高级编程语言。诞生于上个世纪（精确来说是1972年）的它，到今年（2022年）已到了“知天命”的半百年纪。 年纪大、设计久远一直是“C语言过时论”兴起的根源，但如果你相信这一论断，那就大错特错了。下面，我来为你分析下个中缘由。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-3.jpeg" alt="" /></p>
<p>首先，我们说说C语言本身：<strong>C语言一直在演进，从未停下过脚步</strong>。</p>
<p>虽然C语言之父丹尼斯·里奇不幸于2011年永远地离开了我们，但C语言早已成为ANSI（美国国家标准学会）标准以及ISO/IEC（国际标准化组织和国际电工委员会）标准，因此其演进也早已由标准委员会负责。我们来简单回顾一下C语言标准的演进过程：</p>
<ul>
<li>1989年，ANSI发布了首个C语言标准，被称为C89，又称ANSI C。次年，ISO和IEC把ANSI C89标准定为C语言的国际标准（ISO/IEC 9899:1990），又称C90，它也是C语言的第一个官方版本；</li>
<li>1999年，ISO和IEC发布了<a href="https://www.iso.org/standard/29237.html">C99标准(ISO/IEC 9899:1999)</a>，它是C语言的第二个官方版本；</li>
<li>2011年，ISO和IEC发布了<a href="https://www.iso.org/standard/57853.html">C11标准(ISO/IEC 9899:2011)</a>，它是C语言的第三个官方版本；</li>
<li>2018年，ISO和IEC发布了<a href="https://www.iso.org/standard/74528.html">C18标准(ISO/IEC 9899:2018)</a>，它是C语言的第四个官方版本。<br />
目前，ISO/IEC标准化委员会正在致力于C2x标准的改进与制定，预计它会在2023年发布。</li>
</ul>
<p>其次，<strong>时至今日，C语言的流行度仍然非常高</strong>。</p>
<p>著名编程语言排行榜TIOBE的数据显示，各大编程语言年度平均排名的总位次，C语言多年来高居第一，如下图（图片来自<a href="https://www.tiobe.com/tiobe-index">TIOBE</a>）所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-2.png" alt="" /></p>
<p>这说明，无论是在过去还是现在，C语言都是一门被广泛应用的工业级编程语言。</p>
<p>最后，也是最重要的一点是：<strong>C语言是现代IT工业的根基</strong>，我们说C永远不会退出IT行业舞台也不为过。</p>
<p>如今，无论是普通消费者端的Windows、macOS、Android、苹果iOS，还是服务器端的Linux、Unix等操作系统，亦或是各个工业嵌入式领域的操作系统，其内核实现语言都是C语言。互联网时代所使用的主流Web服务器，比如 Nginx、Apache，以及主流数据库，比如MySQL、Oracle、PostgreSQL等，也都是使用C语言开发的杰作。可以说，现代人类每天都在跟由C语言实现的系统亲密接触，并且已经离不开这些系统了。回到我们程序员的日常，Git、SVN等我们时刻在用的源码版本控制软件也都是由C语言实现的。</p>
<p>可以说，C语言在IT工业中的根基地位，不光Go语言替代不了，C++、Rust等系统编程语言也无法动摇，而且不仅短期如此，长期来看也是如此。</p>
<p>总之，C语言具有紧凑、高效、移植性好、对内存的精细控制等优秀特性，这使得我们在任何时候学习它都不会过时。不过，我在这里推荐Gopher去了解和系统学习C语言，其实还有另一个原因。我们继续往下看。</p>
<h2>二. C与Go的相通之处：Gopher拥抱C语言的“先天优势”</h2>
<p>众所周知，Go 是在C语言的基础上衍生而来的，二者之间有很多相通之处，因此 Gopher 在学习C语言时是有“先天优势”的。接下来，我们具体看看C和Go的相通之处有哪些。</p>
<h3>1. 简单且语法同源</h3>
<p>Go语言以简单著称，而作为<strong>Go先祖</strong>的C语言，入门门槛同样不高：Go有25个关键字，C有32个关键字（C89标准），简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言，这与C的简单也不无关系。</p>
<p>和Go不同的是，C语言是一个<strong>小内核、大外延</strong>的编程语言，其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库，我们可以快速掌握它。但需要注意的是，与Go语言“开箱即用、内容丰富”的标准库不同，<a href="https://en.wikipedia.org/wiki/C_standard_library">C标准库</a>非常小（在C11标准之前甚至连thread库都不包含），所以掌握“小内核”后，在LeetCode平台上刷题是没有任何问题的，但要写出某一领域的工业级生产程序，我们还有很多外延知识技能要学习，比如并发原语、操作系统的系统调用，以及进程间通信等。</p>
<p>C语言的这种简单很容易获得Gopher们的认同感。当年Go语言之父们在设计Go语言时，也是主要借鉴了C语言的语法。当然，这与他们深厚的C语言背景不无关系：肯·汤普森（Ken Thompson）是Unix之父，与丹尼斯·里奇共同设计了C语言；罗博·派克（Rob Pike）是贝尔实验室的资深研究员，参与了Unix系统的演进、Plan9操作系统的开发，还是UTF-8编码的发明人；罗伯特·格瑞史莫（Robert Griesemer）也是用C语言手写Java虚拟机的大神级人物。</p>
<p>Go的第一版编译器就是由肯·汤普森（Ken Thompson）用C语言实现的。并且，Go语言的早期版本中，C代码的比例还不小。以Go语言发布的第一个版本，<a href="https://github.com/golang/go/releases/tag/go1">Go 1.0版本</a>为例，我们通过<a href="https://gitlab.com/esr/loccount">loccount工具</a>对其进行分析，会得到下面的结果：</p>
<pre><code>$loccount .
all          SLOC=460992  (100.00%) LLOC=193045  in 2746 files
Go           SLOC=256321  (55.60%)  LLOC=109763  in 1983 files
C            SLOC=148001  (32.10%)  LLOC=73458   in 368 files
HTML         SLOC=25080   (5.44%)   LLOC=0       in 57 files
asm          SLOC=10109   (2.19%)   LLOC=0       in 133 files
... ...
</code></pre>
<p>这里我们看到，在1.0版本中，C语言代码行数占据了32.10%的份额，这一份额直至Go 1.5版本实现自举后，才下降为不到1%。</p>
<p>我当初对Go“一见钟情”，其中一个主要原因就是Go与C语言的<strong>语法同源。</strong>相对应地，相信这种同源的语法也会让Gopher们喜欢上C语言。</p>
<h3>2. 静态编译且基础范式相同</h3>
<p>除了语法同源，C语言与Go语言的另一个相同点是，它们都是静态编译型语言。这意味着它们都有如下的语法特性：</p>
<ul>
<li>变量与函数都要先声明后才能使用；</li>
<li>所有分配的内存块都要有对应的类型信息，并且在确定其类型信息后才能操作；</li>
<li>源码需要先编译链接后才能运行。</li>
</ul>
<p>相似的编程逻辑与构建过程，让学习C语言的Gopher可以做到无缝衔接。</p>
<p>除此之外，Go 和C的基础编程范式都是命令式编程（imperative programming），即面向算法过程，由程序员通过编程告诉计算机应采取的动作。然后，计算机按程序指令执行一系列流程，生成特定的结果，就像菜谱指定了厨师做蛋糕时应遵循的一系列步骤一样。</p>
<p>从Go看 C，没有面向对象，没有函数式编程，没有泛型（Go 1.18已加入），满眼都是类型与函数，可以说是相当亲切了。</p>
<h3>3. 错误处理机制如出一辙</h3>
<p>对于后端编程语言来说，错误处理机制十分重要。如果两种语言的错误处理机制不同，那么这两种语言的代码整体语法风格很可能大不相同。</p>
<p>在C语言中，我们通常用一个类型为整型的函数返回值作为错误状态标识，函数调用者基于值比较的方式，对这一代表错误状态的返回值进行检视。通常，当这个返回值为0时，代表函数调用成功；当这个返回值为其他值时，代表函数调用出现错误。函数调用者需根据该返回值所代表的错误状态，来决定后续执行哪条错误处理路径上的代码。</p>
<p>C语言这种简单的<strong>基于错误值比较</strong>的错误处理机制，让每个开发人员必须显式地去关注和处理每个错误。经过显式错误处理的代码会更为健壮，也会让开发人员对这些代码更有信心。另外，这些错误就是普通的值，我们不需要额外的语言机制去处理它们，只需利用已有的语言机制，像处理其他普通类型值那样去处理错误就可以了。这让代码更容易调试，我们也更容易针对每个错误处理的决策分支进行测试覆盖。</p>
<p>C语言错误处理机制的这种简单与显式，跟Go语言的设计哲学十分契合，于是Go语言设计者决定继承这种错误处理机制。因此，当Gopher们来到C语言的世界时，无需对自己的错误处理思维做出很大的改变，就可以很容易地适应C语言的风格。</p>
<h2>三. 知己知彼，来看看C与Go的差异</h2>
<p>虽说 Gopher 学习C语言有“先天优势”，但是不经过脚踏实地的学习与实践就想掌握和精通C语言，也是不可能的。而且，C 和Go还是有很大差异的，Gopher 们只有清楚这些差异，做到“知己知彼”，才能在学习过程中分清轻重，有的放矢。俗话说，“磨刀不误砍柴功”，下面我们就一起看看C与Go有哪些不同。</p>
<h3>1. 设计哲学</h3>
<p>在人类自然语言学界，有一个很著名的假说——“<a href="https://en.wikipedia.org/wiki/Linguistic_relativity">萨丕尔-沃夫假说</a>”。这个假说的内容是这样的：<strong>语言影响或决定人类的思维方式</strong>。对我来说，<strong>编程语言也不仅仅是一门工具，它还影响着程序员的思维方式</strong>。每次开始学习一门新的编程语言时，我都会先了解这门编程语言的设计哲学。</p>
<p>每种编程语言都有自己的设计哲学，即便这门语言的设计者没有将其显式地总结出来，它也真真切切地存在，并影响着这门语言的后续演进，以及这门语言程序员的思维方式。我在<a href="http://gk.link/a/10AVZ">《Tony Bai · Go语言第一课》</a>专栏里，将Go语言的设计哲学总结成了5点，分别是<strong>简单、显式、组合、并发和面向工程</strong>。</p>
<p>那么C语言的设计哲学又是什么呢？从表面上看，简单紧凑、性能至上、极致资源、全面移植，这些都可以作为C的设计哲学，但我倾向于一种更有人文气息的说法：<strong>满足和相信程序员</strong>。</p>
<p>在这样的设计哲学下，一方面，C语言提供了几乎所有可以帮助程序员表达自己意图的语法手段，比如宏、指针与指针运算、位操作、pragma指示符、goto语句，以及跳转能力更为强大的longjmp等；另一方面，C语言对程序员的行为并没有做特别严格的限定与约束，C程序员可以利用语言提供的这些语法手段，进行天马行空的发挥：访问硬件、利用指针访问内存中的任一字节、操控任意字节中的每个位（bit）等。总之，C语言假定程序员知道他们在做什么，并选择相信程序员。</p>
<p>C语言给了程序员足够的自由，可以说，在C语言世界，你几乎可以“为所欲为”。但这种哲学也是有代价的，那就是你可能会犯一些莫名其妙的错误，比如悬挂指针，而这些错误很少或不可能在其他语言中出现。</p>
<p>这里再用一个比喻来更为形象地表达下：从Go世界到C世界，就好比在动物园中饲养已久的动物被放归到野生自然保护区，有了更多自由，但周围也暗藏着很多未曾遇到过的危险。因此，学习C语言的Gopher们要有足够的心理准备。</p>
<h3>2. 内存管理</h3>
<p>接下来我们来看C与Go在内存管理方面的不同。我把这一点放在第二位，是因为这两种语言在内存管理上有很大的差异，而且这一差异会给程序员的日常编码带来巨大影响。</p>
<p>我们知道，Go是带有垃圾回收机制（俗称GC）的静态编程语言。使用Go编程时，内存申请与释放，在栈上还是在堆上分配，以及新内存块的清零等等，这一切都是自动的，且对程序员透明。</p>
<p>但在C语言中，上面说的这些都是程序员的责任。手工内存管理在带来灵活性的同时，也带来了极大的风险，其中最常见的就是内存泄露（memory leak）与悬挂指针（dangling pointer）问题。</p>
<p>内存泄露主要指的是<strong>程序员手工在堆上分配的内存在使用后没有被释放（free），进而导致的堆内存持续增加</strong>。而悬挂指针的意思是<strong>指针指向了非法的内存地址</strong>，未初始化的指针、指针所指对象已经被释放等，都是导致悬挂指针的主要原因。针对悬挂指针进行解引用（dereference）操作将会导致运行时错误，从而导致程序异常退出的严重后果。</p>
<p>Go语言带有GC，而C语言不带GC，这都是由各自语言设计哲学所决定的。GC是不符合C语言的设计哲学的，因为一旦有了GC，程序员就远离了机器，程序员直面机器的需求就无法得到满足了。并且，一旦有了GC，无论是在性能上还是在资源占用上，都不可能做到极致了。</p>
<p>在C中，手工管理内存到底是一种什么感觉呢？作为一名有着十多年C开发经验的资深C程序员，我只能告诉你：<strong>与内存斗，其乐无穷</strong>！这是在带GC的编程语言中无法体会到的。</p>
<h3>3. 语法形式</h3>
<p>虽然C语言是Go的先祖，并且Go也继承了很多C语言的语法元素，但在变量/函数声明、行尾分号、代码块是否用括号括起、标识符作用域，以及控制语句语义等方面，二者仍有较大差异。因此，对Go已经很熟悉的程序员在初学C时，受之前编码习惯的影响，往往会踩一些“坑”。基于此，我总结了Gopher学习C语言时需要特别注意的几点，接下来我们具体看看。</p>
<p><strong>第一，注意声明变量时类型与变量名的顺序</strong></p>
<p>前面说过，Go与C都是静态编译型语言，这就要求我们在使用任何变量之前，需要先声明这个变量。但Go采用的变量声明语法颇似Pascal语言，即<strong>变量名在前，变量类型在后</strong>，这与C语言恰好相反，如下所示：</p>
<pre><code>Go:

var a, b int
var p, q *int

vs.

C：
int a, b;
int *p, *q;
</code></pre>
<p>此外，Go支持短变量声明，并且由于短变量声明更短小，无需显式提供变量类型，Go编译器会根据赋值操作符后面的初始化表达式的结果，自动为变量赋予适当类型。因此，它成为了Gopher们喜爱和重度使用的语法。但短声明在C中却不是合法的语法元素：</p>
<pre><code>int main() {
    a := 5; //  error: expected expression
    printf("a = %d\n", a);
}
</code></pre>
<p>不过，和上面的变量类型与变量名声明的顺序问题一样，C编译器会发现并告知我们这个问题，并不会给程序带来实质性的伤害。</p>
<p><strong>第二，注意函数声明无需关键字前缀</strong></p>
<p>无论是C语言还是Go语言，函数都是基本功能逻辑单元，我们也可以说<strong>C程序就是一组函数的集合</strong>。实际上，我们日常的C代码编写大多集中在实现某个函数上。</p>
<p>和变量一样，函数在两种语言中都需要先声明才能使用。Go语言使用func关键字作为<strong>函数声明的前缀</strong>，并且函数返回值列表放在函数声明的最后。但在C语言中，函数声明无需任何关键字作为前缀，函数只支持单一返回值，并且返回值类型放在函数名的前面，如下所示：</p>
<pre><code>Go：
func Add(a, b int) int {
    return a+b
}

vs.

C：
int Add(int a, int b) {
    return a+b;
}
</code></pre>
<p><strong>第三，记得加上代码行结尾的分号</strong></p>
<p>我们日常编写Go代码时，<strong>极少手写分号</strong>。这是因为，Go设计者当初为了简化代码编写，提高代码可读性，选择了<strong>由编译器在词法分析阶段自动在适当位置插入分号的技术路线</strong>。如果你是一个被Go编译器惯坏了的Gopher，来到C语言的世界后，一定不要忘记代码行尾的分号。比如上面例子中的C语言Add函数实现，在return语句后面记得要手动加上分号。</p>
<p><strong>第四，补上“省略”的括号</strong></p>
<p>同样是出于简化代码、增加可读性的考虑，Go设计者最初就取消掉了条件分支语句（if）、选择分支语句（switch）和循环控制语句（for）中条件表达式外围的小括号：</p>
<pre><code>// Go代码
func f() int {
    return 5
}
func main() {
    a := 1
    if a == 1 { // 无需小括号包裹条件表达式
        fmt.Println(a)
    }

    switch b := f(); b { // 无需小括号包裹条件表达式
    case 4:
        fmt.Println("b = 4")
    case 5:
        fmt.Println("b = 5")
    default:
        fmt.Println("b = n/a")
    }

    for i := 1; i &lt; 10; i++ { // 无需小括号包裹循环语句的循环表达式
        a += i
    }
    fmt.Println(a)
}
</code></pre>
<p>这一点恰恰与C语言“背道而驰”。因此，我们在使用C语言编写代码时，务必要想着补上这些括号：</p>
<pre><code>// C代码
int f() {
        return 5;
}

int main() {
    int a = 1;
    if (a == 1) { // 需用小括号包裹条件表达式
        printf("%d\n", a);
    }

    int b = f();
    switch (b) { // 需用小括号包裹条件表达式
    case 4:
        printf("b = 4\n");
        break;
    case 5:
        printf("b = 5\n");
        break;
    default:
        printf("b = n/a\n");
    }

    int i = 0;
    for (i = 1; i &lt; 10; i++) { // 需用小括号包裹循环语句的循环表达式
        a += i;
    }
    printf("%d\n", a);
}
</code></pre>
<p><strong>第五，留意C与Go导出符号的不同机制</strong></p>
<p>C语言通过头文件来声明对外可见的符号，所以我们不用管符号是不是首字母大写的。但在Go中，只有首字母大写的包级变量、常量、类型、函数、方法才是可导出的，即对外部包可见。反之，首字母小写的则为包私有的，仅在包内使用。Gopher一旦习惯了这样的规则，在切换到C语言时，就会产生“心理后遗症”：遇到在其他头文件中定义的首字母小写的函数时，总以为不能直接使用。</p>
<p><strong>第六，记得在switch case语句中添加break</strong></p>
<p>C 语言与Go语言在选择分支语句的语义方面有所不同：C语言的 case 语句中，如果没有显式加入break语句，那么代码将向下自动掉落执行。而Go在最初设计时就重新规定了switch case的语义，默认不自动掉落（fallthrough），除非开发者显式使用fallthrough关键字。</p>
<p>适应了Go的switch case语句的语义后再回来写C代码，就会存在潜在的“风险”。我们来看一个例子：</p>
<pre><code>// C代码：
int main() {
    int a = 1;
    switch(a) {
        case 1:printf("a = 1\n");
        case 2:printf("a = 2\n");
        case 3:printf("a = 3\n");
        default:printf("a = ?\n");
    }
}
</code></pre>
<p>这段代码是按Go语义编写的switch case，编译运行后得到的结果如下：</p>
<pre><code>a = 1
a = 2
a = 3
a = ?
</code></pre>
<p>这显然不符合我们输出“a = 1”的预期。对于初学C的Gopher而言，这个问题影响还是蛮大的，因为这样编写的代码在C编译器眼中是完全合法的，但所代表的语义却完全不是开发人员想要的。这样的程序一旦流入到生产环境，其缺陷可能会引发生产故障。</p>
<p>一些Clint 工具可以检测出这样的问题，因此对于写C代码的Gopher，我建议在提交代码前使用lint工具对代码做一下检查。</p>
<h3>4. 构建机制</h3>
<p>Go与C都是静态编译型语言，它们的源码需要经过编译器和链接器处理，这个过程称为<strong>构建(build)</strong>，构建后得到的可执行文件才是最终交付给用户的成果物。</p>
<p>和Go语言略有不同的是，C语言的构建还有一个预处理（pre-processing）阶段，预处理环节的输出才是C编译器的真正输入。C语言中的宏就是在预处理阶段展开的。不过，Go没有预处理阶段。</p>
<p>C语言的编译单元是一个C源文件（.c），每个编译单元在编译过程中会对应生成一个目标文件（.o/.obj），最后链接器将这些目标文件链接在一起，形成可执行文件。</p>
<p>而Go则是以一个包（package）为编译单元的，每个包内的源文件生成一个.o文件，一个包的所有.o文件聚合（archive）成一个.a文件，链接器将这些目标文件链接在一起形成可执行文件。</p>
<p>Go语言提供了统一的Go命令行工具链，且Go编译器原生支持增量构建，源码构建过程不需要Gopher手工做什么配置。但在C语言的世界中，用于构建C程序的工具有很多，主流的包括gcc/clang，以及微软平台的C编译器。这些编译器原生不支持增量构建，为了提升工程级构建的效率，避免每次都进行全量构建，我们通常会使用第三方的构建管理工具，比如make（Makefile）或CMake。考虑移植性时，我们还会使用到configure文件，用于在目标机器上收集和设置编译器所需的环境信息。</p>
<h3>5. 依赖管理</h3>
<p>我在前面提过，C语言仅提供了一个“小内核”。像依赖管理这类的事情，C语言本身并没有提供跟Go中的Go Module类似的，统一且相对完善的解决方案。在C语言的世界中，我们依然要靠外部工具（比如CMake）来管理第三方的依赖。</p>
<p>C语言的第三方依赖通常以静态库（.a）或动态共享库（.so）的形式存在。如果你的应用要使用静态链接，那就必须在系统中为C编译器提供第三方依赖的静态库文件。但在实际工作中，完全采用静态链接有时是会遇到麻烦的。这是因为，很多操作系统在默认安装时是不带开发包的，也就是说，像 libc、libpthread 这样的系统库只提供了动态共享库版本（如/lib下提供了libc的共享库libc.so.6），其静态库版本是需要自行下载、编译和安装的（如libc的静态库libc.a在安装后是放在/usr/lib下面的)。所以<strong>多数情况下，我们是将****静态、动态****两种链接方式混合在一起使用的</strong>，比如像libc这样的系统库多采用动态链接。</p>
<p>动态共享库通常是有版本的，并且按照一定规则安装到系统中。举个例子，一个名为libfoo的动态共享库，在安装的目录下文件集合通常是这样：</p>
<pre><code>2022-03-10 12:28 libfoo.so -&gt; libfoo.so.0.0.0*
2022-03-10 12:28 libfoo.so.0 -&gt; libfoo.so.0.0.0*
2022-03-10 12:28 libfoo.so.0.0.0*
</code></pre>
<p>按惯例，每个动态共享库都有多个名字属性，包括real name、soname和linker name。下面我们来分别看下。</p>
<ul>
<li>real name：实际包含共享库代码的那个文件的名字(如上面例子中的libfoo.so.0.0.0)。动态共享库的真实版本信息就在real name中，显然real name中的版本号符合<a href="https://semver.org/">语义版本规范</a>，即major.minor.patch。当两个版本的major号一致，说明是向后兼容的两个版本；</li>
<li>soname：shared object name的缩写，也是这三个名字中最重要的一个。无论是在编译阶段还是在运行阶段，系统链接器都是通过动态共享库的soname（如上面例子中的libfoo.so.0）来唯一识别共享库的。我们看到的soname实际上是仅包含major号的共享库名字；</li>
<li>linker name：编译阶段提供给编译器的名字（如上面例子中的libfoo.so）。如果你构建的共享库的real name跟上面例子中libfoo.so.0.0.0类似，带有版本号，那么你在编译器命令中直接使用-L path -lfoo是无法让链接器找到对应的共享库文件的，除非你为libfoo.so.0.0.0提供了一个linker name（如libfoo.so，一个指向libfoo.so.0.0.0的符号链接）。linker name一般在共享库安装时手工创建。<br />
动态共享库有了这三个名称属性，依赖管理就有了依据。但由于在链接的时候使用的是linker name，而linker name并不带有版本号，真实版本与主机环境有关，因此要实现C应用的可重现构建还是比较难。在实践中，我们通常会使用专门的构建主机，项目组将该主机上的依赖管理起来，进而保证每次构建所使用的依赖版本是可控的。同时，应用部署的目标主机上的依赖版本也应该得到管理，避免运行时出现动态共享库版本不匹配的问题。</li>
</ul>
<h3>6. 代码风格</h3>
<p>Go语言是历史上首次实现了代码风格全社区统一的编程语言。它基本上消除了开发人员在代码风格上的无休止的、始终无法达成一致的争论，以及不同代码风格带来的阅读、维护他人代码时的低效。gofmt工具格式化出来的代码风格已经成为Go开发者的一种共识，融入到Go语言的开发文化当中了。所以，如果你让某个Go开发者说说gofmt后的代码风格是什么样的，多数Go开发者可能说不出，因为代码会被gofmt自动变成那种风格，大家已经不再关心风格了。</p>
<p>而在C语言的世界，代码风格仍存争议。但经过多年的演进，以及像Go这样新兴语言的不断“教育”，C社区也在尝试进行这方面的改进，涌现出了像<a href="https://clang.llvm.org/docs/ClangFormat.html">clang-format</a>这样的工具。目前，虽然还没有在全社区达成一致的代码风格（由于历史原因，这很难做到），但已经可以减少很多不必要的争论。</p>
<p>对于正在学习C语言，并进行C编码实践的Gopher，我的建议是：<strong>不要拘泥于使用什么代码风格，先用clang-format，并确定一套风格模板就好</strong>。</p>
<h2>四. 小结</h2>
<p>作为一名对Go跟随和研究了近十年的程序员，我深刻体会到，Go的简单性、性能和生产力使它成为了创建面向用户的应用程序和服务的理想语言。快速的迭代让团队能够快速地作出反应，以满足用户不断变化的需求，让团队可以将更多精力集中在保持灵活性上。</p>
<p>但Go也有缺点，比如缺少对内存以及一些低级操作的精确控制，而C语言恰好可以弥补这个缺陷。C 语言提供的更精细的控制允许更多的精确性，使得C成为低级操作的理想语言。这些低级操作不太可能发生变化，并且C相比Go还提高了性能。所以，如果你是一个有性能与低级操作需求的 Gopher ，就有充分的理由来学习C语言。</p>
<p>C 的优势体现在最接近底层机器的地方，而Go的优势在离用户较近的地方能得到最大发挥。当然，这并不是说两者都不能在对方的空间里工作，但这样做会增加“摩擦”。当你的需求从追求灵活性转变为注重效率时，用C重写库或服务的理由就更充分了。</p>
<p>总之，虽然Go和C的设计有很大的不同，但它们也有很多相似性，具备发挥兼容优势的基础。并且，当我们同时使用这二者时，就可以既有很大的灵活性，又有很好的性能，可以说是相得益彰！</p>
<h2>五. 写在最后</h2>
<p>今天的加餐中，我主要是基于C与Go的比较来讲解的，对于Go语言的特性并没有作详细展开。如果你还想进一步了解Go语言的设计哲学、语法特性、程序设计相关知识，欢迎来学习我在极客时间上的专栏<a href="http://gk.link/a/10AVZ">《Tony Bai ·Go语言第一课》</a>。在这门课里，我会用我十年Gopher的经验，带给你一条系统、完整的Go语言入门路径。</p>
<p>感谢你看到这里，如果今天的内容让你有所收获，欢迎把它分享给你的朋友。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</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; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>理解Docker的多阶段镜像构建</title>
		<link>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/</link>
		<comments>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/#comments</comments>
		<pubDate>Sat, 11 Nov 2017 11:26:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alpine]]></category>
		<category><![CDATA[baseimage]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[CGO_ENABLED]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[DockerHub]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[httpserver]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[libc.a]]></category>
		<category><![CDATA[libc.so.6]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[multi-stage-build]]></category>
		<category><![CDATA[musl]]></category>
		<category><![CDATA[musl-libc]]></category>
		<category><![CDATA[strace]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可移植性]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2459</guid>
		<description><![CDATA[Docker技术从2013年诞生到目前已经4年有余了。对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但这是否意味着Docker的image构建机制已经相对完美了呢？不是的，Docker官方依旧在持续优化镜像构建机制。这不，从今年发布的Docker 17.05版本起，Docker开始支持容器镜像的多阶段构建(multi-stage build)了。 什么是镜像多阶段构建呢？直接给出概念定义太突兀，这里先卖个关子，我们先从日常开发中用到的镜像构建的方式和所遇到的镜像构建的问题说起。 一、同构的镜像构建 我们在做镜像构建时的一个常见的场景就是：应用在开发者自己的开发机或服务器上直接编译，编译出的二进制程序再打入镜像。这种情况一般要求编译环境与镜像所使用的base image是兼容的，比如说：我在Ubuntu 14.04上编译应用，并将应用打入基于ubuntu系列base image的镜像。这种构建我称之为“同构的镜像构建”，因为应用的编译环境与其部署运行的环境是兼容的：我在Ubuntu 14.04下编译出来的应用，可以基本无缝地在基于ubuntu:14.04及以后版本base image镜像(比如：16.04、16.10、17.10等)中运行；但在不完全兼容的base image中，比如centos中就可能会运行失败。 1、同构镜像构建举例 这里举个同构镜像构建的例子(后续的章节也是基于这个例子的)，注意：我们的编译环境为Ubuntu 16.04 x86_64虚拟机、Go 1.8.3和docker 17.09.0-ce。 我们用一个Go语言中最常见的http server作为例子： // github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/httpserver.go package main import ( "net/http" "log" "fmt" ) func home(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Welcome to this website!\n")) } func main() { http.HandleFunc("/", home) fmt.Println("Webserver start") fmt.Println(" -&#62; listen on port:1111") err := [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://tonybai.com/tag/docker">Docker</a>技术从<a href="https://www.infoq.com/news/2013/03/Docker">2013年诞生</a>到目前已经4年有余了。对于已经接纳和使用<a href="https://en.wikipedia.org/wiki/Docker_(software)">Docker技术</a>在日常开发工作中的开发者而言，构建<a href="https://docs.docker.com/get-started">Docker镜像</a>已经是家常便饭。但这是否意味着Docker的image构建机制已经相对完美了呢？不是的，Docker官方依旧在持续优化镜像构建机制。这不，从今年发布的<a href="https://github.com/moby/moby/releases/tag/v17.05.0-ce">Docker 17.05版本</a>起，Docker开始支持容器镜像的<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">多阶段构建(multi-stage build)</a>了。</p>
<p>什么是<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">镜像多阶段构建</a>呢？直接给出概念定义太突兀，这里先卖个关子，我们先从日常开发中用到的镜像构建的方式和所遇到的镜像构建的问题说起。</p>
<h2>一、同构的镜像构建</h2>
<p>我们在做镜像构建时的一个常见的场景就是：应用在开发者自己的开发机或服务器上直接编译，编译出的二进制程序再打入镜像。这种情况一般要求编译环境与镜像所使用的base image是兼容的，比如说：我在<a href="https://hub.docker.com/_/ubuntu/">Ubuntu 14.04</a>上编译应用，并将应用打入基于<a href="https://hub.docker.com/_/ubuntu/">ubuntu系列base image</a>的镜像。这种构建我称之为“同构的镜像构建”，因为应用的编译环境与其部署运行的环境是兼容的：我在Ubuntu 14.04下编译出来的应用，可以基本无缝地在基于ubuntu:14.04及以后版本base image镜像(比如：16.04、16.10、17.10等)中运行；但在不完全兼容的base image中，比如<a href="https://hub.docker.com/_/centos/">centos</a>中就可能会运行失败。</p>
<h3>1、同构镜像构建举例</h3>
<p>这里举个同构镜像构建的例子(后续的章节也是基于这个例子的)，注意：我们的编译环境为<strong>Ubuntu 16.04 x86_64虚拟机、<a href="http://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8.3</a>和docker 17.09.0-ce</strong>。</p>
<p>我们用一个Go语言中最常见的http server作为例子：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/httpserver.go
package main

import (
        "net/http"
        "log"
        "fmt"
)

func home(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Welcome to this website!\n"))
}

func main() {
        http.HandleFunc("/", home)
        fmt.Println("Webserver start")
        fmt.Println("  -&gt; listen on port:1111")
        err := http.ListenAndServe(":1111", nil)
        if err != nil {
                log.Fatal("ListenAndServe:", err)
        }
}

</code></pre>
<p>编译这个程序：</p>
<pre><code># go build -o myhttpserver httpserver.go
# ./myhttpserver
Webserver start
  -&gt; listen on port:1111
</code></pre>
<p>这个例子看起来很简单，也没几行代码，但背后Go net/http包在底层做了大量的事情，包括很多系统调用，能够反映出应用与操作系统的“耦合”，这在后续的讲解中会体现出来。接下来我们就来为这个程序构建一个docker image，并基于这个image来启动一个myhttpserver容器。我们选择ubuntu:14.04作为base image：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile
From ubuntu:14.04

COPY ./myhttpserver /root/myhttpserver
RUN chmod +x /root/myhttpserver

WORKDIR /root
ENTRYPOINT ["/root/myhttpserver"]

执行构建：

# docker build -t myrepo/myhttpserver:latest .
Sending build context to Docker daemon  5.894MB
Step 1/5 : FROM ubuntu:14.04
 ---&gt; dea1945146b9
Step 2/5 : COPY ./myhttpserver /root/myhttpserver
 ---&gt; 993e5129c081
Step 3/5 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in 104d84838ab2
 ---&gt; ebaeca006490
Removing intermediate container 104d84838ab2
Step 4/5 : WORKDIR /root
 ---&gt; 7afdc2356149
Removing intermediate container 450ccfb09ffd
Step 5/5 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in 3182766e2a68
 ---&gt; 77f315e15f14
Removing intermediate container 3182766e2a68
Successfully built 77f315e15f14
Successfully tagged myrepo/myhttpserver:latest

# docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttpserver   latest              77f315e15f14        18 seconds ago      200MB

# docker run myrepo/myhttpserver
Webserver start
  -&gt; listen on port:1111

</code></pre>
<p>以上是最基本的image build方法。</p>
<p>接下来，我们可能会遇到如下需求：<br />
* 搭建一个Go程序的构建环境有时候是很耗时的，尤其是对那些依赖很多第三方开源包的Go应用来说，下载包就需要很长时间。我们最好将这些易变的东西统统打包到一个用于Go程序构建的builder image中；<br />
* 我们看到上面我们构建出的myrepo/myhttpserver image的SIZE是200MB，这似乎有些过于“庞大”了。虽然每个主机node上的docker有cache image layer的能力，但我们还是希望能build出更加精简短小的image。</p>
<h3>2、借助golang builder image</h3>
<p>Docker Hub上提供了一个带有go dev环境的官方<a href="https://hub.docker.com/_/golang/">golang image repository</a>，我们可以直接使用这个golang builder image来辅助构建我们的应用image；对于一些对第三方包依赖较多的Go应用，我们也可以以这个golang image为base image定制我们自己的专用builder image。</p>
<p>我们基于golang:latest这个base image构建我们的golang-builder image，我们编写一个Dockerfile.build用于build golang-builder image:</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.build
FROM golang:latest

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go
</code></pre>
<p>在同目录下构建golang-builder image:</p>
<pre><code># docker build -t myrepo/golang-builder:latest -f Dockerfile.build .
Sending build context to Docker daemon  5.895MB
Step 1/4 : FROM golang:latest
 ---&gt; 1a34fad76b34
Step 2/4 : WORKDIR /go/src
 ---&gt; 2361824677d3
Removing intermediate container 01d8f4e9f0c4
Step 3/4 : COPY httpserver.go .
 ---&gt; 1ff14bb0bc56
Step 4/4 : RUN go build -o myhttpserver ./httpserver.go
 ---&gt; Running in 37a1b76b7b9e
 ---&gt; 2ac5347bb923
Removing intermediate container 37a1b76b7b9e
Successfully built 2ac5347bb923
Successfully tagged myrepo/golang-builder:latest

REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
myrepo/golang-builder   latest              2ac5347bb923        3 minutes ago       739MB
</code></pre>
<p>接下来，我们就基于golang-builder中已经build完毕的myhttpserver来构建我们最终的应用image：</p>
<pre><code># docker create --name appsource myrepo/golang-builder:latest
# docker cp appsource:/go/src/myhttpserver ./
# docker rm -f appsource
# docker rmi myrepo/golang-builder:latest
# docker build -t myrepo/myhttpserver:latest .
</code></pre>
<p>这段命令的逻辑就是从基于golang-builder image启动的容器appsource中将已经构建完毕的myhttpserver拷贝到主机当前目录中，然后删除临时的container appsource以及上面构建的那个golang-builder image；最后的步骤和第一个例子一样，基于本地目录中的已经构建完的myhttpserver构建出最终的image。为了方便，你也可以将这一系列命令放到一个Makefile中去。</p>
<h3>3、使用size更小的alpine image</h3>
<p>builder image并不能帮助我们为最终的应用image“减重”，myhttpserver image的Size依旧停留在200MB。要想“减重”，我们需要更小的base image，我们选择了<a href="https://hub.docker.com/_/alpine/">alpine</a>。<a href="https://news.ycombinator.com/item?id=10782897">Alpine image</a>的size不到4M，再加上应用的size，最终应用Image的Size估计可以缩减到20M以下。</p>
<p>结合builder image，我们只需将Dockerfile的base image改为alpine:latest：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.alpine

From alpine:latest

COPY ./myhttpserver /root/myhttpserver
RUN chmod +x /root/myhttpserver

WORKDIR /root
ENTRYPOINT ["/root/myhttpserver"]
</code></pre>
<p>构建alpine版应用image:</p>
<pre><code># docker build -t myrepo/myhttpserver-alpine:latest -f Dockerfile.alpine .
Sending build context to Docker daemon  6.151MB
Step 1/5 : FROM alpine:latest
 ---&gt; 053cde6e8953
Step 2/5 : COPY ./myhttpserver /root/myhttpserver
 ---&gt; ca0527a62d39
Step 3/5 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in 28d0a8a577b2
 ---&gt; a3833af97b5e
Removing intermediate container 28d0a8a577b2
Step 4/5 : WORKDIR /root
 ---&gt; 667345b78570
Removing intermediate container fa59883e9fdb
Step 5/5 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in adcb5b976ca3
 ---&gt; 582fa2aedc64
Removing intermediate container adcb5b976ca3
Successfully built 582fa2aedc64
Successfully tagged myrepo/myhttpserver-alpine:latest

# docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttpserver-alpine   latest              582fa2aedc64        4 minutes ago       16.3MB
</code></pre>
<p>16.3MB，Size的确降下来了！我们基于该image启动一个容器，看应用运行是否有什么问题：</p>
<pre><code># docker run myrepo/myhttpserver-alpine:latest
standard_init_linux.go:185: exec user process caused "no such file or directory"
</code></pre>
<p>容器启动失败了！为什么呢？因为alpine image并非ubuntu环境的同构image。我们在下面详细说明。</p>
<h2>二、异构的镜像构建</h2>
<p>我们的image builder: myrepo/golang-builder:latest是基于golang:latest这个image。<a href="https://github.com/docker-library/golang/">golang base image</a>有两个模板：Dockerfile-debain.template和Dockerfile-alpine.template。而golang:latest是基于debian模板的，与ubuntu兼容。构建出来的myhttpserver对<a href="http://tonybai.com/2010/12/13/also-talk-about-shared-library/">动态共享链接库</a>的情况如下：</p>
<pre><code> # ldd myhttpserver
    linux-vdso.so.1 =&gt;  (0x00007ffd0c355000)
    libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffa8b36f000)
    libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffa8afa5000)
    /lib64/ld-linux-x86-64.so.2 (0x000055605ea5d000)
</code></pre>
<p><a href="https://www.debian.org/">debian</a>系的linux distribution使用了<a href="http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/">glibc</a>。但alpine则不同，<a href="https://alpinelinux.org/">alpine</a>使用的是<a href="http://www.musl-libc.org/">musl libc</a>的实现，因此当我们运行上面的那个容器时，<a href="http://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/">加载器</a>因找不到myhttpserver依赖的libc.so.6而失败退出。</p>
<p>这种构建环境与运行环境不兼容的情况我这里称之为“异构的镜像构建”。那么如何解决这个问题呢？我们继续看：</p>
<h3>1、静态构建</h3>
<p>在主流编程语言中，<a href="http://tonybai.com/2017/06/27/an-intro-about-go-portability/">Go的移植性</a>已经是数一数二的了，尤其是<a href="http://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5</a>之后，Go将runtime中的C代码都用Go重写了，对libc的依赖已经降到最低了，但仍有一些feature提供了两个版本的实现：<a href="http://tonybai.com/tag/c">C实现</a>和Go实现。并且默认情况下，即在CGO_ENABLED=1的情况下，程序和预编译的标准库都采用了C的实现。关于这方面的详细论述请参见我之前写的<a href="http://tonybai.com/2017/06/27/an-intro-about-go-portability/">《也谈Go的可移植性》</a>一文，这里就不赘述了。于是采用了不同libc实现的debian系和alpine系自然存在不兼容的情况。要解决这个问题，我们首先考虑对Go程序进行静态构建，然后将静态构建后的Go应用放入alpine image中。</p>
<p>我们修改一下Dockerfile.build，在编译Go源文件时加上CGO_ENABLED=0：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.build

FROM golang:latest

WORKDIR /go/src
COPY httpserver.go .

RUN CGO_ENABLED=0 go build -o myhttpserver ./httpserver.go
</code></pre>
<p>构建这个builder image：</p>
<pre><code># docker build -t myrepo/golang-static-builder:latest -f Dockerfile.build .
Sending build context to Docker daemon  4.096kB
Step 1/4 : FROM golang:latest
 ---&gt; 1a34fad76b34
Step 2/4 : WORKDIR /go/src
 ---&gt; 593cd9692019
Removing intermediate container ee005d487ad5
Step 3/4 : COPY httpserver.go .
 ---&gt; a095eb69e716
Step 4/4 : RUN CGO_ENABLED=0 go build -o myhttpserver ./httpserver.go
 ---&gt; Running in d9f3b3a6c36c
 ---&gt; c06fe8dccbad
Removing intermediate container d9f3b3a6c36c
Successfully built c06fe8dccbad
Successfully tagged myrepo/golang-static-builder:latest

# docker images
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
myrepo/golang-static-builder   latest              c06fe8dccbad        31 seconds ago      739MB

</code></pre>
<p>接下来，我们再基于golang-static-builder中已经build完毕的静态连接的myhttpserver来构建我们最终的应用image：</p>
<pre><code># docker create --name appsource myrepo/golang-static-builder:latest
# docker cp appsource:/go/src/myhttpserver ./
# ldd myhttpserver
    not a dynamic executable
# docker rm -f appsource
# docker rmi myrepo/golang-static-builder:latest
# docker build -t myrepo/myhttpserver-alpine:latest -f Dockerfile.alpine .
</code></pre>
<p>运行新image:</p>
<pre><code># docker run myrepo/myhttpserver-alpine:latest
Webserver start
  -&gt; listen on port:1111
</code></pre>
<p>Note: 我们可以用strace来证明静态连接时Go只使用的是Go自己的runtime实现，而并未使用到libc.a中的代码：</p>
<pre><code># CGO_ENABLED=0 strace -f go build httpserver.go 2&gt;&amp;1 | grep open | grep -o '/.*\.a'  &gt; go-static-build-strace-file-open.txt
</code></pre>
<p>打开<a href="http://tonybai.com/wp-content/uploads/go-static-build-strace-file-open.txt">go-static-build-strace-file-open.txt</a>文件查看文件内容，你不会找到libc.a这个文件（在Ubuntu下，一般libc.a躺在/usr/lib/x86_64-linux-gnu/下面），这说明go build根本没有尝试去open libc.a文件并获取其中的符号定义。</p>
<h3>2、使用alpine golang builder</h3>
<p>我们的Go应用运行在alpine based的container中，我们可以使用alpine golang builder来构建我们的应用(无需静态链接)。前面提到过golang有alpine模板：</p>
<pre><code>REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
golang                       alpine              9e3f14138abd        7 days ago          269MB
</code></pre>
<p>alpine版golang builder的Dockerfile内容如下：</p>
<pre><code>//github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.alpine.build

FROM golang:alpine

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go

</code></pre>
<p>后续的操作与前面golang builder的操作并不二致：利用alpine golang builder构建我们的应用，并将其打入alpine image，这里就不赘述了。</p>
<h2>三、多阶段镜像构建：提升开发者体验</h2>
<p>在Docker 17.05以前，我们都是像上面那样构建镜像的。你会发现即便采用异构image builder模式，我们也要维护两个Dockerfile，并且还要在docker build命令之外执行一些诸如从容器内copy应用程序、清理build container和build image等的操作。Docker社区看到了这个问题，于是实现了<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">多阶段镜像构建机制</a>（multi-stage）。</p>
<p>我们先来看一下针对上面例子，multi-stage build所使用Dockerfile：</p>
<pre><code>//github.com/bigwhite/experiments/multi_stage_image_build/multi_stages/Dockerfile

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/myhttpserver .
RUN chmod +x /root/myhttpserver

ENTRYPOINT ["/root/myhttpserver"]
</code></pre>
<p>看完这个Dockerfile的内容，你的第一赶脚是不是把之前的两个Dockerfile合并在一块儿了，每个Dockerfile单独作为一个“阶段”！事实也是这样，但这个Docker也多了一些新的语法形式，用于建立各个“阶段”之间的联系。针对这样一个Dockerfile，我们应该知道以下几点：</p>
<ul>
<li>支持Multi-stage build的Dockerfile在以往的多个build阶段之间建立内在连接，让后一个阶段构建可以使用前一个阶段构建的产物，形成一条构建阶段的chain；</li>
<li>Multi-stages build的最终结果仅产生一个image，避免产生冗余的多个临时images或临时容器对象，这正是我们所需要的：我们只要结果。</li>
</ul>
<p>我们来使用multi-stage来build一下上述例子：</p>
<pre><code># docker build -t myrepo/myhttserver-multi-stage:latest .
Sending build context to Docker daemon  3.072kB
Step 1/9 : FROM golang:alpine as builder
 ---&gt; 9e3f14138abd
Step 2/9 : WORKDIR /go/src
 ---&gt; Using cache
 ---&gt; 7a99431d1be6
Step 3/9 : COPY httpserver.go .
 ---&gt; 43a196658e09
Step 4/9 : RUN go build -o myhttpserver ./httpserver.go
 ---&gt; Running in 9e7b46f68e88
 ---&gt; 90dc73912803
Removing intermediate container 9e7b46f68e88
Step 5/9 : FROM alpine:latest
 ---&gt; 053cde6e8953
Step 6/9 : WORKDIR /root/
 ---&gt; Using cache
 ---&gt; 30d95027ee6a
Step 7/9 : COPY --from=builder /go/src/myhttpserver .
 ---&gt; f1620b64c1ba
Step 8/9 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in e62809993a22
 ---&gt; 6be6c28f5fd6
Removing intermediate container e62809993a22
Step 9/9 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in e4000d1dde3d
 ---&gt; 639cec396c96
Removing intermediate container e4000d1dde3d
Successfully built 639cec396c96
Successfully tagged myrepo/myhttserver-multi-stage:latest

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttserver-multi-stage   latest              639cec396c96        About an hour ago   16.3MB
</code></pre>
<p>我们来Run一下这个image：</p>
<pre><code># docker run myrepo/myhttserver-multi-stage:latest
Webserver start
  -&gt; listen on port:1111
</code></pre>
<h2>四、小结</h2>
<p>多阶段镜像构建可以让开发者通过一个Dockerfile，一次性地、更容易地构建出size较小的image，体验良好并且更容易接入CI/CD等自动化系统。不过当前多阶段构建仅是在Docker 17.05及之后的版本中才能得到支持。如果想学习和实践这方面功能，但又没有环境，可以使用<a href="https://labs.play-with-docker.com/">play-with-docker</a>提供的实验环境。</p>
<p><img src="http://tonybai.com/wp-content/uploads/labs-play-with-docker.png" alt="img{512x368}" /><br />
Play with Docker labs</p>
<blockquote>
<p>以上所有示例代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/multi_stage_image_build">这里</a>下载到。</p>
</blockquote>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>Golang跨平台交叉编译</title>
		<link>https://tonybai.com/2014/10/20/cross-compilation-with-golang/</link>
		<comments>https://tonybai.com/2014/10/20/cross-compilation-with-golang/#comments</comments>
		<pubDate>Mon, 20 Oct 2014 08:40:52 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[6g]]></category>
		<category><![CDATA[6l]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[Darwin]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GOARCH]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOOS]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[链接器]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1571</guid>
		<description><![CDATA[近期在某本书上看到Go跨平台交叉编译的强大功能，于是想自己测试一下。以下记录了测试过程以及一些结论，希望能给大家带来帮助。 我的Linux环境如下： uname -a Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ go version go version go1.3.1 linux/amd64 跨平台交叉编译涉及两个重要的环境变量：GOOS和GOARCH，分别代表Target Host OS和Target Host ARCH，如果没有显式设置这些环境变量，我们通过go env可以看到go编译器眼中这两个环境变量的当前值： $ go env GOARCH=&#34;amd64&#34; GOOS=&#34;linux&#34; GOHOSTARCH=&#34;amd64&#34; GOHOSTOS=&#34;linux&#34; &#8230; &#8230; 这里还有两个变量GOHOSTOS和GOHOSTARCH，分别表示的是当前所在主机的的OS和CPU ARCH。我的Go是采用安装包安装的，因此默认情况下，这两组环境变量的值都是来自当前主机的信息。 现在我们就来交叉编译一下：在linux/amd64平台下利用Go编译器编译一个可以运行在linux/amd64下的程序，样例程序如下： //testport.go package main import ( &#160;&#160;&#160;&#160;&#160;&#160;&#160; &#34;fmt&#34; &#160;&#160;&#160;&#160;&#160;&#160;&#160; &#34;os/exec&#34; &#160;&#160;&#160;&#160;&#160;&#160;&#160; &#34;bytes&#34; [...]]]></description>
			<content:encoded><![CDATA[<p>近期在某本书上看到<a href="http://golang.org">Go</a>跨平台交叉编译的强大功能，于是想自己测试一下。以下记录了测试过程以及一些结论，希望能给大家带来帮助。</p>
<p>我的Linux环境如下：</p>
<p><font face="Courier New">uname -a<br />
	Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux</font></p>
<p><font face="Courier New">$ go version<br />
	go version go1.3.1 linux/amd64</font></p>
<p>跨平台交叉编译涉及两个重要的环境变量：GOOS和GOARCH，分别代表Target Host OS和Target Host ARCH，如果没有显式设置这些环境变量，我们通过go env可以看到go编译器眼中这两个环境变量的当前值：</p>
<p><font face="Courier New">$ go env<br />
	GOARCH=&quot;amd64&quot;<br />
	GOOS=&quot;linux&quot;</font><br />
	<font face="Courier New">GOHOSTARCH=&quot;amd64&quot;<br />
	GOHOSTOS=&quot;linux&quot;</font><br />
	&#8230; &#8230;</p>
<p>这里还有两个变量GOHOSTOS和GOHOSTARCH，分别表示的是当前所在主机的的OS和CPU ARCH。我的Go是采用安装包安装的，因此默认情况下，这两组环境变量的值都是来自当前主机的信息。</p>
<p>现在我们就来交叉编译一下：在linux/amd64平台下利用Go编译器编译一个可以运行在linux/amd64下的程序，样例程序如下：</p>
<p><font face="Courier New">//testport.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;os/exec&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;bytes&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cmd := exec.Command(&quot;uname&quot;, &quot;-a&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var out bytes.Buffer<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cmd.Stdout = &amp;out</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := cmd.Run()<br />
	&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; fmt.Println(&quot;Err when executing uname command&quot;)<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; fmt.Println(&quot;I am running on&quot;, out.String())<br />
	}</font></p>
<p>在Linux/amd64下编译运行：</p>
<p><font face="Courier New">$ go build -o testport_linux testport.go<br />
	$ testport_linux<br />
	I am running on Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux</font></p>
<p>接下来，我们来尝试在Linux/amd64上编译一个可以运行在darwin/amd64上的程序。我只需修改GOOS和GOARCH两个标识目标主机OS和ARCH的环境变量：</p>
<p><font face="Courier New">$ GOOS=darwin GOARCH=amd64 go build -o testport_darwin testport.go<br />
	go build runtime: darwin/amd64 must be bootstrapped using make.bash</font></p>
<p>编译器报错了！提示darwin/amd64必须通过make.bash重新装载。显然，通过安装包安装到linux/amd64下的Go编译器还无法直接交叉编译出darwin/amd64下可以运行的程序，我们需要做一些准备工作。我们找找make.bash在哪里！</p>
<p>我们到Go的$GOROOT路径下去找make.bash，Go的安装路径下的组织很简约，扫一眼便知make.sh大概在$GOROOT/src下，打开make.sh，我们在文件头处看到如下一些内容：</p>
<p><font face="Courier New"># Environment variables that control make.bash:<br />
	#<br />
	# GOROOT_FINAL: The expected final Go root, baked into binaries.<br />
	# The default is the location of the Go tree during the build.<br />
	#<br />
	# GOHOSTARCH: The architecture for host tools (compilers and<br />
	# binaries).&nbsp; Binaries of this type must be executable on the current<br />
	# system, so the only common reason to set this is to set<br />
	# GOHOSTARCH=386 on an amd64 machine.<br />
	#<br />
	# GOARCH: The target architecture for installed packages and tools.<br />
	#<br />
	# GOOS: The target operating system for installed packages and tools.</font><br />
	&#8230; &#8230;</p>
<p>make.bash头并未简要说明文件的用途，但名为make.xx的文件想必是用来构建Go编译工具的。这里提到几个环境变量可以控制 make.bash的行为，显然GOARCH和GOOS更能引起我们的兴趣。我们再回过头来输出testport.go编译过程的详细信息：</p>
<p><font face="Courier New">$ go build -x -o testport_linux testport.go<br />
	WORK=/tmp/go-build286732099<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>我们发现Go实际上用的是$GOROOT/pkg/tool/linux_amd64下的6g（编译器）和6l（链接器）来完成整个编译过程的，看到6g 和6l所在目录名为linux_amd64，我们可以大胆猜测编译darwin/amd64 go程序应该使用的是$GOROOT/pkg/tool/darwin_amd64下的工具。不过在我在$GOROOT/pkg/tool下没有发现 darwin_amd64目录，也就是说我们通过安装包安装的Go仅自带了for linux_amd64的编译工具，要想交叉编译出for darwin_amd64的程序，我们需要通过make.bash来手工编译出这些工具。</p>
<p><font face="Courier New">tonybai@ubuntu-Server-14:/usr/local/go/pkg$ ls<br />
	linux_amd64&nbsp; linux_amd64_race&nbsp; obj&nbsp; tool</font></p>
<p><font face="Courier New">tonybai@ubuntu-Server-14:/usr/local/go/pkg/tool$ ls<br />
	linux_amd64</font></p>
<p>根据前面make.bash的用法说明，我们来尝试构建一下：</p>
<p><font face="Courier New">cd $GOROOT/src<br />
	sudo GOOS=darwin GOARCH=amd64 ./make.bash</font></p>
<p><font face="Courier New"># Building C bootstrap tool.<br />
	cmd/dist</font></p>
<p><font face="Courier New"># Building compilers and Go bootstrap tool for host, linux/amd64.<br />
	&#8230; &#8230;<br />
	cmd/cc<br />
	cmd/gc<br />
	cmd/6l<br />
	cmd/6a<br />
	cmd/6c<br />
	cmd/6g<br />
	pkg/runtime<br />
	&#8230; &#8230;<br />
	cmd/go<br />
	pkg/runtime (darwin/amd64)</font></p>
<p><font face="Courier New"># Building packages and commands for host, linux/amd64.<br />
	runtime<br />
	&#8230; &#8230;<br />
	text/scanner</font></p>
<p><font face="Courier New"># Building packages and commands for darwin/amd64.<br />
	runtime<br />
	errors<br />
	&#8230; &#8230;<br />
	testing/quick<br />
	text/scanner</font></p>
<p><font face="Courier New">&#8212;<br />
	Installed Go for darwin/amd64 in /usr/local/go<br />
	Installed commands in /usr/local/go/bin</font></p>
<p>编译后，我们再来试试编译for darwin_amd64的程序：</p>
<p><font face="Courier New">$ GOOS=darwin GOARCH=amd64 go build -x -o testport_darwin testport.go<br />
	WORK=/tmp/go-build972764136<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_darwin -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>将文件copy到我的Mac Air下执行：</p>
<p><font face="Courier New">$chmod +x testport_darwin<br />
	$testport_darwin<br />
	I am running on Darwin TonydeMacBook-Air.local 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64</font></p>
<p>编译虽然成功了，但从-x输出的详细编译过程来看，Go编译连接使用的工具依旧是linux_amd64下的6g和6l，为什么没有使用darwin_amd64下的6g和6l呢？原来$GOROOT/pkg/tool/darwin_amd64下根本就没有6g和6l：</p>
<p><font face="Courier New">/usr/local/go/pkg/tool/darwin_amd64$ ls<br />
	addr2line&nbsp; cgo&nbsp; fix&nbsp; nm&nbsp; objdump&nbsp; pack&nbsp; yac</font>c</p>
<p>但查看一下<font face="Courier New">pkg/tool/linux_amd64/</font>下程序的更新时间：</p>
<p>/usr/local/go/pkg/tool/linux_amd64$ ls -l<br />
	<font face="Courier New">&#8230; &#8230;<br />
	-rwxr-xr-x 1 root root 2482877 10月 20 15:12 6g<br />
	-rwxr-xr-x 1 root root 1186445 10月 20 15:12 6l<br />
	&#8230; &#8230;</font></p>
<p>我们发现6g和6l都是被刚才的make.bash新编译出来的，我们可以得出结论：新6g和新6l目前既可以编译本地程序（linux/amd64)，也可以编译darwin/amd64下的程序了，例如重新编译testport_linux依旧ok：</p>
<p><font face="Courier New">$ go build -x -o testport_linux testport.go<br />
	WORK=/tmp/go-build636762567<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>如果我们还想给Go编译器加上交叉编译windows/amd64程序的功能，我们再执行一次make.bash：</p>
<p><font face="Courier New">sudo GOOS=windows GOARCH=amd64 ./make.bash</font></p>
<p>编译成功后，我们来编译一下Windows程序：</p>
<p><font face="Courier New">$ GOOS=windows GOARCH=amd64 go build -x -o testport_windows.exe testport.go<br />
	WORK=/tmp/go-build626615350<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_windows.exe -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>把testport_windows.exe扔到Windows上执行，结果：</p>
<p><font face="Courier New">Err when executing uname command</font></p>
<p>显然Windows下没有uname命令，提示执行出错。</p>
<p>至此，我的Go编译器具备了在Linux下编译windows/amd64和darwin/amd64的能力。如果你还想增加其他平台的能力，就像上面那样操作执行make.bash即可。</p>
<p>如果在go源文件中有与C语言的交互代码，那么交叉编译功能是否还能奏效呢？毕竟C在各个平台上的运行库、链接库等都是不同的。我们先来看看这个例子，我们使用之前在《<a href="http://tonybai.com/2014/10/12/discussion-on-shared-mem-support-in-docker/">探讨docker容器对共享内存的支持情况</a>》一文中的一个例子：</p>
<p><font face="Courier New">//testport_cgoenabled.go<br />
	package main</font></p>
<p><font face="Courier New">//#include &lt;stdio.h&gt;<br />
	//#include &lt;sys/types.h&gt;<br />
	//#include &lt;sys/mman.h&gt;<br />
	//#include &lt;fcntl.h&gt;<br />
	//<br />
	//#define SHMSZ&nbsp;&nbsp;&nbsp;&nbsp; 27<br />
	//<br />
	//int shm_rd()<br />
	//{<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char c;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *shm = NULL;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *s = NULL;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int fd;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ((fd = open(&quot;./shm.txt&quot;, O_RDONLY)) == -1)&nbsp; {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shm = (char*)mmap(shm, SHMSZ, PROT_READ, MAP_SHARED, fd, 0);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (!shm) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -2;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(fd);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; s = shm;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int i = 0;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (i = 0; i &lt; SHMSZ &#8211; 1; i++) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%c &quot;, *(s + i));<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;\n&quot;);<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 0;<br />
	//}<br />
	import &quot;C&quot;</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := C.shm_rd()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Mmap Share Memory Read Error:&quot;, i)<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; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Mmap Share Memory Read Ok&quot;)<br />
	}</font></p>
<p>我们先编译出一个本地可运行的程序：</p>
<p><font face="Courier New">$ go build -x -o testport_cgoenabled_linux testport_cgoenabled.go<br />
	WORK=/tmp/go-build977176241<br />
	mkdir -p $WORK/command-line-arguments/_obj/<br />
	cd /home/tonybai/Test/Go/porting<br />
	CGO_LDFLAGS=&quot;-g&quot; &quot;-O2&quot; /usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ &#8212; -I $WORK/command-line-arguments/_obj/ testport_cgoenabled.go<br />
	/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_defun.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_defun.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -print-libgcc-file-name<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_main.o -c $WORK/command-line-arguments/_obj/_cgo_main.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_export.o -c $WORK/command-line-arguments/_obj/_cgo_export.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -c $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_cgo_.o $WORK/command-line-arguments/_obj/_cgo_main.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2<br />
	/usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ -dynimport $WORK/command-line-arguments/_obj/_cgo_.o -dynout $WORK/command-line-arguments/_obj/_cgo_import.c<br />
	/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_import.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_import.c<br />
	gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_all.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2 -Wl,-r -nostdlib /usr/lib/gcc/x86_64-linux-gnu/4.8/libgcc.a<br />
	/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -D _/home/tonybai/Test/Go/porting -I $WORK -pack $WORK/command-line-arguments/_obj/_cgo_gotypes.go $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo1.go<br />
	pack r $WORK/command-line-arguments.a $WORK/command-line-arguments/_obj/_cgo_import.6 $WORK/command-line-arguments/_obj/_cgo_defun.6 $WORK/command-line-arguments/_obj/_all.o # internal<br />
	cd .<br />
	/usr/local/go/pkg/tool/linux_amd64/6l -o testport_cgoenabled_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a</font></p>
<p>输出了好多日志！不过可以看出Go编译器先调用CGO对Go源码中的C代码进行了编译，然后才是常规的Go编译，最后通过6l链接在一起。Cgo似乎直接使用了Gcc。我们再来试试跨平台编译：</p>
<p><font face="Courier New">$ GOOS=darwin GOARCH=amd64 go build -x -o testport_cgoenabled_darwin testport_cgoenabled.go<br />
	WORK=/tmp/go-build124869433<br />
	can&#39;t load package: no buildable Go source files in /home/tonybai/Test/Go/porting</font></p>
<p>当我们编译for Darwin/amd64平台的程序时，Go无法像之前那样的顺利完成编译，而是提示错误。从网上给出的资料来看，如果Go源码中包含C互操作代码，那么 目前依旧无法实现交叉编译，因为cgo会直接使用各个平台的本地c编译器去编译Go文件中的C代码。默认情况下，make.bash会置 CGO_ENABLED=0。</p>
<p>如果你非要将CGO_ENABLED设置为1去编译go的话，至少我得到了如下错误，导致无法编译通过：</p>
<p><font face="Courier New">$ sudo CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./make.bash &#8211;no-clean<br />
	&#8230; &#8230;<br />
	# Building packages and commands for darwin/amd64.<br />
	&#8230; &#8230;<br />
	37: error: &#39;AI_MASK&#39; undeclared (first use in this function)</font><br />
	&nbsp;</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/20/cross-compilation-with-golang/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>也谈C应用安装包制作与部署</title>
		<link>https://tonybai.com/2012/02/01/also-talk-about-c-app-install-package-making-and-deploying/</link>
		<comments>https://tonybai.com/2012/02/01/also-talk-about-c-app-install-package-making-and-deploying/#comments</comments>
		<pubDate>Wed, 01 Feb 2012 13:57:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Autotools]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Buildc]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C99]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Makefile]]></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>

		<guid isPermaLink="false">http://tonybai.com/2012/02/01/%e4%b9%9f%e8%b0%88c%e5%ba%94%e7%94%a8%e5%ae%89%e8%a3%85%e5%8c%85%e5%88%b6%e4%bd%9c%e4%b8%8e%e9%83%a8%e7%bd%b2/</guid>
		<description><![CDATA[虽然部门一直在做C应用<br />
，但这么多年来，在C应用的安装包制作以及部署方面做得还是很初级，可以说还没有达到规范的程度。各个产品线的C应用安装包种类多样，水平参差不齐：有些产品的源码包即是安装包，把源码包拿到生产环境下编译后使用；有的项目则将编译好的目标...]]></description>
			<content:encoded><![CDATA[<p>虽然部门一直在做<a href="http://tonybai.com/tag/C" target="_blank">C应用</a>，但这么多年来，在C应用的安装包制作以及部署方面做得还是很初级，可以说还没有达到规范的程度。各个产品线的C应用安装包种类多样，水平参差不齐：有些产品的源码包即是安装包，把源码包拿到生产环境下编译后使用；有的项目则将编译好的目标文件(.o)以及第三方库放在安装包中，在生产环境下重新链接生成可执行文件；有的组则稍微专业一些，安装包中放的是编译好的可执行文件，但在目标主机上安装和执行时也都遇到了一些问题，诸如运行环境中的第三方库版本号与程序所依赖的不一致等。</p>
<p>去年年底，我就将&quot;C应用安装包制作和部署&quot;的改进作为今年的一个工作重点。这两天我粗略地考量了一下这方面的内容，这里也简单地谈谈。</p>
<p>总的来说，摆在我们面前的有三个主要问题：<br />
	1、安装包的组织方式不规范，不统一；<br />
	2、安装包的制作方式不规范，不统一；<br />
	3、安装包的部署方法不规范，不统一；</p>
<p>好了，下面我们就来针对上述问题逐一说说改进思路（注意以下内容并非普适）。</p>
<p>一、安装包组织方式<br />
	在Linux平台上应用的标准安装包是rpm或deb，但这种安装包形式似乎不太适合我们这种C后台应用。我对rpm或deb安装包了解的不多，但印象中这类安装包的安装一般为完全安装，但我们的应用升级版本时多数为增量安装或局部替换，因此做成rpm或deb虽然看起来专业一些，但实际操作起来并不灵活，因此自定义的安装包组织方式似乎更符合我们的需求。</p>
<p>下面是一个安装包的组织结构样例：<br />
	INSTALL_PACKAGE/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; install.sh<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; README<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; app/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; foo-1.0.1*<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; env/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; conf/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; log/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; bin/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; deps/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; libs/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; bar/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; tools/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; scripts/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; deps_check.sh<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; others/</p>
<p>其中:<br />
	app目录下存放的是可执行文件;<br />
	env目录下存放的是可执行程序运行时所需要的目录结构，包括配置文件等;<br />
	deps目录下存放的是可执行程序运行时所依赖的第三方库以及一些工具;<br />
	scripts目录下存放的是安装包安装过程中所需要的辅助脚本;<br />
	others目录下可以存放无法在上述目录下存放的其他数据;<br />
	install.sh是总控安装脚本，可以用于在目标主机上安装app、完整安装运行时环境、安装依赖libs或工具，执行scripts下面的必要脚本。</p>
<p>这样的一种安装包格式比较灵活，我们可以根据需要通过install.sh安装可执行文件或某个配置文件或其他数据文件。将INSTALL_PACKAGE目录打包(.tag.gz or .zip)就得到了我们的安装包。</p>
<p>安装包命名是要符合一定规范的，也便于进行配置管理。一个典型的安装包命名规范：程序名-版本号-平台-操作系统-编译模式.tar.gz[.zip]，例如：<br />
	foo-1.8.3-x86-linux-64bit.tar.gz<br />
	bar-2.9.3-x86-solaris-32bit.tar.gz<br />
	zoo-1.3.2-sparc-solaris-64bit.zip</p>
<p>二、安装包的制作方式<br />
	以往的安装包都是直接基于项目源码库<a href="http://tonybai.com/2012/01/17/also-talk-about-building-c-app/" target="_blank">构建</a>出来的，很多运行时目录、配置文件以及辅助脚本也都与源代码存放在一起，这样一来让源码库看起来很臃肿，二来一份源码控制无法对应部署到多个不同客户现场的安装包，也就是说不同客户生产环境下的配置、数据等都是不同的，但源码库只有一个，我只能保存一份配置，因此在生成不同安装包是似乎要临时修改，且无法将这些修改做版本管理。</p>
<p>我的一个想法就是将安装包涉及到的相关文件和目录从源码库中剥离出来，针对每个项目源码，我都会建立若干个安装包工程，安装包则是这些工程(project)的产物，且可以针对不同客户做有针对性的安装包修改和版本管理。记得Microsoft的Visual Studio就有单独的安装包制作工程模板，这里也算借鉴Visual Studio中安装包工程的思想了^_^。</p>
<p>下面是一个安装包工程的示例：<br />
	foo_INSTALL_Proj/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Makefile<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; distributions/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; foo-2.9.3-x86-linux-64bit.tar.gz<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; src/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; install.sh<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; README<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; app/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; env/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; conf/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; log/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; fifo/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; bin/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; deps/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; libs/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; tools/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; scripts/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; deps_check.sh</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; others/<br />
	其中src下面的内容就是上面提到的安装包的组织，通过安装包工程我们就可以灵活控制安装包中的每一个元素，而对源码没有任何影响。</p>
<p>三、安装包的安装模式<br />
	有了前面两个问题解决作为铺垫，这个问题就很好办了。我们的应用大致有两种安装模式：本地安装和远程安装，实际上也是一回事。本地安装就是手工将安装包放在某个目标主机上，然后解压，并利用安装包中的install.sh来安装需要的文件；而远程安装多半是用远程控制工具将安装包上传到目标主机（可能是多台），并通过远程命令在远程主机上执行本地安装。</p>
<p>这里想到的一个改进就是在目标环境中部署应用前，首先执行一次部署约束检查，检查目标环境是否满足新应用部署和运行的约束条件。这在以前的部署步骤中是没有的，约束检测脚本可随安装包携带，比如放在scripts目录下。</p>
<p>总之，规范化的安装包组织形式、制作方式以及部署方式不仅是一种专业化的表现，它与一些自动化工具的结合还会促进团队或组织整体<a href="http://tonybai.com/2011/10/31/improving-efficiency-should-not-only-be-a-slogan/" target="_blank">效率的提升</a><br />
	。</p>
<p style='text-align:left'>&copy; 2012, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/02/01/also-talk-about-c-app-install-package-making-and-deploying/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>也谈C语言应用构建</title>
		<link>https://tonybai.com/2012/01/17/also-talk-about-building-c-app/</link>
		<comments>https://tonybai.com/2012/01/17/also-talk-about-building-c-app/#comments</comments>
		<pubDate>Tue, 17 Jan 2012 07:20:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Autotools]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[Buildc]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C99]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Makefile]]></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>

		<guid isPermaLink="false">http://tonybai.com/2012/01/17/%e4%b9%9f%e8%b0%88c%e8%af%ad%e8%a8%80%e5%ba%94%e7%94%a8%e6%9e%84%e5%bb%ba/</guid>
		<description><![CDATA[<p>构建是软件开发过程中最常见的活动之一，也是很容易被忽视的环节。规范以及高效的构建对软件开发过程而言是大有裨益的。C语言并非一门年轻的语言，其历史已甚为悠久了(相对于还年轻的IT领域^_^)。从C语言诞生以来，市面上存在的C语言应用何止千千万万。这些C应用的源...</p>]]></description>
			<content:encoded><![CDATA[<p>构建是软件开发过程中最常见的活动之一，也是很容易被忽视的环节。规范以及高效的构建对软件开发过程而言是大有裨益的。<a href="http://tonybai.com/tag/C" target="_blank">C语言</a>并非一门年轻的语言，其历史已甚为悠久了(相对于还年轻的IT领域^_^)。从<a href="http://tonybai.com/2011/10/17/the-state-of-c/" target="_blank">C语言</a>诞生以来，市面上存在的C语言应用何止千千万万。这些C应用的源码组织形式种类万千，从最简单的单个源文件，到复杂的诸如Apache httpd server这样庞大的Project。不过无论这些C应用的源码组织形态如何，构建都是这些应用开发过程中必不可少的一步。</p>
<p>伴随着C语言的普及，C语言应用的构建工具也逐渐发展起来，随着Project构建复杂性的增加，大致可分为四个阶段(个人观点)：<br />
	* 命令行构建<br />
	对于简单应用来说，其源文件数量一般较少，且可能都放在一个同目录下，构建这样的工程的最简单的方法就是直接在命令行上输入编译命令(诸如gcc -o foo foo.c bar.c)。这种方式在C诞生早期的简单应用或对于刚刚C入门朋友来说是最常见的。</p>
<p>* make工具<br />
	随着Project复杂程度的增加，使用命令行编译构建的难度日益加大，大家开始使用make工具。make工具的实质是帮助项目管理依赖关系。C应用构建的最终目标一般都是一个可执行文件，该文件一般是由所有源文件的目标文件以及依赖的第三方库链接后生成的，也就是说该文件依赖项目源文件的目标文件以及第三方库。我们可以将这种依赖关系用make工具指定的专用语法描述出来，形成Makefile文件。后续我们如果要构建该Project，只需敲入make即可。make工具会自动分析Makefile中的依赖关系，并执行依赖关系对应的命令，并最终完成构建。</p>
<p>* autotools<br />
	虽然make工具很好地解决了复杂Project的构建问题，但make本身的学习曲线也是很陡峭的，也就是说要为一个复杂的C应用编写Makefile脚本并非易事，特别是复杂Project中那更为复杂的依赖关系，可以让任一一个程序员望而却步。大家都看到了这一点，因此就有了<a href="http://tonybai.com/2010/09/26/hello-autoconf-and-automake/" target="_blank">autotools</a>工具集的诞生。autotools工具集由autoconf、autoheader、automake和<a href="http://tonybai.com/2010/12/14/create-libraries-with-libtool/" target="_blank">libtool</a>等工具组成，其主要目标就是简化项目Makefile的编写。使用autotools，我们可以为C应用的Project自动生成Makefile，这显然是一个很大的进步，对于复杂的Project尤甚。</p>
<p>* 新兴的通用构建工具<br />
	虽然autotools的出现解决了一些C应用构建难的问题，但autotools自身使用起来也是略显复杂的。特别是它由若干工具组成，并需要这些工具一起配合才能完成一个Project的Makefile的编写和生成，学习这些工具本身也要耗费很多时间。随着一些脚本语言的流行，一些新兴的通用构建工具逐渐出现在大家的视线中，诸如<a href="http://tonybai.com/2008/12/14/learn-scons/" target="_blank">Scons</a>、rake等。这些新工具吸取了make等门槛较高、不易用的教训，利用脚本语言特有的性质打造出了更加简单易用的构建脚本，现在很多C应用都开始使用这些工具简化构建脚本编写了。</p>
<p>究竟是使用哪种构建工具，这还是取决于项目所处的&quot;环境&quot;，包括项目的复杂性，人员的平均技能水准等等。但有了构建工具还不足矣，我们再来看看关于C语言应用构建还有哪些应该关注的地方。</p>
<p>一、规范化项目源码组织<br />
	项目的源码组织是应该先于构建脚本实现的，因此良好的项目源码组织也有助于构建脚本的编写，同时也有利于组织内部的标准化和复用。但C应用的源码组织的确没有统一的标准，也没有最好可言，也许只有适不适合。下面就是我们所使用的一个典型的C应用(非公共库)源码组织示例：</p>
<p>Foo_proj/<br />
	&nbsp;&nbsp;&nbsp; &#8211; Makefile<br />
	&nbsp;&nbsp;&nbsp; &#8211; sub_proj1/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Make.rules(由buildc生成)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Makefile<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; include/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; module1/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; xx.c<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Makefile<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; tests/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; xx_test.c<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Makefile<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; module2/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; &#8211; sub_proj2<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Make.rules(由buildc生成)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Makefile<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; include/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; module1/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; xx.c<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Makefile<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; tests/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; xx_test.c<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; Makefile<br />
	&nbsp;&#8230; &#8230;</p>
<p>针对这个示例有几个注意事项要说明一下：<br />
	a)<br />
	以前在很多Project中，都会包含一个顶层的(toplevel)Make.rules，这样的设计考虑无非是希望项目下的其他sub_proj可以复用该Make.rules，这看起来似乎方便了。但实际这样做是在各个子项目间建立了一层构建耦合关系：很多子项目都有个性化的构建需求，这样一来可能会频繁对该顶层Make.rules进行修改；或是当无法修改顶层Make.rules时子项目还是会在自己下面增加一个子Make.rules以满足构建的个性化需求。我们莫不如去掉顶层Make.rules，而在各个子项目中添加自己的Make.rules。特别是在有了<a href="http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/" target="_blank">buildc</a>工具以后，每个子项目下的Make.rules都是自动生成的，这样不但不会增加太多的额外工作量，还从根本上去除了子项目间的一种耦合，完全可满足sub_proj的个性化的构建需求。</p>
<p>b) 顶层的Makefile依旧保留，一般作为一键构建整个项目时之用。顶层的Makefile实际来看就是将各个sub_proj串接起来，再说白些，就是遍历的调用各个sub_proj下的Makefile。</p>
<p>c) 针对每个module的单元测试代码与被测试的module代码存放在一起(比如放在module下面的tests目录下)，这样使得被测对象与测试代码物理上接近，易于源码的测试，同时逻辑上看也很紧凑。</p>
<p>二、构建执行的简单和高效<br />
	构建是一个频繁的日常开发活动，简单和高效是IT开发者对&quot;构建&quot;活动的两个基本要求。所谓&quot;简单&quot;就是尽量不让或少让我动手，懒惰的程序员们最多只是希望敲入一个命令就可以完成项目的所有构建，这就是我们所说的&quot;一键化&quot;。一键化从另一个角度来说也是一种&quot;高效&quot;，但&quot;高效&quot;更重要的含义则是指尽量缩短构建的时间。要想做到这点，一是需要一个清晰明了的构建脚本实现，把项目内部的各种依赖关系打理清楚，只作必要依赖，减少不必要的重复构建；第二则是选择一款高性能的构建工具，目前来看make本身的性能还是很棒的，一般来说还是强于scons这样以动态脚本语言实现的工具的，特别是再加上并行编译和<a href="http://tonybai.com/2008/10/14/distributed-compiling-make-you-work-more-effectivly/" target="_blank">分布式编译</a>后，构建时间将大大缩短。</p>
<p>三、第三方依赖包的管理<br />
	在开源软件大行其道的今天，很多商业项目都或多或少的用到一些开源包，即使没有用到开源包，组织内部也可能存在项目间相互依赖的情况，比如：业务部门的应用很可能依赖基础研发部门提供的通用库，这样就出现了一个第三方依赖的管理的问题，这也是我们在进行构建设计过程中所不可忽视的一个重要方面。</p>
<p>关于第三方依赖包的管理，至少我是见识过如下几种方式：</p>
<p>* 将第三方依赖包的源码导入到你的项目，伴随项目一并构建<br />
	这样做的好处之一就是完整：大家在构建项目时无需东找西寻，依赖的代码就在项目库中。好处之二是便于一键构建，依赖包的源码就在项目中，可以任你&quot;宰割&quot;；第三则是便于在不同平台上移植，因为直接存储了源码，在每个平台都是依据所在的平台构建对应的版本。</p>
<p>不足之处：这样做会导致项目代码库庞大，构建时间漫长；另外也不便于第三方依赖包的更新升级。一旦第三方依赖包有bugfix或新feature，你可能需要手动的同步代码。一旦依赖的第三方包有很多的话，这可是一笔不小的工作量；最后每个项目都单独存储一份第三方依赖包会导致大量重复，重复可并不是一个好味道。</p>
<p>* 将第三方依赖包构建后的二进制文件放入项目代码库<br />
	这样做的好处在于提高了构建效率，节省了第三方依赖库自身的构建时间。但这样做的不足之处依然很多，直接存储源码方式的大多数不足都被该方式继承了下来，除此之外，这种方式还会导致在不同平台上构建难度的增加(不同平台上的包的二进制文件是不同的)。</p>
<p>* 对第三方依赖包进行集中单独管理<br />
	将各个项目所使用的第三方依赖库做统一集中管理，而不是放在每个项目中，并且只存储构建后的二进制文件而非源码。组织形式示例见下面：</p>
<p>3rds/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; libevent/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; 2.0.10/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; README<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; source_code_package<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; sparc_32_solaris/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;- include/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;- lib/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; sparc_64_solaris/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; x86_64_solaris/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; x86_64_linux/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8211; netsnmp/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;</p>
<p>这种&quot;分门别类&quot;的第三方依赖包集中管理方式既有利于加速构建过程(直接用二进制，省下源码编译)，同时也便于依赖包的统一升级和管理(专人负责，通过版本号区分)。这种第三方依赖包的管理方式也是使用buildc构建辅助工具的前提。这种方式也是有缺点的，那就是需要有专人负责对该公共库进行管理，包括新版二进制包的制作与上传。</p>
<p>至于在具体项目中究竟采用哪种方式还需要根据project的具体情况作出权衡，如果你依赖的第三方包较小且很少，那方式一很适合，<a href="http://redis.io" target="_blank">redis</a>就是这么做的；如果你不要支持多平台，那么第二种方式也可行；对于组织而言，似乎第三种方式是规范、统一和一致的，这也是我推荐的方式。</p>
<p>四、适于与第三方工具集成<br />
	<a href="http://tonybai.com/2011/05/18/set-up-ci-environment-with-buildbot/" target="_blank">持续集成</a>是公认的优秀实践，市面上有很多优秀的ci工具。持续集成的第一步就是构建，因此一个好的工程构建是应该能与ci工具很好结合在一起的，也就是说要充分考虑构建脚本与ci工具的结合。</p>
<p>一般来说持续集成工具判断成败与否的根据就是你委托ci工具执行的脚本的返回值。对于C应用构建过程来说，一般是make的返回值。0即成功，其他均为失败。对于单元测试用例的执行过程而言，也同样是此道理。C的单元测试集实际上就是一个个可执行程序，每个程序的返回值都是需要认真考量的，不能随意。如果你使用类似<a href="http://code.google.com/p/lcut" target="_blank">lcut</a>这样的框架工具，你就完全可以通过框架工具来帮你完成用例执行返回值的设定。</p>
<p>良好的项目构建设计是项目迈向成功的重要一步。在日常开发工作中我们不仅仅要关注软件开发过程中的&quot;前段&quot;，比如需求、设计和编码；对&quot;后段&quot;的一些活动，诸如构建、测试和部署也要给予足够的关注。以上所讲仅是经验之谈，谈不上绝对正确，因为关于C应用构建的资料相对较少，也没有统一的标准，这里权当抛砖引玉了。</p>
<p style='text-align:left'>&copy; 2012, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/01/17/also-talk-about-building-c-app/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>共享库中的符号链接</title>
		<link>https://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/</link>
		<comments>https://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/#comments</comments>
		<pubDate>Sun, 03 Feb 2008 06:43:46 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Compile]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[共享库]]></category>
		<category><![CDATA[堆栈]]></category>
		<category><![CDATA[构建]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[编译]]></category>

		<guid isPermaLink="false">http://tonybai.com/2008/02/03/%e5%85%b1%e4%ba%ab%e5%ba%93%e4%b8%ad%e7%9a%84%e7%ac%a6%e5%8f%b7%e9%93%be%e6%8e%a5/</guid>
		<description><![CDATA[清晨，部门新来的一位小兄弟打来求助电话，说是系统启动的时候出现类似：&#34;ld.so.1: testmain: 致命的: 重定位错误:
文件./libtestshared.so: 符号static_add:
参照的符号没有找到&#34;的错误。这个系统是05年开发的一个复用度很高的自研产品，后续项目只需在其基础上做少量二次开发工作即可满足新功能的要求。为了做
到一定的通用性，我们使用了类似插件的框架，这样系统在启动的时候会根据配置加载一些'共享库'(.so文件)，而这个小同事反映的问题就出在这。]]></description>
			<content:encoded><![CDATA[<p>清晨，部门新来的一位小兄弟打来求助电话，说是系统启动的时候出现类似：&quot;ld.so.1: testmain: 致命的: 重定位错误: 文件./libtestshared.so: 符号static_add: 参照的符号没有找到&quot;的错误。这个系统是05年开发的一个复用度很高的自研产品，后续项目只需在其基础上做少量二次开发工作即可满足新功能的要求。为了做到一定的通用性，我们使用了类似插件的框架，这样系统在启动的时候会根据配置加载一些&#039;共享库&#039;(.so文件)，而这个小同事反映的问题就出在这。</p>
<p>上面仅仅是一个引子，在写下本篇文章之前，这个问题已经被解决，我的那个小同事在连续奋战14个小时(从昨晚21:00到今天上午11:00)后，终于也可以安心踏上返回四川老家的火车了。事后，我深入的想了一下这个问题，觉得有必要说一下。</p>
<p>这里用一个简单的例子来重现一下这个问题吧。我们先来准备一个静态链接库(.a)和一个动态共享库(.so)，都比较简单，能反映出问题就行。</p>
<p>[静态库]<br />
	//teststatic.h<br />
	int static_add(int a, int b);</p>
<p>//teststatic.c<br />
	#include &quot;teststatic.h&quot;<br />
	int static_add(int a, int b) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return a+b;<br />
	}</p>
<p>编译静态库：<br />
	gcc -c teststatic.c<br />
	ar crv libteststatic.a teststatic.o</p>
<p>[动态共享库]<br />
	//testshared.h<br />
	int dynamic_add(int a, int b);</p>
<p>//testshared.c<br />
	#include &quot;testshared.h&quot;<br />
	#include &quot;teststatic.h&quot;<br />
	int dynamic_add(int a, int b) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return static_add(a, b);<br />
	}</p>
<p>编译共享库：<br />
	gcc testshared.c -fPIC -shared -o libtestshared.so</p>
<p>然后，我们再写一个测试桩程序，其主要功能就是：通过dlopen和dlsym在运行时动态加载libtestshared.so，然后得到符号dynamic_add的地址，完成计算功能。<br />
	#include <br />
	#include </p>
<p>typedef int (*PTR)(int, int);</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; void&nbsp;&nbsp;&nbsp; *handle = NULL;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char&nbsp;&nbsp;&nbsp; *errinfo = NULL;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PTR&nbsp;&nbsp;&nbsp;&nbsp; ptr;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; rv;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; handle = dlopen(&quot;./libtestshared.so&quot;, RTLD_LAZY);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (handle == NULL) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; errinfo = dlerror();<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;dlopen失败: %s\n&quot;, errinfo);<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; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ptr = (PTR)dlsym(handle, &quot;dynamic_add&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (ptr == NULL) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; errinfo = dlerror();<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;dlsym失败: %s\n&quot;, errinfo);<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; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rv = ptr(1,2);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;rv = %d\n&quot;, rv);<br />
	}<br />
	编译：gcc -o testmain testmain.c -ldl -L./ -lteststatic<br />
	运行结果：ld.so.1: testmain: 致命的: 重定位错误: 文件./libtestshared.so: 符号static_add: 参照的符号没有找到，被杀掉。</p>
<p>通过运行结果分析：程序在启动时，链接程序并没有找到符号:static_add，无从知道其指令代码，所以报错。这个例子反映的就是我那个小同事犯的&#039;错误&#039;&#8211; 程序在加载阶段链接器无法resolve共享库里调用的其他函数符号。那为什么找不到呢？我们还需简单回顾一下程序启动阶段的一些事情。</p>
<p>程序启动后，由加载器(即常说的loader)将之加载到内存中，过程很复杂和繁琐，我们就说程序中的符号是如何resolved的(我是从<u><a href="http://www.johnlevine.com/">John R.Levine</a></u>的&quot;<u><a href="http://net.gurus.com/toc-linker.html">Linkers &#038; Loaders</a></u>&quot;一书中学到的一些皮毛)。加载阶段，加载器(很多工作由链接器完成)先进行自身的初始化，之后它会根据程序文件的头(Headers)中的信息，查找程序所需要的共享库(静态库是在编译期间就已经链接到程序本身中了)的名字，对于每一个共享库的名字，它都会在搜索路径下搜索该共享库是否存在，如果存在，则处理该共享库文件，处理包括：分配text和data段空间并进行映射，其符号表将被merge到主符号表里；如果该共享库文件依然有依赖的其他共享库，且该依赖的共享库在之前并未被load，则将该依赖的共享库加入到待加载的库列表中。</p>
<p>有人要说，上面的testmain程序与这个加载过程不同啊，testmain是用dlopen和dlsym在运行时而不是加载时加载.so的，其实按照<u><a href="http://www.johnlevine.com/">John R.Levine</a></u>的说法: &quot;The two routines dlopen &#038; dlsym are actually simple wrappers that call back into the dynamic linker&quot;，也就是说：使用dlopen和dlsym的组合时，完成的事情和加载阶段链接器完成的事情是一样的。</p>
<p>那我们来看，testmain编译的时候是不依赖任何显式(C运行时和unix系统库等隐式的除外)的共享库的，那么在加载libtestshare.so时，遇到static_add这个符号时，就不知所措了。这里又有人要问了：编译testmain的时候不是链接了libteststatic.a这个库了吗，这个库里不是有static_add的符号吗？你可以nm testmain &gt; dump.log看一下，看看dump.log中是否有static_add这个符号。其实细想一下也会知道：testmain.c中根本没有使用static_add，编译器当然不会无端将static_add的放入testmain的可执行文件中了，否则在unix系统下的每个用户级程序的&#039;体格&#039;都会极其庞大。</p>
<p>上面说过，因为testmain.c中没有使用static_add，所以不能动态加载so时，不能resolve这个符号，如果testmain.c中使用了static_add，那么程序就没有问题了吧？没错！看下面：<br />
	#include &quot;teststatic.h&quot;<br />
	&#8230; &#8230;<br />
	int main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; void&nbsp;&nbsp;&nbsp; *handle = NULL;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char&nbsp;&nbsp;&nbsp; *errinfo = NULL;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PTR&nbsp;&nbsp;&nbsp;&nbsp; ptr;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; rv;<br />
	&nbsp;&nbsp; &nbsp;<br />
	&nbsp;&nbsp; &nbsp;rv = static_add(5, 6);<br />
	&nbsp;&nbsp; &nbsp;printf(&quot;rv = %d\n&quot;, rv);</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rv = ptr(1,2);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;rv = %d\n&quot;, rv);<br />
	}<br />
	这样一来，static_add就会体现在testmain的符号表里，作为testmain的一部分了。当运行时加载.so后，遇到static_add这个符号时，链接器就有据可依了。</p>
<p>又会有人问：我们不能要求所有.so中出现的符号在主程序中都要有吧？对，这样要求显然是无理的，那么如何是好呢？我们只能在编译.so时将这些符号静态链入.so，比如：gcc testshared.c -fPIC -shared -o libtestshared.so -L./ -lteststatic</p>
<p>我们可以通过nm命令看到链入静态库前后的不同：</p>
<p>未链入静态库时nm *.so，符号static_add处于UNDEF状态<br />
	[67]&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0|NOTY |GLOB |0&nbsp;&nbsp;&nbsp; |UNDEF&nbsp; |static_add<br />
	链入静态库后，nm *.so的结果：<br />
	[68]&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1412|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 36|FUNC |GLOB |0&nbsp;&nbsp;&nbsp; |10&nbsp;&nbsp;&nbsp;&nbsp; |static_add<br />
	static_add的代码被copy一份放到了.so中。</p>
<p>这里关于dlopen函数的第二个参数mode再多写两句。上面的例子中，我们传入的参数是RTLD_LAZY，什么意思呢？RTLD_LAZY是说：.so中的符号只有在其第一次使用的时候，才会由链接器计算出其实际地址，否则在.so加载时是不计算其实际地址的。原因也很简单：一个.so文件中可能有成百上千的符号，我们的程序也许只用到其中的一两个，如果加载时所有符号都要将其实际地址映射好，显然会降低运行时动态加载的性能。还是以testmain.c为例，如果代码中去掉对ptr(1,2)的调用，那么执行testmain是不会出错的。</p>
<p>dlopen中还提供了些许选项，比如：RTLD_NOW，从字面含义也可以猜测出来，其含义与RTLD_LAZY正相反，即.so加载时，其内部所有符号都要计算出实际地址。还以testmain.c为例：<br />
	handle = dlopen(&quot;./libtestshared.so&quot;, RTLD_NOW);<br />
	这时即使去掉对ptr(1,2)的调用，执行时会提示：dlopen失败: ld.so.1: testmain: 致命的: 重定位错误: 文件./libtestshared.so: 符号static_add: 参照的符号没有找到。</p>
<p>看来，共享库中的<u><a href="http://tonybai.com/2007/12/08/those-things-about-symbol-linkage/">符号链接</a></u>没有想象中的那么容易，使用的时候要&#039;小心&#039;。也许正是这些需要你投入和认真思考的问题才让使用<u><a href="http://tonybai.com/2006/03/28/c-refactoring/">C语言</a></u>进行底层或系统开发更具魅力。</p>
<p style='text-align:left'>&copy; 2008, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
