Contents
1️⃣ 開発環境のセットアップ(Go 1.22 以降・モジュール管理)
📌 ポイント
- Go 1.22+ のインストール → 標準ライブラリに多数のセキュリティ改善と高速なモジュールキャッシュが追加。
go.modによる依存管理 → ローカル・CI・本番すべてで同一バージョンを保証。
📖 背景
Go 1.22 は go vet の検査項目増加、staticcheck との相性向上、そしてモジュールの checksum データベース が標準化されたことにより、サプライチェーン攻撃への耐性が高まりました。
CI 環境で同一キャッシュを使うだけで、依存解決時間は 30 % 前後短縮できます。
🛠️ インストール例(Linux x86_64)
|
1 2 3 4 5 6 7 8 9 10 11 |
# 公式サイトから最新版 tar.gz を取得(2026‑04‑18 時点) curl -LO https://go.dev/dl/go1.22.linux-amd64.tar.gz # /usr/local に展開し、PATH を永続化 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 |
⚠️ 注意:インストールスクリプトに認証情報(API トークン等)を埋め込まないでください。
📦 プロジェクト作成とモジュール初期化
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# 作業ディレクトリ作成 & 移動 mkdir -p $HOME/work/go-rest-api && cd $_ # go.mod の生成(module パスは実際のリポジトリ URL に置き換えてください) go mod init github.com/yourname/go-rest-api # 必要なライブラリを取得(バージョンは semver で固定推奨) go get github.com/gin-gonic/gin@v1.9.2 go get gorm.io/gorm@v1.25.0 gorm.io/driver/postgres@v1.5.0 go get github.com/stretchr/testify@v1.8.4 go get golang.org/x/crypto/bcrypt@latest |
ベストプラクティス:
go.mod tidyで未使用依存を除去し、CI のgo mod verifyでチェックすると安全です。
2️⃣ プロジェクト構成とディレクトリ設計
📌 ポイント
- 層ごとの責務分離 → 循環参照防止・テスト容易性向上。
- Go の公式推奨に合わせた
cmd/,internal/,pkg/を活用。
📁 ディレクトリ構造(例)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
go-rest-api/ ├─ cmd/ # アプリケーションのエントリポイント │ └─ server/main.go ├─ internal/ # プロジェクト内部専用パッケージ(外部からはインポート不可) │ ├─ config/ # 環境変数・設定ファイルロード │ ├─ handler/ # Gin ハンドラ │ ├─ middleware/ # 認証・ロギングミドルウェア │ ├─ model/ # Gorm エンティティ定義 │ ├─ repository/ # DB アクセス抽象化(インタフェース+実装) │ └─ service/ # ビジネスロジック ├─ pkg/ # 他プロジェクトでも再利用可能なユーティリティ │ └─ jwt/ # JWT 発行・検証ヘルパー ├─ configs/ # yaml / json 設定例(サンプルは .example で管理) ├─ test/ # 統合テスト用の設定・データセット ├─ Dockerfile └─ docker-compose.yml |
補足:
internal/に配置したパッケージは、Go コンパイラがインポートを禁止するため、誤って外部サービスで再利用されるリスクが根本的に排除できます。
🔗 参考情報
- Zenn の速習シリーズ(閲覧日: 2026‑04‑18)
https://zenn.dev/isawa/articles/069cff08d64904 (※リンク切れの可能性がある場合は、同テーマの「Go Project Layout」記事を検索してください)
3️⃣ Gin を使ったルーティングとハンドラ実装
📌 ポイント
router.Groupと構造体タグによる 入力バリデーション がシンプルに記述可能。- 統一エラー形式でフロントエンド側のハンドリングを楽にする。
🧩 主要コード例
|
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 |
// cmd/server/main.go package main import ( "github.com/gin-gonic/gin" "github.com/yourname/go-rest-api/internal/handler" "github.com/yourname/go-rest-api/internal/middleware" ) func main() { r := gin.New() r.Use(gin.Logger(), gin.Recovery()) apiV1 := r.Group("/api/v1") { apiV1.POST("/users", handler.CreateUser) // ユーザー登録 apiV1.POST("/login", handler.Login) // ログイン protected := apiV1.Group("/") protected.Use(middleware.JWTAuth()) // 認証ミドルウェア { protected.GET("/profile", handler.GetProfile) } } r.Run(":8080") } |
|
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 |
// internal/handler/user.go package handler import ( "net/http" "github.com/gin-gonic/gin" "github.com/yourname/go-rest-api/internal/service" ) type RegisterReq struct { Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8"` } // CreateUser は入力バリデーションエラーを統一フォーマットで返す例 func CreateUser(c *gin.Context) { var req RegisterReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "code": "INVALID_INPUT", "message": err.Error(), }) return } u, err := service.Register(req) if err != nil { // エラーログは Zap など高速ロガーで出力(省略) c.JSON(http.StatusInternalServerError, gin.H{ "code": "INTERNAL", "message": err.Error(), }) return } c.JSON(http.StatusCreated, gin.H{ "id": u.ID, "name": u.Name, "email": u.Email, }) } |
⚠️ セキュリティ注意:パスワードは平文で扱わず、
bcrypt.GenerateFromPasswordでハッシュ化した値をservice.Registerに渡す実装にしてください。
📚 参考情報
- Glukhov の 2025 年版ガイド(閲覧日: 2026‑04‑18)
https://www.glukhov.org/ja/post/2025/11/implementing-api-in-go/ (代替として「Go API Design Patterns」書籍をご参照ください)
4️⃣ 永続化層構築と JWT を用いた認証・認可フロー
📌 ポイント
- Gorm + PostgreSQL のリポジトリパターンでテスト容易性を確保。
- JWT(HS256) によるステートレス認証は、スケールアウト時のセッション共有コストを削減。
🔐 シークレット管理のベストプラクティス
コードにハードコーディングしたシークレットは絶対に残さないでください。以下のように 環境変数 から取得し、必須チェックも行います。
|
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 59 |
// internal/pkg/jwt/token.go package jwt import ( "errors" "os" "time" "github.com/golang-jwt/jwt/v5" ) var ( secret []byte ) // init はパッケージロード時に呼ばれ、環境変数が未設定なら致命的エラーにする func init() { s := os.Getenv("JWT_SECRET") if s == "" { panic("environment variable JWT_SECRET is required") } secret = []byte(s) } // Claims はカスタムクレーム構造体 type Claims struct { UserID uint `json:"uid"` Role string `json:"role"` jwt.RegisteredClaims } // Generate はトークンを生成(有効期限は 24h とする) func Generate(userID uint, role string) (string, error) { if userID == 0 || role == "" { return "", errors.New("invalid arguments for token generation") } c := Claims{ UserID: userID, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } t := jwt.NewWithClaims(jwt.SigningMethodHS256, c) return t.SignedString(secret) } // Validate は受信トークンの検証 func Validate(tokenStr string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) { return secret, nil }) if err != nil || !token.Valid { return nil, err } return token.Claims.(*Claims), nil } |
DB 接続設定例(環境変数使用)
|
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 |
// internal/config/database.go package config import ( "fmt" "os" "gorm.io/driver/postgres" "gorm.io/gorm" ) func NewDB() (*gorm.DB, error) { dsn := os.Getenv("POSTGRES_DSN") if dsn == "" { return nil, fmt.Errorf("environment variable POSTGRES_DSN is required") } db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{}) if err != nil { return nil, fmt.Errorf("failed to connect DB: %w", err) } // AutoMigrate は開発環境のみ有効化 if os.Getenv("APP_ENV") != "production" { if err := db.AutoMigrate(&model.User{}); err != nil { return nil, err } } return db, nil } |
📂 リポジトリ層のインタフェース例
|
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 |
// internal/repository/user_repo.go package repository import ( "context" "github.com/yourname/go-rest-api/internal/model" "gorm.io/gorm" ) type UserRepo interface { Create(ctx context.Context, u *model.User) error FindByEmail(ctx context.Context, email string) (*model.User, error) } // 実装は Gorm を内部に持つ構造体 type userRepo struct{ db *gorm.DB } func NewUserRepo(db *gorm.DB) UserRepo { return &userRepo{db} } func (r *userRepo) Create(ctx context.Context, u *model.User) error { return r.db.WithContext(ctx).Create(u).Error } func (r *userRepo) FindByEmail(ctx context.Context, email string) (*model.User, error) { var u model.User if err := r.db.WithContext(ctx). Where("email = ?", email). First(&u).Error; err != nil { return nil, err } return &u, nil } |
🛡️ ミドルウェアでのトークン検証
|
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 |
// internal/middleware/jwt.go package middleware import ( "net/http" "strings" "github.com/gin-gonic/gin" "github.com/yourname/go-rest-api/internal/pkg/jwt" ) func JWTAuth() gin.HandlerFunc { return func(c *gin.Context) { auth := c.GetHeader("Authorization") if auth == "" || !strings.HasPrefix(auth, "Bearer ") { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "code": "UNAUTHENTICATED", "message": "Missing or malformed Authorization header", }) return } tokenStr := strings.TrimPrefix(auth, "Bearer ") claims, err := jwt.Validate(tokenStr) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "code": "INVALID_TOKEN", "message": err.Error(), }) return } // コンテキストにユーザー情報を注入(後続ハンドラで取得可能) c.Set("uid", claims.UserID) c.Set("role", claims.Role) c.Next() } } |
備考:
jwt.Validateが返すエラーは*jwt.ValidationErrorです。必要に応じてエラー種別(期限切れ・署名不正)を分岐させてもよいでしょう。
🔗 参考情報
- App‑Tatsujin の 2025 年版ガイド(閲覧日: 2026‑04‑18)
https://app-tatsujin.com/go-restful-api-guide-2025/ (代替として「Building APIs with Go」書籍をご参照ください)
5️⃣ テスト・Docker 化・CI/CD の基本
📌 ポイント
| 項目 | 主なツール | 補足 |
|---|---|---|
| ユニットテスト | testing, httptest, testify |
ハンドラ単体、サービス層、リポジトリ層それぞれにテストを書きやすい構造 |
| モック | sqlmock, gomock |
DB 依存を外部化し、CI の実行速度を向上 |
| Docker | マルチステージビルド | ビルド時の依存は golang:1.22-alpine、ランタイムは軽量 alpine:3.20 |
| CI | GitHub Actions | PR 毎にテスト・ビルド・脆弱性スキャン(govulncheck)を走らせる |
🧪 ハンドラ単体テスト例
|
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 |
// internal/handler/user_test.go package handler import ( "bytes" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) func TestCreateUser_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.POST("/users", CreateUser) payload := `{"name":"Taro","email":"taro@example.com","password":"Passw0rd!"}` req, _ := http.NewRequest(http.MethodPost, "/users", bytes.NewBufferString(payload)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) // 期待するキーが JSON に含まれるか簡易検証 assert.Contains(t, w.Body.String(), `"email":"taro@example.com"`) } |
ポイント:テストは外部サービスに依存しないよう、
service.Registerはモック化(例:gomock)しておくと CI が高速になります。
🐳 Dockerfile(マルチステージ)
|
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 |
# ---------- Builder ---------- FROM golang:1.22-alpine AS builder WORKDIR /src # Go モジュールキャッシュを活用 COPY go.mod go.sum ./ RUN go mod download # ソースコード全体をコピー COPY . . # 静的リンクでビルド(CGO 無効化) RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ go build -ldflags="-s -w" -o /app/server ./cmd/server # ---------- Runtime ---------- FROM alpine:3.20 AS runtime LABEL maintainer="yourname@example.com" WORKDIR /app COPY --from=builder /app/server . # 必要なら ca-certificates をインストール(外部 API 呼び出しがある場合) RUN apk add --no-cache ca-certificates EXPOSE 8080 ENTRYPOINT ["./server"] |
🛠️ docker-compose.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 |
version: "3.9" services: api: build: . ports: - "8080:8080" environment: - APP_ENV=development - JWT_SECRET=${JWT_SECRET:-changeme} # .env ファイルで上書き推奨 - POSTGRES_DSN=${POSTGRES_DSN:-host=db user=app password=secret dbname=app port=5432 sslmode=disable} depends_on: - db db: image: postgres:16-alpine restart: always environment: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: app volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata: |
環境変数の取り扱い:
docker-compose.ymlのデフォルトは開発用です。実運用時はdocker secretや外部シークレット管理ツール(例: HashiCorp Vault)に置き換えてください。
⚙️ 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# .github/workflows/ci.yml name: CI on: push: branches: [ main ] pull_request: jobs: test-and-build: runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: app ports: ["5432:5432"] options: >- --health-cmd "pg_isready -U app" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Cache Go 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: Run static analysis run: | go vet ./... staticcheck ./... - name: Run tests env: POSTGRES_DSN: host=localhost user=app password=secret dbname=app port=5432 sslmode=disable JWT_SECRET: test-secret run: go test ./... -v -race - name: Build Docker image run: | docker build -t yourname/go-rest-api:${{ github.sha }} . |
追加の安全対策:
govulncheck ./...をテストステップに組み込むと、依存ライブラリの既知脆弱性を自動検出できます。
6️⃣ まとめ
| 項目 | キーポイント |
|---|---|
| 開発環境 | Go 1.22+ と go.mod による依存一元管理、vet/staticcheck・govulncheckで品質担保 |
| ディレクトリ設計 | cmd/, internal/, pkg/ の層分けで循環参照防止、テスト容易性向上 |
| Gin ハンドラ | router.Group と構造体タグでバリデーション統一、エラーレスポンスはコード・メッセージの2要素 |
| 永続化 & 認証 | Gorm + リポジトリパターン、bcrypt でハッシュ化、JWT シークレットは環境変数管理 |
| テスト・Docker·CI | httptest/testify の単体テスト、マルチステージ Dockerfile、GitHub Actions による自動ビルド・テスト・脆弱性診断 |
この手順に沿ってプロジェクトを構築すれば、ローカル環境 → CI → 本番デプロイ の全工程で同一の挙動が保証され、安全かつ保守しやすい Go 製 RESTful API が完成します。
※本稿は執筆時点(2026‑04‑18)の情報に基づいています。外部リンク先が変更・削除された場合は、同等の公式ドキュメントや信頼できる書籍を代替資料としてご利用ください。