Contents
1. 環境構築とプロジェクト初期化
1‑1. Go のインストール(最新版推奨)
公式サイト https://go.dev/dl/ から Go 1.20 系の最新パッチ を取得し、以下の手順で展開します。
|
1 2 3 4 5 6 7 8 9 10 |
# Linux (amd64) 用例 wget https://golang.org/dl/go1.20.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz # パスを永続化(~/.profile などに追記) echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile source ~/.profile go version # => go version go1.20.x linux/amd64 |
Tip
macOS は Homebrew (brew install go@1.20) でも簡単にインストールできます。
1‑2. プロジェクトディレクトリと go.mod の作成
|
1 2 3 |
mkdir -p myapi && cd myapi go mod init github.com/yourname/myapi # モジュールパスはご自身のリポジトリに合わせてください |
推奨ディレクトリ構造(Clean Architecture にヒント)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
myapi/ ├─ cmd/ │ └─ server/ # main パッケージ(エントリポイント) ├─ internal/ │ ├─ api/ # HTTP ハンドラ・ルーティング │ ├─ model/ # ドメイン構造体 │ ├─ repository/ # 永続化層(インターフェイス + 実装) │ └─ service/ # ビジネスロジック ├─ configs/ # 設定ファイル(YAML / TOML 等) ├─ test/ # 統合テスト向けのテストコード └─ go.mod |
1‑3. 必要なライブラリを取得(バージョンは go.mod に任せる)
| 機能 | 推奨パッケージ | 説明 |
|---|---|---|
| ルーティング | chi (github.com/go-chi/chi/v5) または gorilla/mux(どちらか一方) |
本稿では chi を例に採用します。軽量でミドルウェアの組み込みがシンプルです。 |
| MySQL ドライバ | github.com/go-sql-driver/mysql |
標準的な DB ドライバ |
| テスト用モック | github.com/DATA-DOG/go-sqlmock |
SQL の振る舞いをインメモリで再現 |
| アサーション | github.com/stretchr/testify |
テストコードの可読性向上 |
| バリデーション | github.com/go-playground/validator/v10 |
struct タグベースのバリデータ |
|
1 2 3 4 5 6 7 |
# 例:chi 系列を使用する場合 go get github.com/go-chi/chi/v5 go get github.com/go-sql-driver/mysql go get github.com/DATA-DOG/go-sqlmock go get github.com/stretchr/testify go get github.com/go-playground/validator/v10 |
選択ガイド
chi は標準net/httpと同様のハンドラインターフェイスを保ちつつ、ミドルウェアチェーンがシンプルです。
gorilla/mux はパス変数や正規表現マッチングに強みがありますが、依存関係が若干重くなる傾向があります。プロジェクトの要件(高度な URL パターンが必要か)で選択してください。
2. API 実装とルーティング設計
2‑1. エントリポイント (cmd/server/main.go)
|
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 |
package main import ( "log" "net/http" "github.com/go-chi/chi/v5" "myapi/internal/api" ) func main() { r := chi.NewRouter() // 共通ミドルウェア(例:ログ出力、リクエストID付与など) r.Use(middleware.Logger) // 自前または go-chi の middleware // ルート登録 api.RegisterRoutes(r) srv := &http.Server{ Handler: r, Addr: ":8080", } log.Println("🚀 server listening on :8080") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("❗ server failed: %v", err) } } |
2‑2. ルーティング定義 (internal/api/router.go)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package api import ( "net/http" "github.com/go-chi/chi/v5" ) // RegisterRoutes は外部から呼び出され、全ハンドラを登録します。 func RegisterRoutes(r chi.Router) { r.Post("/users", CreateUserHandler) r.Get("/users/{id}", GetUserHandler) // 例:ヘルスチェックエンドポイント r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) } |
2‑3. データ構造体と JSON タグ (internal/model/user.go)
|
1 2 3 4 5 6 7 8 9 10 |
package model // User は API の入出力で使用するドメインモデルです。 type User struct { ID int64 `json:"id,omitempty"` // DB が生成した PK Name string `json:"name" validate:"required,min=2"` // バリデーションは validator/v10 に委譲 Email string `json:"email" validate:"required,email"` // 同上 CreatedAt string `json:"created_at,omitempty"` // ISO8601 文字列(例: 2023-01-02T15:04:05Z) } |
2‑4. ハンドラ実装 (internal/api/handler.go)
|
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 53 54 55 56 57 58 |
package api import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "myapi/internal/model" "myapi/internal/service" ) // CreateUserHandler POST /users func CreateUserHandler(w http.ResponseWriter, r *http.Request) { var req model.User if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } defer r.Body.Close() // バリデーション(service 層に委譲) if err := service.ValidateUser(&req); err != nil { writeError(w, http.StatusUnprocessableEntity, err.Error()) return } created, err := service.CreateUser(r.Context(), &req) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create user") return } respondJSON(w, http.StatusCreated, created) } // GetUserHandler GET /users/{id} func GetUserHandler(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") user, err := service.GetUserByID(r.Context(), idParam) if err != nil { writeError(w, http.StatusNotFound, "user not found") return } respondJSON(w, http.StatusOK, user) } // ---------- ユーティリティ ---------- func writeError(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) } func respondJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) } |
2‑5. バリデーションロジック (internal/service/validation.go)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package service import ( "github.com/go-playground/validator/v10" "myapi/internal/model" ) var validatorInstance = validator.New() // ValidateUser は構造体タグに基づき入力を検証します。 func ValidateUser(u *model.User) error { return validatorInstance.Struct(u) } |
3. データベース連携とテスト戦略
3‑1. DB 接続ラッパー (internal/repository/mysql.go)
|
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 |
package repository import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) // DB は sql.DB を埋め込んだラッパーです。 type DB struct { *sql.DB } // NewMySQL は接続プール設定付きの *DB を返します。 func NewMySQL(dsn string) (*DB, error) { db, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("open mysql: %w", err) } // 推奨プール設定 db.SetMaxOpenConns(25) db.SetMaxIdleConns(25) db.SetConnMaxLifetime(0) if err = db.Ping(); err != nil { return nil, fmt.Errorf("ping mysql: %w", err) } return &DB{db}, nil } |
3‑2. リポジトリインターフェイスと実装
|
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 |
package repository import ( "context" "myapi/internal/model" ) // UserRepository は永続化層の抽象です。 type UserRepository interface { Create(ctx context.Context, u *model.User) (*model.User, error) GetByID(ctx context.Context, id int64) (*model.User, error) } // mysqlUserRepo は MySQL 実装です。 type mysqlUserRepo struct{ db *DB } func NewUserRepository(db *DB) UserRepository { return &mysqlUserRepo{db: db} } func (r *mysqlUserRepo) Create(ctx context.Context, u *model.User) (*model.User, error) { // 省略:INSERT 文と取得ロジック return nil, nil } func (r *mysqlUserRepo) GetByID(ctx context.Context, id int64) (*model.User, error) { // 省略:SELECT 文 return nil, nil } |
3‑3. サービス層でリポジトリを利用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package service import ( "context" "myapi/internal/model" "myapi/internal/repository" ) var userRepo repository.UserRepository // 実際は DI コンテナや init 時に注入 func CreateUser(ctx context.Context, u *model.User) (*model.User, error) { return userRepo.Create(ctx, u) } func GetUserByID(ctx context.Context, idStr string) (*model.User, error) { // 文字列 → int64 変換 + リポジトリ呼び出し return nil, nil } |
3‑4. ユニットテストのベストプラクティス
ハンドラ単体テスト (internal/api/handler_test.go)
|
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 |
package api_test import ( "bytes" "net/http" "net/http/httptest" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "myapi/internal/api" "myapi/internal/repository" ) func TestCreateUserHandler(t *testing.T) { // ① sqlmock のセットアップ db, mock, err := sqlmock.New() assert.NoError(t, err) defer db.Close() mock.ExpectExec(`INSERT INTO users`). WithArgs("Alice", "alice@example.com"). WillReturnResult(sqlmock.NewResult(1, 1)) userRepo := repository.NewUserRepository(&repository.DB{DB: db}) // テスト用にグローバル変数を書き換える(実装例として簡易的に示す) api.SetUserRepo(userRepo) // ② HTTP リクエスト作成 payload := `{"name":"Alice","email":"alice@example.com"}` req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewBufferString(payload)) rec := httptest.NewRecorder() // ③ chi Router にハンドラを登録して実行 r := chi.NewRouter() api.RegisterRoutes(r) r.ServeHTTP(rec, req) assert.Equal(t, http.StatusCreated, rec.Code) // 必要なら JSON 内容の検証も追加 } |
リポジトリ層テスト(SQL モックだけで完結)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func TestUserRepository_Create(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() mock.ExpectExec(`INSERT INTO users`). WithArgs("Bob", "bob@example.com"). WillReturnResult(sqlmock.NewResult(2, 1)) repo := repository.NewUserRepository(&repository.DB{DB: db}) ctx := context.Background() u := &model.User{Name: "Bob", Email: "bob@example.com"} created, err := repo.Create(ctx, u) assert.NoError(t, err) assert.Equal(t, int64(2), created.ID) } |
ポイント
依存性注入(DI)を利用すれば、テスト時にモック実装だけ差し替えられます。
go-sqlmockはクエリ文字列の正規表現マッチングが可能なので、SQL の書き方が変わってもテストを柔軟に保てます。
4. Lint 設定と CI/CD パイプライン
4‑1. golangci-lint のインストール(バージョンは「最新版」)
|
1 2 3 |
# 最新リリースを自動取得する公式スクリプト curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $HOME/go/bin latest |
注意
固定バージョン(例:v1.57.2)は時間が経つと古くなるため、CI ではlatestタグやgo.modのtoolchainセクションで管理することを推奨します。
4‑2. .golangci.yml(公式推奨リストをベースにカスタマイズ)
|
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 |
run: timeout: 5m modules-download-mode: readonly linters: enable: - bodyclose - containedctx - cyclop - dupl - errname - errorlint - exportloopref - forcetypeassert - gci - gofmt - gosec - lll - nilerr - rowserrcheck - sqlclosecheck - tagliatelle - unconvert - whitespace linters-settings: gci: sections: - standard - default - prefix(myapi) lll: line-length: 120 |
4‑3. GitHub Actions による自動化 (.github/workflows/ci.yml)
|
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 |
name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test-lint: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.20' - name: Cache modules uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Download dependencies run: go mod tidy - name: Run unit & integration tests run: go test ./... -v -coverprofile=coverage.out - name: Install golangci-lint (latest) run: | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ | sh -s -- -b $HOME/go/bin latest env: GOLANGCI_LINT_VERSION: latest - name: Run golangci-lint run: $HOME/go/bin/golangci-lint run ./... |
ベストプラクティス
go testのカバレッジは Pull Request にコメントで自動報告すると、品質向上に役立ちます。
Linter とテストは同一ジョブで実行することで、CI 時間を短縮できます(ステップ間のキャッシュ共有)。
5. マルチステージ Docker と Cloud Run デプロイ
5‑1. Dockerfile(サイズ変動要因と目安)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# ---------- Build stage ---------- FROM golang:1.20-alpine AS builder WORKDIR /src 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/server ./cmd/server # ---------- Runtime stage ---------- FROM alpine:3.19 AS runtime WORKDIR /app # ca-certificates は https 通信の必須パッケージだけをインストール RUN apk add --no-cache ca-certificates COPY --from=builder /app/server . EXPOSE 8080 ENTRYPOINT ["./server"] |
イメージサイズに関する注意点
| 要因 | 影響例 |
|---|---|
| ベースイメージ | golang:1.20-alpine(≈ 200 MB)→ ビルドステージのみで削除すれば、最終イメージは 10 ~ 30 MB 程度になることが多い |
静的リンク (CGO_ENABLED=0) |
musl 系のランタイムだけで動作でき、サイズが抑えられる |
| 依存ライブラリの数 | 大量の C ライブラリや外部バイナリをインストールすると数十 MB 上乗せされる |
結論
正確なサイズはdocker build後にdocker imagesで確認してください。本文中の「数十 MB」はあくまで 目安 とし、実際のサイズはプロジェクト固有の依存関係に左右されます。
5‑2. ローカルビルド・テスト
|
1 2 3 |
docker build -t myapi:dev . docker run --rm -p 8080:8080 myapi:dev |
ブラウザまたは curl http://localhost:8080/healthz が 200 OK を返せば成功です。
5‑3. Docker イメージの CI ビルド・プッシュ(GitHub Actions)
|
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 |
name: Release on: push: tags: - 'v*' # v1.0.0 等のタグが付いたときだけ実行 jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }},ghcr.io/${{ github.repository }}:latest |
5‑4. Cloud Run へのデプロイ手順
- Google Cloud SDK のインストール(ローカルでテストしたい場合)
bash
curl https://sdk.cloud.google.com | bash
exec -l $SHELL
gcloud init
-
イメージを Artifact Registry へプッシュ(GitHub Actions が自動実行)
-
Cloud Run にデプロイ
bash
PROJECT_ID=your-gcp-project
REGION=us-central1
IMAGE=us-central1-docker.pkg.dev/$PROJECT_ID/myapi/myapi:latest
gcloud run deploy myapi \
--image $IMAGE \
--region $REGION \
--platform managed \
--allow-unauthenticated \
--set-env-vars DB_DSN="${DB_USER}:${DB_PASS}@tcp(${DB_HOST}:3306)/${DB_NAME}?parseTime=true"
シークレット管理(Secret Manager 推奨)
|
1 2 3 4 5 6 7 8 9 10 |
# Secret 作成例 gcloud secrets create db-dsn --replication-policy="automatic" echo -n "user:pass@tcp(host:3306)/dbname?parseTime=true" | \ gcloud secrets versions add db-dsn --data-file=- # デプロイ時にシークレットを環境変数として注入 gcloud run deploy myapi \ ... \ --update-secrets=db-dsn=DB_DSN:latest |
セキュリティポイント
環境変数に平文で認証情報を書かない。Secret Manager 経由で注入するのが安全です。
Cloud Run の IAM でアクセス権限を最小化し、不要な公開は避けましょう。
6. 補足:本番運用に向けた追加ベストプラクティス
| 項目 | 内容 |
|---|---|
| ロギング | log/slog(Go 1.21 以降)や zap、zerolog を導入し、JSON ログで構造化。 |
| メトリクス | Prometheus 用エンドポイント (/metrics) と go-metrics ライブラリで CPU・GC 時間等を可視化。 |
| ヘルスチェック | Cloud Run のヘルスチェックは /healthz が 200 を返すだけで OK。必要なら DB 接続確認も追加。 |
| Graceful Shutdown | http.Server.Shutdown とシグナルハンドラで安全にプロセス終了。 |
| 依存関係の自動更新 | Dependabot や Renovate Bot を有効化し、モジュールを常に最新に保つ。 |
| コードレビューのチェックリスト | Lint, テストカバレッジ ≥ 80%、脆弱性スキャン(go vet -tests, gosec) が必須かどうかを CI に組み込む。 |
7. まとめ
| フェーズ | 主なポイント |
|---|---|
| 環境構築 | Go 1.20 の最新版インストール → go.mod 初期化、ディレクトリ設計 |
| ルーティング選択 | chi(軽量・ミドルウェアが簡単)か gorilla/mux(高度なパスマッチ)を要件で判断し、一貫した実装に統一 |
| API 実装 | net/http + ルータ → JSON エンコード/デコード、バリデーション (validator/v10)、統一エラーレスポンス |
| DB & テスト | MySQL ドライバ+リポジトリインターフェイス抽象化 → go-sqlmock と httptest で高速テスト |
| Lint & CI | golangci-lint(最新版)と GitHub Actions による自動ビルド・テスト・Lint |
| Docker | マルチステージ Docker で最終イメージは 10 ~ 30 MB の目安(実際は依存により変動) |
| デプロイ | GitHub Actions → Artifact Registry → Cloud Run、Secret Manager で機密情報管理 |
| 運用拡張 | ロギング・メトリクス・Graceful Shutdown・Dependabot による継続的改善 |
以上の手順とベストプラクティスを踏むことで、ローカル開発から本番環境への自動デプロイまで 一貫したフロー が実現できます。ぜひご自身のプロジェクトで試し、必要に応じてカスタマイズしてみてください!