はじめに
Goで大規模なアプリケーションを開発するとき、複数のサービスや依存関係を効率的に管理する必要があります。本記事では、シンプルな コンポーネント管理フレームワーク を実装し、Goアプリケーションの 依存管理・起動・停止 を統一的に扱う方法について書きます。
1. コンポーネント管理
コンポーネント管理とは、アプリケーションの各機能(例: ロギング、データベース、HTTPサーバーなど)を モジュール化 し、動的に登録・起動・停止できるようにする設計手法 です。
これにより、以下のメリットが得られます。
- 依存関係の明確化: 各コンポーネントの関係性を明示的に管理
- 初期化の統一: すべてのコンポーネントを統一的に
Init()
する - 動的な管理: 追加・削除が容易で、テストがしやすい
- クリーンなシャットダウン:
Close()
を適切に呼び出し、リソースを確実に解放
2. コンポーネント管理フレームワークの設計
今回のフレームワークでは、以下の3種類のコンポーネントをサポートします。
インターフェース | 役割 |
---|---|
Component | すべてのコンポーネントが実装すべき最小限のインターフェース |
ComponentRunnable | バックグラウンドで動作するコンポーネント(例: HTTP サーバー) |
ComponentStatable | 状態変更に応じた処理が可能なコンポーネント |
3. Component インターフェースの定義
すべてのコンポーネントは、以下の Component
インターフェースを実装します。
package app
import "context"
// Component: すべてのコンポーネントが実装するべきインターフェース
type Component interface {
// アプリ起動時に最初に呼ばれる初期化処理
Init(a *App) error
// 一意のコンポーネント名を返す
Name() string
}
この Component インターフェースを実装することで、アプリの起動時に統一的な初期化 (Init()
) を適用できます。
4. ComponentRunnable: バックグラウンド処理の管理
ComponentRunnable
は Component
を継承し、Run()
と Close()
を実装することで バックグラウンド処理 を持つコンポーネントになります。
// ComponentRunnable: バックグラウンドプロセスを持つコンポーネント
type ComponentRunnable interface {
Component
// バックグラウンド処理の開始
Run(ctx context.Context) error
// シャットダウン処理
Close(ctx context.Context) error
}
例えば、HTTPサーバーを管理する HttpServer
を実装する場合、以下のようになります。
type HttpServer struct{}
func (s *HttpServer) Init(a *App) error {
fmt.Println("HTTPサーバーの初期化")
return nil
}
func (s *HttpServer) Name() string {
return "http-server"
}
func (s *HttpServer) Run(ctx context.Context) error {
fmt.Println("HTTPサーバーを起動")
go func() {
<-ctx.Done()
fmt.Println("HTTPサーバーをシャットダウン")
}()
return nil
}
func (s *HttpServer) Close(ctx context.Context) error {
fmt.Println("HTTPサーバーを停止")
return nil
}
5. App 構造体: コンポーネントを統括管理
アプリケーション全体を管理する App 構造体を実装します。
type App struct {
components []Component
mu sync.RWMutex
}
各コンポーネントを リスト (components
) に登録し、統一的に管理 します。
6. コンポーネントの登録 (Register()
)
App にコンポーネントを登録するための Register()
メソッドを実装します。
// Register: コンポーネントを登録する
func (app *App) Register(s Component) {
app.mu.Lock()
defer app.mu.Unlock()
// すでに登録されているかチェック
for _, es := range app.components {
if s.Name() == es.Name() {
panic(fmt.Errorf("コンポーネント '%s' はすでに登録済み", s.Name()))
}
}
app.components = append(app.components, s)
}
7. アプリケーションの起動 (Start())
すべてのコンポーネントを初期化し、バックグラウンド処理を開始します。
func (app *App) Start(ctx context.Context) error {
app.mu.Lock()
defer app.mu.Unlock()
// 1. すべてのコンポーネントを初期化
for _, s := range app.components {
if err := s.Init(app); err != nil {
return fmt.Errorf("コンポーネント '%s' の初期化に失敗: %w", s.Name(), err)
}
}
// 2. ComponentRunnable をバックグラウンドで実行
for _, s := range app.components {
if runnable, ok := s.(ComponentRunnable); ok {
go runnable.Run(ctx)
}
}
fmt.Println("アプリケーション起動完了")
return nil
}
8. アプリケーションの停止 (Close())
すべての ComponentRunnable を逆順で Close() し、リソースを適切に解放します。
func (app *App) Close(ctx context.Context) error {
app.mu.Lock()
defer app.mu.Unlock()
// 逆順で Close() を実行
for i := len(app.components) - 1; i >= 0; i-- {
if closeable, ok := app.components[i].(ComponentRunnable); ok {
closeable.Close(ctx)
}
}
fmt.Println("アプリケーション停止完了")
return nil
}
9. 実際に動かしてみる
コンポーネントの登録
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// アプリケーションを作成
app := &App{}
// コンポーネントを登録
app.Register(&HttpServer{})
// アプリを起動
if err := app.Start(ctx); err != nil {
fmt.Println("アプリ起動エラー:", err)
return
}
// 5秒後に停止
time.Sleep(5 * time.Second)
app.Close(ctx)
}
実行すると、以下のような出力が得られます。
HTTPサーバーの初期化
HTTPサーバーを起動
アプリケーション起動完了
HTTPサーバーを停止
アプリケーション停止完了
Go Playgroundでのお試し
以下のコードをGo Playgroundにコピーして動作確認できます。
Go Playgroundでの動作手順
- Go Playground にアクセス
- 上記のコードをコピー&ペースト
- Run をクリックして実行
package main
import (
"context"
"fmt"
"sync"
"time"
)
// Component: すべてのコンポーネントが実装するべきインターフェース
type Component interface {
Init(a *App) error
Name() string
}
// ComponentRunnable: バックグラウンドプロセスを持つコンポーネント
type ComponentRunnable interface {
Component
Run(ctx context.Context) error
Close(ctx context.Context) error
}
// App: コンポーネントを管理する
type App struct {
components []Component
mu sync.RWMutex
}
// Register: コンポーネントを登録する
func (app *App) Register(s Component) {
app.mu.Lock()
defer app.mu.Unlock()
app.components = append(app.components, s)
}
// Start: すべてのコンポーネントを初期化・実行する
func (app *App) Start(ctx context.Context) error {
app.mu.Lock()
defer app.mu.Unlock()
// すべてのコンポーネントを初期化
for _, s := range app.components {
if err := s.Init(app); err != nil {
return fmt.Errorf("コンポーネント '%s' の初期化に失敗: %w", s.Name(), err)
}
}
// ComponentRunnable をバックグラウンドで実行
for _, s := range app.components {
if runnable, ok := s.(ComponentRunnable); ok {
go runnable.Run(ctx)
}
}
fmt.Println("アプリケーション起動完了")
return nil
}
// Close: すべてのバックグラウンドコンポーネントを停止する
func (app *App) Close(ctx context.Context) error {
app.mu.Lock()
defer app.mu.Unlock()
for _, s := range app.components {
if closeable, ok := s.(ComponentRunnable); ok {
closeable.Close(ctx)
}
}
fmt.Println("アプリケーション停止完了")
return nil
}
// HttpServer: HTTP サーバーの模擬コンポーネント
type HttpServer struct{}
func (s *HttpServer) Init(a *App) error {
fmt.Println("HTTPサーバーの初期化")
return nil
}
func (s *HttpServer) Name() string {
return "http-server"
}
func (s *HttpServer) Run(ctx context.Context) error {
fmt.Println("HTTPサーバーを起動")
go func() {
<-ctx.Done()
fmt.Println("HTTPサーバーをシャットダウン")
}()
return nil
}
func (s *HttpServer) Close(ctx context.Context) error {
fmt.Println("HTTPサーバーを停止")
return nil
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// アプリケーションを作成
app := &App{}
// コンポーネントを登録
app.Register(&HttpServer{})
// アプリを起動
if err := app.Start(ctx); err != nil {
fmt.Println("アプリ起動エラー:", err)
return
}
// 5秒後に停止
time.Sleep(5 * time.Second)
app.Close(ctx)
}
HTTPサーバーの初期化
HTTPサーバーを起動
アプリケーション起動完了
HTTPサーバーを停止
アプリケーション停止完了