The Own Lab The Own Lab

Go Tool Directive

Go 1.24 引入的 tool directive,將開發工具依賴從 require 中分離,避免汙染下游消費者

Overview##

開發 Go 專案時,你幾乎一定會用到一些輔助工具:linter(golangci-lint)、formatter(gofumpt)、code generator(stringer)。這些工具本身是用 Go 寫的,但它們不是你的程式碼在 runtime 需要的東西。

問題是:在 Go 1.24 之前,沒有官方的方式來區分「我的程式碼需要這個 package」和「我的開發流程需要這個工具」。大家要嘛把工具塞進 require,要嘛用社群慣例 tools.go 來處理。兩種做法都有缺陷。

Go 1.24 引入了 tool directive — 在 go.mod 中正式宣告開發工具依賴,讓它們的 transitive dependencies 不會傳播給下游消費者。

Architecture##

Go 的依賴傳播機制###

Go modules 使用 Minimum Version Selection(MVS) 來解析依賴。當你的 module 被別人 require 時,你的 go.mod 中所有 require 的 package 都會進入對方的依賴圖。

這對 runtime 依賴來說完全合理,但對開發工具來說就是災難:

graph TD
  A[your-app] -->|require| B[my-library]
  B -->|require| C[runtime dep A]
  B -->|require| D[runtime dep B]
  B -->|require| E[golangci-lint]
  E -->|transitive| F[160+ indirect deps]
  F -->|pollutes| A

  style E fill:#f66,stroke:#333
  style F fill:#f66,stroke:#333

一個 linter 像 golangci-lint 自身就有 160+ 個 transitive dependencies。如果你把它放在 require 裡,任何 import 你的 library 的專案都會被迫拉進這些完全無關的依賴。

Warning

更隱蔽的問題是 MVS 的版本提升效應。如果 linter 依賴了 google.golang.org/api v0.198.0,而下游專案原本用 v0.114.0,Go 會自動選擇較高的版本。你的開發工具可能無聲地升級了別人的 production 依賴。

三種管理方式的比較###

做法語法傳播給下游官方支援
require(直接放)require example.com/tool v1.0.0會,包含所有 transitive deps不適用於 CLI 工具
tools.go + build tag//go:build tools + blank import會(但不編譯進 binary)社群慣例,非官方
tool directivetool example.com/tool不會Go 1.24+ 官方

Note

tools.go 是 Go 1.23 以前最常見的做法。它透過 //go:build tools 的 build tag 來排除編譯,但依賴仍然在 require 裡,所以下游消費者還是會被汙染。tool directive 是官方對這個問題的根本解決方案。

Setup##

Prerequisites###

  • Go 1.24 以上(tool directive 在此版本引入)
go version
# go version go1.24.6 darwin/arm64

如果你的 go.modgo 版本低於 1.24,需要先升級:

go mod edit -go=1.24

Usage##

Basic Usage###

宣告工具依賴:

使用 go get -tool 一步完成 — 它會同時加入 tool directive 和對應的 require

# 新增一個工具依賴
go get -tool golang.org/x/tools/cmd/stringer

執行後,go.mod 會變成:

module example.com/myproject

go 1.24

tool golang.org/x/tools/cmd/stringer

require golang.org/x/tools v0.29.0

執行工具:

# 用短名稱執行(取 import path 的最後一段)
go tool stringer

# 如果短名稱衝突,使用完整路徑
go tool golang.org/x/tools/cmd/stringer

# 列出所有已註冊的工具
go tool

移除工具:

go mod edit -droptool golang.org/x/tools/cmd/stringer
go mod tidy

Tip

go tool 列出的工具包含 Go 內建工具(如 vetpprof)和你在 go.mod 中宣告的工具。你宣告的工具會使用與主 module 相同的 module graph,所以 replaceexclude directive 也適用。

Advanced Usage###

宣告多個工具:

// go.mod
module example.com/myproject

go 1.24

tool (
    github.com/golangci/golangci-lint/cmd/golangci-lint
    github.com/daixiang0/gci
    mvdan.cc/gofumpt
    golang.org/x/vuln/cmd/govulncheck
)

require (
    // runtime 依賴
    github.com/google/uuid v1.6.0
    go.uber.org/zap v1.27.0

    // 工具的 require(go get -tool 會自動加)
    github.com/golangci/golangci-lint v1.62.2
    github.com/daixiang0/gci v0.13.7
    mvdan.cc/gofumpt v0.9.1
    golang.org/x/vuln v1.1.4
)

依賴傳播的差異:

graph TD
  subgraph Before
    A1[consumer] -->|require| B1[my-library]
    B1 -->|require| C1[uuid]
    B1 -->|require| D1[zap]
    B1 -->|require| E1[golangci-lint + 160 deps]
  end

  subgraph After
    A2[consumer] -->|require| B2[my-library]
    B2 -->|require| C2[uuid]
    B2 -->|require| D2[zap]
    B2 -.->|tool, isolated| E2[golangci-lint + 160 deps]
  end

  style E1 fill:#f66,stroke:#333
  style E2 fill:#4a4,stroke:#333

從 tools.go 遷移:

如果你的專案還在用 tools.go 的舊模式:

// tools/tools.go(舊做法,可以刪除)
//go:build tools

package tools

import (
    _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
    _ "mvdan.cc/gofumpt"
)

遷移步驟:

# 1. 對每個工具執行 go get -tool
go get -tool github.com/golangci/golangci-lint/cmd/golangci-lint
go get -tool mvdan.cc/gofumpt

# 2. 刪除 tools.go
rm tools/tools.go

# 3. 清理不再需要的依賴
go mod tidy

常見搭配情境###

情境工具範例tool directive path
Lintinggolangci-lintgithub.com/golangci/golangci-lint/cmd/golangci-lint
Formattinggofumpt, gcimvdan.cc/gofumpt
Code generationstringer, enumergolang.org/x/tools/cmd/stringer
Vulnerability scangovulncheckgolang.org/x/vuln/cmd/govulncheck
API docsswaggithub.com/swaggo/swag/cmd/swag

Configuration Reference##

指令用途範例
tool <path>在 go.mod 中宣告工具tool mvdan.cc/gofumpt
go get -tool <path>新增工具(自動加 tool + require)go get -tool mvdan.cc/gofumpt
go mod edit -tool <path>手動加入 tool directivego mod edit -tool mvdan.cc/gofumpt
go mod edit -droptool <path>移除 tool directivego mod edit -droptool mvdan.cc/gofumpt
go tool <name>執行已註冊的工具go tool gofumpt -w .
go tool列出所有可用工具

Warning

tool directive 需要對應的 require 才能運作。如果你手動用 go mod edit -tool 加入,記得也要 go get 該 module。使用 go get -tool 則會自動處理兩者。

Summary##

  • tool directive 是 Go 1.24+ 管理開發工具依賴的官方方案。 它將工具的 transitive dependencies 隔離,不會傳播給下游消費者。
  • 永遠不要把 CLI 工具放在 require 裡。 這會汙染所有 import 你的 module 的專案的依賴樹,還可能透過 MVS 無聲地升級別人的 production 依賴版本。
  • tools.go 遷移很簡單。 go get -tool → 刪除 tools.gogo mod tidy,三步完成。
  • 使用 go get -tool 新增、go tool 執行、go mod edit -droptool 移除。 這是完整的工具生命週期管理。

留言 (0)

登入後即可留言