<?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>山行小记</title>
    <link>https://ruijingan.pages.dev/</link>
    <description>Recent content on 山行小记</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <copyright>@2025</copyright>
    <lastBuildDate>Fri, 22 Aug 2025 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://ruijingan.pages.dev/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>又是一年：从客户端到服务端这几年</title>
      <link>https://ruijingan.pages.dev/posts/2025/2025-mid-review/</link>
      <pubDate>Fri, 22 Aug 2025 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2025/2025-mid-review/</guid>
      <description>&lt;p&gt;博客四年没写过年终小结以外的东西了，今天翻归档，看到 2019 年那篇，突然想写点什么。不是总结，更像是给自己留个锚点。&lt;/p&gt;
&lt;p&gt;2019 年那篇里写&amp;quot;明年想做的事：学点服务端的东西&amp;quot;。现在回头看，这个愿望以一种完全没预料到的方式实现了——不是学了点，而是整个换了赛道。2021 年转岗到基础架构，一晃四年多。&lt;/p&gt;
&lt;p&gt;这四年最大的变化不是技术栈，是看问题的位置。做客户端的时候，后端是一个黑盒：接口慢就是后端的问题，挂了就是后端的问题。换了位置之后才发现，&amp;ldquo;后端&amp;quot;自己也在被数据库、网络、机器、甚至别的团队的黑盒卡着。现在再看当初自己在客户端组提的那些工单，很多问题的真实答案不是&amp;quot;谁错了&amp;rdquo;，而是&amp;quot;信息在哪个环节丢掉了&amp;quot;。&lt;/p&gt;
&lt;p&gt;这个认知反过来让我现在的代码风格变了不少：接口里多传一个字段、日志里多打一行、文档里多写一句&amp;quot;这里为什么这样设计&amp;quot;，成本几乎为零，但可能在两年后帮某个排查问题的人省下一晚上。&lt;/p&gt;
&lt;p&gt;技术上的近况：今年主要精力在把推送网关的下一代架构往前推，长连接网关的协议升级是个又脏又慢的活儿，因为要兼容三年前的老客户端。有段时间挺烦的，觉得在还技术债没有成长。后来想通了——&lt;strong&gt;能安全地改一个跑了五年的系统，本身就是一种能力&lt;/strong&gt;，而且这种能力只有在这种活儿里练得出来。&lt;/p&gt;
&lt;p&gt;下半年想捡起来的是写作。翻归档才发现 2024 年只写了两篇，2025 年到现在只有这一篇。倒不是没东西可写，是&amp;quot;要写就写完整&amp;quot;的强迫症越来越重，一篇东西反复改开头，最后烂在草稿箱。试着降低标准：写得糙一点没关系，记录本身就是价值。这篇就算个开始。&lt;/p&gt;</description>
    </item>
    <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>第一次正经管 Kubernetes 的一些坑</title>
      <link>https://ruijingan.pages.dev/posts/2023/k8s-first-lessons/</link>
      <pubDate>Thu, 16 Nov 2023 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2023/k8s-first-lessons/</guid>
      <description>&lt;p&gt;今年公司的基础设施从自建机房迁到了云上的 K8s，运维的活儿有相当一部分落到我们组头上。作为一个写了十年代码没怎么管过机器的人，这半年补了很多课，挑几个印象深的记下来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;资源 requests/limits 别乱填。&lt;/strong&gt; 上线前凭感觉给服务配了 limits：内存 4G、CPU 2 核。结果高峰期 CPU 被 throttle，接口 P99 从 80ms 涨到 400ms，监控上还看不到任何异常——throttle 不报错，就是慢。后来用 Grafana 盯着 CPU throttling 指标重新校准了所有服务的配额。教训：&lt;strong&gt;limits 配小了比不配还危险，因为故障形态是&amp;quot;变慢&amp;quot;而不是&amp;quot;报错&amp;quot;&lt;/strong&gt;，最难排查。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;滚动更新的 maxSurge 和 readiness 探针是一对。&lt;/strong&gt; 我们有个服务启动时要预热本地缓存，要 40 秒。没配 readiness 的时候，新 pod 起来就被接入流量，一堆请求打到没预热完的实例上。配上探针之后，又发现 terminationGracePeriodSeconds 太短，滚动更新时旧 pod 还没处理完存量请求就被杀了。这两个参数要配对一起看，单看哪个都不对。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HPA 不能只看 CPU。&lt;/strong&gt; 推送网关那种长连接服务，CPU 很低但连接数高，按 CPU 扩缩容等于没有。最后接了自定义指标（连接数）做 HPA，才算了结。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最花时间的一次事故&lt;/strong&gt;：某个周四凌晨所有 pod 同时重启。查下来是节点镜像_gc 策略配置问题，kubelet 把正在用的镜像清了，拉镜像又赶上镜像仓库限流。运维同学说这叫&amp;quot;基础组件的雪崩&amp;quot;，我理解了为什么老运维对&amp;quot;全部 egg 放一个篮子&amp;quot;这么警惕。&lt;/p&gt;
&lt;p&gt;总的来说，K8s 把&amp;quot;部署&amp;quot;这个动作的门槛降低了，但把&amp;quot;理解系统为什么这样运行&amp;quot;的门槛提高了。前者是工具问题，后者是知识问题，知识没法速成。我的学习路径就一条：出了事故就去把对应的原理文档读了，比先读完文档再上手记得牢。&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>
    <item>
      <title>Flutter 入坑半年的一点感受</title>
      <link>https://ruijingan.pages.dev/posts/2020/flutter-half-year/</link>
      <pubDate>Tue, 14 Jul 2020 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2020/flutter-half-year/</guid>
      <description>&lt;p&gt;年初组里立了一个新项目，预算紧、工期紧，老板拍板用 Flutter。作为一个只写过原生的人，半年下来有了一些体感，写下来给同样背景的朋友参考。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先说好的。&lt;/strong&gt; 开发效率是真的高，热重载这个体验一旦用上就回不去。UI 还原度也不错，设计师给的稿子基本一比一还原，而且 iOS / Android 两端的差异比我想象中小。我们一个三人小组，半年做了两个业务模块 + 一套通用组件库，换原生至少要五个人。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再说别扭的。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;混合工程的路由和生命周期管理是重灾区。我们用 flutter_boost 做混合栈，内存占用比纯原生页面高不少，低端机上页面切换能感觉到掉帧。官方对混合开发的支持一直不算稳定，升级 Flutter 版本的时候 flutter_boost 兼容性经常掉链子。&lt;/li&gt;
&lt;li&gt;Dart 语言本身没毛病，但生态里的包质量参差不齐。有个下拉刷新的库，作者弃坑半年，issue 堆了几百个没人回，最后我们自己 fork 修的。&lt;/li&gt;
&lt;li&gt;崩溃排查体验比原生差。符号表一多，Crash 堆栈经常对不上，需要自己维护一套符号映射的流程，这块我们还在摸索。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;我的整体判断&lt;/strong&gt;：新项目、纯 UI 展示型页面，Flutter 是划算的；但如果是重交互、深度依赖系统能力的 App，混合开发的复杂度会吃掉你在开发效率上省下来的时间。&lt;/p&gt;
&lt;p&gt;下半年项目主体功能交付了，我大概率会回去继续搞 iOS 组件化的收尾。Flutter 的知识先放着，看它后续发展吧。个人感觉跨端这个方向，五年内不会有终局，工具会一直换，但&amp;quot;用一套代码换两端&amp;quot;的需求是永恒的。&lt;/p&gt;</description>
    </item>
    <item>
      <title>2019 年终小结</title>
      <link>https://ruijingan.pages.dev/posts/2019/2019-review/</link>
      <pubDate>Sun, 29 Dec 2019 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2019/2019-review/</guid>
      <description>&lt;p&gt;年底了，照例写点流水账，权当给自己的记录。&lt;/p&gt;
&lt;p&gt;今年工作上最大的事是组件化改造落地了，从 5 月开始到现在，底层库全部拆完，业务模块拆了三分之一。中间有一段时间特别沮丧：拆到一半的时候两头不靠，旧代码看不上，新架构没收益，全组人都在怀疑这件事的意义。挺过那一段之后就好了，现在新需求基本都能在独立模块里开发，这钱花得值。&lt;/p&gt;
&lt;p&gt;技术上今年的收获：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 Swift 在项目里的占比推到了 60%，剩下的是一些低频改动的老页面，不急着迁；&lt;/li&gt;
&lt;li&gt;学了一点点响应式，Combine 发布之后把项目里一个手写的 EventBus 干掉了，代码量少了一半；&lt;/li&gt;
&lt;li&gt;开始认真写单测。之前一直觉得 iOS 单测是玄学，用了 Quick/Nimble 之后看法有所改观，虽然覆盖率还是惨不忍睹的 11%。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生活上没什么可说的，跑步从三公里进步到八公里，然后 knee 出了点问题，医生说少跑。明年打算换游泳。&lt;/p&gt;
&lt;p&gt;明年想做的事：把组件化收尾；学点服务端的东西，公司内部有 Go 的技术分享，听了一次感觉挺有意思；看书比今年多一点，今年只读完三本，惭愧。&lt;/p&gt;
&lt;p&gt;最后感谢今年帮过我的同事和网上那些写了高质量博客的陌生人。我现在写这个博客的动力之一，就是希望哪天也能帮到别人一次。&lt;/p&gt;</description>
    </item>
    <item>
      <title>iOS 组件化改造：从私有 pod 源开始</title>
      <link>https://ruijingan.pages.dev/posts/2019/ios-component-architecture/</link>
      <pubDate>Tue, 21 May 2019 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2019/ios-component-architecture/</guid>
      <description>&lt;p&gt;App 三岁了，单工程 40 万行代码，全量编译要 15 分钟，改一行代码喝杯水回来才能跑真机。终于说服老板让我们做组件化。这篇记录前期的方案取舍，给后来人省点力气。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先说结论：不要一步到位上&amp;quot;总线&amp;quot;架构。&lt;/strong&gt; 网上流传的组件化方案大多是 CTMediator 那一派，运行时解注册表调用。演示很漂亮，但真落地会发现调用关系全靠字符串，IDE 跳转变瞎，重构基本靠全局搜索。我们最后的方案保守得多：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;底层工具库（网络、缓存、埋点）拆成独立的私有 pod，用 &lt;code&gt;cocoapods-packager&lt;/code&gt; 发到自建的私有源；&lt;/li&gt;
&lt;li&gt;业务模块先只在物理上拆目录，通过 protocol 文件互相依赖，暂时不做运行时解耦；&lt;/li&gt;
&lt;li&gt;主工程退化为一个壳，只负责组装 tab 和路由。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个方案跑了半年，编译时间从 15 分钟降到 4 分钟（业务模块可以用 &lt;code&gt;pod install&lt;/code&gt; 之后按需引入调试），虽然没到网上吹的&amp;quot;秒级编译&amp;quot;，但对日常开发体验的提升已经非常明显。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;私有源的坑。&lt;/strong&gt; 自建源用的是 nexus，权限管理一塌糊涂，最后是靠 git tag + CI 校验版本号一致性来保证不出乱子。后来听说公司内部有 Artifactory 的 license，就迁过去了，省心很多。建议准备做组件化的团队先解决制品库的问题，工具链不顺手，组件化推行不下去——这不是技术问题，是工程习惯问题。&lt;/p&gt;
&lt;p&gt;下一阶段的计划是把业务模块间的 protocol 依赖收敛到一个独立的接口层仓库里，谁改接口谁发版。半年后 hopefully 能再写一篇续集，写写中间件方案到底要不要上。&lt;/p&gt;</description>
    </item>
    <item>
      <title>RecyclerView 卡顿排查的几个套路</title>
      <link>https://ruijingan.pages.dev/posts/2018/recyclerview-perf/</link>
      <pubDate>Tue, 06 Nov 2018 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2018/recyclerview-perf/</guid>
      <description>&lt;p&gt;最近接手了一个商品列表页，产品反馈&amp;quot;滑动的时候有一点点卡，但说不清楚哪里卡&amp;quot;。这种模糊的需求最难办，记录一下我的排查过程，下次遇到可以直接翻这篇。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步永远是先量化，不要靠手感。&lt;/strong&gt; 打开开发者选项里的 GPU 渲染模式，或者直接接 Systrace 看帧耗时。我们的情况是滚动时偶发 30ms+ 的帧，但不是每一帧都卡。这种偶发型卡顿，&lt;code&gt;onCreateViewHolder&lt;/code&gt; 的嫌疑最大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然后按嫌疑大小逐个排除：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;onBindViewHolder&lt;/code&gt; 里有没有做 IO？我们的问题就出在这——图片文案里的表情包配置是从 SharedPreferences 同步读的，第一次滚动到该位置时阻塞了主线程。改成启动时预加载到内存就好了。&lt;/li&gt;
&lt;li&gt;item 布局层级是否太深。用 Layout Inspector 看了下，我们最深的一层嵌套了 7 层，把多余的 LinearLayout 换成 ConstraintLayout 之后测量耗时明显下降。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setHasFixedSize(true)&lt;/code&gt; 加了吗？数据变化不改变 item 高度的话，加上能省一次全量布局。&lt;/li&gt;
&lt;li&gt;DiffUtil 用了没？我们还在用 &lt;code&gt;notifyDataSetChanged()&lt;/code&gt;，全量刷新导致大量无意义重绘，换成 DiffUtil 后滑动顺滑了一个档次。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后一个不算套路的心得：&lt;strong&gt;别过早优化&lt;/strong&gt;。上面这些做完之后帧耗时稳定在 16ms 以内了，我本来还想上 RecyclerView 的 prefetch 调参，试了下收益几乎为零，就回滚了。性能优化到&amp;quot;感知不到卡&amp;quot;就该停手，剩下的时间是业务的时间。&lt;/p&gt;</description>
    </item>
    <item>
      <title>从 Objective-C 到 Swift：一次不太顺利的迁移</title>
      <link>https://ruijingan.pages.dev/posts/2018/objc-to-swift-migration/</link>
      <pubDate>Thu, 12 Apr 2018 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/posts/2018/objc-to-swift-migration/</guid>
      <description>&lt;p&gt;公司的主 App 跑了三年多 Objective-C，今年年初终于下定决心往 Swift 迁。原计划三个月迁完，实际用了五个多月，中间踩的坑值得记一笔。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要想着一次性迁完。&lt;/strong&gt; 我们最初的方案是新开一个 Swift 分支，集中人力把旧代码全部翻译一遍。干了两个星期就发现这条路走不通：业务需求不会停，分支落后主干越来越多，合并冲突解到怀疑人生。后来改成混合编译，新的模块用 Swift 写，旧的 OC 代码按模块逐步迁，才把节奏理顺。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;bridging header 是个大坑。&lt;/strong&gt; 项目里如果既有 OC 引用 Swift，又有 Swift 引用 OC，Xcode 的报错信息基本不可读。我们最后的经验是：所有 OC 头文件里用到的第三方类型，尽量在 pch 里收口，别让 Swift 端直接碰到太深的头文件链。&lt;/p&gt;
&lt;p&gt;还有一个血泪教训：Swift 4.2 之前 API 变动太剧烈了，我们迁移中途 Swift 4.1 发布，&lt;code&gt;#selector&lt;/code&gt; 和 KVO 相关的写法改了不少，又返工了半个月。现在回头看，选对版本窗口期很重要，大版本发布前千万别开迁。&lt;/p&gt;
&lt;p&gt;代码示例没什么好贴的，唯一想留的是这条规律：&lt;strong&gt;迁移期间新代码的 code review 强度要加倍&lt;/strong&gt;。Swift 写起来太舒服，新人很容易在 optional 处理上放飞自我，&lt;code&gt;!&lt;/code&gt; 满天飞。我们在 CI 里加了 SwiftLint 的 &lt;code&gt;implicitly_unwrapped_optional&lt;/code&gt; 规则之后，线上 crash 率才慢慢降下来。&lt;/p&gt;
&lt;p&gt;总结：迁移本身不难，难的是在业务不停的情况下迁。如果重来一次，我会在开始前先把模块依赖关系理清楚，从依赖最少的叶子模块开始迁，而不是按页面从上往下迁。&lt;/p&gt;</description>
    </item>
    <item>
      <title>关于</title>
      <link>https://ruijingan.pages.dev/about/</link>
      <pubDate>Thu, 01 Mar 2018 00:00:00 +0000</pubDate>
      <guid>https://ruijingan.pages.dev/about/</guid>
      <description>&lt;p&gt;2013 年入行，做过六年多移动端，后期偏 iOS。2021 年转后端，现在主要写 Go。&lt;/p&gt;
&lt;p&gt;这个博客从 2018 年开始断断续续写，记的大多是踩坑之后怕忘掉的具体问题，希望对你有点用。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
