<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[成長する]]></title><description><![CDATA[终不似，少年游]]></description><link>https://blog.zmto.io</link><image><url>https://cdn.jsdelivr.net/gh/ELSTHub/PublicServiceCDN@main/blog/favicon.ico</url><title>成長する</title><link>https://blog.zmto.io</link></image><generator>Yohaku (https://github.com/Innei/Yohaku)</generator><lastBuildDate>Sat, 15 Aug 2026 07:14:12 GMT</lastBuildDate><atom:link href="https://blog.zmto.io/feed" rel="self" type="application/rss+xml"/><pubDate>Sat, 15 Aug 2026 07:14:12 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[Go JSON 序列化踩坑：为什么 MarshalJSON 用指针接收者就失效了？]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/go-json-marshal-receiver-pitfall">https://blog.zmto.io/posts/programming/go-json-marshal-receiver-pitfall</a></blockquote><div><h2 id="-go--encodingjson--2006-01-02--timetime--rfc3339-">起因：一段看似合理的代码学习 Go 的 <code>encoding/json</code> 时，我想自定义一个日期类型，让它序列化时只输出 <code>2006-01-02</code> 这种日期格式，而不是 <code>time.Time</code> 默认的 RFC3339 全时间戳。于是写了下面这段：</h2><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;encoding/json&quot;
    &quot;fmt&quot;
    &quot;time&quot;
)

type JSONDate time.Time

func (d JSONDate) MarshalJSON() ([]byte, error) {
    s := fmt.Sprintf(&quot;\&quot;%s\&quot;&quot;, time.Time(d).Format(time.DateOnly))
    return []byte(s), nil
}

func (d *JSONDate) UnmarshalJSON(data []byte) error {
    t, err := time.ParseInLocation(time.DateOnly, string(data), time.Local)
    if err != nil {
        return err
    }
    *d = JSONDate(t)
    return nil
}

type Person struct {
    Name     string
    Age      int     `json:&quot;age&quot;`
    Height   float64
    Birth    JSONDate
}

func main() {
    p := Person{
        Name:   &quot;alice&quot;,
        Age:    25,
        Height: 1.68,
        Birth:  JSONDate(time.Now()),
    }

    bytes, err := json.Marshal(p)
    if err != nil {
        return
    }

    fmt.Println(string(bytes))

    var dst Person
    err = json.Unmarshal(bytes, &amp;dst)
    if err != nil {
        return
    }

    fmt.Printf(&quot;dst: %#v\n&quot;, dst)
}
</code></pre>
<p>运行结果符合预期，<code>Birth</code> 字段被序列化成了 <code>&quot;2026-08-13&quot;</code> 这种纯日期字符串。</p><p>但当我把 <code>MarshalJSON</code> 改成指针接收者后，事情就不对了：</p><pre class="language-go lang-go"><code class="language-go lang-go">func (d *JSONDate) MarshalJSON() ([]byte, error) {
    s := fmt.Sprintf(&quot;\&quot;%s\&quot;&quot;, time.Time(*d).Format(time.DateOnly))
    return []byte(s), nil
}
</code></pre>
<p>输出变成了：</p><pre class="language-json lang-json"><code class="language-json lang-json">{&quot;Name&quot;:&quot;alice&quot;,&quot;age&quot;:25,&quot;Height&quot;:1.68,&quot;Birth&quot;:{}}
</code></pre>
<p><code>Birth</code> 字段变成了空对象 <code>{}</code>，我的 <code>MarshalJSON</code> 像是完全没被调用。这篇文章记录我排查这个问题的全过程，顺带把 Go 的方法集规则和 <code>encoding/json</code> 的接口识别机制讲清楚。</p><hr/><h2 id="encodingjson-">一、encoding/json 如何识别自定义序列化方法</h2><p>Go 标准库 <code>encoding/json</code> 定义了两个接口：</p><pre class="language-go lang-go"><code class="language-go lang-go">// encoding/json/marshal.go
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}
</code></pre>
<p><code>json.Marshal</code> 在序列化每个值时，会通过反射检查这个值的类型是否实现了 <code>Marshaler</code> 接口：</p><ul><li><strong>实现了</strong> → 优先调用 <code>MarshalJSON()</code>，把返回的 <code>[]byte</code> 原样拼进 JSON 输出</li><li><strong>没实现</strong> → 走默认反射逻辑（struct 按 field 逐个序列化、基础类型直接转字符串等）</li></ul><p><code>json.Unmarshal</code> 同理，检查 <code>Unmarshaler</code> 接口。</p><p>关键点：<strong>两个接口各管一头，不会交叉</strong>。只实现 <code>MarshalJSON</code> 不实现 <code>UnmarshalJSON</code>，反序列化时就会走默认逻辑；反之亦然。</p><p>另外，<code>Marshaler</code> 接口的优先级高于 <code>encoding.TextMarshaler</code> 和默认反射逻辑，一旦实现就一定会被调用。</p><hr/><h2 id="">二、为什么改用指针接收者就失效了</h2><p>把 <code>MarshalJSON</code> 从值接收者改成指针接收者后：</p><pre class="language-go lang-go"><code class="language-go lang-go">func (d *JSONDate) MarshalJSON() ([]byte, error)  // 指针接收者
</code></pre>
<p><code>json.Marshal(p)</code> 的执行链路变成了这样：</p><ol start="1"><li><code>p</code> 是 <code>Person</code> <strong>值</strong>，<code>p.Birth</code> 是 <code>JSONDate</code> <strong>值</strong>（非指针）</li><li><code>json.Marshal</code> 反射拿到的是这个<strong>值</strong>，检查的是 <code>JSONDate</code> 类型的方法集</li><li>指针接收者的方法只挂在 <code>*JSONDate</code> 上，<strong>不在</strong> <code>JSONDate</code> 的方法集里</li><li>判定 <code>JSONDate</code> 不实现 <code>Marshaler</code> → 跳过自定义方法</li><li>走默认 struct 反射逻辑</li></ol><p>那为什么输出是 <code>{}</code> 而不是 RFC3339 时间？因为 <code>JSONDate</code> 是 <code>time.Time</code> 的命名类型，而 Go 的类型定义（<code>type A B</code>）<strong>不继承原类型的方法集</strong>。<code>time.Time</code> 自带的 <code>MarshalJSON</code> 不会被 <code>JSONDate</code> 继承，所以 json 包对 <code>JSONDate</code> 走纯 struct 反射——而 <code>time.Time</code> 内部字段 <code>wall</code>/<code>ext</code>/<code>loc</code> 都是私有字段，反射不可见 → 输出 <code>{}</code>。</p><hr/><h2 id="go-">三、Go 方法集规则：问题的根因</h2><p>这是 Go 里最经典也最容易踩的坑。规则只有一句话：</p><blockquote><p><strong><code>*T</code> 的方法集 ⊇ <code>T</code> 的方法集</strong></p></blockquote>
<p>展开说就是：</p><ul><li>类型 <code>T</code> 的方法集：所有接收者为 <code>T</code> 的方法</li><li>类型 <code>*T</code> 的方法集：所有接收者为 <code>T</code> 或 <code>*T</code> 的方法</li></ul><p>用表格更直观：</p><table><thead><tr><th> 接收者写法 </th><th> <code>T</code> 值的方法集 </th><th> <code>*T</code> 指针的方法集 </th></tr></thead><tbody><tr><td> <code>func (t T) M()</code> </td><td> ✅ 有 M </td><td> ✅ 有 M </td></tr><tr><td> <code>func (t *T) M()</code> </td><td> ❌ 没有 M </td><td> ✅ 有 M </td></tr></tbody></table><p>换句话说：</p><ul><li><strong>指针变量能访问值方法</strong> ✅</li><li><strong>值变量不能访问指针方法</strong> ❌</li></ul><p>所以回到原问题：<code>p.Birth</code> 是 <code>JSONDate</code> 值，它的方法集里没有指针接收者实现的 <code>MarshalJSON</code>，json 包判定它不满足 <code>Marshaler</code> 接口，自然就跳过了。</p><hr/><h2 id="--">四、方法调用 ≠ 接口满足</h2><p>这里有个更隐蔽的误区。看下面这段代码：</p><pre class="language-go lang-go"><code class="language-go lang-go">d := JSONDate{}
d.MarshalJSON()         // ✅ 能编译通过！编译器自动取址 (&amp;d).MarshalJSON()
var m Marshaler = d     // ❌ 编译失败！接口检查不自动取址
var m Marshaler = &amp;d    // ✅ 通过
</code></pre>
<p>第一行 <code>d.MarshalJSON()</code> 能编译，是因为 <code>d</code> 是可寻址的，编译器帮你做了 <code>(&amp;d).MarshalJSON()</code>。这会让人产生错觉：&quot;值也能调用指针方法啊&quot;。</p><p>但接口满足的判断<strong>严格按类型的方法集来，不会自动取址</strong>。<code>JSONDate</code> 类型的方法集不含 <code>MarshalJSON</code>，所以 <code>var m Marshaler = d</code> 编译失败。</p><p>而 <code>encoding/json</code> 内部用的是 <code>reflect.Type.Implements(marshalerType)</code>，纯类型层面的检查，跟你 value 可不可寻址完全无关。这就是为什么直接调用方法能成功，但 json 序列化却识别不到——它们走的是两条完全不同的路径。</p><hr/><h2 id="-unmarshaljson-">五、为什么 UnmarshalJSON 用指针接收者却没问题</h2><p>原代码里 <code>UnmarshalJSON</code> 用的是指针接收者，但反序列化工作正常：</p><pre class="language-go lang-go"><code class="language-go lang-go">func (d *JSONDate) UnmarshalJSON(data []byte) error  // 指针接收者
</code></pre>
<p>原因在于调用方式不同：</p><pre class="language-go lang-go"><code class="language-go lang-go">var dst Person
json.Unmarshal(bytes, &amp;dst)  // 传的是 &amp;dst 指针
</code></pre>
<p><code>json.Unmarshal</code> 接收的是 <code>&amp;dst</code>，通过这个指针可以寻址到 <code>dst.Birth</code> 字段。检查接口时看的是 <code>*JSONDate</code> 的方法集——<code>*JSONDate</code> 同时包含值方法和指针方法，所以 <code>UnmarshalJSON</code> 能被识别并调用。</p><p>更重要的是，<strong>反序列化必须修改 <code>*d</code></strong>，用值接收者根本写不回去。所以 <code>UnmarshalJSON</code> 用指针接收者既是必须的，也是能工作的。</p><hr/><h2 id="">六、最佳实践</h2><p>经过这次踩坑，总结出 Go 自定义 JSON 序列化的标准写法：</p><pre class="language-go lang-go"><code class="language-go lang-go">type JSONDate time.Time

// 序列化：用值接收者
// 原因：json.Marshal 检查的是值类型的方法集，值接收者让值和指针都满足接口
func (d JSONDate) MarshalJSON() ([]byte, error) {
    s := fmt.Sprintf(&quot;\&quot;%s\&quot;&quot;, time.Time(d).Format(time.DateOnly))
    return []byte(s), nil
}

// 反序列化：用指针接收者
// 原因1：必须修改 *d，值接收者写不回去
// 原因2：json.Unmarshal 传的是指针，检查 *T 方法集，能识别指针接收者方法
func (d *JSONDate) UnmarshalJSON(data []byte) error {
    t, err := time.ParseInLocation(time.DateOnly, string(data), time.Local)
    if err != nil {
        return err
    }
    *d = JSONDate(t)
    return nil
}
</code></pre>
<p>一句话原则：</p><blockquote><p><strong><code>MarshalJSON</code> 用值接收者，<code>UnmarshalJSON</code> 用指针接收者。</strong></p></blockquote>
<p>这不是随便写的，是 Go 方法集规则和 <code>encoding/json</code> 接口检查机制共同决定的必然结果。</p><hr/><h2 id="">七、验证：几种写法的对照实验</h2><p>为了彻底确认理解，做了几组对照实验：</p><h3 id="-1marshaljson---">实验 1：MarshalJSON 用指针接收者 + 字段是值类型</h3><pre class="language-go lang-go"><code class="language-go lang-go">type Person struct {
    Birth JSONDate  // 值类型
}
func (d *JSONDate) MarshalJSON() ([]byte, error) { ... }
</code></pre>
<p>结果：<code>Birth</code> 输出 <code>{}</code>，方法未被调用。❌</p><h3 id="-2marshaljson---">实验 2：MarshalJSON 用指针接收者 + 字段是指针类型</h3><pre class="language-go lang-go"><code class="language-go lang-go">type Person struct {
    Birth *JSONDate  // 指针类型
}
func (d *JSONDate) MarshalJSON() ([]byte, error) { ... }
</code></pre>
<p>结果：<code>Birth</code> 输出正确的日期字符串。✅</p><p>因为现在字段本身就是 <code>*JSONDate</code>，json 包检查 <code>*JSONDate</code> 的方法集，能找到 <code>MarshalJSON</code>。</p><h3 id="-3marshaljson---">实验 3：MarshalJSON 用值接收者 + 字段是值类型</h3><pre class="language-go lang-go"><code class="language-go lang-go">type Person struct {
    Birth JSONDate  // 值类型
}
func (d JSONDate) MarshalJSON() ([]byte, error) { ... }
</code></pre>
<p>结果：<code>Birth</code> 输出正确的日期字符串。✅</p><p>这是最省心的写法，值和指针字段都能识别。</p><h3 id="-4jsonmarshal-">实验 4：json.Marshal 传指针</h3><pre class="language-go lang-go"><code class="language-go lang-go">json.Marshal(&amp;p)  // 传 Person 指针
</code></pre>
<p>这个<strong>不能</strong>让指针接收者的 <code>MarshalJSON</code> 生效。因为 json 包检查的是<strong>字段类型</strong>的方法集，<code>Birth</code> 字段类型还是 <code>JSONDate</code>（值类型），而不是 <code>*JSONDate</code>。传 <code>&amp;p</code> 只影响 <code>p</code> 本身可寻址，不影响字段类型的接口判断。</p><hr/><h2 id="">八、常见误区汇总</h2><table><thead><tr><th> 误区 </th><th> 真相 </th></tr></thead><tbody><tr><td> &quot;值和指针都能调用对方的方法&quot; </td><td> 只有指针能调用值方法，值不能调用指针方法（接口层面） </td></tr><tr><td> &quot;类型定义 <code>type A B</code> 会继承 B 的方法&quot; </td><td> 不会。<code>type A B</code> 是新类型，方法集为空；<code>type A = B</code>（别名）才共享方法集 </td></tr><tr><td> &quot;传 <code>&amp;p</code> 能让指针接收者的 MarshalJSON 生效&quot; </td><td> 不能。json 检查的是字段类型的方法集，跟传不传指针无关 </td></tr><tr><td> &quot;MarshalJSON 不实现就会报错&quot; </td><td> 不会报错，走默认逻辑（struct 反射），私有字段不可见就输出 <code>{}</code> </td></tr><tr><td> &quot;方法调用能编译通过 = 接口就满足&quot; </td><td> 不等于。方法调用编译器会自动取址，接口判断不会 </td></tr></tbody></table><hr/><h2 id="timetime-">九、延伸：time.Time 是怎么被序列化的</h2><p><code>time.Time</code> 能直接被 json 序列化成 RFC3339 字符串，正是因为它自己实现了 <code>MarshalJSON</code>：</p><pre class="language-go lang-go"><code class="language-go lang-go">// time/time.go
func (t Time) MarshalJSON() ([]byte, error) { ... }  // 值接收者
</code></pre>
<p>注意它用的也是<strong>值接收者</strong>。这就是为什么 <code>time.Time</code> 字段无论声明成值还是指针，都能正确序列化。</p><p>而我们的 <code>JSONDate</code> 是 <code>type JSONDate time.Time</code> 定义的新类型，<strong>不继承</strong> <code>time.Time</code> 的 <code>MarshalJSON</code>，方法集是空的，必须自己实现。这就是为什么需要手写 <code>MarshalJSON</code> 的根本原因。</p><hr/><h2 id="">总结</h2><p>这次踩坑把几个 Go 核心概念串了起来：</p><ol start="1"><li><strong><code>encoding/json</code> 通过 <code>reflect.Type.Implements()</code> 检查接口</strong>，纯类型层面，与 value 是否可寻址无关</li><li><strong>Go 方法集规则</strong>：<code>*T</code> 方法集 ⊇ <code>T</code> 方法集，指针能调值方法，值不能调指针方法</li><li><strong>方法调用 ≠ 接口满足</strong>：编译器对方法调用会自动取址，接口判断不会</li><li><strong>类型定义不继承方法集</strong>：<code>type A B</code> 是新类型，<code>type A = B</code> 才是别名</li><li><strong>标准写法</strong>：<code>MarshalJSON</code> 用值接收者，<code>UnmarshalJSON</code> 用指针接收者</li></ol><p>记住一句话就够了：</p><blockquote><p><strong>序列化看值方法集，反序列化看指针方法集。</strong></p></blockquote>
<p>理解了这条规则，Go 里大量跟接口、反射相关的&quot;为什么没被调用&quot;的问题都能自己解释了。</p><hr/><h2 id="">参考</h2><ul><li><a href="https://pkg.go.dev/encoding/json">encoding/json 包文档</a></li><li><a href="https://go.dev/ref/spec#Method_sets">Go 语言规范：方法集</a></li><li><a href="https://go.dev/ref/spec#Type_definitions">Go 语言规范：类型定义</a></li></ul></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/go-json-marshal-receiver-pitfall#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/go-json-marshal-receiver-pitfall</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/go-json-marshal-receiver-pitfall</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Thu, 13 Aug 2026 13:41:32 GMT</pubDate></item><item><title><![CDATA[Go 中 ele.(type) 是什么语法？类型 switch 与类型断言]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/go-type-switch-type-assertion">https://blog.zmto.io/posts/programming/go-type-switch-type-assertion</a></blockquote><div><p><code>ele.(type)</code> 是 Go 的 <strong>类型 switch</strong> 专用语法，用于判断接口值在运行时保存的具体类型。</p><pre class="language-go lang-go"><code class="language-go lang-go">switch v := ele.(type) {
case string:
    fmt.Println(&quot;字符串：&quot;, v)
case int:
    fmt.Println(&quot;整数：&quot;, v)
default:
    fmt.Printf(&quot;其他类型：%T\n&quot;, v)
}
</code></pre>
<h2 id="-any">接口值与 <code>any</code></h2><p>接口值可以理解为同时保存动态类型和动态值：</p><pre class="language-go lang-go"><code class="language-go lang-go">var ele any = &quot;hello&quot;
</code></pre>
<p>此时 <code>ele</code> 的静态类型是 <code>any</code>，动态类型是 <code>string</code>。<code>any</code> 是 <code>interface{}</code> 的别名，两者完全等价。</p><h2 id="eletype-"><code>ele.(type)</code> 的限制</h2><p><code>.(type)</code> 只能出现在类型 switch 中，不能作为普通表达式单独使用：</p><pre class="language-go lang-go"><code class="language-go lang-go">// 错误：t := ele.(type)

switch v := ele.(type) {
case string:
    fmt.Println(v)
}
</code></pre>
<p>如果只想查看类型，可以使用：</p><pre class="language-go lang-go"><code class="language-go lang-go">fmt.Printf(&quot;%T\n&quot;, ele)
</code></pre>
<h2 id="">类型断言</h2><p>类型断言用于从接口值中取出指定类型：</p><pre class="language-go lang-go"><code class="language-go lang-go">value, ok := ele.(string)
if ok {
    fmt.Println(value)
}
</code></pre>
<p><code>ok == false</code> 表示类型不匹配，不会触发 panic。省略 <code>ok</code> 时，断言失败会 panic：</p><pre class="language-go lang-go"><code class="language-go lang-go">value := ele.(string)
</code></pre>
<p>可以这样区分两者：</p><ul><li>类型断言：只判断一种指定类型。</li><li>类型 switch：根据多种可能类型分别处理。</li></ul><h2 id="-switch-">类型 switch 的注意事项</h2><h3 id="-case">多个类型共用一个 <code>case</code></h3><pre class="language-go lang-go"><code class="language-go lang-go">switch ele.(type) {
case int, int64, float64:
    fmt.Println(&quot;数字&quot;)
}
</code></pre>
<p>一个 <code>case</code> 匹配多个类型时，分支变量不能直接按某一种具体类型使用；需要分别处理时应拆成多个 <code>case</code>。</p><h3 id="">值类型和指针类型不同</h3><pre class="language-go lang-go"><code class="language-go lang-go">type User struct{ Name string }

switch v := ele.(type) {
case User:
    fmt.Println(v.Name)
case *User:
    fmt.Println(v.Name)
}
</code></pre>
<p><code>User</code> 和 <code>*User</code> 是两个不同的类型，类型 switch 不会自动互相匹配。</p><h3 id="">自定义类型和底层类型不同</h3><pre class="language-go lang-go"><code class="language-go lang-go">type UserID int

var ele any = UserID(1)
switch ele.(type) {
case UserID: // 匹配
case int:    // 不匹配
}
</code></pre>
<p>虽然 <code>UserID</code> 的底层类型是 <code>int</code>，但它仍然是独立类型。</p><h3 id="nil--nil-"><code>nil</code> 接口与 nil 指针</h3><pre class="language-go lang-go"><code class="language-go lang-go">var p *User = nil
var ele any = p

fmt.Println(ele == nil) // false
</code></pre>
<p>接口中保存了动态类型 <code>*User</code>，即使动态值为 nil，接口本身也不等于 nil。类型 switch 可能匹配到 <code>*User</code>，需要额外判断 <code>v == nil</code>。</p><h2 id="">接口类型断言</h2><p>类型断言也可以判断动态类型是否实现了某个接口：</p><pre class="language-go lang-go"><code class="language-go lang-go">type Stringer interface {
    String() string
}

value, ok := ele.(Stringer)
if ok {
    fmt.Println(value.String())
}
</code></pre>
<p>判断依据是动态类型是否实现目标接口，而不是变量声明时的静态类型名称。</p><h2 id="">适用场景</h2><p>类型 switch 适合运行时才知道具体类型的场景，例如：</p><ul><li>处理 <code>map[string]any</code> 或 JSON 数据。</li><li>分发不同类型的事件或消息。</li><li>在边界函数中兼容多种输入类型。</li></ul><p>使用 <code>encoding/json</code> 将 JSON 解码到 <code>any</code> 时，数字通常会被解析为 <code>float64</code>，而不是 <code>int</code>。</p><p>如果类型在编译期已经确定，优先使用具体类型或泛型：</p><pre class="language-go lang-go"><code class="language-go lang-go">func Max[T int | float64](a, b T) T {
    if a &gt; b {
        return a
    }
    return b
}
</code></pre>
<h2 id="-switch">类型 switch、类型断言与泛型的选择</h2><table><thead><tr><th> 场景 </th><th> 推荐方式 </th></tr></thead><tbody><tr><td> 判断一种接口动态类型 </td><td> <code>value, ok := ele.(Type)</code> </td></tr><tr><td> 根据多种动态类型处理 </td><td> <code>switch value := ele.(type)</code> </td></tr><tr><td> 编译期已知类型集合 </td><td> 泛型 </td></tr><tr><td> 只查看动态类型名称 </td><td> <code>fmt.Printf(&quot;%T&quot;, ele)</code> </td></tr><tr><td> 数据结构固定 </td><td> 具体 struct 或接口 </td></tr></tbody></table><h2 id="">总结</h2><ol start="1"><li><code>ele.(type)</code> 只能用于类型 switch。</li><li><code>ele.(Type)</code> 是类型断言，带 <code>ok</code> 更安全。</li><li>值类型、指针类型、自定义类型和底层类型会严格区分。</li><li>接口中的 nil 指针不等于 nil 接口。</li><li>运行时处理多种类型用类型 switch；编译期复用算法优先考虑泛型。</li></ol></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/go-type-switch-type-assertion#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/go-type-switch-type-assertion</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/go-type-switch-type-assertion</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Thu, 06 Aug 2026 14:00:29 GMT</pubDate></item><item><title><![CDATA[Go 中 `type T U` 与 `type T = U` 的区别]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/go-type-definition-vs-type-alias">https://blog.zmto.io/posts/programming/go-type-definition-vs-type-alias</a></blockquote><div><p>在 Go 中，下面两种写法看起来相似，但含义完全不同：</p><pre class="language-go lang-go"><code class="language-go lang-go">type UserID int
type Score = int
</code></pre>
<ul><li><code>type UserID int</code>：定义一个全新的类型</li><li><code>type Score = int</code>：为 <code>int</code> 创建一个类型别名</li></ul><h2 id="-type-t-u-">一、使用 <code>type T U</code> 定义新类型</h2><pre class="language-go lang-go"><code class="language-go lang-go">type UserID int
</code></pre>
<p>这会创建一个名为 <code>UserID</code> 的新类型。虽然它的底层类型是 <code>int</code>，但 <code>UserID</code> 与 <code>int</code> 并不是同一种类型。</p><pre class="language-go lang-go"><code class="language-go lang-go">var id UserID = 100
var number int = 200

number = id       // 编译错误
number = int(id)  // 正确，需要显式转换
</code></pre>
<p>新类型之间也不能随意混用：</p><pre class="language-go lang-go"><code class="language-go lang-go">type UserID int
type OrderID int

func findUser(id UserID)  {}
func findOrder(id OrderID) {}

var userID UserID
var orderID OrderID

findUser(userID)   // 正确
findUser(orderID)  // 编译错误
</code></pre>
<p>即使 <code>UserID</code> 和 <code>OrderID</code> 的底层类型都是 <code>int</code>，Go 也会把它们视为两个不同的类型。</p><h2 id="-type-t--u-">二、使用 <code>type T = U</code> 定义类型别名</h2><pre class="language-go lang-go"><code class="language-go lang-go">type Score = int
</code></pre>
<p>这不会创建新类型，<code>Score</code> 只是 <code>int</code> 的另一个名字。</p><pre class="language-go lang-go"><code class="language-go lang-go">var score Score = 100
var number int = 200

number = score // 正确
score = number // 正确
</code></pre>
<p>类型别名常用于给复杂类型提供更短的名字、保持旧代码兼容，或在包迁移时保留原有类型身份：</p><pre class="language-go lang-go"><code class="language-go lang-go">type StringList = []string
</code></pre>
<p>这里的 <code>StringList</code> 仍然就是 <code>[]string</code>，不会产生一个新的类型。</p><h2 id="">三、新类型可以定义方法</h2><p>使用新类型时，可以为它定义专属方法：</p><pre class="language-go lang-go"><code class="language-go lang-go">type UserID int

func (id UserID) Valid() bool {
    return id &gt; 0
}
</code></pre>
<p>但类型别名不会创建独立类型，因此不能借此为 <code>int</code> 增加方法：</p><pre class="language-go lang-go"><code class="language-go lang-go">type Score = int

// 编译错误：Score 只是 int 的别名
func (s Score) Valid() bool {
    return s &gt;= 0
}
</code></pre>
<h2 id="">四、两种写法的对比</h2><table><thead><tr><th> 对比项 </th><th> <code>type UserID int</code> </th><th> <code>type Score = int</code> </th></tr></thead><tbody><tr><td> 是否创建新类型 </td><td> 是 </td><td> 否 </td></tr><tr><td> 是否与原类型完全相同 </td><td> 否 </td><td> 是 </td></tr><tr><td> 是否可以直接赋值 </td><td> 通常不可以 </td><td> 可以 </td></tr><tr><td> 是否需要显式转换 </td><td> 通常需要 </td><td> 不需要 </td></tr><tr><td> 是否可以定义方法 </td><td> 可以 </td><td> 不能创建独立方法 </td></tr><tr><td> 主要用途 </td><td> 表达业务语义、限制误用 </td><td> 重命名、兼容、简化类型 </td></tr></tbody></table><h2 id="">五、实际开发中如何选择？</h2><p>如果类型代表不同的业务概念，建议使用新类型：</p><pre class="language-go lang-go"><code class="language-go lang-go">type UserID int64
type ProductID int64
type Price int64
</code></pre>
<p>这样可以避免把用户 ID、商品 ID 和价格混在一起。</p><p>如果只是想给已有类型换一个名字，并且希望它与原类型完全兼容，可以使用类型别名：</p><pre class="language-go lang-go"><code class="language-go lang-go">type HeaderMap = map[string]string
</code></pre>
<p>简单来说：</p><pre class="language-go lang-go"><code class="language-go lang-go">type UserID int  // 创建新类型
type UserID = int // 创建类型别名
</code></pre>
<p>需要“区分身份”时使用新类型，需要“换个名字”时使用类型别名。</p></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/go-type-definition-vs-type-alias#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/go-type-definition-vs-type-alias</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/go-type-definition-vs-type-alias</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Thu, 06 Aug 2026 13:54:15 GMT</pubDate></item><item><title><![CDATA[Go sync.WaitGroup 详解与避坑指南]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/go-sync-waitgroup-guide">https://blog.zmto.io/posts/programming/go-sync-waitgroup-guide</a></blockquote><div><blockquote><p>从一段「漏掉最后一个结果」的 WorkerPool 代码说起,把 WaitGroup 讲透。</p></blockquote>
<h2 id="waitgroup-">一、WaitGroup 是什么</h2><p><code>sync.WaitGroup</code> 是 Go 标准库提供的并发同步原语,用来<strong>等待一组 goroutine 全部执行完毕</strong>。本质就是一个并发安全的计数器:</p><ul><li><code>Add(n int)</code>:计数器加 n(可为负)</li><li><code>Done()</code>:计数器减 1,等价于 <code>Add(-1)</code></li><li><code>Wait()</code>:阻塞,直到计数器归零</li></ul><p>你可以把它理解成「门卫」:<code>Add</code> 是发通行证,<code>Done</code> 是回收通行证,<code>Wait</code> 是「等所有通行证都收回来再放行主流程」。</p><h2 id="">二、标准使用模板</h2><pre class="language-go lang-go"><code class="language-go lang-go">var wg sync.WaitGroup

for i := 0; i &lt; n; i++ {
    wg.Add(1)            // ① 启动 goroutine 之前 Add
    go func(i int) {
        defer wg.Done()  // ② goroutine 内 defer Done
        // 干活
    }(i)
}
wg.Wait()               // ③ 外部等待
</code></pre>
<p>记住三步:<strong>启动前 Add、协程内 Done、外部 Wait。顺序不能乱。</strong></p><h2 id="-panic-">三、五条铁律(违反即 panic 或死锁)</h2><h3 id="-1add--go-">铁律 1:Add 必须在 <code>go</code> 之前调用</h3><pre class="language-go lang-go"><code class="language-go lang-go">// ❌ 错误:在 goroutine 内 Add
go func() {
    wg.Add(1)   // 主协程的 Wait 可能在此时已经看到 0 并返回
    defer wg.Done()
}()
</code></pre>
<p><code>Wait</code> 只判断计数器是否归零。如果在 goroutine 里才 Add,主协程的 <code>Wait</code> 完全可能在 Add 执行之前就看到 0 并解除阻塞——<strong>goroutine 还没跑完主流程就走了</strong>。这是最常见的并发 bug,而且只在调度时序不巧时才触发,极难复现。</p><h3 id="-2add--wait-">铁律 2:Add 不能和 Wait 并发</h3><pre class="language-go lang-go"><code class="language-go lang-go">// ❌ 错误:一边 Wait 一边 Add
go func() {
    for {
        wg.Add(1)  // Wait 进行中再 Add → panic
        ...
    }
}()
wg.Wait()
</code></pre>
<p>一旦开始 <code>Wait</code>,就只允许计数递减(<code>Done</code>),<strong>不允许再 <code>Add</code></strong>。违者触发:</p><pre class=""><code class="">sync: WaitGroup is reused before previous Wait has returned
</code></pre>
<p>这条铁律是初学者最容易踩的雷。下面会专门展开。</p><h3 id="-3">铁律 3:计数不能变负</h3><p><code>Done</code> 次数绝不能超过 <code>Add</code> 次数,否则:</p><pre class=""><code class="">sync: negative WaitGroup counter
</code></pre>
<h3 id="-4-delta">铁律 4:批量启动用 Delta</h3><pre class="language-go lang-go"><code class="language-go lang-go">// ✅ 推荐
wg.Add(n)
for i := 0; i &lt; n; i++ {
    go work()
}

// 也可以,但每次 Add 都要抢锁
for i := 0; i &lt; n; i++ {
    wg.Add(1)
    go work()
}
</code></pre>
<p><code>Add(n)</code> 一次性加 n,省去循环里逐次抢锁的开销。<strong>注意:<code>Add(n)</code> 同样必须在 <code>go</code> 之前调用,n 要预先计算好。</strong></p><h3 id="-5-waitgroup">铁律 5:不要拷贝 WaitGroup</h3><p><code>WaitGroup</code> 含 <code>noCopy</code> 字段,<strong>按值传递会拷贝出一份独立计数器</strong>,同步立刻失效。要么用指针,要么作为结构体字段按引用持有:</p><pre class="language-go lang-go"><code class="language-go lang-go">// ❌ 错误
func bad(wg sync.WaitGroup) { ... }

// ✅ 正确
func good(wg *sync.WaitGroup) { ... }
</code></pre>
<h2 id="-workerpool-4">四、真实案例:为什么这段 WorkerPool 漏掉了「结果4」</h2><h3 id="">原始问题代码</h3><pre class="language-go lang-go"><code class="language-go lang-go">func (p *WorkerPool) CollectResults() (results []string) {
    go func() {                                    // ① 启动收集 goroutine
        for val := range p.resultChan {
            results = append(results, val)
        }
    }()

    p.wg.Wait()        // ② 等 worker 全部 Done
    close(p.resultChan) // ③ 关通道
    return               // ④ 立即返回 results
}
</code></pre>
<h3 id="">两次踩雷的演进</h3><h4 id="">第一次尝试(错误改法)</h4><p>有人想「让 wg 计数对齐任务数」就能解决,改成这样:</p><pre class="language-go lang-go"><code class="language-go lang-go">func (p *WorkerPool) worker(id int) {
    for val := range p.jobChan {
        p.wg.Add(1)           // ❌ 在 goroutine 内 Add
        // ...
        p.resultChan &lt;- val
    }
}

func (p *WorkerPool) CollectResults() (results []string) {
    go func() {
        for val := range p.resultChan {
            results = append(results, val)
            p.wg.Done()        // ❌ 与 Wait 并发的 Add
        }
    }()
    p.wg.Wait()
    close(p.resultChan)
    return
}
</code></pre>
<p>这个改法<strong>会直接 panic</strong>:</p><ul><li><code>Add</code> 放在 worker 的 for 循环里,与 <code>CollectResults</code> 中的 <code>Wait</code> 并发执行,触发铁律 2 的 panic。</li><li>退一步说,即便不 panic,「wg 计数 == 任务数」也<strong>不是正确性的来源</strong>——原 bug 是「worker 完成」和「收集协程完成」是两件事,用一个 wg 混淆了语义。</li></ul><h4 id="">根因剖析</h4><p>原 <code>wg</code> 计数的是「worker 是否全部产出完毕」(生产侧),但 <code>CollectResults</code> 真正需要等的是「收集协程把缓冲区读完并退出」(消费侧)。<strong>生产 ≠ 消费</strong>,两个独立的 happens-before 链,用一个 wg 串起来就会漏:</p><ol start="1"><li><code>wg.Wait()</code> 返回只表示所有 worker 调了 <code>Done()</code>,但 worker 在 <code>resultChan &lt;- val</code> 之后立刻 <code>Done()</code>,值可能还躺在带缓冲的 <code>resultChan</code>(容量 3)里没被消费。</li><li><code>close(resultChan)</code> 之后 <code>for range</code> 会读完缓冲区剩余值<strong>才退出</strong>,但 <code>return</code> 可能在收集协程跑完前就执行了——<strong>最后一个 append 丢失</strong>,于是「结果4」没了。这是因为 4 是最后写入缓冲区的那一个,主协程 return 时它还没被取出。</li></ol><h2 id="-waitgroup-">五、正确写法:两个 WaitGroup 各管一件事</h2><pre class="language-go lang-go"><code class="language-go lang-go">type WorkerPool struct {
    workerCount int
    jobChan     chan string
    resultChan  chan string
    workerWg    sync.WaitGroup  // 计 worker 是否全部产出完毕
    collectWg   sync.WaitGroup  // 计收集协程是否读完缓冲区
}

func (p *WorkerPool) start() {
    for i := 0; i &lt; p.workerCount; i++ {
        p.workerWg.Add(1)        // ① 启动前 Add
        go p.worker(i)
    }
}

func (p *WorkerPool) worker(id int) {
    defer p.workerWg.Done()       // ② goroutine 内 Done
    for val := range p.jobChan {
        t := time.Duration(rand.Intn(6)) * time.Second
        time.Sleep(t)
        p.resultChan &lt;- val
    }
}

func (p *WorkerPool) CollectResults() (results []string) {
    p.collectWg.Add(1)             // ① 启动前 Add
    go func() {
        defer p.collectWg.Done()    // ② goroutine 内 Done
        for val := range p.resultChan {
            results = append(results, val)
        }
    }()

    p.workerWg.Wait()             // 等所有 worker 产出完毕 → 安全 close
    close(p.resultChan)
    p.collectWg.Wait()            // 等收集协程读完 → 安全 return
    return
}
</code></pre>
<p><strong>两个 wg 各司其职:</strong></p><table><thead><tr><th> WaitGroup </th><th> 计的是什么 </th><th> 决定什么 </th></tr></thead><tbody><tr><td> <code>workerWg</code> </td><td> worker 是否全部产出完毕 </td><td> 何时 <code>close(resultChan)</code> </td></tr><tr><td> <code>collectWg</code> </td><td> 收集协程是否读完缓冲区退出 </td><td> 何时安全 <code>return</code> </td></tr></tbody></table><p>记住这句话:<strong>「关 diver 等消费者完成」才是关键,不是「wg 计数对齐任务数」。</strong> 生产侧的 Wait 决定何时关 chan,消费侧的 Wait 决定何时关完之后还能安全 return。</p><h2 id="-waitgroup-">六、只用一个 WaitGroup 怎么写</h2><p>如果你坚持只用一个 <code>wg</code>,必须引入<strong>一个额外的无缓冲通道</strong>顶替第二个 <code>wg</code> 的同步角色:</p><pre class="language-go lang-go"><code class="language-go lang-go">type WorkerPool struct {
    workerCount int
    jobChan     chan string
    resultChan  chan string
    wg          sync.WaitGroup
    done        chan struct{}   // 代替第二个 wg:收集协程完成信号
}

func (p *WorkerPool) start() {
    for i := 0; i &lt; p.workerCount; i++ {
        p.wg.Add(1)
        go p.worker(i)
    }
}

func (p *WorkerPool) worker(id int) {
    defer p.wg.Done()
    for val := range p.jobChan {
        t := time.Duration(rand.Intn(6)) * time.Second
        fmt.Printf(&quot;%d 等待时间: %s\n&quot;, id, t)
        time.Sleep(t)
        fmt.Printf(&quot;当前Worker %d 拿到了任务: %v\n&quot;, id, val)
        p.resultChan &lt;- val
    }
}

func (p *WorkerPool) CollectResults() (results []string) {
    done := make(chan struct{})
    p.done = done

    go func() {
        for val := range p.resultChan {
            results = append(results, val)
        }
        close(done)           // 收集协程读完缓冲区退出 → 发完成信号
    }()

    p.wg.Wait()               // 等 worker 全部产出完
    close(p.resultChan)       // 安全关 resultChan
    &lt;-done                    // 等收集协程退出 → 安全 return
    return
}
</code></pre>
<p><code>done</code> 这个无缓冲通道,功能上就是第二个 WaitGroup 的等价物——它干的事和 <code>collectWg.Wait()</code> 一模一样:把主协程挡在 return 前,直到收集协程把缓冲区读完并发来信号。「一个 chan」和「一个 WaitGroup」互为替代是 Go 里的常见手法,选哪个看可读性。</p><p><strong>所以「单 wg 方案」其实是把第二个 wg 换成了 chan 凑数,语义反而更绕。</strong> 原架构里 <code>workerWg + collectWg</code> 双 wg 才是量身之选。</p><h3 id="-worker--closeresultchan">为什么不能让 worker 自己 close(resultChan)</h3><p>有人想「让最后一个退出的 worker 关 resultChan」,这条路必然 panic:</p><ul><li>有 3 个 worker,A worker 的 range 退出<strong>不代表</strong> B、C 也退出了。</li><li>A 贸然 <code>close(resultChan)</code>,会和 B、C 还在写的情形冲突 → <code>panic: send on closed channel</code>。</li><li>要安全 close,只能等<strong>所有 worker</strong> 都退出——这天然就需要一个专门等 worker 的 wg。</li></ul><p><strong>worker 之间是平行的,没有一个可靠的「最后一个」概念。</strong></p><h2 id="happens-before-collectwgwait--append-">七、happens-before:为什么 <code>collectWg.Wait()</code> 能保证 append 可见</h2><p><code>sync.WaitGroup</code> 的关键不只是一个计数器,而是它建立的 <strong>happens-before 关系</strong>:</p><blockquote><p>在 <code>Wait</code> 返回之前,所有 <code>Done</code> 调用对内存的写入,对 <code>Wait</code> 之后的代码可见。</p></blockquote>
<p>应用到本例:</p><ol start="1"><li>收集协程 <code>append(results, val)</code> → 后续 <code>defer collectWg.Done()</code>。</li><li>主协程 <code>collectWg.Wait()</code> 解除阻塞 → 紧接着 <code>return results</code>。</li></ol><p>由于 <code>Done → Wait</code> 的 happens-before,收集协程对 <code>results</code> 的 append 写,对主协程 return 时的读<strong>一定可见</strong>。这就是为什么两个 wg 方案没有 data race 的语义保证。</p><h2 id="waitgroup-vs-channel">八、WaitGroup vs Channel:何时用哪个</h2><table><thead><tr><th> 场景 </th><th> 推荐 </th></tr></thead><tbody><tr><td> 等 N 个 goroutine 都完成 </td><td> WaitGroup </td></tr><tr><td> 等待一个事件发生 </td><td> 无缓冲 chan(<code>done := make(chan struct{})</code>) </td></tr><tr><td> 在 goroutine 间传递值 </td><td> 带缓冲/无缓冲 chan </td></tr><tr><td> 需要取消/超时 </td><td> <code>context</code> + chan </td></tr><tr><td> 需要收集首批错误 </td><td> <code>errgroup.Group</code> </td></tr></tbody></table><p>经验法则:<strong>「等 N 个完成」用 wg,「等 1 个事件」用 chan。</strong> 本例里 <code>collectWg</code> 等的是 1 个收集协程,所以 chan 也能胜任——这正是单 wg 方案可行的依据。</p><h2 id="">九、常见误用速查表</h2><table><thead><tr><th> 误用 </th><th> 后果 </th><th> 正确 </th></tr></thead><tbody><tr><td> goroutine 内 Add </td><td> Wait 可能在 Add 前返回 </td><td> 启动前 Add </td></tr><tr><td> Add 与 Wait 并发 </td><td> panic: reused before previous Wait returned </td><td> Add 全部在 Wait 前完成 </td></tr><tr><td> Done 多于 Add </td><td> panic: negative counter </td><td> 保证 Add/Done 配对 </td></tr><tr><td> 按值传 WaitGroup </td><td> 同步失效 </td><td> 用指针 </td></tr><tr><td> 多个 goroutine 同时 Wait </td><td> 安全,可正常返回 </td><td> 无需特殊处理 </td></tr><tr><td> 用 wg 替代 chan 传值 </td><td> 语义错位 </td><td> wg 只做同步,值用 chan </td></tr></tbody></table><h2 id="">十、一句话总结</h2><blockquote><p><strong>Add 在启动前、Done 在协程内、Wait 在外部等。一个 wg 只管一件事:生产或消费,各用各的 wg,不要让一个计数器串起两段本应独立的 happens-before 链。</strong></p></blockquote>
<hr/><h2 id="">参考链接</h2><ul><li><a href="https://pkg.go.dev/sync#WaitGroup">Go 官方文档:sync.WaitGroup</a></li><li><a href="https://go.dev/ref/mem">Go Memory Model</a></li><li><a href="https://go.dev/doc/effective_go#concurrency">Effective Go: 并发</a></li></ul></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/go-sync-waitgroup-guide#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/go-sync-waitgroup-guide</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/go-sync-waitgroup-guide</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Tue, 04 Aug 2026 14:35:18 GMT</pubDate></item><item><title><![CDATA[并发入门：真正理解并发与并行的区别]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/concurrency-vs-parallelism-intro">https://blog.zmto.io/posts/programming/concurrency-vs-parallelism-intro</a></blockquote><div><blockquote><p>本文从一个最朴素的问题出发：CPU 单核到底是怎么&quot;同时&quot;处理 100 个请求的？读完你会彻底搞懂&quot;并发&quot;和&quot;并行&quot;——以及为什么新手最容易把它们搞混。</p></blockquote>
<h2 id="">一、先纠正一句话</h2><p>很多人这样开头：</p><blockquote><p>&quot;并发是通过交错执行来处理多个任务的能力，而并行是在不同 CPU 核心上同时执行多个任务的能力。&quot;</p></blockquote>
<p>这句话<strong>基本正确，但不严谨</strong>。</p><ul><li>前半句（并发 = 交错执行多个任务）✅ 准确</li><li>后半句（并行 = 在不同 CPU 核心上同时执行）❌ 过窄</li></ul><p>并行确实包括&quot;多核同时执行&quot;，但远不止于此。并行的实现形式有：</p><ul><li><strong>单机多核</strong>：多 CPU 核心</li><li><strong>SIMD 指令</strong>：单核内一条指令处理多个数据（向量并行）</li><li><strong>GPU 并行</strong>：数千个线程并行</li><li><strong>多机分布式</strong>：多台机器并行（MPI、分布式计算）</li><li><strong>指令级并行（ILP）</strong>：单核内流水线、超标量乱序执行</li></ul><p>并行对应的是&quot;<strong>同时执行</strong>&quot;这个<strong>结果</strong>，而不是某一种具体的硬件手段。</p><h2 id="-vs-">二、并发 vs 并行：一句话的本质区分</h2><p>Rob Pike（Go 语言之父）有一句经典论断：</p><blockquote><p><strong>&quot;Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.&quot;</strong></p><p>并发是&quot;<strong>应对</strong>&quot;很多事，并行是&quot;<strong>执行</strong>&quot;很多事。</p></blockquote>
<p>翻译成工程师能秒懂的话：</p><table><thead><tr><th> 维度 </th><th> 并发（Concurrency） </th><th> 并行（Parallelism） </th></tr></thead><tbody><tr><td> 本质 </td><td> <strong>结构</strong>（程序组织方式） </td><td> <strong>执行</strong>（实际运行方式） </td></tr><tr><td> 关键词 </td><td> 应对、管理、交错 </td><td> 同时、真跑 </td></tr><tr><td> 单核可行 </td><td> ✅ 可以（时间片轮转） </td><td> ❌ 不可能真正同时 </td></tr><tr><td> 必要条件 </td><td> 多个独立推进的逻辑单元 </td><td> 多个硬件执行单元 </td></tr></tbody></table><p><strong>并发是&quot;一个工人同时接了三件活儿，在三件活儿之间来回切换着干&quot;</strong>——交错。
<strong>并行是&quot;三个工人各分到一件活，三人同时各干各的&quot;</strong>——真正同时。</p><h2 id="">三、为什么并发是&quot;交错执行&quot;</h2><p>回到一个物理事实：<strong>单核 CPU 同一时刻只能执行一条指令流</strong>。</p><p>那单核机器怎么&quot;同时&quot;做三件事？答案是 <strong>时间片轮转（time slicing）</strong>——CPU 被切成极小的时间片，在多个任务之间快速切换：</p><pre class=""><code class="">真实时间线（单核）：
时刻1:  任务A ▌
时刻2:           ▌任务B
时刻3:                    ▌任务C
时刻4:  ▌任务A（继续）
时刻5:           ▌任务B（继续）
...

给人的感觉：A、B、C 在&quot;同时&quot;跑
实际真相：它们被切碎穿插执行，切得极快（毫秒级），人感知不到
</code></pre>
<p>这就是&quot;交错执行&quot;（interleaving）的字面意思：<strong>把多个任务切成小片，交替着推进，制造出&quot;同时&quot;的幻觉</strong>。</p><h3 id="">一个具象例子</h3><p>你在单核电脑上同时开着浏览器、音乐播放器、微信，它们都在&quot;运行&quot;。但 CPU 只有一颗核心，操作系统的做法是：</p><ol start="1"><li>给浏览器跑 10ms</li><li>切走去给播放器跑 10ms（解一帧音频）</li><li>再切去给微信跑 10ms（收一次消息）</li><li>回到浏览器……</li></ol><p>每秒切换几百次。三个任务<strong>类型不同</strong>，但因为被交错切片推进，看起来都在&quot;持续进行&quot;。这就是单核并发。</p><h3 id="">对比：并行不是&quot;交错&quot;</h3><pre class=""><code class="">真实时间线（多核，3 核各跑一个任务）：
核心1:  任务A A A A A A A A...  （连续跑，不切换）
核心2:  任务B B B B B B B B...  （连续跑，不切换）
核心3:  任务C C C C C C C C...  （连续跑，不切换）
</code></pre>
<p>每个核心专注一摊，<strong>真正同时</strong>，不切来切去。</p><h2 id="">四、&quot;多个任务&quot;是什么：同类型还是不同类型？</h2><p>这是新手最容易卡的点。答案：<strong>不区分类型</strong>。</p><p>并发语境下的&quot;任务&quot;指的是一个<strong>逻辑执行单元</strong>（一段独立的工作），跟它是同一种业务还是不同业务无关：</p><table><thead><tr><th> 场景 </th><th> 任务类型 </th><th> 是否并发 </th></tr></thead><tbody><tr><td> Web 服务器同时处理 100 个 HTTP 请求 </td><td> <strong>同类型</strong>（都是处理请求） </td><td> ✅ 并发 </td></tr><tr><td> 服务器一边接请求、一边写日志、一边心跳上报 </td><td> <strong>不同类型</strong> </td><td> ✅ 并发 </td></tr><tr><td> 单线程循环累加一亿次 </td><td> 同一个 </td><td> ❌ 不是并发 </td></tr><tr><td> Word 一边打字、一边拼写检查、一边自动保存 </td><td> <strong>不同类型</strong> </td><td> ✅ 并发 </td></tr></tbody></table><p><strong>关键</strong>：并发关心&quot;有几个<strong>独立推进</strong>的执行单元在交替跑&quot;，不在乎它们干的是不是同一件事。</p><h2 id="-100-">五、重头戏：单核怎么扛 100 个并发请求？</h2><p>设想这个场景：</p><blockquote><p>一个接口，100 个人同时请求过来，CPU 单核怎么处理？</p></blockquote>
<p>直觉上，单核一次只能干一件事，怎么顶得住 100 个并发？</p><p>答案就是你脑海里已经形成的那个画面：</p><pre class=""><code class="">单核（真实情况）：

时刻 t0:    CPU 在跑请求#1  ▌  (其余 99 个等着)
时刻 t1:    CPU 在跑请求#2  ▌
时刻 t2:    CPU 在跑请求#3  ▌
...
时刻 t99:   CPU 在跑请求#100 ▌
时刻 t100:  又回到请求#1 继续 ▌
...

任意瞬间：只有 1 个请求在真正执行
一秒内：  每个请求都被推进了十几万次，加起来像 100 个都在跑
</code></pre>
<p>✅ 你理解对了：<strong>单核分时段处理，第 1 个处理一部分就快速切走，再处理第 2 个，再切，再切……营造并行的幻觉，实际只有一个 CPU 在处理这 100 个请求。</strong></p><h2 id="cpu-">六、但这里有个关键：CPU 真的&quot;全程都在算&quot;吗？</h2><p>这是新手最容易忽略、也最该想明白的一点。</p><p><strong>Web 请求处理绝大部分时间 CPU 是闲着的</strong>。一个 HTTP 请求的时间花在哪：</p><table><thead><tr><th> 阶段 </th><th> 谁在干活 </th><th> CPU 占用 </th></tr></thead><tbody><tr><td> 网络接收数据 </td><td> 网卡 + 内核 </td><td> 几乎不占 CPU </td></tr><tr><td> 解析请求、跑业务逻辑 </td><td> <strong>CPU</strong> </td><td> <strong>真正占 CPU</strong> </td></tr><tr><td> 查数据库、等返回 </td><td> 网络 + 数据库进程 </td><td> CPU 干等 </td></tr><tr><td> 调下游接口、等响应 </td><td> 网络 + 下游 </td><td> CPU 干等 </td></tr><tr><td> 网络发送响应 </td><td> 网卡 + 内核 </td><td> 几乎不占 CPU </td></tr></tbody></table><p>一个典型接口，CPU 真正在算的时间可能只占 <strong>5%~20%</strong>，剩下的 80%+ 都在<strong>等</strong>——等数据库、等下游、等网络。</p><h3 id="">这个&quot;等&quot;恰恰是单核能扛高并发的秘密</h3><p>正因为请求大部分时间在&quot;等 I/O&quot;而不是&quot;占 CPU&quot;，单核并发才玩得转：</p><pre class=""><code class="">没有&quot;等&quot;的情况（纯计算，比如算素数）：
请求#1: CPU算算算算算算算算...（死占 CPU 100ms）
请求#2: 必须等 #1 算完才能开始
→ 单核下 100 个请求要排队，慢死

有&quot;等&quot;的情况（典型 Web 请求）：
请求#1: CPU 算 1ms → 查数据库(干等 9ms) → CPU 算 1ms 完成
请求#2: 趁 #1 在等数据库的 9ms 里，CPU 跑去算 #2 → #2 也去等数据库...
→ 单核下 100 个请求大部分时间能重叠推进，CPU 不闲着也不互相挡道
</code></pre>
<p><strong>这就是为什么 I/O 密集型场景，单核异步并发就够了</strong>。CPU 既能服务 100 个请求，自己还不忙。Go 的 goroutine、Node.js 的事件循环、Python 的 async/await，干的就是这件事：</p><blockquote><p><strong>当一个请求去等 I/O 时，立刻把 CPU 让给下一个请求；等 I/O 回来了再切回来继续。</strong></p></blockquote>
<h2 id="100--cpu-">七、反过来：100 个请求都是 CPU 密集型呢？</h2><p>比如都是&quot;给我把这张图做高斯模糊&quot;——纯 CPU 计算，没有 I/O 等待：</p><pre class=""><code class="">请求#1: CPU 算算算算算算...(占满内核 10ms，不喘气)
请求#2: 排队等 #1 算完
...
单核处理 100 个：约 1000ms 总耗时，每个请求平均等约 500ms
</code></pre>
<p>这时单核并发<strong>没救了</strong>——交错执行也省不出时间，因为每个任务都死咬着 CPU 不松口，切来切去反而有切换开销，100 个请求被串行成一条长队。</p><p><strong>唯一办法就是上并行</strong>——多核，让多核各自真同时算几个：</p><pre class=""><code class="">4 核并行：
核心1: 请求 #1, #5, #9 ...   (真同时各自算)
核心2: 请求 #2, #6, #10 ...
核心3: 请求 #3, #7, #11 ...
核心4: 请求 #4, #8, #12 ...
→ 总耗时降到约 250ms，快 4 倍
</code></pre>
<h2 id="">八、把一切收敛成一句话</h2><ul><li><strong>Web 接口场景</strong>：单核分时段切来切去服务 100 个请求，是单核并发的标准玩法；能跑得动，是因为请求大部分时间在等 I/O，CPU 趁等的时候去服务别人。</li><li><strong>CPU 密集型场景</strong>：同样的切法就玩不转了，CPU 一直被占着没空隙可切，必须靠多核并行才能真正加速。</li></ul><p>所以&quot;并发 = 交错执行&quot;的<strong>完整真相</strong>是：</p><blockquote><p>交错只有在任务<strong>会主动让出 CPU</strong>（等 I/O、yield、sleep）时才跑得顺；纯计算任务不让出，交错就退化成排队。</p></blockquote>
<p>并发解决的是&quot;<strong>如何管理</strong>多个任务&quot;——让它们轮流占资源、不互相阻塞死。
并行解决的是&quot;<strong>如何加速</strong>&quot;——多个干活的人同时上。</p></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/concurrency-vs-parallelism-intro#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/concurrency-vs-parallelism-intro</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/concurrency-vs-parallelism-intro</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Mon, 03 Aug 2026 13:57:08 GMT</pubDate></item><item><title><![CDATA[Go 的 panic 与 recover：一场有边界的失火与灭火]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/go-panic-recover">https://blog.zmto.io/posts/programming/go-panic-recover</a></blockquote><div><p>初学 Go 的人常会问：Go 没有 try-catch，那运行时炸了怎么办？</p><p>答案就是 <code>panic</code> 和 <code>recover</code>。它们长得像别的语言的 throw/catch，但脾气完全不同——更克制、更有边界。</p><h2 id="panic">panic：把火警拉响</h2><p><code>panic</code> 一旦触发，当前 goroutine 不再往下走，而是开始「栈展开」：一层层往回退，每退一层执行该层的 <code>defer</code>，直到有 <code>recover</code> 接住，或者整个程序退出。</p><p>两种触发方式：</p><ul><li><strong>你主动拉</strong>：<code>panic(&quot;divide by zero&quot;)</code></li><li><strong>运行时替你拉</strong>：数组越界、空指针解引用、类型断言失败、往已关闭的 channel 发数据、并发读写 map……</li></ul><p>参数可以是任意类型，叫 panic value。传个 <code>error</code> 比传字符串更好，下游 <code>recover</code> 时方便判断。</p><h2 id="recover-defer-">recover：唯一的灭火器，且只能在 defer 里用</h2><p><code>recover</code> 有个死规矩：<strong>必须在 defer 的闭包里直接调用</strong>。脱离 defer，它永远返回 <code>nil</code>，啥也接不住。</p><pre class="language-go lang-go"><code class="language-go lang-go">defer func() {
    if r := recover(); r != nil {
        fmt.Println(&quot;接住了:&quot;, r)
    }
}()
</code></pre>
<p>为什么非得 defer？因为 panic 触发后，普通代码已经没机会执行了，只有 <code>defer</code> 注册的函数还会被调用。<code>recover</code> 就是借着 defer 这个「发言人位」抢回控制权。</p><h2 id="">标准配合姿势</h2><p>把 recover 塞进 defer，外面包一层「安全函数」，捕获后转成 error 返回：</p><pre class="language-go lang-go"><code class="language-go lang-go">func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf(&quot;内部错误: %v&quot;, r)
            result = 0
        }
    }()
    if b == 0 {
        panic(&quot;divide by zero&quot;)
    }
    return a / b, nil
}
</code></pre>
<h2 id="-goroutine-">给 goroutine 套个盔甲</h2><p>最关键的踩坑点：<strong>子 goroutine 的 panic，父 goroutine 接不住</strong>。一个没防护的 goroutine panic，整个程序就挂了。所以每个 goroutine 都该自带 recover：</p><pre class="language-go lang-go"><code class="language-go lang-go">func runSafely(fn func()) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf(&quot;goroutine 崩了: %v\n%s&quot;, r, debug.Stack())
        }
    }()
    fn()
}

go runSafely(func() { /* 危险操作 */ })
</code></pre>
<h2 id="">几个高频翻车点</h2><ul><li><code>defer recover()</code> <strong>没用</strong>——返回值被丢，panic 照传。必须闭包包起来。</li><li>跨 goroutine <strong>接不到</strong>——recover 只对自己所在函数负责。</li><li><strong>别用 panic 替代 error</strong>：error 是 Go 的常规错误处理，panic 留给「真不该发生」的情况。</li><li>并发读写 map 引发的 panic，应改用 <code>sync.Map</code> 或加锁，而不是 try-recover 绕过。</li></ul><h2 id="">一句话记忆</h2><p><code>panic</code> 是失火警报，<code>recover</code> 是灭火器，但只能在 <code>defer</code> 这个发言位上用，且只管自己这一层。库内部该 panic 就 panic，服务入口和 goroutine 边界该 recover 就 recover——边界划清楚，程序就稳了。</p></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/go-panic-recover#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/go-panic-recover</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/go-panic-recover</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Mon, 03 Aug 2026 13:51:51 GMT</pubDate></item><item><title><![CDATA[iota：Go 语言中最被低估的魔法常量]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/go-iota-deep-dive">https://blog.zmto.io/posts/programming/go-iota-deep-dive</a></blockquote><div><h2 id="">开头</h2><p>在 Go 语言中，<code>iota</code> 是一个预声明标识符，它让你在 <code>const</code> 声明块里生成连续递增的值。看起来很简单？大多数教程只教你这种用法：</p><pre class="language-go lang-go"><code class="language-go lang-go">const (
    Monday = iota + 1
    Tuesday
    Wednesday
    Thursday
    Friday
)
</code></pre>
<p>设计细节——尤其是<strong>同一行内 <code>iota</code> 值不变</strong>这一条——才是真正精巧的地方。不搞懂它，你就永远只会在写星期枚举。</p><hr/><h2 id="iota-">基础回顾：iota 三定律</h2><ol start="1"><li><strong>iota 只在 const 块内部有意义</strong>，且从 0 开始。</li><li><strong>每新起一行，iota 自增 1</strong>（即使这一行没写 iota）。</li><li><strong>隐式重复上一行的表达式</strong>——这是 Go 帮你看懂 iota 的扩写方式。</li></ol><pre class="language-go lang-go"><code class="language-go lang-go">const (
    a = iota  // 0
    b         // 1（隐式重复 a = iota）
    c         // 2
)
</code></pre>
<h2 id="-iota-">核心争议：同一行 iota 为什么不变？</h2><p>看这段代码：</p><pre class="language-go lang-go"><code class="language-go lang-go">const (
    ss, mm = iota + 1, iota + 2
    cc, dd
)
fmt.Println(ss, mm) // 1 2
fmt.Println(cc, dd) // 2 3
</code></pre>
<p><strong>你必须直观接受一个事实：同一行内所有对 <code>iota</code> 的引用共享同一个值。</strong></p><ul><li>第 1 行：<code>iota = 0</code>，<code>ss = 1, mm = 2</code></li><li>第 2 行：<code>iota = 1</code>，<code>cc = 2, dd = 3</code></li></ul><p>为什么呢？因为“一行”是构造常量的最小单元。</p><hr/><h2 id="--">设计哲学：一行 = 一条枚举项</h2><pre class="language-go lang-go"><code class="language-go lang-go">const (
    id, name = iota + 1, fmt.Sprintf(&quot;item-%d&quot;, iota)
    a, _
    b, _
)
</code></pre>
<p><code>iota</code> 在行内不变，就是为了让你在同一行写出&quot;这一项&quot;的不同属性表达式，然后再在下一行用同样的模板自动生成下一项。</p><h3 id="">隐含的重复模板</h3><p>第二行 <code>cc, dd</code> 不写显式表达式，Go 帮你把它展开成：</p><pre class="language-go lang-go"><code class="language-go lang-go">1, iota + 2
</code></pre>
<p>现在 <code>iota = 1</code>，所以 <code>cc = 2, dd = 3</code>。如果 <code>iota</code> 在行内就变了，编译器就是无法判断上一行的模板是什么样的。</p><hr/><h2 id="3-">3. 高阶用法：多值枚举表</h2><p><code>iota</code> 行内不变的特性解锁了第二个能力：<strong>用 <code>const</code> 生成多维数组</strong>。</p><pre class="language-go lang-go"><code class="language-go lang-go">type Color int

const (
    Red    Color = iota
    Green
    Blue
)

const (
    RedAttr   = 255
    RedAttr   = 0
    RedAttr   = 0

    GreenAttr = 0
    GreenAttr = 255
    GreenAttr = 0

    BlueAttr  = 0
    BlueAttr  = 0
    BlueAttr  = 255
)
</code></pre>
<p>每项在“数据库”中有 N 个属性，而 <code>iota</code> 只负责给每个属性赋一个递增的行号。</p><h3 id="-iota-">配合 iota 的掩码</h3><p>同一行不变的另一个经典应用是<strong>标志位与掩码</strong>：</p><pre class="language-go lang-go"><code class="language-go lang-go">const (
    FlagA, MaskA = 1 &lt;&lt; iota, 1 &lt;&lt; (iota + 1) // Flag = 1, Mask = 2
    FlagB, MaskB = 1 &lt;&lt; iota, 1 &lt;&lt; (iots + 1) // Flag = 2, Mask = 4
)
</code></pre>
<p>这两种值必须在同一行上求值才算同一组，否则两者就不能共用同一个模板。</p><hr/><h2 id="3-iota-">3. iota 的常见陷阱</h2><table><thead><tr><th> 误解 </th><th> 实际 </th></tr></thead><tbody><tr><td> “iota 在每个引用位置都加 1” </td><td> 按行进进，行内引用次数不影响 </td></tr><tr><td> “iota 在第 N 个变量上加 1” </td><td> 跟变量的位置无关，不管一行有几个变量 </td></tr><tr><td> “空白行不会推进 iota” </td><td> 空白行也不计算，会推进 iota </td></tr></tbody></table><pre class="language-go lang-go"><code class="language-go lang-go">const (
    _
    _
    c = iota // 错！此时 iota 已经是 2，不是你想要的 0
)
</code></pre>
<p>写复杂 iota 之前先画一张行号表，别猜。</p><hr/><h2 id="">结尾</h2><p><code>iota</code> 是 Go 设计得非常节制的功能。就一条规则：“每行一个步进”——其他所有能力都来自这条规则的结果。理解它不是让你学会“写枚举”，而是让你真正理解 Go 常量系统的“究极思维”。</p></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/go-iota-deep-dive#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/go-iota-deep-dive</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/go-iota-deep-dive</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Sun, 02 Aug 2026 07:54:39 GMT</pubDate></item><item><title><![CDATA[Go 方法集深度解析]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/go-method-set-explained">https://blog.zmto.io/posts/programming/go-method-set-explained</a></blockquote><div><blockquote><p>方法集是 Go 接口编程中最易踩坑的概念。看懂它，接口逻辑就通了。</p></blockquote>
<hr/><h2 id="">一、什么是方法集？</h2><p><strong>方法集</strong>（Method Set）是类型（值 T 或指针 *T）能作为接收者调用的全部方法名单。</p><p>它的唯一作用：<strong>判断一个类型是否实现了某个接口</strong>。</p><hr/><h2 id="">二、核心规则</h2><table><thead><tr><th> 接收者类型 </th><th> 包含 T 的值方法 </th><th> 包含 *T 的指针方法 </th></tr></thead><tbody><tr><td> T       </td><td> ✅            </td><td> ❌               </td></tr><tr><td> *T      </td><td> ✅            </td><td> ✅               </td></tr></tbody></table><p><strong>一句话：*T 是 T 的超集，大圈包小圈。</strong></p><hr/><h2 id="">三、为什么出这个设计？</h2><h3 id="31---">3.1 指针 → 值是确定的</h3><pre class="language-go lang-go"><code class="language-go lang-go">type Cat struct{ name string }

func (c Cat) Eat() {} // 值方法

var p *Cat = &amp;Cat{&quot;小花&quot;}
p.Eat() // ✅ 编译器自动解引用 -&gt; (*p).Eat()
        //    Cat 从 *Cat 是 100% 可及的
</code></pre>
<h3 id="32---">3.2 值 → 指针不一定</h3><pre class="language-go lang-go"><code class="language-go lang-go">func (c *Cat) Chase() {} // 指针方法

Cat{}         // ← 临时值，不存在可用地址
Cat{}.Chase() // ❌ 不可寻址 → 方法集中不包含 Chase
</code></pre>
<p>不能保证所有值都能取地址，所以 T 的方法集不包含指针方法。</p><hr/><h2 id="">四、方法集和接口（核心场景）</h2><pre class="language-go lang-go"><code class="language-go lang-go">type Dog struct{ Name string }

func (d Dog) Bark() { fmt.Println(&quot;汪汪&quot;) }     // 值方法
func (d *Dog) Run() { fmt.Println(&quot;跑10米&quot;)  } // 指针方法

type Runner interface { Run() }

var r Runner

r = Dog{}     // ❌ Dog 的方法集中没有 Run
r = &amp;Dog{}    // ✅ *Dog 的方法集中有 Bark + Run
</code></pre>
<p>接口只看<strong>值本身的方法集</strong>是否完整，不看编译器语法糖有没有帮你取地址。</p><hr/><h2 id="--">五、记忆口诀：编译能调 ≠ 方法集里有</h2><table><thead><tr><th> 操作 </th><th style="text-align:center"> 编译是否通过 </th><th style="text-align:center"> 属于方法集？ </th></tr></thead><tbody><tr><td> d.Run() </td><td style="text-align:center"> ✅（编译器自动 (&amp;d).Run()） </td><td style="text-align:center"> ❌ Run 不在 T 的方法集 </td></tr><tr><td> p.Bark() </td><td style="text-align:center"> ✅（编译器自动 (*p).Bark()） </td><td style="text-align:center"> ✅ *T 的方法集合包含一切 </td></tr><tr><td> Dog{}.Run() </td><td style="text-align:center"> ❌ 编译报错 </td><td style="text-align:center"> ❌ 不可寻址，更勿论方法集 </td></tr></tbody></table><hr/><h2 id="">六、选择值还是指针接收者？</h2><table><thead><tr><th> 场景 </th><th style="text-align:center"> 值接收者 </th><th style="text-align:center"> 指针接收者 </th></tr></thead><tbody><tr><td> 不改字段 </td><td style="text-align:center"> ✅ </td><td style="text-align:center"> ✅ </td></tr><tr><td> 改字段 </td><td style="text-align:center"> ❌ 改副本 </td><td style="text-align:center"> ✅ 改原值 </td></tr><tr><td> 大结构体避免拷贝 </td><td style="text-align:center"> ❌ 拷贝开销大 </td><td style="text-align:center"> ✅ 只传指针 </td></tr><tr><td> 接口用值实例化 </td><td style="text-align:center"> T 满足就行 </td><td style="text-align:center"> 不纠结 </td></tr></tbody></table><p><strong>推荐：</strong> 默认从指针接收器开始设计，未来扩展接口不被动。</p><hr/><h2 id="">七、运行验证</h2><pre class="language-ash lang-ash"><code class="language-ash lang-ash">go doc type.TypeName   # 查看类型的方法集
go vet main.go         # 静态检查接口装量
</code></pre>
<pre class="language-go lang-go"><code class="language-go lang-go">import &quot;reflect&quot;

// 另法：用反射列出一个类型的所有方法
t := reflect.TypeOf((*Dog)(nil)).Elem()
for i:=0; i&lt;t.NumMethod(); i++ {
    fmt.Println(t.Method(i).Name)
}
</code></pre>
<hr/><h2 id="">八、一句话总结</h2><table><thead><tr><th>  </th><th> 方法集 </th></tr></thead><tbody><tr><td> <strong>值类型 T</strong> </td><td> 只含在 T 上面定义的方法 </td></tr><tr><td> <strong>指针类型 *T</strong> </td><td> 含在 T 和 *T 上面定义的所有方法 </td></tr></tbody></table><p>接口不变只看方法集，不看编译器的语法糖帮你自动传参的。</p></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/go-method-set-explained#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/go-method-set-explained</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/go-method-set-explained</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Tue, 28 Jul 2026 14:39:50 GMT</pubDate></item><item><title><![CDATA[文件校验：如何确保下载的文件没被篡改]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/hash-verification">https://blog.zmto.io/posts/programming/hash-verification</a></blockquote><div><h2 id="1-">1. 问题</h2><p>从网上下载文件，如何确认文件没被篡改？官网通常会提供一串哈希值，这串字符是干什么用的？</p><hr/><h2 id="2-">2. 什么是哈希校验</h2><p>哈希校验是通过哈希函数对数据生成固定长度的哈希值，用于验证数据完整性。</p><p>特点：</p><ul><li><strong>固定长度</strong>：无论文件多大，哈希值长度固定</li><li><strong>唯一性</strong>：不同文件的哈希值几乎不可能相同</li><li><strong>不可逆</strong>：无法从哈希值反推原文件</li></ul><pre class=""><code class="">文件内容 → 哈希函数 → 哈希值
</code></pre>
<p>下载文件后，计算哈希值与官方提供的值对比，一致则文件完整。</p><hr/><h2 id="3-">3. 常见哈希算法</h2><table><thead><tr><th> 算法 </th><th> 长度 </th><th> 安全性 </th><th> 用途 </th></tr></thead><tbody><tr><td> MD5 </td><td> 128位 </td><td> 已不安全 </td><td> 文件校验（非安全场景） </td></tr><tr><td> SHA-1 </td><td> 160位 </td><td> 已不安全 </td><td> Git提交、旧系统 </td></tr><tr><td> SHA-256 </td><td> 256位 </td><td> 安全 </td><td> 文件校验、数字签名 </td></tr><tr><td> SHA-512 </td><td> 512位 </td><td> 更安全 </td><td> 高安全要求场景 </td></tr></tbody></table><p><strong>建议</strong>：优先使用 SHA-256，MD5 仅用于非安全场景。</p><hr/><h2 id="4-windows-">4. Windows 校验方法</h2><p><strong>PowerShell（推荐）</strong>：</p><pre class="language-powershell lang-powershell"><code class="language-powershell lang-powershell">Get-FileHash 文件名 -Algorithm MD5
Get-FileHash 文件名 -Algorithm SHA256
</code></pre>
<p><strong>CertUtil（系统内置）</strong>：</p><pre class="language-cmd lang-cmd"><code class="language-cmd lang-cmd">certutil -hashfile 文件名 MD5
certutil -hashfile 文件名 SHA256
</code></pre>
<p><strong>第三方工具</strong>：</p><ul><li><strong>HashCheck</strong>：右键菜单集成，轻量</li><li><strong>7-Zip</strong>：内置哈希计算（右键 → CRC SHA）</li></ul><hr/><h2 id="5-linuxmac-">5. Linux/Mac 校验方法</h2><p><strong>专用命令</strong>：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">md5sum 文件名
sha256sum 文件名
sha1sum 文件名
</code></pre>
<p><strong>shasum（灵活选择算法）</strong>：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">shasum -a 256 文件名  # SHA-256
shasum -a 1 文件名    # SHA-1
shasum 文件名         # 默认 SHA-1
</code></pre>
<hr/><h2 id="6-">6. 编程语言校验</h2><p><strong>Go</strong>：</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import (
    &quot;crypto/sha256&quot;
    &quot;fmt&quot;
    &quot;io&quot;
    &quot;os&quot;
)

func main() {
    f, _ := os.Open(&quot;文件名&quot;)
    defer f.Close()
    
    h := sha256.New()
    io.Copy(h, f)
    
    fmt.Printf(&quot;%x\n&quot;, h.Sum(nil))
}
</code></pre>
<p><strong>Python</strong>：</p><pre class="language-python lang-python"><code class="language-python lang-python">import hashlib

with open(&quot;文件名&quot;, &quot;rb&quot;) as f:
    sha256 = hashlib.sha256(f.read()).hexdigest()
    print(sha256)
</code></pre>
<hr/><h2 id="7-">7. 实际应用场景</h2><p><strong>文件下载</strong>：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 下载后校验
wget https://example.com/file.zip
sha256sum file.zip
# 对比官网提供的哈希值
</code></pre>
<p><strong>Git 提交验证</strong>：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">git log --show-signature  # 查看提交签名
</code></pre>
<p><strong>软件完整性</strong>：</p><ul><li>操作系统镜像</li><li>软件安装包</li><li>重要文档</li></ul><hr/><h2 id="8-">8. 注意事项</h2><ol start="1"><li><strong>哈希值必须从官方渠道获取</strong>，不能用文件内附带的</li><li><strong>MD5 已不安全</strong>，容易被碰撞攻击，仅用于非安全场景</li><li><strong>大文件校验耗时较长</strong>，耐心等待</li><li><strong>哈希值相同不代表文件无病毒</strong>，只说明文件未被篡改</li></ol><hr/><h2 id="">总结</h2><table><thead><tr><th> 场景 </th><th> 推荐算法 </th><th> 工具 </th></tr></thead><tbody><tr><td> 一般文件校验 </td><td> SHA-256 </td><td> 系统自带命令 </td></tr><tr><td> 非安全场景 </td><td> MD5 </td><td> 系统自带命令 </td></tr><tr><td> 高安全要求 </td><td> SHA-512 </td><td> 编程语言实现 </td></tr></tbody></table><p>核心原则：<strong>哈希校验只能验证完整性，不能验证安全性。</strong></p><p>记住这个区别，正确使用哈希校验。</p></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/hash-verification#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/hash-verification</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/hash-verification</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Mon, 13 Jul 2026 12:48:20 GMT</pubDate></item><item><title><![CDATA[Go 包级别变量：声明顺序不影响使用]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://blog.zmto.io/posts/programming/golang-package-scope">https://blog.zmto.io/posts/programming/golang-package-scope</a></blockquote><div><h2 id="1-">1. 问题</h2><pre class="language-go lang-go"><code class="language-go lang-go">package main

import &quot;fmt&quot;

func main() {
    fmt.Println(x) // 可以访问，但 x 在 main 下面声明
}

var x = 10
</code></pre>
<p><code>x</code> 在 <code>main</code> 函数下方声明，但 <code>main</code> 函数中可以正常访问。代码执行不是从上到下的吗？</p><hr/><h2 id="2--vs-">2. 包级别 vs 函数内部</h2><p><strong>函数内部</strong>的代码确实是顺序执行的：</p><pre class="language-go lang-go"><code class="language-go lang-go">func main() {
    fmt.Println(y) // ❌ 编译错误：undefined: y
    y := 10
}
</code></pre>
<p>但<strong>包级别</strong>的声明（在函数外部）顺序无关紧要：</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import &quot;fmt&quot;

var a = b + 1  // ✅ 可以访问 b
var b = 10

func main() {
    fmt.Println(a, b) // 输出：11 10
}
</code></pre>
<hr/><h2 id="3-">3. 为什么可以这样？</h2><p>Go 编译器在编译阶段会<strong>先扫描整个包的所有声明</strong>，建立符号表，然后才生成代码。</p><p>简单来说：</p><ol start="1"><li><strong>编译阶段</strong>：收集所有包级别声明（变量、常量、类型、函数）</li><li><strong>生成阶段</strong>：根据符号表生成可执行代码</li></ol><p>所以声明顺序不影响可见性。</p><hr/><h2 id="4-">4. 函数内部为什么不行？</h2><p>函数内部的代码是<strong>运行时</strong>顺序执行的，编译器不会提前扫描：</p><pre class="language-go lang-go"><code class="language-go lang-go">func main() {
    fmt.Println(x) // 运行时才执行，此时 x 还未声明
    x := 10
}
</code></pre>
<p>这里有两个不同的阶段：</p><ul><li><strong>编译时</strong>：检查语法、类型</li><li><strong>运行时</strong>：顺序执行语句</li></ul><p>函数内部的语句是运行时执行的，所以必须先声明后使用。</p><hr/><h2 id="5-">5. 注意：循环依赖</h2><p>虽然声明顺序无关，但<strong>初始化顺序</strong>是有规则的：</p><pre class="language-go lang-go"><code class="language-go lang-go">var a = b + 1  // ✅ b 先初始化为 10，然后 a = 10 + 1 = 11
var b = 10

var c = d + 1  // ❌ 编译错误：initialization loop
var d = c + 1
</code></pre>
<p>Go 编译器会分析依赖关系，按正确顺序初始化变量，但循环依赖会报错。</p><hr/><h2 id="6-">6. 包的作用域：同包内文件直接共享变量</h2><p>同一目录下的文件如果属于同一个包（相同包名），变量可以直接使用，不需要导入：</p><pre class="language-go lang-go"><code class="language-go lang-go">// file1.go
package mypkg

var shared = &quot;hello&quot;

// file2.go
package mypkg

import &quot;fmt&quot;

func PrintShared() {
    fmt.Println(shared) // ✅ 直接使用，不需要导入
}
</code></pre>
<p>如果包名不同，Go 不允许同一目录下存在多个包（除了 <code>_test</code> 文件）。</p><hr/><h2 id="7-">7. 为什么这样设计？</h2><p><strong>目录即包</strong>：</p><ol start="1"><li><p><strong>同一目录只能有一个包</strong>：简化导入路径，避免混乱。目录就是包的边界，清晰明了。</p></li><li><p><strong>同包内文件直接共享变量</strong>：包是编译的基本单元，同包文件一起编译，共享命名空间。方便将大包拆分成多个文件，同时保持内部一致性。</p></li></ol><p>这种设计让包的组织结构简单可预测。</p><hr/><h2 id="8-">8. 实际应用</h2><p>这个特性在实际开发中很有用：</p><pre class="language-go lang-go"><code class="language-go lang-go">package config

var (
    DBHost     = getEnv(&quot;DB_HOST&quot;, &quot;localhost&quot;)
    DBPort     = getEnv(&quot;DB_PORT&quot;, &quot;5432&quot;)
    DBUser     = getEnv(&quot;DB_USER&quot;, &quot;postgres&quot;)
    DBPassword = getEnv(&quot;DB_PASSWORD&quot;, &quot;&quot;)
)

func getEnv(key, defaultValue string) string {
    // 从环境变量获取配置
    // ...
}
</code></pre>
<p>配置变量可以集中声明，而 <code>getEnv</code> 函数可以在任何地方定义。</p><hr/><h2 id="">总结</h2><table><thead><tr><th> 位置 </th><th> 声明顺序 </th><th> 原因 </th></tr></thead><tbody><tr><td> 包级别 </td><td> 无关 </td><td> 编译时先扫描所有声明 </td></tr><tr><td> 函数内部 </td><td> 必须先声明 </td><td> 运行时顺序执行 </td></tr><tr><td> 同包内文件 </td><td> 直接共享 </td><td> 同包一起编译，共享命名空间 </td></tr></tbody></table><p>核心区别：</p><ul><li><strong>包级别声明</strong>是编译时处理</li><li><strong>函数内部语句</strong>是运行时执行</li><li><strong>同包内文件</strong>共享作用域，目录即包</li></ul><p>记住这些规则，就不会被 Go 的作用域搞混了。</p></div><p style="text-align:right"><a href="https://blog.zmto.io/posts/programming/golang-package-scope#comments">览毕，何不一言？</a></p></div>]]></description><link>https://blog.zmto.io/posts/programming/golang-package-scope</link><guid isPermaLink="true">https://blog.zmto.io/posts/programming/golang-package-scope</guid><dc:creator><![CDATA[枕梦]]></dc:creator><pubDate>Mon, 13 Jul 2026 12:47:11 GMT</pubDate></item></channel></rss>