Soft Mocks for Go! (overriding functions and methods in runtime)
Soft Mocks for Go!
The main idea of Soft Mocks for PHP is to rewrite the code on the fly before include (), so that you can change the implementation of any methods, functions and constants at runtime. Since go is a compiled language, it is logical to do the same at the compilation stage. In this article I will talk about my project Soft Mocks for Go.
Functionality
The possibilities 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())
}
It will print this:
File is going to be closed: /dev/null
Hello, world: !
You can download the library here .
Analogs
There is already a library for go for monkey patching: github.com/bouk/monkey . This library also allows you to replace the implementation of functions and methods of structures, but it works on a different principle and tries to "patch" the function code directly at runtime, overwriting the application memory. This method also has a right to exist, but it seems to me that the Soft Mocks approach is better in the long run.
How it works
I started with a simple proof-of-concept by making the following changes to the 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 the outside (who would have thought?), So I had to make a symlink
/usr/local/go/src/soft
that leads to $GOPATH/src/github.com/YuriyNasretdinov/golang-soft-mocks
. After that, the code worked and I managed to achieve that it was possible to enable and disable interception at will.Function Address
A bit strange, but in go you can’t make such a map:
map[func()]bool
The fact is that functions do not support the comparison operator and therefore are not supported as keys for maps: golang.org/ref/spec#Map_types . But this limitation can be circumvented if used
reflect.ValueOf(f).Pointer()
to get a pointer to the beginning of the function code. The reason why the functions are not compared with each other is that the pointer to the function in go is actually a double pointer and may contain additional fields, such as, for example, receiver. This is described in more detail here .Concurrency
Since go has goroutines (pun intended), a simple boolean flag will trigger a race condition when calling an intercepted function from several goroutines. The library
github.com/bouk/monkey
explicitly states that the method is monkey.Patch()
not thread safe because it patches memory directly. In our case, instead of a simple bool, you can make int32 (to save memory it is not int64), which we will change with
atomic.LoadInt32
and atomic.StoreInt32
. In x86 architecture, atomic operations are the usual LOAD and STORE, so atomic read and write will not affect the performance of the resulting code too much.Package dependencies
As you can see, we include in each file a package
soft
, which is an alias for our package github.com/YuriyNasretdinov/golang-soft-mocks
. This package uses the reflect package, so we cannot rewrite the reflect, atomic packages and their dependencies, otherwise we will get circular imports. And reflect package has surprisingly many dependencies : 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 in go, for example, like this:
func (TestDeps) StartCPUProfile(w io.Writer) error {
return pprof.StartCPUProfile(w)
}
Please note that the receiver (TestDeps) does not have a name! Similarly, you can omit the argument names if you do not use them (arguments).
In the standard library, type shadowing is sometimes found (variable name and type name are the same):
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 did Soft Mocks for Go in just a few evenings, unlike Soft Mocks for PHP, which was developed around 2 weeks. This is partly due to the fact that Go has good built-in tools for working with AST files, as well as simple syntax - Go has much less 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 wishes.