CLI tool
agiledragon/gomonkey avatar
agiledragon/gomonkey

Two notes in the README are the whole risk profile

gomonkey is a library to make monkey patching in unit tests easy

2,265 stars193 forksGoMIT

At a glance

What is it?
gomonkey replaces a function's entry code with a jump to your replacement, which is why the repository is full of files named after architectures and operating systems, why the three most recent releases are all ARM64 and Darwin bug fixes, and why the README's two notes matter more than its feature list: you have to disable inlining, and the library is not threadsafe.
Who is it for?
Adopt gomonkey for the code you cannot restructure, a third-party package, an unexported method, a package-level function or a global that your own test needs to control, and where injecting a seam would mean forking. Do not adopt it where dependency injection is possible, because a patched function still runs the original code everywhere the compiler inlined it, and because the library rewrites the running program's code rather than changing the program before it runs.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 11 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

It rewrites the entry of a function in place

The idea is not the library's own, and the README says so, crediting the concept to a post about monkey patching in Go. A patch works by overwriting the code at the start of a target function with a jump to your replacement, so every subsequent call goes to the new implementation. The repository layout is the clearest description of what that costs. There are five files named after architectures, one each for 386, amd64, arm64, loong64 and riscv64, and three named after operating systems, one each for Darwin, Linux and Windows, plus assembly helpers for writing the jump on Darwin for two architectures. Writing to a function's code means asking the operating system for permission to modify a page that is normally executable but not writable, which is a per-platform problem and the reason the operating system files exist at all. A patch on a function variable, a global variable or an interface value is a different operation again, which is why the library lists those as separate features rather than as one.

Inlining has to be off, or the patch does nothing

The first of the two notes is the one that catches everyone. gomonkey fails to patch a function or a member method if inlining is enabled, and the fix is a command line argument: -gcflags=-l below Go 1.10, and -gcflags=all=-l on Go 1.10 and above. The reason is not subtle. If the compiler inlined a call, there is no call site left to redirect, because the callee's body was copied into the caller, so overwriting the original function changes nothing for that call. Turning inlining off for the test build makes every call go through the function you can patch. That flag is not cosmetic and it is not optional, which is why the README also documents the test command with it attached:

go
$ go test . ./test -gcflags=all=-l

The cost is a real one. Disabling inlining changes the performance characteristics of everything under test and makes builds slower, and a function that the compiler inlined may still be unreachable for patching reasons unrelated to the flag in more complex builds. If your team runs tests through a tool that builds its own command line, the flag has to be threaded through that tool or the patches will silently do nothing and your test will pass for the wrong reason.

Three releases in a row are ARM64 and Darwin fixes

The release titles are a maintenance log you can read without opening a single file. The three most recent are version 2.14.3, fixing ARM64 trampoline clobbering of R10, described as the eleventh argument slot; version 2.14.2, fixing a same-page SIGBUS on Darwin ARM64 and corruption in private method patches; and version 2.14.1, fixing a potential SIGBUS on Darwin. Three consecutive releases, all of them correctness fixes in the low-level layer, two of them about the same platform. That pattern tells you where the difficulty lives. The API is nine feature lines long and will not change. The difficulty is the assembly and the memory permissions, and the platforms where that difficulty bites hardest are the newest ones, which is exactly what you would predict from a library whose newest supported architectures are the ones with the least settled tooling. If you run tests on an ARM64 Mac, you are on the critical path for this project, and the bug you would have hit is one of these three.

Five architectures, three systems, and one file per combination

The supported platform list is worth reading closely, because two entries in it are unusual. Architectures: amd64, arm64, 386, loong64 and riscv64. Operating systems: Linux, macOS and Windows. The 386 entry is a legacy holdover that nobody minds supporting because the jump is small, and loong64 and riscv64 are there because they are real and the jump code is not hard when you already have four. The mapping to the file tree is exact, with a jump file per architecture and a binary modification file per operating system, and the assembly writers are specific to Darwin on two architectures. That is a maintenance matrix, not a portability claim, and it is worth reading as a statement about who pays when a new architecture appears. The go.mod file declares a language version of 1.14 and a single dependency, a test framework, so the library itself is small and has almost no third-party surface. A low declared minimum Go version alongside support for two of the newest architectures is not a contradiction, since one is a language level and the other is assembly, but it does mean the build metadata is not telling you much about what is actually exercised.

Private methods take a different path on ARM64 Macs

The feature list includes patching a private member method, which is a capability most languages make awkward and which Go makes possible because the runtime is not hiding the symbols from you. The file tree shows how much special-casing it takes. There is a private method implementation specific to Darwin on ARM64, with both a Go file and an assembly file and its own test, and a default implementation for everything else. Two files and a dedicated test for one platform is the clearest evidence in the repository that this path is where the bugs live, and it lines up exactly with the private method corruption named in the 2.14.2 release. The rest of the structure is more conventional. A patch file is the entry point, a directory with a reflection helper is presumably for reaching unexported fields and methods through the runtime's type information, and a directory named for a domain-specific language is the fluent interface the feature list implies with its sequence variants. The library is therefore two things stacked: a small domain-specific wrapper and a platform layer underneath it, and only one of those two is likely to need your attention.

The documentation is the test suite

The README's guidance on how to use the library is one sentence: refer to the test cases as idioms, very complete and detailed. That is a deliberate choice and for this library it is the right one, because a patch is a side effect on the running program and the only convincing demonstration is a test that fails without the patch and passes with it. The test directory is therefore the reference documentation, and the Makefile reflects that arrangement by having a single test target that runs a shell script, which in turn is presumably the place where the inlining flag and the per-platform variations are handled once rather than in every developer's command. The dependency list supports the reading: the only third-party package is a test assertion library, so nothing in the test suite is standing in for the thing being tested. If you are evaluating a patch target you have not used before, the fastest route is to find the corresponding test, copy the idiom, and change the target, rather than to read any documentation page.

Not threadsafe, and that is a different kind of warning

The second note says a panic may happen when a goroutine is patching a function or a member method that is another goroutine is visiting at the same time, and concludes plainly that gomonkey is not threadsafe. Put next to the inlining note, those two lines are the entire risk profile of the library, and they are quite different in character. The inlining problem is a configuration mistake you can fix once in a build script. The concurrency problem is a property of when you are allowed to patch. Patching while other goroutines run is a data race on the code page, and the outcome is a crash rather than a wrong answer, which is at least honest. The practical consequence is that patches belong in a setup phase, before the parallel tests start, rather than inside individual test cases that a runner may execute concurrently. Nothing in the library enforces that, and nothing in the visible documentation schedules it for you, so it is the kind of rule that belongs in a team convention rather than in one library.

Against injecting a seam, and against the module path gotcha

The alternative approach is to not patch. If the code is yours, take a function variable and assign it in the test, take an interface parameter and pass a fake, or wrap the call site in a thin indirection. Every one of those is a change to the program before it runs, which means no machine code is rewritten, no inlining flag is needed, and no race is possible. The difference in practice is that injection is free at the point where the code is already designed for it and expensive everywhere else, and gomonkey exists for the everywhere else: a third-party package's function, an unexported method, a global variable set in an initialiser. For that case there is no alternative that does not involve a fork. The other alternative is the language's own facilities, which in Go means interfaces and function variables, and the honest comparison is between minutes of refactoring and a test suite that needs a compiler flag. A separate practical note belongs here, because it will cost you an afternoon otherwise. The module path changed at version 2.1.0, so an import for an older release has no version suffix while a current one does, and a project that has both in its history will have two different import paths for what looks like one library.

Editorial conclusion

Adopt gomonkey for the code you cannot restructure, a third-party package, an unexported method, a package-level function or a global that your own test needs to control, and where injecting a seam would mean forking. Do not adopt it where dependency injection is possible, because a patched function still runs the original code everywhere the compiler inlined it, and because the library rewrites the running program's code rather than changing the program before it runs. Verify four things before you rely on it: that your test command carries the inlining flag, which is -gcflags=all=-l on Go 1.10 and above, that patches happen where no other goroutine is running, since the README says a concurrent visit can panic, that your architecture is in the list of amd64, arm64, 386, loong64 and riscv64 on Linux, macOS or Windows, and that you are importing the v2 module path, since below v2.1.0 the import path has no version suffix. The licence is MIT, the most recent release is v2.14.3 from 2026-09-20, and the last push is the same day.

Frequently asked questions

Why does patching not work unless I disable inlining?

If the compiler inlined a call there is no call site left to redirect, so overwriting the function changes nothing for that caller. The README says to run tests with -gcflags=-l below Go 1.10 or -gcflags=all=-l on Go 1.10 and above, and its own test command includes the flag.

What can gomonkey patch?

A function, a public member method, a private member method, an interface, a function variable and a global variable. It also supports patches of a specified sequence for a function, a member method, an interface and a function variable, so a replacement can behave differently on successive calls.

Is gomonkey safe to use with parallel tests?

No. The README states that a panic may happen when a goroutine patches a function or member method that another goroutine is visiting at the same time, and that the library is not threadsafe. Patches therefore belong in a setup phase before parallel tests begin.

Which platforms does gomonkey support?

Architectures amd64, arm64, 386, loong64 and riscv64, on Linux, macOS and Windows. The repository has one jump implementation file per architecture and one binary modification file per operating system, plus assembly writers for Darwin on two architectures.

How do I install gomonkey?

From v2.1.0 onwards the import path carries the version suffix, so go get github.com/agiledragon/gomonkey/v2 at the release you want. Below v2.1.0, for example v2.0.2, the import path has no suffix: go get github.com/agiledragon/gomonkey.

Official sources

  1. agiledragon/gomonkey on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/agiledragon-gomonkey.svg)](https://hysenlabs.com/projects/agiledragon-gomonkey)