<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>PHP on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/php/</link>
        <description>Recent content in PHP on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Sat, 30 May 2026 17:57:01 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/php/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>从 PHP 到 Go：真正迁移的是复杂度的归属</title>
            <link>https://www.wujiachen.com.cn/posts/php-to-go-migration/</link>
            <pubDate>Sat, 30 May 2026 17:56:38 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/php-to-go-migration/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/cover.png&#34; alt=&#34;Featured image of post 从 PHP 到 Go：真正迁移的是复杂度的归属&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;本文不讲性能对比，只讲迁移后判断标准怎么变。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;本文五章看什么：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;#%e4%b8%80-%e9%94%99%e8%af%af%e5%bc%80%e5%a7%8b%e6%9b%b4%e6%97%a9%e5%9c%b0%e5%87%ba%e7%8e%b0&#34; &gt;错误开始更早地出现&lt;/a&gt;：用一个 JSON 解码实验拆开&amp;quot;Go 更严格&amp;quot;这句话&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;#%e4%ba%8c-%e5%a4%8d%e6%9d%82%e5%ba%a6%e6%b2%a1%e6%9c%89%e6%b6%88%e5%a4%b1%e5%8f%aa%e6%98%af%e6%8d%a2%e4%ba%86%e5%9c%b0%e6%96%b9&#34; &gt;复杂度没有消失，只是换了地方&lt;/a&gt;：六类责任在两种语言默认路径之间的位移&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;#%e4%b8%89-%e7%9c%9f%e6%ad%a3%e4%b8%8d%e9%80%82%e5%ba%94%e7%9a%84%e6%98%af%e8%b4%a3%e4%bb%bb%e8%be%b9%e7%95%8c&#34; &gt;真正不适应的是责任边界&lt;/a&gt;：用一个迁移 PR 现场把变化讲具体&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;#%e5%9b%9b-php-%e6%95%99%e4%bc%9a%e6%88%91%e7%9a%84%e4%b8%9c%e8%a5%bfgo-%e6%9b%bf%e4%bb%a3%e4%b8%8d%e4%ba%86&#34; &gt;PHP 教会我的东西，Go 替代不了&lt;/a&gt;：避免迁移中最常见的反向误区&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;#%e4%ba%94-%e8%bf%81%e7%a7%bb%e5%ae%8c%e6%88%90%e7%9a%84%e6%a0%87%e5%bf%97%e4%b8%8d%e6%98%af%e4%bb%a3%e7%a0%81%e8%b7%91%e8%b5%b7%e6%9d%a5&#34; &gt;迁移完成的标志，不是代码跑起来&lt;/a&gt;：一张判断表 + 三类证据&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;很多人以为，从 PHP 到 Go，是从灵活走向规整。&lt;/p&gt;&#xA;&lt;p&gt;我一开始也这么理解。&lt;/p&gt;&#xA;&lt;p&gt;换一套语法，换一套框架，换一种部署方式。原来在 PHP 里写 controller、service、repository，现在在 Go 里写 handler、service、repository。原来靠 FPM 和框架生命周期，现在靠一个常驻进程、一个二进制、几组 goroutine。&lt;/p&gt;&#xA;&lt;p&gt;表面看，这确实是一场语言迁移。&lt;/p&gt;&#xA;&lt;p&gt;后来真正让我卡住的，不是语法。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;$&lt;/code&gt; 少了，习惯几天就好。&lt;code&gt;if err != nil&lt;/code&gt; 多了，也只是手感问题。二进制部署当然爽，但也没有改变我看问题的方式。&lt;/p&gt;&#xA;&lt;p&gt;真正别扭的是：很多过去可以晚一点处理的问题，突然提前站到了你面前。&lt;/p&gt;&#xA;&lt;p&gt;请求字段类型对不对，不能先让数据流进业务函数再说。&lt;/p&gt;&#xA;&lt;p&gt;失败路径怎么传，不能只靠统一异常兜底。&lt;/p&gt;&#xA;&lt;p&gt;副作用失败怎么办，不能藏在框架事件里假装它只是一个&amp;quot;后置动作&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;共享状态归谁管，不能靠&amp;quot;这个请求结束，请求态就清掉了&amp;quot;的习惯绕过去。&lt;/p&gt;&#xA;&lt;p&gt;这些问题在 PHP 里不是不存在。&lt;/p&gt;&#xA;&lt;p&gt;它们只是经常被框架、运行时、团队经验和上线后的日志吸收了。换到 Go 之后，它们更早出现，更难隐身，也更需要你在写代码之前说清楚。&lt;/p&gt;&#xA;&lt;p&gt;所以我现在更愿意这样理解这场迁移：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;从 PHP 到 Go，真正迁移的不是语法，而是复杂度的归属。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;复杂度没有消失。&lt;/p&gt;&#xA;&lt;p&gt;它只是从运行时、框架兜底和人的经验里，被搬到了类型定义、错误返回、测试、代码审查和并发边界里。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch0-complexity-relocation.png&#34; alt=&#34;复杂度搬家示意图&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;工程师把输入校验、错误处理、副作用等责任，从运行时/框架兜底区搬到类型/测试/审查区&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;下面用一个小实验来拆开看。&lt;/p&gt;&#xA;&lt;h2 id=&#34;一错误开始更早地出现&#34;&gt;&lt;a href=&#34;#%e4%b8%80%e9%94%99%e8%af%af%e5%bc%80%e5%a7%8b%e6%9b%b4%e6%97%a9%e5%9c%b0%e5%87%ba%e7%8e%b0&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一、错误开始更早地出现&#xA;&lt;/h2&gt;&lt;p&gt;迁移最容易给人制造一种错觉：Go 更严格，所以 Go 更安全。&lt;/p&gt;&#xA;&lt;p&gt;这句话太粗。&lt;/p&gt;&#xA;&lt;p&gt;更准确的说法是：Go 的默认路径更容易让一部分错误提前暴露。但 PHP 也不是完全没有办法把错误前移。区别不在&amp;quot;能不能&amp;quot;，而在&amp;quot;默认把失败路径放在哪里&amp;quot;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;实验设置一段故意写错的-json&#34;&gt;&lt;a href=&#34;#%e5%ae%9e%e9%aa%8c%e8%ae%be%e7%bd%ae%e4%b8%80%e6%ae%b5%e6%95%85%e6%84%8f%e5%86%99%e9%94%99%e7%9a%84-json&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;实验设置：一段故意写错的 JSON&#xA;&lt;/h3&gt;&lt;p&gt;我做了一个很小的对照实验。&lt;/p&gt;&#xA;&lt;p&gt;输入只有一段 JSON：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;42&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;&amp;#34;email&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;123&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意两个字段都故意传错类型：&lt;code&gt;user_id&lt;/code&gt; 本来应该是整数，却传成了字符串；&lt;code&gt;email&lt;/code&gt; 本来应该是字符串，却传成了数字。&lt;/p&gt;&#xA;&lt;p&gt;我分别跑了三条路径：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;PHP 默认弱类型；&lt;/li&gt;&#xA;&lt;li&gt;PHP 开启 &lt;code&gt;strict_types=1&lt;/code&gt;；&lt;/li&gt;&#xA;&lt;li&gt;Go 用 &lt;code&gt;encoding/json&lt;/code&gt; 解码到强类型 struct。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;实测环境是 Go 1.26.2 和 PHP 8.4.21 CLI（运行日期 2026-05-29，完整代码与原始运行输出见&lt;a class=&#34;link&#34; href=&#34;#evidence-repo&#34; &gt;文末仓库&lt;/a&gt;）。&lt;/p&gt;&#xA;&lt;h3 id=&#34;三条路径的实测结果&#34;&gt;&lt;a href=&#34;#%e4%b8%89%e6%9d%a1%e8%b7%af%e5%be%84%e7%9a%84%e5%ae%9e%e6%b5%8b%e7%bb%93%e6%9e%9c&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;三条路径的实测结果&#xA;&lt;/h3&gt;&lt;p&gt;结果很有意思。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[PHP weak types]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;user_id: &amp;#34;42&amp;#34; -&amp;gt; integer 42&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;email: 123 -&amp;gt; string &amp;#34;123&amp;#34;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[PHP strict_types=1]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;TypeError: findUser(): Argument #1 ($userId) must be of type int, string given&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[Go JSON struct decode]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;json: cannot unmarshal string into Go struct field UserPayload.user_id of type int&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个实验只能证明类型不匹配会在解码阶段提前暴露。它不能自动覆盖必填校验、格式验证、未知字段拒绝（需要 &lt;code&gt;Decoder.DisallowUnknownFields&lt;/code&gt;）和零值语义——这些仍需要 decoder 配置或显式 validator 完成。&lt;/p&gt;&#xA;&lt;p&gt;另外，strict 模式下第一个参数已在函数调用边界失败，第二个字段未继续验证；如要观察 email 行为，需要单独跑 user_id 正确、email 错误的 case。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch1-error-boundary-compare.png&#34; alt=&#34;错误暴露位置对比图&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;三条路径并列：PHP weak 继续流动、PHP strict 在函数调用边界拦截、Go 在解码边界拦截&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;结果的真正含义默认路径训练不同的责任摆放&#34;&gt;&lt;a href=&#34;#%e7%bb%93%e6%9e%9c%e7%9a%84%e7%9c%9f%e6%ad%a3%e5%90%ab%e4%b9%89%e9%bb%98%e8%ae%a4%e8%b7%af%e5%be%84%e8%ae%ad%e7%bb%83%e4%b8%8d%e5%90%8c%e7%9a%84%e8%b4%a3%e4%bb%bb%e6%91%86%e6%94%be&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;结果的真正含义：默认路径训练不同的责任摆放&#xA;&lt;/h3&gt;&lt;p&gt;如果只看第一行，你很容易得出一个轻率结论：PHP 会悄悄转换类型，Go 会拦住错误。&lt;/p&gt;&#xA;&lt;p&gt;但第二行马上把这个结论打掉了一半。&lt;/p&gt;&#xA;&lt;p&gt;PHP 开启 &lt;code&gt;strict_types=1&lt;/code&gt; 后，也会在函数调用边界抛出 &lt;code&gt;TypeError&lt;/code&gt;。它不是没有能力前移错误。现代 PHP 也有类型声明、严格模式、静态分析、成熟框架校验，这些都能把一部分问题提前暴露。&lt;/p&gt;&#xA;&lt;p&gt;所以这组实验真正说明：&lt;strong&gt;默认路径会训练不同的责任摆放方式。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;PHP 默认弱类型路径里，输入可以先被转换，再流进业务函数。这个过程在很多场景下很方便，尤其是面向表单、后台管理、快速交付的业务系统。你能很快把请求接住，把字段拿出来，把逻辑跑通。&lt;/p&gt;&#xA;&lt;p&gt;便利的另一面，是边界问题会继续往后走。它可能在业务判断里露头，也可能等到数据库约束、联调日志，甚至某个奇怪用户输入才冒出来。&lt;/p&gt;&#xA;&lt;p&gt;Go 的路径通常更早卡住你。&lt;/p&gt;&#xA;&lt;p&gt;你定义了：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;UserPayload&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;UserID&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;`json:&amp;#34;user_id&amp;#34;`&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Email&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;`json:&amp;#34;email&amp;#34;`&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后输入里的 &lt;code&gt;&amp;quot;42&amp;quot;&lt;/code&gt; 想进 &lt;code&gt;int&lt;/code&gt; 字段，&lt;code&gt;json.Unmarshal&lt;/code&gt; 就会直接返回错误。它还没有进入业务逻辑，还没有查数据库，还没有走到 repository。&lt;/p&gt;&#xA;&lt;p&gt;错误停在了解码边界。&lt;/p&gt;&#xA;&lt;p&gt;这一步改变的是提问顺序。&lt;/p&gt;&#xA;&lt;p&gt;以前我更容易问：&amp;ldquo;这个字段拿到之后怎么兼容？&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;现在我会先问：&amp;ldquo;这个字段允许这样进来吗？&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;语法没变出什么哲学。责任的位置变了。&lt;/p&gt;&#xA;&lt;p&gt;同样的坏输入，在 PHP 默认路径里可能被运行时转换吸收；在 PHP strict 模式里会停在函数调用边界；在 Go 的 struct 解码里会停在输入解析边界。&lt;/p&gt;&#xA;&lt;p&gt;每一种选择都可以成立。&lt;/p&gt;&#xA;&lt;p&gt;但它们训练出的工程习惯不一样。&lt;/p&gt;&#xA;&lt;p&gt;Go 的默认路径更常把类型边界推到解码阶段——你还没写业务逻辑，输入契约就已经在 struct tag 里等着了。&lt;/p&gt;&#xA;&lt;p&gt;你还没写业务逻辑，失败路径就已经开始敲门。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch1-decode-vs-call-boundary.png&#34; alt=&#34;解码边界 vs 函数调用边界&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;三种语言路径的拦截位置：Go 在解码边界、PHP 严格在函数调用边界、PHP 弱类型一路流到业务深处&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;这就是很多 PHP 工程师刚迁到 Go 时那种&amp;quot;不顺手&amp;quot;的来源。&lt;/p&gt;&#xA;&lt;p&gt;不是 Go 难。&lt;/p&gt;&#xA;&lt;p&gt;是它不太允许你把某些决定继续往后推。&lt;/p&gt;&#xA;&lt;p&gt;这个实验只验证了输入错误的暴露位置；后文用 PR 场景和归属矩阵把同一逻辑扩展到错误语义、副作用和并发边界，扩展依据是迁移现场的 review 经验，不是统计结论。&lt;/p&gt;&#xA;&lt;h2 id=&#34;二复杂度没有消失只是换了地方&#34;&gt;&lt;a href=&#34;#%e4%ba%8c%e5%a4%8d%e6%9d%82%e5%ba%a6%e6%b2%a1%e6%9c%89%e6%b6%88%e5%a4%b1%e5%8f%aa%e6%98%af%e6%8d%a2%e4%ba%86%e5%9c%b0%e6%96%b9&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;二、复杂度没有消失，只是换了地方&#xA;&lt;/h2&gt;&lt;p&gt;把这个现象只理解成&amp;quot;Go 更严格&amp;quot;，文章就浅了。&lt;/p&gt;&#xA;&lt;p&gt;严格只是表面。&lt;/p&gt;&#xA;&lt;p&gt;更底层的问题是：系统里的责任总要有人承担。&lt;/p&gt;&#xA;&lt;p&gt;有时是运行时和框架替你兜住，有时是编译器和测试提前拦住。再晚一点，就是代码审查、运维，或者线上事故来收账。&lt;/p&gt;&#xA;&lt;p&gt;语言迁移真正改变的，是这些责任默认被放在哪。&lt;/p&gt;&#xA;&lt;h3 id=&#34;六类责任在两种默认路径上的位移&#34;&gt;&lt;a href=&#34;#%e5%85%ad%e7%b1%bb%e8%b4%a3%e4%bb%bb%e5%9c%a8%e4%b8%a4%e7%a7%8d%e9%bb%98%e8%ae%a4%e8%b7%af%e5%be%84%e4%b8%8a%e7%9a%84%e4%bd%8d%e7%a7%bb&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;六类责任在两种默认路径上的位移&#xA;&lt;/h3&gt;&lt;p&gt;我把后端系统里几类常见的失败路径整理成一张表。这里比较的是典型 PHP-FPM + 框架业务系统与典型 Go HTTP/JSON 服务的常见默认倾向，不代表两种生态的能力上限。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;类型&lt;/th&gt;&#xA;          &lt;th&gt;PHP 常见默认归属&lt;/th&gt;&#xA;          &lt;th&gt;Go 常见默认归属&lt;/th&gt;&#xA;          &lt;th&gt;迁移后的认知变化&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;输入类型错误&lt;/td&gt;&#xA;          &lt;td&gt;请求数组、运行时转换、框架校验&lt;/td&gt;&#xA;          &lt;td&gt;JSON 解码、struct 字段类型、显式校验&lt;/td&gt;&#xA;          &lt;td&gt;先定义输入契约，再写业务逻辑&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;错误处理&lt;/td&gt;&#xA;          &lt;td&gt;异常、中间件、框架统一错误处理&lt;/td&gt;&#xA;          &lt;td&gt;函数返回 &lt;code&gt;error&lt;/code&gt;，调用点逐层决定&lt;/td&gt;&#xA;          &lt;td&gt;失败路径从&amp;quot;兜底机制&amp;quot;变成主流程的一部分&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;空值和缺字段&lt;/td&gt;&#xA;          &lt;td&gt;默认值、helper、运行时 warning&lt;/td&gt;&#xA;          &lt;td&gt;零值、指针、可选字段、校验函数&lt;/td&gt;&#xA;          &lt;td&gt;必须区分&amp;quot;没传&amp;quot;和&amp;quot;传了零值&amp;quot;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;并发共享状态&lt;/td&gt;&#xA;          &lt;td&gt;请求模型下较少显式暴露&lt;/td&gt;&#xA;          &lt;td&gt;goroutine、channel、mutex、context&lt;/td&gt;&#xA;          &lt;td&gt;状态所有权必须提前设计&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;副作用失败&lt;/td&gt;&#xA;          &lt;td&gt;事件、队列、框架插件&lt;/td&gt;&#xA;          &lt;td&gt;显式返回、补偿策略、上下文取消&lt;/td&gt;&#xA;          &lt;td&gt;主流程和副作用的关系必须说清楚&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;团队风格&lt;/td&gt;&#xA;          &lt;td&gt;框架约定、代码规范、review 习惯&lt;/td&gt;&#xA;          &lt;td&gt;gofmt、包边界、接口、显式错误&lt;/td&gt;&#xA;          &lt;td&gt;风格争议减少，接口设计争议增加&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;核心结论：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;输入错误从运行时日志挪到了类型定义和解码阶段&lt;/li&gt;&#xA;&lt;li&gt;失败路径从框架兜底变成你必须逐层决定怎么传&lt;/li&gt;&#xA;&lt;li&gt;并发边界从&amp;quot;请求结束自动清理&amp;quot;变成&amp;quot;你来设计谁拥有这个状态&amp;quot;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch2-complexity-ownership-matrix.png&#34; alt=&#34;复杂度归属矩阵&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;六类责任在 PHP 默认路径和 Go 默认路径之间的位移&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;这张表不是说 PHP 的方式不好，Go 的方式好。&lt;/p&gt;&#xA;&lt;p&gt;它只是把问题摊开：系统里的责任不会因为你换语言就消失。&lt;/p&gt;&#xA;&lt;p&gt;它只会重新分配。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch2-ownership-shift.png&#34; alt=&#34;复杂度承担方位移示意&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;责任从运行时/框架/经验区，迁到类型定义、错误返回、测试、代码审查、并发边界&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;为什么这才是迁移真正的本质&#34;&gt;&lt;a href=&#34;#%e4%b8%ba%e4%bb%80%e4%b9%88%e8%bf%99%e6%89%8d%e6%98%af%e8%bf%81%e7%a7%bb%e7%9c%9f%e6%ad%a3%e7%9a%84%e6%9c%ac%e8%b4%a8&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;为什么这才是迁移真正的本质&#xA;&lt;/h3&gt;&lt;p&gt;PHP 的很多生产效率，来自框架和运行时替你吸收了大量日常工作。&lt;/p&gt;&#xA;&lt;p&gt;表单请求怎么拿，路由怎么接，错误怎么转响应，ORM 怎么映射，异常怎么统一处理，队列怎么挂事件。成熟框架把这些路径铺得很平。你专注写业务时，确实会感觉快。&lt;/p&gt;&#xA;&lt;p&gt;这不是幻觉。&lt;/p&gt;&#xA;&lt;p&gt;这是 PHP 生态长期打磨出来的生产力。&lt;/p&gt;&#xA;&lt;p&gt;但 Go 的默认气质不一样。&lt;/p&gt;&#xA;&lt;p&gt;Go 不太热衷把所有东西都包成一个大框架体验。它更倾向于把各层的职责摊开，让你在代码里写明白。&lt;/p&gt;&#xA;&lt;p&gt;你要传 &lt;code&gt;context.Context&lt;/code&gt;，决定哪些信号往下走。&lt;/p&gt;&#xA;&lt;p&gt;你要处理每一个 &lt;code&gt;error&lt;/code&gt;，决定它在哪里被识别。&lt;/p&gt;&#xA;&lt;p&gt;接口放在哪里、包之间的依赖方向怎么走，这些得你自己定。&lt;/p&gt;&#xA;&lt;p&gt;结构体字段是不是指针、零值语义是什么，也得你自己定。&lt;/p&gt;&#xA;&lt;p&gt;后台 goroutine 怎么退出，退出时谁负责清理——Go 不会帮你做这个决定。&lt;/p&gt;&#xA;&lt;p&gt;这些决定一开始会让人烦。&lt;/p&gt;&#xA;&lt;p&gt;尤其是从 PHP 迁过来时，你会很自然地怀念框架的&amp;quot;顺手&amp;quot;：一个 request 进来，一套中间件跑完，业务函数里拿字段、查模型、保存、返回。很多事不需要你每次都重新表达。&lt;/p&gt;&#xA;&lt;p&gt;但 Go 会把这些事重新摆到你面前。&lt;/p&gt;&#xA;&lt;p&gt;它像是在说：可以继续写，但先说清楚这段代码的责任归属。&lt;/p&gt;&#xA;&lt;p&gt;这也是为什么很多迁移讨论只谈性能会跑偏。&lt;/p&gt;&#xA;&lt;p&gt;性能当然重要。&lt;/p&gt;&#xA;&lt;p&gt;老 PHP 版本进入 EOL、运行模型不适合常驻任务、并发任务不好写、部署形态想统一，这些都可能是迁移动机。旧版本系统迟早会遇到生命周期压力。&lt;/p&gt;&#xA;&lt;p&gt;但如果你把迁移理解成&amp;quot;Go 更快，所以迁&amp;quot;，你会错过更重要的部分。&lt;/p&gt;&#xA;&lt;p&gt;真正会改变团队长期写法的，不是 QPS 提高多少。&lt;/p&gt;&#xA;&lt;p&gt;而是团队开始更早讨论：输入契约是什么，失败路径是什么，副作用算不算主流程，状态归谁拥有，错误要不要被上层识别。&lt;/p&gt;&#xA;&lt;p&gt;这些讨论以前也存在。&lt;/p&gt;&#xA;&lt;p&gt;只是很多时候，它们出现得更晚。&lt;/p&gt;&#xA;&lt;p&gt;到了联调。到了线上日志。到了某个接口偶发失败之后。到了某个统一异常处理把内部错误吞掉之后。&lt;/p&gt;&#xA;&lt;p&gt;迁到 Go 后，它们更容易出现在 PR 里。&lt;/p&gt;&#xA;&lt;p&gt;这就是归属变化的第二层含义：责任不只是换了代码位置，也换了讨论时机。&lt;/p&gt;&#xA;&lt;h2 id=&#34;三真正不适应的是责任边界&#34;&gt;&lt;a href=&#34;#%e4%b8%89%e7%9c%9f%e6%ad%a3%e4%b8%8d%e9%80%82%e5%ba%94%e7%9a%84%e6%98%af%e8%b4%a3%e4%bb%bb%e8%be%b9%e7%95%8c&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;三、真正不适应的是责任边界&#xA;&lt;/h2&gt;&lt;p&gt;为了把这个变化讲具体，我构造一个迁移 PR 的场景。&lt;/p&gt;&#xA;&lt;p&gt;它不是某家公司的真实项目，也不包含真实业务数据。它只是后端迁移里很常见的一类接口：用户资料更新。&lt;/p&gt;&#xA;&lt;p&gt;接口本身很普通。&lt;/p&gt;&#xA;&lt;p&gt;接收 &lt;code&gt;user_id&lt;/code&gt;、&lt;code&gt;email&lt;/code&gt;、&lt;code&gt;nickname&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;更新用户资料。&lt;/p&gt;&#xA;&lt;p&gt;写一条审计日志。&lt;/p&gt;&#xA;&lt;h3 id=&#34;php-版本一条很顺的快路径&#34;&gt;&lt;a href=&#34;#php-%e7%89%88%e6%9c%ac%e4%b8%80%e6%9d%a1%e5%be%88%e9%a1%ba%e7%9a%84%e5%bf%ab%e8%b7%af%e5%be%84&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;PHP 版本：一条很顺的快路径&#xA;&lt;/h3&gt;&lt;p&gt;在 PHP 版本里，这类接口可能长得很顺。&lt;/p&gt;&#xA;&lt;p&gt;请求先进入框架的 request 对象，字段从数组里取出来，必要时做一下类型转换。校验失败丢给框架规则，业务异常往上抛，统一错误处理中间件转成响应。&lt;/p&gt;&#xA;&lt;p&gt;你真正关注的是主路径：找到用户，改字段，保存，返回。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$user&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$repo&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;user_id&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$user&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$repo&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;save&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;成熟项目里也可能通过 FormRequest/DTO/中间件把输入校验和错误处理显式化；这里只截取最常见的快路径写法。&lt;/p&gt;&#xA;&lt;p&gt;这段代码不一定差。&lt;/p&gt;&#xA;&lt;p&gt;在一个成熟框架里，&lt;code&gt;find&lt;/code&gt; 找不到可以抛异常，ORM 保存失败可以抛异常，外层有统一错误处理中间件。只要团队约定清楚，它也能工作得很好。&lt;/p&gt;&#xA;&lt;h3 id=&#34;go-版本失败路径一行一行站出来&#34;&gt;&lt;a href=&#34;#go-%e7%89%88%e6%9c%ac%e5%a4%b1%e8%b4%a5%e8%b7%af%e5%be%84%e4%b8%80%e8%a1%8c%e4%b8%80%e8%a1%8c%e7%ab%99%e5%87%ba%e6%9d%a5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;Go 版本：失败路径一行一行站出来&#xA;&lt;/h3&gt;&lt;p&gt;迁到 Go 之后，PR 评审的关注点会变。&lt;/p&gt;&#xA;&lt;p&gt;Reviewer 看到的第一件事，可能不是业务逻辑，而是请求结构体。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;UpdateProfileRequest&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;UserID&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;   &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int64&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;`json:&amp;#34;user_id&amp;#34;`&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Email&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;`json:&amp;#34;email&amp;#34;`&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Nickname&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;`json:&amp;#34;nickname&amp;#34;`&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;他会问：如果前端把 &lt;code&gt;user_id&lt;/code&gt; 传成字符串，这个请求应该失败，还是兼容？&lt;/p&gt;&#xA;&lt;p&gt;这个问题很小。&lt;/p&gt;&#xA;&lt;p&gt;小到你在 PHP 里可能根本不会专门讨论。&lt;/p&gt;&#xA;&lt;p&gt;因为字符串 &lt;code&gt;&amp;quot;42&amp;quot;&lt;/code&gt; 进入业务函数之后，很多时候还能转成整数。框架校验也可以兜住。数据库层也可能最终拒绝。系统不是一定会坏。&lt;/p&gt;&#xA;&lt;p&gt;但 Go 的结构体会把问题提前摆出来：你必须决定输入契约，而不是先让数据流进去。&lt;/p&gt;&#xA;&lt;p&gt;第一层责任就在这里。&lt;/p&gt;&#xA;&lt;p&gt;问题不是&amp;quot;字段怎么拿&amp;quot;，而是&amp;quot;谁有权决定这个字段算合法&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;接着是失败路径。&lt;/p&gt;&#xA;&lt;p&gt;迁到 Go 之后，失败路径会一行一行站出来。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;repo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;UserID&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Email&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Email&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Nickname&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;req&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Nickname&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;repo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Save&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;很多人刚写 Go 时，会觉得这只是样板代码。&lt;/p&gt;&#xA;&lt;p&gt;确实，它有样板感。&lt;/p&gt;&#xA;&lt;p&gt;但如果你只把它看成样板，就会错过背后的强迫动作：每一步失败都必须经过你的手。&lt;/p&gt;&#xA;&lt;p&gt;你要决定 &lt;code&gt;Find&lt;/code&gt; 的 not found 是业务错误，还是系统错误。&lt;/p&gt;&#xA;&lt;p&gt;你要决定 DB timeout 要不要包装成可重试错误。&lt;/p&gt;&#xA;&lt;p&gt;你要决定权限不匹配在哪里拦。&lt;/p&gt;&#xA;&lt;p&gt;你要决定上层看到的是一个可识别的错误类型，还是一段已经丢失语义的字符串。&lt;/p&gt;&#xA;&lt;p&gt;比如 not found 是业务期望的失败，通常封装为可识别错误类型，上层返回 404；permission denied 必须让上层看到，触发 401/403 区分；DB timeout 归为可重试错误，但要区分读操作（安全重试）、幂等写（可重试）和非幂等写（不能重试，需要幂等键或补偿）。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch3-error-decision-doors.png&#34; alt=&#34;失败路径每一步都是决定&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;四类失败的归属决策：not found / 权限拒绝 / DB 超时 / 非幂等写&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;失败不再只是&amp;quot;抛出去让框架处理&amp;quot;。它变成了业务流程的一部分。&lt;/p&gt;&#xA;&lt;h3 id=&#34;副作用归谁负责审计日志失败要不要让主流程失败&#34;&gt;&lt;a href=&#34;#%e5%89%af%e4%bd%9c%e7%94%a8%e5%bd%92%e8%b0%81%e8%b4%9f%e8%b4%a3%e5%ae%a1%e8%ae%a1%e6%97%a5%e5%bf%97%e5%a4%b1%e8%b4%a5%e8%a6%81%e4%b8%8d%e8%a6%81%e8%ae%a9%e4%b8%bb%e6%b5%81%e7%a8%8b%e5%a4%b1%e8%b4%a5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;副作用归谁负责：审计日志失败要不要让主流程失败&#xA;&lt;/h3&gt;&lt;p&gt;再往后看副作用。&lt;/p&gt;&#xA;&lt;p&gt;用户资料更新成功后，要写审计日志。PHP 版本里，这一步可能在框架事件里，也可能在 service 里顺手调用一下。失败了怎么办？有些系统会忽略，有些系统会统一异常，有些系统会丢到队列重试。&lt;/p&gt;&#xA;&lt;p&gt;迁到 Go 的 PR 里，reviewer 很可能会问：&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;审计日志失败，要不要让用户资料更新失败？如果不失败，怎么补偿？如果失败，用户看到什么？&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;这个问题没有语言标准答案。&lt;/p&gt;&#xA;&lt;p&gt;Go 不会替你决定。&lt;/p&gt;&#xA;&lt;p&gt;但 Go 的写法会让你更难假装这个边界不存在。&lt;/p&gt;&#xA;&lt;p&gt;你得写出函数签名和返回值——这不是风格问题，是接口契约的一部分。&lt;/p&gt;&#xA;&lt;p&gt;context 取消时，主流程和副作用谁先停？审计日志失败要不要进入错误链？这些问题以前可以不回答，现在写不出返回值就编译不过。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch3-side-effect-decision.png&#34; alt=&#34;副作用归属决策三岔路&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;主流程成功、审计日志失败的三种处理路径：忽略 / 补偿重试 / 主流程回滚&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;这就是我说的&amp;quot;复杂度归属&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;以前这些决定可能藏在框架生命周期里，藏在异常链里，藏在团队某个资深同事的经验里。迁到 Go 之后，它更容易被放到 PR 讨论、接口签名和测试里。&lt;/p&gt;&#xA;&lt;p&gt;它没有变少。&lt;/p&gt;&#xA;&lt;p&gt;它变得更早、更硬、更可见。&lt;/p&gt;&#xA;&lt;p&gt;这会让人不舒服。&lt;/p&gt;&#xA;&lt;p&gt;但这种不舒服，恰恰是迁移真正发生的地方。&lt;/p&gt;&#xA;&lt;p&gt;如果你只是把 PHP controller 翻译成 Go handler，把 &lt;code&gt;$request&lt;/code&gt; 改成 &lt;code&gt;req&lt;/code&gt;，把 &lt;code&gt;throw&lt;/code&gt; 改成 &lt;code&gt;return err&lt;/code&gt;，那只是写法迁移。&lt;/p&gt;&#xA;&lt;p&gt;当你开始重新问——输入契约归谁，失败路径归谁，副作用归属是什么——这三个审查标签就是 PR 里真正在讨论的东西。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch3-pr-review-boundaries.png&#34; alt=&#34;迁移 PR 评审桌面&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;PR 评审现场：左侧 PHP 框架兜底，右侧 Go struct、error、context，中间标注三类责任归属&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;四php-教会我的东西go-替代不了&#34;&gt;&lt;a href=&#34;#%e5%9b%9bphp-%e6%95%99%e4%bc%9a%e6%88%91%e7%9a%84%e4%b8%9c%e8%a5%bfgo-%e6%9b%bf%e4%bb%a3%e4%b8%8d%e4%ba%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;四、PHP 教会我的东西，Go 替代不了&#xA;&lt;/h2&gt;&lt;p&gt;写到这里，很容易滑向另一个极端：既然 Go 把失败路径提前暴露，那 PHP 的经验是不是就该被丢掉？&lt;/p&gt;&#xA;&lt;p&gt;不该。&lt;/p&gt;&#xA;&lt;p&gt;这是我最想避免的误读。&lt;/p&gt;&#xA;&lt;h3 id=&#34;业务敏感度才是-php-训练出来的真本事&#34;&gt;&lt;a href=&#34;#%e4%b8%9a%e5%8a%a1%e6%95%8f%e6%84%9f%e5%ba%a6%e6%89%8d%e6%98%af-php-%e8%ae%ad%e7%bb%83%e5%87%ba%e6%9d%a5%e7%9a%84%e7%9c%9f%e6%9c%ac%e4%ba%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;业务敏感度才是 PHP 训练出来的真本事&#xA;&lt;/h3&gt;&lt;p&gt;PHP 工程师迁到 Go，最不应该丢掉的，不是某些语法习惯，而是业务敏感度。&lt;/p&gt;&#xA;&lt;p&gt;很多 PHP 系统之所以能长期服务业务，不是因为代码天然优雅，而是因为它们非常贴近业务现场。后台页面、运营工具、表单流程、用户状态、订单规则、权限判断，这些东西变化快、细节碎、需求多。&lt;/p&gt;&#xA;&lt;p&gt;PHP 生态很擅长把这类需求迅速变成可用系统。&lt;/p&gt;&#xA;&lt;p&gt;框架约定多，开发路径短，业务同学改一个字段、加一个状态、调一条规则，工程师往往能很快响应。&lt;/p&gt;&#xA;&lt;p&gt;这种能力不是低级能力。&lt;/p&gt;&#xA;&lt;p&gt;它是业务系统里非常贵的能力。&lt;/p&gt;&#xA;&lt;p&gt;迁到 Go 之后，容易发生一个反向误区：代码更规整了，抽象更多了，接口更清楚了，但业务反馈变慢了。&lt;/p&gt;&#xA;&lt;p&gt;你开始为每个输入设计类型。&lt;/p&gt;&#xA;&lt;p&gt;开始为每个错误包装语义。&lt;/p&gt;&#xA;&lt;p&gt;开始讨论包结构、接口位置、依赖方向。&lt;/p&gt;&#xA;&lt;p&gt;这些都重要。&lt;/p&gt;&#xA;&lt;p&gt;但如果每一次小业务变化都被你处理成一场架构设计会议，迁移就走偏了。&lt;/p&gt;&#xA;&lt;h3 id=&#34;一场关于-user_id-兼容性的半小时讨论&#34;&gt;&lt;a href=&#34;#%e4%b8%80%e5%9c%ba%e5%85%b3%e4%ba%8e-user_id-%e5%85%bc%e5%ae%b9%e6%80%a7%e7%9a%84%e5%8d%8a%e5%b0%8f%e6%97%b6%e8%ae%a8%e8%ae%ba&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一场关于 user_id 兼容性的半小时讨论&#xA;&lt;/h3&gt;&lt;p&gt;举一个我见过的典型纠结：一个内部运营后台接口，需要兼容 &lt;code&gt;user_id&lt;/code&gt; 既传整数也传字符串——因为前端不同模块的传参习惯不一样。PHP 里加一个类型判断两行代码搞定。到了 Go 的 PR 里，有人提议用 &lt;code&gt;json.Number&lt;/code&gt;，有人想定义 &lt;code&gt;FlexibleInt&lt;/code&gt; 类型，有人说应该让前端统一。讨论了半小时。&lt;/p&gt;&#xA;&lt;p&gt;这种场景下，PHP 训练出来的判断就很有价值：这是一个还在变的内部接口，先兼容住，等前端统一后再收紧。不要把还没稳定的业务规则过早冻成一套漂亮抽象。&lt;/p&gt;&#xA;&lt;p&gt;Go 不反对这种判断。&lt;/p&gt;&#xA;&lt;p&gt;Go 只是要求你把&amp;quot;兼容&amp;quot;也说清楚——用 &lt;code&gt;json.RawMessage&lt;/code&gt; 先接，文档里标注为临时方案，加个 TODO。&lt;/p&gt;&#xA;&lt;p&gt;PHP 让你对业务变化保持敏感。&lt;/p&gt;&#xA;&lt;p&gt;Go 让你对代码责任保持清醒。&lt;/p&gt;&#xA;&lt;p&gt;一个只靠框架兜底的人，可能把问题推迟到线上。&lt;/p&gt;&#xA;&lt;p&gt;一个只追求显式设计的人，可能把业务变化冻死在抽象里。&lt;/p&gt;&#xA;&lt;p&gt;更有价值的迁移，是把两种能力接起来。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch4-business-instinct-boundary-awareness.png&#34; alt=&#34;业务敏感度与边界意识的互补图&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;左栏：PHP 训练出的业务敏感度；右栏：Go 逼出的代码责任意识；中间：迁移后的判断接口&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;这也是为什么我不喜欢&amp;quot;从 PHP 升级到 Go&amp;quot;这种说法。&lt;/p&gt;&#xA;&lt;p&gt;它听起来像技术身份升级。&lt;/p&gt;&#xA;&lt;p&gt;但真实迁移更像工程判断升级：你开始知道哪些失败路径该提前暴露，哪些规则可以暂时留在业务层，哪些决定必须交给测试和审查。&lt;/p&gt;&#xA;&lt;h2 id=&#34;五迁移完成的标志不是代码跑起来&#34;&gt;&lt;a href=&#34;#%e4%ba%94%e8%bf%81%e7%a7%bb%e5%ae%8c%e6%88%90%e7%9a%84%e6%a0%87%e5%bf%97%e4%b8%8d%e6%98%af%e4%bb%a3%e7%a0%81%e8%b7%91%e8%b5%b7%e6%9d%a5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;五、迁移完成的标志，不是代码跑起来&#xA;&lt;/h2&gt;&lt;p&gt;从 PHP 到 Go，最容易完成的是语法迁移。&lt;/p&gt;&#xA;&lt;p&gt;变量名改了。目录结构改了。服务能编译。接口能返回。部署方式也换成了二进制和容器。&lt;/p&gt;&#xA;&lt;p&gt;这些都重要，但它们不是迁移真正完成的标志。&lt;/p&gt;&#xA;&lt;p&gt;真正的标志，是你开始用新的方式判断责任该放在哪里。&lt;/p&gt;&#xA;&lt;p&gt;拿到一个输入错误，你不再只问&amp;quot;怎么兼容过去&amp;quot;，而是问&amp;quot;这个输入契约该不该允许&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;遇到一个失败路径，第一反应变了——不是想&amp;quot;谁来 catch&amp;quot;，而是想调用方能不能识别这个错误、要不要区分它。&lt;/p&gt;&#xA;&lt;p&gt;副作用的设计也是。以前顺手写一行调用就完事，现在你得先想清楚：它挂了，主流程要不要跟着挂。&lt;/p&gt;&#xA;&lt;p&gt;共享状态更明显。问题从&amp;quot;现在有没有并发&amp;quot;变成了&amp;quot;未来谁拥有它，退出时谁清理&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;这些问题没有 PHP 答案，也没有 Go 答案。&lt;/p&gt;&#xA;&lt;p&gt;它们是工程答案。&lt;/p&gt;&#xA;&lt;p&gt;Go 只是更频繁地把它们推到你眼前。&lt;/p&gt;&#xA;&lt;h3 id=&#34;一张可以贴在工位的判断表&#34;&gt;&lt;a href=&#34;#%e4%b8%80%e5%bc%a0%e5%8f%af%e4%bb%a5%e8%b4%b4%e5%9c%a8%e5%b7%a5%e4%bd%8d%e7%9a%84%e5%88%a4%e6%96%ad%e8%a1%a8&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一张可以贴在工位的判断表&#xA;&lt;/h3&gt;&lt;p&gt;所以，如果要给这场迁移留一张判断表，我会这么写：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;迁移表象&lt;/th&gt;&#xA;          &lt;th&gt;更深一层的问题&lt;/th&gt;&#xA;          &lt;th&gt;你可以问自己的检查问题&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;把数组换成 struct&lt;/td&gt;&#xA;          &lt;td&gt;输入字段是否被明确建模&lt;/td&gt;&#xA;          &lt;td&gt;解码阶段能拦住多少种非法输入？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;把异常换成 error&lt;/td&gt;&#xA;          &lt;td&gt;失败路径是否需要被上层理解&lt;/td&gt;&#xA;          &lt;td&gt;调用方是否需要区分 not found 和 timeout？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;把框架事件换成显式调用&lt;/td&gt;&#xA;          &lt;td&gt;副作用是否属于主流程&lt;/td&gt;&#xA;          &lt;td&gt;副作用失败时主流程回滚、补偿还是忽略？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;把请求级代码换成常驻服务&lt;/td&gt;&#xA;          &lt;td&gt;状态生命周期变长&lt;/td&gt;&#xA;          &lt;td&gt;谁拥有这份共享状态，退出时谁负责清理？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;把框架约定换成包边界&lt;/td&gt;&#xA;          &lt;td&gt;团队协作方式变化&lt;/td&gt;&#xA;          &lt;td&gt;哪些约定写进代码，哪些留给 review comment？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;可以用三类证据自检迁移完成度：解码/校验单测覆盖了关键路径、错误类型断言测试能区分不同失败语义、副作用失败有对应的补偿或降级测试。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch5-migration-completion-checklist.png&#34; alt=&#34;迁移完成判断框架&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;五个检查点：输入契约、错误语义、副作用归属、状态所有权、团队约定&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;判断框架是问题清单，但回答这些问题不能靠口头声明。&lt;/p&gt;&#xA;&lt;p&gt;每一项都需要有可被验证的证据，不是&amp;quot;我们觉得做到了&amp;quot;，而是&amp;quot;这里有一个测试证明了它&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;测试不是补丁，是迁移完成度的证书。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/php-to-go-migration/ch5-three-evidence-self-check.png&#34; alt=&#34;三类证据自检卡&#34; loading=&#34;lazy&#34;&gt;&#xA;&lt;em&gt;用三类测试证据兑现迁移完成度：解码单测 / 错误类型断言 / 副作用补偿降级&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;这张表比&amp;quot;PHP vs Go 性能对比&amp;quot;更接近迁移的本质。&lt;/p&gt;&#xA;&lt;p&gt;性能可能是迁移理由之一。生命周期、部署形态、并发模型、团队招聘、生态统一，也都可能是理由。&lt;/p&gt;&#xA;&lt;p&gt;但这些理由只能解释&amp;quot;为什么要迁&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;它们不能解释&amp;quot;迁完之后你变成了什么样的工程师&amp;quot;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;把-php-经验和-go-责任意识接起来&#34;&gt;&lt;a href=&#34;#%e6%8a%8a-php-%e7%bb%8f%e9%aa%8c%e5%92%8c-go-%e8%b4%a3%e4%bb%bb%e6%84%8f%e8%af%86%e6%8e%a5%e8%b5%b7%e6%9d%a5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;把 PHP 经验和 Go 责任意识接起来&#xA;&lt;/h3&gt;&lt;p&gt;以前你相信框架能兜住很多事情——后来你发现，有些事情应该更早写进类型、更早出现在测试里、更早摆到 PR 讨论中。&lt;/p&gt;&#xA;&lt;p&gt;但反过来也一样。&lt;/p&gt;&#xA;&lt;p&gt;从 PHP 里学到的快速交付、业务敏感、对变化的耐受，不应该被 Go 的规整感洗掉。&lt;/p&gt;&#xA;&lt;p&gt;最好的迁移，不是把旧经验清空。&lt;/p&gt;&#xA;&lt;p&gt;而是把旧经验重新安放。&lt;/p&gt;&#xA;&lt;p&gt;让 PHP 教你的业务直觉继续存在。让 Go 逼出来的责任意识变成新的底盘。&lt;/p&gt;&#xA;&lt;p&gt;迁移有没有真正发生，不看你会不会写 &lt;code&gt;if err != nil&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;看你有没有开始问：这份责任，应该由谁承担，应该在哪个阶段暴露，应该用什么证据证明它被处理过。&lt;/p&gt;&#xA;&lt;p&gt;如果这个问题开始变成你的本能，迁移才算真的完成。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;a id=&#34;evidence-repo&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;本文实验代码、PHP/Go 原始运行输出见仓库：&#xA;👉 &lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/php-to-go-migration&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;zhiyulab-evidence/php-to-go-migration&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/php-to-go-migration&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item></channel>
</rss>
