プログラミング初心者がよくやる間違いは、文法とはあまり関係がありません。初心者はエラーメッセージを読み飛ばし、50 行書いてから初めて実行し、入力がおかしい場合を無視し、値を確認せずにバグの原因を推測します。どれも習慣の問題で、習慣は早いうちなら直せます。
この記事では、そのうち 13 個を取り上げます。コードがあるとわかりやすいものには短い Go の例を添え、直し方も紹介します。Go はコンパイラやツールがこうした間違いのいくつかを最初から受け付けないので、学習用の言語として向いています。それらは終盤のセクションで取り上げます。
要約
- エラーメッセージは最後まで読みましょう。ファイル、行、何が問題なのかが書かれています。
- コードは数行書くごとに実行します。小刻みに進めればバグは簡単に見つかります。
- 1 行ずつ説明できないコードは、AI の出力も含めて貼り付けないようにしましょう。
- エラーの経路を処理しましょう。Go では、一緒に返された値を使う前に必ず
errをチェックします。 - 名前は中身がわかるように付け、関数は小さく保ち、空の場合と境界のケースもテストします。
- 初日から Git を使い、シークレットは環境変数に置き、必要だと感じる前にテストを書きましょう。
- デバッグは、やみくもにコードを変えるのではなく、値を出力するか Delve でステップ実行して行いましょう。
- 抽象化や高速化の前に、まず動くようにしましょう。そのうえで、チュートリアルではないものを作りましょう。
目次
- 1. エラーメッセージを読まない
- 2. 一度も実行せずに大量のコードを書く
- 3. 説明できないコードをコピペする
- 4. 正常系しか書かない
- 5. 曖昧な名前を付ける
- 6. 何でもこなす巨大な関数を 1 つ書く
- 7. off-by-one エラーと空の入力
- 8. 初日からバージョン管理を使わない
- 9. 設定やシークレットをハードコードする
- 10. テストを一度も書かない
- 11. 当てずっぽうでデバッグする
- 12. 動く前に抽象化や最適化をする
- 13. チュートリアルに留まり、再現コードなしで助けを求める
- Go が防いでくれるのはどの間違いですか?
1. エラーメッセージを読まない
初心者は赤い文字を見ると、すぐにコードに戻って何かを変えようとしがちです。しかし、画面上でいちばん役に立つのはエラーメッセージです。たいていはファイル、行、列、そして何が問題なのかが書かれています。
次のサーバー設定ローダーはコンパイルできません。
example.gogopackage main import ( "fmt" "os" ) func main() { port := os.Getenv("PORT") fmt.Println("starting server") }
example.texttext./main.go:9:2: declared and not used: port
左から右へ読んでいきます。main.go がファイル、9 が行、2 が列で、残りの部分は変数 port が一度も使われていないと言っています。使うか削除するかのどちらかです。プログラムのほかの部分には何の問題もありません。
実行時エラーも同じです。Go のプログラムが panic すると、スタックトレースに発生した関数と行が、直近の呼び出しから順に表示されます。自分のファイルを指している最初の行を探し、そこから調べ始めましょう。
エラーメッセージが暗号のように感じられるのは、あなただけではありません。研究をレビューした 2019 年の ITiCSE ワーキンググループは、コンパイラのエラーメッセージが初心者にとって「かなりの難しさをもたらす」と結論づけています(Becker et al.、2019 年)。ゆっくり文字どおりに読むのも技術のうちで、練習すればすぐに上達します。
2. 一度も実行せずに大量のコードを書く
プログラム全体を書いてから初めて実行するのは効率的に感じますが、バグを見つけるコストが高くなります。新しく書いた 80 行が失敗したら、バグはその中のどこにあってもおかしくありません。新しい 5 行が失敗したなら、バグはその 5 行の中にあります。
直し方は、短いループを回すことです。数行書いて実行し、出力を読み、これを繰り返します。作りながら途中の値を出力し、その部分が動いたら出力の行は消します。Go はコンパイルが速いので、小さな変更のたびに go run . を実行しても 1、2 秒しかかかりません。
3. 説明できないコードをコピペする
Stack Overflow や AI アシスタントからコピーするのは問題ありません。間違いなのは、説明できないコードを貼り付けることです。そのコードが壊れたとき(いずれ必ず壊れます)、どこから手をつければいいのかまったくわかりません。学ぶ過程も飛ばしてしまいます。
自分にルールを課しましょう。貼り付ける前に、すべての行を読んで何をしているかを言葉にします。できなければ、その行を AI に説明してもらうか、説明をもとに自分で書き直します。今ではほとんどの人が AI を使って学んでいるので、このルールはいっそう重要です。プログラミングを学んでいる人のうち 73.3% が AI ツールを使っているか使う予定で、39.5% は毎日使っています(Stack Overflow Developer Survey、2025 年)。詳しい議論は今でもコードを手で書くべきかにまとめています。
4. 正常系しか書かない
初心者のコードは、ファイルが存在し、ネットワークがつながっていて、ユーザーが数字を入力したことを前提にしがちです。実際の入力はもっと雑然としています。次のプログラムはポート番号を読み取り、エラーを無視しています。
example.gogopackage main import ( "fmt" "strconv" ) func main() { input := "80a0" port, _ := strconv.Atoi(input) fmt.Println("listening on port", port) }
example.texttextlistening on port 0
タイプミスが、何の警告もなくポート 0 になってしまいました。Go はエラーを普通の値として返すので、直し方はそれをチェックして何が失敗したのかを伝えることです。
example.gogopackage main import ( "fmt" "os" "strconv" ) func parsePort(raw string) (int, error) { port, err := strconv.Atoi(raw) if err != nil { return 0, fmt.Errorf("invalid port %q: %w", raw, err) } if port < 1 || port > 65535 { return 0, fmt.Errorf("port %d out of range 1-65535", port) } return port, nil } func main() { port, err := parsePort("80a0") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("listening on port", port) }
example.texttextinvalid port "80a0": strconv.Atoi: parsing "80a0": invalid syntax exit status 1
入力ごとに、それが欠けていたら、空だったら、型が違ったら、大きすぎたらどうなるかを考えましょう。その答えが一つひとつコード内のチェックになります。
5. 曖昧な名前を付ける
data、tmp、res、x2、thing といった名前は、次にコードを読む人に何も伝えません。そして 2 週間後、その読み手はあなた自身です。名前は、その値が何を保持しているのかを問題の言葉で表すべきです。
example.gogo// Hard to follow for _, d := range data { if d.s == 500 { tmp++ } } // Clear for _, req := range requests { if req.Status == 500 { serverErrors++ } }
Go の慣習では、スコープが短ければ短い名前、広ければ長い名前を使います。3 行のループなら i で十分です。パッケージレベルの変数を c と名付けるのはよくありません。名前がなかなか決まらないときは、それが 2 つのことをしている場合がよくあります。
6. 何でもこなす巨大な関数を 1 つ書く
ファイルを読み込み、パースし、合計を計算し、レポートを出力する 200 行の main は、テストしにくく、変更するのはさらに大変です。役割ごとに分割しましょう。次の例は、アクセスログに含まれる HTTP ステータスコードを数える小さな CLI です。
example.gogopackage main import ( "bufio" "fmt" "io" "maps" "os" "slices" "strings" ) func main() { if len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "usage: logstats <access.log>") os.Exit(2) } f, err := os.Open(os.Args[1]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer f.Close() counts, err := countStatusCodes(f) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } printReport(os.Stdout, counts) } // countStatusCodes reads access log lines and counts the status code, // which is the last field on each line. func countStatusCodes(r io.Reader) (map[string]int, error) { counts := make(map[string]int) sc := bufio.NewScanner(r) for sc.Scan() { fields := strings.Fields(sc.Text()) if len(fields) == 0 { continue } counts[fields[len(fields)-1]]++ } return counts, sc.Err() } func printReport(w io.Writer, counts map[string]int) { for _, code := range slices.Sorted(maps.Keys(counts)) { fmt.Fprintf(w, "%s %d\n", code, counts[code]) } }
example.texttext200 2 404 1 500 1
これで main は各部分をつなぐだけになりました。countStatusCodes はファイル名ではなく io.Reader を受け取るので、テストではディスクに触れずに strings.Reader を渡せます。printReport が io.Writer を受け取るのも同じ理由です。どの関数も 1 画面に収まり、1 つのことだけをしています。
7. off-by-one エラーと空の入力
ループが 1 回多く回ってしまうのは典型的なバグです。次のコードは CSV の 1 行をパースしていますが、< を使うべきところで <= を使っています。
example.gogopackage main import ( "fmt" "strings" ) func main() { fields := strings.Split("alice,admin,active", ",") for i := 0; i <= len(fields); i++ { fmt.Println(fields[i]) } }
example.texttextalice admin active panic: runtime error: index out of range [3] with length 3 goroutine 1 [running]: main.main() /home/you/csvrow/main.go:11 +0xc0 exit status 2
panic のメッセージは、何が起きたのかを正確に伝えています。11 行目で、最後の有効なインデックスが 2 である長さ 3 の slice に対してインデックス 3 が使われました。Go でいちばん簡単な直し方は、インデックスを自分で管理しないことです。for _, field := range fields なら末尾を越えることはありません。
もう少し気づきにくいのが空の入力です。次の関数はリクエストのレイテンシの平均を計算します。
example.gogopackage main import "fmt" func averageLatency(ms []float64) float64 { var total float64 for _, v := range ms { total += v } return total / float64(len(ms)) } func main() { fmt.Println(averageLatency([]float64{120, 80, 100})) fmt.Println(averageLatency(nil)) }
example.texttext100 NaN
リクエストが 0 件だとゼロ除算になり、浮動小数点数なら NaN、整数なら panic になります。関数が要素 0 個、1 個、最大数のときにどう動くかを必ず考えましょう。この 3 つのケースで多くの境界値バグを見つけられます。
8. 初日からバージョン管理を使わない
多くの初心者は、「本格的な」プロジェクトになるまで Git を後回しにします。そして 1 時間前には動いていたものを壊し、元に戻す手段がなくなります。Git はプロジェクト全体の「元に戻す」ボタンで、始めるのにほとんど手間はかかりません。
example.bashbashgit init git add . git commit -m "Parse port from PORT env var"
何かが動くたびに、何を変えたのかがわかるメッセージを付けてコミットしましょう。壊してしまったときは、git diff で最後に正常だった状態からの変更を確認できます。GitHub などのホスティングサービスに push すれば、バックアップとポートフォリオも手に入ります。
9. 設定やシークレットをハードコードする
データベースのパスワードや API キーをソースコードに直接書いても、公開リポジトリに push するまでは動きます。GitGuardian の集計では、2025 年に公開 GitHub のコミットへ新たに追加されたハードコードされたシークレットは 2,865 万件で、前年から 34% 増えました(GitGuardian、2026 年)。ポート、URL、認証情報は手元のノート PC と本番環境でも異なるので、ハードコードするとデプロイのたびにコードを編集することになります。
代わりに環境変数から読み込みましょう。
example.gogopackage main import ( "cmp" "fmt" "os" ) func main() { addr := ":" + cmp.Or(os.Getenv("PORT"), "8080") apiKey := os.Getenv("PAYMENTS_API_KEY") if apiKey == "" { fmt.Fprintln(os.Stderr, "PAYMENTS_API_KEY is not set") os.Exit(1) } fmt.Println("listening on", addr) }
cmp.Or は空でない最初の値を返すので、PORT には妥当なデフォルト値が入ります。API キーにはデフォルト値がなく、キーがなければプログラムは起動を拒否します。ローカル用の値を .env ファイルに置く場合は、最初のコミットの前にそのファイルを .gitignore に追加しましょう。
10. テストを一度も書かない
初心者はプログラムを実行して出力を目で見ることでテストします。1 回きりならそれで足ります。ただ、後の変更でそれまで動いていたものが壊れても気づけません。Go は標準のツールチェーンにテスト機能が含まれているので、何もインストールする必要はありません。まず、間違い 7 の averageLatency を、データがあったかどうかを返すように直します。
example.gogofunc averageLatency(ms []float64) (float64, bool) { if len(ms) == 0 { return 0, false } var total float64 for _, v := range ms { total += v } return total / float64(len(ms)), true }
次に、その隣にテーブルテストを追加します。
example.gogofunc TestAverageLatency(t *testing.T) { tests := []struct { name string in []float64 want float64 wantOK bool }{ {"three requests", []float64{120, 80, 100}, 100, true}, {"one request", []float64{42}, 42, true}, {"no requests", nil, 0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := averageLatency(tt.in) if got != tt.want || ok != tt.wantOK { t.Errorf("averageLatency(%v) = %v, %v, want %v, %v", tt.in, got, ok, tt.want, tt.wantOK) } }) } }
example.texttext--- PASS: TestAverageLatency (0.00s) --- PASS: TestAverageLatency/three_requests (0.00s) --- PASS: TestAverageLatency/one_request (0.00s) --- PASS: TestAverageLatency/no_requests (0.00s) PASS
これを _test.go で終わるファイルに置き、go test ./... を実行します。各行が 1 つのケースなので、新しいエッジケースの追加は 1 行増やすだけです。no requests の行は、間違い 7 で NaN を返していた入力です。今ではその入力に決まった答えがあり、それを保証するテストもあります。
11. 当てずっぽうでデバッグする
初心者のデバッグは、何かを変えて実行し、まだ壊れているので別の何かを変える、というループになりがちです。10 回も繰り返すと、コードには新しいバグが増え、元のバグはそのまま残っています。7 つの大学の学生 21 人を対象にした研究では、一部の学生が「使う戦略が少なく、その使い方も効果的でなかった」うえに、デバッグ中に新しいバグを持ち込んでいました(Murphy et al.、2008 年)。
変更する前に、まず観察しましょう。何が問題なのか仮説を立て、実際の値を出力して確かめます。
example.gogolog.Printf("order=%+v total=%d items=%d", order, total, len(order.Items))
%+v は struct のフィールド名も出力するので、どの数字が何なのかを推測せずに済みます。出力がうるさくなってきたら、デバッガーを使いましょう。Go の標準的なデバッガーは Delve で、VS Code も GoLand も内部で Delve を使っています。
example.bashbashdlv debug . -- access.log (dlv) break main.go:43 (dlv) continue (dlv) print fields []string len: 3, cap: 3, [ "GET", "/api/orders", "200", ]
43 行目は、間違い 6 のログ集計プログラムにある counts[...]++ の行です。プログラムはそこで停止し、fields の値を推測するのではなく、実際の値を確認できます。
バグを見つけたら、直す前にそのバグを再現するテストを書きましょう。そうすれば、そのバグが知らないうちに戻ってくることはありません。
12. 動く前に抽象化や最適化をする
まだ動かないプログラムのために、interface や設定システムやプラグイン層を設計したくなるものです。あるいは、速いかもしれないという理由で、わかりやすいループを凝ったコードに置き換えたくなります。どちらも、コードが何をすべきかわかる前に、コードを変更しにくくしてしまいます。
まずは、書ける範囲でいちばん素直なコードで動くようにしましょう。同じものを 3 回書くまでは、共通の関数に切り出すのを待ちます。Go では、2 つ目の実装を想像した時点ではなく、実際に現れた時点で interface を定義します。最適化は計測してから行いましょう。Go にはそのためのツールがあります。ベンチマークには go test -bench、プロファイルには pprof を使います。
13. チュートリアルに留まり、再現コードなしで助けを求める
チュートリアルはすべてのステップがうまくいくので、生産的な気分になれます。初めて空のファイルを開いたとき、実際にどれだけ身についているかがわかります。どこかの時点で、誰にも手順を示されていないものを作る必要があります。ファイル名を一括変更する CLI、小さな HTTP API、チャットアプリ用のボットなどです。出来は雑になるでしょうが、次のチュートリアル 3 本よりも多くのことを学べます。その練習の組み立て方は 2026 年のプログラミング勉強法で、Go の学習計画全体は Go の学び方で紹介しています。
行き詰まったら助けを求めることになりますが、聞き方しだいで結果が変わります。「コードが動きません」では何も返ってきません。バグが再現する最小のプログラムまで問題を削り込み、そのプログラム、正確なエラー出力、期待した結果、試したことを共有しましょう。削り込んでいる途中で自分でバグを見つけることもよくあります。Go なら、Go Playground で実行可能なプログラムの共有リンクを作れます。
Go が防いでくれるのはどの間違いですか?
Go は多くの人が保守する大規模なコードベースのために設計されました。そのルールのいくつかが、結果として初心者の間違いをコードの実行前に防いでくれます。
- 使われていない変数や import はコンパイルできません。コンパイラは
declared and not usedや"strings" imported and not usedを拒否します。Go FAQ はこれを「目先の便利さを、長期的なビルド速度とプログラムのわかりやすさと引き換えにする」ことだと説明し、使われていない変数はバグの兆候かもしれないと述べています。 gofmtはすべての Go ファイルを同じ形式に整えるので、スペースや波かっこの位置で議論することはなく、乱れたインデントがバグを隠すこともありません(gofmt)。go vetは怪しいコードを見つけます。たとえば文字列に%d動詞を渡すコードは問題なくコンパイルできますが、でたらめな出力になります。go vetはこうしたものを指摘します(cmd/vet)。- エラーは処理しなければならない値です。失敗しうる関数は
errorを返します。値とエラーを返す関数では、エラーも受け取らなければ値を使えません。受け取ったあと、目に見える_で捨てることはできます。セクション 4 の正常系だけの間違いは、わざと書かなければ起きないものになります(Errors are values)。 - テスト機能は最初から組み込まれています。
go testとtestingパッケージは Go に同梱されているので、最初のテストを書く前にフレームワークを選ぶ必要はありません(testing)。 - データ競合はレースディテクターが見つけます。テストやプログラムを
-race付きで実行すると、Go は共有メモリへの並行アクセスを、両方のアクセスのファイルと行とともに報告します(Data Race Detector)。
次の例では、コンパイルは通るフォーマットのバグを go vet が見つけています。
example.gogopackage main import "fmt" func main() { active := "12" fmt.Printf("%d active users\n", active) }
example.texttext./main.go:7:14: fmt.Printf format %d has arg active of wrong type string
Go は 200 行の関数を書くことも、変数を tmp と名付けることも止めません。それでも機械的な間違いは検出してくれるので、判断が必要な間違いにより多くの時間を使えます。シャドーイングされたエラーや append の意外な挙動など、Go 特有の落とし穴については Go でよくある間違いを参照してください。
よくある質問
プログラミング初心者が最もよくする間違いは何ですか?
初心者のコンパイルエラーに関する最大規模の研究では、25 万人以上の Java 学習者による 3,700 万回のコンパイルを対象にしています。最も多かったエラーは、かっこや引用符の対応の誤りでした。これらはすぐに修正されていました。コストが大きかったのは、戻り値を無視するなど、コンパイラが指摘しない間違いです。こうした間違いは修正に 17 分以上かかるのが普通で、修正されないままのこともありました(Altadmri & Brown、2015 年)。習慣の面でいえば、ほかのすべてを遅らせるのはエラーメッセージを読まないことです。
同じプログラミングの間違いを繰り返さないにはどうすればよいですか?
修正したバグごとにテストを書きましょう。そうすれば、気づかないうちにバグが戻ってくることはありません。2 回以上やってしまった間違いの短いリストを作り、コミットする前にそのリストと照らし合わせてコードを確認します。ツールも役に立ちます。Go では、go vet と gofmt がある種の間違いを自動で見つけてくれます。
間違いの多い初心者にとって Go は良い言語ですか?
はい。Go のコンパイラは使われていない変数や import を拒否し、エラーは明示的な戻り値で、go vet、gofmt、go test が言語に同梱されています。エラーメッセージは短く、ファイルと行を指し示します。詳しい比較は Go は最初のプログラミング言語に向いているかを参照してください。
初心者は AI にコードを書かせるべきですか?
AI はコード、エラー、概念の説明に使い、自分が理解できないコードを書かせるのには使わないようにしましょう。翌日に何も見ずに書き直せないコードは貼り付けない、というのがよいルールです。AI のコードがほぼ正しいときにそれを直すには、その理解が必要になります。
初心者のミスがなくなるまでどのくらいかかりますか?
ほとんどの人は、数か月ほど定期的にコードを書くうちに、こうした癖が抜けていきます。どの間違いも、直すまでは時間を奪い続けるからです。たまに長時間取り組むより、実際のプログラムで毎日練習するほうが早く上達します。機械的な間違いから先になくなります。名前付けや関数の大きさのような判断が必要なものには、もっと時間がかかります。
今日から Go を書き始めましょう
このリストのどの間違いも、たくさんコードを書き、何が問題だったのかを早く知るほど避けやすくなります。それが LevelUpGo の考え方です。各レッスンはブラウザ上で本物の Go ツールチェーンで動く Go の演習なので、初日から実際のコンパイラエラーを読み、err の値を処理し、テストが失敗するところを目にします。まずは Go Fundamentals トラックから始めましょう。
