1. 5000個のゴルーチン
  2. 10000個でも合う
  3. 1個のとき
  4. 0個のとき
コース一覧
Go入門
並行の落とし穴

Go入門

変数とゼロ値、Go唯一のループであるfor、複数戻り値とエラー処理、スライスとマップ、構造体とメソッド、ゴルーチンによる並行処理、net/httpによるAPI開発まで、Goの基礎を、毎レッスン実際にコードを書き、フリマアプリのAPIをGoで作りながら学びます。

1
Hello, Go
01. Goとは15分
02. 実行してみる15分
03. packageとmain15分
04. fmtで出力する15分
05. go buildと1バイナリ15分
06. つくる: 起動バナー20分
07. 第1章クイズ10分
2
変数と型
01. varで宣言する15分
02. := の短縮宣言15分
03. ゼロ値(Goならでは)15分
04. 基本の型15分
05. 暗黙変換なし15分
06. 定数とiota15分
07. つくる: 商品データ表示20分
08. 第2章クイズ10分
3
制御構文(forしかない世界)
01. Goのif15分
02. forしかない世界15分
03. forの七変化15分
04. Goのswitch15分
05. 定番: FizzBuzz15分
06. ループで集計15分
07. つくる: 商品集計20分
08. 第3章クイズ10分
4
関数とエラー処理
01. funcで関数を作る15分
02. 複数戻り値(Goならでは)15分
03. if err != nil(Goの心臓)15分
04. エラーを作る15分
05. deferで後始末15分
06. つくる: 検証つき関数群20分
07. 第4章クイズ10分
5
スライスとマップ
01. 配列15分
02. スライス15分
03. appendで育てる15分
04. rangeで走査する15分
05. マップ15分
06. カンマokイディオム15分
07. つくる: 検索と集計20分
08. 第5章クイズ10分
6
構造体とメソッド
01. 構造体15分
02. 初期化とフィールド15分
03. メソッド15分
04. ポインタレシーバ20分
05. New関数の慣習15分
06. 構造体のスライス15分
07. つくる: Itemで再設計20分
08. 第6章クイズ10分
7
ゴルーチンと並行処理のさわり
01. ゴルーチン15分
02. チャネル20分
03. 待ち合わせ20分
04. 並行の落とし穴20分
05. つくる: 並行集計20分
06. 第7章クイズ10分
8
Web API(net/http)
01. サーバーを立てる20分
02. ハンドラ関数20分
03. JSONを返す20分
04. ルーティング20分
05. 一覧API20分
06. 詳細APIと40420分
07. 出品API20分
08. つくる: API主要部完成25分
09. 第8章クイズ10分
9
総合制作
01. 仕様互換を仕上げる25分
02. Node版と差し替える25分
03. 1バイナリでデプロイ25分
04. 自由拡張25分
05. 完成と次のステップ20分

並行の落とし穴

同じ変数を同時に触ると壊れる

Go

total := 0 var wg sync.WaitGroup for i := 0; i < 5000; i++ { wg.Add(1) go func() { defer wg.Done() total++ }() } wg.Wait() fmt.Println(total)

5000 が出ると思いますが、実際は 4873 や 4991 になります。しかも毎回変わります。

なぜ数が合わないのか

total++ は1行ですが、機械の側では3段階に分かれています。

プレーンテキスト

1. total を読む 2. 1 を足す 3. total に書き戻す

2つのゴルーチンが同時にこれをやると、こうなります。

プレーンテキスト

A: total を読む (5) B: total を読む (5) ← A がまだ書き戻していない A: 6 を書き戻す B: 6 を書き戻す ← A の分が消えた

2回足したのに1しか増えていません。これが データ競合 です。

見つけにくいのが厄介

この手のバグには、たちの悪い性質があります。

プレーンテキスト

毎回起きるわけではない たまに合ってしまう 数が少ないと再現しない 負荷が高いときだけ出る エラーが出ない 静かに数字が狂う

数件を並行に処理する程度では、まず起きません。 手元では正しく動き、本番で数字が合わなくなる、という現れ方をします。第6章のポインタレシーバも「静かに効かない」問題でしたが、こちらは毎回結果が違うぶん、さらに厄介です。

Go には検出する道具があります。

プレーンテキスト

go run -race main.go

-race を付けると、危ない同時アクセスを見つけて教えてくれます。数が少なくて表面化しない場合でも指摘してくれるので、並行処理を書いたら一度は通す価値があります。

Mutex で守る

直し方は、1人ずつしか触れないようにすることです。

Go

var mu sync.Mutex go func() { defer wg.Done() mu.Lock() total++ mu.Unlock() }()

Lock から Unlock までの間は、他のゴルーチンが待たされます。1人が終わるまで次が入れないので、さっきの割り込みが起こりません。

Unlock の呼び忘れは、全員が待ち続けて止まる原因になります。defer mu.Unlock() と書くのが安全です。

そもそも共有しない

もっとよい解決があります。共有しなければ守る必要もありません。

前のレッスンでやった形がそれです。

Go

results[idx] = v * v // 添字が違うので衝突しない

別々の場所に書けば、Mutex は要りません。待ちが発生しないぶん速くもなります。

Go にはこういう言い回しがあります。

解説

メモリを共有して通信するな。通信してメモリを共有せよ。

チャネルで値を渡す設計にすれば、そもそも同じ変数を奪い合わないという意味です。Mutex は最後の手段で、まず分けるか渡すを考えるのが Go の流儀です。

要件

  1. 5000個のゴルーチンをそれぞれ go で走らせる
  2. sync.WaitGroup で全部の完了を待つ
  3. count++ を sync.Mutex で守る
  4. Unlock の呼び忘れが起きない書き方にする
  5. 戻り値がちょうど 5000 になる

入出力例

raceCount(5000) → 5000 raceCount(10000) → 10000 raceCount(1) → 1 raceCount(0) → 0

ヒント

`var mu sync.Mutex` を `wg` の隣に用意します

`count++` の前に `mu.Lock()`、その直後に `defer mu.Unlock()` を書きます

初期コードを実行すると、5000 より少し小さい値が毎回違って返ります。それがデータ競合の症状です

生田 陸人
監修生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア監修者プロフィールを見る →
編集 ゆめさく編集部·公開 2026/08/10

関連レッスン

  • つくる: 並行集計

    第7章の知識でカテゴリ別の集計を並行に実行し、結果を安全にまとめられるようになります。

  • 第7章クイズ

    ゴルーチン、チャネル、WaitGroup、データ競合についての理解を確認します。

  • サーバーを立てる

    net/httpでHTTPサーバーを立て、リクエストに応答を返せるようになります。

  • 仕様互換を仕上げる

    ステータスコードとエラー形式を仕様に揃え、互換のあるAPIとして仕上げられるようになります。

このレッスンに出てくる用語

意味があいまいなまま進んだ語は、ここから読み直せます。

  • 変数データに名前をつけて参照する仕組み
  • 処理計算や代入を表す長方形
  • メモリプログラムとデータを一時保持する高速領域
  • 設計何をどう作るかを決める前工程
  • 戻り値呼び出し元への返答を表す点線矢印
main.go
学習モード
エディタを読み込んでいます

メモ

並行の落とし穴

⌘S で保存