最近、AIにコードを書かせる話は、全部かゼロかで語られすぎだと感じます。丸ごと任せてアプリを生やす話は派手ですが、実際に毎日使う道具を書く場面だと、あのやり方はあまり長続きしません。特にGoはそこがはっきりしていて、退屈な骨組みだけ渡してもらうぶんには便利ですが、設計まで預けると急に濁ります。
週末に小さなログ整形CLIを書いたとき、私は最初の `flag` 定義と JSON 設定読み込みだけAIに下書きさせました。そこから自分で削っていくほうが早かったです。足すより削るほうが楽だった、という感じです。最終的に残ったのは、最初に出てきた量の半分くらいでした。
任せる場所を狭くする
AIが得意なのは、だいたい同じ形のところです。`flag.String` を並べるところ、`json.Unmarshal` のまわり、`if err != nil` が続くところ、そのあたりはかなり素直に出ます。こちらも別にそこへ思想を込めたいわけではないので、下書きとしては十分です。
ただ、そこから先は急に怪しくなります。ファイルをどこに置くのか、失敗したときにどこまで黙るのか、毎朝叩く道具なら出力を何行で止めるのか。そういう癖の部分までまとめて生成させると、動いてはいても自分の道具ではなくなります。非エンジニアがバイブコーディングで一気に形にしたものが、少し崩れただけで触れなくなるのは、たぶんこの境目を飛び越えているからです。
Goの下書きは削る前提で使う
Goで気楽なのは、削ったあとに形が整いやすいところです。PythonやJavaScriptだと、AIが気を利かせて足した抽象化がしぶとく残ることがあります。Goはそこまで飾れません。関数を減らし、構造体を減らし、依存を減らしていくと、ちゃんと薄くなっていきます。私はこの感じがかなり好きです。現代のC言語っぽいと思うのは、こういう場面です。
たとえば、設定ファイルと引数を受け取るだけの骨組みなら、このくらいで十分です。
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
)
type Config struct {
Path string `json:"path"`
Limit int `json:"limit"`
}
func loadConfig(file string) (Config, error) {
b, err := os.ReadFile(file)
if err != nil {
return Config{}, err
}
var cfg Config
if err := json.Unmarshal(b, &cfg); err != nil {
return Config{}, err
}
return cfg, nil
}
func main() {
configFile := flag.String("config", "config.json", "config file")
limit := flag.Int("limit", 5, "max rows")
flag.Parse()
cfg, err := loadConfig(*configFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if *limit > 0 {
cfg.Limit = *limit
}
fmt.Printf("path=%s limit=%d\n", cfg.Path, cfg.Limit)
}
この程度なら、AIに最初の形だけ出させても問題ありません。むしろ自分でゼロから打つより少しだけ気が楽です。ただし、そのまま採用はしません。エラーメッセージの出し方、デフォルト値、引数名の雑さは、最後に自分の手で揃えます。ここを触らないと、コードは動いても手触りが借り物のままです。
速さより、どこを自分で持つか
私は最近、AIには考える前の作業を渡して、考える作業だけ手元に残すほうが性に合っていると思っています。フレームワークの儀式や定型文を全部手打ちしたいわけではありません。そこはもう任せていいです。でも、何を残して何を削るかまで渡してしまうと、最後に残るのは便利さではなく他人行儀さです。
だから、AIにコードを書かせるときは、能力より範囲を先に決めたほうがいいです。全部書けるかどうかより、この一時間の退屈だけ引き受けてもらえるかのほうが大事です。Goはその線引きがしやすいです。骨だけ出してもらって、自分で肉を落とすと、ちゃんと自分の道具が残ります。