# go_rbac_admin **Repository Path**: chichengyu/go_rbac_admin ## Basic Information - **Project Name**: go_rbac_admin - **Description**: go语言实现的一个RBAC权限验证的后台服务 - **Primary Language**: Go - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-26 - **Last Updated**: 2026-09-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # go_rbac_admin 基于 Gin + Viper + Zerolog + GORM + Casbin 的 RBAC 权限管理后台服务。 ## 技术栈 - **Web 框架**: [Gin](https://github.com/gin-gonic/gin) - **ORM**: [GORM](https://gorm.io) — 多数据源(MySQL / PostgreSQL / SQLite)、连接池管理 - **配置管理**: [Viper](https://github.com/spf13/viper) — 多环境 YAML 配置、环境变量覆盖 - **日志**: [Zerolog](https://github.com/rs/zerolog) + [Lumberjack](https://gopkg.in/natefinsh/lumberjack.v2) — 零分配结构化日志 + 按天按大小双轮转 - **权限控制**: [Casbin](https://casbin.org) — RBAC 策略引擎,SyncedEnforcer 并发安全,鉴权纯内存计算 - **缓存**: [go-redis/v9](https://github.com/redis/go-redis) — 连接池 + 缓存穿透辅助方法 - **Go 版本**: 1.25.0 ## 项目结构 采用 **go-zero 原生风格** 按层平铺,`domain/` 下按 api / service / repo / model / types 分层,`ServiceContext` 集中持有全部依赖: ``` go_rbac_admin/ ├── etc/ # 配置文件 │ ├── app.yml # 主配置(支持 app-{env}.yml 环境覆盖) │ ├── app-dev.yml # 开发环境覆盖 │ ├── app-staging.yml # 预发布环境覆盖 │ ├── app-prod.yml # 生产环境覆盖 │ └── rbac_model.conf # Casbin RBAC 模型定义 ├── db/ │ └── go_rbac_admin.sql # 初始数据库 DDL + 种子数据 ├── internal/ # 内部包(不可被外部导入) │ ├── app/ # 应用编排层 │ │ ├── app.go # App:gin 引擎 + 中间件 + GetRouter() │ │ └── server.go # HTTP 启动 + 信号监听 + 优雅停机 │ ├── code/ # 业务状态码 + BizError 统一错误类型 │ ├── config/ # 配置加载与结构体定义 │ │ ├── config.go # 顶层 Config 结构体 + Init 加载 │ │ ├── casbin.go # Casbin 配置 │ │ ├── database.go # 多数据源配置 │ │ ├── logger.go # 日志配置 │ │ ├── redis.go # Redis 配置 │ │ └── server.go # 服务配置 │ ├── data/ # 数据基础设施层 │ │ ├── db.go # 多数据源连接池 + Casbin Enforcer(Module 持有,非全局变量) │ │ ├── redis.go # Redis 客户端封装(缓存穿透等辅助方法) │ │ └── casbin/ # Casbin 鉴权封装 │ │ └── enforcer.go # SyncedEnforcer 初始化(复用 GORM DB 存策略) │ ├── domain/ # 业务代码(按层平铺) │ │ ├── api/ # HTTP 处理层(6 个 handler + routes.go 集中注册) │ │ ├── service/ # 业务逻辑层(base.go + 5 个 service) │ │ ├── repo/ # 数据访问层(base.go + 5 个 repo) │ │ ├── model/ # 数据模型(User / Role / Permission / SysLog / Test + table.go) │ │ └── types/ # 请求/响应 DTO(含 resp/ 子包) │ ├── svc/ # 依赖容器(ServiceContext,go-zero 风格) │ │ └── servicecontext.go # 只持有 Service,Repo 在内部创建后注入不暴露 │ ├── middleware/ # Gin 中间件 │ │ ├── middleware.go # TraceID + AccessLog + Recovery │ │ └── authz.go # Casbin Authz 鉴权中间件 │ └── pkg/ # 内部公共工具 │ ├── async/ # 并发控制(泛型 Group[T] / Pool[T] / NoResult / Map / ForEach) │ ├── ginutil/ # Gin HTTP 工具集 │ │ ├── response.go # 统一响应结构 + Wrap + ErrHandler │ │ ├── context.go # 请求上下文类型安全存取(UserID/Username/Ctx) │ │ ├── param.go # 单参数解析(PathID/QueryID/QueryInt…) │ │ └── bind.go # 参数绑定封装(BindQuery/BindJSON/BindUri) │ ├── gormx/ # GORM 扩展工具 │ │ ├── base.go # Base 模型:UUID 主键自动生成 │ │ ├── paginate.go # PageQuery 链式构建 + ScanPage[T] 泛型分页 │ │ └── chunk.go # ChunkSlice / ChunkIDs 分批工具 │ ├── jsonx/ # 带日志的 JSON 序列化/反序列化 │ ├── jwtx/ # JWT 签发与解析(HS256,实例化 + 依赖注入) │ ├── passx/ # 密码加密与校验(bcrypt,内置信号量并发控制) │ ├── httpx/ # HTTP 客户端(resty 封装,GET/POST/PUT/DELETE 一行调用) │ ├── captchax/ # 验证码生成与校验(数字验证码,Redis 存储,自动过期) │ ├── safe/ # goroutine 安全封装(GoSafe + panic recover) │ ├── trace/ # trace_id 生成与上下文传递 │ ├── validx/ # 参数校验:中文翻译 + 只返回第一个错误 │ └── zlog/ # 日志初始化 │ ├── init.go # 三层写入链路:levelRouter → asyncWriter → dateWriter │ ├── gorm.go # GORM 日志适配(带 trace_id) │ └── zlog.go # 上下文日志访问 ├── logs/ # 日志输出目录(按天按级别分文件) ├── main.go # 入口:配置 → 日志 → 数据基础设施 → ServiceContext → 注册路由 → 启动 ├── go.mod └── go.sum ``` ## 架构设计 ### go-zero 按层平铺 业务代码统一收拢在 `internal/domain/` 下,按 api / service / repo / model / types 平铺分层,与基础设施(`app/`、`config/`、`data/`、`middleware/`、`pkg/`)清晰隔离。依赖方向 `api → service → repo → model`,`types` 为独立 DTO 包: ``` internal/domain/ ├── api/ # HTTP 处理层:6 个 handler(Login + Test + User + Role + Permission + SysLog) ├── service/ # 业务逻辑层:base.go + 5 个 service,嵌入 BaseService ├── repo/ # 数据访问层:base.go + 5 个 repo,嵌入 BaseRepo ├── model/ # 数据模型:User / Role / Permission / SysLog / Test + table.go └── types/ # 请求/响应 DTO:按操作拆文件,与 model 解耦 ``` | 子包 | 职责 | 依赖 | |------|------|------| | `types` | 请求/响应 DTO,按操作拆文件,含 `ToModel()` 转换方法 | → `model` | | `model` | 数据模型,表名常量集中管理 | 无 | | `repo` | 数据访问,嵌入 BaseRepo 继承通用 CRUD,补充模块专属查询 | → `model` | | `service` | 业务逻辑,嵌入 BaseService 继承通用方法,重写/补充业务逻辑 | → `repo`, `model` | | `api` | HTTP handler,路由注册,透传 ctx | → `svc`, `types` | ### ServiceContext 依赖容器(go-zero 风格) `ServiceContext` 只持有 Service 实例,Repo 是 Service 的内部实现细节,在 `NewServiceContext` 中创建后注入,不对外暴露。API 层无法绕过 Service 直接访问 Repo: ```go // internal/svc/servicecontext.go type ServiceContext struct { Config *config.Config DataMod *data.Module JWT *jwtx.JWT // 配置驱动的工具组件 MailClient *mailx.Client FileClient *filex.Client FsClient *fpx.Client Captcha *captchax.Captcha // 只持有 Service,不持有 Repo TestService *service.TestService UserService *service.UserService RoleService *service.RoleService PermissionService *service.PermissionService SysLogService *service.SysLogService } func NewServiceContext(cfg *config.Config, dataMod *data.Module) *ServiceContext { db := dataMod.DB() // 工具组件 jwt := jwtx.New(cfg.JWT.Secret, cfg.JWT.Issuer, cfg.JWT.Expire) captcha := captchax.New(captchax.Config{Redis: dataMod.RDB.Client}) // Repo 作为局部变量,创建后注入 Service,不作为字段暴露 userRepo := repo.NewUserRepo(db) roleRepo := repo.NewRoleRepo(db) permRepo := repo.NewPermissionRepo(db) // services(跨模块依赖通过构造函数注入) userSvc := service.NewUserService(userRepo, roleRepo, dataMod.Enforcer, jwt, dataMod.RDB, captcha) roleSvc := service.NewRoleService(roleRepo, permRepo, dataMod.Enforcer) return &ServiceContext{ JWT: jwt, Captcha: captcha, TestService: service.NewTestService(testRepo, &cfg.Server, dataMod.RDB), UserService: userSvc, RoleService: roleSvc, PermissionService: service.NewPermissionService(permRepo), SysLogService: service.NewSysLogService(sysLogRepo), } } ``` **分层封装**:API 层只能调 `svcCtx.XxxService`,无法直接访问 Repo,业务规则集中在 Service 层。 ### 全局共享 Model 层 所有模型结构体统一放在 `internal/domain/model/`,消除重复定义。Casbin 作为权限关系唯一真相源,`User` 不再持有 `Roles` GORM 关联,角色数据通过 Casbin `GetRolesForUser` 获取: ```go // internal/domain/model/user.go type User struct { gormx.Base Username string `json:"username" gorm:"unique"` Password string `json:"password"` // 不再有 Roles []Role gorm:"many2many:sys_user_role" // 角色关系由 Casbin g 策略管理,查询走 enforcer.GetRolesForUser(userID) } // internal/domain/model/table.go — 表名常量集中管理 const ( TableUser = "sys_user" TableRole = "sys_role" TablePermission = "sys_permission" TableSysLog = "sys_log" TableTest = "test" ) ``` ### 泛型 BaseRepo[T] + BaseService[T] 通过 Go 泛型消除各模块 repo/service 的重复 CRUD 样板代码,业务模块嵌入即继承: ```go // repo 层 — 嵌入 BaseRepo 自动拥有全部通用方法(同包,无需别名) type UserRepo struct { *repo.BaseRepo[model.User] } // service 层 — 嵌入 BaseService 自动拥有全部通用方法 type UserService struct { *service.BaseService[model.User] repo *repo.UserRepo roleRepo *repo.RoleRepo // 跨模块依赖直接注入 enforcer *casbin.SyncedEnforcer jwt *jwtx.JWT // JWT 签发 redis *data.RedisClient // Redis 缓存 captcha *captchax.Captcha // 验证码校验 } ``` #### 方法一览 | 分类 | 方法 | 说明 | |------|------|------| | **单条** | `GetByID` / `Create` / `Update` / `Delete` | 基础 CRUD | | | `ExistsByID` / `Count` | 存在性判断、计数 | | **批量** | `GetByIDs` / `CreateBatch` / `UpdateBatch` / `DeleteByIDs` | 上限 MaxBatchSize(1000),超出返回错误 | | **分批** | `GetByIDsChunk` / `CreateBatchChunk` / `UpdateBatchChunk` / `DeleteByIDsChunk` | 自动分批,无上限,事务保护 | | **查询** | `FindAll` / `FindPage` | FindPage 支持 Preload + Where 链式构建 | | **事务** | `Transaction` | 闭包事务,自动提交/回滚 | | **工具** | `DB(ctx)` | 获取带上下文的 GORM 实例,确保 SQL 日志带 trace_id | #### 子类重写(method shadowing) Go 通过结构体嵌入 + 同名方法实现"重写",外层方法优先: ```go // RoleService 重写 GetByID,加上"角色不存在"的业务错误 func (s *RoleService) GetByID(ctx context.Context, id string) (*model.Role, error) { role, err := s.repo.FindRoleByID(ctx, id) if err != nil { return nil, code.NewBizError(code.NotFound, "角色不存在") } return role, nil } // 调用"父类"方法:s.BaseService.GetByID(ctx, id) ``` #### 分页查询 — PageQuery 链式构建 ```go query := s.NewPageQuery(req.PageRequest). WithWhereIf(req.Name != "", func(db *gorm.DB) *gorm.DB { // 条件搜索 return db.Where("username LIKE ?", "%"+req.Name+"%") }). WithWhere(func(db *gorm.DB) *gorm.DB { // 无条件,始终生效 return db.Where("status = ?", 1) }) result, err := s.repo.FindPage(ctx, query) ``` #### 批量操作上限保护 普通批量方法超过 `MaxBatchSize`(1000) 返回错误,强制使用 Chunk 方法: ```go // 少量数据 → 普通批量 users, err := repo.GetByIDs(ctx, []string{"1","2","3"}) // ✅ // 超过 1000 → 自动报错 users, err := repo.GetByIDs(ctx, hugeIDList) // ❌ "批量查询超过上限 1000,请使用 GetByIDsChunk" // 大量数据 → Chunk 自动分批 users, err := repo.GetByIDsChunk(ctx, hugeIDList, 1000) // ✅ 自动拆批 + 合并结果 ``` ### 请求上下文传递(trace_id 链路追踪) 整个调用链显式传递 `ctx`,确保 SQL 日志携带 trace_id: ``` HTTP 请求 → TraceID 中间件注入 trace_id 到 ctx → API 取 ginutil.Ctx(c) → Service 透传 ctx → Repo 用 DB(ctx) → GORM Trace() 收到带 trace_id 的 ctx → log.Ctx(ctx) 取出带 trace_id 的 logger → SQL 日志带 trace_id ✅ ``` ```go // API 层 pageList, err := a.svcCtx.UserService.FindPage(ginutil.Ctx(c), req) // Service 层 func (s *UserService) FindPage(ctx context.Context, req types.UserPageListReq) (*gormx.PageResult[types.UserPageListRespVo], error) // Repo 层 db := r.DB(ctx).Model(&model.User{}) // DB(ctx) = db.WithContext(ctx) ``` ### 跨模块依赖注入(go-zero 风格) 跨模块调用不通过接口层解耦,而是 `ServiceContext` 集中创建时通过构造函数注入。例如 `UserService` 需要 `RoleRepo`,直接在 `NewServiceContext` 中传入: ```go // internal/svc/servicecontext.go func NewServiceContext(cfg *config.Config, dataMod *data.Module) *ServiceContext { // repos userRepo := repo.NewUserRepo(db) roleRepo := repo.NewRoleRepo(db) permRepo := repo.NewPermissionRepo(db) // jwt + redis jwt := jwtx.New(cfg.JWT.Secret, cfg.JWT.Issuer, cfg.JWT.Expire) // services(跨模块依赖通过构造函数注入) userSvc := service.NewUserService(userRepo, roleRepo, dataMod.Enforcer, jwt, dataMod.RDB) // ← 注入 RoleRepo + Enforcer + JWT + Redis roleSvc := service.NewRoleService(roleRepo, permRepo, dataMod.Enforcer) // ... } ``` API 层只依赖 `svcCtx`,不直接 import service 包: ```go // internal/domain/api/user.go type UserApi struct { svcCtx *svc.ServiceContext } func (a *UserApi) FindPage(c *gin.Context) error { pageList, err := a.svcCtx.UserService.FindPage(ginutil.Ctx(c), req) // ... } ``` ### 路由集中注册 路由注册集中在 `internal/app/app.go` 的 `RegisterRoutes` 方法,main.go 只需调用一次: ```go // internal/app/app.go func (app *App) RegisterRoutes(svcCtx *svc.ServiceContext) { // 公开路由组(无需 JWT 认证和 Casbin 鉴权,如验证码接口) publicGroup := app.r.Group("/api") loginApi := api.NewLoginApi(svcCtx) loginApi.RegisterPublicRoutes(publicGroup) apiGroup := app.r.Group("/api") // JWT 认证 → Casbin 鉴权 → 操作日志 apiGroup.Use(middleware.AuthJWT(svcCtx.JWT, svcCtx.Config.JWT.SkipPaths...)) apiGroup.Use(middleware.Authz(svcCtx.DataMod.Enforcer)) apiGroup.Use(middleware.OpLog(svcCtx.SysLogService, &svcCtx.Config.Oplog)) loginApi.RegisterRoutes(apiGroup) api.NewTestApi(svcCtx).RegisterRoutes(apiGroup) api.NewUserApi(svcCtx).RegisterRoutes(apiGroup) api.NewRoleApi(svcCtx).RegisterRoutes(apiGroup) api.NewPermissionApi(svcCtx).RegisterRoutes(apiGroup) api.NewSysLogApi(svcCtx).RegisterRoutes(apiGroup) } ``` ```go // main.go — 路由部分只需一行 application.RegisterRoutes(svcCtx) ``` 新增 api 只需在 `RegisterRoutes` 中追加一行,main.go 永不动。 ### 应用启动流程 `App` 只负责 HTTP 基础设施(gin 引擎 + 中间件 + 优雅停机),不关心业务模块构造: ```go // main.go func main() { cfg := config.Init("etc") gin.SetMode(cfg.Server.Mode) zlog.Init(&cfg.Log) if err := validx.Init(); err != nil { log.Fatal().Err(err).Msg("valid 初始化失败") } dataMod := data.NewModule(&cfg.Database, &cfg.Redis, &cfg.Casbin, cfg.Server.Env) defer dataMod.Close() // 资源释放 svcCtx := svc.NewServiceContext(cfg, dataMod) application := app.New(cfg) application.RegisterRoutes(svcCtx) // 集中注册路由 app.Run(application) // 阻塞启动 + 优雅停机 } ``` 停机流程:收到 SIGINT/SIGTERM → `srv.Shutdown` 等待活跃请求 → `zlog.Close` 刷新日志 → `defer dataMod.Close()` 释放连接池。 ### Casbin RBAC 权限控制 #### 模型定义(rbac_model.conf) ```ini [request_definition] r = sub, obj, act # sub=用户ID, obj=请求路径, act=HTTP方法 [policy_definition] p = sub, obj, act # p 策略:角色→路径→方法 [role_definition] g = _, _ # g 分组:用户→角色 [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && r.act == p.act ``` **鉴权逻辑**:用户继承角色(g 策略),角色拥有权限(p 策略),路径支持 `keyMatch` 通配符匹配。 #### g 策略与 p 策略 | 策略类型 | 全称 | 含义 | 存储格式 | 示例 | |----------|------|------|----------|------| | **g** | grouping | 用户→角色 分组关系 | `g, user_id, role_id` | `g, user-1, admin` — user-1 属于 admin 角色 | | **p** | policy | 角色→权限 策略规则 | `p, role_id, obj, act` | `p, admin, /api/user/*, GET` — admin 角色可 GET 用户接口 | ``` 用户(user) ──g──→ 角色(role) ──p──→ 权限(perm) ──p──→ 权限(perm) 用户(user) ──g──→ 角色(role) ──p──→ 权限(perm) ``` 一个用户可通过多条 g 策略拥有多个角色,一个角色可通过多条 p 策略拥有多个权限,最终鉴权时 Casbin 取并集:**任一角色拥有该权限即放行**。 #### 策略存储 - Casbin 作为权限关系**唯一真相源**,策略存于 `casbin_rule` 表(gorm-adapter 自动建表) - `SyncedEnforcer` 并发安全,策略矩阵常驻内存,`Enforce` 纯内存计算(微秒级) - 启动时 `LoadPolicy` 全量加载,不缓存 Enforce 结果,无缓存膨胀与一致性问题 #### 策略管理 API | 操作 | Casbin 策略类型 | 说明 | |------|-----------------|------| | `AssignRole(userID, roleID)` | g 策略 | 给用户分配角色:`g, user_id, role_id` | | `RemoveRole(userID, roleID)` | g 策略 | 移除用户角色 | | `GetUserRoles(userID)` | g 策略 | 查询用户角色列表 | | `AssignPermission(roleID, permID)` | p 策略 | 给角色分配权限:`p, role_id, url, method` | | `RemovePermission(roleID, permID)` | p 策略 | 移除角色权限 | | `GetRolePermissions(roleID)` | p 策略 | 查询角色权限 ID 列表 | #### Authz 鉴权中间件 ```go // middleware/authz.go func Authz(e *casbin.SyncedEnforcer) gin.HandlerFunc { return func(c *gin.Context) { sub := ginutil.GetUserID(c) // 从认证中间件注入的 user_id obj := c.Request.URL.Path // 请求路径 act := c.Request.Method // HTTP 方法 passed, err := e.Enforce(sub, obj, act) if !passed { c.Abort() ginutil.Fail(c, code.Forbidden) return } c.Next() } } ``` 需配合 Authn 中间件使用:Authn 负责认证并注入 `user_id`,Authz 负责授权。 #### 用户角色查询(Casbin 内存计算) 用户分页查询时,角色数据从 Casbin 内存矩阵获取,不查 `sys_user_role` 表(已删除)。`UserService.FindPage` 的查询流程: ``` 1. repo.FindPage → 1 次 SQL(分页查用户) 2. enforcer.GetRolesForUser(userID) × N → 0 次 SQL(Casbin 纯内存,微秒级) 3. roleRepo.FindByIDs → 1 次 SQL(批量查角色实体,去重后 IN 查询) ───────────── 合计 2 次 SQL,与用户数无关 ``` Casbin `SyncedEnforcer` 启动时全量加载 `casbin_rule` 到内存构建邻接表,运行时 `GetRolesForUser` 是纯内存查询,不碰数据库。 ### 日志架构 三层写入链路,兼顾高性能与可观测性: ``` zerolog.Event(零分配序列化) │ ▼ levelRouter.WriteLevel()(按级别分流) │ ├──→ asyncWriter.info ──→ channel[4096] ──→ dateWriter ──→ lumberjack ──→ 磁盘 ├──→ asyncWriter.error ──→ channel[4096] ──→ dateWriter ──→ lumberjack ──→ 磁盘 └──→ os.Stdout(控制台,both 模式下) ``` | 层 | 职责 | 性能特性 | |----|------|---------| | `levelRouter` | 按级别分流:error 双写 info+error,其他仅写 info | 零额外开销 | | `asyncWriter` | channel 异步缓冲,业务 goroutine 非阻塞写入 | 纳阻塞,纳秒级延迟 | | `dateWriter` | 按天切换文件,热路径无锁(atomic.Pointer) | 无锁热路径 | | `lumberjack` | 按大小轮转 + 自动清理 + 压缩 | 标准轮转 | GORM SQL 日志通过自定义 Logger 适配 zerolog,优先从 ctx 取带 trace_id 的 logger,取不到则回退全局 logger,确保 SQL 日志与请求日志可串联。 文件命名规则: | 场景 | 文件名 | 示例 | |------|--------|------| | 当天日志 | `{name}-{level}.log` | `logs/go_rbac_admin-info.log` | | 历史日志 | `{name}-{level}-{date}.log` | `logs/go_rbac_admin-info-2026-08-27.log` | ### 统一错误处理 `ErrHandler` + `Wrap` 模式,业务 handler 只管 `return err`,框架统一兜底: ```go // api 层 — 只管返回 error func (a *UserApi) GetUserApi(ctx *gin.Context) error { id, err := ginutil.QueryID(ctx, "id") if err != nil { return err } user, err := a.svcCtx.UserService.GetByID(ginutil.Ctx(ctx), id) if err != nil { return ginutil.ErrBind(err) } ginutil.OkData(ctx, user) return nil } // 路由注册 — Wrap 转为 gin.HandlerFunc r.GET("/user/getUser", ginutil.Wrap(a.GetUserApi)) ``` `Wrap` 自动处理: - 返回 `nil` → 正常响应 - 返回 `*BizError` → 对应状态码 + 消息 - 返回其他 `error` → 500 + 日志记录 - panic 恢复 → 500 + 堆栈日志 ### 统一响应结构 所有 API 响应遵循统一格式,自动注入 traceId: ```json { "code": 200, "message": "操作成功", "data": {}, "traceId": "a1b2c3d4e5f6..." } ``` | 方法 | 说明 | |------|------| | `ginutil.OkData(c, data)` | 成功响应,状态码 200 | | `ginutil.OkDefault(c)` | 成功响应,空数据 | | `ginutil.OkMsg(c, msg)` | 成功响应,自定义消息 | | `ginutil.Fail(c, code)` | 失败响应,指定状态码 | | `ginutil.FailErr(c, err)` | 统一错误响应,自动识别 BizError | ### 参数绑定(ginutil) `ginutil` 封装了 Gin 的参数绑定,统一 `ShouldBind` + `ErrBind` 样板代码: ```go // Query 参数绑定(GET 请求) var req types.PageListReq if err := ginutil.BindQuery(c, &req); err != nil { return err } // JSON 请求体绑定(POST/PUT) var req types.PermissionCreateReq if err := ginutil.BindJSON(c, &req); err != nil { return err } // 路径参数绑定(如 /test/:id) var req types.GetByIDReq if err := ginutil.BindUri(c, &req); err != nil { return err } ``` 单参数接口免定义结构体: ```go id, err := ginutil.QueryID(c, "id") // query 取字符串 ID uid, err := ginutil.PathID(c, "id") // 路径取字符串 ID page := ginutil.QueryInt(c, "page", 1) // query 取 int,带默认值 ``` ### 参数校验(validx) `validx` 封装了 `validator/v10` 的中文翻译,核心设计: - **只返回第一个错误**:`Translate()` 取 `errs[0]`,用户每次只看到一条提示 - **中文翻译**:注册 `zh` 翻译器,`required` → `xxx为必填字段` - **JSON 格式错误识别**:`json:` 前缀的解析错误自动转为 `请求体JSON格式错误` ### DTO 与 Model 转换(ToModel) `types` 层的请求结构体提供 `ToModel()` 方法,将 DTO 转换为数据库模型,API 层不直接操作 `model` 包: ```go // types/create.go type PermissionCreateReq struct { Title string `json:"title" binding:"required"` Code string `json:"code" binding:"required"` } func (r *PermissionCreateReq) ToModel() *model.Permission { return &model.Permission{Title: r.Title, Code: r.Code} } // api/permission.go permission := req.ToModel() // 一行完成转换 ``` ### Redis 缓存 `data.RedisClient` 封装 `go-redis/v9`,核心约定: - **缓存未命中不是错误**:`Get` 系列方法返回空值 + nil error,业务层用 `len(val) == 0` 判断 - **缓存穿透**:`GetOrSet` / `GetBytesOrSet` 未命中时自动调用 fn 回源加载并回写缓存 ```go // 简单读写 val, err := rdb.Get(ctx, "user:1:name") err = rdb.Set(ctx, "user:1:name", "tom", 10*time.Minute) // 缓存穿透(最常用模式) val, err := rdb.GetOrSet(ctx, "user:1:name", 10*time.Minute, func(ctx context.Context) (any, error) { return userRepo.GetName(ctx, 1) }) // 缓存 JSON 对象 data, err := rdb.GetBytesOrSet(ctx, "user:1:detail", 10*time.Minute, func(ctx context.Context) ([]byte, error) { user, _ := userRepo.GetByID(ctx, 1) return jsonx.Marshal(ctx, user) }) ``` ## API 路由 所有路由注册在 `/api` 路由组下: ### 登录模块 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/captcha` | 获取验证码图片(返回 `captcha_id` + base64 图片) | | POST | `/api/login` | 用户登录(需携带 `captcha_id` + `captcha_code`,返回 JWT token) | ### 测试模块 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/test/:id` | 根据 ID 查询测试数据 | ### 用户模块 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/user/findPage` | 分页查询用户列表 | | GET | `/api/user/getUser` | 根据 ID 查询用户详情 | ### 角色模块 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/role/list` | 查询角色列表 | | POST | `/api/role/assignRole` | 给用户分配角色(Casbin g 策略) | | POST | `/api/role/removeRole` | 移除用户角色(Casbin g 策略) | | GET | `/api/role/userRoles` | 查询用户的角色列表 | | POST | `/api/role/assignPermission` | 给角色分配权限(Casbin p 策略) | | POST | `/api/role/removePermission` | 移除角色权限(Casbin p 策略) | | GET | `/api/role/permissions` | 查询角色的权限 ID 列表 | ### 权限模块 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/permission/findAll` | 查询所有权限 | | GET | `/api/permission/findById` | 根据 ID 查询权限详情 | | POST | `/api/permission/create` | 创建权限 | | POST | `/api/permission/update` | 更新权限 | | POST | `/api/permission/delete` | 删除权限 | ### 系统日志模块 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/log/findAll` | 查询所有日志 | ## 快速开始 ```bash # 克隆项目 git clone cd go_rbac_admin # 安装依赖 go mod tidy # 初始化数据库(MySQL 示例) mysql -u root -p < db/go_rbac_admin.sql # 启动服务(默认 dev 环境) go run main.go ``` 服务启动后监听 `127.0.0.1:8000`(可在 `etc/app.yml` 中修改)。 ## 配置说明 配置文件位于 `etc/` 目录,采用多环境管理: | 文件 | 说明 | |------|------| | `app.yml` | 基础配置(所有环境共享) | | `app-dev.yml` | 开发环境覆盖(可选) | | `app-staging.yml` | 预发布环境覆盖(可选) | | `app-prod.yml` | 生产环境覆盖(可选) | ### 环境变量覆盖 所有配置项均可通过环境变量覆盖,规则:`APP_` 前缀 + 下划线分隔路径。 ```bash APP_SERVER_HOST=0.0.0.0 APP_SERVER_PORT=9000 go run main.go ``` ### 跨字段引用 支持 `${key}` 语法引用其他配置项: ```yaml log: env: ${server.env} # 引用 server.env 的值 ``` ### 多数据源配置 ```yaml database: default: primary # 默认数据源名称 sources: primary: # 主库(读写)— MySQL 示例 driver: mysql host: 127.0.0.1 port: 3306 database: go_rbac_admin username: root password: "" charset: utf8mb4 max_idle_conns: 25 max_open_conns: 100 conn_max_lifetime: 300s slow_threshold: 200ms skip_default_transaction: true prepare_stmt: true ``` 支持 `mysql` / `postgres` / `sqlite` 三种驱动,可配置多数据源(如主库 + 日志库)。 代码中通过 `svcCtx.DB(config.DSPrimary)` 获取数据源,支持多库切换。Repo 通过构造函数接收 `*gorm.DB`,显式依赖注入。 ### Casbin 配置 ```yaml casbin: model_path: etc/rbac_model.conf # RBAC 模型文件路径 ``` Casbin 复用默认数据源的 GORM DB 存储策略(`casbin_rule` 表),无需额外配置数据源。 ### Redis 配置 ```yaml redis: host: 127.0.0.1 port: 6379 password: "" # 生产环境通过环境变量 APP_REDIS_PASSWORD 注入 db: 0 # 数据库编号 0-15 pool_size: 100 # 连接池大小,建议 CPU核数 × 10 min_idle_conns: 10 # 最小空闲连接数 max_retries: 3 # 命令重试次数 dial_timeout: 3s # 建连超时 read_timeout: 2s # 读超时 write_timeout: 2s # 写超时 conn_max_idle_time: 300s # 空闲连接最大存活时间 ``` ### 日志配置 ```yaml log: level: debug # 日志级别 format: console # console(开发) / json(生产) output: both # stdout / file / both file_path: "logs/${server.name}.log" max_size_mb: 100 # 单文件最大 MB,超过按大小轮转 max_backups: 7 # 保留旧文件数 max_age_days: 30 # 旧文件保留天数 compress: true # gzip 压缩旧文件 sample_burst: 10 # 采样限流:突发窗口内最多通过条数 sample_period: 1s # 采样限流:窗口时长 ``` - 开发环境:控制台彩色输出 - 生产环境:JSON 格式 + 按天按级别分文件 + 采样限流 ## 数据库 初始 SQL 脚本位于 `db/go_rbac_admin.sql`,包含以下表结构: | 表名 | 说明 | |------|------| | `sys_user` | 用户表 | | `sys_role` | 角色表 | | `sys_permission` | 权限表(菜单/按钮) | | `sys_log` | 系统日志表 | | `casbin_rule` | Casbin 策略存储表(gorm-adapter 自动建表,权限关系唯一真相源) | > **注意**:用户-角色、角色-权限的关系数据已迁入 `casbin_rule` 表(g 策略 + p 策略),不再使用独立的 `sys_user_role` / `sys_role_permission` 关联表。 ## 内部工具包 ### async — 并发控制 `internal/pkg/async` 提供类型安全的并发控制工具,覆盖 Go 生产项目 90% 的并发场景。 #### 核心组件 | 组件 | 说明 | 适用场景 | |------|------|---------| | `Group[T]` | 泛型并发任务组,一次性使用 | 批量任务:并发查多个下游、批量发邮件 | | `Pool[T]` | 泛型协程池,goroutine 复用 | 长期运行的高频短任务:RPC 调用池 | | `NoResult` | 无返回值的并发任务组(`Group[struct{}]`) | 只关心成功/失败数,不需要返回值 | | `NoResultPool` | 无返回值协程池(`Pool[struct{}]`) | 长期运行的高频无返回值任务 | | `Go / GoWithTimeout` | 单 goroutine 启动(返回 `*Task`) | 可取消的后台任务 | | `GoResult` | 单 goroutine 异步取结果(返回 `*AsyncResult[T]`) | 异步计算后需要取值 | | `Map / ForEach` | 对切片并发映射/遍历 | 并发处理 URL 列表、批量发邮件 | | `MapChunk / ForEachChunk` | 分批并发映射/遍历 | 大切片分批处理,控制内存 | | `MapChunked / ForEachChunked` | 逐元素分批并发映射/遍历 | 单元素耗时长、需精细控制 | | `Reduce` | 并发归约 | 并发聚合统计 | | `RateLimiter` | 固定速率限制器 | API 限流、下游保护 | | `Pipeline` | 多阶段流水线 | 数据 ETL 管道 | #### Group vs Pool 选择 - **Group**:每次 `Go` 新建 goroutine,任务完成后销毁。适合一次性批量任务 - **Pool**:预启固定数量 worker goroutine,任务通过 channel 分发、复用。适合长期服务中的高频短任务 #### 快速失败 vs 宽容模式 - **默认(宽容)**:所有任务跑完,通过 `FailCount()` 统计失败数。适合"发 100 封邮件,统计成功/失败" - **WithFailFast**:任一任务失败或 panic → 自动 cancel context → 其余任务收到 `ctx.Done()`。适合"查 3 个下游,一个挂了其他也别等" #### 并发数选择 | 函数 | 并发数 | 适用 | |------|--------|------| | `CPU()` | 逻辑 CPU 核心数 | CPU 密集型 | | `IO()` | CPU × 2 | IO 密集型(默认推荐) | | `IOMulti(n)` | CPU × n | IO 密集型,自定义倍数 | | `WithConfig(n)` | n > 0 用 n,否则回退 `IO()` | 配置化并发数 | #### 使用示例 ```go // 示例 1:并发查多个下游拼详情页 g, ctx := async.NewGroup[string](async.IO()).WithTraceID(context.Background()) g.Go(ctx, func(ctx context.Context) (string, error) { return fetchUser(ctx, id) }) g.Go(ctx, func(ctx context.Context) (string, error) { return fetchOrders(ctx, id) }) g.Go(ctx, func(ctx context.Context) (string, error) { return fetchProfile(ctx, id) }) results := g.Wait() // 示例 2:快速失败——一个挂了其他也别等 g, ctx := async.NewGroup[string](5).WithFailFast(context.Background()) g.Go(ctx, func(ctx context.Context) (string, error) { return fetchA(ctx) }) g.Go(ctx, func(ctx context.Context) (string, error) { select { case <-ctx.Done(): return "", ctx.Err() default: return fetchB(ctx) } }) results := g.Wait() // 示例 3:批量发邮件,统计成功/失败 g, ctx := async.NewGroup[bool](async.IO()).WithTraceID(context.Background()) for _, email := range emails { email := email g.Go(ctx, func(ctx context.Context) (bool, error) { return true, sendEmail(ctx, email) }) } results := g.Wait() fmt.Printf("成功 %d, 失败 %d\n", g.SuccessCount(), g.FailCount()) // 示例 4:协程池——长期复用 pool := async.DefaultPool[string]() defer pool.Close() pool.Submit(ctx, func(ctx context.Context) (string, error) { return callRPC(ctx, req) }) results := pool.Wait() pool.Reset() // 示例 5:Map——对切片并发映射 results := async.Map(ctx, urls, async.IO(), func(ctx context.Context, url string) (string, error) { return httpGet(ctx, url) }) // 示例 6:单 goroutine 可取消后台任务 task := async.Go(ctx, func(ctx context.Context) { _ = sendNotification(ctx, orderID) }) task.Cancel() // 提前取消任务 err := task.Wait() // 等待完成 // 示例 7:异步取结果 ar := async.GoResult(ctx, func(ctx context.Context) (string, error) { return compute(ctx) }) value, err := ar.Wait() ``` #### Group 方法一览 | 分类 | 方法 | 说明 | |------|------|------| | **配置** | `WithTraceID` / `WithContext` / `WithFailFast` / `WithTimeout` / `WithSubmitTimeout` | 链式配置,返回新 Group + ctx | | **提交** | `Go` / `GoWithTimeout` / `GoAt` / `GoAtWithTimeout` | 提交任务,支持指定索引和超时 | | **等待** | `Wait` / `WaitTimeout` / `WaitContext` | 阻塞等待全部任务完成 | | **统计** | `SuccessCount` / `FailCount` / `HasError` / `TotalCount` / `Active` / `Busy` / `Stats` | 任务执行统计 | | **错误** | `Errors` / `FirstError` / `JoinErrors` | 错误收集 | | **结果** | `Values` | 提取所有成功值 | | **重置** | `Reset` | 清空结果,复用 Group 处理下一批 | ### jwtx — JWT 签发与解析 `internal/pkg/jwtx` 基于 `golang-jwt/jwt/v5` 提供 HS256 JWT 签发与解析,采用实例化 + 依赖注入范式: ```go // 1. 启动时构造实例(ServiceContext 中) j := jwtx.New(cfg.JWT.Secret, cfg.JWT.Issuer, cfg.JWT.Expire) // 2a. 登录成功后签发 token(便捷方法,屏蔽 Claims 细节) token, err := j.SignUser(u.ID, u.Username) // 2b. 或使用完整方法(需手动构造 Claims) token, err := j.Generate(jwtx.Claims{UserID: u.ID, Username: u.Username}) // 3a. 后续请求解析 token(便捷方法,直接返回业务字段) uid, name, err := j.ParseUser(tokenStr) ginutil.SetUserID(c, uid) ginutil.SetUsername(c, name) // 3b. 或使用完整方法(返回完整 Claims) claims, err := j.Parse(tokenStr) ginutil.SetUserID(c, claims.UserID) ginutil.SetUsername(c, claims.Username) ``` **Claims 结构**:嵌入标准 `RegisteredClaims`,业务字段 `UserID` / `Username` / `RoleID` 统一用 string(与 `gormx.Base.ID` UUID 一致)。 **安全特性**:构造后无内部可变状态,goroutine 安全。密钥不写进 yml,生产环境通过环境变量 `APP_JWT_SECRET` 注入。 ### passx — 密码加密与校验 `internal/pkg/passx` 基于 `golang.org/x/crypto/bcrypt` 提供密码哈希与校验: ```go // 加密(自动加盐,同一明文每次生成不同哈希) hashed, err := passx.Hash("plaintext") // 校验 ok := passx.Verify("plaintext", hashed) ``` **内置信号量并发控制**:bcrypt 是 CPU 密集型操作(cost=10 ≈ 100ms/次),高并发下若不限制会打满 CPU。`passx` 内置信号量(默认 = CPU 核数),保证同时最多 `NumCPU()` 个哈希在计算,其余阻塞等待,防止 CPU 争用影响其他请求。 | 参数 | 默认值 | 说明 | |------|--------|------| | `DefaultCost` | 10 | bcrypt cost,≈ 100ms/次,随硬件升级可调 | | `MinCost` | 4 | 最小 cost | | `MaxCost` | 31 | 最大 cost | **方法一览**: | 方法 | 说明 | |------|------| | `Hash(password)` | 使用默认 cost(10) 加盐哈希,返回哈希字符串 | | `HashWithCost(password, cost)` | 自定义 cost(4-31) 加盐哈希 | | `Verify(password, hash)` | 校验明文密码是否匹配,返回 error | | `Matches(password, hash)` | 校验明文密码是否匹配,返回 bool(不关心具体错误原因) | ### httpx — HTTP 客户端 `internal/pkg/httpx` 基于 `github.com/go-resty/resty/v2` 封装,Go 生产项目最主流的 HTTP 客户端库。 **实例化 + 依赖注入范式**: ```go // 1. ServiceContext 中构造一次 client := httpx.New(httpx.Config{ BaseURL: "https://api.example.com", Timeout: 10 * time.Second, RetryCount: 2, }) // 2. 业务层一行调用 var user UserResp resp, err := client.Get(ctx, "/user/1", &user) resp, err := client.Post(ctx, "/user", reqBody, &result) resp, err := client.Put(ctx, "/user/1", reqBody, &result) resp, err := client.Delete(ctx, "/user/1", &result) // 3. 带 query 参数的 GET resp, err := client.GetQuery(ctx, "/users", map[string]string{"page": "1"}, &list) // 4. 表单提交 resp, err := client.PostForm(ctx, "/login", map[string]string{"user": "tom"}, &result) ``` **设计要点**: | 要点 | 说明 | |------|------| | 自动序列化 | body 传 any,`SetBody` 自动 JSON 序列化 | | 自动反序列化 | result 传指针,`SetResult` 自动 JSON 反序列化 | | 自动日志 | 每次请求打 Info/Warn/Error,含 status、latency、size | | 自动 trace_id | 通过 `SetContext(ctx)` 传递,zerolog 自动关联 | | 无内部状态 | 构造后 goroutine 安全,可全局复用 | | 高级自定义 | 通过 `R(ctx)` 返回 `*resty.Request`,可调用 resty 全部能力 | ### captchax — 验证码 `internal/pkg/captchax` 基于 `github.com/mojocn/base64Captcha` 封装,Go 生产项目最主流的验证码库。 **默认六大决策**: | 决策 | 默认值 | 说明 | |------|-------|------| | 类型 | 数字验证码 | 4 位纯数字,最高可用性 | | 尺寸 | 80×240 | 适合移动端和 PC 端 | | 存储 | Redis | 复用项目 Redis,支持分布式 | | 过期 | 5 分钟 | 防止暴力破解 | | 销毁 | 验证后立即删除 | 防止重复使用 | | 输出 | base64 图片 | 前端直接渲染,无需临时文件 | **实例化 + 依赖注入范式**: ```go // 1. ServiceContext 中构造一次(复用项目 Redis) captcha := captchax.New(captchax.Config{ Redis: dataMod.RDB.Client, // 复用项目 go-redis 客户端(需传 *redis.Client 接口) Length: 4, TTL: 5 * time.Minute, }) // 2. API 层:生成验证码,返回 id + base64 图片 id, b64img, err := captcha.Generate(ctx) // 前端: // 3. 登录接口:校验验证码 if !captcha.Verify(ctx, id, userInput) { return errors.New("验证码错误") } ``` ### safe — goroutine 安全启动 `internal/pkg/safe` 提供 `GoSafe` 函数,启动带 panic recover 的 goroutine: ```go // 线上每个后台 goroutine 都应通过 GoSafe 启动,避免 panic 导致整个进程退出 safe.GoSafe(func() { // 后台任务逻辑 }) ``` ### trace — trace_id 生成与传递 `internal/pkg/trace` 作为 middleware 与 ginutil 的公共底层依赖,提供 trace_id 的注入与读取: | 方法 | 说明 | |------|------| | `NewID()` | 生成 32 字符十六进制 trace_id(对齐 OTel 格式) | | `SetTraceID(c, id)` | 将 trace_id 写入 gin.Context | | `GetTraceID(c)` | 从 gin.Context 读取 trace_id | 常量 `Header = "X-Trace-Id"` 为请求/响应头字段名,`CtxKey = "trace_id"` 为 gin.Context 存储键名。 ### gormx — GORM 扩展工具 `internal/pkg/gormx` 提供 GORM 扩展工具: #### Base — UUID 主键自动生成 嵌入 `gormx.Base` 的模型自动获得 UUID 主键: ```go type User struct { gormx.Base // ID(string) + CreatedTime + UpdatedTime,BeforeCreate 自动生成 UUID Name string } ``` #### ChunkIDs — IN 查询分批 解决 IN 列表过长问题(MySQL 参数绑定上限 65535、SQLite 上限 999): ```go users, err := gormx.ChunkIDs[model.User](ctx, db, ids, 1000, func(db *gorm.DB, batch []string) ([]*model.User, error) { var list []*model.User if err := db.Where("id IN ?", batch).Find(&list).Error; err != nil { return nil, err } return list, nil }) ``` 每批自动去重合并,结果集顺序不保证。 ### jsonx — 带日志的 JSON 序列化 `internal/pkg/jsonx` 封装标准库 `encoding/json`,在序列化/反序列化时自动记录日志(携带 trace_id): ```go // 序列化 data, err := jsonx.Marshal(ctx, user) // 成功打 Info,失败打 Error data := jsonx.MustMarshal(ctx, config) // 失败返回 nil // 反序列化 var user User err := jsonx.Unmarshal(ctx, data, &user) // 成功打 Info,失败打 Error ``` ### Redis 键名约定 `internal/data/rediskeys.go` 定义 Redis 键名生成规则,统一前缀 `rbac:{env}:`,按环境隔离: ```go // 用户相关 data.UserKey.Info(userID) // rbac:dev:user:{id} data.UserKey.Token(userID) // rbac:dev:token:user:{id} data.UserKey.Roles(userID) // rbac:dev:user:{id}:roles data.UserKey.Perms(userID) // rbac:dev:user:{id}:perms data.UserKey.Profile(userID) // rbac:dev:user:{id}:profile data.UserKey.LoginFail(userID) // rbac:dev:login:fail:{id} data.UserKey.LoginLock(userID) // rbac:dev:lock:login:{id} // 角色相关 data.RoleKey.Permissions(roleID) // rbac:dev:role:{id}:perms data.RoleKey.Detail(roleID) // rbac:dev:role:{id}:detail ``` ### jwtx 补充 — SignUser / ParseUser 便捷方法 除 `Generate` / `Parse` 外,`jwtx` 还提供屏蔽 Claims 细节的便捷方法: ```go // 签发 token(业务层无需感知 Claims 结构) token, err := j.SignUser(user.ID, user.Username) // 解析 token(直接返回业务字段) uid, name, err := j.ParseUser(tokenStr) ``` ### async 补充 — 高级功能 `async` 包还提供以下高级并发控制组件: #### RateLimiter — 速率限制 按固定速率控制操作频率,支持突发容量(token bucket 风格): ```go // 每秒 100 次,前 5 次立即通过 rl := async.NewRateLimiterWithBurst(100, time.Second, 5) defer rl.Stop() for i := 0; i < 1000; i++ { if err := rl.Wait(ctx); err != nil { break // ctx 取消或已停止 } // 执行受限操作 } ``` #### Pipeline — 多阶段流水线 将输入依次通过多个阶段(stage)处理,任一阶段失败则终止: ```go result, err := async.NewPipeline[string](ctx, func(ctx context.Context, input string) (string, error) { return extract(ctx, input) }, func(ctx context.Context, input string) (string, error) { return transform(ctx, input) }, func(ctx context.Context, input string) (string, error) { return load(ctx, input) }, ).Run("raw_data") ``` #### AsyncResult — 异步计算结果 封装 channel 为易用 API,支持阻塞等待、超时等待、非阻塞检查: ```go ar := async.GoResult(ctx, func(ctx context.Context) (string, error) { return compute(ctx, input) }) value, err := ar.Wait() // 阻塞等待 value, err, ok := ar.WaitTimeout(5*time.Second) // 带超时 select { case <-ar.Done(): ... default: ... } // 非阻塞检查 ``` #### Task — 可取消的后台任务 ```go task := async.Go(ctx, func(ctx context.Context) { // 长时间运行的任务,可通过 task.Cancel() 提前取消 }) task.Cancel() // 取消任务 err := task.Wait() // 等待完成 if task.IsPanic() { /* 任务 panic */ } ``` #### Map/ForEach 变体一览 | 变体 | 行为 | |------|------| | `Map` / `ForEach` | 宽容模式,所有任务跑完 | | `MapWithFailFast` / `ForEachWithFailFast` | 快速失败,任一失败立即返回 | | `MapWithTimeout` / `ForEachWithTimeout` | 带超时 | | `MapWithFFTimeout` / `ForEachWithFFTimeout` | 快速失败 + 超时 | | `Default*` 系列 | 使用 `IO()` 默认并发数 | #### MapChunk / ForEachChunk — 分批并发 将大切片按 `batchSize` 分批,每批作为一个并发任务: ```go // 每批 100 个 URL,并发处理 results := async.MapChunk(ctx, urls, async.IO(), 100, func(ctx context.Context, chunk []string) (string, error) { return batchProcess(ctx, chunk) }) ``` #### MapChunked / ForEachChunked — 逐元素分批并发 将大切片分批,但每批内逐元素并发处理,适合单元素处理耗时长、需要精细控制的场景。 #### Reduce — 并发归约 ```go result, err := async.DefaultReduce(ctx, items, func(acc *Stats, val Result[int]) (*Stats, error) { acc.Sum += val.Value return acc, nil }, ) ``` #### NoResultPool — 无返回值协程池 `NoResultPool` 是 `Pool[struct{}]` 的类型别名,适合只关心成功/失败数、不需要返回值的长期任务场景。 #### 哨兵错误一览 | 错误 | 说明 | |------|------| | `ErrPoolClosed` | 池已关闭,提交的任务被丢弃 | | `ErrSubmitTimeout` | 提交超时(worker 长时间无空闲) | | `ErrGroupWaited` | Group 已调用 Wait,之后再 Go 提交被丢弃 | | `ErrGroupWaiting` | Group 的 Wait 正在执行中,此时 Go 提交被丢弃 | | `ErrSkipped` | 任务因前序失败被跳过(快速失败场景) | | `ErrRateLimiterStopped` | 速率限制器已停止 | | `ErrTimeout` | 操作超时 | | `ErrPoolWaiting` | Pool 已调用 Wait,之后再 Submit 提交被丢弃 | #### 全局超时配置 ```go async.SetDefaultTimeout(0) // 关闭全局默认超时,适合长任务场景 async.SetSubmitTimeout(10 * time.Second) // 设置 Pool.Submit 等待超时 async.SetMaxCleanupDuration(10 * time.Minute) // 设置 WaitTimeout 清理 goroutine 最大存活时间 ```