Day 6: 高度なパターン - 解答例

Exercise 1: Context-Based HTTP Client

問題

タイムアウトとキャンセル機能を持つHTTPクライアントを実装してください。

解答

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)

// HTTPClientはcontext対応のHTTPクライアントです
type HTTPClient struct {
    client  *http.Client
    timeout time.Duration
}

// NewHTTPClientは新しいHTTPクライアントを作成します
func NewHTTPClient(timeout time.Duration) *HTTPClient {
    return &HTTPClient{
        client: &http.Client{
            Timeout: timeout,
        },
        timeout: timeout,
    }
}

// Getは指定されたURLにGETリクエストを送信します
func (c *HTTPClient) Get(ctx context.Context, url string) ([]byte, error) {
    // タイムアウト付きのcontextを作成
    ctx, cancel := context.WithTimeout(ctx, c.timeout)
    defer cancel()

    // リクエストを作成
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("failed to create request: %w", err)
    }

    // リクエストを送信
    resp, err := c.client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("failed to send request: %w", err)
    }
    defer resp.Body.Close()

    // レスポンスボディを読み取り
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("failed to read response: %w", err)
    }

    // ステータスコードをチェック
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
    }

    return body, nil
}

// GetWithRetriesはリトライ機能付きのGETリクエストを送信します
func (c *HTTPClient) GetWithRetries(ctx context.Context, url string, maxRetries int) ([]byte, error) {
    var lastErr error

    for i := 0; i < maxRetries; i++ {
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
        }

        body, err := c.Get(ctx, url)
        if err == nil {
            return body, nil
        }

        lastErr = err
        fmt.Printf("Attempt %d failed: %v\n", i+1, err)

        // 指数バックオフ
        if i < maxRetries-1 {
            backoff := time.Duration(1<<uint(i)) * time.Second
            select {
            case <-time.After(backoff):
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }
    }

    return nil, fmt.Errorf("all retries failed: %w", lastErr)
}

// 使用例
func main() {
    client := NewHTTPClient(10 * time.Second)

    // 基本的な使用
    ctx := context.Background()
    body, err := client.Get(ctx, "https://api.example.com/data")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("Response: %s\n", body)

    // リトライ付き
    body, err = client.GetWithRetries(ctx, "https://api.example.com/data", 3)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("Response: %s\n", body)
}

---

Exercise 2: Worker Pool with Context

問題

context対応のワーカープールを実装してください。

解答

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

// Task represents a unit of work
type Task struct {
    ID      int
    Data    string
    Process func(string) (string, error)
}

// TaskResult represents the result of a task
type TaskResult struct {
    TaskID int
    Result string
    Error  error
}

// WorkerPool manages a pool of workers
type WorkerPool struct {
    workerCount int
    taskQueue   chan Task
    resultQueue chan TaskResult
    wg          sync.WaitGroup
    ctx         context.Context
    cancel      context.CancelFunc
}

// NewWorkerPool creates a new worker pool
func NewWorkerPool(workerCount int) *WorkerPool {
    ctx, cancel := context.WithCancel(context.Background())
    return &WorkerPool{
        workerCount: workerCount,
        taskQueue:   make(chan Task, 100),
        resultQueue: make(chan TaskResult, 100),
        ctx:         ctx,
        cancel:      cancel,
    }
}

// Start starts all workers
func (wp *WorkerPool) Start() {
    for i := 0; i < wp.workerCount; i++ {
        wp.wg.Add(1)
        go wp.worker(i)
    }
}

// worker processes tasks from the queue
func (wp *WorkerPool) worker(id int) {
    defer wp.wg.Done()

    fmt.Printf("[Worker %d] Started\n", id)

    for {
        select {
        case task, ok := <-wp.taskQueue:
            if !ok {
                fmt.Printf("[Worker %d] Task queue closed\n", id)
                return
            }

            fmt.Printf("[Worker %d] Processing task %d\n", id, task.ID)

            // タスクを処理
            result, err := task.Process(task.Data)

            // 結果を送信
            select {
            case wp.resultQueue <- TaskResult{
                TaskID: task.ID,
                Result: result,
                Error:  err,
            }:
            case <-wp.ctx.Done():
                fmt.Printf("[Worker %d] Cancelled\n", id)
                return
            }

        case <-wp.ctx.Done():
            fmt.Printf("[Worker %d] Context cancelled\n", id)
            return
        }
    }
}

// Submit submits a task to the pool
func (wp *WorkerPool) Submit(task Task) error {
    select {
    case wp.taskQueue <- task:
        return nil
    case <-wp.ctx.Done():
        return wp.ctx.Err()
    }
}

// Results returns the result channel
func (wp *WorkerPool) Results() <-chan TaskResult {
    return wp.resultQueue
}

// Shutdown gracefully shuts down the worker pool
func (wp *WorkerPool) Shutdown() {
    fmt.Println("Shutting down worker pool...")
    close(wp.taskQueue)
    wp.wg.Wait()
    close(wp.resultQueue)
}

// Cancel cancels all workers
func (wp *WorkerPool) Cancel() {
    fmt.Println("Cancelling worker pool...")
    wp.cancel()
}

// 使用例
func demonstrateWorkerPool() {
    // ワーカープールを作成
    pool := NewWorkerPool(3)
    pool.Start()

    // 結果を収集するゴルーチン
    done := make(chan bool)
    go func() {
        for result := range pool.Results() {
            if result.Error != nil {
                fmt.Printf("Task %d failed: %v\n", result.TaskID, result.Error)
            } else {
                fmt.Printf("Task %d completed: %s\n", result.TaskID, result.Result)
            }
        }
        done <- true
    }()

    // タスクを投入
    for i := 0; i < 10; i++ {
        task := Task{
            ID:   i,
            Data: fmt.Sprintf("data-%d", i),
            Process: func(data string) (string, error) {
                time.Sleep(500 * time.Millisecond)
                return fmt.Sprintf("Processed: %s", data), nil
            },
        }

        if err := pool.Submit(task); err != nil {
            fmt.Printf("Failed to submit task: %v\n", err)
        }
    }

    // シャットダウン
    pool.Shutdown()
    <-done
}

---

Exercise 3: Error Wrapping and Custom Errors

問題

カスタムエラー型とエラーラッピングを使用した設定管理システムを実装してください。

解答

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "os"
)

// カスタムエラー型の定義

// ConfigError represents a configuration error
type ConfigError struct {
    Path    string
    Message string
    Err     error
}

func (e *ConfigError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("config error at %s: %s: %v", e.Path, e.Message, e.Err)
    }
    return fmt.Sprintf("config error at %s: %s", e.Path, e.Message)
}

func (e *ConfigError) Unwrap() error {
    return e.Err
}

// ValidationError represents a validation error
type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field '%s' with value '%v': %s",
        e.Field, e.Value, e.Message)
}

// センチネルエラー
var (
    ErrConfigNotFound = errors.New("config not found")
    ErrInvalidFormat  = errors.New("invalid config format")
    ErrMissingField   = errors.New("missing required field")
)

// Config represents application configuration
type Config struct {
    Server   ServerConfig   `json:"server"`
    Database DatabaseConfig `json:"database"`
}

type ServerConfig struct {
    Host string `json:"host"`
    Port int    `json:"port"`
}

type DatabaseConfig struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Username string `json:"username"`
    Password string `json:"password"`
}

// ConfigLoader loads and validates configuration
type ConfigLoader struct {
    path string
}

// NewConfigLoader creates a new config loader
func NewConfigLoader(path string) *ConfigLoader {
    return &ConfigLoader{path: path}
}

// Load loads the configuration from file
func (cl *ConfigLoader) Load() (*Config, error) {
    // ファイルを読み込む
    data, err := os.ReadFile(cl.path)
    if err != nil {
        if os.IsNotExist(err) {
            return nil, &ConfigError{
                Path:    cl.path,
                Message: "file not found",
                Err:     ErrConfigNotFound,
            }
        }
        return nil, &ConfigError{
            Path:    cl.path,
            Message: "failed to read file",
            Err:     err,
        }
    }

    // JSONをパース
    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, &ConfigError{
            Path:    cl.path,
            Message: "failed to parse JSON",
            Err:     fmt.Errorf("%w: %v", ErrInvalidFormat, err),
        }
    }

    // バリデーション
    if err := cl.validate(&config); err != nil {
        return nil, &ConfigError{
            Path:    cl.path,
            Message: "validation failed",
            Err:     err,
        }
    }

    return &config, nil
}

// validate validates the configuration
func (cl *ConfigLoader) validate(config *Config) error {
    errs := &MultiError{}

    // サーバー設定のバリデーション
    if config.Server.Host == "" {
        errs.Add(&ValidationError{
            Field:   "server.host",
            Value:   config.Server.Host,
            Message: "cannot be empty",
        })
    }

    if config.Server.Port < 1 || config.Server.Port > 65535 {
        errs.Add(&ValidationError{
            Field:   "server.port",
            Value:   config.Server.Port,
            Message: "must be between 1 and 65535",
        })
    }

    // データベース設定のバリデーション
    if config.Database.Host == "" {
        errs.Add(&ValidationError{
            Field:   "database.host",
            Value:   config.Database.Host,
            Message: "cannot be empty",
        })
    }

    if config.Database.Port < 1 || config.Database.Port > 65535 {
        errs.Add(&ValidationError{
            Field:   "database.port",
            Value:   config.Database.Port,
            Message: "must be between 1 and 65535",
        })
    }

    if config.Database.Username == "" {
        errs.Add(&ValidationError{
            Field:   "database.username",
            Value:   config.Database.Username,
            Message: "cannot be empty",
        })
    }

    if errs.HasErrors() {
        return errs
    }

    return nil
}

// MultiError aggregates multiple errors
type MultiError struct {
    Errors []error
}

func (m *MultiError) Error() string {
    if len(m.Errors) == 0 {
        return "no errors"
    }

    msg := fmt.Sprintf("%d validation error(s):\n", len(m.Errors))
    for i, err := range m.Errors {
        msg += fmt.Sprintf("  %d. %v\n", i+1, err)
    }
    return msg
}

func (m *MultiError) Add(err error) {
    if err != nil {
        m.Errors = append(m.Errors, err)
    }
}

func (m *MultiError) HasErrors() bool {
    return len(m.Errors) > 0
}

// エラーハンドリングの例
func handleConfigError(err error) {
    // ConfigErrorかチェック
    var configErr *ConfigError
    if errors.As(err, &configErr) {
        fmt.Printf("Configuration error at %s: %s\n", configErr.Path, configErr.Message)

        // センチネルエラーをチェック
        if errors.Is(err, ErrConfigNotFound) {
            fmt.Println("Please create a configuration file")
        } else if errors.Is(err, ErrInvalidFormat) {
            fmt.Println("Please check the JSON format")
        }
    }

    // MultiErrorかチェック
    var multiErr *MultiError
    if errors.As(err, &multiErr) {
        fmt.Println("Multiple validation errors occurred:")
        for i, e := range multiErr.Errors {
            fmt.Printf("  %d. %v\n", i+1, e)
        }
    }

    // ValidationErrorかチェック
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        fmt.Printf("Field '%s' validation failed: %s\n",
            validationErr.Field, validationErr.Message)
    }
}

// 使用例
func main() {
    loader := NewConfigLoader("config.json")

    config, err := loader.Load()
    if err != nil {
        handleConfigError(err)
        os.Exit(1)
    }

    fmt.Printf("Configuration loaded successfully:\n")
    fmt.Printf("  Server: %s:%d\n", config.Server.Host, config.Server.Port)
    fmt.Printf("  Database: %s:%d\n", config.Database.Host, config.Database.Port)
}

---

Exercise 4: Reflection-Based Validator

問題

リフレクションを使用して構造体のバリデーションを行うライブラリを実装してください。

解答

package main

import (
    "errors"
    "fmt"
    "reflect"
    "strings"
)

// Validator validates struct fields based on tags
type Validator struct {
    errors []error
}

// NewValidator creates a new validator
func NewValidator() *Validator {
    return &Validator{
        errors: make([]error, 0),
    }
}

// Validate validates a struct
func (v *Validator) Validate(s interface{}) error {
    v.errors = make([]error, 0)

    val := reflect.ValueOf(s)
    typ := reflect.TypeOf(s)

    // ポインタの場合は実体を取得
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
        typ = typ.Elem()
    }

    // 構造体でない場合はエラー
    if val.Kind() != reflect.Struct {
        return errors.New("input must be a struct")
    }

    // 各フィールドをバリデーション
    for i := 0; i < val.NumField(); i++ {
        field := typ.Field(i)
        value := val.Field(i)

        // validateタグを取得
        tag := field.Tag.Get("validate")
        if tag == "" {
            continue
        }

        // バリデーションルールを適用
        v.validateField(field.Name, value, tag)
    }

    if len(v.errors) > 0 {
        return &MultiValidationError{Errors: v.errors}
    }

    return nil
}

// validateField validates a single field
func (v *Validator) validateField(fieldName string, value reflect.Value, tag string) {
    rules := strings.Split(tag, ",")

    for _, rule := range rules {
        parts := strings.Split(rule, "=")
        ruleName := parts[0]

        switch ruleName {
        case "required":
            if value.IsZero() {
                v.addError(fieldName, "is required")
            }

        case "min":
            if len(parts) < 2 {
                continue
            }
            minLen := 0
            fmt.Sscanf(parts[1], "%d", &minLen)

            switch value.Kind() {
            case reflect.String:
                if len(value.String()) < minLen {
                    v.addError(fieldName, fmt.Sprintf("must be at least %d characters", minLen))
                }
            case reflect.Int, reflect.Int64:
                if value.Int() < int64(minLen) {
                    v.addError(fieldName, fmt.Sprintf("must be at least %d", minLen))
                }
            }

        case "max":
            if len(parts) < 2 {
                continue
            }
            maxLen := 0
            fmt.Sscanf(parts[1], "%d", &maxLen)

            switch value.Kind() {
            case reflect.String:
                if len(value.String()) > maxLen {
                    v.addError(fieldName, fmt.Sprintf("must be at most %d characters", maxLen))
                }
            case reflect.Int, reflect.Int64:
                if value.Int() > int64(maxLen) {
                    v.addError(fieldName, fmt.Sprintf("must be at most %d", maxLen))
                }
            }

        case "email":
            if value.Kind() == reflect.String {
                email := value.String()
                if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
                    v.addError(fieldName, "must be a valid email address")
                }
            }
        }
    }
}

// addError adds a validation error
func (v *Validator) addError(field, message string) {
    v.errors = append(v.errors, &FieldValidationError{
        Field:   field,
        Message: message,
    })
}

// FieldValidationError represents a field validation error
type FieldValidationError struct {
    Field   string
    Message string
}

func (e *FieldValidationError) Error() string {
    return fmt.Sprintf("%s %s", e.Field, e.Message)
}

// MultiValidationError represents multiple validation errors
type MultiValidationError struct {
    Errors []error
}

func (e *MultiValidationError) Error() string {
    messages := make([]string, len(e.Errors))
    for i, err := range e.Errors {
        messages[i] = err.Error()
    }
    return strings.Join(messages, "; ")
}

// 使用例

type User struct {
    Name     string `validate:"required,min=3,max=50"`
    Email    string `validate:"required,email"`
    Age      int    `validate:"required,min=0,max=150"`
    Password string `validate:"required,min=8"`
}

func main() {
    // 有効なユーザー
    validUser := User{
        Name:     "John Doe",
        Email:    "john@example.com",
        Age:      30,
        Password: "secret123",
    }

    validator := NewValidator()
    if err := validator.Validate(validUser); err != nil {
        fmt.Println("Validation failed:", err)
    } else {
        fmt.Println("Valid user")
    }

    // 無効なユーザー
    invalidUser := User{
        Name:     "Jo",
        Email:    "invalid-email",
        Age:      200,
        Password: "123",
    }

    if err := validator.Validate(invalidUser); err != nil {
        fmt.Println("Validation failed:", err)
    }
}

---

まとめ

これらの解答例では、以下の重要な概念を実装しました:

  • Context統合: HTTPクライアントとワーカープールでのcontext使用
  • エラーハンドリング: カスタムエラー型、エラーラッピング、エラーコレクション
  • リフレクション: 動的なバリデーションシステムの構築
  • プロダクションパターン: リトライロジック、グレースフルシャットダウン

各実装は本番環境で使用できる品質を目指しており、エラーハンドリング、リソース管理、並行処理の安全性を重視しています。