プロジェクトデプロイと最適化
レッスン28:プロジェクトデプロイと最適化
現実世界のアナロジー
レストランを開いたと想像してください。レシピは完成しました(コードはできましたが)、実際に営業を開始するには以下が必要です:
- 立地と内装 — クロスコンパイルにより、同じレシピが異なる都市で動作します(異なるプラットフォームで実行)
- キッチン設計 — Dockerマルチステージビルドは、調理器具だけをキッチンに持ち込み、余分な家具は持ち込まないようなものです
- 従業員マニュアル — 環境変数設定は、異なる支店が異なる営業時間を使うが、マニュアルの形式は同じようなものです
- 防犯カメラ — ログ管理は、問題が発生した際にトレースできるよう、すべての料理の調理過程を記録します
- 健康診断レポート — パフォーマンスプロファイリングは定期健診のようなもので、どの部分が遅いかを発見します
- 閉店手順 — グレースフルシャットダウンは、閉店時にまだ食べているお客様を追い出さず、食べ終わってから閉店するようなものです
コアコンセプト
| コンセプト | 説明 |
|---|---|
| クロスコンパイル | あるプラットフォーム上で別のプラットフォーム用の実行ファイルをコンパイルする |
| マルチステージビルド | Dockerビルドでコンパイル環境とランタイム環境を分離し、イメージサイズを削減 |
| 環境変数 | コードを変更せずにプログラムの動作を設定する |
| 構造化ログ | log/slogを使用して機械的に解析可能なログ形式を出力する |
| パフォーマンスプロファイリング | pprofを使用してCPUとメモリのボトルネックを特定する |
| グレースフルシャットダウン | 終了シグナル受信後、進行中のリクエストが完了するまで待ってから終了する |
基本構文と使い方
1. クロスコンパイル
Goはネイティブにクロスコンパイルをサポートしており、GOOSとGOARCH環境変数を設定するだけです:
# 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マルチステージビルド
# ---- ビルドステージ ----
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"]
3. 環境変数設定
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形式の出力をサポートしました:
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",
)
}
slogはslog.SetDefault()でグローバルなデフォルトロガーを設定でき、プロジェクト全体で統一的に使用できます。
5. パフォーマンスプロファイリング(pprof)
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. グレースフルシャットダウン
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()は新しい接続の受け入れを停止し、既存の接続が処理を完了するまで待機してから戻ります。
例:基本的なクロスコンパイルスクリプト(難易度⭐)
複数プラットフォーム用のバイナリを一括コンパイルするスクリプトを作成します:
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("すべてのコンパイルが完了しました!")
}
実行:
go run build.go
出力ディレクトリ構造:
dist/
├── myapp-darwin-amd64
├── myapp-darwin-arm64
├── myapp-linux-amd64
├── myapp-linux-arm64
└── myapp-windows-amd64.exe
例:環境変数設定付きHTTPサービス(難易度⭐⭐)
完全な環境変数設定スキームを実演します:
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("サービスがシャットダウンしました")
}
起動:
# デフォルト設定を使用
go run main.go
# カスタム設定
APP_PORT=3000 APP_ENV=production LOG_LEVEL=debug go run main.go
リクエストテスト:
# ヘルスチェック
curl http://localhost:8080/health
# トップページ
curl http://localhost:8080/
例:完全なDockerデプロイソリューション(難易度⭐⭐⭐)
Dockerfile、docker-compose、Makefileを含む完全なデプロイソリューションです:
プロジェクト構造:
myproject/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── Makefile
└── .env.example
main.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:
# ========== ビルドステージ ==========
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:
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:
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:
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
ENABLE_PPROF=false
PPROF_PORT=6060
ビルドとデプロイのワークフロー:
# 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サービスでは、ログを集中ログシステムに収集する必要があります:
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()
}
実行後、各リクエストは構造化ログを出力します:
{"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を使用して診断します:
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を使用した分析:
# 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を設定してください:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp .
CGOが必要な場合は、対象プラットフォーム用のクロスコンパイルツールチェーン(gcc-aarch64-linux-gnuなど)をインストールする必要があります。
賡問2:Dockerイメージが大きすぎる場合は?
scratchをベースイメージとして使用してください(システムツールが一切ない状態):
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)については、接続プールを自分で管理する必要があります:
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はネイティブにこれをサポートしており、
GOOSとGOARCHを設定するだけです - Dockerマルチステージビルドはコンパイル環境とランタイム環境を分離し、イメージサイズを大幅に削減しつつセキュリティを向上させます
- 環境変数設定は12-Factor Appのベストプラクティスで、同じコードが異なる環境に適応できるようにします
log/slogは標準ライブラリレベルの構造化ログを提供し、JSON形式でログ収集システムの解析を容易にしますpprofはGo組み込みのパフォーマンス分析ツールで、CPU、メモリ、goroutineなどの問題を特定できます- グレースフルシャットダウンは、終了シグナル受信後、サービスが進行中のリクエストの完了を待ってから終了し、データの損失を回避します
これらのスキルをマスターすれば、Goプロジェクトを本番環境に安全かつ効率的にデプロイできます。
📝 演習
演習1:クロスコンパイルスクリプトを作成
現在のプロジェクトをlinux/amd64、darwin/arm64、windows/amd64プラットフォーム用に自動コンパイルするGoプログラムを作成してください。出力ファイル名にはバージョンとプラットフォーム情報を含めてください。
演習2:リクエストログミドルウェアを追加
HTTPサービスにログミドルウェアを追加して、各リクエストの以下を記録してください:メソッド、パス、ステータスコード、レスポンスタイム、クライアントIP。log/slogでJSON形式の出力を行ってください。
演習3:プロジェクトをDockerデプロイ
以前のレッスンで作成したプロジェクト(Todo APIなど)に対して、完全なDockerfileとdocker-compose.ymlを作成してください。以下の要件:
- マルチステージビルドを使用
- 最終イメージは
alpineまたはscratchをベース - ヘルスチェックを含む
- ルート以外のユーザーとして実行
次のレッスン
このレッスンを完了すると、Goプロジェクトのデプロイに関するコアスキルを習得しました。次は実践プロジェクトフェーズに入ります:レッスン29:実践プロジェクト(パート1)



