🔩 ねじき教室 Go と Web の教室

最小の Web サーバー

読了目安 約3分

net/http で最小の Web サーバーを書いて起動し、curl とブラウザから叩いて動きを確かめます。

この章の目次

Part 2 で外から観察したレスポンスを、今度は返す側として自分の手で書きます。 使うのは標準ライブラリ net/http だけ、コードは空行込みで 13 行です。

この Part のコードは、ファイルに保存して go run で実行するのが基本です(準備は学習環境を整える)。 章末には、ページ内で実行できる自己完結版も置いてあります。

サーバーを書いて起動する

作業用ディレクトリに、次の内容を main.go として保存します。

Go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, ねじき教室!")
	})
	http.ListenAndServe(":8080", nil)
}

起動します。

シェル
go run main.go

ターミナルは止まったように見えますが、正常です。 別のターミナルから curl を打ちます(ブラウザで開いても同じです)。

シェル
curl http://localhost:8080/hello
テキスト
Hello, ねじき教室!

localhost は「このマシン自身」を指すホスト名です。

コードを読む

http.HandleFunc は、「このパスにリクエストが来たら、この関数を呼べ」という対応を登録します。 リクエストを受け取ってレスポンスを作る関数をハンドラと呼びます。

fmt.Fprintlnfmt.Println の書き込み先を選べる版です。 ここでは w、つまりレスポンスに書き込んでいます。

http.ListenAndServe(":8080", nil) がサーバーの本体です。 wr、第 2 引数 nil の正体は次章で読みます。

ListenAndServe は返ってこない

止まったように見えたのは、http.ListenAndServe が内部でループし続け、main 関数に戻らないからです。 プログラムは終了せず、サーバーであり続けます。

止めるには、サーバー側のターミナルで Ctrl+C を押します。 止めた状態で curl を打つと、connection refused というエラーになることを確かめてみてください。

http.ListenAndServe が戻るのは失敗したときだけなので、返り値のエラーをこう扱うのが定番です。

Go
log.Fatal(http.ListenAndServe(":8080", nil))

ポート 8080 が使用中だと address already in use で失敗します。 前のサーバーの止め忘れを疑ってください。

ポートで待ち受けるということ

8080 はポート番号、つまり 1 台のマシンの中で通信の窓口を区別する番号です。

Web の標準ポートは HTTP が 80 番、HTTPS が 443 番です。 1023 番以下は多くの OS で管理者権限が要るため、開発中は 8080 のような大きい番号が慣例です。

リクエストごとに goroutine が走っている

net/http のサーバーは、リクエストごとに新しい goroutine を起動し、その上でハンドラを実行します。 この仕組みは goroutine と並行性で見たとおりです。

ハンドラの先頭に time.Sleep(3 * time.Second)"time" の import を足し、2 つのターミナルからほぼ同時に curl を打ってみてください。 逐次処理なら 2 本目は約 6 秒待つはずですが、どちらも約 3 秒で返ってきます。

ページ内で試す

サーバーと「curl 役」を 1 つのプログラムにまとめると、ページ内でも実行できます。

Go
package main

import (
	"fmt"
	"io"
	"net/http"
	"time"
)

func main() {
	// サーバー側: この章で書いたものと同じ
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, ねじき教室!")
	})
	go http.ListenAndServe(":8080", nil)
	time.Sleep(100 * time.Millisecond) // 起動を待つ

	// curl 役: 自分自身にリクエストを送る
	res, err := http.Get("http://localhost:8080/hello")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println("ステータス:", res.Status)
	fmt.Print(string(body))
}

試してみよう:パスを /hello 以外に変えて実行し、404 になることを確かめてください。