Menu
Coddy logo textTech

Singleton パターン

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 86/107。

シングルトンパターンは、プログラム全体である型がインスタンスを1つしか持たないことを保証し、それへのグローバルなアクセスポイントを提供します。これは、設定マネージャー、データベース接続、ロガーなどの共有リソースに役立ちます。

Goでは、スレッドセーフな初期化を保証するために、パッケージレベルの変数と sync.Once を組み合わせてシングルトンを実装します。

package config

import "sync"

type Config struct {
    DatabaseURL string
    MaxRetries  int
}

var (
    instance *Config
    once     sync.Once
)

func GetInstance() *Config {
    once.Do(func() {
        instance = &Config{
            DatabaseURL: "localhost:5432",
            MaxRetries:  3,
        }
    })
    return instance
}

sync.Once は、複数のゴルーチンが同時に GetInstance() を呼び出した場合でも、初期化関数が正確に一度だけ実行されることを保証します。それ以降のすべての呼び出しは、初期化コードを再実行することなく、同じインスタンスを返します。

cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 と cfg2 は同一のインスタンスを指します

Singleton(シングルトン)は控えめに使用してください。便利な反面、グローバルな状態を導入するため、テストが困難になったり、依存関係が隠れたりする可能性があります。あなたのユースケースにおいて、依存性の注入(dependency injection)がより良い代替案にならないか検討してください。

challenge icon

チャレンジ

簡単

アプリケーション全体でロガーのインスタンスが1つしか存在しないことを保証する、Loggerシングルトンを構築しましょう!これはシングルトンパターンの典型的なユースケースです。プログラムのすべての部分で、同じロガーの設定と状態を共有したい場合に適しています。

コードは2つのファイルに分けて構成します:

  • logger.go: スレッドセーフなシングルトンロガーを実装します。

    2つのフィールドを持つ Logger 構造体を作成します:ログメッセージの接頭辞のための Prefix (string) と、これまでにログに記録されたメッセージ数を追跡するための MessageCount (int) です。

    sync.Once を使用したパッケージレベルの変数を使用して、インスタンスが1つしか作成されないようにします。デフォルトの接頭辞を "[LOG]" としてロガーを初期化し、シングルトンインスタンスを返す GetLogger() 関数を実装します。

    Loggerに以下のメソッドを追加します:

    • SetPrefix(prefix string) - ロガーの接頭辞を変更します
    • Log(message string) string - メッセージカウントをインクリメントし、フォーマットされた文字列 [prefix] #[count]: [message] を返します
    • GetCount() int - ログに記録されたメッセージの総数を返します
  • main.go: GetLogger() を複数回呼び出しても同じインスタンスが返されることを実証します。

    新しい接頭辞の値を読み取り、次にメッセージの数、続いてログに記録する各メッセージを読み取ります。

    ロガーインスタンスを取得し、カスタム接頭辞を設定してから、各メッセージをログに記録して結果を出力します。すべてのメッセージをログに記録した後、再度ロガーインスタンスを取得し(同じものであることを証明するため)、メッセージの総数を出力します。

以下の入力が提供されます:

  • 1行目:設定するカスタム接頭辞
  • 2行目:メッセージの数
  • 続く行:ログに記録する各メッセージ

例えば、以下が与えられた場合:

[APP]
3
Server started
User connected
Request processed

出力は以下のようになります:

[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3

また、以下が与えられた場合:

[DEBUG]
2
Initializing cache
Cache ready

出力は以下のようになります:

[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2

また、以下が与えられた場合:

[ERROR]
1
Connection failed

出力は以下のようになります:

[ERROR] #1: Connection failed
Total messages logged: 1

重要なポイントは、GetLogger() を何度呼び出しても、共有された状態を持つ同じインスタンスが常に取得されるということです。ロガーは世界に1つしか存在しないため、メッセージカウントは維持されます!

チートシート

Singleton パターンは、プログラム全体で特定の型のインスタンスが1つだけであることを保証し、それに対するグローバルなアクセスポイントを提供します。これは、設定マネージャー、データベース接続、ロガーなどの共有リソースに役立ちます。

スレッドセーフな初期化のために、パッケージレベルの変数と sync.Once を組み合わせて Singleton を実装します。

package config

import "sync"

type Config struct {
    DatabaseURL string
    MaxRetries  int
}

var (
    instance *Config
    once     sync.Once
)

func GetInstance() *Config {
    once.Do(func() {
        instance = &Config{
            DatabaseURL: "localhost:5432",
            MaxRetries:  3,
        }
    })
    return instance
}

sync.Once は、複数のゴルーチンが同時に GetInstance() を呼び出した場合でも、初期化関数が一度だけ実行されることを保証します。それ以降のすべての呼び出しは、同じインスタンスを返します。

cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 and cfg2 point to the same instance

Singleton はグローバルな状態を導入するため、テストを困難にしたり依存関係を隠したりする可能性があります。控えめに使用してください。代替案として依存性の注入(Dependency Injection)を検討してください。

自分で試してみよう

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// カスタムプレフィックスを読み込む
	prefix, _ := reader.ReadString('\n')
	prefix = strings.TrimSpace(prefix)

	// メッセージの数を読み込む
	countStr, _ := reader.ReadString('\n')
	countStr = strings.TrimSpace(countStr)
	count, _ := strconv.Atoi(countStr)

	// 各メッセージを読み込む
	messages := make([]string, count)
	for i := 0; i < count; i++ {
		msg, _ := reader.ReadString('\n')
		messages[i] = strings.TrimSpace(msg)
	}

	// TODO: GetLogger() を使用してロガーインスタンスを取得する

	// TODO: SetPrefix() を使用してカスタムプレフィックスを設定する

	// TODO: 各メッセージをログに記録し、結果を出力する

	// TODO: 再度ロガーインスタンスを取得し(同じものであることを証明するため)、
	// 合計メッセージ数を次の形式で出力する: "Total messages logged: X"
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン