AIにコードを書かせるのが当たり前になってきました。
実際、Goでもそれっぽいコードはかなりの精度で出力されます。
CLI、Web API、バッチ、並行処理のひな形まで、非常に速く作れます。
しかしその一方で強く感じるのは、動くコードを書けることと壊れにくいコードを育てられることは別だということです。
特にGoは、文法が比較的シンプルなぶん、表面上は読めた気になるのに、概念を誤解すると事故につながりやすい言語でもあります。
AIに任せる時代だからこそ、すべてを暗記する必要はなくても、ここだけは理解しておくべきという土台は存在します。
この記事では、AI時代にGoを書くなら最低限押さえておきたい概念を、実務目線でまとめます。
この記事で言いたいこと
Goで本当に理解しておくべきなのは、細かい文法よりも次のような概念です。
-
sliceは配列のコピーではなく、参照のようなもの -
interfaceは継承ではなく、振る舞いの約束 -
contextはただの引数ではなく、処理の寿命 -
errorは例外の代替ではなく、値 -
goroutineは軽いからといって雑に増やしていいわけではない - 並行処理では、たまたま動く状態が一番危ない
このあたりを理解していると、AIが生成したコードをそのまま信じずに、このコードはなぜ動くのか、どこで壊れうるのかを判断しやすくなります。
1. slice は配列そのものではない
Goを書き始めた人が早めにハマるのが slice です。
見た目が配列に似ているため、値がコピーされるものと思いがちです。
しかし実際には、sliceは配列の一部を指すディスクリプタ(記述子)です。
つまり、sliceを切っても、中身のデータが自動でコピーされるわけではありません。
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4}
b := a[:2]
b[0] = 999
fmt.Println(a) // [999 2 3 4]
fmt.Println(b) // [999 2]
}
このコードで重要なのは、b を変更したのに a も変わることです。
これは b が a の一部を参照しているからです。
なぜこれを理解すべきか
AIはsliceを使ったコードを大量に生成します。そして、ぱっと見では正しそうに見えます。
しかし実務では、以下のような問題が普通に起こります。
- 関数に渡したsliceの変更が呼び出し元に波及する
- 一部だけ必要なのに大きな配列を握り続けてメモリを無駄にする
-
appendで再利用されると思ったら別領域に逃げる
Goで配列のようなものを見たら、まずこれは値なのか、共有されたビューなのかを考える癖をつけるだけで、事故は大幅に減ります。
2. interface は継承ではなく振る舞い
Goの interface を、他言語のオブジェクト指向の前提で読むと認識がズレます。
Goの interface は、このメソッド群を持っていれば、この型として扱えるという仕組みです。
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct{}
func (Dog) Speak() string {
return "wan"
}
func say(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
d := Dog{}
say(d)
}
Dog は Speaker を明示的に implements していません。
それでも Speak() を持っているので Speaker として扱えます。
なぜこれを理解すべきか
AIが生成したコードでありがちなのが、必要以上にinterfaceを増やすことです。
Goでは、interfaceは先に設計して全体に配るものではなく、使う側が必要な最小限だけ切るほうがきれいにまとまることが多いです。
悪い例は以下の通りです。
type UserService interface {
CreateUser(name string) error
UpdateUser(id int, name string) error
DeleteUser(id int) error
FindUser(id int) (*User, error)
}
一見それらしいですが、これが本当に必要かは別問題です。
Goでは巨大なinterfaceより、以下のような小さいinterfaceのほうが自然な場面が多いです。
type UserFinder interface {
FindUser(id int) (*User, error)
}
抽象化のためにinterfaceを置くのではなく、差し替えたい振る舞いだけを抽象化する。
これを理解していると、AIが出力した過剰設計のコードを見抜けます。
3. context はおまじないではなく処理の寿命
GoのWebアプリやAPIのコードで、何も考えずに ctx context.Context を先頭引数に置くケースをよく見ます。
しかし、context は様式美ではありません。
キャンセル・期限・リクエストスコープの値を、処理の流れ全体に伝えるための仕組みです。
func fetchUser(ctx context.Context, id string) (*User, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com/users/"+id, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// ...
return &User{}, nil
}
なぜこれを理解すべきか
context を理解していないと、AIが書いたコードはだいたい次のいずれかになります。
-
context.Background()を深い階層で勝手に作る - タイムアウトを設定したのに、下位処理へ伝搬しない
- キャンセルされたのにgoroutineだけ生き残る
- 何でも
context.Value()に入れてしまう
Goの context は処理の寿命を上流から下流へ伝えるものです。
とりあえず付けるのではなく、この処理はいつ中止されるべきかまで考えられると、コードの質が一段上がります。
4. error は例外ではなく値
Goを触り始めたとき、if err != nil が多くて面倒だと感じる人は多いです。
しかしGoの思想は明確で、errorは値として扱うものです。公式でも "Errors are values" として、単なるボイラープレートではなく値として設計・扱うことの重要性が説明されています。
user, err := repo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("find user by id %s: %w", id, err)
}
Go 1.13以降は、%w によるラップと errors.Is / errors.As が標準的な手法になりました。
ラップしたエラーは呼び出し側でチェーンをたどって判定できます。
if err := doSomething(); err != nil {
if errors.Is(err, ErrNotFound) {
// not found 向けの処理
}
return err
}
なぜこれを理解すべきか
AIはエラー処理を雑に書きがちです。
例えば以下のようなコードが頻繁に出力されます。
- 文脈のない
return err - 何でも
%wで包む(内部実装の詳細を外へ漏らすべきではない場面でも) - sentinel error(定義済みエラー)と型付きerrorの使い分けが曖昧
- ログに出して返し、上の階層でもまたログに出す
Goのerrorで大事なのは、失敗したことだけでなく、呼び出し側に何を判断させたいかです。
- 再試行してほしいのか
- 404相当なのか
- 入力不正なのか
- 内部障害なのか
- ラップして公開してよい内部情報なのか
ここが見えていると、AIの生成コードを「動くけど運用で困るコード」のまま採用せずに済みます。
5. goroutine は軽い。でも無料ではない
Goの魅力のひとつは goroutine です。軽量で、並行処理をシンプルに書けます。
ただし、軽いからといって雑に増やしてよいわけではありません。
Goのメモリモデルでは、あるgoroutineの書き込みが別goroutineから必ず見えるようにするためには、チャネル・ロック・atomicなどによる同期が必要です。
package main
import (
"fmt"
"time"
)
var n int
func main() {
go func() {
n = 1
}()
go func() {
n = 2
}()
time.Sleep(10 * time.Millisecond)
fmt.Println(n)
}
このコードは動くことがあります。しかし正しいとは言えません。同期がないためデータレースが発生しています。「たまたま今は動く」は正しさの根拠になりません。
なぜこれを理解すべきか
AIは並行処理のひな形を出すのが得意ですが、担保されているのは見た目であって、正しさの証明ではありません。よくある危険パターンは以下の通りです。
- goroutineを起動しただけで終了待ちをしない
- channelを閉じる責務が曖昧
- 共有状態(shared state)をmutexなしで触る
-
WaitGroupのAddとDoneの整合性が怪しい - cancelされてもworkerが止まらない
Goの並行処理は、文法よりも誰が所有して、誰が止めて、いつ同期するのかを理解しているかが重要です。
6. 昔のGoの罠をそのまま信じない
Goについて調べると、古い記事がたくさん出てきます。
そこに書かれている注意点には、今も有効なものもあれば、すでに事情が変わったものもあります。
代表例が、for range とクロージャのループ変数問題です。
以前はgoroutine内でループ変数をそのまま捕まえると意図しない値共有が起きることで有名でしたが、Go 1.22で forループ変数が反復ごとのスコープになる変更が入り、この長年の落とし穴は解消されました。
for _, v := range values {
go func() {
fmt.Println(v) // Go 1.22以降は安全
}()
}
だからといって理解が不要になったわけではありません。大事なのは、昔の罠を丸暗記することではなく、なぜそれが罠だったのかを理解することです。
AIは古い知識を混ぜたコードを平然と出力することがあります。
その注意点、今のGoでも本当に正しい?と疑える視点自体に価値があります。
7. AI時代にGoで本当に強い人は、全部書ける人ではない
AI時代に強いのは、すべてをゼロから手で書ける人より、AIが出したコードの危なさを嗅ぎ分けられる人です。
Goに限るなら、最低限見るべきポイントはかなり絞れます。
AIが出したGoコードでまず見る場所
-
slice/mapの共有が意図通りか -
interfaceが過剰設計になっていないか -
contextが上流から下流へ正しく渡っているか -
errorに文脈があり、公開すべき情報だけを包んでいるか -
goroutineに終了条件と同期手段があるか
この5つを意識してレビューするだけで、コードの品質は大きく変わります。
まとめ
AIのおかげで、Goのコードは以前よりずっと速く書けるようになりました。
しかし、本当に差がつくのは「書けるか」よりも、そのコードが安全か、読みやすいか、運用に耐えるかを判断できるかです。
Goはシンプルな言語です。
シンプルだからこそ、概念を誤解したままでも一応動いてしまう。そこが難しさでもあり、面白さでもあります。
AIにコードを書かせる時代だからこそ、自分で全部書けること以上に、このコードは何を前提に正しく見えているのかを読めることの価値は、むしろ上がっていると感じています。
おまけ: 学習するときのおすすめ視点
Goを学ぶとき、文法を上から順に追うよりも、次の順で押さえると理解しやすいです。
- 値と参照っぽい振る舞いの違い
-
slice/map/pointerの扱い -
interfaceの切り方 -
error設計 context-
goroutine/channel/sync - 標準ライブラリの読み方
この順で学ぶと、AIが生成したコードを見たときにも「何が怪しいか」がかなり見えやすくなります。
最後に
もしGoを手を動かしながら体系的に学びたい方がいれば、以下の講座一覧も参考にしてみてください。
すべて演習型で、実際に書きながら進められる構成になっています。(割引価格で受講できるリンクです)
Udemy講座一覧