mcpField
G
go-patterns

go-patterns

github.com/geoffjay開発836 回インストール51 週間前に更新
リポジトリ

go-patterns について

go-patterns は、Go 開発者向けに現代的なイディオム、デザインパターン、ベストプラクティスをまとめたスキルです。Go 1.18 以降の機能を中心に、例と参考資料を提供します。

主な機能

  1. 01現代的な Go の機能とジェネリクス
  2. 02一般的なデザインパターンの例
  3. 03エラーとインターフェースの扱い方
  4. 04Context の使用パターン
  5. 05Go のコーディングプラクティス

活用シーン

  1. 01イディオマティックな Go コードの作成
  2. 02既存 Go プロジェクトのリファクタリング
  3. 03イディオム遵守のコードレビュー
  4. 04ジェネリクスとワークスペースの学習

インストール

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

$npx skills add https://github.com/geoffjay/claude-plugins --skill go-patterns

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

詳細

Go パターンのスキル

このスキルでは、モダンな Go のパターン、イディオム、ベストプラクティスについて包括的に解説します。特に、Go 1.18 以降で導入された機能に重点を置いています。

使用するタイミング

以下の場合にこのスキルを有効にしてください:

  • イディオムに沿った Go コードを書く
  • Go でデザインパターンを実装する
  • モダンな Go の機能(ジェネリクス、ファジング、ワークスペース)を使用する
  • よりイディオムに沿ったコードにリファクタリングする
  • Go のベストプラクティスを教える
  • イディオムへの準拠をコードレビューする

モダンな Go の機能

ジェネリクス(Go 1.18 以降)

型パラメーター:

// Generic function
func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

// Usage
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(n int) int { return n * 2 })

型制約:

// Ordered constraint
type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64 | ~string
}

func Min[T Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// Custom constraints
type Numeric interface {
    ~int | ~int64 | ~float64
}

func Sum[T Numeric](values []T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}

ジェネリックなデータ構造:

// Generic stack
type Stack[T any] struct {
    items []T
}

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{items: make([]T, 0)}
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// Generic map utilities
func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

func Values[K comparable, V any](m map[K]V) []V {
    values := make([]V, 0, len(m))
    for _, v := range m {
        values = append(values, v)
    }
    return values
}
ワークスペース(Go 1.18 以降)

go.work ファイル:

go 1.21

use (
    ./service
    ./shared
    ./tools
)

replace example.com/legacy => ./vendor/legacy

利点:

  • 複数モジュールの開発
  • ローカルの依存関係の上書き
  • モジュール間のテストの簡素化
  • モノレポのサポート向上

重要な Go パターン

関数オプションパターン
type Server struct {
    host    string
    port    int
    timeout time.Duration
    logger  *log.Logger
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

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

func WithLogger(logger *log.Logger) Option {
    return func(s *Server) {
        s.logger = logger
    }
}

func NewServer(opts ...Option) *Server {
    s := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
        logger:  log.Default(),
    }

    for _, opt := range opts {
        opt(s)
    }

    return s
}

// Usage
server := NewServer(
    WithHost("0.0.0.0"),
    WithPort(3000),
    WithTimeout(60 * time.Second),
)
ビルダーパターン
type Query struct {
    table      string
    where      []string
    orderBy    string
    limit      int
    offset     int
}

type QueryBuilder struct {
    query Query
}

func NewQueryBuilder(table string) *QueryBuilder {
    return &QueryBuilder{
        query: Query{table: table},
    }
}

func (b *QueryBuilder) Where(condition string) *QueryBuilder {
    b.query.where = append(b.query.where, condition)
    return b
}

func (b *QueryBuilder) OrderBy(field string) *QueryBuilder {
    b.query.orderBy = field
    return b
}

func (b *QueryBuilder) Limit(limit int) *QueryBuilder {
    b.query.limit = limit
    return b
}

func (b *QueryBuilder) Offset(offset int) *QueryBuilder {
    b.query.offset = offset
    return b
}

func (b *QueryBuilder) Build() Query {
    return b.query
}

// Usage
query := NewQueryBuilder("users").
    Where("age > 18").
    Where("active = true").
    OrderBy("created_at DESC").
    Limit(10).
    Offset(20).
    Build()
ストラテジーパターン
// Strategy interface
type PaymentStrategy interface {
    Pay(amount float64) error
}

// Concrete strategies
type CreditCardPayment struct {
    cardNumber string
}

func (c *CreditCardPayment) Pay(amount float64) error {
    fmt.Printf("Paying $%.2f with credit card %s\n", amount, c.cardNumber)
    return nil
}

type PayPalPayment struct {
    email string
}

func (p *PayPalPayment) Pay(amount float64) error {
    fmt.Printf("Paying $%.2f with PayPal account %s\n", amount, p.email)
    return nil
}

type CryptoPayment struct {
    walletAddress string
}

func (c *CryptoPayment) Pay(amount float64) error {
    fmt.Printf("Paying $%.2f to wallet %s\n", amount, c.walletAddress)
    return nil
}

// Context
type PaymentProcessor struct {
    strategy PaymentStrategy
}

func NewPaymentProcessor(strategy PaymentStrategy) *PaymentProcessor {
    return &PaymentProcessor{strategy: strategy}
}

func (p *PaymentProcessor) ProcessPayment(amount float64) error {
    return p.strategy.Pay(amount)
}

// Usage
processor := NewPaymentProcessor(&CreditCardPayment{cardNumber: "1234-5678"})
processor.ProcessPayment(100.00)

processor = NewPaymentProcessor(&PayPalPayment{email: "[email protected]"})
processor.ProcessPayment(50.00)
オブザーバーパターン
type Observer interface {
    Update(event Event)
}

type Event struct {
    Type string
    Data interface{}
}

type Subject struct {
    observers []Observer
}

func (s *Subject) Attach(observer Observer) {
    s.observers = append(s.observers, observer)
}

func (s *Subject) Detach(observer Observer) {
    for i, obs := range s.observers {
        if obs == observer {
            s.observers = append(s.observers[:i], s.observers[i+1:]...)
            break
        }
    }
}

func (s *Subject) Notify(event Event) {
    for _, observer := range s.observers {
        observer.Update(event)
    }
}

// Concrete observer
type Logger struct {
    name string
}

func (l *Logger) Update(event Event) {
    fmt.Printf("[%s] Received event: %s\n", l.name, event.Type)
}

// Usage
subject := &Subject{}
logger1 := &Logger{name: "Logger1"}
logger2 := &Logger{name: "Logger2"}

subject.Attach(logger1)
subject.Attach(logger2)

subject.Notify(Event{Type: "UserCreated", Data: "user123"})

イディオムに沿った Go のパターン

エラー処理

センチネルエラー:

var (
    ErrNotFound     = errors.New("resource not found")
    ErrUnauthorized = errors.New("unauthorized access")
    ErrInvalidInput = errors.New("invalid input")
)

func GetUser(id string) (*User, error) {
    if id == "" {
        return nil, ErrInvalidInput
    }

    user := findUser(id)
    if user == nil {
        return nil, ErrNotFound
    }

    return user, nil
}

// Check with errors.Is
if errors.Is(err, ErrNotFound) {
    // Handle not found
}

カスタムエラー型:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}

// Check with errors.As
var valErr *ValidationError
if errors.As(err, &valErr) {
    fmt.Printf("Validation failed: %s\n", valErr.Field)
}

エラーのラップ:

func ProcessUser(id string) error {
    user, err := GetUser(id)
    if err != nil {
        return fmt.Errorf("process user: %w", err)
    }

    if err := ValidateUser(user); err != nil {
        return fmt.Errorf("validate user %s: %w", id, err)
    }

    return nil
}
インターフェースパターン

小さなインターフェース:

// Good: Small, focused interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

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

type Closer interface {
    Close() error
}

// Compose interfaces
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

インターフェースの分離:

// Instead of one large interface
type Repository interface {
    Create(ctx context.Context, user *User) error
    Read(ctx context.Context, id string) (*User, error)
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id string) error
    List(ctx context.Context) ([]*User, error)
    Search(ctx context.Context, query string) ([]*User, error)
}

// Better: Separate interfaces
type UserCreator interface {
    Create(ctx context.Context, user *User) error
}

type UserReader interface {
    Read(ctx context.Context, id string) (*User, error)
    List(ctx context.Context) ([]*User, error)
}

type UserUpdater interface {
    Update(ctx context.Context, user *User) error
}

type UserDeleter interface {
    Delete(ctx context.Context, id string) error
}

type UserSearcher interface {
    Search(ctx context.Context, query string) ([]*User, error)
}
コンテキストパターン

コンテキストの適切な使用:

func FetchData(ctx context.Context, url string) ([]byte, error) {
    // Create request with context
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, fmt.Errorf("create request: %w", err)
    }

    // Check for cancellation before expensive operation
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
    }

    // Execute request
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("execute request: %w", err)
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

// Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

data, err := FetchData(ctx, "https://api.example.com/data")

コンテキスト値:

type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

func WithRequestID(ctx context.Context, requestID string) context.Context {
    return context.WithValue(ctx, requestIDKey, requestID)
}

func GetRequestID(ctx context.Context) (string, bool) {
    requestID, ok := ctx.Value(requestIDKey).(string)
    return requestID, ok
}

func WithUserID(ctx context.Context, userID string) context.Context {
    return context.WithValue(ctx, userIDKey, userID)
}

func GetUserID(ctx context.Context) (string, bool) {
    userID, ok := ctx.Value(userIDKey).(string)
    return userID, ok
}

ベストプラクティス

  1. インターフェースを受け取り、構造体を返す
  2. ゼロ値を有用にする
  3. 継承よりコンポジションを使う
  4. エラーを明示的に処理する
  5. クリーンアップには defer を使う
  6. 読み取りが多い処理には sync.RWMutex を優先する
  7. キャンセルとタイムアウトには context を使う
  8. インターフェースを小さく保つ
  9. エクスポートされる識別子にドキュメントを付ける
  10. go fmt と go vet を使う

リソース

追加のパターンと例は assets/ ディレクトリにあります:

  • examples/ - 完全なコード例
  • patterns/ - デザインパターンの実装
  • antipatterns/ - 避けるべきよくある間違い

以下については references/ ディレクトリを参照してください:

  • Go 公式ドキュメントへのリンク
  • Effective Go のガイドライン
  • Go の格言
  • コミュニティのベストプラクティス

コメント