go-architect について
依存関係の明示、標準ライブラリ優先のルーティング、実用的な構成案を重視する Go 開発スキルです。Go アプリケーションの実装やレビューを行う開発者向けです。
主な機能
- 01Go 1.22+ ServeMux のルーティング指針
- 02コンストラクターによる依存性注入
- 03フラットまたはモジュール型の構成案
- 04HTTP サーバーのグレースフルシャットダウン
- 05実装レビューの必須チェック
活用シーン
- 01新しい Go プロジェクトの土台作り
- 02HTTP サービスの依存関係のレビュー
- 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}"のようなメソッド+パスのパターン、r.PathValue、または{path...})を使用している場合は、go versionを実行し、報告されたツールチェーンが go1.22+ の場合にのみ合格とします。プロジェクトで古い Go を使い続ける必要がある場合は、これらの API を使わない(互換性のあるルーターまたは従来のパターンを使う)ことでのみ合格とし、レビューまたは PR でその旨を説明します。 - コンポジションルート —
main.goまたはcmd/.../main.goでサーバーを明示的に構築し、共有依存関係(DB、ロガー、設定)を注入している場合に合格です。main(またはmainから呼び出されるrun())で明示的に構築せず、init()またはパッケージレベルのvarで共有依存関係を配線している場合は不合格です。 - 本番 HTTP のシャットダウン — 長時間稼働する HTTP サービスでは、
signal.NotifyContext(または同等の処理)を待機した後、上限時間のあるコンテキスト(例:context.WithTimeout)を使ってhttp.Server.Shutdownを呼び出す場合にのみ合格です。報告時にはファイルパスを示してください。全体のパターンについては references/graceful-shutdown.md を参照してください。 - ハンドラーで環境変数やグローバル変数を使わない — ハンドラーとドメインコードが、構造体や引数を介して依存関係を受け取っている場合に合格です。ハンドラーがシークレットの取得に
os.Getenvを読み取る場合や、DB/クライアントにパッケージレベルのvarを使っている場合は不合格です(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 で OS シグナルを監視し、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(...)
}
代わりに、db を Server または Service 構造体経由で渡します。
フレームワークを先に考える
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
}
コンストラクターを通じて設定値やクライアントを注入します。