読者です 読者をやめる 読者になる 読者になる

モンキーパッチのライブラリを使って,golang のメソッドを差し替える黒魔術を使ってみた

golang Go言語

モンキーパッチというのは,オリジナルのソースコードを変更せずに実行時にコードを拡張したり変更したりする手法のことです.動的言語で見かけるようなモンキーパッチを golang でもやってのけることができます(って,そういうライブラリを作っちゃった人がいます!

手法については,ライブラリの作者による詳説をご覧ください.

Monkey Patching in Go

ざっくり言うと,メソッドの開始アドレスをすげ替えて他のメソッド呼び出すようにするという感じのものです.

なので,

  • セキュリティ厳しい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!