mcpField
G
golang-best-practices

golang-best-practices

github.com/difyz9開発294 回インストール38 週間前に更新
リポジトリ

golang-best-practices について

Go のコードを作成、レビュー、リファクタリングする開発者向けの実践ガイドです。プロジェクト構成、エラー処理、並行処理、テスト、性能、Uber FX による依存性注入を扱います。

主な機能

  1. 01Go のプロジェクト構成指針
  2. 02エラーの確認とラップ
  3. 03並行処理とチャネルのパターン
  4. 04インターフェースとレイヤー設計
  5. 05テストと性能の指針
  6. 06Uber FX による依存性注入

活用シーン

  1. 01Go プロジェクトの構成を計画
  2. 02エラー処理と並行コードをレビュー
  3. 03依存関係を注入したコードをテスト
  4. 04FX で依存関係とライフサイクルを管理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/difyz9/golang-best-practices --skill golang-best-practices

インストール後、Claude Code で/golang-best-practicesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.gitignore
.DS_Store
fx-advanced-guide.md
LICENSE
CONTRIBUTING.md
fx-simplified-architecture.md
CHANGELOG.md
fx-basic-integration.md
README.md
references/
fx-architecture.md
patterns.md
tools.md
stdlib.md

詳細

Go のベストプラクティス

概要

このスキルでは、プロジェクト構成、エラー処理、並行処理パターン、テスト戦略、パフォーマンス最適化、コード品質基準を網羅した、実績のある Go 開発のベストプラクティスを紹介します。Go コードの作成、レビュー、リファクタリングの際に、これらのガイドラインを活用してください。

プロジェクト構成

標準レイアウト

整理された実用的な Go プロジェクト構成に従います。

project/
├── cmd/
│   └── server/
│       └── main.go           # Application entry point
├── internal/
│   ├── model/                # Domain models/entities
│   │   ├── user.go
│   │   └── product.go
│   ├── handler/              # HTTP handlers
│   │   ├── user_handler.go
│   │   ├── product_handler.go
│   │   └── middleware.go
│   ├── service/              # Business logic & data access
│   │   ├── user_service.go
│   │   └── product_service.go
│   └── config/               # Configuration
│       └── config.go
├── pkg/                      # Reusable utilities
│   ├── logger/
│   ├── validator/
│   └── utils/
├── migrations/               # Database migrations
│   ├── 001_create_users.up.sql
│   └── 001_create_users.down.sql
├── configs/                  # Config files
│   ├── config.yaml
│   └── config.local.yaml
├── scripts/                  # Helper scripts
│   └── build.sh
├── docs/                     # Documentation
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── Makefile
├── .env.example
├── .gitignore
└── README.md

ディレクトリの説明:

  • cmd/server - アプリケーションのエントリーポイント。main.go は最小限にします
  • internal/model - ドメインエンティティとビジネスモデル
  • internal/handler - HTTP リクエストハンドラーとミドルウェア
  • internal/service - ビジネスロジックとデータベース操作
  • internal/config - アプリケーション設定の読み込み
  • pkg/ - 独立したパッケージとして切り出せる共有ユーティリティ
  • migrations/ - データベーススキーマの SQL マイグレーションファイル
  • configs/ - 環境ごとの YAML/JSON 設定ファイル
主な原則
  • 外部プロジェクトからインポートさせないコードには internal/ を使用します
  • 再利用可能なライブラリコードには pkg/ を使用します
  • cmd/ は初期化と配線だけにして、最小限に保ちます
  • 1 パッケージにつき 1 つの責務を持たせます

エラー処理

エラーを必ず確認する
// ❌ BAD
data, _ := os.ReadFile("file.txt")

// ✅ GOOD
data, err := os.ReadFile("file.txt")
if err != nil {
    return fmt.Errorf("read file: %w", err)
}
エラーのラップ

エラーチェーンを調べられるように、エラーのラップには %w を使用します。

if err != nil {
    return fmt.Errorf("failed to process user %s: %w", userID, err)
}
カスタムエラー

センチネルエラーとカスタムエラー型を作成します。

var ErrNotFound = errors.New("resource not found")

type ValidationError struct {
    Field string
    Err   error
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %v", e.Field, e.Err)
}
エラーの確認
// Check for specific errors
if errors.Is(err, ErrNotFound) {
    // handle not found
}

// Check error type
var valErr *ValidationError
if errors.As(err, &valErr) {
    // handle validation error
}

並行処理

Goroutine のベストプラクティス

Goroutine のライフサイクルを必ず管理します:

// ✅ Use context for cancellation
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // do work
        }
    }
}

// ✅ Use WaitGroup for synchronization
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        // work
    }(i)
}
wg.Wait()
チャネルのパターン

プロデューサー・コンシューマー:

func producer(ch chan<- int) {
    defer close(ch)
    for i := 0; i < 10; i++ {
        ch <- i
    }
}

func consumer(ch <-chan int) {
    for val := range ch {
        fmt.Println(val)
    }
}

ファンアウト・ファンイン:

func fanOut(in <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    for i := 0; i < workers; i++ {
        channels[i] = worker(in)
    }
    return channels
}

func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for val := range c {
                out <- val
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}
競合状態を避ける
  • 共有状態には sync.Mutex を使用します
  • 通信にはチャネルを使用します
  • -race フラグを付けてテストを実行します
  • 不変のデータ構造を優先します

インターフェース設計

小さなインターフェース
// ✅ Small, focused interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Compose when needed
type ReadWriter interface {
    Reader
    Writer
}
インターフェースを受け取り、構造体を返す
// ✅ Accept interface for flexibility
func Process(r io.Reader) error {
    // implementation
}

// ✅ Return concrete type
func NewClient() *Client {
    return &Client{}
}

コード品質

命名規則
  • 非公開には camelCase、公開には PascalCase を使用します
  • 名前は短く、かつ内容が分かるものにします
  • 冗長な名前を避けます:user.UserID → user.ID
  • 一貫した命名を使用します:Get、Set、New、Init
関数の設計
  • 関数は小さく、目的を絞ります
  • 引数の数を制限します(最大 3~4 個)
  • 設定には関数オプションを使用します:
type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) {
        s.timeout = d
    }
}

func NewServer(opts ...Option) *Server {
    s := &Server{timeout: 30 * time.Second}
    for _, opt := range opts {
        opt(s)
    }
    return s
}
定数と列挙型
// Use iota for enums
type Status int

const (
    StatusPending Status = iota
    StatusActive
    StatusInactive
)

// Group related constants
const (
    maxRetries    = 3
    retryDelay    = time.Second
    timeout       = 30 * time.Second
)

テスト

テーブル駆動テスト
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -2, -3, -5},
        {"mixed", -2, 3, 1},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("got %d, want %d", result, tt.expected)
            }
        })
    }
}
テストヘルパー
func TestMain(m *testing.M) {
    // Setup
    code := m.Run()
    // Teardown
    os.Exit(code)
}

func setupTest(t *testing.T) func() {
    // Setup code
    return func() {
        // Cleanup code
    }
}

func TestSomething(t *testing.T) {
    cleanup := setupTest(t)
    defer cleanup()
    // test code
}
モック

テストを可能にするため、依存関係にはインターフェースを使用します:

type UserStore interface {
    Get(id string) (*User, error)
}

type Service struct {
    store UserStore
}

// In tests, provide mock implementation
type mockStore struct{}
func (m *mockStore) Get(id string) (*User, error) {
    return &User{ID: id}, nil
}

パフォーマンス

メモリ割り当て
  • サイズが分かっている場合はスライスを事前に割り当てます:make([]int, 0, capacity)
  • sync.Pool でバッファーを再利用します
  • 大きな構造体にはポインターを使用します
  • ループ内での文字列連結は避けます(strings.Builder を使用します)
プロファイリング
import _ "net/http/pprof"

// CPU profiling
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

プロファイリングを実行します:

go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
go tool pprof cpu.prof
ベンチマーク
func BenchmarkFunction(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Function()
    }
}

// With setup
func BenchmarkWithSetup(b *testing.B) {
    data := setupData()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Process(data)
    }
}

依存関係

モジュール管理
go mod init github.com/user/project
go mod tidy
go mod vendor  # Optional: vendor dependencies
バージョンの固定
go get github.com/pkg/[email protected]
プライベートモジュール
export GOPRIVATE=github.com/myorg/*

ドキュメント

パッケージのドキュメント
// Package mypackage provides utilities for X.
//
// This package includes:
//   - Feature A
//   - Feature B
package mypackage
関数のドキュメント
// ProcessData validates and transforms the input data.
// It returns an error if validation fails.
//
// Example:
//   result, err := ProcessData(input)
//   if err != nil {
//       log.Fatal(err)
//   }
func ProcessData(input string) (string, error) {
    // implementation
}

レイヤードアーキテクチャ

2 層パターン

関心事をそれぞれ異なる層に分離します:

  • ハンドラー層:HTTP リクエスト/レスポンスの処理、入力検証、ルーティング、データベース操作
  • サービス層:複雑なビジネスロジック、外部連携、共有ユーティリティ

このシンプルなアーキテクチャには、次の利点があります:

  • ほとんどのアプリケーションで複雑さを軽減します
  • データアクセスロジックを HTTP ハンドラーの近くに保てます
  • 理解しやすく、保守しやすくなります
  • MVP や小~中規模プロジェクトの開発を迅速化します

Uber FX による依存性注入

主な概念

Uber FX は、アプリケーションのライフサイクルと依存関係を自動的に管理する依存性注入フレームワークです。

主なメリット:

  • ✅ 依存関係を自動的に解決します
  • ✅ ライフサイクル管理を標準で備えています(OnStart/OnStop フック)
  • ✅ 依存関係グラフが明確になります
  • ✅ モックを注入してテストしやすくなります
ハンドラーがルートを自己管理するパターン

各ハンドラーは、RegisterRoutes メソッドを通じて自身のルートを管理します:

package handler

import (
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

type UserHandler struct {
    db *gorm.DB
}

func NewUserHandler(db *gorm.DB) *UserHandler {
    return &UserHandler{db: db}
}

func (h *UserHandler) Register(c *gin.Context) {
    // Handle user registration
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    if err := h.db.Create(&user).Error; err != nil {
        c.JSON(500, gin.H{"error": "failed to create user"})
        return
    }
    
    c.JSON(200, gin.H{"data": user})
}

func (h *UserHandler) Login(c *gin.Context) {
    // Handle user login
}

func (h *UserHandler) GetProfile(c *gin.Context) {
    userID := c.GetUint("user_id")
    
    var user User
    if err := h.db.First(&user, userID).Error; err != nil {
        c.JSON(404, gin.H{"error": "user not found"})
        return
    }
    
    c.JSON(200, gin.H{"data": user})
}

// RegisterRoutes - Each handler registers its own routes
func (h *UserHandler) RegisterRoutes(r *gin.Engine) {
    api := r.Group("/api/user")
    {
        api.POST("/register", h.Register)
        api.POST("/login", h.Login)
        
        // Protected routes
        auth := api.Group("", AuthMiddleware())
        {
            auth.GET("/profile", h.GetProfile)
        }
    }
}
FX アプリケーションの完全な例
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
    
    "github.com/gin-gonic/gin"
    "go.uber.org/fx"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    
    "your-project/internal/config"
    "your-project/internal/handler"
    "your-project/internal/service"
    "your-project/internal/model"
)

func main() {
    app := fx.New(
        // ==================== Configuration ====================
        fx.Provide(config.Load),
        
        // ==================== Database ====================
        fx.Provide(NewDatabase),
        
        // ==================== Services ====================
        fx.Provide(service.NewEmailService),
        fx.Provide(service.NewSMSService),
        
        // ==================== Handlers ====================
        fx.Provide(handler.NewUserHandler),
        fx.Provide(handler.NewChatHandler),
        fx.Provide(handler.NewHealthHandler),
        
        // ==================== HTTP Server ====================
        fx.Provide(NewGinEngine),
        fx.Provide(NewHTTPServer),
        
        // ==================== Route Registration ====================
        fx.Invoke(RegisterAllRoutes),
        
        // ==================== Lifecycle ====================
        fx.Invoke(RegisterLifecycle),
    )
    
    // Start application
    startCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    
    if err := app.Start(startCtx); err != nil {
        log.Fatal("failed to start application:", err)
    }
    
    log.Println("✅ Application started successfully")
    
    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    log.Println("📡 Shutting down gracefully...")
    
    // Graceful shutdown
    stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := app.Stop(stopCtx); err != nil {
        log.Fatal("failed to stop application:", err)
    }
    
    log.Println("👋 Application stopped")
}

// NewDatabase creates database connection with lifecycle management
func NewDatabase(lc fx.Lifecycle, cfg *config.Config) (*gorm.DB, error) {
    db, err := gorm.Open(mysql.Open(cfg.Database.DSN), &gorm.Config{})
    if err != nil {
        return nil, err
    }
    
    sqlDB, err := db.DB()
    if err != nil {
        return nil, err
    }
    
    // Configure connection pool
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)
    sqlDB.SetConnMaxLifetime(time.Hour)
    
    // Auto-migrate models
    if err := db.AutoMigrate(&model.User{}, &model.Chat{}); err != nil {
        return nil, err
    }
    
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            log.Println("📊 Database connected")
            return sqlDB.Ping()
        },
        OnStop: func(ctx context.Context) error {
            log.Println("📊 Closing database connection")
            return sqlDB.Close()
        },
    })
    
    return db, nil
}

// NewGinEngine creates Gin engine with middleware
func NewGinEngine(cfg *config.Config) *gin.Engine {
    if cfg.App.Env == "production" {
        gin.SetMode(gin.ReleaseMode)
    }
    
    engine := gin.New()
    engine.Use(gin.Recovery())
    engine.Use(LoggerMiddleware())
    engine.Use(CORSMiddleware())
    
    return engine
}

// NewHTTPServer creates HTTP server with lifecycle
func NewHTTPServer(lc fx.Lifecycle, engine *gin.Engine, cfg *config.Config) *http.Server {
    srv := &http.Server{
        Addr:           cfg.Server.Address,
        Handler:        engine,
        ReadTimeout:    cfg.Server.ReadTimeout,
        WriteTimeout:   cfg.Server.WriteTimeout,
        MaxHeaderBytes: 1 << 20,
    }
    
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            go func() {
                log.Printf("🚀 HTTP server listening on %s", cfg.Server.Address)
                if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
                    log.Fatal("HTTP server error:", err)
                }
            }()
            return nil
        },
        OnStop: func(ctx context.Context) error {
            log.Println("🛑 Shutting down HTTP server")
            return srv.Shutdown(ctx)
        },
    })
    
    return srv
}

// RegisterAllRoutes registers all handler routes
func RegisterAllRoutes(
    engine *gin.Engine,
    healthHandler *handler.HealthHandler,
    userHandler *handler.UserHandler,
    chatHandler *handler.ChatHandler,
) {
    healthHandler.RegisterRoutes(engine)
    userHandler.RegisterRoutes(engine)
    chatHandler.RegisterRoutes(engine)
    
    log.Println("✅ All routes registered")
}

// RegisterLifecycle adds global lifecycle hooks
func RegisterLifecycle(lc fx.Lifecycle) {
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            log.Println("🎯 Application starting...")
            return nil
        },
        OnStop: func(ctx context.Context) error {
            log.Println("🎯 Application stopping...")
            return nil
        },
    })
}
FX を使用したサービスレイヤー

ビジネスロジックが複雑な場合は、サービスに切り出します。

package service

import (
    "fmt"
    "gorm.io/gorm"
)

type EmailService struct {
    db     *gorm.DB
    apiKey string
}

func NewEmailService(db *gorm.DB, cfg *config.Config) *EmailService {
    return &EmailService{
        db:     db,
        apiKey: cfg.Email.APIKey,
    }
}

func (s *EmailService) SendWelcome(email string) error {
    // Complex email sending logic
    return nil
}

func (s *EmailService) SendVerification(userID uint) error {
    var user User
    if err := s.db.First(&user, userID).Error; err != nil {
        return fmt.Errorf("user not found: %w", err)
    }
    
    // Generate verification token and send email
    return nil
}
サービスを使用するハンドラー
type UserHandler struct {
    db           *gorm.DB
    emailService *service.EmailService
}

func NewUserHandler(db *gorm.DB, emailService *service.EmailService) *UserHandler {
    return &UserHandler{
        db:           db,
        emailService: emailService,
    }
}

func (h *UserHandler) Register(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // Direct database access for simple operations
    if err := h.db.Create(&user).Error; err != nil {
        c.JSON(500, gin.H{"error": "failed to create user"})
        return
    }
    
    // Use service for complex operations
    go h.emailService.SendWelcome(user.Email)
    
    c.JSON(200, gin.H{"data": user})
}
FX のベストプラクティス

推奨事項 ✅

  1. ポインターを返すコンストラクター関数を使用します:func NewX() *X
  2. コンストラクターのパラメーターを通じて依存関係を注入します
  3. 起動/終了処理には fx.Lifecycle を使用します
  4. ハンドラーは HTTP に関する処理に専念させます
  5. 複雑なロジックはサービスに切り出します

禁止事項 ❌

  1. サービス内で gin.Context にアクセスしないでください
  2. グローバル変数を作成しないでください
  3. HTTP のロジックとビジネスロジックを混在させないでください
  4. ライフサイクルエラーの処理を忘れないでください
  5. 循環依存を作成しないでください
FX を使用したテスト
func TestUserHandler(t *testing.T) {
    // Create test database
    db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    db.AutoMigrate(&User{})
    
    // Create mock service
    emailService := &MockEmailService{}
    
    // Inject dependencies manually for testing
    handler := NewUserHandler(db, emailService)
    
    // Test handler methods
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)
    
    handler.Register(c)
    
    assert.Equal(t, 200, w.Code)
}

実装例の全容については、references/fx-architecture.md を参照してください。

リソース

詳細なガイドについては、同梱のリファレンスを参照してください。

  • references/fx-architecture.md:ハンドラー、サービス、GORM リポジトリー、設定管理を含む、Fx の依存性注入アーキテクチャの全容
  • references/patterns.md:一般的な Go のデザインパターンとイディオム
  • references/stdlib.md:標準ライブラリーの主要パッケージに関するガイド
  • references/tools.md:開発ツールとリンターの設定

コメント