【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 数据库时,需要确认三处:

  1. 建表语句中的 MySQL 专有语法,如 utf8mb4
  2. gorm:"type:varchar(255)" 等硬编码字段类型
  3. 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 一起完善:

posted @ 2026-09-24 15:19  天行1st-  阅读(2)  评论(0)    收藏  举报