<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>开源贡献 on JasperSao的博客</title><link>https://jasperstonnne.github.io/MyBlog/tags/%E5%BC%80%E6%BA%90%E8%B4%A1%E7%8C%AE/</link><description>Recent content in 开源贡献 on JasperSao的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 30 Aug 2026 08:12:59 +0000</lastBuildDate><atom:link href="https://jasperstonnne.github.io/MyBlog/tags/%E5%BC%80%E6%BA%90%E8%B4%A1%E7%8C%AE/index.xml" rel="self" type="application/rss+xml"/><item><title>Go 项目反推：Feed 流系统实战——从 Docker 到开源贡献</title><link>https://jasperstonnne.github.io/MyBlog/p/go-feed-system-reverse-engineering/</link><pubDate>Sun, 19 Jul 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/go-feed-system-reverse-engineering/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/go-feed-system-reverse-engineering/cover.svg" alt="Featured image of post Go 项目反推：Feed 流系统实战——从 Docker 到开源贡献" /&gt;&lt;p&gt;承接 &lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/go-basics-variables-to-pointers/" &gt;Go 语言基础：从变量到指针&lt;/a&gt;、&lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/go-methods-interfaces-errors/" &gt;Go 语言进阶：方法、接口与错误处理&lt;/a&gt;、&lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/go-engineering-modules-project-structure/" &gt;Go 语言工程化：模块管理与项目结构&lt;/a&gt;、&lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/go-concurrency-goroutines-worker-pool/" &gt;Go 语言并发：从 Goroutine 到 Worker Pool&lt;/a&gt; 和 &lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/go-web-net-http-to-gin/" &gt;Go Web 开发：从 net-http 到 Gin&lt;/a&gt;，这一篇跳出语法学习，反推一个真实的开源项目——&lt;a class="link" href="https://github.com/LeoninCS/feedsystem_video_go" target="_blank" rel="noopener"&#10; &gt;feedsystem_video_go&lt;/a&gt;（279 star，Go + Vue 3 短视频 Feed 流系统），从跑通项目到提交 PR，走一遍完整的工程实战。&lt;/p&gt;&#10;&lt;h2 id="项目概览"&gt;项目概览&#10;&lt;/h2&gt;&lt;p&gt;这个项目是一个短视频 Feed 流系统，技术栈：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;后端&lt;/strong&gt;：Go + Gin + GORM + RabbitMQ + Redis&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;前端&lt;/strong&gt;：Vue 3 + TypeScript&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;基础设施&lt;/strong&gt;：Docker Compose 编排 MySQL、Redis、RabbitMQ&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;核心功能包括用户注册登录、视频发布、点赞评论、关注取关、Feed 流推送。架构上采用 API + Worker 的消息队列模式。&lt;/p&gt;&#10;&lt;h2 id="docker-compose启动基础设施"&gt;Docker Compose：启动基础设施&#10;&lt;/h2&gt;&lt;p&gt;项目的三个&amp;quot;帮手&amp;quot;——MySQL、Redis、RabbitMQ——通过 Docker Compose 一键启动：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dev/projects/feedsystem_video_go&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d mysql redis rabbitmq&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;docker compose up&lt;/code&gt; — 根据 &lt;code&gt;docker-compose.yml&lt;/code&gt; 启动服务&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; — 后台运行（detach），不占终端窗口&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mysql redis rabbitmq&lt;/code&gt; — 只启动这三个，不启动 backend/worker/frontend&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="镜像-vs-容器"&gt;镜像 vs 容器&#10;&lt;/h3&gt;&lt;p&gt;Docker 里有两个核心概念：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;镜像&lt;/strong&gt; = 打包好的集装箱（里面有程序 + 配置），是静态文件&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;容器&lt;/strong&gt; = 集装箱正在运行的实例&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;类比：&lt;code&gt;.app&lt;/code&gt; 文件是镜像，双击打开后正在运行就是容器。&lt;/p&gt;&#10;&lt;h3 id="端口映射"&gt;端口映射&#10;&lt;/h3&gt;&lt;p&gt;MySQL 默认端口 3306，但本机可能已经装了 MySQL 占用了 3306。所以 &lt;code&gt;docker-compose.yml&lt;/code&gt; 里写的是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;3307:3306&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;意思是：外部访问 3307 → 转发到容器内部的 3306。就像你家门牌号是 3307，但内部房间编号是 3306。项目配置文件里写的也是 3307，Go 程序连接 MySQL 时用的就是 3307。&lt;/p&gt;&#10;&lt;p&gt;通过 &lt;code&gt;docker compose ps&lt;/code&gt; 可以查看所有容器的运行状态：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;NAME IMAGE STATUS PORTS&#10;feedsystem_video_go-mysql-1 mysql:8.0 Up 5 minutes (healthy) 0.0.0.0:3307-&amp;gt;3306/tcp&#10;feedsystem_video_go-redis-1 redis:7-alpine Up 5 minutes (healthy) 0.0.0.0:6379-&amp;gt;6379/tcp&#10;feedsystem_video_go-rabbitmq-1 rabbitmq:3 Up 5 minutes (healthy) 0.0.0.0:5672-&amp;gt;5672/tcp&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;0.0.0.0&lt;/code&gt; 表示&amp;quot;接受任何来源的连接&amp;quot;，即对外部开放。&lt;/p&gt;&#10;&lt;h2 id="启动-go-后端"&gt;启动 Go 后端&#10;&lt;/h2&gt;&lt;p&gt;基础设施就位后，启动后端：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; backend&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;configs/config.compose-local.yaml go run ./cmd&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;CONFIG_PATH=...&lt;/code&gt; — 告诉程序用哪个配置文件（里面写好了 MySQL 3307、Redis 6379、RabbitMQ 5672 的地址）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;go run ./cmd&lt;/code&gt; — 编译并运行 &lt;code&gt;cmd/main.go&lt;/code&gt; 入口程序&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="日志解读"&gt;日志解读&#10;&lt;/h3&gt;&lt;p&gt;启动后会看到几段日志：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;第一段：依赖下载&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;go: downloading github.com/gin-gonic/gin v1.11.0&#10;go: downloading gorm.io/gorm v1.31.1&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Go 在下载第三方库，类似 &lt;code&gt;npm install&lt;/code&gt;，下载一次以后就不需要了。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;第二段：连接建立&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Loading config from configs/config.compose-local.yaml&#10;Redis connected (cache enabled)&#10;RabbitMQ connected&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注意没有 &amp;ldquo;MySQL connected&amp;rdquo;，因为 GORM 连接 MySQL 是在后面 AutoMigrate 时才真正连接。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;第三段：Gin 路由注册&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;[GIN-debug] GET /healthz&#10;[GIN-debug] POST /account/register&#10;[GIN-debug] POST /account/login&#10;[GIN-debug] POST /video/publish&#10;[GIN-debug] POST /like/like&#10;[GIN-debug] POST /feed/listLatest&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;格式是 &lt;code&gt;HTTP方法 路由路径&lt;/code&gt;，后面的 &lt;code&gt;(4 handlers)&lt;/code&gt; 表示这条路由经过几个中间件（如限流 + JWT 鉴权 + 业务处理）。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;第四段：消息队列&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;DLX ready: exchange=dlx.events queue=like.events.dlx&#10;Timeline consumer 已启动, queue=video.timeline.update.queue&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;DLX = Dead Letter Exchange（死信队列），专门处理失败的消息。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;第五段：启动完成&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Server is running on port 8080&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="为什么不把-go-也放进-docker"&gt;为什么不把 Go 也放进 Docker？&#10;&lt;/h3&gt;&lt;p&gt;Docker 适合最终部署，不适合日常开发调试。本地直接 &lt;code&gt;go run&lt;/code&gt; 的好处：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;日志直接打印在终端，实时可见&lt;/li&gt;&#10;&lt;li&gt;改了代码立刻能跑，不用重新打包镜像&lt;/li&gt;&#10;&lt;li&gt;出错了能直接看到报错信息&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="日志不是中间件"&gt;日志不是中间件&#10;&lt;/h3&gt;&lt;p&gt;日志是 Go 标准库 &lt;code&gt;log&lt;/code&gt; 包写出来的，是程序员手动写的&amp;quot;程序进行到哪一步了&amp;quot;的提示。中间件是处理请求的&amp;quot;关卡&amp;quot;，两者没有关系。&lt;/p&gt;&#10;&lt;h2 id="核心概念缓存与消息队列"&gt;核心概念：缓存与消息队列&#10;&lt;/h2&gt;&lt;h3 id="redis-缓存"&gt;Redis 缓存&#10;&lt;/h3&gt;&lt;p&gt;缓存 = 把经常用的数据放在&amp;quot;取用更快的地方&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;类比：冰箱里有水（MySQL，容量大但取用慢），你拿了一瓶放在桌上（Redis 缓存，取用极快），下次喝水直接从桌上拿，不用再去冰箱。&lt;/p&gt;&#10;&lt;p&gt;代码里 &lt;code&gt;cache enabled&lt;/code&gt; 表示数据会先从 Redis 取，取不到再去 MySQL。&lt;/p&gt;&#10;&lt;h3 id="rabbitmq-消息队列"&gt;RabbitMQ 消息队列&#10;&lt;/h3&gt;&lt;p&gt;消息队列是 API 和 Worker 之间的&amp;quot;传话筒&amp;quot;。API 处理完核心逻辑后，把&amp;quot;后续要做的事&amp;quot;发到队列，Worker 从队列里取任务执行。&lt;/p&gt;&#10;&lt;h2 id="前后端分离架构"&gt;前后端分离架构&#10;&lt;/h2&gt;&lt;p&gt;后端和前端是两个完全不同的程序：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;后端&lt;/strong&gt;：Go 写的，负责业务逻辑、读写数据库，监听 8080 端口&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;前端&lt;/strong&gt;：Vue 写的，负责页面展示、用户交互，监听 5173 端口&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;它们通过 HTTP 请求通信：用户在浏览器点&amp;quot;登录&amp;quot; → 前端发请求到 &lt;code&gt;localhost:8080/account/login&lt;/code&gt; → 后端处理 → 返回结果 → 前端展示。&lt;/p&gt;&#10;&lt;p&gt;就像餐厅里厨房（后端）和前厅（前端）是两个地方，前厅接到顾客点单传给厨房，厨房做完菜传给前厅。&lt;/p&gt;&#10;&lt;p&gt;前端用 npm（Node Package Manager）管理依赖，跟 Go 的 &lt;code&gt;go get&lt;/code&gt;、Python 的 &lt;code&gt;pip install&lt;/code&gt; 是同一个道理：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; frontend&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm install &lt;span class="c1"&gt;# 下载依赖&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm run dev &lt;span class="c1"&gt;# 启动开发服务器&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="worker-进程模式"&gt;Worker 进程模式&#10;&lt;/h2&gt;&lt;p&gt;这是整个项目最核心的架构设计。Worker 是一个独立进程，和 API 分开启动：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; backend&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;configs/config.compose-local.yaml go run ./cmd/worker&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="为什么要分开"&gt;为什么要分开？&#10;&lt;/h3&gt;&lt;p&gt;想象一个外卖平台：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;方案 A：一个人又要接单又要送餐&lt;/strong&gt; — 接单时有新订单没人接，送餐时又有新订单没人接，效率低。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;方案 B：两个人分工&lt;/strong&gt; — 前台专门接单（API），骑手专门送餐（Worker），前台接完单把订单放进&amp;quot;待送餐队列&amp;quot;（RabbitMQ），骑手从队列里取订单去送。&lt;/p&gt;&#10;&lt;p&gt;对应到项目：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;角色&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;职责&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;API 进程&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;接收用户请求（点赞、评论、发布视频），处理核心业务逻辑，把&amp;quot;后续要做的事&amp;quot;发到消息队列&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Worker 进程&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;监听消息队列，处理异步任务：更新点赞数、计算热度、推送通知&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;这个项目有四个 Worker：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;LikeWorker&lt;/strong&gt; — 处理点赞/取消点赞事件&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;PopularityWorker&lt;/strong&gt; — 更新视频热度&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;SocialWorker&lt;/strong&gt; — 处理关注/取关事件&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CommentWorker&lt;/strong&gt; — 处理评论事件&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="分开的好处"&gt;分开的好处&#10;&lt;/h3&gt;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：用户点赞时，API 只需要返回&amp;quot;点赞成功&amp;quot;，不需要等热度计算完成&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;独立伸缩&lt;/strong&gt;：请求量大时启动多个 API 进程，任务多时启动多个 Worker 进程&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;故障隔离&lt;/strong&gt;：Worker 挂了 API 还能正常接收请求，API 挂了 Worker 还能把队列里的任务处理完&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;一句话总结：&lt;strong&gt;API 是&amp;quot;接单的&amp;quot;，Worker 是&amp;quot;干活的&amp;quot;，通过消息队列解耦。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;h2 id="开源贡献实战修复-jwt-密钥缓存-bug"&gt;开源贡献实战：修复 JWT 密钥缓存 Bug&#10;&lt;/h2&gt;&lt;p&gt;跑通项目后，在实际使用中发现了一个真实 Bug。&lt;/p&gt;&#10;&lt;h3 id="问题复现"&gt;问题复现&#10;&lt;/h3&gt;&lt;ol&gt;&#10;&lt;li&gt;不设置 &lt;code&gt;JWT_SECRET&lt;/code&gt; 环境变量，启动后端&lt;/li&gt;&#10;&lt;li&gt;注册一个账号&lt;/li&gt;&#10;&lt;li&gt;登录成功（页面提示&amp;quot;登录成功&amp;quot;）&lt;/li&gt;&#10;&lt;li&gt;但左侧栏仍然显示&amp;quot;未登录&amp;quot;&lt;/li&gt;&#10;&lt;li&gt;浏览器控制台报错：&lt;code&gt;invalid or expired token (401)&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;登录本身成功，但所有需要鉴权的接口全部返回 401。&lt;/p&gt;&#10;&lt;h3 id="根因定位"&gt;根因定位&#10;&lt;/h3&gt;&lt;p&gt;文件：&lt;code&gt;backend/internal/auth/jwt.go&lt;/code&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;jwtSecret&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;JWT_SECRET&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nb"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;问题出在这个函数&lt;strong&gt;每次调用都生成一个新的随机密钥&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;登录时：&lt;code&gt;GenerateToken()&lt;/code&gt; 调用 &lt;code&gt;jwtSecret()&lt;/code&gt; → 拿到密钥 A → 用 A 签名 token&lt;/li&gt;&#10;&lt;li&gt;验证时：&lt;code&gt;ParseToken()&lt;/code&gt; 调用 &lt;code&gt;jwtSecret()&lt;/code&gt; → 拿到密钥 B → 用 B 验证&lt;/li&gt;&#10;&lt;li&gt;A ≠ B → 签名不匹配 → 返回 401&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="修复方案"&gt;修复方案&#10;&lt;/h3&gt;&lt;p&gt;用包级别的变量缓存密钥，保证每个进程只生成一次：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;jwtSecret&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;JWT_SECRET&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;FATAL: cannot generate JWT secret: %v&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nb"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fallback-unsafe-key-change-me&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;WARNING: JWT_SECRET not set, generated random key.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nb"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;修复方案兼顾安全性（缓存保证签名一致）和灵活性（仍然允许随机密钥兜底）。&lt;/p&gt;&#10;&lt;h3 id="走通开源贡献流程"&gt;走通开源贡献流程&#10;&lt;/h3&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;步骤&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;内容&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1. 本地复现&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;按步骤触发 Bug&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2. 阅读源码&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;从前端 → API → JWT 中间件，定位根因&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3. 提 Issue&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;描述问题，附带复现步骤 → &lt;a class="link" href="https://github.com/LeoninCS/feedsystem_video_go/issues/15" target="_blank" rel="noopener"&#10; &gt;#15&lt;/a&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4. Fork + 修复分支&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;fix/jwt-secret-caching&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5. 提 PR&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;关联 Issue → &lt;a class="link" href="https://github.com/LeoninCS/feedsystem_video_go/pull/16" target="_blank" rel="noopener"&#10; &gt;#16&lt;/a&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="面试话术"&gt;面试话术&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;在一个 279 star 的开源项目中发现了真实 bug&lt;/li&gt;&#10;&lt;li&gt;从前端（Vue/TypeScript）追踪到 API（Gin/Go）再到 JWT 鉴权中间件，定位了根因&lt;/li&gt;&#10;&lt;li&gt;提出的修复方案兼顾安全性和灵活性&lt;/li&gt;&#10;&lt;li&gt;走通了完整的开源贡献流程&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;这一篇从&amp;quot;跑通项目&amp;quot;出发，反推了 Docker Compose 编排、Gin 路由注册、Redis 缓存、RabbitMQ 消息队列、Worker 进程模式等工程概念，最后通过一次真实的开源贡献把整条链路串了起来。相比纯语法学习，反推真实项目能更快建立工程直觉。&lt;/p&gt;&#10;</description></item></channel></rss>