golang-enterprise-patterns について
エンタープライズ向け Go アプリケーションのアーキテクチャとコード構成を案内するスキルです。複雑な業務ロジックや大規模コードベース向けに、Clean Architecture、ヘキサゴナルアーキテクチャ、DDD を扱います。
主な機能
- 01Clean Architecture のレイヤー構成
- 02ヘキサゴナルアーキテクチャとポート/アダプター
- 03DDD の集約、値オブジェクト、ドメインイベント
- 04依存性注入のパターン
- 05エラー処理と設定管理の実践
活用シーン
- 01複雑な Go アプリケーションの設計
- 02大規模な Go コードベースの整理
- 03チーム内の実装パターンの統一
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ 89jobrien/ steve --skill golang-enterprise-patternsインストール後、Claude Code で/golang-enterprise-patternsを入力して読み込まれたか確認します。
詳細
Golang エンタープライズパターン
このスキルでは、エンタープライズレベルの Go アプリケーションのアーキテクチャ、デザインパターン、本番環境に対応したコード構成について解説します。
このスキルを使用する場面
- 複雑なビジネスロジックを持つ新しい Go アプリケーションを設計するとき
- クリーンアーキテクチャやヘキサゴナルアーキテクチャを実装するとき
- ドメイン駆動設計(DDD)の原則を適用するとき
- 大規模な Go コードベースを整理するとき
- チーム内の一貫性を保つためのパターンを確立するとき
クリーンアーキテクチャ
レイヤー構造
/cmd
/api - HTTP/gRPC entry points
/worker - Background job runners
/internal
/domain - Business entities and interfaces
/application - Use cases and application services
/infrastructure
/persistence - Database implementations
/messaging - Queue implementations
/http - HTTP client implementations
/interfaces
/api - HTTP handlers
/grpc - gRPC handlers
/pkg - Shared libraries (public)
依存関係のルール
依存関係は内側にのみ向かいます。
Interfaces → Application → Domain
↓ ↓
Infrastructure (implements domain interfaces)
ドメインレイヤー
// domain/user.go
package domain
import "time"
type UserID string
type User struct {
ID UserID
Email string
Name string
CreatedAt time.Time
}
// UserRepository defines the contract for user persistence
type UserRepository interface {
FindByID(ctx context.Context, id UserID) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id UserID) error
}
// UserService defines domain business logic
type UserService interface {
Register(ctx context.Context, email, name string) (*User, error)
Authenticate(ctx context.Context, email, password string) (*User, error)
}
アプリケーションレイヤー
// application/user_service.go
package application
type UserServiceImpl struct {
repo domain.UserRepository
hasher PasswordHasher
logger Logger
}
func NewUserService(repo domain.UserRepository, hasher PasswordHasher, logger Logger) *UserServiceImpl {
return &UserServiceImpl{repo: repo, hasher: hasher, logger: logger}
}
func (s *UserServiceImpl) Register(ctx context.Context, email, name string) (*domain.User, error) {
// Check if user exists
existing, err := s.repo.FindByEmail(ctx, email)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
return nil, fmt.Errorf("checking existing user: %w", err)
}
if existing != nil {
return nil, domain.ErrUserAlreadyExists
}
user := &domain.User{
ID: domain.UserID(uuid.New().String()),
Email: email,
Name: name,
CreatedAt: time.Now(),
}
if err := s.repo.Save(ctx, user); err != nil {
return nil, fmt.Errorf("saving user: %w", err)
}
return user, nil
}
ヘキサゴナルアーキテクチャ(ポートとアダプター)
ポートの定義
// ports/primary.go - Driving ports (input)
package ports
type UserAPI interface {
CreateUser(ctx context.Context, req CreateUserRequest) (*UserResponse, error)
GetUser(ctx context.Context, id string) (*UserResponse, error)
}
// ports/secondary.go - Driven ports (output)
type UserStorage interface {
Save(ctx context.Context, user *domain.User) error
FindByID(ctx context.Context, id string) (*domain.User, error)
}
type NotificationSender interface {
SendWelcomeEmail(ctx context.Context, user *domain.User) error
}
アダプターの実装
// adapters/postgres/user_repository.go
package postgres
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) Save(ctx context.Context, user *domain.User) error {
query := `INSERT INTO users (id, email, name, created_at) VALUES ($1, $2, $3, $4)`
_, err := r.db.ExecContext(ctx, query, user.ID, user.Email, user.Name, user.CreatedAt)
return err
}
ドメイン駆動設計(DDD)
集約ルート
// domain/order/aggregate.go
package order
type Order struct {
id OrderID
customerID CustomerID
items []OrderItem
status OrderStatus
events []DomainEvent
}
func NewOrder(customerID CustomerID) *Order {
o := &Order{
id: OrderID(uuid.New().String()),
customerID: customerID,
status: StatusPending,
}
o.recordEvent(OrderCreated{OrderID: o.id, CustomerID: customerID})
return o
}
func (o *Order) AddItem(productID ProductID, quantity int, price Money) error {
if o.status != StatusPending {
return ErrOrderNotModifiable
}
o.items = append(o.items, OrderItem{
ProductID: productID,
Quantity: quantity,
Price: price,
})
return nil
}
func (o *Order) Submit() error {
if len(o.items) == 0 {
return ErrEmptyOrder
}
o.status = StatusSubmitted
o.recordEvent(OrderSubmitted{OrderID: o.id})
return nil
}
値オブジェクト
// domain/money.go
type Money struct {
amount int64 // cents
currency string
}
func NewMoney(amount int64, currency string) (Money, error) {
if amount < 0 {
return Money{}, ErrNegativeAmount
}
return Money{amount: amount, currency: currency}, nil
}
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, ErrCurrencyMismatch
}
return Money{amount: m.amount + other.amount, currency: m.currency}, nil
}
ドメインイベント
// domain/events.go
type DomainEvent interface {
EventName() string
OccurredAt() time.Time
}
type OrderCreated struct {
OrderID OrderID
CustomerID CustomerID
occurredAt time.Time
}
func (e OrderCreated) EventName() string { return "order.created" }
func (e OrderCreated) OccurredAt() time.Time { return e.occurredAt }
依存性注入
Wire 形式の DI
// wire.go
//+build wireinject
func InitializeApp(cfg *config.Config) (*App, error) {
wire.Build(
NewDatabase,
NewUserRepository,
NewUserService,
NewHTTPServer,
NewApp,
)
return nil, nil
}
手動 DI(シンプルさを重視する場合に推奨)
// main.go
func main() {
cfg := config.Load()
db := database.Connect(cfg.DatabaseURL)
userRepo := postgres.NewUserRepository(db)
orderRepo := postgres.NewOrderRepository(db)
userService := application.NewUserService(userRepo)
orderService := application.NewOrderService(orderRepo, userRepo)
handler := api.NewHandler(userService, orderService)
server := http.NewServer(cfg.Port, handler)
server.Run()
}
エラー処理パターン
カスタムエラー型
// domain/errors.go
type Error struct {
Code string
Message string
Err error
}
func (e *Error) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *Error) Unwrap() error { return e.Err }
var (
ErrNotFound = &Error{Code: "NOT_FOUND", Message: "resource not found"}
ErrUserAlreadyExists = &Error{Code: "USER_EXISTS", Message: "user already exists"}
ErrInvalidInput = &Error{Code: "INVALID_INPUT", Message: "invalid input"}
)
設定管理
// config/config.go
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
}
func Load() (*Config, error) {
cfg := &Config{}
cfg.Server.Port = getEnvInt("PORT", 8080)
cfg.Server.ReadTimeout = getEnvDuration("READ_TIMEOUT", 30*time.Second)
cfg.Database.URL = mustGetEnv("DATABASE_URL")
cfg.Database.MaxConns = getEnvInt("DB_MAX_CONNS", 25)
return cfg, nil
}
ベストプラクティス
- ドメインを純粋に保つ - ドメインレイヤーにフレームワークへの依存関係を持ち込まない
- インターフェース分離 - 小さく、目的を絞ったインターフェースを使う
- 依存性逆転 - 具体的な実装ではなく、抽象に依存する
- 依存関係を明示する - コンストラクター経由で依存関係を渡す
- 早期に失敗させる - 境界で検証し、内部コードを信頼する
- 不正な状態を表現できないようにする - 型を使って不変条件を適用する
コメント
Markdown 対応