👁

net/http入門

この章では、Goの標準ライブラリnet/httpを使ったWebサーバの基礎について学び、ハンズオン形式で学習します。これにより、ハンドラ・ルーティング・ミドルウェアなど、Goで自前のWebサーバを作る基本が身につきます。

1. 本章の概要

1.1 本章の目的

Goは、Webフレームワークを持ち込まなくても標準ライブラリだけでWebサーバを作れる言語です。まず標準ライブラリで書けるようになっておくと、フレームワークを選定・比較する際にも「フレームワークがどこを楽にしてくれているか」の判断がしやすくなります。本章では、net/http パッケージを使ったWebサーバの基本、ハンドラ関数と http.HandlerFunc の関係、ルーティングに加えて、ミドルウェアパターンやリクエスト・レスポンスの読み書きも扱います。

1.2 ハンズオンの流れ

net/http で単一エンドポイントのWebサーバを立ち上げるところから始めて、複数ルートへの拡張、ミドルウェアの追加、JSONレスポンスの返却まで段階的に実装します。実際に curl で叩きながら挙動を確認します。

1.3 事前準備

前提となる講座

この章では、以下の知識を前提としています。自信がない場合は先に関連講座を実施してみましょう。

講座名 必要な知識
Webアプリケーションの基本 HTTPリクエスト・レスポンスやWebアプリケーションの基本構造

必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
Visual Studio Code Visual Studio Codeのインストール net/httpを使ったコードを記述するエディタとして使用する
Go Goのインストール net/httpの動作を確認するランタイムとして使用する

2. net/http とは

net/http は、GoでWebアプリケーションを作るための標準ライブラリです。Goは標準ライブラリだけで本格的なWebサーバを立てられるのが特徴のひとつで、フレームワークを使わない状態でも十分に実用的です。まずは net/http の動きを理解することで、Gin などのフレームワークが何を薄くラップしているのかが見えるようになります。

2.1 最小のWebサーバ

まずは、ポート 8080 で待ち受けて Hello, Go Web! を返すだけの最小のWebサーバを立ち上げます。以降の節では、この main.go をベースにハンドラの書き方・ルーティング・ミドルウェアを段階的に足していきます。

本章では、任意の場所に hello-http フォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から、作成した hello-http フォルダを開きます。以降の操作は、Visual Studio Codeのターミナルから行います。

hello-http/  ← このフォルダを作成

main.go を作成します。Visual Studio Codeのエクスプローラーで hello-http フォルダを右クリックし、「新しいファイル」を選択して main.go という名前で作成してください。

hello-http/
└── main.go  ← このファイルを作成

書き方は以下のとおりです。まず http.HandleFunc でパスとハンドラ関数の対応を登録します。

http.HandleFunc("パス", func(w http.ResponseWriter, r *http.Request) {
    // レスポンス処理
})

第1引数は受け付けるURLパス、第2引数はそのパスに来たリクエストを処理するハンドラ関数です。ハンドラ関数とは、特定のパスへのリクエストに対して「どんなレスポンスを返すか」を書いた関数のことです。http.ResponseWriter(レスポンスの書き込み先)と *http.Request(受け取ったリクエスト)を引数に取ります。

続いて、http.ListenAndServe で指定ポートの待ち受けを開始します。

http.ListenAndServe(":ポート", nil)

第1引数は待ち受けるアドレスで、":ポート" の形式で指定します。第2引数はルーティング用の ServeMux です。nil を渡すと、Goが内部で持つデフォルトの ServeMux が使われ、先ほど http.HandleFunc で登録した内容がそのまま反映されます。

作成したファイルに以下の内容を記述して保存します。これは、ポート 8080 で待ち受けて、どんなパスへのアクセスに対しても Hello, Go Web! を返す最小のWebサーバです。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, Go Web!")
    })

    fmt.Println("http://localhost:8080 で起動")
    http.ListenAndServe(":8080", nil)
}

Visual Studio Codeのメニューから「ターミナル」→「新しいターミナル」を選択して、ターミナルを開きます。

以下のコマンドで、Go がインストールされていることを確認します。

go version

以下のように Go のバージョンが表示されれば、インストールは確認できています。

go version go1.x.x darwin/arm64

バージョンが表示されない場合は、Goのインストール を先に実施してください。

インストールが確認できたら、以下のコマンドを実行します。

go run main.go

以下のような実行結果が表示されます。

http://localhost:8080 で起動

起動メッセージだけが表示され、そのままサーバがリクエスト待ちの状態になります(実行中)。

別のターミナルから curl でアクセスします。

curl http://localhost:8080/

以下のような実行結果が表示されます。

Hello, Go Web!

登録したハンドラが Hello, Go Web! をレスポンスボディとして返し、curl の出力に現れました。たった数行でWebサーバが立ち上がっていることが確認できます。

コードを解説します。

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    ...
})

http.HandleFunc は「このパスに来たリクエストを、この関数で処理する」という登録です。第2引数のハンドラ関数は、レスポンスへの書き出し口(http.ResponseWriter)と、受け取ったリクエスト(*http.Request)を受け取ります。

http.ListenAndServe(":8080", nil)

指定ポートでサーバを起動します。第2引数の nil は「デフォルトのマルチプレクサ(http.DefaultServeMux)を使う」を意味します。この節では明示的に ServeMux を作る書き方も後で紹介します。

3. ハンドラ関数の書き方

ハンドラ関数には、レスポンスを書き込むための http.ResponseWriter と、リクエスト情報を保持する *http.Request の2つの引数があります。ここでは、それぞれの役割と書き方を具体例で順に押さえていきます。

3.1 ResponseWriter への書き込み

http.ResponseWriter は、クライアントに返すレスポンス(ヘッダ・ステータスコード・ボディ)を書き込むための引数です。書き方は以下のとおりです。

まず、レスポンスヘッダは w.Header().Set で設定します。

w.Header().Set("ヘッダ名", "値")

続いて、ステータスコードは w.WriteHeader で明示的に書き込みます(省略時は 200)。

w.WriteHeader(ステータスコード)

最後に、レスポンスボディは fmt.Fprintln などの関数で書き込みます。

fmt.Fprintln(w, "本文")

main.go を以下の内容で置き換えて保存します。これは、/hello にアクセスするとレスポンスヘッダ・ステータスコード・ボディをすべて明示的に書いた こんにちは を返すコードです。

package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    // レスポンスヘッダを設定
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")

    // ステータスコードを明示(省略時は200)
    w.WriteHeader(http.StatusOK)

    // レスポンスボディを書く
    fmt.Fprintln(w, "こんにちは")
}

func main() {
    http.HandleFunc("/hello", hello)

    fmt.Println("http://localhost:8080 で起動")
    http.ListenAndServe(":8080", nil)
}

既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。

go run main.go

別のターミナルで以下のコマンドを実行します。-i オプションは、レスポンスヘッダとステータスコードもまとめて表示するオプションです。

curl -i http://localhost:8080/hello

以下のような実行結果が表示されます。

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: XXX, XX XXX XXXX XX:XX:XX GMT
Content-Length: 16

こんにちは

コードで指定した 200 OKContent-Type: text/plain; charset=utf-8 がヘッダとして返り、レスポンスボディに こんにちは が書き込まれていることが確認できます。

コードを解説します。

w.Header().Set("Content-Type", "text/plain; charset=utf-8")

w.Header().Set はレスポンスヘッダの設定です。Content-Type を指定することで、クライアントに「返すのは UTF-8 のプレーンテキスト」と伝えます。

w.WriteHeader(http.StatusOK)

w.WriteHeader はステータスコードを明示的に書き込みます。省略した場合は 200 OK が自動的に使われます。

fmt.Fprintln(w, "こんにちは")

レスポンスボディへの書き込みです。ヘッダやステータスコードは、ボディを書き始める前に設定する必要があります。

3.2 Request からの情報取り出し

*http.Request は、クライアントから届いたリクエストを保持する引数です。ここからクエリパラメータやHTTPメソッド、パスなどを取り出せます。書き方は以下のとおりです。

まず、クエリパラメータの値は r.URL.Query().Get で取得します。存在しないキーの場合は空文字列が返ります。

r.URL.Query().Get("キー名")

続いて、HTTPメソッドは r.Method から取り出します。

r.Method

最後に、URLのパスは r.URL.Path から取り出します。

r.URL.Path

main.go を以下の内容で置き換えて保存します。これは、/query にアクセスすると、URL のクエリパラメータ・HTTP メソッド・パスの3つを取り出して表示するコードです。

package main

import (
    "fmt"
    "net/http"
)

func query(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    method := r.Method
    path := r.URL.Path

    fmt.Fprintf(w, "path=%s method=%s name=%s\n", path, method, name)
}

func main() {
    http.HandleFunc("/query", query)

    fmt.Println("http://localhost:8080 で起動")
    http.ListenAndServe(":8080", nil)
}

既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。

go run main.go

別のターミナルで以下のコマンドを実行します。

curl "http://localhost:8080/query?name=Taro"

以下のような実行結果が返ります。

path=/query method=GET name=Taro

パスは /query、メソッドは GET、クエリパラメータ name の値は Taro が取り出せていることが分かります。

コードを解説します。

name := r.URL.Query().Get("name")

r.URL.Query().Get("キー名") で、クエリパラメータ(URLの ? 以降)から指定したキーの値を取り出します。存在しないキーの場合は空文字列を返します。

method := r.Method
path := r.URL.Path

r.Method は HTTP メソッド(GET / POST など)、r.URL.Path は URL のパス(/query など)を返します。

3.3 リクエストボディの読み取り

POSTメソッドの場合、クライアントから送られたリクエストボディを *http.Requestr.Body から受け取れます。JSONボディをGoの構造体に読み込むときは、json.NewDecoder(r.Body).Decode(...) を使うのが一般的です。書き方は以下のとおりです。

json.NewDecoder(r.Body).Decode(&変数)   // リクエストボディのJSONを 変数 に読み込む

main.go を以下の内容で置き換えて保存します。ここでは、/body パスに、JSONボディを受け取って中身をそのまま返すハンドラを登録します。受け取ったJSON({"name":"...", "email":"..."} 形式)を RequestBody 構造体に読み込み、nameemail の値をレスポンスに書き出します。

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type RequestBody struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func showBody(w http.ResponseWriter, r *http.Request) {
    var input RequestBody
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        http.Error(w, "JSONの解析に失敗", http.StatusBadRequest)
        return
    }
    fmt.Fprintf(w, "Bodyの値: name=%s email=%s\n", input.Name, input.Email)
}

func main() {
    http.HandleFunc("/body", showBody)

    fmt.Println("http://localhost:8080 で起動")
    http.ListenAndServe(":8080", nil)
}

既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。

go run main.go

別のターミナルで -d オプションを付けてJSONボディをPOSTします。

curl -X POST -d '{"name":"Taro","email":"taro@example.com"}' http://localhost:8080/body

以下のような実行結果が表示されます。

Bodyの値: name=Taro email=taro@example.com

送信したJSONの nameemail の値が読み取られ、Bodyの値: name=Taro email=taro@example.com の形でレスポンスに書き出されました。

コードを解説します。

if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
    http.Error(w, "JSONの解析に失敗", http.StatusBadRequest)
    return
}

json.NewDecoder(r.Body) はリクエストボディを読み取るJSONデコーダを作ります。.Decode(&input) で JSON を inputRequestBody 構造体)に読み込みます。失敗した場合は http.Error400 Bad Request を返します。

http.Error はエラーメッセージとステータスコードを設定して即座にレスポンスを返す関数で、エラー処理の定型パターンです。

4. ServeMuxによるルーティングの整理

http.HandleFunc は内部で http.DefaultServeMux(グローバル変数)に登録します。テストや複数サーバを立てるときには、独立した ServeMux を作るほうが管理しやすくなります。Go 1.22 からは HTTP メソッドとパスパターンをまとめて指定できるようになり、実用性が大きく上がりました。

4.1 独立したServeMuxによる複数ルートの実装

書き方は以下のとおりです。まず、http.NewServeMux() で独立した ServeMux を作ります。

mux := http.NewServeMux()

作った ServeMux に、HandleFunc でパスとハンドラを登録します。Go 1.22 からはHTTPメソッドとパスパターンをまとめて指定できます。

mux.HandleFunc("メソッド パス", ハンドラ関数)

最後に、この ServeMuxhttp.ListenAndServe の第2引数に渡して待ち受けを開始します(ListenAndServe の使い方は「最小のWebサーバ」で扱ったものと同じで、第2引数に nil の代わりに mux を渡す形になります)。

これは、独立した ServeMux に3つのルート(/health / /users / /users/{id})を登録するサーバの例です。

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "ok")
    })

    mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
        users := []User{
            {ID: 1, Name: "Taro", Email: "taro@example.com"},
            {ID: 2, Name: "Jiro", Email: "jiro@example.com"},
        }
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(users)
    })

    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"id": id})
    })

    fmt.Println("http://localhost:8080 で起動")
    http.ListenAndServe(":8080", mux)
}

main.go を上記の内容で置き換えて保存します。既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。

go run main.go

別のターミナルで、以下のコマンドを1つずつ実行して動作を確認します。まず、/health にアクセスします。

curl http://localhost:8080/health

以下のような実行結果が表示されます。

ok

続いて、/users にアクセスします。

curl http://localhost:8080/users

以下のような実行結果が表示されます。

[{"id":1,"name":"Taro","email":"taro@example.com"},{"id":2,"name":"Jiro","email":"jiro@example.com"}]

続いて、/users/1 にアクセスします。

curl http://localhost:8080/users/1

以下のような実行結果が表示されます。

{"id":"1"}

/healthok を、/users は User の一覧を JSON 配列として、/users/1 はパスパラメータ id=1 を JSON オブジェクトとして返しました。すべて想定通りのレスポンスが返っていれば、ルーティングとJSONエンコードが正しく動作しています。

コードを解説します。

mux := http.NewServeMux()

独立した ServeMux を作ります。以降はこの mux に登録し、ListenAndServe の第2引数に渡します。

mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    ...
})

Go 1.22 で導入された書き方で、HTTP メソッド(GET)とパスパターン(/users/{id})を一緒に指定できます。パスパラメータは r.PathValue("id") で取得します。

json.NewEncoder(w).Encode(users)

json.NewEncoder(w)io.Writer(ここでは w)に対する JSON エンコーダを作ります。.Encode(users) で struct のスライスがそのまま JSON 配列としてレスポンスに書き出されます。

5. ミドルウェアパターン

Webアプリケーションでは、リクエスト処理の前後にすべてのリクエストに共通して行いたい処理があります。例えば、「アクセスログを出す」「ログイン済みか確認する」などが該当します。これを毎回ハンドラ関数の中に書くと同じコードが繰り返しになり、追加や変更のたびに全ハンドラを書き換える手間がかかります。

このような共通処理を、実際のハンドラから切り離して外側にまとめる仕組みがミドルウェアです。ミドルウェアは、リクエストが届いた直後やレスポンスを返す直前に共通処理を差し込めるようにする層として機能します。リクエストとハンドラの間に、共通処理の層をひとつ挟む形になります。

リクエスト → [ミドルウェア: 前処理] → [ハンドラ本体] → [ミドルウェア: 後処理] → レスポンス

net/http では、ミドルウェアは「元の http.Handler を受け取り、共通処理を挟み込んだ新しい http.Handler を返す関数」として書きます。それにより、ログ出力や認証などの共通処理をひとつのミドルウェアにまとめてハンドラ群に一括で適用でき、必要に応じてミドルウェアを重ねて組み合わせることが行えるようになります。

5.1 ミドルウェアの実装例

書き方は以下のとおりです。

func ミドルウェア名(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 前処理
        next.ServeHTTP(w, r)   // 次のハンドラへ
        // 後処理
    })
}

これは、リクエストの処理時間をログに出す loggingMiddleware を作り、それで mux 全体をラップするコードです。

package main

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

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s (%s)", r.Method, r.URL.Path, time.Since(start))
    })
}

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "ok")
    })

    handler := loggingMiddleware(mux)

    fmt.Println("http://localhost:8080 で起動")
    http.ListenAndServe(":8080", handler)
}

main.go を上記の内容で置き換えて保存します。既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。

go run main.go

別のターミナルで以下のコマンドを実行します。

curl http://localhost:8080/health

サーバを起動しているターミナル側に、以下のようなログが出力されます。

2026/07/29 12:00:00 GET /health (123.456µs)

/health へのリクエストが処理されたあと、ミドルウェアがメソッド(GET)、パス(/health)、処理時間をまとめてログに書き出しています。

コードを解説します。

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s (%s)", r.Method, r.URL.Path, time.Since(start))
    })
}

loggingMiddlewarehttp.Handler を受け取り、ラップした http.Handler を返します。中では「開始時刻を記録」→「次のハンドラ(next)を呼ぶ」→「所要時間をログに出す」という流れになっています。

handler := loggingMiddleware(mux)
http.ListenAndServe(":8080", handler)

ListenAndServe に渡すのは、muxloggingMiddleware でラップした handler です。これで全てのリクエストが自動でログに残るようになります。

6. Webサーバの安全な起動と終了

http.ListenAndServe はサーバを起動して、リクエストを待ち続ける関数です。呼び出したあとは後続のコードに進まず、シグナル(Ctrl+C や SIGTERM)を受け取るまでそのまま動き続けます。実運用ではこのシグナルを受けたときに、処理中のリクエストが途中で切れないよう、それらが終わってからサーバを止めたい場面が多くあります。この「進行中のリクエストを最後まで処理してから安全に止める」方法をグレースフルシャットダウンと呼び、http.Server を明示的に用意して Shutdown メソッドを呼ぶことで実現できます。

これは、グレースフルシャットダウンを実現する main.go の全体コードです。main.go を以下の内容で置き換えて保存します。

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "ok")
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()
    fmt.Println("http://localhost:8080 で起動")

    // シグナル受信を待つ
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    <-sigCh

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    srv.Shutdown(ctx)
    fmt.Println("shutdown完了")
}

サーバを起動して、Ctrl+C で停止します。

go run main.go

起動メッセージが表示された状態でCtrl+Cを押すと、以下のような実行結果が表示されます。

http://localhost:8080 で起動
^Cshutdown完了

Shutdown(ctx) が呼ばれてサーバが正常に停止し、shutdown完了 のメッセージが表示されました。処理中のリクエストがあれば、それが終わるまで待ってから終了します。

コードを解説します。

srv := &http.Server{
    Addr:    ":8080",
    Handler: mux,
}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()

http.Server を明示的に用意し、ListenAndServe を goroutine で起動しています。http.ErrServerClosedShutdown 呼び出し後に返る想定内のエラーなので、無視しています。

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh

シグナル(Ctrl+C や SIGTERM)を受け取るための channel を用意し、signal.Notify でシグナルを流し込みます。<-sigCh はシグナルが来るまでその場で待ち、コードの実行を先に進めません。

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)

srv.Shutdown(ctx) は「新しい接続を受け付けず、進行中のリクエストが終わるまで待つ」動きをします。context でタイムアウトを付けておくと、長時間終わらない接続を強制切断できます。詳しくは公式ドキュメント http package に記載があります。

7. まとめ

この章では、net/http を使ったWebサーバの基本を学びつつ、実際にハンドラ・ルーティング・ミドルウェアの実装を体験しました。

  • http.HandleFunc + http.ListenAndServe の2行でWebサーバを立てられる
  • ハンドラは (w http.ResponseWriter, r *http.Request) を受け取り、w に書き込むことでレスポンスを返す
  • クエリは r.URL.Query().Get(...)、JSONボディは json.NewDecoder(r.Body).Decode(...) で受け取れる
  • Go 1.22 の mux.HandleFunc("GET /users/{id}", ...) により、メソッドとパスパラメータを一緒に扱える
  • ミドルウェアパターンにより、ログの出力や認証などの共通処理をハンドラの外側に差し込める
  • グレースフルシャットダウンは、http.Server を明示的に作り Shutdown(ctx) を呼ぶことで実現できる

次の章では、GoのWebフレームワークであるGinの基本を学びつつ、実際にコードでルーティングやリクエスト処理を体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000