💬

GoでのAOP(aspect oriented programming)的なことを実現する方法

に公開

概要

タイトルどおり、Go言語でAOP(aspect oriented programming)的なことを実現する方法を考えてみました。
JavaでSpringフレームワークで実現されているAOPやpythonのデコレータをイメージしていただけたらと思います。

具体的な実現方法

思いついた方法は以下の3つでした。

  • ラッパー関数で実現
  • リフレクションで実現
  • ツールで実現

以下で処理の前後にログを差し挟む、という処理を実現する方法を検証してみました。

ラッパー関数で実現

機能を付与したい構造体と同じinterfaceを満たす実装で実現する。
一番シンプルだが、機能を付与したい構造体の全てのinterfaceを満たすのは面倒であまり実用的でないように思う。

package main

import "fmt"

func main() {
	w := WrapProcess(NewProcess())
	w.Execute()
	// Output:
	//   Before
	//   Executing process...
	//   After
}

// 実現したい処理
type Process interface {
	Execcute()
}

func NewProcess() Process {
	return &process{}
}

type process struct{}

func (p *process) Execcute() {
	fmt.Println("Executing process...")
}

// wrapper
type Wrapper struct {
	wrappedProcess Process
}

func WrapProcess(p Process) Wrapper {
	return Wrapper{
		wrappedProcess: p,
	}
}

func (w *Wrapper) Execute() {
	fmt.Println("Before")
	w.wrappedProcess.Execcute()
	fmt.Println("After")
}

リフレクションで実現

リフレクションを使用することで上記のラッパー関数を用意する方法に比べたら、どこにでも適用しやすい分実用的だとは思う。
少なくともpythonとデコレータ相当のことはこれで実現できそう。
ただし、リフレクションを使用しているので毎回呼び出される場合のオーバーヘッドが許容できるかどうかは作成するアプリによる。

package main

import (
	"fmt"
	"reflect"
)

func wrap[T any](fn T) T {
	rfn := reflect.ValueOf(fn)

	// 関数でない場合はpanic
	if rfn.Kind() != reflect.Func {
		panic("fn should be a function")
	}

	// 同じ引数・戻り値を持つ関数を作成
	wfn := reflect.MakeFunc(rfn.Type(), func(args []reflect.Value) []reflect.Value {
		fmt.Println("before")
		result := rfn.Call(args)
		fmt.Println("after")
		return result
	})
	return wfn.Interface().(T)
}

func main() {
	wrap(display)()      // こんにちは、世界!
	wrap(greet)("山田")   // こんにちは、山田さん! 
	wrapedAdd := wrap(add)
	fmt.Println("result", wrapedAdd(5, 3)) // 8
}

func display() {
	fmt.Println("こんにちは、世界!")
}

func greet(name string) {
	fmt.Printf("こんにちは、%sさん!\n", name)
}

func add(a, b int) int {
	return a + b
}

参考にした記事

https://zenn.dev/youta_t/articles/e73b78733cbbec

ツールで実現

ライブラリやツールがないかと探し出したところ、gowrapというコード生成ツールがあり、使えそうだった。

https://github.com/hexdigest/gowrap

このツールは横断的に適用したい処理をテンプレートとして記述し、適用対象のinterfaceをCLIツールで指定してコード生成するツール。
詳しい使い方は以下に記述。

使った感想としては本格的なアプリを作る場合、上記の2つの手段に比べて一番実用的に感じた。
ただ、最初にテンプレート用の独自の関数を知っておく必要があるが、それについては特にキチンとしたドキュメントが用意されているわけではない為、最初はテンプレート作成に苦労するかも。

使い方

インストール

CLIとしていストールしたかったので下記のコマンドを実行。

go install github.com/hexdigest/gowrap/cmd/gowrap@latest

テンプレート作成

gowrapにはすでにいくつかのテンプレートが作成されていたので、それを元に下記のようなテンプレートを作成。
元々の処理の呼び出し前後で"Before"と"After"を出力するだけのシンプルな処理のテンプレート。

./gowrap/log_template.txt

import (
  "io"
  "fmt"
)

{{ $decorator := (or .Vars.DecoratorName (printf "%sWithLog" .Interface.Name)) }}

// {{$decorator}} implements {{.Interface.Type}} that is instrumented with logging
type {{$decorator}} struct {
  _base {{.Interface.Type}}
}

// New{{$decorator}} instruments an implementation of the {{.Interface.Type}} with simple logging
func New{{$decorator}}(base {{.Interface.Type}}, stdout, stderr io.Writer) {{$decorator}} {
  return {{$decorator}}{
    _base: base, 
  }
}

{{range $method := .Interface.Methods}}
  // {{$method.Name}} implements {{$.Interface.Type}}
  func (_d {{$decorator}}) {{$method.Declaration}} {
      fmt.Println("Before")
      {{- if $method.HasResults}}
        {{$method.ResultsNames}} = _d._base.{{$method.Call}}
        fmt.Println("After")
        return {{$method.ResultsNames}}
      {{else}} 
        _d._base.{{$method.Call}}
        fmt.Println("After")
      {{end}} 
  }
{{end}}

テンプレートは正直、慣れないとわかりづらい部分があると思うが、以下のページと既存テンプレートを見ることでなんとなくわかると思う。

https://pkg.go.dev/github.com/hexdigest/gowrap/generator#pkg-types

生成元になるinterfaceを定義

./gowrap/main.go

package main

type Process interface {
	Execute1()
	Execute2() string
}

テンプレートとinterfaceを元にコード生成

以下のコマンドでいよいよコード生成する。

gowrap gen -p ./gowrap -i Process -t ./gowrap/log_template.txt -o ./gowrap/log_process.go

できたコードは以下のようになる。

// Code generated by gowrap. DO NOT EDIT.
// template: log_template.txt
// gowrap: http://github.com/hexdigest/gowrap

package main

//go:generate gowrap gen -p github.com/miyazi777/test1/gowrap -i Process -t log_template.txt -o log_process.go -l ""

import (
	"fmt"
	"io"
)

// ProcessWithLog implements Process that is instrumented with logging
type ProcessWithLog struct {
	_base Process
}

// NewProcessWithLog instruments an implementation of the Process with simple logging
func NewProcessWithLog(base Process, stdout, stderr io.Writer) ProcessWithLog {
	return ProcessWithLog{
		_base: base,
	}
}

// Execute1 implements Process
func (_d ProcessWithLog) Execute1() {
	fmt.Println("Before")
	_d._base.Execute1()
	fmt.Println("After")

}

// Execute2 implements Process
func (_d ProcessWithLog) Execute2() (s1 string) {
	fmt.Println("Before")
	s1 = _d._base.Execute2()
	fmt.Println("After")
	return s1

}

参考

gowrapを使う際に参考した記事。

https://www.wantedly.com/companies/wantedly/post_articles/195312

その他

実をいうともう1つ方法があるにはある。
コンパイル時に-toolexecオプションを使用することでも同様のことは実現できるっぽい。
実現方法も一番スマートだと思う。
こんなライブラリも存在するぐらい。

https://github.com/dengsgo/go-decorator

ただし、-toolexecオプション自体に関する情報がなさすぎて、これを使っていいのかどうか。
個人的なプロジェクトで使用する分には全然良いけど、商用のプロダクションコードで使用するには躊躇するぐらい情報がなかった。

Discussion