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使用
- エラーハンドリング: カスタムエラー型、エラーラッピング、エラーコレクション
- リフレクション: 動的なバリデーションシステムの構築
- プロダクションパターン: リトライロジック、グレースフルシャットダウン
各実装は本番環境で使用できる品質を目指しており、エラーハンドリング、リソース管理、並行処理の安全性を重視しています。