项目已开源: 下载地址
Go 后端项目从零起步,真正花时间的从来不是业务代码。
分层怎么划、错误码从哪个号段开始、权限在哪一层拦、日志怎么切分、配置从哪里读、版本号怎么注入、镜像怎么打小——这些决定项目长期可维护性的决定,每个新项目都要重做一遍,而且每个项目做得都不一样。做的时候不觉得,半年后回头看,自己都嫌乱。
信创和内网交付又会额外压上三件事:服务器不出外网,
go mod download 直接卡死;等保密评要求国密算法,而多数模板里密码存储还停在 bcrypt;客户指定数据库,今天 MySQL,明天可能换成 openGauss 或者人大金仓。
hertz-admin 把这三件事和那堆重复劳动,一起固化成了工程骨架。
它基于字节跳动开源的 CloudWeGo Hertz,内置国密算法,已完成七种数据库的适配,全部依赖 vendor 进仓库,在完全离线的内网环境里无需代理、无需联网即可编译。
下面按能力逐块说明。
| 特性 | 说明 |
|---|---|
| 高性能底座 | 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/ 目录随仓库提交,内网无代理、无外网也能编译 |
| 单元测试 | 缓存、加解密、错误码、国密、随机数等核心模块均有测试用例 |
一句话总结:这是一个可以直接进入交付现场的项目骨架,而不是只能跑在开发者本机上的示例代码。
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 社区最广泛认可的项目布局标准,选它的直接好处是:任何人打开仓库,不需要先读一份"本项目目录说明"。
等保和密评场景里,非国密算法是要扣分的。而绝大多数 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),版权声明保留在源文件中。
internal/apiserver/store/db.go 实现了七种数据库的适配,覆盖驱动选择、DSN 构造与自动建库。
仓库默认仅启用 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 数据库时,需要确认三处:
utf8mb4gorm:"type:varchar(255)" 等硬编码字段类型soft_delete.DeletedAt 的软删除标记行为一句话总结:数据库不是绑死在代码里的,而是一个配置项加上三行注释的开关——客户决定换库的那天,你不用改业务代码。
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/,而不是依赖有问题。
权限不在每个接口里手写判断,而是在路由组上声明式注册:
// 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 就完事。
前置条件: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 |
系统运行状态 | 已登录 |
这些是骨架自带的基础接口,业务接口按同样的分层往下加即可。
├── 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 |
日志级别 |
Makefile 产物名与 docker-compose 中的服务镜像名内网交付里的麻烦,绝大多数不是技术难题,而是没人提前替你踩过的坑。
依赖拉不下来、算法不合规、数据库要换——每一件单拎出来都不难,难的是它们会在项目最忙的时候同时出现。
如果你也在做信创或者内网交付,有三件事建议尽早做,越往后代价越大:
第一, 依赖尽早 vendor。别等要去客户现场才想起来,那时候你手边可能连外网都没有。
第二, 国密算法一开始就定下来。中途更换哈希算法,意味着所有存量密码都得重算,用户得重新设置一遍密码。
第三, 数据库适配留好开关,别把某一种数据库的语法写进业务代码。客户决定换库的时间点,通常不会提前通知你。
这三件事,hertz-admin 都已经替你做好了。
仓库在这里, 如果对你有用,欢迎点个 Star,也欢迎提 Issue 一起完善:
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70050990/viewspace-3183847/ ,如需转载,请注明出处,否则将追究法律责任。