TL; DR
ガイドラインをskillに直書きせずCLI経由で取得することで
- 特定バージョンに適用可能なガイドラインのみ絞り込む
- コンテキスト消費を減らす
この記事より、以下の公式記事を読んだ方が分かりやすいとおもいます
はじめに
go-modern-guidelinesは、Goのソースコードを書き方を現代風に書き直すためのAgent Skillです。
GoLandやIntellij等でおなじみJetBrains社が開発しています。
昔は言語仕様上冗長な書き方をしていたが、新しいバージョンではもっと簡潔な書き方ができる箇所を修正します1。
例えば、以下のソースコードがあったとします。
*int 型として 1 のポインタを得るためにstartという一時変数を使っています。
func main() {
start := 1
vr := &ValueRange{
Start: &start,
End: 10,
}
fmt.Printf("%v\n", vr)
}
modern-go-guidelinesで書き直してみます。
/modern-go-guidelines:use-modern-go main.go
new(1) に書き直されました。
Go1.26から、new() に式を直接渡せるようになり、リテラルからポインタを作れるようになったのを利用しています。
func main() {
vr := &ValueRange{
Start: new(1),
End: 10,
}
fmt.Printf("%v\n", vr)
}
このスキルの実装を見ていると、スキルのMarkdownファイルの他にCLIを使用しているのを見つけました。
本記事では、このCLIがどのようにスキルと連携しているのかを紹介します。
スキルが呼び出すCLI
スキルのinstructionには、コマンドの使い方のみが書かれており、ガイドラインとなるGoの書き方はCLIへ移譲しています。
# Modern Go Guidelines CLI
常に現代的で自然なGoコードを書いてください。Modern Go Guidelines CLIを、あなたの知識カットオフよりも新しいかもしれない現代的なGo イディオムに対する信頼できる情報源(source of truth)としてください。
Command:
- Linux or macOS: `sh "<skill-dir>/scripts/run-tool.sh"`
- Windows PowerShell: `'<skill-dir>\scripts\run-tool.ps1'`
...
Subcommands:
- `list`
- `explain`
Goコードの編集前に:
1. 関連するGoファイルに対してラッパーコマンドの `list` サブコマンドを実行
...
2. ターゲットのGoバージョンが既知であれば、バージョンを(コマンドに)直接指定
...
3. どのガイドラインを適用するか決める前に、出力された完全なリストを最後まで読む
...
4. 編集しているGoコードに対して、(上記で)得られたガイドラインを信頼できる現代的なGoスタイルとして扱う
...
呼び出されるコマンドはラッパーで、その中でさらにgo-modern-guidelines というCLIを呼び出しています。
CLIの動作
本体の go-modern-guidelines を見ていきます。スキルにあったように、
- list
- explain
の2つの機能があります。
一覧表示
go-modern-guidelines list は、ガイドラインの一覧を返します。
$ ./go-modern-guidelines list
generic_methods: Use generic methods instead of package-level generic helper functions when the operation naturally belongs to the type itself.
json_v2: Use `encoding/json/v2` for new JSON code in Go 1.27+; leave existing `encoding/json` code unchanged unless migration is explicitly requested.
promoted_field_literals: Set embedded struct fields directly with promoted field names in Go 1.27+ struct literals instead of constructing the embedded struct explicitly.
strings_bytes_cut_last: Use `strings.CutLast` and `bytes.CutLast` instead of `LastIndex` plus manual slicing around the last separator.
stdlib_uuid: Use the standard library `uuid` package instead of third-party libraries or custom UUID implementations when targeting Go 1.27+.
url_clone: Use the `URL.Clone` and `Values.Clone` methods from `net/url` to copy URLs and `URL` values instead of manual copying.
new_expression: Use `new(value)` for pointer fields or arguments instead of generic/type-specific pointer helper functions or temporary variables used only for `&value`.
errors_as_type: Use `errors.AsType[T](err)` when checking whether an error matches a specific type.
sync_waitgroup_go: Use `wg.Go` when spawning goroutines tracked by a `sync.WaitGroup`.
testing_t_context: Use `t.Context()` when a test function needs a context tied to the test lifetime.
json_omitzero: Use `omitzero` on JSON-tagged bool, numeric, struct, and time fields whose zero value should be omitted; keep `omitempty` for empty strings, slices, and maps.
testing_b_loop: Use `b.Loop()` for the main loop in benchmark functions.
strings_split_seq: Use `strings.SplitSeq`, `strings.FieldsSeq`, `bytes.SplitSeq`, or `bytes.FieldsSeq` when iterating over split results.
maps_keys_values_iter: Use `maps.Keys` or `maps.Values` directly as iterators instead of manually looping over a map.
slices_collect: Use `slices.Collect` to build a slice from an iterator.
slices_sorted: Use `slices.Sorted` to collect and sort iterator values in one step.
time_tick_gc: Use `time.Tick` when it fits; Go 1.23 can recover unreferenced tickers without requiring `Stop` for GC.
range_over_int: Use `for i := range n` when iterating from `0` to `n-1`.
loopvar_capture: Do not add redundant loop-variable copies before closures or taking addresses; Go 1.22 gives each iteration its own variables.
cmp_or: Use `cmp.Or` to pick the first non-zero value from a fallback chain.
reflect_type_for: Use `reflect.TypeFor[T]()` instead of `reflect.TypeOf((*T)(nil)).Elem()`.
http_servemux_patterns: Use method-aware `ServeMux` patterns and `r.PathValue` for path parameters.
min_max: Use built-in `min` and `max` instead of handwritten comparisons.
clear: Use `clear(m)` to delete all map entries or `clear(s)` to zero slice elements.
slices_contains: Use `slices.Contains` instead of a manual search loop.
slices_index: Use `slices.Index` to find the index of an element, returning `-1` when absent.
slices_index_func: Use `slices.IndexFunc` to find an element by predicate.
slices_sort_func: Use `slices.SortFunc` with `cmp.Compare` instead of `sort.Slice` for typed comparisons.
slices_sort: Use `slices.Sort` for slices of ordered values.
slices_max_min: Use `slices.Max` and `slices.Min` instead of manual loops over ordered values.
slices_reverse: Use `slices.Reverse` instead of a manual swap loop.
slices_compact: Use `slices.Compact` to remove consecutive duplicates in place.
slices_clip: Use `slices.Clip` to remove unused capacity.
slices_clone: Use `slices.Clone` to copy a slice.
maps_clone: Use `maps.Clone` instead of manual map iteration.
maps_copy: Use `maps.Copy` to copy entries from one map into another.
maps_delete_func: Use `maps.DeleteFunc` to delete map entries that match a predicate.
sync_once_func: Use `sync.OnceFunc` instead of `sync.Once` plus a wrapper closure.
sync_once_value: Use `sync.OnceValue` to memoize a computed value.
context_after_func: Use `context.AfterFunc` to run cleanup when a context is canceled.
context_timeout_deadline_cause: Use timeout and deadline contexts with causes when callers need to inspect the cancellation reason.
bytes_clone: Use `bytes.Clone` to copy a byte slice.
strings_cut_prefix_suffix: Use `strings.CutPrefix` or `strings.CutSuffix` when you need both the trimmed result and whether it matched.
errors_join: Use `errors.Join` to combine multiple errors while preserving error matching.
context_cancel_cause: Use `context.WithCancelCause` and `context.Cause` when cancellation needs to carry an error cause.
fmt_appendf: Use `fmt.Appendf` when appending formatted text to a byte slice and an intermediate `fmt.Sprintf` string is unnecessary.
atomic_types: Use typed atomics such as `atomic.Bool`, `atomic.Int64`, and `atomic.Pointer[T]` instead of untyped atomic functions.
any: Use `any` instead of `interface{}`.
bytes_cut: Use `bytes.Cut` instead of `bytes.Index` plus manual slicing.
strings_clone: Use `strings.Clone` to copy a string without retaining shared backing memory.
strings_cut: Use `strings.Cut` instead of `strings.Index` plus manual slicing.
errors_is: Use `errors.Is(err, target)` instead of `err == target` so wrapped errors are handled correctly.
time_until: Use `time.Until(deadline)` instead of `deadline.Sub(time.Now())`.
time_since: Use `time.Since(start)` instead of `time.Now().Sub(start)`.
冒頭で使用されたnew関数によるポインタ生成は
new_expression: Use `new(value)` for pointer fields or arguments instead of generic/type-specific pointer helper functions or temporary variables used only for `&value`.
のガイドラインによるものです。
概要を箇条書きにし、今回関係あるガイドラインのみ別途詳細を取得することでコンテキスト消費を減らしています。
これだけならskillファイルを分割した場合と変わりませんが、go-modern-guidelines コマンドでは特定のGoバージョンが対応しているガイドラインのみ出力することが可能です。
例えば、Go1.20のガイドラインは以下の通りです。
$ ./go-modern-guidelines list --go-version 1.20
bytes_clone: Use `bytes.Clone` to copy a byte slice.
strings_cut_prefix_suffix: Use `strings.CutPrefix` or `strings.CutSuffix` when you need both the trimmed result and whether it matched.
errors_join: Use `errors.Join` to combine multiple errors while preserving error matching.
context_cancel_cause: Use `context.WithCancelCause` and `context.Cause` when cancellation needs to carry an error cause.
fmt_appendf: Use `fmt.Appendf` when appending formatted text to a byte slice and an intermediate `fmt.Sprintf` string is unnecessary.
atomic_types: Use typed atomics such as `atomic.Bool`, `atomic.Int64`, and `atomic.Pointer[T]` instead of untyped atomic functions.
any: Use `any` instead of `interface{}`.
bytes_cut: Use `bytes.Cut` instead of `bytes.Index` plus manual slicing.
strings_clone: Use `strings.Clone` to copy a string without retaining shared backing memory.
strings_cut: Use `strings.Cut` instead of `strings.Index` plus manual slicing.
errors_is: Use `errors.Is(err, target)` instead of `err == target` so wrapped errors are handled correctly.
time_until: Use `time.Until(deadline)` instead of `deadline.Sub(time.Now())`.
time_since: Use `time.Since(start)` instead of `time.Now().Sub(start)`.
new_expressionのガイドラインが消えています。
Go 1.20にもうっかりnew(1) と書いてコンパイルエラーになる心配はありません2。
項目の詳細説明
関連しそうな項目を見つけたタイミングで、改めてガイドラインの詳細をgo-modern-guidelines explainで参照します。
`explain` は、特定のガイドラインが適用可能で、かつ詳細な説明や例が欲しい場合のみ実行してください。評価、または適用したいガイドラインIDのみリクエストしてください:
```
sh "<skill-dir>/scripts/run-tool.sh" explain sync_waitgroup_go
```
ガイドラインの中身は以下の通りです。詳細説明やサンプルコードが出力されます。
$ ./go-modern-guidelines explain new_expression
new_expression:
Since: Go 1.26
Summary:
Use `new(value)` for pointer fields or arguments instead of generic/type-specific pointer helper functions or temporary variables used only for `&value`.
Details:
`new(value)` creates a `*T` from a value expression. In struct literals, prefer `Field: new(value)` for pointer fields over helper calls whose only purpose is returning `&value`; keep helpers only when they add behavior.
Examples:
Example 1:
Before:
func Pointer[T any](value T) *T {
return &value
}
cfg := Config{
Timeout: Pointer(30),
Debug: Pointer(true),
}
After:
cfg := Config{
Timeout: new(30),
Debug: new(true),
}
Example 2:
Before:
timeout := 30
debug := true
cfg := Config{
Timeout: &timeout,
Debug: &debug,
}
After:
cfg := Config{
Timeout: new(30),
Debug: new(true),
}
これを読んでスキルがコード修正を適用した結果、冒頭の修正が行われます。
おわりに
以上、go-modern-guidelines のスキル内で利用されるCLIについての紹介でした。
本スキルはGo向けですが、バージョンごとに適用するルールを変えたい場面で広く応用できる設計だと感じました。
言語、ライブラリ、フレームワーク等のスキルを開発されている方は参考にしてみてはいかがでしょうか?