Soft Mocks для Go!
Основная идея Soft Mocks для PHP — это переписывание кода «на лету» перед include(), чтобы можно было поменять реализацию любых методов, функций и констант во время исполнения. Поскольку go — компилируемый язык, то логично делать то же самое на этапе компиляции. В этой статье я расскажу по свой проект Soft Mocks for Go.
Функциональность
Возможности Soft Mocks for Go весьма ограничены — вы можете временно переопределить нужные вам функции и методы, а потом откатить свои правки. Также можно вызвать оригинальную функцию.
При использовании soft mocks, следующий код:
func main() {
closeFunc := (*os.File).Close
soft.Mock(closeFunc, func(f *os.File) error {
fmt.Printf("File is going to be closed: %sn", 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/YuriyNasretdinov/golang-soft-mocks
Аналоги
Для go уже есть библиотека для monkey patching: github.com/bouk/monkey. Эта библиотека точно также позволяет подменять реализацию функций и методов структур, но работает она по другому принципу и пытается «пропатчить» код функции прямо во время исполнения, переписывая память приложения. Этот способ тоже имеет право на существование, но мне кажется, что подход Soft Mocks лучше в долгосрочной перспективе.
Как это работает
Я начал с простого proof-of-concept, сделав следующую правку в файле 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)
+}
Однако оказалось, что стандартная библиотека не разрешает импорты извне (кто бы мог подумать?), поэтому пришлось сделать симлинк /usr/local/go/src/soft
, который ведет на $GOPATH/src/github.com/YuriyNasretdinov/golang-soft-mocks
. После этого код заработал и у меня получилось достичь того, чтобы можно было включать и отменять перехват по желанию.
Адрес функции
Немного странно, но в go нельзя сделать вот такой map:
map[func()]bool
Дело в том, что функции не поддерживают оператор сравнения и поэтому не поддерживаются в качестве ключей для map'ов: golang.org/ref/spec#Map_types. Но это ограничение можно обойти, если использовать reflect.ValueOf(f).Pointer()
для получения указателя на начало кода функции. Причина же, почему функции не сравниваются между собой, заключается в том, что указатель на функцию в go на самом деле является двойным указателем и может содержать в себе дополнительные поля, такие как, например, receiver. Об этом более подробно рассказано вот здесь: bouk.co/blog/monkey-patching-in-go.
Concurrency
Поскольку в go есть горутины (pun intended), то простой булевый флаг будет вызывать race condition при вызове перехватываемой функции из нескольких горутин. В библиотеке github.com/bouk/monkey
явно говорится о том, что метод monkey.Patch()
не является потокобезопасным, поскольку патчит память напрямую.
В нашем же случае можно вместо простого bool сделать int32 (для экономии памяти это не int64), который мы будем изменять с помощью atomic.LoadInt32
и atomic.StoreInt32
. В архитектуре x86 атомарные операции представляют из себя обычные LOAD и STORE, поэтому атомарное чтение и запись не будут слишком сильно влиять на производительность полученного кода.
Зависимости пакета reflect
Как можно видеть, мы подключаем в каждом файле пакет soft
, который является алиасом для нашего пакета github.com/YuriyNasretdinov/golang-soft-mocks
. Этот пакет использует пакет reflect, поэтому мы не можем переписывать пакеты reflect, atomic и их зависимости, иначе мы получим циклические импорты. А зависимостей у пакета reflect на удивление много:
Поэтому Soft Mocks для Go не поддерживает подмену функций и методов из приведенных выше пакетов.
Неожиданные грабли
Также, помимо всего прочего, оказалось, что в go можно писать, например, вот так:
func (TestDeps) StartCPUProfile(w io.Writer) error {
return pprof.StartCPUProfile(w)
}
Обратите внимание на то, что у ресивера (TestDeps) нет имени! Точно также можно не писать имена аргументов, если вы их (аргументы) не используете.
В стандартной библиотеке иногда встречается type shadowing (имя переменной и имя типа совпадают):
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, а попытку разыменовать переменную file и взять оттуда свойство close, и такой код, конечно же, не компилируется.
Заключение
Я сделал Soft Mocks for Go буквально за несколько вечеров, в отличие от Soft Mocks для PHP, который разрабатывался порядка 2 недель. Это отчасти объясняется тем, что для Go есть хорошие встроенные инструменты для работы с AST файлов, а также с простотой синтаксиса — в Go намного меньше возможностей и меньше подводных камней, поэтому разработка такой утилиты была достаточно простой.
Скачать утилиту (вместе с инструкцией по использованию) можно по адресу github.com/YuriyNasretdinov/golang-soft-mocks. Я буду рад услышать критику и пожелания.
Автор: Юрий Насретдинов