用 Go 实现一个可长期运行的 GitHub Webhook 服务实践
前段时间我写过一篇文章,记录自己作为一名 PHP 开发者自学 Go 的过程 那篇更多是学习阶段的整理。这次则是一次完整实践的复盘。 单点知识和系统能力之间始终存在差距。 理解一个概念并不难,但要把多个能力组合起来,形成一个可以长期运行的系统,往往需要真实项目去反复打磨。很多看似基础的东西,只有亲手做过,理解才会真正扎实。 最近我完成了一个小工具:github-webhook-listener 一个用 Go 实现的 GitHub Webhook 接收服务,可以根据规则执行 Shell 命令,并内置一个简单的 Vue 面板,用于查看运行状态和执行记录。 功能本身并不复杂,AI 也完全可以在较短时间内生成类似的实现。但在实际开发过程中,我更在意的并不是功能本身,而是一些基础层面的设计问题:项目结构如何划分,依赖如何组织,边界如何定义,以及构建与部署如何简化。 这些内容未必新鲜,但当它们被组合到一个完整系统中时,体会是不同的。 项目地址我放在文章末尾,感兴趣可以自行查看下载使用。 下面我会从结构设计、并发模型以及构建方式三个方面,做一次相对完整的技术复盘。 项目核心代码放在 这种结构并不追求“标准答案”,重点在于依赖方向清晰。 在功能简单时,这种分层似乎有些“多余”。 但当涉及到任务调度、执行记录、重试机制时,结构边界开始体现价值。 边界明确之后,功能扩展基本是“局部修改”,而不是结构性调整。 所有初始化逻辑集中在 依赖关系在入口处完全展开,而不是在各个文件中隐式创建。 这种方式带来的最大好处是: 在没有使用任何 DI 框架的情况下,通过显式构造函数完成依赖注入,本身就是对依赖关系的一种约束。 当项目规模不大时,这种方式反而比自动注入更透明。 这个项目的核心之一,是执行 Shell 命令并控制并发数量。 我实现的是一个“双队列 Worker Pool”结构,主要包含三个核心组件: 当 Webhook 触发或 Web 面板手动触发任务时,任务被封装为一个结构体,发送到调度队列。 这一层只负责“生成任务”,不关心执行细节。 调度器内部维护: 调度流程: worker 数量可控,因此系统并发是有上限的。 这种结构的优点: 相比“每来一个请求直接开 goroutine 执行”的写法,这种结构在可控性和可扩展性上更好。 worker 不直接写数据库,而是把结果推送到结果队列。 结果处理器负责: 这样做的目的,是进一步解耦: 这就是“双队列”的意义: 这种分离在系统规模变大时尤为重要,因为执行耗时和持久化耗时是两个不同维度的问题。 项目使用 Makefile 统一管理: Makefile 在这里的意义并不是“少打几行命令”,而是: 当一个项目开始涉及前后端协作、交叉编译和发布时,构建流程本身就成为项目的一部分。 这是我在这个项目中感受最明显的“Go 工程优势”。 前端使用 Vue 构建完成后,静态资源通过 然后通过: 直接提供访问。 最终效果是: 从架构上看,它仍然是前后端分离: 但从交付形态看,它又像是传统单体应用: 这种组合非常适合工具型项目和内部服务。 Go 在这一点上确实有明显优势:编译后就是完整产物,不需要运行时环境,不依赖包管理器,不依赖额外解释器。 分发成本几乎为零。 这个项目没有刻意追求复杂设计,也没有引入额外框架。 它更像是一次完整的工程实践:把分层、依赖组织、并发控制、构建管理这些已经学过的能力组合在一起,形成一个可长期运行的系统。 我自己已经在实际环境中持续使用它,用来自动化部署和执行脚本,稳定性和可维护性都符合预期。对我来说,它已经从“练手项目”变成了日常工具。 如果你刚好也需要一个简单的 GitHub Webhook 执行工具,可以直接拿去用; 如果你正在学习 Go,想找一个结构完整、但复杂度可控的小项目作为参考,也可以看看实现细节。 GitHub 仓库地址:点击查看 有问题或者想法,也欢迎直接在 GitHub 上交流。


项目结构与职责划分
internal 目录:internal/
├── bootstrap
├── handler
├── service
├── repository
├── model
├── dtorepository
service
handler
在 bootstrap 中组织依赖关系
bootstrap 包中完成:双队列 Worker Pool 的并发调度模型
第一层:任务生产者
第二层:集中式调度器 + Worker Pool
第三层:结果处理器
Makefile 作为构建入口
使用 embed 将前端资源打包进二进制
embed 打包进 Go 二进制中。http.FileServer(http.FS(...))写在最后