-
go generate
を調べたり試したりしてみて、「これを使って柔軟且つpluggableに機能拡張/追加出来る仕組み作れないかな?」と考えていたところ、既にそれっぽい事を実現してくれているパッケージがあった。 - こちらの progrium/go-extpoints。
- 使ってみたら、個人的にドストライクなツボを抑えてくれていたので、お試しメモとして残しておく。
- なお、
go generate
を使用するので、goのバージョンは1.4(以上)が前提。
go-extpointsをインストール
普通に go install
するだけ。
$ go install github.com/progrium/go-extpoints
試してみる
使ったソースは こちら
ざっくり、こういう流れで実装していくことになる。
※(以降の記述では、公式で"extension point"とか"component"と言及されている物については分かりやすいように"プラグイン"という言葉を使う)
-
extpoints
サブパッケージ下に、プラグインのinterfaceを作る -
//go:generate
だけ行うgoファイルを作る(cliアプリなら mainパッケージ下 とかに) -
go generate
する - プラグインを使用するクライアント処理を書く(cliアプリなら mainパッケージ下 とかに)
- プラグインのinterfaceの実装を書く(cliアプリなら mainパッケージ下 とかに、作りたい数だけ作る)
extpoints
サブパッケージ下に、プラグインのinterfaceを作る
-
extpoints
( この名前でなければならない )サブパッケージを作成し、そこに提供する機能のinterfaceを定義する。例として事前処理・主処理・事後処理、という形の処理を行うinterfaceを定義してみる。
extpoints/interfaces.go
package extpoints
// 提供する機能のinterface群を定義する(1つ or 複数)
type Hoge interface {
Before(args []string) error // 事前処理
Run(args []string) error // 主処理
After(args []string) error // 事後処理
String() string
}
//go:generate
だけ行うgoファイルを作る
- go generateお決まりのコメントフォーマット
//go:generate <実行したいコマンド>
を記述。コマンドにはgo-extpoints
コマンドを指定する。 - 今回のような単純なcliアプリなら
main.go
の中に//go:generate
も書いちゃえばいいよね?と一瞬思った 。- が、プラグインのinterfaceとその具象実装 "だけ" を公開するようなプロジェクトを作りたいケースとかもありそう。
- そういうケースだとmain.goのようなクライアント・ソースは存在しないよね。
- なので、今回のケースでは冗長とは思いつつ個人的に「今後のベターなお作法」となる気がしているこの形にしておいた。
extensions.go
//go:generate go-extpoints
package main
go generate
する
-
go generate
すると上記extensions.go
が処理される -
extpoints/interfaces.go
で定義したinterfaceをパースし、結果がextpoints
サブパッケージ下にextpoints/extpoints.go
というgoファイルで生成される- 生成時に使われるテンプレートは go-extpoints/template.go
$ go generate
extpoints: Processing file extpoints/interfaces.go
extpoints: Found interfaces: []string{"Hoge"}
extpoints: Writing file extpoints/extpoints.go
- 生成された
extpoints/extpoints.go
のpublicな関数・メソッドを中心に主要機能を抜粋して内容を見てみると、プラグインの参照・追加・削除を行う処理が定義されている。
generateで生成されたextpoints/extpoints.go
// generated by go-extpoints -- DO NOT EDIT
package extpoints
:
// プラグインを登録する関数
func Register(component interface{}, name string) []string {
:
}
// プラグインを削除する関数
func Unregister(name string) []string {
:
}
// Hoge
// 定義したinterface(の複数形) の変数をexportしている
// 実体は "該当interfaceをextensionPoint型でラップしたオブジェクト" になっている。
// この変数がgo-exportsのキモで、プラグインを利用するクライアント側は、この変数経由でメタAPIを呼び出して追加・削除・実行等を行う
var Hoges = &hogeExt{
newExtensionPoint(new(Hoge)), // newExtensinonPointの処理内容は本家のソースを参照
}
type hogeExt struct {
*extensionPoint
}
// プラグインを登録するメソッド
func (ep *hogeExt) Register(component Hoge, name string) bool {
:
}
// プラグインを削除するメソッド
func (ep *hogeExt) Unregister(name string) bool {
:
}
// 登録時のnameをキーに、プラグインのオブジェクトを取得するメソッド
func (ep *hogeExt) Lookup(name string) (Hoge, bool) {
:
}
// 登録済のプラグインオブジェクト全てを、nameをキーにしたmap型で取得するメソッド
func (ep *hogeExt) All() map[string]Hoge {
:
}
プラグインを使用するクライアント処理を書く
-
Before
,Run
,After
を順に呼び出すだけのクライアントを実装してみる。
main.go
package main
import (
"fmt"
"os"
"github.com/goldeneggg/geggg-go-extpoint-example/extpoints"
)
var (
hoges = extpoints.Hoges // extpoints.Hogesは、generateで生成したメタAPIアクセス用の変数
)
func main() {
var s int
defer os.Exit(s)
// 登録済のプラグインを All メソッドで確認
// プラグイン未実装の初期状態では何も出力されない
for _, registered := range hoges.All() {
fmt.Println("Registered object: ", registered)
}
args := os.Args[1:]
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "Required name argument")
s = 1
return
}
// 登録済のHoge型のプラグインを Lookup メソッドで取得
// nameには引数で指定した内容を使う
// プラグイン未実装の初期状態では、okがfalseとなり終了
// (するはずだが、本家の処理でnilチェックが漏れてる箇所があって期待通りには動かない。pull-reqするか。。。)
name := args[0]
hoge, ok := hoges.Lookup(name)
if !ok {
fmt.Fprintf(os.Stderr, "Name %s is not registered\n", name)
s = 1
return
}
// Before, Run, Afterを順次実行
if err := hoge.Before(args); err != nil {
fmt.Fprintln(os.Stderr, "Before error", err)
s = 1
return
}
if err := hoge.Run(args); err != nil {
fmt.Fprintln(os.Stderr, "Run error", err)
s = 1
return
}
if err := hoge.After(args); err != nil {
fmt.Fprintln(os.Stderr, "After error", err)
s = 1
return
}
}
- この段階で実行しても、まだプラグインを登録していないので何も起こらない。
$ go run *.go
Required name argument
$ go run *.go nothing
# (本来ならココで "Name nothing is not registered" が出力されて欲しいが、、、)
プラグインのinterfaceの実装を書く
- ポイントは、定義したinterfaceを実装する事 と、
init
関数内でメタAPIのRegister
メソッドを呼んでその処理の登録を行う 事。- (
Register
関数 の方(func Register
)は、後の例で使う場面が出てくるがココではスルー)
- (
gekiteki.go
package main
import "fmt"
func init() {
// main.goで定義した hoges 変数経由でメタAPIの Register メソッドを実行してプラグインを登録する
// 第1引数に具象型(の参照)を、第2引数で名前を指定する(省略時は型と同じ名前で登録される)
hoges.Register(new(gekiteki), "")
}
type gekiteki struct{}
var _ extpoints.Hoge = new(gekiteki)
func (g *gekiteki) Before(args []string) error {
fmt.Println("ビフォー")
}
func (g *gekiteki) Run(args []string) error {
fmt.Println("匠の劇的な技!")
}
func (g *gekiteki) After(args []string) error {
fmt.Println("アフター")
}
func (g *gekiteki) String() string {
return "劇的!ビフォーアフター"
}
- 実装した型名を引数で指定して実行すると、登録したプラグインの処理が呼び出される。
% go run *.go gekiteki
Registered object: 劇的!ビフォーアフター
ビフォー
匠の劇的な技!
アフター
- 別のプラグイン "xyz" を追加してみて(ソースは省略)実行するとこうなる
% go run *.go xyz
Registered object: 劇的!ビフォーアフター
Registered object: I am xyz
Exec Before
Exec Run
Exec After
こんな具合に、一度クライアント側を実装してしまえば、以降はプラグインのinterfaceを実装した型を追加するだけで機能追加が容易に行える( という開発の流れを促進してくれる )のが go-extpoints。
上記は「引数で処理を切り替える」というよくあるCMD SUBCMD
形式の実装例だが、こうしたアプリの実装にgo-extpointsは向いていると思う。
外部のプロジェクトで定義したプラグインを使いたい
例えば additional という 別プロジェクト(パッケージ)で実装された同じinterfaceのプラグインを、既存クライアントから使いたい というケースが出てきそう。
先ほど作ったHoge
インタフェースを使って、こういうケースでの対応を試してみる。
- 新たに作成するプラグイン追加用プロジェクト(パッケージ)側で、既存interface
Hoge
を実装した型を作る。 - 既存と同様に、そのソースの
init
関数内でRegister
を実行してプラグインを登録する。したい。。- しかし、 既存プロジェクトで使っていたメタAPIアクセス用変数(
hoges
)はこのパッケージからは見えなくてRegister
を呼び出せない。
- しかし、 既存プロジェクトで使っていたメタAPIアクセス用変数(
- ここで、 既存プロジェクトの
extpoints
パッケージ直下で、インスタンスを経由せず直接実行する為に定義されたRegister
関数 が使える。この関数はこのような "外部プロジェクトで定義したプラグインを登録する" 為に用意されていた関数だった、というわけ。
additionalextpoint/additional.go
package additionalextpoint
import (
"fmt"
"github.com/goldeneggg/geggg-go-extpoint-example/extpoints"
)
func init() {
// Register "関数" を使ってプラグインを登録
extpoints.Register(new(additional), "")
}
type additional struct{}
var _ extpoints.Hoge = new(additional)
func (a *additional) Before(args []string) error {
fmt.Println("Additional Before")
return nil
}
func (a *additional) Run(args []string) error {
fmt.Println("Additional Run")
return nil
}
func (a *additional) After(args []string) error {
fmt.Println("Additional After")
return nil
}
func (a *additional) String() string {
return "I am additional"
}
- リモートに存在するプロジェクトであれば、まず
go install PACKAGE
する。
$ go install github.com/goldeneggg/additionalextpoint
- 追加したプラグインを使いたいクライアントプログラム側では
import _ PACKAGE
するだけで良い。
main.go
package main
import (
"fmt"
"os"
"github.com/goldeneggg/geggg-go-extpoint-example/extpoints"
// 追加したいプラグインのパッケージをブランク識別子付きでimport
_ "github.com/goldeneggg/additionalextpoint"
)
:
:
- 外部プラグインの名前 "additional" を指定して実行 → OK。
% go run *.go additional
Registered object: I am additional
Registered object: 劇的!ビフォーアフター
Registered object: I am xyz
Additional Before
Additional Run
Additional After