📜 ⬆️ ⬇️

Soft Mocks for Go! (redefinition of functions and methods in runtime)

Soft Mocks for Go!


The basic idea of Soft Mocks for PHP is to rewrite code "on the fly" before include () so that you can change the implementation of any methods, functions and constants during execution. Since go is a compiled language, it is logical to do the same at the compilation stage. In this article I will tell about my project Soft Mocks for Go.

Functionality


The capabilities of Soft Mocks for Go are very limited - you can temporarily override the functions and methods you need, and then roll back your edits. You can also call the original function.

When using soft mocks, the following code:
')
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()) } 

Prints this:

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

Download the library here .

Analogs


For go already there is a library for monkey patching: github.com/bouk/monkey . This library also allows you to substitute the implementation of functions and methods of structures, but it works according to a different principle and tries to patch the function code during execution by rewriting the application's memory. This method also has a right to exist, but it seems to me that the approach of Soft Mocks is better in the long term.

How it works


I started with a simple proof-of-concept, making the following edit in the file standard library 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) +} 

However, it turned out that the standard library does not allow imports from outside (who would have thought?), So I had to make a /usr/local/go/src/soft symlink that leads to $GOPATH/src/github.com/YuriyNasretdinov/golang-soft-mocks . After that, the code earned and I managed to achieve that it was possible to turn on and cancel interception at will.

Function address


A bit strange, but you can't make such a map in go:

 map[func()]bool 

The fact is that the functions do not support the comparison operator and therefore are not supported as keys for maps: golang.org/ref/spec#Map_types . But this restriction can be circumvented by using reflect.ValueOf(f).Pointer() to get a pointer to the beginning of the function code. The reason why functions are not compared with each other is that a pointer to a function in go is actually a double pointer and may contain additional fields, such as, for example, receiver. About this in more detail here .

Concurrency


Since there are gorutines in go (pun fit), a simple boolean flag will cause a race condition when calling a function to be captured from several gorutins. The github.com/bouk/monkey library explicitly states that the monkey.Patch() method is not thread-safe because it patches the memory directly.

In our case, instead of a simple bool, you can do int32 (to save memory, this is not int64), which we will modify using atomic.LoadInt32 and atomic.StoreInt32 . In the x86 architecture, atomic operations are the usual LOAD and STORE, so atomic reading and writing will not affect the performance of the resulting code too much.

Package dependencies reflect


As you can see, we include a soft package in each file, which is an alias for our package github.com/YuriyNasretdinov/golang-soft-mocks . This package uses the reflect package, so we cannot rewrite reflect, atomic packages and their dependencies, otherwise we will get cyclic imports. And there are surprisingly a lot of dependencies on the package:



Therefore, Soft Mocks for Go does not support the substitution of functions and methods from the above packages.

Unexpected rake


Also, among other things, it turned out that you can write to go, for example, like this:

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

Please note that the receiver (TestDeps) has no name! Likewise, you can not write the names of the arguments, if you do not use them (the arguments).

In the standard library, sometimes there is type shadowing (the name of the variable and the name of the type match):

 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 } 

In this case, the expression (*file).close inside the function body will not mean a pointer to the close method, but an attempt to dereference the file variable and take the close property from there, and such code, of course, does not compile.

Conclusion


I made Soft Mocks for Go in just a few nights, unlike Soft Mocks for PHP, which was developed for about 2 weeks. This is partly due to the fact that Go has good built-in tools for working with AST files, as well as the simplicity of the syntax - Go has much fewer features and fewer pitfalls, so the development of such a utility was quite simple.

Download the utility (along with instructions for use) at github.com/YuriyNasretdinov/golang-soft-mocks . I will be glad to hear criticism and suggestions.

Source: https://habr.com/ru/post/328620/


All Articles