go-patterns について
go-patterns は、Go 開発者向けに現代的なイディオム、デザインパターン、ベストプラクティスをまとめたスキルです。Go 1.18 以降の機能を中心に、例と参考資料を提供します。
主な機能
- 01現代的な Go の機能とジェネリクス
- 02一般的なデザインパターンの例
- 03エラーとインターフェースの扱い方
- 04Context の使用パターン
- 05Go のコーディングプラクティス
活用シーン
- 01イディオマティックな Go コードの作成
- 02既存 Go プロジェクトのリファクタリング
- 03イディオム遵守のコードレビュー
- 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
}
ベストプラクティス
- インターフェースを受け取り、構造体を返す
- ゼロ値を有用にする
- 継承よりコンポジションを使う
- エラーを明示的に処理する
- クリーンアップには defer を使う
- 読み取りが多い処理には sync.RWMutex を優先する
- キャンセルとタイムアウトには context を使う
- インターフェースを小さく保つ
- エクスポートされる識別子にドキュメントを付ける
- go fmt と go vet を使う
リソース
追加のパターンと例は assets/ ディレクトリにあります:
examples/- 完全なコード例patterns/- デザインパターンの実装antipatterns/- 避けるべきよくある間違い
以下については references/ ディレクトリを参照してください:
- Go 公式ドキュメントへのリンク
- Effective Go のガイドライン
- Go の格言
- コミュニティのベストプラクティス
コメント
Markdown 対応