【Go】开源hertz-admin:面向信创与内网交付的 Go 后端脚手架
项目已开源:下载地址
Go 后端项目从零起步,真正花时间的从来不是业务代码。
分层怎么划、错误码从哪个号段开始、权限在哪一层拦、日志怎么切分、配置从哪里读、版本号怎么注入、镜像怎么打小——这些决定项目长期可维护性的决定,每个新项目都要重做一遍,而且每个项目做得都不一样。做的时候不觉得,半年后回头看,自己都嫌乱。
信创和内网交付又会额外压上三件事:服务器不出外网,go mod download 直接卡死;等保密评要求国密算法,而多数模板里密码存储还停在 bcrypt;客户指定数据库,今天 MySQL,明天可能换成 openGauss 或者人大金仓。
hertz-admin 把这三件事和那堆重复劳动,一起固化成了工程骨架。
它基于字节跳动开源的 CloudWeGo Hertz,内置国密算法,已完成七种数据库的适配,全部依赖 vendor 进仓库,在完全离线的内网环境里无需代理、无需联网即可编译。
下面按能力逐块说明。
1. 核心特性
| 特性 | 说明 |
|---|---|
| 高性能底座 | CloudWeGo Hertz,基于 netpoll 的非阻塞 I/O |
| 标准分层 | router → controller → service → store → model 单向依赖,遵循 golang-standards 布局 |
| 统一错误码 | 所有 error 收敛为可观测错误码,业务码与 HTTP 状态码分离 |
| 三级权限模型 | 登录态 / 管理员 / 超级管理员,中间件按路由组注册 |
| 国密算法内置 | SM3、SM4(CBC/ECB/CFB/OFB)、SM4-GCM,密码以 HMAC-SM3 存储 |
| 多数据库支持 | MySQL / PostgreSQL / openGauss / 人大金仓 / 达梦 / SQLite / ClickHouse,七种均已适配 |
| 登录安全 | 密码错误次数累计与锁定、完整性校验、密码有效期 |
| 结构化日志 | logrus + lumberjack,支持标准输出与文件、日志切割、调用者信息 |
| 版本注入 | 借鉴 K8s 做法,编译期把 git tag/commit 写入二进制并暴露 /version 接口 |
| 容器化就绪 | 多架构 Dockerfile、docker-compose、K8s Deployment、Jenkinsfile |
| 离线可构建 | 完整 vendor/ 目录随仓库提交,内网无代理、无外网也能编译 |
| 单元测试 | 缓存、加解密、错误码、国密、随机数等核心模块均有测试用例 |
一句话总结:这是一个可以直接进入交付现场的项目骨架,而不是只能跑在开发者本机上的示例代码。
2. 技术底座
为什么是 Hertz,而不是 Gin
Go 生态里做 HTTP 服务,Gin 是事实上的默认答案。这个骨架没有选它,原因不在 API 好不好用,而在网络模型——面对高并发连接时,两者的资源模型不一样。
Hertz 是字节跳动开源的 Go HTTP 框架,底层是自研的 netpoll,走非阻塞 I/O,扛住了字节内部大量线上流量。
Gin 架在标准库 net/http 之上。 这决定了它的连接模型:一个连接对应一个 goroutine,请求的读写、等待、处理都在这个 goroutine 里完成。这是 Go 最经典也最省心的写法。
Hertz 不走 net/http,网络层是自研的 netpoll。 netpoll 用 epoll/kqueue 做事件驱动,由少量事件循环 goroutine(数量与 CPU 核数相关)统一管理所有连接,连接的读写由 I/O 事件驱动,不靠"一连接一 goroutine"堆出来。
两者放在一起看:
| 维度 | Gin(net/http) | Hertz(netpoll) |
|---|---|---|
| 连接与 goroutine 的关系 | 一连接一 goroutine | 少量事件循环 goroutine 管理全部连接 |
| 等待 I/O 时 | goroutine 被运行时 park,但该连接仍占着一份 goroutine 栈与上下文 | 无连接级 goroutine,连接状态在事件循环里维护 |
| 长连接大规模场景 | goroutine 数量随连接数线性增长,栈与缓冲区开销同步增长 | goroutine 数量与连接数解耦,压力收敛到少量事件循环上 |
有一点要说清楚,避免过度解读:Go 运行时本身就有 netpoller,阻塞在 I/O 上的 goroutine 会被 park 掉,并不会白占系统线程,所以 Gin 的性能远没有被"一连接一 goroutine"这件事拖垮。差别在量级——连接数从几千涨到十万,每个连接一份栈空间和读写缓冲,这笔账会变得可测量;netpoll 把"连接数"和"goroutine 数"解耦之后,这条增长曲线被压平了。
打个比方:Gin 的方式是"一个窗口配一个服务员,客人不走,服务员不下班";Hertz 的方式是"少数几个调度员盯着所有窗口,哪个窗口有动静就去处理"。客人少的时候两种没差别,客人多到一定程度,第二种才显出余量。
一句话总结:这个骨架选 Hertz,是给高并发和长连接留余量。 至于性能数字,这里不引用——官方基准里 Hertz 有领先的实测数据,但我自己没有做过两个框架的横向对比,所以不给结论,也不拿别人的数据当自己的。Gin 从来不是错的选择,生态厚度和团队熟悉度它都更好;只是当连接规模真的要往上走,网络模型这一层的余量,是后面很难再补回来的东西。
请求流转
一个请求进来,经过的路径是固定的:
HTTP 请求
→ 中间件层(recovery / CORS / 访问日志)
→ Router(ha/v1)
→ 鉴权(公开 / LoginRequired / AdminRequired / SuperAdminRequired)
→ Controller(参数校验、响应封装)
→ Service(业务编排)
→ Store(数据访问:database / cache)
→ MySQL + gcache
这条链路的价值在于边界清晰。Controller 不碰数据库,Service 不关心 HTTP 状态码,Store 不做业务判断。新人接手时,只要知道自己在改哪一层,就不会牵动全身。
依赖方向单向
router → controller → service → store → model
工程布局遵循 golang-standards/project-layout。这个规范是 Go 社区最广泛认可的项目布局标准,选它的直接好处是:任何人打开仓库,不需要先读一份"本项目目录说明"。
3. 国密支持
等保和密评场景里,非国密算法是要扣分的。而绝大多数 Go 脚手架模板,密码存储还停在 bcrypt 或 MD5。
pkg/utils/gm 提供了符合国密标准的算法实现:
| 算法 | 实现 | 模式 |
|---|---|---|
| SM3 | gm.Sm3Sum() / gm.New() |
摘要、HMAC |
| SM4 | gm.NewCipher() |
CBC、ECB、CFB、OFB |
| SM4-GCM | 包内 GCM 封装 | 认证加密 |
密码存储方式:base64(HMAC-SM3(key=用户名, data=明文密码))
// 加密
cipher := gm.EncryptPasswd(username, password)
// 校验(常量时间比较,防时序攻击)
ok := gm.CheckPasswd(username, password, cipher)
这里有一个设计细节值得单独说:HMAC 的 key 用的是用户名,而不是一个固定密钥。用户 A 和用户 B 即使设置了完全相同的密码,存进数据库的密文也完全不同。彩虹表、批量比对这类攻击直接失效——相当于每个人一把锁,锁芯都不一样。
校验环节用常量时间比较,避免通过响应时间反推出密码内容。
再配合用户表上的 err_num(错误次数累计,超过 5 次锁定)和 pwd_updated_at(密码有效期),构成一套完整的登录安全策略。
国密算法实现参考自苏州同济金融科技研究院的开源实现(Apache-2.0),版权声明保留在源文件中。
4. 多数据库支持
internal/apiserver/store/db.go 实现了七种数据库的适配,覆盖驱动选择、DSN 构造与自动建库。
默认只开 MySQL 是刻意的
仓库默认仅启用 MySQL,其余驱动的 gorm.Open 保持注释状态。 这不是"还没做完",而是一个按需开启的开关,基于两点考虑:
其一,多数项目只需要一种数据库,全量启用会把用不到的驱动依赖编进二进制,白白增大产物体积。
其二,那些驱动包在 init 阶段会执行各自的初始化逻辑,用不到也照跑一遍。
需要哪一种,打开哪一种。
切换方式
| 步骤 | 位置 | 操作 |
|---|---|---|
| 1 | internal/apiserver/store/db.go |
注释 MySQL 分支,解除目标数据库分支的注释 |
| 2 | go.mod |
引入目标数据库驱动(必须,否则编译失败) |
| 3 | configs/config.yml |
将 base.dbtype 置为目标数据库标识 |
# configs/config.yml
base:
# mysql,postgresql,opengauss,kingbase,clickhouse,sqlite,dm(达梦)
dbtype: postgresql
方言差异
切换至非 MySQL 数据库时,需要确认三处:
- 建表语句中的 MySQL 专有语法,如
utf8mb4 gorm:"type:varchar(255)"等硬编码字段类型soft_delete.DeletedAt的软删除标记行为
一句话总结:数据库不是绑死在代码里的,而是一个配置项加上三行注释的开关——客户决定换库的那天,你不用改业务代码。
5. 离线与内网构建
依赖全部装进仓库
Go 有个命令叫 go mod vendor,作用是把项目用到的所有依赖源码,从模块缓存里复制一份,放进项目根目录的 vendor/。
打个比方:平时我们是去公共仓库取零件,内网环境相当于把整个零件库搬进了自己的车间,以后装配不用再出门采购。
hertz-admin 的 vendor/ 随仓库一起提交,实测 2004 个文件、63.77 MB。有了它,内网环境不需要 GOPROXY、不需要 module cache、也不需要外网,go build 直接就能编译。
代价是仓库体积多了约 64 MB,git clone 会相应变慢。做内网交付,这个代价是值得的。
一个必须保留的例外
.dockerignore 中有一行 !vendor/**,请勿删除。
模板自带的 **/obj 规则本意是排除 .NET 编译产物,但它会连带命中 vendor/github.com/twitchyliquid64/golang-asm/obj —— 那是 Go 语言的一个源码包(69 个 .go 文件),不是编译产物。被排除出构建上下文后,go build -mod=vendor 会直接失败:
vendor/github.com/twitchyliquid64/golang-asm/asm/arch/arch.go:9:2:
cannot find module providing package github.com/twitchyliquid64/golang-asm/obj:
import lookup disabled by -mod=vendor
记住一个判据就够了:宿主机 go build -mod=vendor 通过、Docker 里同一条命令失败,九成是 .dockerignore 误伤了 vendor/,而不是依赖有问题。
6. 权限与错误码
三级权限
权限不在每个接口里手写判断,而是在路由组上声明式注册:
// internal/apiserver/router/v1/auth.go
initSys(r) // 登录即可访问
initAuthAdminRouter(r) // 需管理员权限
initSuperAdminRouter(r) // 需超级管理员权限
| 角色 ID | 角色 | 中间件 |
|---|---|---|
| 1 | 超级管理员 | middleware.SuperAdminRequired() |
| 2 | 管理员 | middleware.AdminRequired() |
| — | 已登录用户 | middleware.LoginRequired() |
鉴权走 Authorization: Bearer <token>,校验通过后将 userId / username / roleId 注入请求上下文供后续使用。
这样做的好处是权限收敛在一处:想知道某个接口谁能访问,看它在哪个路由组里注册即可,不需要翻遍 Controller 找 if 判断。
统一错误码
所有 error 统一收敛到 pkg/errcode,业务错误码与 HTTP 状态码解耦:
// 定义
var (
Success = New(0, "success")
ServerError = New(10000, "服务内部错误")
DBError = New(10001, "数据库操作失败")
...
)
// 使用
c.JSON(http.StatusOK, response.Fail(errcode.DBError))
前端可以据此弹精确提示,而不是从 500 里猜到底发生了什么。
版本注入
这个设计借鉴了 K8s 的做法。编译期通过 -ldflags 把 git 版本号、commit、构建时间、Go 版本、目标平台一起写进二进制,运行时通过 GET /ha/v1/version 查出来。
为什么值得做?因为排查线上问题的第一句话通常是"你跑的是哪个版本"。没有这个接口,你得登机器翻文件、猜构建时间、对 commit;有了它,一条 curl 就完事。
7. 三分钟跑起来
前置条件:Go 1.27+,以及一个可连的 MySQL。
# 1. 克隆
git clone https://github.com/gjing1st/hertz-admin.git
cd hertz-admin
# 2. 修改数据库配置(configs/config.yml)
# database.host / username / password / dbname
# 3. 启动
make run
服务默认监听 9680 端口。数据库和数据表会自动创建,并注入一个超级管理员账号:
| 账号 | 密码 |
|---|---|
superAdmin12 |
Best@213 |
首次登录后请立刻修改默认密码,生产环境务必替换。
验证服务是否正常:
curl http://localhost:9680/ha/v1/ping # -> "pong"
curl http://localhost:9680/ha/v1/version # -> 版本信息
接口文档(Swagger)已经挂好,浏览器直接打开:
http://localhost:9680/swagger/index.html
常用命令:
make help # 查看全部可用命令
make run # 本地运行
make build # 编译二进制(自动注入 git 版本信息)
make docker # 构建镜像并导出 tar.gz
make swag # 重新生成 Swagger 文档
已内置的接口
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /ha/v1/ping |
健康检查 | 公开 |
| GET | /ha/v1/version |
版本信息 | 公开 |
| GET | /ha/v1/login-type |
支持的登录方式 | 公开 |
| GET | /ha/v1/init/step |
初始化状态 | 公开 |
| POST | /ha/v1/user/login |
登录 | 公开 |
| POST | /ha/v1/user/register |
注册 | 公开 |
| POST | /ha/v1/logout |
登出 | 公开 |
| GET | /ha/v1/sys/run |
系统运行时长 | 已登录 |
| GET | /ha/v1/sys/status |
系统运行状态 | 已登录 |
这些是骨架自带的基础接口,业务接口按同样的分层往下加即可。
8. 工程结构
├── build
│ ├── ci # 持续集成打包脚本
│ └── docker # Dockerfile(支持多架构)
├── cmd
│ └── ha # 主程序入口
├── configs
│ └── config.yml # 应用配置
├── deployments
│ ├── docker-compose # Docker Compose 部署
│ ├── jenkins # Jenkins Pipeline
│ └── k8s # Kubernetes Deployment
├── docs # Swagger 文档(自动生成)
├── internal
│ ├── apiserver # 核心业务(MCSS 分层)
│ │ ├── controller # 控制器:参数校验、响应封装
│ │ ├── router # 路由注册与权限分组
│ │ ├── service # 业务逻辑
│ │ ├── store # 数据访问(database / cache / 初始化数据)
│ │ └── model # entity / dict / request / response
│ └── pkg # 内部公共能力
│ ├── middleware # 鉴权中间件
│ ├── config # 配置加载
│ └── functions # 日志封装
├── pkg
│ ├── errcode # 统一错误码定义
│ ├── global # 全局变量与错误
│ └── utils # 工具集(国密 gm / uuid / slice / map ...)
├── scripts # 环境与构建脚本
├── vendor # 依赖副本(离线构建用,随仓库提交)
├── version # 版本信息(编译期注入)
└── Makefile
部署方式两种都已带齐:
# Docker Compose
cd deployments/docker-compose && docker-compose up -d
# Kubernetes
kubectl apply -f deployments/k8s/ha-deployment.yaml
configs/config.yml 的常用配置项:
| 配置项 | 默认值 | 说明 |
|---|---|---|
base.port |
9680 |
服务监听端口 |
base.dbtype |
mysql |
数据库类型,七种可选 |
base.cachetype |
gcache |
缓存类型(内存缓存) |
base.enableIntegrity |
true |
是否开启数据完整性校验 |
base.pwdMaxErrNum |
5 |
密码最大错误次数 |
log.output |
std |
日志输出:std / file |
log.level |
info |
日志级别 |
9. Roadmap
- CI 流水线:补充 GitHub Actions(构建、Lint、单元测试)
- 镜像命名统一:对齐
Makefile产物名与 docker-compose 中的服务镜像名 - 可选组件:Casbin 权限模型、Redis 缓存实现、Wire 依赖注入
- 在线文档站:基于 GitHub Pages 搭建使用文档
- 性能基准:补充框架基准测试并与同类脚手架横向对比
10. 结语
内网交付里的麻烦,绝大多数不是技术难题,而是没人提前替你踩过的坑。
依赖拉不下来、算法不合规、数据库要换——每一件单拎出来都不难,难的是它们会在项目最忙的时候同时出现。
如果你也在做信创或者内网交付,有三件事建议尽早做,越往后代价越大:
第一,依赖尽早 vendor。别等要去客户现场才想起来,那时候你手边可能连外网都没有。
第二,国密算法一开始就定下来。中途更换哈希算法,意味着所有存量密码都得重算,用户得重新设置一遍密码。
第三,数据库适配留好开关,别把某一种数据库的语法写进业务代码。客户决定换库的时间点,通常不会提前通知你。
这三件事,hertz-admin 都已经替你做好了。
仓库在这里,如果对你有用,欢迎点个 Star,也欢迎提 Issue 一起完善:

浙公网安备 33010602011771号