mcpField
G
go-architect

go-architect

github.com/existential-birds开发8592 次安装11 周前更新
代码仓库

关于 go-architect

用于设计和审查 Go 服务的开发技能,强调显式依赖、优先使用标准库路由,以及实用的项目结构指导。适合实现或评估 Go 应用的开发者。

主要功能

  1. 01Go 1.22+ ServeMux 路由指导
  2. 02通过构造函数注入依赖
  3. 03扁平或模块化项目结构建议
  4. 04HTTP 服务优雅关闭
  5. 05实现审查的硬性检查

适用场景

  1. 01搭建新的 Go 项目
  2. 02审查 HTTP 服务的依赖装配
  3. 03实现响应信号的服务关闭

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/existential-birds/beagle --skill go-architect

安装后在 Claude Code 中输入/go-architect确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
dependency-injection.md
graceful-shutdown.md
project-structure.md

详细介绍

Go 首席架构师

快速参考

主题参考资料
扁平与模块化项目布局、迁移信号references/project-structure.md
信号处理与优雅关闭references/graceful-shutdown.md
依赖注入模式、测试接缝references/dependency-injection.md

核心原则

  1. 优先使用标准库 —— 使用 net/http 和 Go 1.22+ 增强版 ServeMux 进行路由。只有在有明确需求且标准库无法满足时,才考虑使用框架(chi、echo、gin),例如复杂的中间件链或正则路由。
  2. 依赖注入优于全局变量 —— 通过结构体字段和构造函数传递数据库、日志记录器和服务,绝不使用包级 var。
  3. 明确优于魔法 —— 不要使用 init() 副作用,也不要使用框架自动装配。main.go 是组合根,所有内容都应在其中显式组装。
  4. 接口小,结构体大 —— 在使用方定义接口,并保持接口精简(1-3 个方法)。由具体类型承载实现。

硬性门槛

在实现或审查声称遵循此技能的工作时,请按以下顺序操作。不要跳步;每一步都有可通过工具或具体文件路径验证的通过条件。

  1. 工具链与 API — 如果代码使用 Go 1.22+ 的 ServeMux 特性(如 "GET /x/{id}" 这样的 method+path 模式、r.PathValue 或 {path...}):运行 go version,只有报告的工具链为 go1.22+ 才算通过。如果项目必须继续使用较旧版本的 Go,则只有不使用这些 API(改用兼容的路由器或旧版模式)并在审查或 PR 中说明,才算通过。
  2. 组合根 — 当 main.go 或 cmd/.../main.go 明确构造服务器并注入共享依赖项(DB、日志记录器、配置)时,才算通过。如果共享依赖项通过 init() 或包级 var 而非在 main(或由 main 调用的 run())中显式构造来装配,则算不通过。
  3. 生产环境 HTTP 关闭 — 对于长期运行的 HTTP 服务,只有在等待 signal.NotifyContext(或等效机制)后,使用带有限时长的上下文(例如 context.WithTimeout)调用 http.Server.Shutdown,才算通过。报告时请注明文件路径;完整模式请参阅 references/graceful-shutdown.md。
  4. 处理程序中不得使用环境变量或全局变量 — 当处理程序和领域代码通过结构体或参数获取依赖项时,才算通过。如果处理程序通过 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
}

通过构造函数注入配置值或客户端。

评论

支持 Markdown