|
| 1 | +--- |
| 2 | +title: "Go 1.26で go fix が面白くなった" |
| 3 | +date: 2026/01/29 00:00:00 |
| 4 | +postid: a |
| 5 | +tag: |
| 6 | + - Go |
| 7 | + - Go1.26 |
| 8 | +category: |
| 9 | + - Programming |
| 10 | +thumbnail: /images/2026/20260129a/thumbnail.png |
| 11 | +author: 真野隼記 |
| 12 | +lede: "go fixコマンドのアップデートについて解説します。" |
| 13 | +--- |
| 14 | +# はじめに |
| 15 | + |
| 16 | +TIG(Technology Innovation Group)の真野です。 |
| 17 | + |
| 18 | +[Go 1.26ブログ連載](https://future-architect.github.io/articles/20260127a/) の3日目は、`go fix` コマンドのアップデートについて解説します。 |
| 19 | + |
| 20 | +## go fix の出発点 |
| 21 | + |
| 22 | +リリース内容へ入る前に、`go fix` そのものについて解説します。 |
| 23 | + |
| 24 | +まず、Go 1.26で新しく `go fix` コマンドが追加されたわけではありません。コマンド自体は2011年4月15日に公開された[Introducing Gofix](https://go.dev/blog/introducing-gofix)というブログで紹介され、その翌月リリースの[r57](https://go.dev/doc/devel/pre_go1#r57) で追加されました。つまり、誕生から15年近く経過する由緒ある(?)ツールとも言えます。 |
| 25 | + |
| 26 | +それにも関わらず、 `go fix` コマンドはあまり有名で無いと思います。なぜでしょうか。 |
| 27 | + |
| 28 | +まず、Go言語が1.0に到達したのは、[2012年3月28日](https://go.dev/blog/go1)で、`go fix` コマンドが紹介されたのはその1年前です。現在と大きく異なり、正式版に向けて週次ベースでリリースしており、破壊的な仕様変更も行われていた時期です。その中には `http` や `os` といった利用頻度が高いパッケージのAPI変更も含まれるため、開発者としては追随が大変でした。 |
| 29 | + |
| 30 | +`go fix` の出自は、こうしたAPIの破壊的変更にともなる既存コードの書き換えすることです(Goチーム公式からバージョンアップに伴う移行ツールが提供されていたという訳で、ホスピタリティが凄さを感じます)。もちろん、バイナリを書き換えるわけではないので再ビルドが必要ですが、利用者視点では手間という面で、このツールがあるのと無いでは大きな差でしょう。Go開発チームとしても、 `go fix` という変換ツールがあるこそ、API変更のコストに囚われすぎずより良い言語にすることが集中できた(意訳)という訳で、当時はとても重要な位置づけでした。 |
| 31 | + |
| 32 | +## バージョン1.0以降の go fix |
| 33 | + |
| 34 | +一方で、Goはバージョンが1.0に到達してからは破壊的な変更が行われなくなりました。 |
| 35 | + |
| 36 | +これ自体は良いことですが、`go fix` の存在感は低下しました。今となっては不要では?みたいな声を聞いたこともあります。実際、1.0以降のリリースノートで `go fix` の更新は、`context` の書き換えくらいでした(他にも見落としていたらすいません)。 |
| 37 | + |
| 38 | +- [Go 1.8](https://go.dev/doc/go1.8#tool_fix)、[Go 1.10](https://go.dev/doc/go1.10#fix) |
| 39 | + - `golang.org/x/net/context` を `context` に書き換える機能が追加 |
| 40 | + |
| 41 | +これ以降は更新が無かったため、ほとんどの開発者の意識から外れていたのではないでしょうか。 `go fmt` や `go vet` は広く活用されていたのに、不憫な子。 |
| 42 | + |
| 43 | +## Go 1.26での go fix |
| 44 | + |
| 45 | +さて、Go 1.26です。 |
| 46 | + |
| 47 | +[リリースノート](https://go.dev/doc/go1.26)やIssueである[cmd/go: fix: apply fixes from modernizers, inline, and other analyzers #71859](https://github.com/golang/go/issues/71859) を読むと、以下の点が更新されました。 |
| 48 | + |
| 49 | +1. `go fix` の内部実装が `golang.org/x/tools/go/analysis` という、`go vet` や `gopls` が利用しているのと同じフレームワークを使うようになった。型情報や変数のスコープなどを理解して安全な書き換えが可能になるとのこと |
| 50 | +2. 従来の機能は廃止された |
| 51 | +3. 新しい文法や書き方に自動的に書き換える(モダン化する)ツールになった |
| 52 | + |
| 53 | +特に3点目はさらっと書いていますが重要です。標準パッケージのAPIで破壊的変更を修正するツールから、コンパイル上はエラーにならないけど今となっては古く、非推奨になった書き方を変更するツールになりました。先程紹介した、`context` の書き換えに近いことがメインになると考えると、AIが生成した古いコードを変換したりにも便利そうです。 |
| 54 | + |
| 55 | +## 実際に何を書き換えてくれるのか |
| 56 | + |
| 57 | +`go fix` で追加されるモダン化処理ですが、GoのLanguage Serverである `gopls` で使われていた実装が利用できるようです。モダン化とインライン化という2大機能があります。 |
| 58 | + |
| 59 | +1. モダン化([#75266](https://github.com/golang/go/issues/75266)) |
| 60 | + - https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize |
| 61 | + - Go 1.26時点で24個機能がある |
| 62 | + - 詳細は後述 |
| 63 | +2. インライン化([#75267](https://github.com/golang/go/issues/75267)) |
| 64 | + - https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/inline |
| 65 | + - 単一機能 |
| 66 | + - `//go:fix inline` ディレクティブを関数に付与することで、その関数の利用者のコードを自動的にインライン展開して書き換えることができる |
| 67 | + |
| 68 | +ここから先は、実際の動作を見ていきます。 |
| 69 | + |
| 70 | +## 動かしてみる |
| 71 | + |
| 72 | +モダン化からは以下の4つを動かしてみます。 |
| 73 | + |
| 74 | +- **rangeint:** `for i := 0; i < 10; i++` を `for i := range 10` に |
| 75 | +- **minmax:** `if a > b { m = a }` を `m = max(a, b)` に |
| 76 | +- **any:** `interface{}` を `any` に |
| 77 | +- **slicessort:** `sort.Slice` を Go 1.21で追加された `slices.Sort` に |
| 78 | + |
| 79 | +さらにインライン化を試すという流れを考えています。 |
| 80 | + |
| 81 | +### 環境情報 |
| 82 | + |
| 83 | +`1.26rc2` で動かします。RCバージョンの場合、`go.mod` でGoバージョンを1.26にしておく必要があります。 |
| 84 | + |
| 85 | +```bash |
| 86 | +$ go version |
| 87 | +go version go1.26rc2 linux/amd64 |
| 88 | + |
| 89 | +$ go mod edit -go=1.26 |
| 90 | +$ cat go.mod |
| 91 | +module blogsample |
| 92 | + |
| 93 | +go 1.26 |
| 94 | +``` |
| 95 | + |
| 96 | +### 1. モダン化 |
| 97 | + |
| 98 | +ちょっと古くさいファイルを用意します。 |
| 99 | + |
| 100 | +```go main.go |
| 101 | +package main |
| 102 | + |
| 103 | +import ( |
| 104 | + "fmt" |
| 105 | + "sort" |
| 106 | +) |
| 107 | + |
| 108 | +func main() { |
| 109 | + // 1. 古いループ(range over int にできるはず) |
| 110 | + for i := 0; i < 5; i++ { |
| 111 | + fmt.Println(i) |
| 112 | + } |
| 113 | + |
| 114 | + // 2. max(a, b) になるはず |
| 115 | + var m, a, b = 1, 2, 3 |
| 116 | + if a > b { |
| 117 | + m = a |
| 118 | + } else { |
| 119 | + m = b |
| 120 | + } |
| 121 | + fmt.Println(m) |
| 122 | + |
| 123 | + // 3. interface{} (any にできるはず) |
| 124 | + var x interface{} = "hello" |
| 125 | + fmt.Println(x) |
| 126 | + |
| 127 | + // 4. 古いソート (slices.Sort にできるはず) |
| 128 | + nums := []int{3, 1, 2} |
| 129 | + sort.Ints(nums) |
| 130 | + fmt.Println(nums) |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +`go fix` コマンドを実行します。 |
| 135 | + |
| 136 | +```bash |
| 137 | +# go fixの実行 |
| 138 | +$ go fix ./... |
| 139 | +``` |
| 140 | + |
| 141 | +差分です。おー、これがモダン化..!! |
| 142 | + |
| 143 | +```diff |
| 144 | +$ git diff |
| 145 | +diff --git a/modern/main.go b/modern/main.go |
| 146 | +index 4217b13..147e77f 100644 |
| 147 | +--- a/modern/main.go |
| 148 | ++++ b/modern/main.go |
| 149 | +@@ -2,30 +2,26 @@ package main |
| 150 | + |
| 151 | + import ( |
| 152 | + "fmt" |
| 153 | +- "sort" |
| 154 | ++ "slices" |
| 155 | + ) |
| 156 | + |
| 157 | + func main() { |
| 158 | + // 1. 古いループ(range over int にできるはず) |
| 159 | +- for i := 0; i < 5; i++ { |
| 160 | ++ for i := range 5 { |
| 161 | + fmt.Println(i) |
| 162 | + } |
| 163 | + |
| 164 | + // 2. max(a, b) になるはず |
| 165 | + var m, a, b = 1, 2, 3 |
| 166 | +- if a > b { |
| 167 | +- m = a |
| 168 | +- } else { |
| 169 | +- m = b |
| 170 | +- } |
| 171 | ++ m = max(a, b) |
| 172 | + fmt.Println(m) |
| 173 | + |
| 174 | + // 3. interface{} (any にできるはず) |
| 175 | +- var x interface{} = "hello" |
| 176 | ++ var x any = "hello" |
| 177 | + fmt.Println(x) |
| 178 | + |
| 179 | + // 4. 古いソート (slices.Sort にできるはず) |
| 180 | + nums := []int{3, 1, 2} |
| 181 | +- sort.Ints(nums) |
| 182 | ++ slices.Sort(nums) |
| 183 | + fmt.Println(nums) |
| 184 | + } |
| 185 | +``` |
| 186 | + |
| 187 | +`go vet` と似ている使い勝手で、最新のGoの流儀に合わせてくれるのは嬉しい感じがします。 `-diff` コマンドで差分を出すこともできるので、CIでの使い勝手も良いと思います。 |
| 188 | + |
| 189 | +### 2. インライン化 |
| 190 | + |
| 191 | +従来、ライブラリ提供側の視点で、関数の非推奨化はできましたが、あくまで非推奨と伝えるだけで一括で変換などは行えませんでした。インライン化はそれを支援する方法です。 |
| 192 | + |
| 193 | +```go lib/lib.go |
| 194 | +package lib |
| 195 | + |
| 196 | +//go:fix inline |
| 197 | +func OldFunc(s string) string { |
| 198 | + return NewFunc(s, true) |
| 199 | +} |
| 200 | + |
| 201 | +func NewFunc(s string, flag bool) string { |
| 202 | + return fmt.Sprint(s, flag) |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +```go main.go |
| 207 | +package main |
| 208 | +import "your-module/library" |
| 209 | + |
| 210 | +func main() { |
| 211 | + lib.OldFunc("hello") |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +`go fix ./...` を実行すると、`main.go` が以下のように書き換えられます。 |
| 216 | + |
| 217 | +```diff |
| 218 | +$ git diff |
| 219 | +diff --git a/inline/main.go b/inline/main.go |
| 220 | +index ead5e9d..3448403 100644 |
| 221 | +--- a/inline/main.go |
| 222 | ++++ b/inline/main.go |
| 223 | +@@ -5,5 +5,5 @@ import ( |
| 224 | + ) |
| 225 | + |
| 226 | + func main() { |
| 227 | +- lib.OldFunc("hello") |
| 228 | ++ lib.NewFunc("hello", true) |
| 229 | + } |
| 230 | +``` |
| 231 | + |
| 232 | +ライブラリ提供者側の視点としては、公開した関数を変更する場合に機械的にマイグレーションする手段ができたということで、心理的に余裕が生まれるのではないでしょうか。 |
| 233 | + |
| 234 | +ちなみにですが、従来の `// Deprecated` のコメントとの併用も可能です。 |
| 235 | + |
| 236 | +```diff lib/lib.go |
| 237 | ++// Deprecated: Use NewFunc instead. |
| 238 | +//go:fix inline |
| 239 | +func OldFunc(s string) string { |
| 240 | + return NewFunc(s, true) |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +呼び出し側は次のように取り消し線などで、非推奨であることがフィードバックされます。 |
| 245 | + |
| 246 | +<img src="/images/2026/20260129a/image.png" alt="image.png" width="863" height="568" loading="lazy"> |
| 247 | + |
| 248 | +基本的には、 `//go:fix inline` を追加するときは、 `// Deprecated` もセットで運用することになるのかなと予測します。 |
| 249 | + |
| 250 | +## golangci-lintでも --fix オプションがあるけど使い分けは? |
| 251 | + |
| 252 | +`golangci-lint run --fix` などとすれば、Golangci-lintもコードの置換も行ってくれます。 |
| 253 | + |
| 254 | +[リンター一覧](https://golangci-lint.run/docs/linters/)で `Autofix` タグがついているものがその対象です。 |
| 255 | + |
| 256 | +モダン化はいくつか重複している機能もありそうですが、詳しく見ていません。おそらく、衝突するような機能はgolangci-lint 側で無効化/修正されると思いますので(根拠はなく予想です)、まずは `go fix` と `golangci-lint run --fix` の両方を実行してみて試すのが良いのではないかと思いました。 |
| 257 | + |
| 258 | +インライン化は該当の機能はないのでこちらについては `go fix` の利用が必須になるかと思いした。 |
| 259 | + |
| 260 | +## さいごに |
| 261 | + |
| 262 | +コードレビューなどでより新しい書き方をsuggestするというのは、あまり創造的では無いと思っていました。これが `go fix` でかなり省略されるということで、AIが古いコードを出してきても矯正できる点は良いと思います(`go fix` で直してまた古いコードに書き換えられたりはあるかもですが)。 |
| 263 | + |
| 264 | +とりあえず、 `go fmt`、`go vet`、`go fix` の3点セットで使っていこうと思いました。 |
0 commit comments