取り組みの背景 — なぜ Go × Claude Code なのか
Go(Golang)は、Googleが開発したコンパイル型の静的型付け言語です。シンプルな文法、高速なコンパイル、優れた並行処理サポートにより、マイクロサービスやREST APIのバックエンドとして急速に普及しています。GitHubの調査では、2025年時点でGoはバックエンド開発者の間でPythonに次ぐ人気を誇っています。
Claude Codeとの組み合わせが特に強力な理由は3つあります。
第一に、型安全なコード生成の精度が高い 点です。Go言語の厳格な型システムは、Claude Codeがコードを生成する際に「コンパイルエラーを含まない正確なコード」を出力しやすい構造を持っています。TypeScriptと比べても、型推論の仕組みがシンプルなため、AIが生成するコードの品質が安定しています。
第二に、標準ライブラリの充実 です。Goはネットワーク処理、HTTP、JSON、暗号化などを標準ライブラリに含んでいます。Claude Codeは余分な依存関係を追加せずに機能を実装できるため、生成コードがシンプルに保たれます。
第三に、パフォーマンスと移植性 です。Goはシングルバイナリにコンパイルされるため、Dockerイメージを極限まで小さくできます。本番環境への展開がシンプルで、Claude Codeとの組み合わせでCI/CDパイプラインの自動化が非常に効果的に機能します。
Ginフレームワークによる高性能REST API
GORMとPostgreSQLによる型安全なデータ層
JWTによる認証・認可
Dockerマルチステージビルドによるコンテナ化
GitHub Actions CI/CDパイプライン
構造化ログとヘルスチェックエンドポイント
前提知識と開発環境の準備
必要な環境
本ガイドを進めるには以下の環境が必要です。
Claude Code(最新版)が動作する環境
Go 1.22以上(go versionで確認)
Docker Desktop
PostgreSQL(ローカルまたはDocker)
Git
Go環境のセットアップは、公式ダウンロードページ から行えます。
Claude Codeのターミナルで確認します。
# バージョン確認
go version
# go version go1.22.4 darwin/arm64
docker --version
# Docker version 26.1.0
git --version
# git version 2.44.0
Claude Code CLAUDE.md の設定
プロジェクト固有のコンテキストをClaude Codeに伝えるために、CLAUDE.mdを最初に用意します。
# Project: Go REST API
## 技術スタック
- Go 1.22+ / Gin v1.10 / GORM v2 / PostgreSQL 16
## アーキテクチャ
- Clean Architecture (Handler → Service → Repository)
- DI (Dependency Injection) パターン
## コード規約
- パッケージ名は小文字のみ
- エラーハンドリングは必ずwrappingする(fmt.Errorf("%w", err))
- ログはlog/slogを使用(標準ライブラリ)
- テストはtestifyを使用
## 禁止事項
- グローバル変数の使用
- panic()の使用(エラーは必ず返す)
- init()関数(DI以外での使用)
この設定があることで、Claude Codeは一貫したコードスタイルでコードを生成します。
プロジェクト構造の設計
Clean Architecture をGoで実現する
Claude Codeに以下のプロンプトでプロジェクト構造を生成させます。
CLAUDE.mdの規約に従い、ユーザー管理APIのプロジェクト構造を
Clean Architectureで設計してください。
ディレクトリ構造とgo.modを作成してください。
生成される構造は以下のようになります。
go-api/
├── cmd/
│ └── api/
│ └── main.go ← エントリーポイント
├── internal/
│ ├── domain/
│ │ ├── user.go ← ドメインモデル
│ │ └── errors.go ← ドメインエラー定義
│ ├── repository/
│ │ ├── user_repository.go ← インターフェース
│ │ └── postgres/
│ │ └── user_repository.go ← 実装
│ ├── service/
│ │ └── user_service.go ← ビジネスロジック
│ └── handler/
│ ├── user_handler.go ← HTTPハンドラー
│ └── middleware/
│ └── auth.go ← JWT認証ミドルウェア
├── pkg/
│ ├── database/
│ │ └── postgres.go ← DB接続
│ ├── jwt/
│ │ └── jwt.go ← JWTユーティリティ
│ └── validator/
│ └── validator.go ← バリデーション
├── migrations/ ← SQLマイグレーション
├── Dockerfile
├── docker-compose.yml
├── .github/
│ └── workflows/
│ └── ci.yml
├── CLAUDE.md
├── go.mod
└── go.sum
go.mod の初期化
cd go-api
go mod init github.com/yourusername/go-api
必要なパッケージをインストールします。
# Webフレームワーク
go get github.com/gin-gonic/gin@v1.10.0
# ORM
go get gorm.io/gorm@v1.25.10
go get gorm.io/driver/postgres@v1.5.9
# 環境変数
go get github.com/joho/godotenv@v1.5.1
# JWT
go get github.com/golang-jwt/jwt/v5@v5.2.1
# バリデーション
go get github.com/go-playground/validator/v10@v10.22.0
# テスト
go get github.com/stretchr/testify@v1.9.0
go get github.com/DATA-DOG/go-sqlmock@v1.5.0
ドメインモデルとリポジトリ層の実装
ドメインモデル定義
Claude Codeへのプロンプト例。
internal/domain/user.go を作成してください。
ユーザーモデルには以下のフィールドが必要です:
- ID(UUID)
- Email(ユニーク)
- PasswordHash
- Name
- CreatedAt / UpdatedAt / DeletedAt
GORMのBaseModelを活用してください。
生成される internal/domain/user.go の例。
package domain
import (
" time "
" github.com/google/uuid "
)
// User はユーザーのドメインモデルです
type User struct {
ID uuid . UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
Email string `gorm:"uniqueIndex;not null;size:255"`
PasswordHash string `gorm:"not null"`
Name string `gorm:"not null;size:100"`
CreatedAt time . Time
UpdatedAt time . Time
DeletedAt * time . Time `gorm:"index"`
}
// UserRepository はユーザーデータアクセスのインターフェースです
type UserRepository interface {
Create ( user * User ) error
FindByID ( id uuid . UUID ) ( * User , error )
FindByEmail ( email string ) ( * User , error )
Update ( user * User ) error
Delete ( id uuid . UUID ) error
List ( offset , limit int ) ([] * User , int64 , error )
}
// ドメインエラー定義
var (
ErrUserNotFound = & DomainError {Code: "USER_NOT_FOUND" , Message: "ユーザーが見つかりません" }
ErrUserAlreadyExists = & DomainError {Code: "USER_ALREADY_EXISTS" , Message: "このメールアドレスは既に使用されています" }
ErrInvalidCredential = & DomainError {Code: "INVALID_CREDENTIAL" , Message: "メールアドレスまたはパスワードが正しくありません" }
)
// DomainError はドメイン層のエラー型です
type DomainError struct {
Code string
Message string
}
func ( e * DomainError ) Error () string {
return e.Message
}
PostgreSQLリポジトリ実装
// internal/repository/postgres/user_repository.go
package postgres
import (
" errors "
" fmt "
" github.com/google/uuid "
" gorm.io/gorm "
" github.com/yourusername/go-api/internal/domain "
)
type userRepository struct {
db * gorm . DB
}
// NewUserRepository は PostgreSQL ユーザーリポジトリを生成します
func NewUserRepository ( db * gorm . DB ) domain . UserRepository {
return & userRepository {db: db}
}
func ( r * userRepository ) Create ( user * domain . User ) error {
result := r.db. Create (user)
if result.Error != nil {
// UNIQUE制約違反を検出
if isDuplicateError (result.Error) {
return domain.ErrUserAlreadyExists
}
return fmt. Errorf ( "ユーザー作成エラー: %w " , result.Error)
}
return nil
}
func ( r * userRepository ) FindByID ( id uuid . UUID ) ( * domain . User , error ) {
var user domain . User
result := r.db. Where ( "id = ? AND deleted_at IS NULL" , id). First ( & user)
if result.Error != nil {
if errors. Is (result.Error, gorm.ErrRecordNotFound) {
return nil , domain.ErrUserNotFound
}
return nil , fmt. Errorf ( "ユーザー取得エラー: %w " , result.Error)
}
return & user, nil
}
func ( r * userRepository ) FindByEmail ( email string ) ( * domain . User , error ) {
var user domain . User
result := r.db. Where ( "email = ? AND deleted_at IS NULL" , email). First ( & user)
if result.Error != nil {
if errors. Is (result.Error, gorm.ErrRecordNotFound) {
return nil , domain.ErrUserNotFound
}
return nil , fmt. Errorf ( "ユーザー検索エラー: %w " , result.Error)
}
return & user, nil
}
func ( r * userRepository ) List ( offset , limit int ) ([] * domain . User , int64 , error ) {
var users [] * domain . User
var total int64
// 総件数を取得
r.db. Model ( & domain . User {}). Where ( "deleted_at IS NULL" ). Count ( & total)
// ページネーション付きで取得
result := r.db. Where ( "deleted_at IS NULL" ).
Order ( "created_at DESC" ).
Offset (offset). Limit (limit).
Find ( & users)
if result.Error != nil {
return nil , 0 , fmt. Errorf ( "ユーザー一覧取得エラー: %w " , result.Error)
}
return users, total, nil
}
// isDuplicateError はPostgreSQLのUNIQUE制約違反を検出します
func isDuplicateError ( err error ) bool {
return err != nil && (
errors. Is (err, gorm.ErrDuplicatedKey) ||
containsString (err. Error (), "duplicate key" ) ||
containsString (err. Error (), "UNIQUE constraint failed" ))
}
サービス層とハンドラー層の実装
ビジネスロジック(サービス層)
サービス層では、パスワードのハッシュ化やJWT発行などのビジネスロジックを担当します。
// internal/service/user_service.go
package service
import (
" fmt "
" log/slog "
" github.com/google/uuid "
" golang.org/x/crypto/bcrypt "
" github.com/yourusername/go-api/internal/domain "
" github.com/yourusername/go-api/pkg/jwt "
)
type UserService struct {
repo domain . UserRepository
jwtManager * jwt . Manager
logger * slog . Logger
}
func NewUserService ( repo domain . UserRepository , jwtManager * jwt . Manager , logger * slog . Logger ) * UserService {
return & UserService {
repo: repo,
jwtManager: jwtManager,
logger: logger,
}
}
// Register は新規ユーザー登録を行います
func ( s * UserService ) Register ( email , password , name string ) ( * domain . User , error ) {
// パスワードハッシュ化(コスト12は本番推奨値)
hash, err := bcrypt. GenerateFromPassword ([] byte (password), 12 )
if err != nil {
return nil , fmt. Errorf ( "パスワードハッシュ化エラー: %w " , err)
}
user := & domain . User {
Email: email,
PasswordHash: string (hash),
Name: name,
}
if err := s.repo. Create (user); err != nil {
return nil , err // ドメインエラーをそのまま返す
}
s.logger. Info ( "ユーザー登録完了" ,
slog. String ( "user_id" , user.ID. String ()),
slog. String ( "email" , email),
)
return user, nil
}
// Login はログイン認証とJWTトークン発行を行います
func ( s * UserService ) Login ( email , password string ) ( string , error ) {
user, err := s.repo. FindByEmail (email)
if err != nil {
// ユーザーが見つからない場合も同じエラーを返す(タイミング攻撃対策)
return "" , domain.ErrInvalidCredential
}
// パスワード検証
if err := bcrypt. CompareHashAndPassword ([] byte (user.PasswordHash), [] byte (password)); err != nil {
s.logger. Warn ( "ログイン失敗" , slog. String ( "email" , email))
return "" , domain.ErrInvalidCredential
}
// JWTトークン発行
token, err := s.jwtManager. Generate (user.ID, user.Email)
if err != nil {
return "" , fmt. Errorf ( "トークン生成エラー: %w " , err)
}
s.logger. Info ( "ログイン成功" , slog. String ( "user_id" , user.ID. String ()))
return token, nil
}
HTTPハンドラー(Gin)
Claude Codeへのプロンプト例。
internal/handler/user_handler.go を作成してください。
以下のエンドポイントを実装:
- POST /api/v1/auth/register
- POST /api/v1/auth/login
- GET /api/v1/users/:id(JWT認証必須)
- GET /api/v1/users(JWT認証必須、ページネーション対応)
リクエストボディはJSONで、バリデーションにはgo-playground/validatorを使用。
エラーレスポンスは統一フォーマットにしてください。
// internal/handler/user_handler.go
package handler
import (
" errors "
" log/slog "
" net/http "
" strconv "
" github.com/gin-gonic/gin "
" github.com/google/uuid "
" github.com/yourusername/go-api/internal/domain "
" github.com/yourusername/go-api/internal/service "
)
type UserHandler struct {
service * service . UserService
logger * slog . Logger
}
func NewUserHandler ( service * service . UserService , logger * slog . Logger ) * UserHandler {
return & UserHandler {service: service, logger: logger}
}
// 統一エラーレスポンス
type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}
// 登録リクエスト
type RegisterRequest struct {
Email string `json:"email" validate:"required,email,max=255"`
Password string `json:"password" validate:"required,min=8,max=72"`
Name string `json:"name" validate:"required,min=1,max=100"`
}
// Register は新規ユーザー登録を処理します
func ( h * UserHandler ) Register ( c * gin . Context ) {
var req RegisterRequest
if err := c. ShouldBindJSON ( & req); err != nil {
c. JSON (http.StatusBadRequest, ErrorResponse {
Code: "INVALID_REQUEST" ,
Message: "リクエストの形式が正しくありません" ,
})
return
}
user, err := h.service. Register (req.Email, req.Password, req.Name)
if err != nil {
h. handleServiceError (c, err)
return
}
c. JSON (http.StatusCreated, gin . H {
"id" : user.ID,
"email" : user.Email,
"name" : user.Name,
"created_at" : user.CreatedAt,
})
}
// Login はログインとJWTトークン発行を処理します
func ( h * UserHandler ) Login ( c * gin . Context ) {
var req struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
if err := c. ShouldBindJSON ( & req); err != nil {
c. JSON (http.StatusBadRequest, ErrorResponse {Code: "INVALID_REQUEST" , Message: "入力内容を確認してください" })
return
}
token, err := h.service. Login (req.Email, req.Password)
if err != nil {
h. handleServiceError (c, err)
return
}
c. JSON (http.StatusOK, gin . H { "token" : token})
}
// GetUser はユーザー詳細を返します(JWT認証済み)
func ( h * UserHandler ) GetUser ( c * gin . Context ) {
idStr := c. Param ( "id" )
id, err := uuid. Parse (idStr)
if err != nil {
c. JSON (http.StatusBadRequest, ErrorResponse {Code: "INVALID_ID" , Message: "IDの形式が正しくありません" })
return
}
user, err := h.service. GetByID (id)
if err != nil {
h. handleServiceError (c, err)
return
}
c. JSON (http.StatusOK, gin . H {
"id" : user.ID,
"email" : user.Email,
"name" : user.Name,
"created_at" : user.CreatedAt,
})
}
// ListUsers はユーザー一覧をページネーション付きで返します
func ( h * UserHandler ) ListUsers ( c * gin . Context ) {
page, _ := strconv. Atoi (c. DefaultQuery ( "page" , "1" ))
perPage, _ := strconv. Atoi (c. DefaultQuery ( "per_page" , "20" ))
if page < 1 { page = 1 }
if perPage < 1 || perPage > 100 { perPage = 20 }
offset := (page - 1 ) * perPage
users, total, err := h.service. List (offset, perPage)
if err != nil {
c. JSON (http.StatusInternalServerError, ErrorResponse {Code: "INTERNAL_ERROR" , Message: "サーバーエラーが発生しました" })
return
}
c. JSON (http.StatusOK, gin . H {
"users" : users,
"total" : total,
"page" : page,
"per_page" : perPage,
})
}
// handleServiceError はサービス層のエラーをHTTPレスポンスに変換します
func ( h * UserHandler ) handleServiceError ( c * gin . Context , err error ) {
var domainErr * domain . DomainError
if errors. As (err, & domainErr) {
status := http.StatusBadRequest
switch domainErr.Code {
case "USER_NOT_FOUND" :
status = http.StatusNotFound
case "INVALID_CREDENTIAL" :
status = http.StatusUnauthorized
case "USER_ALREADY_EXISTS" :
status = http.StatusConflict
}
c. JSON (status, ErrorResponse {Code: domainErr.Code, Message: domainErr.Message})
return
}
h.logger. Error ( "予期せぬエラー" , slog. String ( "error" , err. Error ()))
c. JSON (http.StatusInternalServerError, ErrorResponse {Code: "INTERNAL_ERROR" , Message: "サーバーエラーが発生しました" })
}
JWT認証ミドルウェアとルーター設定
JWT管理ユーティリティ
// pkg/jwt/jwt.go
package jwt
import (
" fmt "
" time "
" github.com/golang-jwt/jwt/v5 "
" github.com/google/uuid "
)
type Manager struct {
secretKey [] byte
duration time . Duration
}
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt . RegisteredClaims
}
func NewManager ( secretKey string , duration time . Duration ) * Manager {
return & Manager {
secretKey: [] byte (secretKey),
duration: duration,
}
}
// Generate はJWTトークンを生成します
func ( m * Manager ) Generate ( userID uuid . UUID , email string ) ( string , error ) {
claims := & Claims {
UserID: userID. String (),
Email: email,
RegisteredClaims: jwt . RegisteredClaims {
ExpiresAt: jwt. NewNumericDate (time. Now (). Add (m.duration)),
IssuedAt: jwt. NewNumericDate (time. Now ()),
Issuer: "go-api" ,
},
}
token := jwt. NewWithClaims (jwt.SigningMethodHS256, claims)
signed, err := token. SignedString (m.secretKey)
if err != nil {
return "" , fmt. Errorf ( "JWT署名エラー: %w " , err)
}
return signed, nil
}
// Verify はJWTトークンを検証しClaimsを返します
func ( m * Manager ) Verify ( tokenStr string ) ( * Claims , error ) {
token, err := jwt. ParseWithClaims (tokenStr, & Claims {}, func ( t * jwt . Token ) ( interface {}, error ) {
if _, ok := t.Method.( * jwt . SigningMethodHMAC ); ! ok {
return nil , fmt. Errorf ( "予期しない署名方式: %v " , t.Header[ "alg" ])
}
return m.secretKey, nil
})
if err != nil {
return nil , fmt. Errorf ( "トークン検証失敗: %w " , err)
}
claims, ok := token.Claims.( * Claims )
if ! ok || ! token.Valid {
return nil , fmt. Errorf ( "無効なトークン" )
}
return claims, nil
}
Ginルーター設定
// cmd/api/main.go(抜粋)
package main
import (
" log/slog "
" os "
" time "
" github.com/gin-gonic/gin "
" github.com/joho/godotenv "
" github.com/yourusername/go-api/internal/handler "
" github.com/yourusername/go-api/internal/handler/middleware "
" github.com/yourusername/go-api/internal/repository/postgres "
" github.com/yourusername/go-api/internal/service "
pkgdb " github.com/yourusername/go-api/pkg/database "
" github.com/yourusername/go-api/pkg/jwt "
)
func main () {
// 環境変数ロード
godotenv. Load ()
// 構造化ログ設定
logger := slog. New (slog. NewJSONHandler (os.Stdout, & slog . HandlerOptions {
Level: slog.LevelInfo,
}))
slog. SetDefault (logger)
// データベース接続
db, err := pkgdb. NewPostgres (os. Getenv ( "DATABASE_URL" ))
if err != nil {
logger. Error ( "DB接続失敗" , slog. String ( "error" , err. Error ()))
os. Exit ( 1 )
}
// DI(依存性注入)
jwtManager := jwt. NewManager (os. Getenv ( "JWT_SECRET" ), 24 * time.Hour)
userRepo := postgres. NewUserRepository (db)
userSvc := service. NewUserService (userRepo, jwtManager, logger)
userHandler := handler. NewUserHandler (userSvc, logger)
authMiddleware := middleware. NewAuthMiddleware (jwtManager)
// Ginルーター
r := gin. New ()
r. Use (gin. Recovery ())
r. Use (middleware. RequestLogger (logger))
// ヘルスチェック
r. GET ( "/health" , func ( c * gin . Context ) {
c. JSON ( 200 , gin . H { "status" : "ok" })
})
// API v1
v1 := r. Group ( "/api/v1" )
{
auth := v1. Group ( "/auth" )
auth. POST ( "/register" , userHandler.Register)
auth. POST ( "/login" , userHandler.Login)
users := v1. Group ( "/users" )
users. Use (authMiddleware. Authenticate ())
users. GET ( "/:id" , userHandler.GetUser)
users. GET ( "" , userHandler.ListUsers)
}
port := os. Getenv ( "PORT" )
if port == "" {
port = "8080"
}
logger. Info ( "サーバー起動" , slog. String ( "port" , port))
r. Run ( ":" + port)
}
テスト戦略 — ユニット・統合・E2Eテスト
Claude Codeによるテスト生成を活用する場合、以下のプロンプトが効果的です。
internal/service/user_service_test.go を作成してください。
- Registerのテスト(正常系・重複エラー・無効なパスワード)
- Loginのテスト(正常系・ユーザー未存在・パスワード不一致)
- リポジトリはモック(interface)を使用
- testifyのassertを使用
// internal/service/user_service_test.go(抜粋)
package service_test
import (
" testing "
" github.com/stretchr/testify/assert "
" github.com/stretchr/testify/mock "
" github.com/yourusername/go-api/internal/domain "
" github.com/yourusername/go-api/internal/service "
)
// MockUserRepository はテスト用モックリポジトリ
type MockUserRepository struct {
mock . Mock
}
func ( m * MockUserRepository ) Create ( user * domain . User ) error {
args := m. Called (user)
return args. Error ( 0 )
}
func ( m * MockUserRepository ) FindByEmail ( email string ) ( * domain . User , error ) {
args := m. Called (email)
if args. Get ( 0 ) == nil {
return nil , args. Error ( 1 )
}
return args. Get ( 0 ).( * domain . User ), args. Error ( 1 )
}
// (他のメソッドも同様に実装)
func TestRegister_Success ( t * testing . T ) {
mockRepo := new ( MockUserRepository )
// 重複なし(Createが成功する)を設定
mockRepo. On ( "Create" , mock. AnythingOfType ( "*domain.User" )). Return ( nil )
svc := service. NewUserService (mockRepo, testJWTManager (), testLogger ())
user, err := svc. Register ( "test@example.com" , "SecurePass123!" , "テストユーザー" )
assert. NoError (t, err)
assert. NotNil (t, user)
assert. Equal (t, "test@example.com" , user.Email)
mockRepo. AssertExpectations (t)
}
func TestRegister_DuplicateEmail ( t * testing . T ) {
mockRepo := new ( MockUserRepository )
mockRepo. On ( "Create" , mock.Anything). Return (domain.ErrUserAlreadyExists)
svc := service. NewUserService (mockRepo, testJWTManager (), testLogger ())
_, err := svc. Register ( "existing@example.com" , "Pass123!" , "ユーザー" )
assert. ErrorIs (t, err, domain.ErrUserAlreadyExists)
}
func TestLogin_WrongPassword ( t * testing . T ) {
// bcryptハッシュ(パスワード: "CorrectPass123!")
hash := "$2a$12$..."
mockRepo := new ( MockUserRepository )
mockRepo. On ( "FindByEmail" , "user@example.com" ). Return ( & domain . User {
Email: "user@example.com" , PasswordHash: hash,
}, nil )
svc := service. NewUserService (mockRepo, testJWTManager (), testLogger ())
_, err := svc. Login ( "user@example.com" , "WrongPass!" )
assert. ErrorIs (t, err, domain.ErrInvalidCredential)
}
テストを実行します。
# 全テスト実行
go test ./... -v
# カバレッジ確認
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out
本番品質のAPIでは、カバレッジ70%以上を目標にします。
Dockerマルチステージビルド
Goのシングルバイナリという特性を最大限に活かした、最小イメージサイズのDockerfileを作成します。
# ─── Stage 1: ビルドステージ ───────────────────────────────────────────────
FROM golang:1.22-alpine AS builder
WORKDIR /app
# モジュールキャッシュを活用するため先にコピー
COPY go.mod go.sum ./
RUN go mod download
# ソースコードをコピーしてビルド
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags= "-w -s" \
-o /bin/api \
./cmd/api
# ─── Stage 2: 実行ステージ ─────────────────────────────────────────────────
FROM scratch
# CA証明書(HTTPS通信に必要)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# バイナリのみコピー
COPY --from=builder /bin/api /api
EXPOSE 8080
ENTRYPOINT [ "/api" ]
FROM scratch を使うことで、最終イメージを約15MB以下に抑えられます(Node.jsベースのAPIと比べると1/10以下)。
# docker-compose.yml(開発環境)
services :
api :
build : .
ports :
- "8080:8080"
environment :
DATABASE_URL : postgres://postgres:postgres@db:5432/go_api?sslmode=disable
JWT_SECRET : your-secret-key-change-in-production
depends_on :
db :
condition : service_healthy
db :
image : postgres:16-alpine
environment :
POSTGRES_USER : postgres
POSTGRES_PASSWORD : postgres
POSTGRES_DB : go_api
healthcheck :
test : [ "CMD-SHELL" , "pg_isready -U postgres" ]
interval : 5s
timeout : 5s
retries : 5
volumes :
- postgres_data:/var/lib/postgresql/data
volumes :
postgres_data :
GitHub Actions CI/CDパイプライン
# .github/workflows/ci.yml
name : CI/CD Pipeline
on :
push :
branches : [ main , develop ]
pull_request :
branches : [ main ]
jobs :
test :
name : Test
runs-on : ubuntu-latest
services :
postgres :
image : postgres:16-alpine
env :
POSTGRES_USER : postgres
POSTGRES_PASSWORD : postgres
POSTGRES_DB : go_api_test
options : >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports :
- 5432:5432
steps :
- uses : actions/checkout@v4
- name : Go環境セットアップ
uses : actions/setup-go@v5
with :
go-version : '1.22'
cache : true
- name : 依存関係インストール
run : go mod download
- name : リント(静的解析)
uses : golangci/golangci-lint-action@v6
with :
version : latest
- name : テスト実行
env :
DATABASE_URL : postgres://postgres:postgres@localhost:5432/go_api_test?sslmode=disable
JWT_SECRET : test-secret-key
run : |
go test ./... -v -race -coverprofile=coverage.out
go tool cover -func=coverage.out | tail -1
- name : カバレッジレポートアップロード
uses : codecov/codecov-action@v4
with :
files : ./coverage.out
build :
name : Docker Build
runs-on : ubuntu-latest
needs : test
if : github.ref == 'refs/heads/main'
steps :
- uses : actions/checkout@v4
- name : Docker Buildx セットアップ
uses : docker/setup-buildx-action@v3
- name : Docker Hub ログイン
uses : docker/login-action@v3
with :
username : ${{ secrets.DOCKER_USERNAME }}
password : ${{ secrets.DOCKER_TOKEN }}
- name : イメージビルド&プッシュ
uses : docker/build-push-action@v5
with :
context : .
push : true
tags : |
${{ secrets.DOCKER_USERNAME }}/go-api:latest
${{ secrets.DOCKER_USERNAME }}/go-api:${{ github.sha }}
cache-from : type=gha
cache-to : type=gha,mode=max
よくあるエラーと対処法
エラー1: gorm: record not found が意図せず返る
原因 : GORMのFirst()はレコードが存在しない場合にErrRecordNotFoundを返しますが、Find()は空のスライスを返します。用途に合わせた使い分けが必要です。
対処法 : 1件取得にはFirst()を使い、必ずerrors.Is(err, gorm.ErrRecordNotFound)でチェックします。
// ✅ 正しい使い方
var user domain . User
if err := db. First ( & user, "id = ?" , id).Error; err != nil {
if errors. Is (err, gorm.ErrRecordNotFound) {
return nil , domain.ErrUserNotFound
}
return nil , fmt. Errorf ( "DB取得エラー: %w " , err)
}
エラー2: JWT token is expired エラーが頻発する
原因 : サーバーの時刻がずれていると、JWTのExpiresAt検証に失敗することがあります。Dockerコンテナ内でも発生しやすい問題です。
対処法 : コンテナの時刻同期を確認し、JWTの有効期限を適切に設定します。また、leeway(許容誤差)を設定することで軽減できます。
// leeway 30秒を許容
token, err := jwt. ParseWithClaims (tokenStr, & Claims {},
keyFunc,
jwt. WithLeeway ( 30 * time.Second),
)
エラー3: bind: address already in use
原因 : 前のプロセスがポートを占有しています。Claude Codeでのホットリロード開発時に起きやすいエラーです。
対処法 : Claude Codeに依頼するか、以下のコマンドで解決します。
# 8080ポートを使用しているプロセスを確認
lsof -ti:8080
# プロセスを強制終了
lsof -ti:8080 | xargs kill -9
Claude Code × Go 開発の実践テクニック
テクニック1: エラーハンドリングをまとめて生成する
Goでは全ての関数でエラーを返す必要があり、エラーハンドリングが冗長になりがちです。Claude Codeに以下のプロンプトを使うと、一貫したエラー処理を持つ関数群を生成できます。
以下の仕様に従い、service層の全メソッドのエラーハンドリングを
ドメインエラーへの変換を含む形で実装してください:
- gorm.ErrRecordNotFound → domain.ErrUserNotFound
- 重複エラー → domain.ErrUserAlreadyExists
- その他 → wrappedエラー(fmt.Errorf付き)
テクニック2: テーブル駆動テストを依頼する
user_handler_test.go に、以下のエンドポイントのテーブル駆動テストを
作成してください(httptest.NewRecorderを使用):
- POST /api/v1/auth/register
- 正常系(201)
- 重複エラー(409)
- バリデーションエラー(400)
- 無効なJSON(400)
各テストケースをTestCasesスライスで管理してください。
テクニック3: マイグレーションファイルの自動生成
migrations/ フォルダに、以下のテーブルのSQLマイグレーション
(up/down)を作成してください:
- users(UUID主キー、email unique、created_at/updated_at/deleted_at)
- 命名規則: 001_create_users_table.up.sql / .down.sql
まとめ
ここで扱うのはClaude Code × Go言語を組み合わせた本番グレードのREST API開発を、設計からCI/CDまで体系的に解説しました。
重要なポイントをまとめます。
設計 : CLAUDE.mdでアーキテクチャ方針を明記することが、Claude Codeのコード生成品質を大きく左右します。
実装 : Clean Architectureの層分離(Handler → Service → Repository)を守ることで、テスタビリティが高いコードが維持できます。
テスト : インターフェースを積極的に活用したモック設計により、データベースなしでのユニットテストが可能になります。
デプロイ : FROM scratchを使ったマルチステージビルドにより、15MB以下の最小Dockerイメージを実現できます。
CI/CD : golangci-lintによる静的解析とカバレッジ計測を自動化することで、コード品質が継続的に保証されます。
GoとClaude Codeの組み合わせは、型安全なコード生成・高速なコンパイル・最小デプロイサイズという三拍子が揃い、個人開発者からエンタープライズチームまで幅広いユースケースに対応できます。次のステップとして、Claude Code × Next.js 15 App Router 本番開発マスターガイド でフロントエンドと組み合わせた実装にも挑戦してみてください。