Contents
開発環境の構築とプロジェクト設定
このセクションでは、Go 1.22 以降のインストール手順、モジュール化、そしてクリーンアーキテクチャに最適なディレクトリ構成を解説します。開発初期に環境が不完全だとビルドエラーや依存関係の齟齬が頻発し、後工程で大幅な手戻りが発生します。ここで示す手順に従えば、Acme Cloud のマイクロサービス基盤上でも即座に安定した開発環境を構築できます。
Go 1.22 のインストールとバージョン管理
Go 1.22 は 2024 年 2 月に公式リリースされました【[^1]】。以下のコマンドで最新版を取得し、go version で確認してください。複数プロジェクトで異なる Go バージョンを使う場合は asdf や gvm といったバージョン管理ツールが便利です。
|
1 2 3 4 5 6 7 8 9 |
# macOS / Linux (Homebrew) brew install go@1.22 # Windows (Chocolatey) choco install golang --version=1.22.0 # インストール確認 go version # => go version go1.22 linux/amd64 |
Tip:
GOROOTとGOPATHはデフォルトのままで問題ありません。
モジュール化 (go.mod / go.work)
Go のモジュール機能は依存関係を明示的に管理し、CI/CD パイプラインで再現性の高いビルドを保証します。go.mod はプロジェクト単位、go.work は複数モジュールを同一ワークスペースで扱うときに利用します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# ルートディレクトリでモジュール初期化 go mod init github.com/acme/sample-api # 複数モジュールがある場合は go.work を作成 cat <<EOF > go.work go 1.22 use ( ./cmd ./internal ./pkg ) EOF # 依存関係を取得し、整合性を確認 go mod tidy |
推奨ディレクトリ構成 (cmd/, internal/, pkg/)
Acme Cloud の内部標準として採用している三層構造は、クリーンアーキテクチャの「境界線」をファイルシステム上でも明確に分離できる点が特徴です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
sample-api/ ├─ cmd/ # エントリーポイント (main パッケージ) │ └─ server/ │ └─ main.go ├─ internal/ # ビジネスロジック、ユースケース、リポジトリインターフェイス │ ├─ entity/ │ ├─ usecase/ │ ├─ repository/ │ └─ handler/ ├─ pkg/ # 再利用可能な汎用ライブラリ (ロガー等) │ └─ logger/ ├─ go.mod └─ go.work |
この構成により、internal/... 配下のコードは外部からインポートできず、層ごとのカプセル化が徹底されます。
クリーンアーキテクチャと主要ライブラリ選定
本章では 層ごとの責務分離 と、Acme Cloud が実務で標準採用している Go ライブラリの選定理由を説明します。適切な依存関係設計は保守性・拡張性の根幹です。
Clean Architecture の概念と層の役割
Clean Architecture は「外側のインフラが内側のビジネスロジックに依存しない」ことを基本原則とします。これによりデータベースや Web フレームワークを差し替えてもユースケースは影響を受けません。
| 層 | 主な責務 | 例 (ディレクトリ) |
|---|---|---|
| エンティティ層 | ビジネスルール・データ構造 | internal/entity |
| ユースケース層 | アプリケーションの操作フロー | internal/usecase |
| インターフェイス層 | 外部システムへの入出力 (HTTP, DB, Queue) | internal/handler, internal/repository |
| フレームワーク層 | 具体的な実装(router、logger 等) | cmd/server/main.go |
実務で標準採用しているライブラリ
以下の4つは Acme Cloud のマイクロサービスで 軽量・高速・拡張しやすい ことが評価され、公式ドキュメントでも推奨されています。バージョン情報は Go Modules の go.mod に記載されたものです【[^2]】。
| ライブラリ | 用途 | バージョン例 |
|---|---|---|
| github.com/go-chi/chi/v5 | 軽量ルーティング & ミドルウェアチェーン | v5.0.9 |
| go.uber.org/zap | 高速構造化ロガー | v1.25.0 |
| github.com/go-playground/validator/v10 | タグベースの入力バリデーション | v10.14.0 |
| github.com/swaggo/swag/cmd/swag | コメント駆動の OpenAPI/Swagger 自動生成 | v1.8.12 |
|
1 2 3 4 5 |
go get github.com/go-chi/chi/v5@v5.0.9 go get go.uber.org/zap@v1.25.0 go get github.com/go-playground/validator/v10@v10.14.0 go get github.com/swaggo/swag/cmd/swag@v1.8.12 |
ハンドラ実装・バリデーション・レスポンス設計
この章では HTTP ハンドラの作成手順、入力検証、統一された JSON レスポンス を具体例とともに示します。正しいハンドラ実装は API の信頼性と開発者体験を大きく左右します。
chi によるルーティング設定
chi.Router にエンドポイントを登録し、ハンドラ関数は「入力 → ビジネスロジック呼び出し → 結果レスポンス」の流れで実装します。以下はユーザー関連エンドポイントの登録例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
package handler import ( "github.com/go-chi/chi/v5" ) func RegisterUserRoutes(r chi.Router, h *UserHandler) { // ルーティングだけを担当し、ロジックはハンドラ内部に委譲 r.Post("/users", h.CreateUser) r.Get("/users/{id}", h.GetUser) } |
リクエストバインドと validator による検証
構造体タグで必須項目やフォーマットを定義し、json.NewDecoder と validator.Struct の組み合わせで入力チェックを一元化します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
type CreateUserRequest struct { Name string `json:"name" validate:"required,min=2,max=50"` Email string `json:"email" validate:"required,email"` Age int `json:"age" validate:"gte=0,lte=130"` } func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.Error(w, http.StatusBadRequest, "invalid JSON") return } if err := h.validate.Struct(&req); err != nil { response.ValidationError(w, err) return } // ビジネスロジック呼び出し user, err := h.uc.Create(r.Context(), req.Name, req.Email, req.Age) if err != nil { response.Error(w, http.StatusInternalServerError, err.Error()) return } response.JSON(w, http.StatusCreated, user) } |
統一レスポンスフォーマット
Acme Cloud では全 API が同一スキーマ { "code": int, "message": string, "data": any } を返すことを社内規約としています。pkg/response に共通ユーティリティを置くことで、ハンドラは「何を返すか」だけに集中できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package response type APIResponse struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data,omitempty"` } func JSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(APIResponse{ Code: status, Message: http.StatusText(status), Data: data, }) } func Error(w http.ResponseWriter, status int, msg string) { JSON(w, status, map[string]string{"error": msg}) } |
ミドルウェア、エラーハンドリング、テスト
本章では ロギング・リカバリー・CORS・JWT 認証 のミドルウェア実装例と、エラーを一元管理する手法、そして httptest と testify を用いたテスト戦略を示します。
ミドルウェア実装例
ロギング(Zap ラッパー)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package middleware import ( "net/http" "time" "go.uber.org/zap" ) func ZapLogger(logger *zap.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) logger.Info("request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.Int64("latency_ms", time.Since(start).Milliseconds()), ) }) } } |
パニックリカバリー
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func Recover(logger *zap.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { logger.Error("panic recovered", zap.Any("error", rec)) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`{"code":500,"message":"internal server error"}`)) } }() next.ServeHTTP(w, r) }) } } |
CORS
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func CORS(allowed []string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") for _, o := range allowed { if o == "*" || o == origin { w.Header().Set("Access-Control-Allow-Origin", o) w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") break } } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } } |
JWT 認証
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func JWT(secret []byte) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Bearer ") { tokenStr := strings.TrimPrefix(auth, "Bearer ") token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { return secret, nil }) if err == nil && token.Valid { ctx := context.WithValue(r.Context(), "user", token.Claims) r = r.WithContext(ctx) } } next.ServeHTTP(w, r) }) } } |
統一エラーハンドリング
ビジネス層で定義した AppError を全ハンドラが参照することで、エラーコード・メッセージ・対応 HTTP ステータスを一元管理します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package infra import "net/http" type AppError struct { Code int // ビジネス固有コード Message string // ユーザー向け文言 HTTP int // 返すべき HTTP ステータス } func (e *AppError) Error() string { return e.Message } var ( ErrNotFound = &AppError{Code: 1001, Message: "resource not found", HTTP: http.StatusNotFound} ErrInvalidInput = &AppError{Code: 1002, Message: "invalid input data", HTTP: http.StatusBadRequest} // 必要に応じて追加 ) |
ハンドラ側は次のように利用します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
user, err := h.uc.GetUser(ctx, id) if err != nil { if appErr, ok := err.(*infra.AppError); ok { response.JSON(w, appErr.HTTP, map[string]any{ "code": appErr.Code, "message": appErr.Message, }) return } response.Error(w, http.StatusInternalServerError, "unexpected error") return } response.JSON(w, http.StatusOK, user) |
テスト戦略(httptest + testify)
ユニットテスト例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
func TestCreateUser_Success(t *testing.T) { // Arrange: モックユースケースを用意 mockUC := &mocks.UserUseCase{} mockUC.On("Create", mock.Anything, "Alice", "alice@example.com", 30). Return(&entity.User{ID: 1, Name: "Alice"}, nil) h := NewUserHandler(mockUC) r := chi.NewRouter() RegisterUserRoutes(r, h) payload := `{"name":"Alice","email":"alice@example.com","age":30}` req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Act r.ServeHTTP(w, req) // Assert res := w.Result() require.Equal(t, http.StatusCreated, res.StatusCode) var body struct { Code int `json:"code"` Data struct { ID int `json:"id"` Name string `json:"name"` } `json:"data"` } err := json.NewDecoder(res.Body).Decode(&body) require.NoError(t, err) require.Equal(t, 1, body.Data.ID) require.Equal(t, "Alice", body.Data.Name) mockUC.AssertExpectations(t) } |
統合テスト例(Docker + DB コンテナ)
CI 環境では docker compose で PostgreSQL を立ち上げ、httptest.NewServer に本番と同等のハンドラスタックを注入してエンドツーエンドテストを走らせます。詳細は Acme Cloud の内部ドキュメント [CI‑Test Guide] を参照してください。
CI/CD・デプロイとドキュメント自動生成
この章では OpenAPI/Swagger 自動生成、マルチステージ Dockerfile、GitHub Actions によるビルド・テスト・Lint パイプライン、Kubernetes デプロイ のフローを具体的に示します。全工程がコードベースで管理できれば、リリース作業は数クリックで完了し、品質リスクを最小化できます。
Swaggo による OpenAPI 自動生成
swag init はハンドラや DTO に付与した @Summary などのコメントから OpenAPI 3.0 スペックを生成します。Acme Cloud の API ポータルはこの JSON をそのままインポートして利用しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// CreateUser godoc // @Summary ユーザー作成 // @Description 名前・メールアドレスと年齢を受け取り新規ユーザーを登録します // @Tags users // @Accept json // @Produce json // @Param user body CreateUserRequest true "Create User" // @Success 201 {object} response.APIResponse{data=entity.User} // @Failure 400 {object} response.APIResponse{data=map[string]string} // @Router /users [post] func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { /* ... */ } |
生成手順(ローカル・CI 共通):
|
1 2 3 |
go install github.com/swaggo/swag/cmd/swag@v1.8.12 # 【[^3]】インストール例 swag init -g cmd/server/main.go -o ./docs/swagger |
生成された docs/swagger/swagger.json は以下のようにエンドポイントで配信できます。
|
1 2 |
r.Mount("/swagger/", http.StripPrefix("/swagger/", http.FileServer(http.Dir("./docs/swagger")))) |
マルチステージ Dockerfile
Acme Cloud では distroless イメージを本番環境のデフォルトとしています。ビルドキャッシュを活用すれば CI の実行時間も短縮できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# ---------- Build stage ---------- FROM golang:1.22-alpine AS builder WORKDIR /app # 依存関係だけ先に取得してキャッシュを有効化 COPY go.mod go.sum ./ RUN go mod download # ソース全体コピー & ビルド COPY . . ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 RUN go build -ldflags="-s -w" -o /app/bin/server ./cmd/server # ---------- Runtime stage ---------- FROM gcr.io/distroless/static-debian12 AS runtime COPY --from=builder /app/bin/server /server EXPOSE 8080 ENTRYPOINT ["/server"] |
ローカルでのビルド・実行例:
|
1 2 3 |
docker build -t acme/sample-api:dev . docker run --rm -p 8080:8080 acme/sample-api:dev |
GitHub Actions による CI パイプライン
以下は テスト、golangci‑lint、Docker ビルド を自動化した ci.yml の抜粋です。Acme Cloud のリポジトリではこのワークフローが必須ステップとなっています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
name: CI on: push: branches: [ main ] pull_request: branches: [ '**' ] jobs: build-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Cache modules uses: actions/cache@v3 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }} - name: Install dependencies run: go mod tidy - name: Lint (golangci-lint) uses: golangci/golangci-lint-action@v3 with: version: v1.58 args: --timeout 5m - name: Run unit tests run: | go test ./... -coverprofile=coverage.out go tool cover -func=coverage.out - name: Build Docker image (no push) uses: docker/build-push-action@v5 with: context: . tags: acme/sample-api:${{ github.sha }} load: true # ビルドだけでレジストリにはプッシュしない |
Kubernetes デプロイ手順(ConfigMap・Secret・Probe)
Acme Cloud の本番環境は GKE 上にデプロイされます。以下は最小構成のマニフェスト例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
apiVersion: apps/v1 kind: Deployment metadata: name: sample-api spec: replicas: 3 selector: matchLabels: app: sample-api template: metadata: labels: app: sample-api spec: containers: - name: api image: ghcr.io/acme/sample-api:${IMAGE_TAG} ports: - containerPort: 8080 envFrom: - configMapRef: name: sample-api-config - secretRef: name: sample-api-secret livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 15 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
apiVersion: v1 kind: ConfigMap metadata: name: sample-api-config data: LOG_LEVEL: "info" --- apiVersion: v1 kind: Secret metadata: name: sample-api-secret type: Opaque stringData: JWT_SECRET: "super‑secure‑random‑string" |
デプロイ手順:
|
1 2 3 4 5 6 |
# イメージは GitHub Actions が自動プッシュしたものを使用 docker push ghcr.io/acme/sample-api:v1.0.0 # マニフェスト適用 kubectl apply -f k8s/ |
まとめ
- 開発環境は Go 1.22 の公式インストールと
go.mod/go.workによるモジュール化、クリーンアーキテクチャ向きの三層ディレクトリで整備します。 - 主要ライブラリ(chi, zap, validator.v10, swaggo)は Acme Cloud が実務で標準採用している軽量・高速なスタックです。バージョンは
go.modに明示し、出典をフッターで示しています【[^2]】。 - ハンドラ実装は入力バインド+validator で検証し、統一レスポンスユーティリティで JSON を返すことでクライアント側の扱いやすさを確保します。
- ミドルウェアはロギング・リカバリー・CORS・JWT 認証を関数化し、
AppErrorでエラーを一元管理。テストはhttptestとtestifyによる高速かつ網羅的なユニット/統合テストが可能です。 - CI/CDは Swaggo の自動 OpenAPI 生成、マルチステージ Dockerfile、GitHub Actions のビルド・Lint・テストパイプラインで品質を保証し、Kubernetes デプロイでは ConfigMap/Secret とヘルスチェックを必ず設定します。
以上のベストプラクティスに従うことで、Acme Cloud のマイクロサービスは 信頼性が高く保守しやすい RESTful API として迅速に本番環境へリリースできます。
参考文献
[^1]: Go 1.22 リリースノート – https://go.dev/blog/go1.22release
[^2]: go.mod に記載されたライブラリバージョン(例: chi v5.0.9、zap v1.25.0、validator v10.14.0、swag v1.8.12) – 2026‑07‑02 時点の公式リポジトリタグ
[^3]: Swaggo CLI インストール手順 – https://github.com/swaggo/swag#install