<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Go on 山行小记</title>
    <link>https://ruijingan.pages.dev/tags/go/</link>
    <description>Recent content in Go on 山行小记</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <copyright>@2025</copyright>
    <lastBuildDate>Thu, 17 Apr 2025 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://ruijingan.pages.dev/tags/go/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>gRPC 在生产环境用了两年之后</title>
      <link>https://ruijingan.pages.dev/posts/2025/grpc-two-years/</link>
      <pubDate>Thu, 17 Apr 2025 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2025/grpc-two-years/</guid>
      <description>&lt;p&gt;内部服务全面从 HTTP/JSON 切到 gRPC 两年了，当初写迁移方案的人里还在组的只剩我一个。趁着记忆还热，把两年里沉淀下来的经验写一篇，算是给这套选型做个中期复盘。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;选对了什么：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;强类型契约的价值被严重低估了。&lt;/strong&gt; proto 文件 + 代码生成让接口变更的 review 变得极其直观，破坏性变更在 CI 阶段就能拦住。两年里因为接口不一致引发的线上问题，是零。以前 HTTP/JSON 时代一年总有那么两三次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;grpc-go 的拦截器生态很好用。&lt;/strong&gt; 我们把日志、trace、限流、panic recovery 全部收敛在 server 端拦截器里，业务代码完全不用感知。这是迁移以来工程体验提升最大的部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;踩过的坑，按疼痛程度排序：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;默认负载均衡是&amp;quot;假&amp;quot;的。&lt;/strong&gt; gRPC 客户端和一条 HTTP/2 长连接通信，如果用普通的 DNS 解析 + 单连接，所有请求会打到同一个后端实例上。必须用 &lt;code&gt;dns://&lt;/code&gt; resolver 加上客户端的 round-robin，或者上 xDS。这个问题在我们灰度的时候表现为&amp;quot;新版本流量永远打不到新实例&amp;quot;，排查了整整两天。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;keepalive 参数两端要协商好。&lt;/strong&gt; 中间一层的 LVS 把&amp;quot;空闲&amp;quot;的 gRPC 连接静默掐掉，客户端还以为连接健在，请求 timeout 谷底式抖动。ping 间隔改到小于 LVS 的空闲超时之后解决。这种网络设备层的问题文档里不会写，只能靠抓包。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deadline 必须全链路传递。&lt;/strong&gt; 没传 context 的下游超时设置，等于没有超时。我们后来在 lint 里加了规则，gRPC 调用不传 ctx 直接报错。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;一个想法&lt;/strong&gt;：gRPC 的成熟度曲线是陡峭的——入门半天，但进入生产要补的课（连接管理、负载均衡、可观测性）是一整个学期。如果重来一次，小团队内部系统我还是会先推荐 HTTP/JSON，等服务和调用量到了一定规模再切 gRPC，收益才盖得住成本。&lt;/p&gt;</description>
    </item>
    <item>
      <title>手撸一个简易分布式定时任务的记录</title>
      <link>https://ruijingan.pages.dev/posts/2024/distributed-cron/</link>
      <pubDate>Tue, 03 Dec 2024 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2024/distributed-cron/</guid>
      <description>&lt;p&gt;公司内部系统有个痛点：十几个 Go 服务各自跑着 cron，日志、监控、去重各玩各的，出了问题要去十几个服务里翻。申请专项做统一调度被拒了（理由：收益不明确），于是用了两个月的晚间时间自己撸了个最小可用版本，先在自己负责的服务里跑起来，跑通再说。这篇记录核心设计。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需求收敛。&lt;/strong&gt; 首先想清楚不做什么：不做秒级调度（cron 表达式到分钟足够）、不做依赖编排（任务依赖用上线时间错开解决）、不做可视化界面（CLI + 配置文件够用）。砍完之后剩下的核心就三件事：&lt;strong&gt;时间触发、任务分发、不重复执行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键设计是抢锁。&lt;/strong&gt; 多实例部署下同一个任务只能有一个实例执行，我们没有引入额外的调度中心，而是直接用 MySQL 的行锁：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- t_job 表，每分钟由各实例尝试 CAS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;UPDATE&lt;/span&gt; job_lock
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SET&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;owner&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt;, locked_until &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; NOW() &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; INTERVAL &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;MINUTE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WHERE&lt;/span&gt; job_name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;AND&lt;/span&gt; (locked_until &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; NOW() &lt;span style=&#34;color:#66d9ef&#34;&gt;OR&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;owner&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;抢到锁的实例负责执行并把心跳写进去，锁过期自然释放。整个方案只依赖已经有的 MySQL，没有新组件。有人会问为什么不用 Redis：因为任务幂等性出错的代价是重复扣库存，而 MySQL 事务的可靠性在我们团队是经过验证的，不熟的东西不引入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分片是第二个关键。&lt;/strong&gt; 单机任务量上来之后，把任务按 &lt;code&gt;jobName 的 hash % 实例数&lt;/code&gt; 分片，每个实例只认领自己的那份。简单，但配合上面的锁，二十万级任务/天的量完全够。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩的坑&lt;/strong&gt;：时钟漂移。有两台机器的 NTP 没配好，相差 40 秒，导致任务在整点附近被执行了两次。锁的有效期改成大于任务最长执行时间 + 漂移容忍之后解决。现在新机器进集群先校时，成了硬性流程。&lt;/p&gt;
&lt;p&gt;现在这个调度器稳定跑了三个月，另外两个组也接进来了。回头看，&amp;ldquo;先做一个丑但能跑的版本，让收益自己说话&amp;quot;这个策略是对的——下个季度统一调度的专项预算，批了。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Go 泛型用了一年，说说真实感受</title>
      <link>https://ruijingan.pages.dev/posts/2024/go-generics-one-year/</link>
      <pubDate>Thu, 09 May 2024 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2024/go-generics-one-year/</guid>
      <description>&lt;p&gt;Go 1.18 出泛型的时候，团队讨论过要不要用，当时的结论是观望。去年初我们把最低版本升到 1.21 之后，终于把泛型放进了代码规范，允许使用。一年下来，谈谈真实感受。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;泛型真正帮到我们的地方，比想象中窄。&lt;/strong&gt; 一年下来高频使用的场景其实就三类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;容器类工具函数。&lt;code&gt;Map&lt;/code&gt;、&lt;code&gt;Filter&lt;/code&gt;、&lt;code&gt;Contains&lt;/code&gt; 这些，以前用 &lt;code&gt;interface{}&lt;/code&gt; + 反射写，又慢又不安全，现在干净多了。&lt;/li&gt;
&lt;li&gt;类型安全的并发原语。我们自己封了个 &lt;code&gt;sync.Map&lt;/code&gt; 的泛型包装，key 和 value 的类型在编译期就确定，用起来踏实。&lt;/li&gt;
&lt;li&gt;数据访问层的通用分页查询，一个 &lt;code&gt;func Query[T any](...)&lt;/code&gt; 替掉了以前代码生成器吐出来的几十行重复代码。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;但有些地方我们明确禁止用泛型：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;业务结构体不搞泛型。&lt;/strong&gt; &lt;code&gt;Service[T]&lt;/code&gt;、&lt;code&gt;Repo[T]&lt;/code&gt; 这种&amp;quot;框架感&amp;quot;的东西一旦放出来，业务代码的可读性会断崖式下跌。领域模型就该是具体的，&lt;code&gt;UserRepo&lt;/code&gt; 比 &lt;code&gt;Repo[User]&lt;/code&gt; 好读得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能用接口解决的不要用泛型。&lt;/strong&gt; 泛型和接口最大的区别是：接口是运行时多态，泛型是编译期展开。大部分业务场景需要的是前者。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个观察：团队里泛型使用量最大的，是以前用 &lt;code&gt;interface{}&lt;/code&gt; 最多的那批代码。这说明泛型在 Go 里解决的主要不是&amp;quot;表达力&amp;quot;问题，而是&amp;quot;以前只能靠反射硬扛&amp;quot;的那部分代码的类型安全问题。从这个角度看，泛型是改良，不是革命。&lt;/p&gt;
&lt;p&gt;最后记录一个实用主义的心得：升级到 1.21 之后 &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; 内置函数直接删掉了我们自己写的工具函数，这种小快乐积累起来，也是升级的意义。&lt;/p&gt;</description>
    </item>
    <item>
      <title>GORM 用了两年后的一些反思</title>
      <link>https://ruijingan.pages.dev/posts/2023/gorm-reflection/</link>
      <pubDate>Wed, 08 Mar 2023 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2023/gorm-reflection/</guid>
      <description>&lt;p&gt;用 Go 之后一直拿 GORM 当主力 ORM，两年下来项目里大大小小的查询都过过手，这篇是阶段性的反思，有一些想法可能和主流意见不一致。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ORM 用得最爽的地方不是 CRUD，是迁移和事务。&lt;/strong&gt; AutoMigrate 在开发阶段确实是生产力，事务的 callback 风格也比手写 begin/commit/rollback 干净。这两块我没打算退回裸写 SQL。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但我现在很后悔的几件事：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;早期放任了链式调用的滥用。&lt;/strong&gt; 业务代码里到处是 &lt;code&gt;db.Where().Where().Joins().Preload()&lt;/code&gt; 长链，SQL 长什么样全靠脑补。有一次一个 Preload 链带出来三千行数据，接口直接超时。现在我们要求超过两个条件的查询一律下沉到 SQL 文件（用 sqlc 管理），ORM 只处理简单的单表操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;First&lt;/code&gt; 和 &lt;code&gt;Take&lt;/code&gt; 的区别踩过坑。&lt;/strong&gt; &lt;code&gt;First&lt;/code&gt; 按主键排序，表大了以后 where 条件没走索引的话性能很难看。我的原则变成了：能确定唯一性的用 &lt;code&gt;Take&lt;/code&gt;，别让 ORM 替你排序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;hook 里有业务逻辑是灾难。&lt;/strong&gt; 前任同事在 &lt;code&gt;AfterUpdate&lt;/code&gt; 里发了 MQ 消息，某天这个逻辑挂了导致所有用户更新接口静默失败，排查了一晚上才找到。现在我们规定：GORM 的 hook 里只允许写与数据库直接相关的逻辑，业务副作用一律上移到 service 层。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;还没想清楚的：&lt;/strong&gt; 要不要全面转向 sqlx 或 sqlc。团队里年轻的同事喜欢 ORM 的省事，老一些的觉得 SQL 可见性更重要。目前的项目状态是&amp;quot;两者混用&amp;quot;，边界靠 code review 人的自觉维持——这不健康，但也没烂到需要立刻改的程度。&lt;/p&gt;
&lt;p&gt;技术选型这种事，最诚实的答案往往是&amp;quot;当前规模下都行&amp;quot;。真正的问题从来不是工具，是什么时候该承认现在的用法已经不对了。&lt;/p&gt;</description>
    </item>
    <item>
      <title>用 Go 重写了公司的推送网关</title>
      <link>https://ruijingan.pages.dev/posts/2022/push-gateway-rewrite/</link>
      <pubDate>Thu, 27 Oct 2022 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2022/push-gateway-rewrite/</guid>
      <description>&lt;p&gt;折腾了三个月的事终于上线了：把老的推送网关从 Java 迁到了 Go。这篇记录一下过程和数据，也算给自己一个交代。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么要迁。&lt;/strong&gt; 老网关是五年前写的，Netty + 一堆历史包袱，单机维持 5 万长连接就很吃力，每次大促前都要提前扩容到 20 台机器，还经常半夜被告警叫起来。不能优雅重启是压死骆驼的最后一根稻草：每次发版都断推送几分钟，运营那边意见很大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;新架构。&lt;/strong&gt; 接入层用 Gnet 做自定义协议解析（老客户端协议不能动，这点很烦），业务逻辑层独立部署，中间用 NATS 解耦。连接状态存 Redis。选 Gnet 而不是直接用标准库 net，是因为老协议里有粘包处理和一些奇怪的字节操作，想要更细的控制粒度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;效果数据&lt;/strong&gt;（灰度一周后的均值）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单机长连接：5 万 -&amp;gt; 22 万，内存占用反而从 8G 降到 3G；&lt;/li&gt;
&lt;li&gt;发版不再断连：连接迁移方案虽然土（客户端自动重连 + 状态外置），但实测断连窗口小于 2 秒；&lt;/li&gt;
&lt;li&gt;机器数：大促预留从 20 台降到 6 台。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;goroutine 泄漏的教训这次也吃了个现场版：压测到 30 万连接的时候内存突然起飞，pprof 看下去是心跳超时后的清理 goroutine 阻塞在往一个满载的 channel 里写。改成 &lt;code&gt;select&lt;/code&gt; 加 default 分支，接受偶尔的清理延迟，问题消失。&lt;strong&gt;在连接数这个量级上，任何&amp;quot;看起来没关系&amp;quot;的分配都要重新审视&lt;/strong&gt;，一个连接 2KB 的额外开销就是 600MB。&lt;/p&gt;
&lt;p&gt;写 Java 的老同事问我是不是 Go 天生就这么快。我的回答是：语言贡献了一部分，但更大头是老系统这些年积累的暗坑全被我们重新设计掉了。重写永远比改造爽，因为你是带着答案去写题的。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Go 项目里 error 到底该怎么处理</title>
      <link>https://ruijingan.pages.dev/posts/2022/go-error-handling/</link>
      <pubDate>Mon, 11 Apr 2022 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2022/go-error-handling/</guid>
      <description>&lt;p&gt;团队接了一个新项目，定代码规范的时候在 error 处理上吵了整整一个下午。这篇文章整理一下我们最终达成的共识，以及为什么。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先说反模式。&lt;/strong&gt; 常见的两种流派都有问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;warp 派&amp;rdquo;：每一层都 &lt;code&gt;fmt.Errorf(&amp;quot;xxx: %w&amp;quot;, err)&lt;/code&gt; 包一层，最后日志里出现五行重复的 &lt;code&gt;query user: query user: query user: record not found&lt;/code&gt;。信息量为零，日志长度翻倍。&lt;/li&gt;
&lt;li&gt;&amp;ldquo;裸传派&amp;rdquo;：err 原样往上传，顶层打日志的时候只知道出错了，不知道错在哪个业务环节。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;我们现在的做法&lt;/strong&gt;，说起来很朴素：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;只有&amp;quot;当前层新增了语义&amp;quot;的时候才包装。&lt;/strong&gt; 比如数据层返回 &lt;code&gt;record not found&lt;/code&gt;，到 service 层要变成 &lt;code&gt;ErrUserNotFound&lt;/code&gt;，这是有语义提升的，值得包。纯粹透传的调用直接 &lt;code&gt;return err&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;哨兵错误 + errors.Is 为主，自定义错误类型按需用。&lt;/strong&gt; 90% 的场景一个 &lt;code&gt;var ErrXxx = errors.New(&amp;quot;xxx&amp;quot;)&lt;/code&gt; 就够了，别上来就定义结构体。真需要携带上下文（比如重试次数、限流详情）再上类型断言 &lt;code&gt;errors.As&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打日志和返回 error 只做一件事。&lt;/strong&gt; 这是我们的铁律：中间层不要既打日志又往上返回，否则同一次错误在日志里出现 N 遍，定位的时候全是噪音。要么就地处理并打日志（返回 nil），要么不打日志继续上传。顶层（通常是 HTTP handler 或 cron 入口）统一打一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三方库的 error 要在边界收口。&lt;/strong&gt; 库的 error 类型不该泄漏到业务代码里，否则哪天换库就是全项目翻新。在 repository 层把 gorm 的错误翻译成领域错误，业务层只认识领域错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;另外一个和 error 无关但经常一起争论的问题：panic 什么时候用。我们的边界也很清晰——只在程序初始化阶段（连不上数据库这种&amp;quot;起不来就没意义&amp;quot;的场景）panic，运行期的错误一律返回。以及一个共识：&lt;code&gt;recover&lt;/code&gt; 只允许出现在最顶层的中间件里，业务代码里出现 recover 一律打回重写。&lt;/p&gt;</description>
    </item>
    <item>
      <title>从线程池到 goroutine：并发思维的转变</title>
      <link>https://ruijingan.pages.dev/posts/2021/goroutine-mindset/</link>
      <pubDate>Thu, 23 Sep 2021 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2021/goroutine-mindset/</guid>
      <description>&lt;p&gt;写 Go 半年了，今天想聊聊我理解中客户端和 Go 在并发上最本质的差别。标题有点大，内容是务实的。&lt;/p&gt;
&lt;p&gt;做移动端的时候，并发是个&amp;quot;危险操作&amp;quot;：线程宝贵，要线程池；主线程神圣不可侵犯；到处想的是怎么避免并发。到了 Go 这边，第一课就是反转：&lt;strong&gt;goroutine 很便宜，并发是常态，要担心的是共享状态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举一个我实际改过的例子。刚入职时我写了一个聚合接口，习惯性地写成了串行调用：先查用户信息，再查订单，再查优惠券，一个 200ms 的接口被我写成了 600ms。mentor 看了一眼说，这三个没依赖关系，用 errgroup 并发去。改完的代码大概长这样：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;errgroup&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;user&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;User&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Go&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;user&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;userRepo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;orders&lt;/span&gt; []&lt;span style=&#34;color:#a6e22e&#34;&gt;Order&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Go&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;orders&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;orderRepo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;List&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wait&lt;/span&gt;(); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这段代码现在看稀松平常，但当时给我震撼挺大的：在 OC 里做同样的事要 dispatch_group 加一堆样板，而 Go 里就几行。而且 &lt;code&gt;WithContext&lt;/code&gt; 把&amp;quot;一个下游失败就取消其他请求&amp;quot;这件事也顺带做了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但便宜是有代价的。&lt;/strong&gt; goroutine 开起来太容易，泄漏也就太容易。我见过最隐蔽的一个 bug：一个 goroutine 往无缓冲 channel 写数据，下游超时提前返回了，这个 goroutine 就永远阻塞在那里。流量一上来，内存曲线肉眼可见地往上爬。排查工具是 pprof 的 goroutine profile，看到几千个阻塞在同一个位置的 goroutine 时，那个画面我忘不了。&lt;/p&gt;
&lt;p&gt;所以现在我的习惯是：&lt;strong&gt;每个 goroutine 都要有明确的退出路径&lt;/strong&gt;。要么由 ctx 控制生命周期，要么用带缓冲的 channel 并接受可能丢数据，绝不裸开。这大概是我从线程池时代带过来、但用新方式表达的旧智慧：资源永远是有限的。&lt;/p&gt;</description>
    </item>
    <item>
      <title>写了七年移动端，我开始学 Go</title>
      <link>https://ruijingan.pages.dev/posts/2021/start-learning-go/</link>
      <pubDate>Thu, 18 Mar 2021 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2021/start-learning-go/</guid>
      <description>&lt;p&gt;去年年底提了内部转岗，从 iOS 组转到基础架构组，主要语言是 Go。今年三月正式过过去了。趁记忆新鲜，写写一个纯客户端背景的人学 Go 的真实体验，给想转的后端朋友一点参考。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;语法层面：三天就敢写，三个月才敢合并。&lt;/strong&gt; Go 的语法是出了名的简单，关键字二十几个，我有 OC 和 Swift 的底子，看完 tour 一天就能上手写。但敢写和敢上线是两回事——前两个月我写的代码被 mentor 挑出的问题比写出来的功能还多：channel 用完不关、err 一路往上传但没人真正处理、map 并发读写直接 panic 上线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;客户端背景带来的三个思维定势：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;总想找&amp;quot;主线程&amp;quot;。&lt;/strong&gt; 客户端开发里 UI 更新必须在主线程，这个惯性让我一开始畏手畏脚。后端的世界里没有这个概念，goroutine 随便开，要担心的是别的东西。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不习惯显式的 err。&lt;/strong&gt; 客户端习惯了异常和回调，&lt;code&gt;if err != nil&lt;/code&gt; 的样板代码看得我密集恐惧症都犯了。后来才理解这是设计取舍：错误就是返回值的一部分，不该被藏起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低估了部署环境。&lt;/strong&gt; 客户端最多适配几个 OS 版本，后端要面对的是内存、连接数、磁盘 IO，这些我以前从没操心过的东西，现在都是日常。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;学习方法上最有效的一件事&lt;/strong&gt;：mentor 让我先别看资料，直接读公司里一个成熟服务的源码，遇到不懂的再回头查。比从书上一章章啃快多了，而且读到的是&amp;quot;生产级的 Go&amp;quot;而不是&amp;quot;教科书式的 Go&amp;quot;。&lt;/p&gt;
&lt;p&gt;转岗三个月，最大的感受是：客户端转后端，语法是最不重要的部分，真正要补的是对分布式系统的直觉。这条路还长，慢慢来。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
