Contents
- 1 Go 1.20 開発環境の構築手順とインストール方法
- 2 4️⃣ 推奨 IDE と拡張機能
- 3 5️⃣ プロジェクト雛形の作成
- 4 6️⃣ ディレクトリレイアウト(cmd, internal, pkg)
- 5 7️⃣ go.mod と依存関係のベストプラクティス
- 6 8️⃣ net/http と chi の比較と選択理由
- 7 9️⃣ ハンドラ実装例(コンテキスト活用)
- 8 10️⃣ JSON バインディングとバリデーション
- 9 11️⃣ エラーハンドリングとステータスコード統一
- 10 12️⃣ ドライバ設定と DSN のベストプラクティス
- 11 13️⃣ 環境変数から設定をロード
- 12 14️⃣ CRUD リポジトリ実装例(MySQL)
- 13 15️⃣ golangci-lint の推奨設定 (.golangci.yml)
- 14 16️⃣ Dockerfile(マルチステージビルド)
- 15 17️⃣ docker-compose.yml(ローカル開発向け)
- 16 18️⃣ GitHub Actions ワークフロー (.github/workflows/ci.yml)
- 17 19️⃣ テーブル駆動テスト(ユニットテスト例)
- 18 20️⃣ 環境変数・シークレット管理
- 19 21️⃣ TLS 終端とヘルスチェック
- 20 22️⃣ データベースユーザーの最小権限化
- 21 23️⃣ 本番デプロイ時のチェックリスト
- 22 🎉 まとめ
Go 1.20 開発環境の構築手順とインストール方法
1️⃣ 結論
公式サイトから配布されている Go 1.20 のバイナリ を取得し、OS に合わせた手順で展開・PATH を通すだけで go run や go build が即座に利用可能になります。
2️⃣ 理由
- Go は シングルバイナリ配布(実行ファイルと標準ライブラリが一体)なので、インストール後は環境変数
PATHにディレクトリを追加するだけでどこでもビルド・実行ができる。 - バージョン管理が明示的になるため、CI でも同一バイナリを再利用でき、再現性 が担保される。
3️⃣ 各 OS のインストール手順
| OS | 手順概要 |
|---|---|
| macOS (Homebrew) | bash<br>brew install go@1.20<br># Homebrew がインストール先を出力 → /opt/homebrew/opt/go/libexec/bin(Apple Silicon)<br># または /usr/local/opt/go/libexec/bin(Intel)<br>echo 'export PATH=$(brew --prefix go@1.20)/libexec/bin:$PATH' >> ~/.zshrc<br>source ~/.zshrc |
| Ubuntu 22.04 | bash<br>wget https://go.dev/dl/go1.20.linux-amd64.tar.gz -O /tmp/go1.20.tar.gz<br>sudo tar -C /usr/local -xzf /tmp/go1.20.tar.gz<br># /usr/local/go がデフォルトの展開先<br>echo 'export PATH=/usr/local/go/bin:$PATH' >> ~/.profile<br>source ~/.profile |
| Windows 11 | 1. https://go.dev/dl/go1.20.windows-amd64.msi をダウンロード |
| 2. インストーラを実行(「Add to PATH」チェックボックスは必ずオン) 3. cmd または PowerShell で go version が表示されれば完了。 |
共通インストール確認
|
1 2 3 |
$ go version go version go1.20 linux/amd64 # macOS や Windows の場合はそれぞれの OS 表示になる |
ポイント:インストール直後に
go env GOROOTとgo env GOPATHが期待通り設定されているか確認すると、後続の手順でトラブルが減ります。
4️⃣ 推奨 IDE と拡張機能
| エディタ | 主な拡張機能 | メリット |
|---|---|---|
| VSCode | Go(公式)・gopls·golangci-lint |
コード補完、インラインドキュメント、デバッグ、静的解析が統合。 |
| GoLand (JetBrains) | 標準搭載 | 高度なリファクタリングとプロファイラが利用可能(有料)。 |
Tip:VSCode では
settings.jsonに以下を追記すると快適です。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "go.formatTool": "gofmt", "go.lintTool": "golangci-lint", "go.useLanguageServer": true, "[go]": { "editor.codeActionsOnSave": { "source.organizeImports": true, "source.fixAll": true } } } |
5️⃣ プロジェクト雛形の作成
|
1 2 3 4 |
mkdir todo-api && cd $_ go mod init github.com/yourname/todo-api # module 名はリポジトリ URL が推奨 code . # VSCode 起動(インストール済み前提) |
注意:
go.modのmodule行は GitHub の所有者名 / リポジトリ名 をそのまま記述すると、外部パッケージからの import が自然に機能します。
プロジェクト構成とモジュール管理
6️⃣ ディレクトリレイアウト(cmd, internal, pkg)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
todo-api/ ├─ cmd/ # エントリポイント (main パッケージ) │ └─ server/ │ └─ main.go ├─ internal/ # アプリ内部でのみ使用するパッケージ │ ├─ todo/ # ドメインロジック │ │ ├─ handler.go │ │ ├─ service.go │ │ └─ repository.go │ └─ config/ # 設定構造体・ロード処理 ├─ pkg/ # 他プロジェクトでも再利用したいユーティリティ │ └─ logger/ │ └─ logger.go └─ go.mod |
cmd:実行ファイルをビルドする場所。1 つのバイナリにつき 1 ディレクトリ が基本。internal:Go のinternal制約 により、モジュール外部からインポートできないことが保証されるため、実装のカプセル化に最適。pkg:汎用的なライブラリはここに置く。別リポジトリへ切り出す際もパスを変えるだけで済む。
7️⃣ go.mod と依存関係のベストプラクティス
|
1 2 3 4 5 |
# 必要な外部パッケージを取得(バージョンは SemVer 推奨) go get github.com/go-chi/chi/v5@v5.0.9 go get github.com/go-sql-driver/mysql@v1.7.1 go get golang.org/x/tools/cmd/golangci-lint@latest |
- モジュール名は
github.com/<ユーザー>/<リポジトリ>をそのまま使用。 - 依存パッケージは
go.modに明示的に記載し、go.sumが自動生成・検証されるので CI の再現性が向上する。
|
1 2 3 |
# CI 時の整合性チェック例 go mod verify |
API 実装:ルーティング・ハンドラ・バリデーション
8️⃣ net/http と chi の比較と選択理由
| 項目 | net/http (標準) | chi |
|---|---|---|
| 学習コスト | ★☆☆☆☆(標準なので敷居は低い) | ★★☆☆☆(ミドルウェア概念が追加される) |
| ルート定義の簡潔さ | 冗長になりやすい | DSL 風にシンプル |
| ミドルウェア | 手作業でラップ必要 | Use メソッドでチェーン化可能 |
| パフォーマンス | 高速(標準実装) | 同等か若干オーバーヘッド |
結論:開発速度と保守性を重視するなら chi を採用。必要に応じて
net/httpのハンドラをそのまま利用できる互換性もある。
9️⃣ ハンドラ実装例(コンテキスト活用)
|
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 |
// internal/todo/handler.go package todo import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" ) func RegisterRoutes(r chi.Router, svc Service) { r.Post("/todos", createTodoHandler(svc)) r.Get("/todos/{id}", getTodoHandler(svc)) // 省略:PUT /todos/{id}, DELETE /todos/{id} } // POST /todos func createTodoHandler(svc Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req CreateReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := validateCreate(req); err != nil { writeError(w, http.StatusUnprocessableEntity, err.Error()) return } todo, err := svc.Create(r.Context(), req) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create") return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(todo) } } // GET /todos/{id} func getTodoHandler(svc Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") todo, err := svc.GetByID(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, "todo not found") return } json.NewEncoder(w).Encode(todo) } } |
10️⃣ JSON バインディングとバリデーション
- 構造体タグで
jsonとvalidateを同時に指定。 - バリデーションは go-playground/validator/v10 が業界標準。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
type CreateReq struct { Title string `json:"title" validate:"required,min=1,max=200"` Done bool `json:"done"` } // validator のインスタンスは再利用できるようにパッケージレベルで保持するのがベストプラクティス var v = validator.New() func validateCreate(req CreateReq) error { return v.Struct(req) } |
11️⃣ エラーハンドリングとステータスコード統一
|
1 2 3 4 5 6 |
func writeError(w http.ResponseWriter, code int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) } |
- エラーメッセージは JSON で返すことでクライアント側のパースが容易になる。
google/go-githubの実装を参考に、独自エラー型(例:NotFoundError,ValidationError)と HTTP ステータスコードのマッピングを集中管理すると保守性が上がる。
MySQL 連携と CRUD 実装
12️⃣ ドライバ設定と DSN のベストプラクティス
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// internal/config/db.go package config import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func NewDB(user, pass, host, dbname string) (*sql.DB, error) { dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true&loc=Local", user, pass, host, dbname) db, err := sql.Open("mysql", dsn) if err != nil { return nil, err } // 接続プールのチューニング例 db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(0) return db, nil } |
parseTime=trueとloc=Localを付与すると MySQL のDATETIMEが自動的にtime.Timeに変換でき、タイムゾーン問題が回避できる。
13️⃣ 環境変数から設定をロード
|
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 |
// internal/config/config.go package config import ( "os" ) type Config struct { DBUser string DBPass string DBHost string DBName string } func Load() (*Config, error) { cfg := &Config{ DBUser: os.Getenv("DB_USER"), DBPass: os.Getenv("DB_PASS"), DBHost: os.Getenv("DB_HOST"), DBName: os.Getenv("DB_NAME"), } // 必要な環境変数が未設定ならエラーにする if cfg.DBUser == "" || cfg.DBPass == "" || cfg.DBHost == "" || cfg.DBName == "" { return nil, fmt.Errorf("database environment variables are not fully set") } return cfg, nil } |
- 起動時に
cfg, _ := config.Load()で取得し、db, _ := config.NewDB(cfg.DBUser, ...)と組み合わせる。 - アプリ終了時は
defer db.Close()を必ず呼び出す。
14️⃣ CRUD リポジトリ実装例(MySQL)
|
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 |
// internal/todo/repository.go package todo import ( "context" "database/sql" ) type Todo struct { ID int64 `json:"id"` Title string `json:"title"` Done bool `json:"done"` } type Repository interface { Create(ctx context.Context, t Todo) (int64, error) GetByID(ctx context.Context, id int64) (*Todo, error) Update(ctx context.Context, t Todo) error Delete(ctx context.Context, id int64) error } type mysqlRepo struct{ db *sql.DB } func NewRepository(db *sql.DB) Repository { return &mysqlRepo{db: db} } func (r *mysqlRepo) Create(ctx context.Context, t Todo) (int64, error) { res, err := r.db.ExecContext(ctx, "INSERT INTO todos (title, done) VALUES (?, ?)", t.Title, t.Done) if err != nil { return 0, err } return res.LastInsertId() } func (r *mysqlRepo) GetByID(ctx context.Context, id int64) (*Todo, error) { row := r.db.QueryRowContext(ctx, "SELECT id, title, done FROM todos WHERE id = ?", id) var t Todo if err := row.Scan(&t.ID, &t.Title, &t.Done); err != nil { return nil, err } return &t, nil } // Update / Delete は同様に ExecContext を利用 |
テーブル駆動テストの例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func TestCreateTodo(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("failed to open mock db: %v", err) } defer db.Close() mock.ExpectExec(`INSERT INTO todos`). WithArgs("Buy milk", false). WillReturnResult(sqlmock.NewResult(1, 1)) repo := NewRepository(db) id, err := repo.Create(context.Background(), Todo{Title: "Buy milk"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if id != 1 { t.Errorf("expected ID=1, got %d", id) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("unfulfilled expectations: %s", err) } } |
品質向上と CI/CD:Lint・Docker・GitHub Actions・テスト
15️⃣ golangci-lint の推奨設定 (.golangci.yml)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
run: timeout: 5m linters: enable: - errcheck # エラーハンドリング漏れ検出 - govet # 静的解析 - staticcheck # 高度なコード診断 - bodyclose # http.Response.Body のクローズ忘れ防止 - gofmt # フォーマット違反 - goimports # import 整理 issues: exclude-use-default: false |
- ローカルで
golangci-lint run ./...が成功すれば、同コマンドを CI の最初のステップ に配置するだけでコード品質が保証される。
16️⃣ Dockerfile(マルチステージビルド)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# ---------- Builder ---------- FROM golang:1.20-alpine AS builder WORKDIR /src # モジュールキャッシュを有効化 COPY go.mod go.sum ./ RUN go mod download # ソースコード全体をコピーし、CGO を無効にしてビルド COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ go build -ldflags "-s -w" -o /app/server ./cmd/server # ---------- Runtime ---------- FROM alpine:3.18 AS runtime RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /app/server . EXPOSE 8080 USER 10001:10001 # 非 root ユーザーで実行(後述の USER 定義は任意) ENTRYPOINT ["./server"] |
- サイズ:
strip -s -wによりバイナリが ~10 MB、最終イメージは 30 MB 程度に収まる。
17️⃣ 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 27 28 |
version: "3.9" services: api: build: . ports: - "8080:8080" environment: DB_USER: root DB_PASS: password DB_HOST: db DB_NAME: todo depends_on: - db db: image: mysql:8.0 command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: todo ports: - "3306:3306" volumes: - db_data:/var/lib/mysql volumes: db_data: |
depends_onにより DB が起動してから API コンテナが開始される。
18️⃣ 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
name: CI on: push: branches: [main] pull_request: jobs: build-test: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: todo ports: ["3306:3306"] options: >- --health-cmd="mysqladmin ping -h127.0.0.1 --silent" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Go 1.20 uses: actions/setup-go@v5 with: go-version: '1.20' - name: Cache Go modules uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - name: Install golangci-lint run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - name: Run linters run: golangci-lint run ./... - name: Run unit tests env: DB_USER: root DB_PASS: password DB_HOST: 127.0.0.1 DB_NAME: todo run: go test -v ./... - name: Build Docker image run: | docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} . - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push Docker image run: | docker push ghcr.io/${{ github.repository }}:${{ github.sha }} |
- テストはローカルの MySQL コンテナに対して実行し、
DB_*環境変数で接続情報を渡す。
19️⃣ テーブル駆動テスト(ユニットテスト例)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func TestValidateCreate(t *testing.T) { cases := []struct { name string req CreateReq want error }{ {"valid", CreateReq{Title: "Buy milk"}, nil}, {"empty title", CreateReq{Title: ""}, validator.ValidationErrors{}}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { err := validateCreate(c.req) if (err == nil) != (c.want == nil) { t.Fatalf("expected %v, got %v", c.want, err) } }) } } |
validator.ValidationErrorsはエラーハンドリング側で ユーザー向けメッセージへ変換して返すと UI が親切になる。
本番デプロイとセキュリティベストプラクティス
20️⃣ 環境変数・シークレット管理
| 場所 | 方法 | メリット |
|---|---|---|
| GitHub Actions | secrets.* → env: で注入 |
リポジトリ外部に保存され、暗号化された状態で利用できる。 |
| Kubernetes / ECS | Secret オブジェクト + IAM ロール | 実行時にだけメモリ上へ展開し、イメージやログに残らない。 |
| Docker Compose (ローカル) | .env ファイル(.gitignore で除外) |
開発者間の共有が簡単。 |
ベストプラクティス:コード中にハードコーディングされたパスワードやキーは一切書かない。
21️⃣ TLS 終端とヘルスチェック
- TLSはリバースプロキシ(NGINX, Traefik)で終端し、アプリは
http://のみで動作させる。証明書は Let's Encrypt + Cert‑Manager で自動更新。 /healthzエンドポイントは Liveness Probe と Readiness Probe に共通利用できるように実装。
|
1 2 3 4 5 6 7 8 |
func healthHandler(w http.ResponseWriter, r *http.Request) { if err := db.PingContext(r.Context()); err != nil { w.WriteHeader(http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) } |
22️⃣ データベースユーザーの最小権限化
|
1 2 3 4 |
CREATE USER 'todo_app'@'%' IDENTIFIED BY 'StrongPass!23'; GRANT SELECT, INSERT, UPDATE, DELETE ON todo.* TO 'todo_app'@'%'; FLUSH PRIVILEGES; |
DROP,CREATE,ALTERなどの権限は付与しない。- 必要に応じて ロールベースアクセス制御 (RBAC) を導入し、運用時に権限変更を容易にする。
23️⃣ 本番デプロイ時のチェックリスト
| 項目 | 推奨設定 |
|---|---|
| コンテナイメージ | マルチステージビルド + CGO_ENABLED=0 → サイズ ≤ 30 MB |
| 実行ユーザー | USER 10001:10001(root 以外) |
| 環境変数注入 | GitHub Secrets → Kubernetes Secret → envFrom |
| ログ出力 | JSON 形式で標準出力へ。Fluent Bit / Fluentd が集約し、CloudWatch/Stackdriver に転送 |
| メトリクス | prometheus/client_golang を組み込み /metrics エンドポイント公開 |
| オートスケーリング | CPU 使用率 70 % 超過で水平ポッド自動増加(K8s HPA) |
🎉 まとめ
- Go 1.20 のインストールは公式バイナリか Homebrew/Chocolatey を使い、
PATHに追加すれば完了。 - プロジェクト構成は
cmd / internal / pkgの三層に分け、モジュール管理はgo.modとgo.sumで一元化。 - API 実装は chi をベースにし、JSON バインディングと
validator/v10による入力チェックを徹底。 - MySQL 接続は
parseTime=true&loc=Localを付与した DSN で安全にtime.Timeを扱う。 - CI/CDは
golangci-lint, Docker マルチステージ, GitHub Actions のフローを組み合わせ、テスト・ビルド・イメージプッシュまで自動化。 - 本番デプロイではシークレット管理、TLS 終端、最小権限 DB ユーザー、ヘルスチェック/メトリクス公開を必ず実装。
これらの手順とベストプラクティスに従うだけで、Go 1.20 と最新ツールチェーンを活用した本番レベルの ToDo API が安全・高速・保守性高く構築できます。 🚀