Go!のソフトモック (実行時の関数とメソッドのオーバーライド)

Go!のソフトモック



Soft Mocks for PHPの主なアイデアは、include()の前にコードをオンザフライで書き換えることです。これにより、実行時にメソッド、関数、定数の実装を変更できます。 goはコンパイルされた言語であるため、コンパイル段階で同じことを行うのが論理的です。 この記事では、私のプロジェクトSoft Mocks for Goについて説明します。



機能性



Soft Mocks for Goの可能性は非常に限られています。必要な機能とメソッドを一時的にオーバーライドしてから、編集をロールバックできます。 元の関数を呼び出すこともできます。



ソフトモックを使用する場合、次のコード:



func main() { closeFunc := (*os.File).Close soft.Mock(closeFunc, func(f *os.File) error { fmt.Printf("File is going to be closed: %s\n", f.Name()) res, _ := soft.CallOriginal(closeFunc, f)[0].(error) return res }) fp, _ := os.Open("/dev/null") fmt.Printf("Hello, world: %v!\n", fp.Close()) }
      
      





これを印刷します:



 File is going to be closed: /dev/null Hello, world: <nil>!
      
      





ライブラリはこちらからダウンロードできます。



アナログ



既にモンキーパッチ適用のためのライブラリgithub.com/bouk/monkeyがあります。 このライブラリを使用すると、関数の実装や構造のメソッドを置き換えることもできますが、異なる原理で動作し、実行時に関数コードを直接「パッチ」して、アプリケーションメモリを上書きしようとします。 この方法には存在する権利もありますが、Soft Mocksのアプローチは長期的には優れているように思えます。



仕組み



簡単な概念実証から始め、ファイルstandard_file_unix.goファイルで次の編集を行いました。



 @@ -9,6 +9,8 @@ import ( "runtime" "syscall" + + "github.com/YuriyNasretdinov/golang-soft-mocks" ) // fixLongPath is a noop on non-Windows platforms. @@ -126,6 +128,11 @@ // Close closes the File, rendering it unusable for I/O. // It returns an error, if any. func (f *File) Close() error { + if closeFuncIntercepted { + println("Intercepted!") + return nil + } + if f == nil { return ErrInvalid } @@ -293,3 +300,9 @@ } return nil } + +var closeFuncIntercepted bool + +func init() { + soft.RegisterFunc((*File).Close, &closeFuncIntercepted) +}
      
      





しかし、標準ライブラリでは外部からのインポートが許可されていないことがわかりました(だれが考えたでしょうか?) $GOPATH/src/github.com/YuriyNasretdinov/golang-soft-mocks



。 その後、コードは機能し、インターセプトを自由に有効または無効にできることを達成することができました。



機能アドレス



少し変ですが、そのようなマップを作成することはできません。



 map[func()]bool
      
      





実際、関数は比較演算子をサポートしていないため、マップのキーとしてサポートされていません: golang.org/ref/spec#Map_types 。 ただし、 reflect.ValueOf(f).Pointer()



を使用して関数コードの先頭へのポインターを取得すると、この制限を回避できます。 関数が相互に比較されない理由は、goの関数へのポインターが実際にはダブルポインターであり、たとえばレシーバーなどの追加フィールドを含むことがあるためです。 これについては、 ここで詳しく説明します



並行性



goにはゴルーチンが含まれているため(しゃれを意図)、いくつかのゴルーチンからインターセプトされた関数を呼び出すときに、単純なブールフラグが競合状態をトリガーします。 github.com/bouk/monkey



ライブラリは、 monkey.Patch()



メソッドがメモリに直接パッチを適用するため、スレッドセーフではないと明示的に述べています。



この場合、単純なブールの代わりに、int32を作成して(int64ではないメモリを保存するため)、 atomic.LoadInt32



およびatomic.StoreInt32



を使用して変更します。 x86アーキテクチャでは、アトミック操作は通常のLOADおよびSTOREであるため、アトミック読み取りおよび書き込みは、結果のコードのパフォーマンスにあまり影響しません。



パッケージの依存関係



ご覧のとおり、各ファイルにsoft



パッケージを含めます。これは、パッケージgithub.com/YuriyNasretdinov/golang-soft-mocks



エイリアスです。 このパッケージはリフレクトパッケージを使用しているため、リフレクト、アトミックパッケージ、およびそれらの依存関係を書き換えることはできません。そうしないと、周期的なインポートが行われます。 そしてリフレクトパッケージには驚くほど多くの依存関係があります:







したがって、Soft Mocks for Goは、上記のパッケージからの関数およびメソッドの置換をサポートしていません。



予期しないレーキ



また、とりわけ、次のようにgoで記述できることがわかりました。



 func (TestDeps) StartCPUProfile(w io.Writer) error { return pprof.StartCPUProfile(w) }
      
      





レシーバー(TestDeps)には名前がないことに注意してください! 同様に、引数名(引数)を使用しない場合は、引数名を省略できます。



標準ライブラリでは、タイプシャドウイングが見つかることがあります(変数名とタイプ名は同じです)。



 func (file *file) close() error { if file == nil || file.fd == badFd { return syscall.EINVAL } var err error if e := syscall.Close(file.fd); e != nil { err = &PathError{"close", file.name, e} } file.fd = -1 // so it can't be closed again // no need for a finalizer anymore runtime.SetFinalizer(file, nil) return err }
      
      





この場合、関数本体内の式(*file).close



は、closeメソッドへのポインターを意味するものではありませんが、ファイル変数を逆参照し、そこからcloseプロパティを取得しようとすると、もちろんそのようなコードはコンパイルされません。



おわりに



2週間ほどで開発されたPHP用ソフトモックとは異なり、私はほんの数晩でGo用ソフトモックを行いました。 これは、GoにはASTファイルを操作するための優れた組み込みツールがあり、構文がシンプルであるためです-Goの機能と落とし穴ははるかに少ないため、このようなユーティリティの開発は非常に簡単でした。



github.com/YuriyNasretdinov/golang-soft-mocksでユーティリティを(使用手順とともに)ダウンロードします。 批判や希望を聞いてうれしいです。



All Articles