Soft Mocks для Go! (переопределение функций и методов в runtime)

от автора

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: %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/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. Я буду рад услышать критику и пожелания.
ссылка на оригинал статьи https://habrahabr.ru/post/328620/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *