name: goddd description: GoDDD 六边形架构开发指南。当使用 goddd 架构实现代码、创建新领域、新增 CRUD、数据库表定义、领域间依赖解耦、领域内子包拆分与依赖、排序功能、Core 层需要 HTTP 请求信息时使用此技能。也应在以下隐含场景主动触发:新增业务模块、讨论 Core/Store/API 分层、使用 godddx 生成代码、实现适配器模式、添加 Wire provider、使用 web.WrapH/PagerFilter/DateFilter/WithContext 等框架工具、Core 需要后台任务/定时任务/心跳检测/goroutine、优雅停机、Wire 循环依赖、Core 生命周期分离、SessionHandler、修改 store/xxxcache 缓存层(判断内存缓存 vs Redis 缓存、SETNX/SETEX 防竞态、WarmUp 预热)、领域内子包间依赖方向、子包是否需要接口隔离、子包循环依赖处理。即使用户没有提到"goddd",只要涉及六边形架构、领域驱动、依赖倒置、CRUD 生成、Core 职责过重、缓存层改造、子包拆分依赖等概念,都应使用此技能。
GoDDD 六边形架构开发指南
本技能指导在 GoDDD 六边形架构下进行开发,覆盖代码生成、领域间解耦、排序方案、HTTP 上下文透传、Web 工具使用等核心场景。
遇到不确定的写法时,优先参考项目中已有的符合规范的领域代码。
目录
- 架构概览
- godddx 代码生成
- 参数定义规范
- 领域间解耦(适配器模式)
- 领域内子包依赖
- 排序功能实现
- WithContext:Core 层获取 HTTP 信息
- Core 生命周期分离
- Web 工具函数速查
- Store 缓存层规范
- API 层规范
详细参考文档在 references/ 目录下,实现对应功能时必须先阅读:
references/sort.md— 实现拖拽排序时阅读(接收有序 ID 数组、重分配 sort 值)references/with-context.md— Core/Adapter 需要 HTTP 请求信息(scheme、host、IP)时阅读references/adapter-pattern.md— 新增领域间依赖、实现 Port/Adapter/Option 注入时阅读references/web-toolkit.md— 使用 WrapH、PagerFilter、DateFilter、SSE 等 web 包工具时阅读references/lifecycle-split.md— Core 需要后台 goroutine(定时任务、心跳检测)且遇到 Wire 循环依赖时阅读;体现 SRP:Core 值类型专注业务,生命周期委托给独立 Handler,避免 Core 职责过重references/cache-layer.md— 修改 store/xxxcache 时阅读;判断内存/Redis 缓存、SETNX/SETEX 防竞态、WarmUp 预热、API 层装配
架构概览
┌──────────────────────────────────────────────────────────┐
│ API 层 (主动适配器) │
│ internal/web/api/ │
│ 职责: HTTP 协议转换 → 调用 Core → 返回响应 │
└──────────────────────┬───────────────────────────────────┘
│ 依赖
▼
┌──────────────────────────────────────────────────────────┐
│ Core 层 (领域层/业务核心) │
│ internal/core/<domain>/ │
│ │
│ ├─ core.go Core 结构体 + Storer 接口 │
│ ├─ port.go 被动适配器接口 │
│ ├─ doc.go 领域说明 │
│ ├─ model.go 非 GORM 类型定义 │
│ ├─ <entity>.go 业务方法 + EntityStorer 接口 │
│ ├─ <entity>.model.go 领域模型 (GORM 映射) │
│ ├─ <entity>.param.go List/Create/Update Input 参数 │
│ ├─ <provider>adapter/ 对外提供的适配器实现 │
│ └─ store/<domain>db/ 数据库实现 (被动适配器) │
└──────────────────────────────────────────────────────────┘
依赖方向:API → Core ← Store/Adapter(外层依赖内层,内层通过接口反转依赖)
Core 值类型选项:当 Core 需要后台 goroutine 且出现 Wire 循环依赖时,可将生命周期剥离到独立的
Handler结构体,Core 内嵌其指针作为字段。Core 保持值类型,职责仅为业务逻辑;Handler 负责 goroutine、ctx、cancel、优雅停机。详见references/lifecycle-split.md。
godddx 代码生成
CRUD 场景必须使用 godddx 生成代码,确保结构一致。
步骤
- 在
tables/<domain>/下创建表定义文件 - 结构体必须包含
ID、CreatedAt、UpdatedAt字段 - 若使用随机字符 ID,使用
uniqueid.Core类型 - 同一领域多个结构体放在同一个 tables 文件中
- 执行生成:
godddx -f tables/<domain>/<entity>.go - 在
internal/web/api/provider.go注册 Wire provider - 调用生成的
Register<Domain>函数注册路由 - 在领域目录下创建
doc.go描述领域用途
表定义示例
// tables/task/task.go
package task
import (
"time"
"github.com/ixugo/goddd/domain/uniqueid"
)
type Task struct {
ID uniqueid.Core `gorm:"primaryKey"`
Name string
Status int
CreatedBy string
Sort int64 `gorm:"autoIncrement"`
CreatedAt time.Time
UpdatedAt time.Time
}
Wire 注册模式
在 provider.go 中添加 NewXxxCore 和 NewXxxAPI:
var ProviderSet = wire.NewSet(
wire.Struct(new(Usecase), "*"),
NewHTTPHandler,
// ... 已有 provider
NewTaskCore, NewTaskAPI, // 新增领域
)
func NewTaskCore(db *gorm.DB) task.Core {
store := taskdb.NewDB(db).AutoMigrate(orm.GetEnabledAutoMigrate())
return task.NewCore(store)
}
参数定义规范
核心原则:归属字段(TenantID、CreatedBy)由 API 层填充,编辑时不可修改。
ListInput — 查询参数
type ListEntityInput struct {
web.PagerFilter // 分页
web.DateFilter // 日期范围(start_ms, end_ms 毫秒时间戳)
Name string `form:"name"` // 模糊查询字段
TenantID string `form:"-"` // API 层填充
CreatedBy string `form:"-"` // API 层填充
}
CreateInput — 新增参数
type CreateEntityInput struct {
Name string `json:"name"`
TenantID string `json:"-"` // API 层填充
CreatedBy string `json:"-"` // API 层填充
}
UpdateInput — 编辑参数
type UpdateEntityInput struct {
Name string `json:"name"`
// 不包含 TenantID、CreatedBy 等归属字段
}
领域间解耦
领域间必须通过适配器解耦,不能直接依赖其他领域的 Core。
核心规则
| 规则 | 说明 |
|---|---|
| Port 定义在提供方 | 接口和模型定义在提供能力的领域子包中 |
| Adapter 实现在提供方 | 适配器放在 <provider>adapter/ 子包 |
| 消费方通过 Option 注入 | NewCore(store, opts...) 模式 |
| 返回类型定义在提供方子包 | 避免重复定义 |
Option 注入模式
// 消费方 core.go
type Core struct {
store Storer
userProvider useradapter.BriefProvider
}
type Option func(*Core)
func WithUserProvider(p useradapter.BriefProvider) Option {
return func(c *Core) { c.userProvider = p }
}
func NewCore(store Storer, opts ...Option) Core {
c := Core{store: store}
for _, opt := range opts { opt(&c) }
return c
}
API 层注入
func NewMessageCore(db *gorm.DB, briefProvider useradapter.BriefProvider) message.Core {
store := messagedb.NewDB(db).AutoMigrate(orm.GetEnabledAutoMigrate())
return message.NewCore(store,
message.WithUserProvider(briefProvider),
)
}
详细示例和完整代码请阅读
references/adapter-pattern.md
领域内子包依赖
同一领域目录下(如 internal/core/sms/)可能存在多个子包(如 scheduler、store、adapter 等),它们共同组成该领域的完整实现。与跨领域解耦规则不同,领域内子包间的依赖更直接灵活。
核心规则
| 规则 | 说明 |
|---|---|
| 子包单向依赖根包 | 所有子包都可以 import 领域根包(如 sms),根包不感知子包 |
| 子包间可直接依赖 | 子包 A 可直接 import 子包 B,无需通过接口隔离 |
| 禁止循环依赖 | 若子包 A 和 B 需要双向引用,一方直接依赖,另一方通过接口依赖 |
| 基础设施仍需接口 | 隔离 MQ 客户端、数据库驱动等外部依赖的接口仍定义在根包(一般在 port.go 中) |
判断是否需要接口
需要接口的场景:
├── 实现方在领域外(MQ 客户端、DB 驱动、跨领域适配器)
├── 子包间存在双向依赖(一方用接口打破循环)
└── 需要可测试性(mock 外部服务)
不需要接口的场景:
├── 子包 A 单向依赖子包 B(直接 import)
├── 子包依赖根包的类型/常量(直接引用)
└── 子包内部的辅助函数/工具类型
示例拓扑
domain/ ← 领域根包(定义端口接口、模型、共享类型)
├── sub-a/ ← 单向依赖 domain(import domain 根包)
├── sub-b/ ← 单向依赖 domain
├── sub-infra/ ← 单向依赖 domain(实现 domain.Publisher 等接口)
└── store/domaindb/ ← 单向依赖 domain(实现 domain.Storer)
子包之间若无交叉依赖,全部通过根包共享类型和接口定义。若未来 sub-a 需要调用 sub-b 的某个方法,可直接 import sub-b;若反向也需要,则一方定义 narrow interface 打破循环。
排序功能实现
实现拖拽排序:接收有序 ID 数组,重新分配 sort 值而不影响未传入的记录。
核心逻辑
- 查询传入 ID 的记录,获取现有
sort值 - 将
sort值升序排列 - 按传入 ID 顺序重新分配排序值
- 事务批量更新
完整实现代码请阅读
references/sort.md
要点
- 数据库字段
sort使用 gorm tagautoIncrement自增 - Store 层用事务批量更新,Core 层编排逻辑,API 层只做协议转换
- 校验所有 ID 存在,不存在则返回错误
WithContext
解决 Core 层不依赖 HTTP 框架、但 Adapter 需要 HTTP 请求信息的矛盾。
原理
web.Context 扩展了 context.Context,携带 *http.Request,通过类型断言按需获取:
// API 层 — 构造 web.Context
ctx := web.WithContext(c.Request)
core.DoSomething(ctx, ...)
// Adapter 层 — 类型断言获取 HTTP 信息
func (p *impl) resolveCover(ctx context.Context, cover string) string {
if wc, ok := ctx.(web.Context); ok {
return p.coverURLFunc(wc.Request(), cover)
}
return cover // 非 HTTP 场景降级
}
设计要点
| 特性 | 说明 |
|---|---|
| 零破坏性 | 实现 context.Context,现有签名无需修改 |
| 渐进式采用 | 只改调用处(API)和使用处(Adapter),Core 层透传 |
| 优雅降级 | 类型断言失败时返回原始值,非 HTTP 场景正常工作 |
| 可扩展 | 接口可定义子接口扩展 |
适用场景
- 动态 URL 拼接(需要 scheme + host)
- 请求级元信息透传(IP、User-Agent)
- 跨领域 Adapter 需要 HTTP 上下文辅助数据转换
不适用
- 已有明确参数的简单场景 → 直接传参
- 与 HTTP 无关的逻辑 → 使用标准
context.Context - 需要修改请求状态 → 在 API 层处理
完整设计文档请阅读
references/with-context.md
Core 生命周期分离
触发条件:Core 需要后台 goroutine,且 Wire 注入因值/指针类型冲突产生循环依赖。 设计原则:代码可读性第一,避免 Core 职责过重(SRP)。
核心思想
将 goroutine、ctx、cancel、quit 等生命周期逻辑从 Core 中剥离,委托给独立的 Handler 结构体:
| 结构体 | 类型 | 职责 |
|---|---|---|
Core |
值类型 | 纯业务逻辑(查询 DB、计算、编排) |
XxxHandler |
指针类型,Core 内嵌 | goroutine 启动/停止、ctx 管理、优雅停机 |
方法分配原则
- Core 方法:查询 DB、纯业务计算(直接实现)
- Core 委托方法:需要转发给 Handler 的公开方法(一行转发,不含业务逻辑)
- Handler 方法:goroutine 内部调用的私有逻辑
// Core 委托方法:只转发,不含业务逻辑
func (c Core) TrackHeartbeat(mediaID, sessionID, userID string, t int) {
c.ss.TrackHeartbeat(mediaID, sessionID, userID, t)
}
func (c Core) Close() { c.ss.Close() }
Wire 注入
NewCore 返回值类型 Core + 清理函数 func(),Wire 无需处理指针:
func NewXxxCore(...) (xxx.Core, func()) { ... }
完整结构、构造函数、注意事项请阅读
references/lifecycle-split.md
Web 工具函数速查
github.com/ixugo/goddd/pkg/web 提供 HTTP 开发基础设施。
完整函数签名和使用示例请阅读
references/web-toolkit.md
请求处理
| 函数/类型 | 用途 |
|---|---|
WrapH(fn) |
核心包装函数,将 func(*gin.Context, *Input) (Output, error) 包装为 gin.HandlerFunc |
WrapHs(fn, mid...) |
同 WrapH,附加前置中间件 |
PagerFilter |
分页参数(Page, Size, Sort, SortSafelist),含 Offset(), Limit(), SortColumn() |
NewPagerFilterMaxSize() |
不分页查询(Size=99999) |
DateFilter |
日期范围(StartMs, EndMs 毫秒时间戳),含 StartAt(), EndAt(), DefaultStartAt(), DefaultEndAt() |
Validator |
参数校验,Check(ok, key, msg), AddError(key, msg), Valid(), List() |
CustomMethods |
自定义方法路由 |
Limit(v, minV, maxV) |
将整数值限制在 [minV, maxV] 区间内 |
Offset(page, size) |
计算分页偏移量(page 从 1 开始) |
响应处理
| 函数/类型 | 用途 |
|---|---|
PageOutput[T] |
分页响应 {Items, Total} |
ScrollPageOutput[T] |
滚动分页 {Items, Next} |
Success(c, data) |
统一成功响应 |
Fail(c, err) |
统一错误响应,自动映射 HTTP 状态码,不打断后续 handler |
AbortWithStatusJSON(c, err) |
错误响应并 Abort,打断后续 handler(用于中间件) |
ResponseMsg |
通用消息响应 {Msg} |
Context 与 URL
| 函数/类型 | 用途 |
|---|---|
WithContext(r) |
*http.Request → web.Context,携带 HTTP 元信息 |
GetBaseURL(r) |
提取 scheme://host |
BaseURLJoin(r,...string) |
拼接 scheme://host/fullpath |
GetHost(r) / GetScheme(r) |
提取 host / scheme |
XForwardedPrefix(r, path) |
处理反向代理前缀 |
TraceID(ctx) / MustTraceID(ctx) |
获取请求追踪 ID |
SetTraceID(ctx, id) |
设置追踪 ID |
JWT 鉴权
| 函数 | 用途 |
|---|---|
NewToken(data, secret, opts...) |
创建 JWT(默认 6h 过期) |
ParseToken(token, secret) |
解析 JWT |
AuthMiddleware(secret) |
JWT 鉴权中间件 |
AuthLevel(level) |
等级鉴权中间件(等级越小权限越大) |
NewClaimsData() |
创建 Claims 数据,链式 SetUserID/SetUsername/SetRoleID/SetLevel/Set |
GetUID/GetUsername/GetRoleID/GetLevel/GetToken/GetInt |
从上下文获取用户信息 |
WithExpires(duration) |
Token Option:设置相对过期时长 |
WithExpiresAt(time.Time) |
Token Option:设置绝对过期时间 |
WithIssuedAt(time.Time) |
Token Option:设置签发时间 |
WithIssuer(issuer) |
Token Option:设置签发人 |
WithNotBefore(time.Time) |
Token Option:设置生效时间 |
中间件
| 函数 | 用途 |
|---|---|
Logger(ignoreFn...) |
请求日志 |
LoggerWithBody(limit, ignoreFn...) |
记录请求体/响应体 |
LoggerWithUseTime(maxLimit, ignoreFn...) |
耗时记录,超时打 warn |
RateLimiter(r, b) |
全局限流 |
IPRateLimiterForGin(r, b) |
按 IP 限流 |
IDRateLimiter(r, b, ttl) |
按 ID 限流 |
LimitContentLength(limit) |
请求体大小限制 |
CacheControlMaxAge(second) |
Cache-Control 头 |
EtagHandler() |
ETag + 304 支持 |
Recover() |
panic 恢复 |
SetDeadline(duration) |
读写超时 |
Metrics() |
请求计数统计 |
SSE(Server-Sent Events)
| 函数/类型 | 用途 |
|---|---|
NewSSE(length, timeout) |
创建 SSE 实例 |
SSE.Publish(event) |
发布事件 |
SSE.ServeHTTP(w, r) |
实现 http.Handler |
SendChunk(ch, c) / SendChunkPro(ch, c) |
分块进度发送 |
NewEventMessage(event, data) |
创建 SSE 事件消息 |
忽略选项(用于日志/限流中间件)
| 函数 | 用途 |
|---|---|
IgnorePrefix(prefix...) |
忽略路径前缀 |
IgnorePath(path...) |
忽略完整路径 |
IgnoreMethod(method) |
忽略 HTTP 方法 |
IgoreContains(substrs...) |
忽略路径含子串 |
IgnoreBool(v) |
固定布尔值忽略 |
性能分析
| 函数 | 用途 |
|---|---|
SetupPProf(r, &ips) |
注册 pprof 路由,IP 白名单 |
SetupMutexProfile(rate) |
启用互斥锁采样 |
CountGoroutines(d, num) |
记录 goroutine 数量 |
WrapH 入参规则
- POST/PUT/DELETE/PATCH → 绑定 Request Body(
jsontag) - GET → 绑定 URL Query(
formtag) - 入参第二个参数必须是指针,
*struct{}表示无参数 - 路由参数用
c.Param()获取,不走自动绑定
错误处理
Core 层返回 reason.Error 类型错误,web.WrapH 自动映射 HTTP 状态码:
return nil, reason.ErrBadRequest.SetMsg("参数不合法") // → 400
return nil, reason.ErrDB.Withf("查询失败: %s", err) // → 500
return nil, reason.ErrUnauthorized.SetMsg("未登录") // → 401
SetMsg()— 给用户的友好提示Withf()— 给开发者的 details(SetRelease()后不输出)
Store 缓存层规范
修改 store/<domain>cache/ 时,首先判断是内存缓存(conc.Cacher)还是 Redis 缓存(*redis.Client)。
若为 Redis 缓存:删除 conc.Cacher 依赖,换 *redis.Client,使用 SETNX/SETEX 防竞态。
核心规则
| 操作 | Redis 命令 | 理由 |
|---|---|---|
| 读穿透回填 | singleflight.Do + SetNX |
合并并发穿透 + 不覆盖写入的新值 |
| Create / Update | Set(ctx, key, val, ttl) |
写完 DB 后用最新值覆盖缓存 |
| Delete | Expire(key, 3s) |
墓碑保护期 3s,防 SetNX 回填已删记录 |
| WarmUp | SetNX |
不覆盖运行期间已更新的缓存 |
完整改造步骤、代码模板和 key 命名规范请阅读
references/cache-layer.md
API 层规范
- 只做 HTTP 协议转换:参数绑定 → 填充归属字段 → 调用 Core → 返回响应
- 归属字段在 API 层填充:TenantID、CreatedBy 等通过
json:"-"/form:"-"标记 - 路由参数用
c.Param:不走自动绑定,仅路由参数时入参用_ *struct{} - 适配器不定义在 API 层:统一放在领域的
<provider>adapter/目录 - Handler 若需访问后续赋值字段:使用指针接收者
路由注册模式
func registerTask(r gin.IRouter, api TaskAPI, handler ...gin.HandlerFunc) {
g := r.Group("/tasks", handler...)
g.GET("", web.WrapH(api.listTasks))
g.POST("", web.WrapH(api.createTask))
g.GET("/:id", web.WrapH(api.getTask))
g.PUT("/:id", web.WrapH(api.updateTask))
g.DELETE("/:id", web.WrapH(api.deleteTask))
g.PUT("/sort", web.WrapH(api.sortTasks))
}