フューチャー技術ブログ

Go 1.26で go fix が面白くなった

はじめに

TIG(Technology Innovation Group)の真野です。

Go 1.26ブログ連載 の3日目は、go fix コマンドのアップデートについて解説します。

go fix の出発点

リリース内容へ入る前に、go fix そのものについて解説します。

まず、Go 1.26で新しく go fix コマンドが追加されたわけではありません。コマンド自体は2011年4月15日に公開されたIntroducing Gofixというブログで紹介され、その翌月リリースのr57 で追加されました。つまり、誕生から15年近く経過する由緒ある(?)ツールとも言えます。

それにも関わらず、 go fix コマンドはあまり有名で無いと思います。なぜでしょうか。

まず、Go言語が1.0に到達したのは、2012年3月28日で、go fix コマンドが紹介されたのはその1年前です。現在と大きく異なり、正式版に向けて週次ベースでリリースしており、破壊的な仕様変更も行われていた時期です。その中には httpos といった利用頻度が高いパッケージのAPI変更も含まれるため、開発者としては追随が大変でした。

go fix の出自は、こうしたAPIの破壊的変更にともなる既存コードの書き換えすることです(Goチーム公式からバージョンアップに伴う移行ツールが提供されていたという訳で、ホスピタリティが凄さを感じます)。もちろん、バイナリを書き換えるわけではないので再ビルドが必要ですが、利用者視点では手間という面で、このツールがあるのと無いでは大きな差でしょう。Go開発チームとしても、 go fix という変換ツールがあるこそ、API変更のコストに囚われすぎずより良い言語にすることが集中できた(意訳)という訳で、当時はとても重要な位置づけでした。

バージョン1.0以降の go fix

一方で、Goはバージョンが1.0に到達してからは破壊的な変更が行われなくなりました。

これ自体は良いことですが、go fix の存在感は低下しました。今となっては不要では?みたいな声を聞いたこともあります。実際、1.0以降のリリースノートで go fix の更新は、context の書き換えくらいでした(他にも見落としていたらすいません)。

  • Go 1.8Go 1.10
    • golang.org/x/net/contextcontext に書き換える機能が追加

これ以降は更新が無かったため、ほとんどの開発者の意識から外れていたのではないでしょうか。 go fmtgo vet は広く活用されていたのに、不憫な子。

Go 1.26での go fix

さて、Go 1.26です。

リリースノートやIssueであるcmd/go: fix: apply fixes from modernizers, inline, and other analyzers #71859 を読むと、以下の点が更新されました。

  1. go fix の内部実装が golang.org/x/tools/go/analysis という、go vetgopls が利用しているのと同じフレームワークを使うようになった。型情報や変数のスコープなどを理解して安全な書き換えが可能になるとのこと
  2. 従来の機能は廃止された
  3. 新しい文法や書き方に自動的に書き換える(モダン化する)ツールになった

特に3点目はさらっと書いていますが重要です。標準パッケージのAPIで破壊的変更を修正するツールから、コンパイル上はエラーにならないけど今となっては古く、非推奨になった書き方を変更するツールになりました。先程紹介した、context の書き換えに近いことがメインになると考えると、AIが生成した古いコードを変換したりにも便利そうです。

実際に何を書き換えてくれるのか

go fix で追加されるモダン化処理ですが、GoのLanguage Serverである gopls で使われていた実装が利用できるようです。モダン化とインライン化という2大機能があります。

  1. モダン化(#75266
  2. インライン化(#75267

ここから先は、実際の動作を見ていきます。

動かしてみる

モダン化からは以下の4つを動かしてみます。

  • rangeint: for i := 0; i < 10; i++for i := range 10
  • minmax: if a > b { m = a }m = max(a, b)
  • any: interface{}any
  • slicessort: sort.Slice を Go 1.21で追加された slices.Sort

さらにインライン化を試すという流れを考えています。

環境情報

1.26rc2 で動かします。RCバージョンの場合、go.mod でGoバージョンを1.26にしておく必要があります。

$ go version
go version go1.26rc2 linux/amd64

$ go mod edit -go=1.26
$ cat go.mod
module blogsample

go 1.26

1. モダン化

ちょっと古くさいファイルを用意します。

main.go
package main

import (
"fmt"
"sort"
)

func main() {
// 1. 古いループ(range over int にできるはず)
for i := 0; i < 5; i++ {
fmt.Println(i)
}

// 2. max(a, b) になるはず
var m, a, b = 1, 2, 3
if a > b {
m = a
} else {
m = b
}
fmt.Println(m)

// 3. interface{} (any にできるはず)
var x interface{} = "hello"
fmt.Println(x)

// 4. 古いソート (slices.Sort にできるはず)
nums := []int{3, 1, 2}
sort.Ints(nums)
fmt.Println(nums)
}

go fix コマンドを実行します。

# go fixの実行
$ go fix ./...

差分です。おー、これがモダン化..!!

$ git diff
diff --git a/modern/main.go b/modern/main.go
index 4217b13..147e77f 100644
--- a/modern/main.go
+++ b/modern/main.go
@@ -2,30 +2,26 @@ package main

import (
"fmt"
- "sort"
+ "slices"
)

func main() {
// 1. 古いループ(range over int にできるはず)
- for i := 0; i < 5; i++ {
+ for i := range 5 {
fmt.Println(i)
}

// 2. max(a, b) になるはず
var m, a, b = 1, 2, 3
- if a > b {
- m = a
- } else {
- m = b
- }
+ m = max(a, b)
fmt.Println(m)

// 3. interface{} (any にできるはず)
- var x interface{} = "hello"
+ var x any = "hello"
fmt.Println(x)

// 4. 古いソート (slices.Sort にできるはず)
nums := []int{3, 1, 2}
- sort.Ints(nums)
+ slices.Sort(nums)
fmt.Println(nums)
}

go vet と似ている使い勝手で、最新のGoの流儀に合わせてくれるのは嬉しい感じがします。 -diff コマンドで差分を出すこともできるので、CIでの使い勝手も良いと思います。

2. インライン化

従来、ライブラリ提供側の視点で、関数の非推奨化はできましたが、あくまで非推奨と伝えるだけで一括で変換などは行えませんでした。インライン化はそれを支援する方法です。

lib/lib.go
package lib

//go:fix inline
func OldFunc(s string) string {
return NewFunc(s, true)
}

func NewFunc(s string, flag bool) string {
return fmt.Sprint(s, flag)
}
main.go
package main
import "your-module/library"

func main() {
lib.OldFunc("hello")
}

go fix ./... を実行すると、main.go が以下のように書き換えられます。

$ git diff
diff --git a/inline/main.go b/inline/main.go
index ead5e9d..3448403 100644
--- a/inline/main.go
+++ b/inline/main.go
@@ -5,5 +5,5 @@ import (
)

func main() {
- lib.OldFunc("hello")
+ lib.NewFunc("hello", true)
}

ライブラリ提供者側の視点としては、公開した関数を変更する場合に機械的にマイグレーションする手段ができたということで、心理的に余裕が生まれるのではないでしょうか。

ちなみにですが、従来の // Deprecated のコメントとの併用も可能です。

lib/lib.go
+// Deprecated: Use NewFunc instead.
//go:fix inline
func OldFunc(s string) string {
return NewFunc(s, true)
}

呼び出し側は次のように取り消し線などで、非推奨であることがフィードバックされます。

image.png

基本的には、 //go:fix inline を追加するときは、 // Deprecated もセットで運用することになるのかなと予測します。

golangci-lintでも –fix オプションがあるけど使い分けは?

golangci-lint run --fix などとすれば、Golangci-lintもコードの置換も行ってくれます。

リンター一覧Autofix タグがついているものがその対象です。

モダン化はいくつか重複している機能もありそうですが、詳しく見ていません。おそらく、衝突するような機能はgolangci-lint 側で無効化/修正されると思いますので(根拠はなく予想です)、まずは go fixgolangci-lint run --fix の両方を実行してみて試すのが良いのではないかと思いました。

インライン化は該当の機能はないのでこちらについては go fix の利用が必須になるかと思いした。

さいごに

コードレビューなどでより新しい書き方をsuggestするというのは、あまり創造的では無いと思っていました。これが go fix でかなり省略されるということで、AIが古いコードを出してきても矯正できる点は良いと思います(go fix で直してまた古いコードに書き換えられたりはあるかもですが)。

とりあえず、 go fmtgo vetgo fix の3点セットで使っていこうと思いました。