コース目次 / 第4章

自分のものにする — 速くして、配れる形にする

goroutineで並行化して速度差を体感し、単一の実行ファイルに固めます。

第4章 / 全5章目安 約45分この章のゴール: 並行化で速くなったことを実測し、配れる1個のファイルができる

最後の章です。Goの顔である並行処理で速くして、配れる1個のファイルに固めます。

1. まず、いまの時間を測る

VS Codeのターミナルターミナル
go run .

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

最後の行の時間をメモしておいてください。このあと比べます。

2. ミニ解説: 同時に走らせる

試食でやったことを、そのまま本物に使います。

書き方 何をするか
go func() { ... }() この中身を同時に走らせる
sync.WaitGroup 走らせたもの全部が終わるまで待つ係
wg.Add(1) / wg.Done() 「1つ増えた」「1つ終わった」
wg.Wait() 全部終わるまでここで待つ

結果の順番が崩れないようにする工夫を1つ入れます。先に「結果を入れる箱」を人数分用意しておき、何番目の結果かを決めて入れるやり方です。こうすると、終わった順に関係なく urls.txt と同じ並びで表示されます。

3. 書き換える

main.go+1 / -1
-// urls.txt に並べたURLを1つずつ見に行って、生きているかどうかを表示する。+// urls.txt に並べたURLをまとめて見に行って、生きているかどうかを表示する。 package main

行頭の + - は変更の目印です。コピーすると、記号と削除された行を除いたこの章を終えた時点の内容が入ります。

main.go+2 / -0
 	"net/http" 	"os"+	"path/filepath" 	"strings"+	"sync" 	"time" )

行頭の + - は変更の目印です。コピーすると、記号と削除された行を除いたこの章を終えた時点の内容が入ります。

main.go+18 / -0
 } +// resolveList は、URLの一覧をどこから読むかを決める。+// いまいるフォルダに無ければ、実行ファイルと同じ場所を探す。+// (実行ファイルをダブルクリックしたときは、いる場所が変わるため)+func resolveList(path string) string {+	if _, err := os.Stat(path); err == nil {+		return path+	}+	exe, err := os.Executable()+	if err != nil {+		return path+	}+	beside := filepath.Join(filepath.Dir(exe), path)+	if _, err := os.Stat(beside); err == nil {+		return beside+	}+	return path+}+ func main() { 	path := "urls.txt"

行頭の + - は変更の目印です。コピーすると、記号と削除された行を除いたこの章を終えた時点の内容が入ります。

main.go+1 / -0
 		path = os.Args[1] 	}+	path = resolveList(path)  	urls, err := readURLs(path)

行頭の + - は変更の目印です。コピーすると、記号と削除された行を除いたこの章を終えた時点の内容が入ります。

main.go+15 / -2
 	} +	// 結果を入れる箱を先に用意する。番号で入れるので、順番は崩れない。+	lines := make([]string, len(urls))+	var wg sync.WaitGroup+ 	start := time.Now()-	for _, url := range urls {-		fmt.Println(check(url))+	for i, url := range urls {+		wg.Add(1)+		go func() {+			defer wg.Done()+			lines[i] = check(url)+		}() 	}+	wg.Wait()++	for _, line := range lines {+		fmt.Println(line)+	} 	fmt.Printf("\n%d件を %v で調べました\n", len(urls), time.Since(start).Round(time.Millisecond)) }

行頭の + - は変更の目印です。コピーすると、記号と削除された行を除いたこの章を終えた時点の内容が入ります。

4. 速さを比べる

VS Codeのターミナルターミナル
go run .

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

さっきメモした時間と比べてください。件数が多いほど差が出ます。

これが「同時にやる」の効果です。1件ずつ待っていた時間が、重なって消えました。
そして、そのために書いたのは goWaitGroup だけです。他の多くの言語では、ここはもっと大がかりな話になります。Goが「並行処理の言語」と呼ばれる理由が、この差分の小ささです。

5. 配れる1個のファイルにする

VS Codeのターミナルターミナル
go build

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

フォルダの中に linkcheck(Windowsは linkcheck.exe)ができています。約8MBの、1個のファイルです。

直接動かしてみてください。

VS Codeのターミナル(Windowsは linkcheck.exe)ターミナル(Windowsは linkcheck.exe)
./linkcheck

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

配るときは urls.txt も一緒に。この道具は調べたいURLの一覧を urls.txt から読むので、実行ファイルと同じフォルダに urls.txt を置いて、2つセットで渡してください。
そうしておけば、フォルダごとどこに置いても動きます(実行ファイルは、自分と同じ場所にある urls.txt も探すように書いてあります)。

このファイルには、Go本体ごと全部入っています。だから Goが入っていないパソコンでも、そのまま動きます。友だちに送っても、USBに入れても動きます。
これがGoの「配れる道具を一瞬で作る」という価値の正体です。第2章の図で言った「先に翻訳しておく」の見返りが、ここで返ってきました。

あなたの番

  • urls.txt を自分のブックマークにして、20件くらい入れてみてください。並行化の効果がはっきり分かります(いろいろなサイトを混ぜてください。同じサイトばかり並べると相手に集中してしまいます)
  • check 関数の中の表示を、自分の好きな形に変えてみてください(絵文字を足す、応答時間を出す、など)
  • できた実行ファイルを urls.txt ごと別のフォルダにコピーして、そこから動かしてみてください。2つ揃っていれば、どこに置いても動きます

発展: 同時に投げる数を絞る

いまのコードは、URLの数だけ一斉にリクエストを飛ばします。100件なら100本同時です。相手に優しくないので、同時に走る数に上限をつけてみましょう。

Goでは「席が5つある待合室」を作って、席が空くまで待たせます。

main.go(発展課題)
	// 同時に走らせるのは5本まで。席が空くまで次は待つ。
	sem := make(chan struct{}, 5)

	for i, url := range urls {
		wg.Add(1)
		go func() {
			defer wg.Done()
			sem <- struct{}{}        // 席を取る(空くまでここで待つ)
			defer func() { <-sem }() // 終わったら席を返す
			lines[i] = check(url)
		}()
	}

make(chan struct{}, 5) は「5個まで入る箱」です。入れられなければ、そこで待ちます。チャネルはGoの並行処理のもう1つの主役で、これがいちばん短い実用例です。

数字を 1 にすると1件ずつ(元の遅さに戻ります)、100 にすると実質無制限。変えて時間を測ってみてください。

持ち帰りカード

README.md
# はるとのリンクチェッカー

Tsumiki のミニ体験コース「Go — 配れる道具を一瞬で作る」の成果物です。

## また動かすとき(3行)

1. ターミナルを開く
2. `cd` でこのフォルダに入る(VS Code なら「フォルダーを開く」でこのフォルダを開いてから「ターミナル」→「新しいターミナル」)
3. `go run .` と打つ

配れる1個のファイルにするときは `go build` を打ちます。できた `linkcheck`
(Windowsは `linkcheck.exe`)は、Goが入っていないパソコンでも動きます。
**`urls.txt` を実行ファイルと同じフォルダに入れて一緒に配ってください。**

## 中身

| ファイル | 何をするか |
|---|---|
| `main.go` | `urls.txt` のURLをまとめて調べて、生死を表示する |
| `urls.txt` | 調べたいURLの一覧。`#` で始まる行は飛ばす |
| `go.mod` | このフォルダが1つのGoのプロジェクトであるという印 |

## この先に進むなら

- A Tour of Go(日本語) https://go.dev/tour/
- 今日使った `net/http``sync` は標準ライブラリです。Goは標準ライブラリだけで
  かなり遠くまで行けます
- `go build` で作った実行ファイルは、`GOOS=windows go build` のように書くと
  他のOS向けにも作れます

試食レポート

おつかれさまでした。手元には動く道具と、配れる実行ファイルが残りました。

Goは「動かす場所を選ばない道具」を作るのに最も向いた言語のひとつです。今日の go build 一発で配れるものができる感覚は、他の言語ではなかなか味わえません。

この言語、合いそうでしたか?
決まり事の多さ、if err != nil の頻度、そのかわりに得られる速さと配りやすさ。合いそうなら、A Tour of Go がそのまま次の一歩です。
ぴんと来なかったなら、それも収穫です。別の言語を試食してみてください。

ハマったら

ZIPで戻したい

このページの下のZIPは、この章を終えた状態のフォルダです。解凍して、その中で作業を続けられます。自分で書き換えた部分は消えてしまうので、先に自分のファイルを別の場所へ待避してから使ってください。

こうなっていればOK

うまくいかないときは、この章の完成ファイル一式(ZIP)をダウンロードして続きから進めても大丈夫です。

この章で卒業です。

この章はまだ完了していません。