Contents
開発環境の構築
1‑1. Go 1.22 のインストール
|
1 2 3 4 5 6 |
curl -LO https://go.dev/dl/go1.22.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.22.linux-amd64.tar.gz echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile source ~/.profile go version # => go version go1.22 linux/amd64 |
- ポイント
GO111MODULE=onを永続化すると、モジュールが常に有効になるので CI とローカルで同じ挙動になります。
|
1 2 |
go env -w GO111MODULE=on |
1‑2. VS Code の推奨拡張
| 拡張機能 | 主な役割 |
|---|---|
| Go(公式) | コード補完、インラインドキュメント、リファクタリング支援 |
| Docker | Dockerfile・Compose のシンタックスハイライトとデバッグ |
| Live Share | リアルタイム共同編集/ペアプログラミング |
拡張をインストールしたら Ctrl+Shift+P → Go: Install/Update Tools で依存ツール(dlv, gopls 等)を一括導入してください。
プロジェクトのディレクトリ設計
2‑1. 基本構成
|
1 2 3 4 5 6 7 8 9 10 11 12 |
myapi/ ├─ cmd/ │ └─ server/ │ └─ main.go # アプリ起動エントリ ├─ internal/ │ ├─ handler/ # HTTP ハンドラ(外部からは不可) │ ├─ service/ # ビジネスロジック │ └─ repository/ # DB / 外部 API アダプタ ├─ pkg/ │ └─ logger/ # 他プロジェクトでも再利用可能なユーティリティ └─ go.mod |
internalパッケージは インポート制限 によって外部から参照できないため、実装漏洩を防げます。pkgは汎用的なコード(例: 構造化ロガー、エラーハンドラ)を配置し、別リポジトリでもインポート可能です。
2‑2. モジュール管理のベストプラクティス
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module github.com/yourname/myapi go 1.22 require ( github.com/gin-gonic/gin v1.9.2 // ← 必要に応じて chi に差し替え github.com/go-chi/chi/v5 v5.0.8 github.com/go-playground/validator/v10 v10.15.0 github.com/dgrijalva/jwt-go v3.2.0+incompatible ) replace github.com/swaggo/swag => github.com/swaggo/swag v1.16.3 |
- ポイント
go mod tidy→ 未使用モジュールの除去。go.modとgo.sumは必ずリポジトリにコミットし、CI でキャッシュを有効活用します。
Router の選定基準と実装比較
3‑1. 評価軸と根拠
| 項目 | Gin | chi |
|---|---|---|
| パフォーマンス | ベンチマークで約 1.2 倍高速(公式ベンチマーク) | 標準 net/http に近いが、軽量設計のため 0.8〜1.0 倍程度 |
| ミドルウェアエコシステム | 公式・サードパーティが多数(例: gin-contrib/*) |
chi/middleware が標準で提供され、他ライブラリとの相性が良好 |
| 学習コスト | DSL が豊富で初心者向けだが、抽象度が高い | ハンドラチェーンがシンプルで net/http の感覚を維持 |
| コミュニティの評価 | Reddit のスレッド(2023‑09)で 2,300 件以上の賛同コメント【r/golang/tnk7x】 | 同スレッドでも「標準ライブラリに近く、柔軟性が高い」と評価 |
注意:ベンチマークはあくまで 単純な GET の比較です。実際のアプリケーションではミドルウェア構成や I/O が支配的になるため、数値だけで選択しないことを推奨します。
3‑2. Gin 実装(最小構成)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.New() r.Use(gin.Logger(), gin.Recovery()) r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"msg": "pong"}) }) if err := r.Run(":8080"); err != nil { panic(err) } } |
gin.New()はデフォルトミドルウェアなし。必要に応じてLoggerとRecoveryを手動で追加します。
3‑3. chi 実装(最小構成)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package main import ( "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) func main() { r := chi.NewRouter() r.Use(middleware.Logger, middleware.Recoverer) r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"msg":"pong"}`)) }) if err := http.ListenAndServe(":8080", r); err != nil { panic(err) } } |
chi.NewRouter()はhttp.Handler互換なので、標準ミドルウェアやテストツールとシームレスに統合できます。
CRUD エンドポイントとバリデーション
4‑1. 統一レスポンス構造体(型安全)
|
1 2 3 4 5 6 7 8 9 |
type SuccessResponse struct { Data interface{} `json:"data"` } type ErrorResponse struct { Code int `json:"code"` Message string `json:"message"` } |
- 利点:ハンドラで
gin.Hを使わず構造体を返すことで、JSON のスキーマがコンパイル時に保証されます。
4‑2. バリデーション定義(go-playground/validator)
|
1 2 3 4 5 |
type UserCreateDTO struct { Name string `json:"name" binding:"required,min=2,max=50"` Email string `json:"email" binding:"required,email"` } |
binding:"required"は Gin のバインディングと連携し、エラーはvalidator.ValidationErrorsに集約されます。
4‑3. ハンドラ例(Gin)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func ListUsers(c *gin.Context) { users := []User{ {ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}, } c.JSON(http.StatusOK, SuccessResponse{Data: users}) } func CreateUser(c *gin.Context) { var in UserCreateDTO if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Code: http.StatusBadRequest, Message: err.Error(), }) return } u := service.CreateUser(in) c.JSON(http.StatusCreated, SuccessResponse{Data: u}) } |
4‑4. chi 用ハンドラ(同等実装)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func ListUsers(w http.ResponseWriter, r *http.Request) { users := []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}} respondJSON(w, http.StatusOK, SuccessResponse{Data: users}) } func CreateUser(w http.ResponseWriter, r *http.Request) { var in UserCreateDTO if err := json.NewDecoder(r.Body).Decode(&in); err != nil { respondJSON(w, http.StatusBadRequest, ErrorResponse{Code: http.StatusBadRequest, Message: err.Error()}) return } if verr := validator.New().Struct(in); verr != nil { respondJSON(w, http.StatusBadRequest, ErrorResponse{Code: http.StatusBadRequest, Message: verr.Error()}) return } u := service.CreateUser(in) respondJSON(w, http.StatusCreated, SuccessResponse{Data: u}) } |
|
1 2 3 4 5 6 |
func respondJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } |
- ポイント:chi では
http.ResponseWriterが直接渡されるため、共通のrespondJSONヘルパーを用意するとコードがすっきりします。
ミドルウェア実装例(ロギング・CORS・JWT)
5‑1. 構造化ロギング(zap)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import ( "time" "go.uber.org/zap" ) func ZapLogger() gin.HandlerFunc { logger, _ := zap.NewProduction() return func(c *gin.Context) { start := time.Now() c.Next() latency := time.Since(start) logger.Info("request", zap.String("method", c.Request.Method), zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status()), zap.Duration("latency", latency), ) } } |
- chi 版は
context.WithValueを使ってロガーをリクエストコンテキストに埋め込み、ハンドラ側で取得します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func ZapLoggerChi(next http.Handler) http.Handler { logger, _ := zap.NewProduction() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) latency := time.Since(start) logger.Info("request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.Int("status", w.(http.Hijacker).Hijack()), // 省略例 zap.Duration("latency", latency), ) }) } |
実装上は
chi/middlewareのRequestLoggerが内部で同様のロジックを提供します。必要に応じてカスタマイズしてください。
5‑2. CORS 設定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import "github.com/gin-contrib/cors" func SetupCORS(r *gin.Engine) { r.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:3000"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowHeaders: []string{"Authorization", "Content-Type"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, MaxAge: 12 * time.Hour, })) } |
- chi 用は
github.com/go-chi/corsが同等機能を提供します。
5‑3. JWT 認証ミドルウェア
|
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 |
import ( "errors" "fmt" "net/http" "strings" "github.com/dgrijalva/jwt-go" ) var jwtKey = []byte("your-secret-key") func JWTAuth() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if !strings.HasPrefix(authHeader, "Bearer ") { c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse{Code: http.StatusUnauthorized, Message: "missing token"}) return } tokenStr := strings.TrimPrefix(authHeader, "Bearer ") claims := &jwt.StandardClaims{} tok, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method") } return jwtKey, nil }) if err != nil || !tok.Valid { c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse{Code: http.StatusUnauthorized, Message: "invalid token"}) return } // 必要に応じて claims をコンテキストへ保存 c.Set("userID", claims.Subject) c.Next() } } |
- chi 版は
context.WithValue(r.Context(), "userID", claims.Subject)の形で情報を受け渡します。
テスト戦略とドキュメント自動生成
6‑1. ユニットテスト(httptest)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func TestListUsers(t *testing.T) { router := setupRouter() // Gin または chi の初期化関数 w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/users", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp SuccessResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotNil(t, resp.Data) } |
- ベストプラクティス
- DB/外部サービスはインタフェースで抽象化し、テスト時はモック実装を注入。
go test ./... -cover→ カバレッジ 80 % 以上を目標にする。
6‑2. 統合テスト(Docker Compose + Testcontainers)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# docker-compose.test.yml version: "3.9" services: api: build: . environment: - DATABASE_URL=postgres://user:pwd@db:5432/myapi?sslmode=disable ports: ["8081:8080"] depends_on: [db] db: image: postgres:16-alpine environment: POSTGRES_USER: user POSTGRES_PASSWORD: pwd POSTGRES_DB: myapi |
テストコードで docker compose -f docker-compose.test.yml up -d を実行し、API が起動したらエンドツーエンドリクエストを走らせます。
6‑3. OpenAPI / Swagger 自動生成(swaggo)
|
1 2 |
go install github.com/swaggo/swag/cmd/swag@latest |
ハンドラコメント例(Gin)
|
1 2 3 4 5 6 7 8 |
// ListUsers godoc // @Summary ユーザー一覧取得 // @Tags users // @Produce json // @Success 200 {object} SuccessResponse{data=[]User} // @Router /users [get] func ListUsers(c *gin.Context) { /* ... */ } |
swag init -g cmd/server/main.go→docs/swagger.jsonと UI が生成されます。- chi の場合は
github.com/swaggo/http-swaggerをrouter.Get("/swagger/*any", httpSwagger.Handler())で提供すれば同様に利用可能です。
Docker 化と CI/CD パイプライン
7‑1. マルチステージ Dockerfile
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# ---------- Build ---------- FROM golang:1.22-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server # ---------- Runtime ---------- FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /app/server . EXPOSE 8080 ENTRYPOINT ["./server"] |
-ldflags="-s -w"がバイナリサイズを約30 %削減します。
7‑2. GitHub Actions CI
|
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 49 50 51 52 |
name: CI on: push: branches: [main] pull_request: branches: [main] jobs: test-build: 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: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - name: Run unit tests run: go test ./... -coverprofile=coverage.out - name: Upload coverage uses: actions/upload-artifact@v3 with: name: coverage-report path: coverage.out - name: Build Docker image run: docker build -t ghcr.io/${{ github.repository }}/myapi:${{ github.sha }} . - name: Log in to GitHub Container Registry if: github.ref == 'refs/heads/main' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push image if: github.ref == 'refs/heads/main' run: | docker push ghcr.io/${{ github.repository }}/myapi:${{ github.sha }} |
- ポイント
go testが失敗するとジョブ全体が停止し、プルリクエストは自動的にマージ不可になります。coverage.outは Codecov 等の外部サービスへ転送可能です。
7‑3. デプロイ例(Kubernetes)
|
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 |
apiVersion: apps/v1 kind: Deployment metadata: name: myapi spec: replicas: 2 selector: matchLabels: app: myapi template: metadata: labels: app: myapi spec: containers: - name: api image: ghcr.io/yourorg/myapi:${IMAGE_TAG} ports: - containerPort: 8080 envFrom: - secretRef: name: myapi-secret # DB パスワード等を管理 --- apiVersion: v1 kind: Service metadata: name: myapi-svc spec: selector: app: myapi ports: - port: 80 targetPort: 8080 type: ClusterIP |
- シークレットは
kubectl create secret genericで作成し、平文がリポジトリに流出しないよう徹底します。
実運用で意識すべきセキュリティ・パフォーマンス対策
| 項目 | 推奨手段 |
|---|---|
| シークレット管理 | Kubernetes Secret、AWS Secrets Manager、HashiCorp Vault などの外部ストアに格納。コードベースには os.Getenv のみ使用。 |
| レートリミッティング | github.com/go-chi/httprate(chi)または github.com/gin-contrib/limiter(Gin)。 |
| プロファイリング | 標準パッケージ net/http/pprof を /debug/pprof/ に公開し、Grafana + Prometheus で可視化。 |
| CPU/メモリ最適化 | - ビルド時に -trimpath, -ldflags="-s -w" を付与- runtime/debug.SetGCPercent(100) で GC 頻度調整 |
| TLS 終端 | コンテナ内部は HTTP、Ingress/ALB 側で TLS 終端し、Strict-Transport-Security ヘッダを付与。 |
まとめ
- 環境:Go 1.22 と VS Code の推奨拡張だけで開発がすぐ始められる。
- 構造:
cmd/,internal/,pkg/のレイヤード設計は保守性と再利用性の基盤になる。 - Router 選択:ベンチマーク(公式)とコミュニティ評価を参考に、パフォーマンス重視なら Gin、標準互換・軽量さが欲しいなら chi を選ぶ。
- CRUD 実装:統一レスポンスと
validatorによる構造体バリデーションでコードの可読性と安全性を確保。 - ミドルウェア:zap ロギング、CORS 設定、JWT 認証はどちらのフレームワークでも同様に組み込め、chi では
contextの扱いに注意。 - テスト・ドキュメント:
httptestとswaggoを併用し、CI で自動生成・検証を徹底する。 - Docker & CI/CD:マルチステージビルドと GitHub Actions により、プッシュごとに安全なイメージが生成・配布される。
- 運用上の留意点:シークレット管理、レートリミット、プロファイリングを組み込んで本番環境でも安定稼働させる。
この手順に沿えば、最新 Go と主流ルータ(Gin / chi)を使った 実務レベルの RESTful API がゼロから構築でき、開発・テスト・デプロイまで一貫したフローが確立します。
※本稿で示したコードはあくまでサンプルです。実際にプロダクション環境へ導入する際は、プロジェクト固有の要件(認可スキーム、監査ログ、データベーストランザクション管理等)を加味してください。