モンキーパッチというのは,オリジナルのソースコードを変更せずに実行時にコードを拡張したり変更したりする手法のことです.動的言語で見かけるようなモンキーパッチを golang でもやってのけることができます(って,そういうライブラリを作っちゃった人がいます!
手法については,ライブラリの作者による詳説をご覧ください.
ざっくり言うと,メソッドの開始アドレスをすげ替えて他のメソッド呼び出すようにするという感じのものです.
なので,
- セキュリティ厳しいOSでは動かない.
- スレッドセーフじゃないし,そもそも何もセーフじゃない.
ということにご注意ください.
動かなくても文句は言わない.ただし,時々動作がおかしくなるようなら go test -gcflags=-l のようにフラグをつけてみるといいかもしれません.
かなり黒魔術ですが,テストでどうしてもモックに置き換えたいときとか使えるかもしれません.
インストール
go get github.com/bouk/monkey
使い方
置き換える前のメソッドと,置き換えたいメソッドを以下のように指定するだけ.ただし,インターフェースはそろえましょう.インターフェースそろってないと実行時に panic します.
monkey.Patch(<target function>, <replacement function>)
元に戻すには
monkey.Unpatch(<target function>)
monkeypatchをあてたすべてのメソッドを元に戻したいときは
monkey.UnpatchAll()
を呼べばいいです.
以下のサンプルは,ライブラリの解説に書いてあったものを少しだけいじったものです.
サンプル1
fmt.Printlnを呼ぶとかわりにmyPrintlnが呼ばれる.hell という文字列を印字しようとしたら「ピー」を入れます.
package main
import (
"fmt"
"os"
"strings"
"github.com/bouk/monkey"
)
func myPrintln(a ...interface{}) (n int, err error) {
s := make([]interface{}, len(a))
for i, v := range a {
s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
}
return fmt.Fprintln(os.Stdout, s...)
}
func main() {
monkey.Patch(fmt.Println, myPrintln)
fmt.Println("what the hell, hell, hell ?")
monkey.Unpatch(fmt.Println)
fmt.Println("what the hell, hell, hell ?")
}
output:
$ go run sample1.go what the *bleep*, *bleep*, *bleep* ? what the hell, hell, hell ?
1回目の呼び出しは,hell が ピー (*bleep*)でおきかえられてます.
2回目の呼び出しは元に戻ってます.
サンプル2
インスタンスのメソッドを置き換えるパターン.ただし,今は特定のインスタンスのメソッドを置き換えることは出来ない.このパターンだと,1回だけ置き換えて綺麗に元に戻すというのが簡単にできる.(defer するメソッドを間違えないこと.本家のサンプルが間違っててはまった)
package main
import (
"fmt"
"net/http"
"reflect"
"strings"
"github.com/bouk/monkey"
)
func main() {
var guard *monkey.PatchGuard
guard = monkey.PatchInstanceMethod(
reflect.TypeOf(http.DefaultClient), "Get",
func(c *http.Client, url string) (*http.Response, error) {
defer guard.Unpatch()
guard.Restore()
if !strings.HasPrefix(url, "https://") {
return nil, fmt.Errorf("only https requests allowed")
}
return c.Get(url)
})
_, err := http.Get("http://google.com")
fmt.Println(err) // only https requests allowed
resp, err := http.Get("https://google.com") // https
fmt.Println(resp.Status, err) // 200 OK <nil>
resp, err = http.Get("http://google.com") // http
fmt.Println(resp.Status, err) // 200 OK <nil>
output:
$ go run sample2.go only https requests allowed 200 OK <nil> 200 OK <nil>
最初の呼び出しは,メソッドが置き換えられてて警告される.それ以降は元のメソッドが呼び出されているのが分かる.ちょっとわかりにくいけど,これはグローバルに定義されてるhttp.DefaultClient変数のメソッドが置き換わっていると云うこと.
Happy, hacking!