プロジェクトデプロイと最適化

レッスン28:プロジェクトデプロイと最適化

現実世界のアナロジー

レストランを開いたと想像してください。レシピは完成しました(コードはできましたが)、実際に営業を開始するには以下が必要です:

コアコンセプト

コンセプト 説明
クロスコンパイル あるプラットフォーム上で別のプラットフォーム用の実行ファイルをコンパイルする
マルチステージビルド Dockerビルドでコンパイル環境とランタイム環境を分離し、イメージサイズを削減
環境変数 コードを変更せずにプログラムの動作を設定する
構造化ログ log/slogを使用して機械的に解析可能なログ形式を出力する
パフォーマンスプロファイリング pprofを使用してCPUとメモリのボトルネックを特定する
グレースフルシャットダウン 終了シグナル受信後、進行中のリクエストが完了するまで待ってから終了する

基本構文と使い方

1. クロスコンパイル

Goはネイティブにクロスコンパイルをサポートしており、GOOSGOARCH環境変数を設定するだけです:

BASH
# Linux amd64バージョンをコンパイル(Windows/Macで実行)
GOOS=linux GOARCH=amd64 go build -o myapp-linux .

# Windowsバージョンをコンパイル(Linux/Macで実行)
GOOS=windows GOARCH=amd64 go build -o myapp.exe .

# macOS ARM (M1/M2) バージョンをコンパイル
GOOS=darwin GOARCH=arm64 go build -o myapp-darwin .
💡 ヒント: クロスコンパイル中、CGOはデフォルトで無効になっています。Cライブラリに依存している場合は、クロスコンパイルツールチェーンをインストールする必要があります。

2. Dockerマルチステージビルド

DOCKERFILE
# ---- ビルドステージ ----
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 go build -o server .

# ---- ランタイムステージ ----
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
💡 ヒント: 最終イメージはコンパイルされたバイナリと最小限のランタイム環境のみを含むため、サイズが1GBから20MB未満に削減されます。

3. 環境変数設定

GO
package main

import (
	"fmt"
	"os"
	"strconv"
)

func main() {
	// デフォルト値付きで環境変数を読み取る
	port := getEnv("APP_PORT", "8080")
	dbHost := getEnv("DB_HOST", "localhost")
	debug := getEnv("DEBUG", "false")

	fmt.Printf("起動設定: port=%s, db=%s, debug=%s\n", port, dbHost, debug)
}

// getEnvは環境変数を取得し、設定されていない場合はデフォルト値を返す
func getEnv(key, defaultVal string) string {
	if val := os.Getenv(key); val != "" {
		return val
	}
	return defaultVal
}
💡 ヒント: 本番環境では、godotenvライブラリと.envファイルの使用を推奨しますが、.envファイルをバージョン管理にコミットしないでください。

4. 構造化ログ(log/slog)

Go 1.21で標準ライブラリlog/slogが導入され、JSON形式の出力をサポートしました:

GO
package main

import (
	"log/slog"
	"os"
)

func main() {
	// JSON形式のハンドラを作成
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	// 構造化ログを使用
	logger.Info("サービスが起動しました",
		"port", 8080,
		"env", "production",
	)

	logger.Error("データベース接続に失敗しました",
		"host", "db.example.com",
		"error", "connection refused",
	)
}
💡 ヒント: slogslog.SetDefault()でグローバルなデフォルトロガーを設定でき、プロジェクト全体で統一的に使用できます。

5. パフォーマンスプロファイリング(pprof)

GO
package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof" // インポートでpprofルートを登録
	"time"
)

func main() {
	// 別のポートでpprofサービスを起動
	go func() {
		fmt.Println("pprofが :6060 でリッスン中")
		http.ListenAndServe(":6060", nil)
	}()

	// メインのビジネスロジック
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(10 * time.Millisecond)
		fmt.Fprintln(w, "Hello, World!")
	})

	http.ListenAndServe(":8080", nil)
}
💡 ヒント: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30で30秒間のCPUデータを収集できます。

6. グレースフルシャットダウン

GO
package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(2 * time.Second) // 長時間リクエストをシミュレート
		fmt.Fprintln(w, "処理完了")
	})

	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	// goroutineでサーバーを起動
	go func() {
		fmt.Println("サーバーが :8080 でリッスン中")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			fmt.Printf("サーバーが異常終了しました: %v\n", err)
		}
	}()

	// 割り込みシグナルを待機
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	fmt.Println("\nシャットダウンシグナルを受信しました。グレースフルに終了します...")

	// 進行中のリクエストに最大10秒の猶予を与える
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		fmt.Printf("強制シャットダウン: %v\n", err)
	} else {
		fmt.Println("サーバーが正常にシャットダウンしました")
	}
}
💡 ヒント: server.Shutdown()は新しい接続の受け入れを停止し、既存の接続が処理を完了するまで待機してから戻ります。


例:基本的なクロスコンパイルスクリプト(難易度⭐)

複数プラットフォーム用のバイナリを一括コンパイルするスクリプトを作成します:

GO
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// 対象プラットフォームリスト
type Target struct {
	OS   string
	Arch string
}

func main() {
	targets := []Target{
		{"linux", "amd64"},
		{"linux", "arm64"},
		{"darwin", "amd64"},
		{"darwin", "arm64"},
		{"windows", "amd64"},
	}

	outputDir := "dist"
	os.MkdirAll(outputDir, 0755)

	for _, t := range targets {
		outputName := fmt.Sprintf("myapp-%s-%s", t.OS, t.Arch)
		if t.OS == "windows" {
			outputName += ".exe"
		}

		outputPath := filepath.Join(outputDir, outputName)
		fmt.Printf("コンパイル中: %s/%s -> %s\n", t.OS, t.Arch, outputPath)

		cmd := exec.Command("go", "build", "-o", outputPath, ".")
		cmd.Env = append(os.Environ(),
			"GOOS="+t.OS,
			"GOARCH="+t.Arch,
			"CGO_ENABLED=0",
		)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr

		if err := cmd.Run(); err != nil {
			fmt.Printf("コンパイル失敗 %s/%s: %v\n", t.OS, t.Arch, err)
		}
	}

	fmt.Println("すべてのコンパイルが完了しました!")
}
▶ 試してみよう

実行:

BASH
go run build.go

出力ディレクトリ構造:

TEXT
dist/
├── myapp-darwin-amd64
├── myapp-darwin-arm64
├── myapp-linux-amd64
├── myapp-linux-arm64
└── myapp-windows-amd64.exe

例:環境変数設定付きHTTPサービス(難易度⭐⭐)

完全な環境変数設定スキームを実演します:

GO
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"
)

// Configはアプリケーション設定構造体
type Config struct {
	Port        int    `json:"port"`
	Env         string `json:"env"`
	LogLevel    string `json:"log_level"`
	DBHost      string `json:"db_host"`
	DBPort      int    `json:"db_port"`
	IdleTimeout int    `json:"idle_timeout"` // 秒
}

// LoadConfigは環境変数から設定を読み込む
func LoadConfig() Config {
	return Config{
		Port:        getEnvInt("APP_PORT", 8080),
		Env:         getEnvStr("APP_ENV", "development"),
		LogLevel:    getEnvStr("LOG_LEVEL", "info"),
		DBHost:      getEnvStr("DB_HOST", "localhost"),
		DBPort:      getEnvInt("DB_PORT", 5432),
		IdleTimeout: getEnvInt("IDLE_TIMEOUT", 30),
	}
}

func getEnvStr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func getEnvInt(key string, fallback int) int {
	if v := os.Getenv(key); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			return n
		}
	}
	return fallback
}

func (c Config) String() string {
	data, _ := json.MarshalIndent(c, "", "  ")
	return string(data)
}

func main() {
	cfg := LoadConfig()

	// 環境に基づいてログレベルを設定
	var level slog.Level
	switch cfg.LogLevel {
	case "debug":
		level = slog.LevelDebug
	case "warn":
		level = slog.LevelWarn
	case "error":
		level = slog.LevelError
	default:
		level = slog.LevelInfo
	}

	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: level,
	}))
	slog.SetDefault(logger)

	logger.Info("アプリケーション設定を読み込みました", "config", cfg.String())

	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"status": "ok",
			"env":    cfg.Env,
		})
	})

	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		logger.Info("リクエストを受信", "method", r.Method, "path", r.URL.Path)
		fmt.Fprintf(w, "%s 環境からこんにちは!", cfg.Env)
	})

	server := &http.Server{
		Addr:        fmt.Sprintf(":%d", cfg.Port),
		Handler:     mux,
		IdleTimeout: time.Duration(cfg.IdleTimeout) * time.Second,
	}

	go func() {
		logger.Info("サービスが起動しました", "port", cfg.Port, "env", cfg.Env)
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			logger.Error("サービスエラー", "error", err)
			os.Exit(1)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	logger.Info("サービスをシャットダウン中...")
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	server.Shutdown(ctx)
	logger.Info("サービスがシャットダウンしました")
}
▶ 試してみよう

起動:

BASH
# デフォルト設定を使用
go run main.go

# カスタム設定
APP_PORT=3000 APP_ENV=production LOG_LEVEL=debug go run main.go

リクエストテスト:

BASH
# ヘルスチェック
curl http://localhost:8080/health

# トップページ
curl http://localhost:8080/

例:完全なDockerデプロイソリューション(難易度⭐⭐⭐)

Dockerfile、docker-compose、Makefileを含む完全なデプロイソリューションです:

プロジェクト構造:

TEXT
myproject/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── Makefile
└── .env.example
▶ 試してみよう

main.go:

GO
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	_ "net/http/pprof"
	"os"
	"os/signal"
	"runtime"
	"syscall"
	"time"
)

type AppConfig struct {
	Port      string
	Env       string
	LogLevel  string
	PprofPort string
}

func loadConfig() AppConfig {
	return AppConfig{
		Port:      getEnv("APP_PORT", "8080"),
		Env:       getEnv("APP_ENV", "production"),
		LogLevel:  getEnv("LOG_LEVEL", "info"),
		PprofPort: getEnv("PPROF_PORT", "6060"),
	}
}

func getEnv(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

func setupLogger(level string) *slog.Logger {
	var lvl slog.Level
	switch level {
	case "debug":
		lvl = slog.LevelDebug
	case "warn":
		lvl = slog.LevelWarn
	case "error":
		lvl = slog.LevelError
	default:
		lvl = slog.LevelInfo
	}
	return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}))
}

func main() {
	cfg := loadConfig()
	logger := setupLogger(cfg.LogLevel)
	slog.SetDefault(logger)

	// pprofを起動(非本番環境または明示的に有効な場合)
	if cfg.Env != "production" || os.Getenv("ENABLE_PPROF") == "true" {
		go func() {
			logger.Info("pprofサービスが起動しました", "port", cfg.PprofPort)
			http.ListenAndServe(":"+cfg.PprofPort, nil)
		}()
	}

	mux := http.NewServeMux()

	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{
			"status":    "ok",
			"env":       cfg.Env,
			"goroutine": runtime.NumGoroutine(),
			"uptime":    time.Since(startTime).String(),
		})
	})

	mux.HandleFunc("GET /api/info", func(w http.ResponseWriter, r *http.Request) {
		logger.Info("infoリクエスト", "remote", r.RemoteAddr)
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"app":      "myproject",
			"version":  version,
			"go":       runtime.Version(),
			"env":      cfg.Env,
		})
	})

	mux.HandleFunc("POST /api/data", func(w http.ResponseWriter, r *http.Request) {
		// データ処理をシミュレート
		time.Sleep(100 * time.Millisecond)
		logger.Info("データ処理完了", "content_length", r.ContentLength)
		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(map[string]string{"result": "created"})
	})

	server := &http.Server{
		Addr:         ":" + cfg.Port,
		Handler:      mux,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	// グレースフルシャットダウンロジック
	done := make(chan struct{})
	go func() {
		quit := make(chan os.Signal, 1)
		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
		sig := <-quit
		logger.Info("シャットダウンシグナルを受信", "signal", sig)

		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		// pprofを停止
		// 注意:標準ライブラリのpprofにはShutdownがなく、プロセス終了に依存

		if err := server.Shutdown(ctx); err != nil {
			logger.Error("サーバーシャットダウンエラー", "error", err)
		}
		close(done)
	}()

	logger.Info("サービスが起動しました",
		"port", cfg.Port,
		"env", cfg.Env,
		"version", version,
	)

	if err := server.ListenAndServe(); err != http.ErrServerClosed {
		logger.Error("サーバーが異常終了しました", "error", err)
		os.Exit(1)
	}

	<-done
	logger.Info("サービスが完全にシャットダウンしました")
}

var (
	startTime = time.Now()
	version   = "1.0.0"
)

Dockerfile:

DOCKERFILE
# ========== ビルドステージ ==========
FROM golang:1.22-alpine AS builder

# gitをインストール(go modで必要になる場合がある)
RUN apk add --no-cache git

WORKDIR /build

# 依存関係ファイルを先にコピーし、Dockerキャッシュレイヤーを活用
COPY go.mod go.sum ./
RUN go mod download

# ソースをコピーしてコンパイル
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o /app/server .

# ========== ランタイムステージ ==========
FROM alpine:3.19

# CA証明書(HTTPSリクエスト用)とタイムゾーンデータをインストール
RUN apk --no-cache add ca-certificates tzdata

# ルート以外のユーザーを作成
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# ビルドステージからバイナリをコピー
COPY --from=builder /app/server .
COPY --from=builder /build/.env.example .env.example

# ルート以外のユーザーとして実行
USER appuser

EXPOSE 8080

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget -qO- http://localhost:8080/health || exit 1

CMD ["./server"]

docker-compose.yml:

YAML
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_ENV=production
      - LOG_LEVEL=info
      - APP_PORT=8080
      - ENABLE_PPROF=false
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.5'
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Makefile:

MAKEFILE
APP_NAME := myproject
VERSION  := 1.0.0

.PHONY: build run clean docker docker-run docker-stop

# ローカルビルド
build:
	CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME) .

# ローカル実行
run: build
	./bin/$(APP_NAME)

# クリーン
clean:
	rm -rf bin/

# Dockerイメージをビルド
docker:
	docker build -t $(APP_NAME):$(VERSION) .

# Dockerコンテナを起動
docker-run:
	docker-compose up -d

# Dockerコンテナを停止
docker-stop:
	docker-compose down

# すべてのプラットフォームにクロスコンパイル
cross-build:
	GOOS=linux   GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-linux-amd64 .
	GOOS=darwin  GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-darwin-amd64 .
	GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-windows-amd64.exe .

.env.example:

TEXT
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
ENABLE_PPROF=false
PPROF_PORT=6060

ビルドとデプロイのワークフロー:

BASH
# 1. ローカルテスト
make run

# 2. Dockerイメージをビルド
make docker

# 3. コンテナを起動
make docker-run

# 4. サービスをテスト
curl http://localhost:8080/health
curl http://localhost:8080/api/info

# 5. ログを確認
docker-compose logs -f

# 6. サービスを停止
make docker-stop

シナリオ1:本番環境のログとモニタリング

実際のWebサービスでは、ログを集中ログシステムに収集する必要があります:

GO
package main

import (
	"context"
	"log/slog"
	"net/http"
	"os"
	"time"
)

// RequestLoggerミドルウェア:各リクエストの詳細情報を記録
func RequestLogger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// ResponseWriterをラップしてステータスコードをキャプチャ
		rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
		next.ServeHTTP(rw, r)

		slog.Info("HTTPリクエスト",
			"method", r.Method,
			"path", r.URL.Path,
			"status", rw.statusCode,
			"duration_ms", time.Since(start).Milliseconds(),
			"remote_addr", r.RemoteAddr,
			"user_agent", r.UserAgent(),
		)
	})
}

type responseWriter struct {
	http.ResponseWriter
	statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
	rw.statusCode = code
	rw.ResponseWriter.WriteHeader(code)
}

// RecoveryMiddlewareはパニックをキャッチしてログに記録する
func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				slog.Error("リクエスト処理パニック",
					"error", err,
					"path", r.URL.Path,
					"method", r.Method,
				)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	// 本番環境ではJSON形式を使用し、ログ収集システムでの解析を容易にする
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))
	slog.SetDefault(logger)

	mux := http.NewServeMux()
	mux.HandleFunc("GET /api/users", func(w http.ResponseWriter, r *http.Request) {
		// ビジネスロジックをシミュレート
		ctx := r.Context()
		slog.DebugContext(ctx, "ユーザーリストをクエリ中", "page", 1)
		// ...
	})

	// ミドルウェアチェーン:Recovery -> Logger -> Router
	handler := RecoveryMiddleware(RequestLogger(mux))

	server := &http.Server{Addr: ":8080", Handler: handler}

	slog.Info("サービスが起動しました", "addr", server.Addr)
	server.ListenAndServe()
}

実行後、各リクエストは構造化ログを出力します:

JSON
{"time":"2024-01-15T10:30:00Z","level":"INFO","msg":"HTTPリクエスト","method":"GET","path":"/api/users","status":200,"duration_ms":5,"remote_addr":"127.0.0.1","user_agent":"curl/7.68.0"}

シナリオ2:pprofを使用したパフォーマンスボトルネックの特定

サービスのレスポンスが遅くなった場合、pprofを使用して診断します:

GO
package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"strings"
	"time"
)

func main() {
	// メインサービス
	mux := http.NewServeMux()

	// パフォーマンスに問題のあるエンドポイントをシミュレート
	mux.HandleFunc("GET /api/search", func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query().Get("q")
		result := slowSearch(query) // 意図的に遅い関数
		fmt.Fprint(w, result)
	})

	// pprofはデフォルトのhttp.DefaultServeMuxに登録され、別ポートが必要
	go func() {
		fmt.Println("pprof: http://localhost:6060/debug/pprof/")
		http.ListenAndServe(":6060", nil)
	}()

	fmt.Println("メインサービス: http://localhost:8080")
	http.ListenAndServe(":8080", mux)
}

// slowSearchはメモリ割り当ての問題があるCPU集約的な関数をシミュレート
func slowSearch(query string) string {
	var results []string
	// 意図的にループ内でメモリを繰り返し割り当て
	for i := 0; i < 100000; i++ {
		data := fmt.Sprintf("item_%d_%s", i, query) // 各反復で新しい文字列を割り当て
		if strings.Contains(data, query) {
			results = append(results, data)
		}
	}
	// 追加の遅延をシミュレート
	time.Sleep(50 * time.Millisecond)
	return fmt.Sprintf("%d 件の結果が見つかりました", len(results))
}

pprofを使用した分析:

BASH
# 1. pprofツールをインストール(未インストールの場合)
go install github.com/google/pprof@latest

# 2. 30秒間のCPUプロファイルを収集
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# 3. pprofのインタラクティブインターフェースで表示
(pprof) top          # CPU集約的な関数を表示
(pprof) list slowSearch  # 特定のコード行のコストを表示
(pprof) web          # 可視化を生成(graphvizが必要)

# 4. メモリ割り当てを確認
go tool pprof http://localhost:6060/debug/pprof/heap

# 5. goroutine数を確認(リーク検出)
go tool pprof http://localhost:6060/debug/pprof/goroutine

# 6. ブラウザですべての指標を確認
# http://localhost:6060/debug/pprof/ を開く

❓ よくある質問

質問1:クロスコンパイル中にcgo: exec gcc: exec: "gcc": not foundが発生した場合は?

コードがCGOを使用しているためです。Cライブラリの依存関係が必要ない場合は、CGO_ENABLED=0を設定してください:

BASH
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp .

CGOが必要な場合は、対象プラットフォーム用のクロスコンパイルツールチェーン(gcc-aarch64-linux-gnuなど)をインストールする必要があります。

賡問2:Dockerイメージが大きすぎる場合は?

scratchをベースイメージとして使用してください(システムツールが一切ない状態):

DOCKERFILE
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
CMD ["/server"]

コンパイル時のデバッグ情報の削除と組み合わせて:go build -ldflags="-s -w"-sはシンボルテーブルを削除し、-wはDWARFデバッグ情報を削除し、通常サイズを20-30%削減します。

質問3:log/slogとサードパーティログライブラリ(zap、zerologなど)の比較は?

log/slogの利点は標準ライブラリであり、追加の依存関係がないこと、そしてほとんどのシナリオで十分なパフォーマンスを持っていることです。アプリケーションが非常に大量のログ(毎秒数万件)を生成する場合、zapのゼロ割り当て設計がより高速になります。新しいプロジェクトではslogから始め、パフォーマンスボトルネックに遭遇した場合にのみ切り替えを検討してください。

質問4:グレースフルシャットダウン中にWebSocket接続を正しく処理するには?

http.Server.Shutdown()はHTTPリクエストのみを処理します。長寿命の接続(WebSocket)については、接続プールを自分で管理する必要があります:

GO
var connections sync.Map // アクティブな接続を格納

// 新しい接続が到着したときに登録
connections.Store(conn, true)

// シャットダウン時にすべての接続を反復処理し、クローズフレームを送信
connections.Range(func(key, value any) bool {
    wsConn := key.(*websocket.Conn)
    wsConn.WriteMessage(websocket.CloseMessage, closeMsg)
    wsConn.Close()
    return true
})

📖 まとめ

このレッスンではGoプロジェクトの開発からデプロイまでの完全なワークフローを学びました:

これらのスキルをマスターすれば、Goプロジェクトを本番環境に安全かつ効率的にデプロイできます。


📝 演習

演習1:クロスコンパイルスクリプトを作成

現在のプロジェクトをlinux/amd64darwin/arm64windows/amd64プラットフォーム用に自動コンパイルするGoプログラムを作成してください。出力ファイル名にはバージョンとプラットフォーム情報を含めてください。

演習2:リクエストログミドルウェアを追加

HTTPサービスにログミドルウェアを追加して、各リクエストの以下を記録してください:メソッド、パス、ステータスコード、レスポンスタイム、クライアントIP。log/slogでJSON形式の出力を行ってください。

演習3:プロジェクトをDockerデプロイ

以前のレッスンで作成したプロジェクト(Todo APIなど)に対して、完全なDockerfileとdocker-compose.ymlを作成してください。以下の要件:


次のレッスン

このレッスンを完了すると、Goプロジェクトのデプロイに関するコアスキルを習得しました。次は実践プロジェクトフェーズに入ります:レッスン29:実践プロジェクト(パート1)

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%