{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/tool-directive/",
  "description": "Pin tool versions in Go 1.24 with the new 'tool' directive. Replace tools.go pattern with native go.mod support for project tooling.",
  "path": "/go/tool-directive/",
  "publishedAt": "2025-04-13T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "TIL",
    "DevOps"
  ],
  "textContent": "Go 1.24 added a new tool directive that makes it easier to manage your project's tooling.\n\nI used to rely on Make targets to install and run tools like stringer, mockgen, and\nlinters like gofumpt, goimports, staticcheck, and errcheck. Problem is, these\ninstallations were global, and they'd often clash between projects.\n\nAnother big issue was frequent version mismatch. I ran into cases where people were\nformatting the same codebase differently because they had different versions of the tools\ninstalled. Then CI would yell at everyone because it was always installing the latest\nversion of the tools before running them. Chaos!\n\nThe tools.go convention\n\nTo avoid this mess, the Go community came up with a convention where you'd pin your tool\nversions in a tools.go file. I've [written about omitting dev dependencies before]. But\nthe gist is, you'd have a tools.go file in your root directory that imports the tooling\nand assigns them to _:\n\nSince these dependencies aren't used directly in the codebase, the //go:build tools\ndirective ensures they're excluded from the main build.\n\nThen running go mod tidy keeps things clean and includes these dev dependencies in the\ngo.mod and go.sum files.\n\nThis works, but it always felt a bit clunky. You end up polluting your main go.mod with\ntooling-only dependencies. And sometimes, transitive dependencies of those tools clash with\nyour app's dependencies.\n\nThe new tool directive in Go 1.24 solves [some of the tools.go pain points].\n\nEnter the tool directive\n\nWith Go 1.24, you can now add tooling with the -tool flag when using go get:\n\nThis adds the dependency to your go.mod like this:\n\nNotice the tool directive clearly separates these from regular module dependencies.\n\nThen you can run the tool with:\n\nOne thing to keep in mind: the first time you run a tool this way, it might take a second -\nGo needs to compile it before running if it isn't already compiled. After that, it's cached,\nso subsequent runs are fast.\n\nWhat about go generate?\n\nThis also plays nicely with go generate. I've started replacing direct tool calls with\ngo tool, so contributors don't need to install tools globally. Just run go generate and\nyou're done:\n\nNo further setup needed, no path issues, and it's always using the version you pinned.\n\nStill not perfect\n\nThat said, one thing still bugs me: go get -tool adds these dev tools to the main go.mod\nfile. That means your application and dev dependencies are still mixed together. Same\nproblem the tools.go hack had.\n\nThere's no built-in way to avoid this yet. So your options are:\n\n- Accept that dev and app deps will live in the same go.mod file.\n- Create a separate tools module to isolate your tooling. A bit clunky, but doable.\n\nI went with the second option.\n\nMy layout looks like this:\n\nThen I install tools like this:\n\nAnd run them from the root directory as follows:\n\nThe go tool command supports a -modfile flag that you can use to specify where to pull\nthe tool version from. I _really_ wish go get supported -modfile too - that way you\nwouldn't need to manage the dependencies in such a wonky manner. This was close to being\nperfect. Well, maybe in a future release.\n\nOnly works with Go-native tools\n\nAnother limitation is that it only works with tools written in Go. So if you're using stuff\nlike eslint, prettier, or jq, you're on your own. But for most of my projects, the dev\ntooling is written in Go anyway, so this setup has been working okay.\n\n\n\n\n[written about omitting dev dependencies before]:\n    /go/omit-dev-dependencies-in-binaries/\n\n[some of the tools.go pain points]:\n    /go/tool-directive/#still-not-perfect",
  "title": "Go 1.24's \"tool\" directive"
}