certimateはSSL証明書の発行・デプロイ・更新を自動化するセルフホスト型ツールです。ファイル一覧には internal/certacme/certifiers/ 配下に sp_cloudflare.go, sp_aws_route53.go など80個以上のDNSプロバイダー用ファイルが並んでいます。これだけの数の「同じ形をした部品」をif文やswitch文の羅列で管理すると読みにくくなります。それを防ぐために使われているのがこのregistry.goの「ジェネリクスを使った登録レジストリ」パターンです。同じ形の実装が大量にある場面は1年目でも必ず出会うので教材に選びました。

コード

type ProviderFactoryFunc func(options *ProviderFactoryOptions) (core.ACMEChallenger, error)

// …略…

type Registry[T comparable] interface {
	Register(T, ProviderFactoryFunc) error
	RegisterAlias(T, T) error
	MustRegister(T, ProviderFactoryFunc)
	MustRegisterAlias(T, T)
	Get(T) (ProviderFactoryFunc, error)
}

type registry[T comparable] struct {
	factories map[T]ProviderFactoryFunc
}

func (r *registry[T]) Register(name T, factory ProviderFactoryFunc) error {
	if _, exists := r.factories[name]; exists {
		return fmt.Errorf("provider '%v' already registered", name)
	}

	r.factories[name] = factory
	return nil
}

func (r *registry[T]) RegisterAlias(name T, alias T) error {
	factory, err := r.Get(alias)
	if err != nil {
		return err
	}

	err = r.Register(name, factory)
	if err != nil {
		return err
	}

	return nil
}

func (r *registry[T]) MustRegister(name T, factory ProviderFactoryFunc) {
	if err := r.Register(name, factory); err != nil {
		panic(err)
	}
}

// …略…

func (r *registry[T]) Get(name T) (ProviderFactoryFunc, error) {
	if factory, exists := r.factories[name]; exists {
		return factory, nil
	}

	return nil, fmt.Errorf("provider '%v' not registered", name)
}

func newRegistry[T comparable]() Registry[T] {
	return &registry[T]{factories: make(map[T]ProviderFactoryFunc)}
}

var (
	ACMEDns01Registries  = newRegistry[domain.ACMEDns01ProviderType]()
	ACMEHttp01Registries = newRegistry[domain.ACMEHttp01ProviderType]()
)

引用はリポジトリの実物と機械で照合しています。「// …略…」は省略した行です。

上から順に読む

type ProviderFactoryFunc func(options *ProviderFactoryOptions) (core.ACMEChallenger, error)

Goでは関数そのものに型をつけられます。この行は「ProviderFactoryFuncという名前は、*ProviderFactoryOptions(構造体へのポインタ。*は『このアドレスにある値を指す』という意味)を1つ受け取り、core.ACMEChallengerとerrorの2つを返す関数」だという型エイリアスです。以降、長い関数シグネチャを毎回書かずに「ProviderFactoryFunc型の値」として関数を変数や引数に渡せます。Goの関数は変数に代入したり引数として渡したりできる値(第一級オブジェクト)なので、こういう『関数を作る関数(ファクトリ)』の型を定義するのはよくあるパターンです。

type Registry[T comparable] interface {

`[T comparable]`はGoのジェネリクス(型引数)の書き方です。`T`はこの型の中で使う仮の型名で、呼び出し側が使うときに実際の型(例えばstringなど)に置き換わります。`comparable`は「T型は`==`や`!=`で比較できる型でなければならない」という制約です。map(辞書)のキーにするには比較可能である必要があるため、この制約がついています。`interface`は「これらのメソッドを持っていること」だけを約束し、実装の詳細を外部に隠すためのGoの型です。

Register(T, ProviderFactoryFunc) error
	RegisterAlias(T, T) error
	MustRegister(T, ProviderFactoryFunc)

インターフェース内のメソッド宣言は、引数名を省略して型だけを書けます(例:`Register(T, ProviderFactoryFunc) error`は「T型とProviderFactoryFunc型の引数を受け取り、errorを返すRegisterというメソッドを持つこと」の宣言)。これを実装する型は、Registryが持つ5つのメソッドすべてを同じシグネチャで持って初めて「Registry[T]を満たしている」とみなされます。

type registry[T comparable] struct {
	factories map[T]ProviderFactoryFunc
}

先頭が大文字の`Registry`(インターフェース)に対し、こちらは小文字始まりの`registry`(構造体)です。Goでは識別子の先頭が大文字だとパッケージ外からも見える(exported)、小文字だとパッケージ内限定(unexported)というルールがあります。外部のコードは`Registry`インターフェース経由でしか操作できず、内部実装の`registry`構造体やそのフィールド`factories`(プロバイダー名から生成関数を引く辞書。`map[キーの型]値の型`という書き方)には直接触れません。これがカプセル化です。

func (r *registry[T]) Register(name T, factory ProviderFactoryFunc) error {
	if _, exists := r.factories[name]; exists {

`(r *registry[T])`はレシーバといい、「このメソッドは*registry[T]型(registry構造体へのポインタ)に対して呼び出せる」ことを示します。ポインタレシーバを使うのは、メソッド内でフィールドを書き換えたいからです(値レシーバだとコピーへの変更になり、呼び出し元には反映されません)。`if _, exists := r.factories[name]; exists {`はmapから値を取り出す『カンマOKイディオム』で、1つ目の戻り値(ここでは`_`で捨てている)がキーに対応する値、2つ目`exists`がキーが存在したかを示すbool値です。

return fmt.Errorf("provider '%v' already registered", name)
	}

	r.factories[name] = factory
	return nil
}

すでに同じ名前が登録済みなら`fmt.Errorf`でエラーメッセージを作って返します。`%v`は値をデフォルト形式で文字列化する書式指定子です。まだ登録されていなければ`r.factories[name] = factory`でmapに新しいキーと値を書き込み、最後に`return nil`で『エラーなし』を表して処理を終えます。

func (r *registry[T]) RegisterAlias(name T, alias T) error {
	factory, err := r.Get(alias)
	if err != nil {
		return err
	}

『alias(別名)を実際の名前に対応させる』メソッドです。まず`r.Get(alias)`で既存の生成関数を取得し、エラーがあればそこで打ち切って呼び出し元に伝えます。`if err != nil { return err }`はGoで最も頻出するエラーハンドリングの型で、`err`にエラーが入っていたら即座にreturnして後続の処理を実行しません。

err = r.Register(name, factory)
	if err != nil {
		return err
	}

	return nil
}

取得できた`factory`を新しい`name`として登録し直します。`err =`が`:=`ではなく`=`である点に注目してください。`err`はすでに直前で`:=`により宣言済みなので、ここでは再宣言せず既存の変数に上書き代入しています。どちらのエラーチェックも通過したら最後に`return nil`で成功を表します。

func (r *registry[T]) MustRegister(name T, factory ProviderFactoryFunc) {
	if err := r.Register(name, factory); err != nil {
		panic(err)
	}
}

Go標準ライブラリの流儀では『失敗したら回復不能なのでプログラムを止める』バージョンの関数に`Must`という接頭辞をつける慣習があります(`regexp.MustCompile`が有名)。ここでは通常の`Register`が返すエラーを`panic(err)`でプログラムをクラッシュさせる形に変換しています。`if err := r.Register(...); err != nil`はif文の条件式の前にセミコロンで区切って変数を宣言できるGo特有の構文で、`err`のスコープをこのif文の中だけに限定します。

func (r *registry[T]) Get(name T) (ProviderFactoryFunc, error) {
	if factory, exists := r.factories[name]; exists {
		return factory, nil
	}

ここでも『カンマOKイディオム』が使われていますが、今度は取り出した値`factory`を実際に使っています。見つかれば`(factory, nil)`という『値とerrorのペアを返す』Goの定番パターンでそのまま返します。呼び出し側は2つ目の戻り値(error)を必ずチェックしてから1つ目の値を使うことが期待されています。

func newRegistry[T comparable]() Registry[T] {
	return &registry[T]{factories: make(map[T]ProviderFactoryFunc)}
}

レジストリを作るコンストラクタです。戻り値の型が具体的な`*registry[T]`ではなくインターフェースの`Registry[T]`になっている点が重要で、呼び出し側はインターフェースが約束するメソッドしか使えないため内部実装を後から変更しても影響しません。`&registry[T]{...}`は構造体リテラルの前に`&`をつけて『その構造体のポインタ』を作る書き方です。`make(map[T]ProviderFactoryFunc)`はmapを使える状態で初期化する組み込み関数で、これを呼ばずに`var m map[T]F`だけだとnil mapになり書き込み時にパニックします。

var (
	ACMEDns01Registries  = newRegistry[domain.ACMEDns01ProviderType]()
	ACMEHttp01Registries = newRegistry[domain.ACMEHttp01ProviderType]()
)

パッケージレベルの`var`ブロックで、プログラム起動時(パッケージが読み込まれるタイミング)にこの2つの変数が初期化されます。`newRegistry[domain.ACMEDns01ProviderType]()`は、先ほど定義したジェネリック関数`newRegistry`に`[domain.ACMEDns01ProviderType]`という具体的な型を指定して呼び出している箇所です。これにより『DNS-01用のプロバイダーだけを集めたレジストリ』と『HTTP-01用のプロバイダーだけを集めたレジストリ』という、型引数だけが違う2つの独立したレジストリが1つずつ作られます。各`sp_*.go`ファイルは起動時にこの`ACMEDns01Registries`などへ自分自身を登録します。

← 2026.09.01 の号を読む