Patching Dependencies Without Forking in Go

Go modules are self-contained development environments. That makes it straightforward to tweak a dependency locally — to debug an issue, add logging, or test a hypothesis about a bug — without waiting for an upstream fix or maintaining a fork. Here's how to do it with replace directives, Go workspaces, and a dedicated tool called gohack.

A Quick Test Setup

To demonstrate, create a test module and add the popular go-cmp package as a dependency:

package main

import (
  "fmt"

  "github.com/google/go-cmp/cmp"
  "github.com/google/go-cmp/cmp/cmpopts"
)

func main() {
  s1 := []int{42, 12, 23, 2}
  s2 := []int{12, 2, 23, 42}

  if cmp.Equal(s1, s2, cmpopts.SortSlices(intLess)) {
    fmt.Println("slices are equal")
  }
}

func intLess(x, y int) bool {
  return x < y
}

Run go mod tidy; the resulting go.mod will reference github.com/google/go-cmp at some version. Next, clone the dependency from https://github.com/google/go-cmp/ to a local directory of your choice; call it $DEP. Edit $DEP/cmp/compare.go to add a log statement (or any other temporary change of your own):

func Equal(x, y interface{}, opts ...Option) bool {
  log.Println("options:", opts)
  s := newState(opts)
  s.compareAny(rootStep(x, y))
  return s.result.Equal()
}

Running the test module at this point produces no change — Go doesn't know about your local clone yet. You need to tell it explicitly to use it in the build.

Using a replace Directive

The most direct way to redirect a dependency path to a local directory is a replace directive in go.mod. In the test module's directory, run:

$ go mod edit -replace github.com/google/go-cmp=$DEP

This adds a replace line to go.mod, pointing uses of github.com/google/go-cmp to $DEP on your filesystem. A subsequent go run . picks up the local patched code:

$ go run .
2024/06/29 06:57:17 options: [FilterValues(cmpopts.sliceSorter.filter, Transformer(cmpopts.SortSlices, cmpopts.sliceSorter.sort))]
slices are equal

Using Go Workspaces

Go workspaces (via go.work files, available since Go 1.18) offer another route. Start by removing the replace directive from go.mod — either by editing it out or running go mod edit -dropreplace github.com/google/go-cmp. Then, in the module's directory, execute:

$ go work init
$ go work use . $DEP

This creates an empty workspace, then adds use directives for both the current directory (.) and $DEP. The Go tool now sees two modules and builds using the local version of go-cmp. A go run . in the test module will show the patched behavior.

Workspaces have a subtle advantage over editing go.mod: they don't modify the module's own source-control-managed manifest. A replace left in go.mod can accidentally get committed, causing remote builds to behave differently. go.work files are typically local and not checked in, which makes them safer for temporary patches.

Using gohack

For a slightly more automated approach, gohack — a tool that predates Go workspaces — handles the same workflow. Install it with:

$ go install github.com/rogpeppe/gohack@latest

Then run:

$ gohack get github.com/google/go-cmp
github.com/google/go-cmp => $HOME/gohack/github.com/google/go-cmp

This invocation does two things: it fetches the dependency source into a local store you can control with the $GOHACK environment variable (defaulting to $HOME/gohack), and it inserts the corresponding replace line into your go.mod. You'll then edit the copied code at its new location (e.g., $HOME/gohack/github.com/google/go-cmp/cmp/compare.go) to add your logging change, and run the module to see it reflected. The gohack undo command reverses the changes cleanly.

Choosing an Approach

gohack is handy for quick checks, since it fetches the dependency for you and is simple to revert. But like the manual replace-directive approach, it modifies go.mod — with the same risk of accidentally committing the temporary redirect. Workspaces give a cleaner separation between the module's declared dependencies and your local development overrides. The go.mod-based approaches offer fine-grained control, while go.work scales better if you expect to patch multiple dependencies or work across multiple modules simultaneously.

One additional option worth mentioning is go mod vendor — yet another way to work with modified dependency code, documented in the official module reference — though it carries its own set of tradeoffs for version control and module resolution.