关于 go-architect
用于设计和审查 Go 服务的开发技能,强调显式依赖、优先使用标准库路由,以及实用的项目结构指导。适合实现或评估 Go 应用的开发者。
主要功能
- 01Go 1.22+ ServeMux 路由指导
- 02通过构造函数注入依赖
- 03扁平或模块化项目结构建议
- 04HTTP 服务优雅关闭
- 05实现审查的硬性检查
适用场景
- 01搭建新的 Go 项目
- 02审查 HTTP 服务的依赖装配
- 03实现响应信号的服务关闭
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill go-architect安装后在 Claude Code 中输入/go-architect确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/dependency-injection.mdgraceful-shutdown.mdproject-structure.md详细介绍
Go 首席架构师
快速参考
| 主题 | 参考资料 |
|---|---|
| 扁平与模块化项目布局、迁移信号 | references/project-structure.md |
| 信号处理与优雅关闭 | references/graceful-shutdown.md |
| 依赖注入模式、测试接缝 | references/dependency-injection.md |
核心原则
- 优先使用标准库 —— 使用
net/http和 Go 1.22+ 增强版ServeMux进行路由。只有在有明确需求且标准库无法满足时,才考虑使用框架(chi、echo、gin),例如复杂的中间件链或正则路由。 - 依赖注入优于全局变量 —— 通过结构体字段和构造函数传递数据库、日志记录器和服务,绝不使用包级
var。 - 明确优于魔法 —— 不要使用
init()副作用,也不要使用框架自动装配。main.go是组合根,所有内容都应在其中显式组装。 - 接口小,结构体大 —— 在使用方定义接口,并保持接口精简(1-3 个方法)。由具体类型承载实现。
硬性门槛
在实现或审查声称遵循此技能的工作时,请按以下顺序操作。不要跳步;每一步都有可通过工具或具体文件路径验证的通过条件。
- 工具链与 API — 如果代码使用 Go 1.22+ 的
ServeMux特性(如"GET /x/{id}"这样的 method+path 模式、r.PathValue或{path...}):运行go version,只有报告的工具链为 go1.22+ 才算通过。如果项目必须继续使用较旧版本的 Go,则只有不使用这些 API(改用兼容的路由器或旧版模式)并在审查或 PR 中说明,才算通过。 - 组合根 — 当
main.go或cmd/.../main.go明确构造服务器并注入共享依赖项(DB、日志记录器、配置)时,才算通过。如果共享依赖项通过init()或包级var而非在main(或由main调用的run())中显式构造来装配,则算不通过。 - 生产环境 HTTP 关闭 — 对于长期运行的 HTTP 服务,只有在等待
signal.NotifyContext(或等效机制)后,使用带有限时长的上下文(例如context.WithTimeout)调用http.Server.Shutdown,才算通过。报告时请注明文件路径;完整模式请参阅 references/graceful-shutdown.md。 - 处理程序中不得使用环境变量或全局变量 — 当处理程序和领域代码通过结构体或参数获取依赖项时,才算通过。如果处理程序通过
os.Getenv读取密钥,或使用包级var保存 DB/客户端,则算不通过(在main或专用配置包中加载环境变量是允许的)。
Go 1.22+ 增强路由
Go 1.22 为 http.ServeMux 增加了基于方法的路由和路径参数,从而消除了使用第三方路由器最常见的理由。
基于方法的路由和路径参数
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", s.handleListUsers)
mux.HandleFunc("GET /api/users/{id}", s.handleGetUser)
mux.HandleFunc("POST /api/users", s.handleCreateUser)
mux.HandleFunc("DELETE /api/users/{id}", s.handleDeleteUser)
提取路径参数
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
user, err := s.users.GetUser(r.Context(), id)
if err != nil {
s.logger.Error("getting user", "err", err, "id", id)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
通配符与精确匹配
// Exact match on trailing slash -- serves /api/files/ only
mux.HandleFunc("GET /api/files/", s.handleListFiles)
// Wildcard to end of path -- /api/files/path/to/doc.txt
mux.HandleFunc("GET /api/files/{path...}", s.handleGetFile)
路由优先级
新的 ServeMux 采用最具体匹配优先的规则:
GET /api/users/{id}比GET /api/users/更具体GET /api/users/me比GET /api/users/{id}更具体- 带方法的路由优先于不带方法的路由
Server 结构体模式
Server 结构体是应用程序的核心依赖容器。它持有所有共享依赖项,并实现 http.Handler。
type Server struct {
db *sql.DB
logger *slog.Logger
router *http.ServeMux
}
func NewServer(db *sql.DB, logger *slog.Logger) *Server {
s := &Server{
db: db,
logger: logger,
router: http.NewServeMux(),
}
s.routes()
return s
}
func (s *Server) routes() {
s.router.HandleFunc("GET /api/users/{id}", s.handleGetUser)
s.router.HandleFunc("POST /api/users", s.handleCreateUser)
s.router.HandleFunc("GET /healthz", s.handleHealth)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
中间件包装
在 http.Server 级别或按路由应用中间件:
// Wrap entire server
httpServer := &http.Server{
Addr: ":8080",
Handler: requestLogger(s),
}
// Or per-route
s.router.Handle("GET /api/admin/", adminOnly(http.HandlerFunc(s.handleAdmin)))
中间件签名
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
})
}
项目结构
根据项目规模选择:
- 扁平结构——单个包,所有文件都放在根目录。适用于 CLI、小型服务和少于约 10 个处理器的项目。请参阅 references/project-structure.md。
- 模块化/领域驱动结构——使用
cmd/、包含领域包的internal/。适用于包含多个限界上下文的大型应用。请参阅 references/project-structure.md。
从扁平结构开始。当出现参考文档中所述的迹象时,再进行迁移。
优雅关闭
每个用于生产环境的 Go 服务端都需要优雅关闭。此模式使用 signal.NotifyContext 监听操作系统信号,并使用 http.Server.Shutdown 排空连接。
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer cancel()
// ... start server in goroutine ...
<-ctx.Done()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
httpServer.Shutdown(shutdownCtx)
请参阅 references/graceful-shutdown.md,了解包含清理顺序的完整模式。
何时加载参考文档
在以下情况加载 project-structure.md:
- 搭建新的 Go 项目
- 讨论包布局或目录组织
- 项目不断增长,需要重构
在以下情况加载 graceful-shutdown.md:
- 设置生产环境的 HTTP 服务端
- 实现信号处理或干净关闭
- 讨论部署或容器就绪状态
在以下情况加载 dependency-injection.md:
- 设计服务、存储和处理器之间的连接方式
- 使用接口让代码可测试
- 审查构造函数或组装逻辑
反模式
全局数据库变量
// BAD -- untestable, hidden dependency
var db *sql.DB
func handleGetUser(w http.ResponseWriter, r *http.Request) {
db.QueryRow(...)
}
改为通过 Server 或 Service 结构体传递 db。
框架优先思维
不要从 gin.Default() 或 echo.New() 开始。从 http.NewServeMux() 开始。只有在遇到标准库确实无法解决的限制时,才引入框架。
臃肿的包
单个包含 50 个文件的 handlers 包并不算组织良好。应按领域(user、order、billing)分组,而不是按技术层分组。
使用 init() 进行设置
// BAD -- invisible side effects, untestable
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
所有初始化都应放在 main() 或 run() 函数中,这样才能进行测试并处理错误。
在业务逻辑中读取配置
// BAD -- couples handler to environment
func (s *Server) handleSendEmail(w http.ResponseWriter, r *http.Request) {
apiKey := os.Getenv("SENDGRID_API_KEY") // don't do this
}
通过构造函数注入配置值或客户端。