LoginSignup
109
66
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

【Go】公式ツール "eg" を使って効率的にGoのコードをリファクタリングする

Last updated at Posted at 2024-07-11

はじめに

こんにちは、ken です。お仕事では Go をよく書きます。
最近、Go の公式パッケージであるgolang.org/x/toolsを眺めていたら、なにやら有用そうなパッケージを見つけたので今回はそれについて書こうと思います。
それはegというリファクタリングツールです。

eg とは

eg は、例ベースで Go コードをリファクタリングするためのツールです。このツールを使用することで、特定のコードパターンを別のコードに置き換えることができ、効率的にリファクタリングが行えます。

先ほど貼った公式ドキュメントに詳しい説明があるかと思いきや

The eg command performs example-based refactoring. For documentation, run the command, or see Help in golang.org/x/tools/refactor/eg.

としか書かれてませんでした...(なんでや...)
詳しく知りたかったらヘルプを見てねということだったので指示通りヘルプを出力してみました。1

egツールのヘルプ

This tool implements example-based refactoring of expressions.

The transformation is specified as a Go file defining two functions,
'before' and 'after', of identical types. Each function body consists
of a single statement: either a return statement with a single
(possibly multi-valued) expression, or an expression statement. The
'before' expression specifies a pattern and the 'after' expression its
replacement.

package P

import ( "errors"; "fmt" )
func before(s string) error { return fmt.Errorf("%s", s) }
func after(s string) error { return errors.New(s) }

The expression statement form is useful when the expression has no
result, for example:

func before(msg string) { log.Fatalf("%s", msg) }
func after(msg string) { log.Fatal(msg) }

The parameters of both functions are wildcards that may match any
expression assignable to that type. If the pattern contains multiple
occurrences of the same parameter, each must match the same expression
in the input for the pattern to match. If the replacement contains
multiple occurrences of the same parameter, the expression will be
duplicated, possibly changing the side-effects.

The tool analyses all Go code in the packages specified by the
arguments, replacing all occurrences of the pattern with the
substitution.

So, the transform above would change this input:
err := fmt.Errorf("%s", "error: " + msg)
to this output:
err := errors.New("error: " + msg)

Identifiers, including qualified identifiers (p.X) are considered to
match only if they denote the same object. This allows correct
matching even in the presence of dot imports, named imports and
locally shadowed package names in the input program.

Matching of type syntax is semantic, not syntactic: type syntax in the
pattern matches type syntax in the input if the types are identical.
Thus, func(x int) matches func(y int).

This tool was inspired by other example-based refactoring tools,
'gofmt -r' for Go and Refaster for Java.

LIMITATIONS

EXPRESSIVENESS

Only refactorings that replace one expression with another, regardless
of the expression's context, may be expressed. Refactoring arbitrary
statements (or sequences of statements) is a less well-defined problem
and is less amenable to this approach.

A pattern that contains a function literal (and hence statements)
never matches.

There is no way to generalize over related types, e.g. to express that
a wildcard may have any integer type, for example.

It is not possible to replace an expression by one of a different
type, even in contexts where this is legal, such as x in fmt.Print(x).

The struct literals T{x} and T{K: x} cannot both be matched by a single
template.

SAFETY

Verifying that a transformation does not introduce type errors is very
complex in the general case. An innocuous-looking replacement of one
constant by another (e.g. 1 to 2) may cause type errors relating to
array types and indices, for example. The tool performs only very
superficial checks of type preservation.

IMPORTS

Although the matching algorithm is fully aware of scoping rules, the
replacement algorithm is not, so the replacement code may contain
incorrect identifier syntax for imported objects if there are dot
imports, named imports or locally shadowed package names in the input
program.

Imports are added as needed, but they are not removed as needed.
Run 'goimports' on the modified file for now.

Dot imports are forbidden in the template.

TIPS

Sometimes a little creativity is required to implement the desired
migration. This section lists a few tips and tricks.

To remove the final parameter from a function, temporarily change the
function signature so that the final parameter is variadic, as this
allows legal calls both with and without the argument. Then use eg to
remove the final argument from all callers, and remove the variadic
parameter by hand. The reverse process can be used to add a final
parameter.

To add or remove parameters other than the final one, you must do it in
stages: (1) declare a variant function f' with a different name and the
desired parameters; (2) use eg to transform calls to f into calls to f',
changing the arguments as needed; (3) change the declaration of f to
match f'; (4) use eg to rename f' to f in all calls; (5) delete f'.

長いので折りたたんでおきますが、気になる人は読んでみてください。
一応ここから先はこのヘルプを読んでいないという前提で eg を紹介するので、面倒な方は読まなくても OK です。

実際につかってみる

eg ツールを使用するには、まず以下のコマンドでインストールします

go install golang.org/x/tools/cmd/eg@latest

次に、変換のルールを定義するテンプレートファイルを作成します。例えば、fmt.Errorferrors.Newに置き換える場合、以下のようなtemplate.goを作成します。beforeafterという名前が直感的でいいですね。

template.go
package template1

import (
	"errors"
	"fmt"
)

func before(s string) error { return fmt.Errorf("%s", s) }
func after(s string)  error { return errors.New(s) }

そして、以下のコマンドを実行します。

eg -t {テンプレートのファイルのパス} -w {変換対象となるコードへのパス}

すると、プロジェクト内のfmt.Errorfが自動的にerrors.Newへと更新されます。
例えば以下のようなファイルがあったとすれば、それは次のように変換されます。単にステートメントが置き換わるだけでなく、もともとなかったerrorsパッケージの import も追加されています...!

sample1.go(Before)
package samples

import (
	"fmt"
)

func sample() {
    msg := "something went wrong"
    err := fmt.Errorf("%s", "error: " + msg)
    fmt.Println(err)
}
sample1.go(After)
package samples

import (
	"errors"
	"fmt"
)

func sample() {
	msg := "something went wrong"
	err := errors.New("error: " + msg)
	fmt.Println(err)
}

実際に実行している様子を映した GIF 画像も置いておきます。

eg.gif

ちなみに eg ツールにはいくつかのオプションがあります。先ほどつけていた-wは任意のオプションで、これをつけるとファイルを直接書き換えることになります。このオプションを使用しない場合、変更は標準出力に表示されるだけになります。

Usage: eg -t template.go [-w] [-transitive] <packages>

-help            show detailed help message
-t template.go   specifies the template file (use -help to see explanation)
-w               causes files to be re-written in place.
-transitive      causes all dependencies to be refactored too.
-v               show verbose matcher diagnostics
-beforeedit cmd  a command to exec before each file is modified.
                 "{}" represents the name of the file.

具体的なユースケース

API の更新

eg ツールは deprecated となったライブラリ内の関数を置き換える際に便利そうです。例えば、Go1.16 でioutilパッケージが非推奨となり、多くの機能がosパッケージに移行されましたが、その移行も eg ツールを使えば楽にできます。

template2
package template2

import (
    "io/ioutil"
    "os"
)

func before(filename string) ([]byte, error) {
    return ioutil.ReadFile(filename)
}
func after(filename string) ([]byte, error) {
    return os.ReadFile(filename)
}

また、社内で使っているライブラリを新調したいときにも使えそうです。特にロギングライブラリはプロジェクト内の至るところで使われていると思うので、手動で行うよりも eg ツールを使って一括で行ったほうが効率良く置換できそうです。

package templates

import (
    "original/logger"
    "original/newLogger"
)

func before(msg string) {
    return logger.Info(msg)
}

func after(msg string) {
    return newLogger.Info(msg)
}

コード品質の統一

不適切なコードをプロジェクト内から一掃するときにも eg ツールは有用です。例えば、time.Time型のゼロ値判定について考えてみましょう。

与えられたtime.Time型の変数がゼロ値かどうかを判定する際には、一般的にはIsZero()メソッドを使用するのが推奨されていますが、このメソッドの存在を知らない開発者がt != time.Time{}という比較を行うことはあり得なくもないです。

この二つの方法は同じ結果をもたらしますが、プロジェクト内でコードの記述方法が統一されていないことに対して良く思わない方もいるでしょう。
eg ツールを使用すれば、このような問題も簡単に解決できます。繰り返しになりますが、以下のようなテンプレートファイルを作成し、eg コマンドを実行するだけで良いのです。

template3
package template3

import "time"

func before(t time.Time) bool {
    return t != time.Time{}
}
func after(t time.Time) bool {
    return !t.IsZero()
}

エディタの一括置換より優れている点

「エディタの一括置換を使えばこんなの使わなくていいんじゃないの?」と思った方もいると思います。しかし、eg ツールにはテキストエディタで行うような単純な置換に比べていくつかの利点があります。

Go の文法を考慮した変換が可能

eg は Go の文法を理解するため、単純な文字列置換では難しい、コンテキストに応じた置換が可能です。これにより同じ名前の変数や関数であっても、それぞれが使用されるコンテキストに基づいて適切に置換できます。例えば、次の例では変数名が違っていても同じ処理であれば置換をしてくれています。

template4
package template4

func before(x int) int { return x + x }
func after(x int) int { return 2 * x }
sample4.go(Before)
package samples

func double(x int, y int) (int, int) {
    doubledX := x + x
    doubledY := y + y
    return doubledX, doubledY
}
sample4.go(After)
package samples

func double(x int, y int) (int, int) {
    doubledX := 2 * x
    doubledY := 2 * y // 変数名がyであっても変換されている
    return doubledX, doubledY
}

型の考慮

変数や関数の型を考慮した置換ができるため、より安全なリファクタリングが可能です。eg は型情報を利用して必要な置換のみを行います。これにより、見た目上は同じ式であっても引数の型が異なる場合は置換が行われません。

template5
package template5

func before(x int) int { return x + x }
func after(x int) int { return 2 * x }
sample5.go(Before)
package samples

func doubleInt(x int) int { return x + x }
func doubleFloat(x float64) float64 { return x + x }
sample5.go(After)
package samples

func doubleInt(x int) int { return 2 * x } // 変更あり
func doubleFloat(x float64) float64 { return x + x } // 変更なし(型が異なるため)

インポートの自動管理

先ほどのfmt.Errorferrors.Newに置き換えるケースでも見ましたが、eg ツールは必要なインポートを自動的に追加してくれます。
ただし、不要になったインポートの削除は行わないことに注意が必要です。公式からは修正されたファイルに対してgoimportsを実行することが推奨されています。(最初に貼ったヘルプ参照)

template6
package template6

import (
    "fmt"
    "strconv"
)

func before(x int) { fmt.Println(x) }
func after(x int) { fmt.Println(strconv.Itoa(x)) }
sample6.go(Before)
package samples

import "fmt"

func main() {
    fmt.Println(0)
}
sample6.go(After)
package samples

import (
    "fmt"
    "strconv" // 自動的に追加されている
)

func main() {
    fmt.Println(strconv.Itoa(0))
}

再現性

テンプレートファイルを用いることで、同じ変換を他のプロジェクトや開発者と共有しやすくなります。これにより、チーム全体で一貫したリファクタリングを行うことが可能になります。

shared_template.go
package main

import "time"

func before(t time.Time) bool { return t != time.Time{} }
func after(t time.Time) bool { return !t.IsZero() }

// 使用例
// $ eg -t shared_template.go -w ./...
// これにより、プロジェクト全体で time.Time がゼロ値かどうかを判定する方法が統一されます

さいごに

今回はgolang.org/x/tools内にある eg ツールについて紹介してみました。
このツールはあまり知られていないようで執筆時点では紹介記事がほとんどありませんでしたが、かなり便利な使い方ができそうなので頭の片隅に入れておいても良いかなと思いました。
今回の記事で書いたコードは GitHub にあげているので、良かったらご自身でも動かしてみてください。

ここまで読んでいただきありがとうございました。間違いなどあればコメントにてご指摘ください。

  1. go installでの eg ツールのインストール後、eg -helpで出力できます。

109
66
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
109
66