🔰

Go 標準パッケージ理解 net/http編

に公開

前置き

  • 自分の理解用
  • 学習兼記事進捗メモ
    • httpクライアント 〇
    • httpサーバー △
      • handle, handleFunc, serveMux 〇
      • error 未
      • コンストラクタ 未
    • コンテキスト 未
    • jsonエンコードデコード △
    • io.Reader △
    • ミドルウェアとか...etc 未

net/httpとは

  • Goで用意されている標準パッケージの1つ
  • HTTP関連の型や関数を提供する
  • 製品レベルのHTTP/2クライアントとサーバーが含まれている

HTTPクライアント

構造体http.Clientが定義されており、HTTPリクエストの生成とレスポンスの受信ができる。

デフォルトクライアント

デフォルトのクライアント(net/httpに含まれている)では、タイムアウトが無いため、本番環境での利用は推奨されていない。

http.Get関数もDefaultClientを利用している(GetはDefautlClient.Getのラッパー)

// デフォルトのクライアントを使ったHTTP応答
package main

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

type DogResponse struct {
	Message string `json:"message"`
	Status  string `json:"status"`
}

resp, err := http.DefaultClient.Get("https://dog.ceo/api/breeds/image/random")
// resp, err := http.Get("https://dog.ceo/api/breeds/image/random")
if err != nil {
    // エラーハンドリング
}

defer resp.Body.Close()

var dogResponse DogResponse
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&dogResponse); err != nil {
    // エラーハンドリング
}

fmt.Println(dogResponse.Message)

開発環境はデフォルトのクライアントを使うという考え方もあるが、個人的には独自のクライアントインスタンスを作った方がいいと思う。

  1. 書き方に慣れる
  2. 本番環境と開発環境のコードの差異が少なくなる

以下では、明示的にクライアントのインスタンスを作成している。
プログラム全体でhttp.Clientをひとつだけ作成する。

client := http.Client{
    Timeout: 30 * time.Second, // タイムリミットの設定  30秒
}

リクエスト

*http.Requestインスタンスの作成

リクエストを送りたい時には、http.NewRequestWithContext関数を使って新しい*http.Requestのインスタンスを作成する

  • *http.Request = 構造体Requestへのアクセス

  • コンテキスト、メソッド、接続先URLを渡す

  • PUTPOSTPATCHのいずれかのリクエストの場合、最後の引数をio.ReaderとしてリクエストのBodyを指定する

    • Bodyが無い場合はnilを指定する(つまりGetの時)
  1. context.Background()はまだコンテキスト未勉強。勉強したら加筆するかも
  2. http.MethodGet定数
  3. urlはそのまんま変数url
  4. nilは上記でも記述したように、Bodyが無い(Get)ためnilとしている
    • id.ReaderBodyを指定する
    • ぶっちゃけid.Readerはよく分かってない。勉強したら加筆するかも。
url := "https://dog.ceo/api/breeds/image/random"
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
    fmt.Printf("リクエストの作成に失敗しました: %v", err)
}

ヘッダーの設定、リクエストの送信と結果格納

*http.Requestのインスタンスを作成したら、http.Requestを使ってhttp.Clientを呼び出す

  1. http.ClientのメソッドはDoを指定する
  2. http.Responseに結果が返る(func (*Client)Do(*Response, error)の部分)

ヘッダー追加すると何ができるか、何が嬉しいのかは未勉強。
認証、キャッシュ、クッキー用のヘッダーとかかなぁと今のところイメージしてる。

req.Header.Add("X-My-Client", "Go-Learn") // リクエストのヘッダーに追加
res, err := client.Do(req) // リクエストを送信して結果を格納
if err != nil {
    fmt.Printf("リクエストの作成に失敗しました: %v", err)
}

レスポンスの確認、jsonのデコード

  1. レスポンスのステータスコードはフィールドStatusCodeに格納される
  2. レスポンスのテキストはStatusに格納される
  3. レスポンスヘッダーはHeaderに格納される
    • Responseの構造体で受け取り、HeaderフィールドはHeader型のマップを参照している
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
    fmt.Printf("APIからエラーステータスが返されました: %s", res.Status)
}
fmt.Println(res.Header.Get("Content-Type")) // 結果確認用。不要ならコメントアウト

var data Data
type Data struct {
	Message string `json:"message"`
	Status  string `json:"status"`
}

err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
    fmt.Printf("JSONのデコードに失敗しました: %v", err)
}

fmt.Printf("%+v\n", data)

理解するためのメモ

  1. Bodyio.ReadCloser型のフィールドBodyに格納される
  2. jsonパッケージのデコーダを使うことで、簡単にレスポンスを処理することができる
  3. defer res.Body.Close()
    1. 接続のクローズ。deferなのでプログラムの最後(return)のタイミングで実行される
    2. ループ文を処理するときはdeferステートメントを使わず、関数が終了する直前に実行するようにすべきとのこと(他の関数に処理を切り出すとdeferはいい感じに使えるとのこと)
      • 接続がスタックして閉じない状態になり、接続数が跳ね上がりやすくなる
  4. if res.StatusCode != http.StatusOK
    1. レスポンスのステータスコードがステータスOK(200)じゃなければエラー出力を行う
    2. StatusOKは定数
  5. jsonタグは可読性を向上させる(ほかにも理由があるらしいがまだ詳しくは見れてない)
    • フィールドはint型、bool型など色々指定可能
      var data Data
      type Data struct {
          Message string `json:"message"`
          Status  string `json:"status"`
      }
      
  6. err = json.NewDecoder(res.Body).Decode(&data)
    • jsonパッケージのNewDecoderメソッドにres.Bodyを引数として渡しつつ、Decodeメソッドに&dataを引数として渡している・・・という理解
      1. NewDecoderjsonのデコーダを作成
      2. Decode:作成されたjsonデコーダに対してデコードを実施
      3. NewDecoderJsonを読み取るためのデコーダ(解読器)を作成し、デコーダのDecodeメソッドでGo内で作った構造体に格納しているって感じ?

結果

application/json
{Message:https://images.dog.ceo/breeds/pembroke/n02113023_5985.jpg Status:success}

HTTPサーバー

構造体http.Serverとインタフェースhttp.Handlerがサーバー機能の中心として用意されている

  • http.Server: HTTPサーバーの設定を管理しつつ、HTTPリクエストを受信して処理する
  • http.Handler: http.ServerのフィールドにあるHandler型のインタフェース。サーバーへのリクエストの処理を担うところ

処理の流れをイメージしてみる

HTTPリクエストと比べて意味不明だったので処理の流れをイメージしてみる。

まずはシンプルに

まずはシンプルな構成で考えてみる

  • SurveMuxを使わない(デフォルト)
  • コンストラクタ関数は使わない

以下はイメージ図。(絵画センスありません)

  1. HTTPリクエスト
    • (補足) この構成だとリクエストにどんなパスを指定しても同じレスポンスで返ってくる
  2. HandlerフィールドのHealthzHandlerを呼び出し
  3. ServeHTTPメソッドを実行
    • (補足) Handlerインタフェース
      • 暗黙的に​実装
      • HealthzHandler​(実装)が​http.Handler​(インタフェース)​の​条件を​満たしている
  4. ResponseWriterを使ってレスポンスを構築
    • (補足) ResponseWriterインタフェース
      • ServeHTTPは引数としてhttp.ResponseWriterインターフェースを満たした値を受け取る
  5. HTTPレスポンスを返す
    • 200 OK
    • X-My-Server: Go-Learn
    • Body: OK

コード全文

クリック
package main

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

type HealthzHandler struct{}

func (v *HealthzHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("X-My-Server", "Go-Learn")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}

func main() {
	server := http.Server{
		Addr:         ":8080",
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 90 * time.Second,
		IdleTimeout:  120 * time.Second,
		Handler:      &HealthzHandler{},
	}
	fmt.Println("ブラウザで http://localhost:8080/ を開いてください。")

	if err := server.ListenAndServe(); err != nil {
		if err != http.ErrServerClosed {
			log.Fatalf("サーバーが異常終了しました: %v", err)
		}
	}
}

各処理についての詳細

1. 構造体http.Serverでリクエストを受け取る
  • サーバーの動作を詳細に制御する際はhttp.Serverを用いてカスタムサーバーを作成する
  • http.Serverを明示的に定義しないとタイムアウトがデフォルトだと設定されてない。なので本番利用は定義が必須とのこと
2. 構造体http.Serverにあるhttp.Handler型のインタフェースでリクエストを処理をする
server := http.Server{
  Addr:         ":8080",
  ReadTimeout:  30 * time.Second,
  WriteTimeout: 90 * time.Second,
  IdleTimeout:  120 * time.Second,
  Handler:      &HealthzHandler{},
}
3. インタフェースhttp.HandlerはメソッドServeHTTPを持つ
type Handler interface {
  ServeHTTP(http.ResponseWriter, *http.Request)
}
4. つまり、ServeHTTPメソッドを実装することで、暗黙的にhttp.Handlerインタフェースを満たしているとみなされ、ハンドラとして扱われる
// HealthzHandler型のインスタンスで利用する構造体を定義
type HealthzHandler struct{}

func (h *HealthzHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
5. 引数http.ResponseWriterという名称のインタフェースは3つのメソッドを持つ

また、決まった順序で呼び出す必要がある。呼び出す順番が大事。

  1. Header() http.Header: 必要なレスポンスヘッダーを設定する。ヘッダーを設定する必要がなければ呼び出す必要は無し
  2. WriteHeader(statusCode int): HTTPステータスコードを指定してWriteHeaderを呼び出す。ステータスコードが200のレスポンスを送信する場合は、WriteHeaderを省略してもよい
  3. Write([]byte) (int, error): メソッドWriteを呼び出してレスポンスのBodyを設定する
type ResponseWriter interface {
  Header() http.Header
  Write([]byte) (int, error)
  WriteHeader(statusCode int)
}

// 例
func (h *HealthzHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  w.Header().Add("X-My-Server", "Go-Learn")
  w.WriteHeader(http.StatusOK)
  w.Write([]byte("OK"))
}

リクエストルーター

http.ServeMuxを使うことでリクエストをパス毎にルーティングすることができる。
使わないとひとつのリクエストしか処理できない。状況に応じて利用。

自身もhttp.Handlerを実装しているため、階層化が可能。
http.Handlehttp.HandleFuncはデフォルトのhttp.ServeMuxであるhttp.DefaultServeMuxを使用している。

関数http.NewServeMuxでインスタンスを生成する。
インタフェースhttp.Handlerを満たしているため、http.ServerのフィールドにあるHandlerに代入することが可能。

http.ServeMuxHandleメソッド

// func (*ServeMux) Handle
func (mux *ServeMux) Handle(pattern string, handler Handler)

http.ServeMuxHandleFuncメソッド

// func (*ServeMux) HandleFunc
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))

http.ServeMuxServeHTTPメソッド

// func (*ServeMux) ServeHTTP
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

リクエストルーターを盛り込む

  • SurveMuxを使う
  • コンストラクタ関数は使わない

http.Serverhttp.Handleの間にhttp.SurveMuxが入り込み、http.SurveMuxが各http.Handleにディスパッチ(割り振り)をしている理解。
めんどくさくなってきたので図に起こしません。ある程度イメージできるようになったからいいかなって・・・

コード全文

クリック
package main

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

type HealthzHandler struct{}

func (v *HealthzHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("X-My-Server", "Go-Learn")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("/healthz OK"))
}

func main() {
	mux := http.NewServeM_x()
	mux.Handle("/healthz", &HealthzHandler{})
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/ OK"))
	})

	// ワイルドカード
	mux.HandleFunc("GET /content/{name}", func(w http.ResponseWriter, r *http.Request) {
		contentName := r.PathValue("name") // URL の {name} を取得
		fmt.Fprintf(w, "/content/%s OK", contentName)
	})

	// ルーターの階層化(blue/green)
	blueMux := http.NewServeMux()
	blueMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/blue/healthz OK"))
	})
	greenMux := http.NewServeMux()
	greenMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/green/healthz OK"))
	})

	mux.Handle("/blue/", http.StripPrefix("/blue", blueMux))    // "/blue/"のパターンはusersMuxが処理する
	mux.Handle("/green/", http.StripPrefix("/green", greenMux)) // "/green/"のパターンはusersMuxが処理する

	// ルーターの階層化(users)
	usersMux := http.NewServeMux()
	usersMux.HandleFunc("/mypage", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/users/mypage OK"))
	})
	usersMux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/users/settings OK"))
	})

	mux.Handle("/users/", http.StripPrefix("/users", usersMux)) // "/user/"のパターンはusersMuxが処理する

	server := http.Server{
		Addr:         ":8080",
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 90 * time.Second,
		IdleTimeout:  120 * time.Second,
		Handler:      mux,
	}
	fmt.Println("ブラウザで http://localhost:8080/ を開いてください。")

	if err := server.ListenAndServe(); err != nil {
		if err != http.ErrServerClosed {
			log.Fatalf("サーバーが異常終了しました: %v", err)
		}
	}
}

各処理についての詳細

1. 新しいServeMuxのインスタンスを作成して割り当てる
mux := http.NewServeMux()
2. ServeMuxのインスタンスをHandleメソッドに渡してルーティング登録
type HealthzHandler struct{}

func (v *HealthzHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("X-My-Server", "Go-Learn")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("/healthz OK"))
}

// 追加分
mux.Handle("/healthz", &HealthzHandler{})
実行結果
$ curl -i http://localhost:8080/healthz

HTTP/1.1 200 OK
X-My-Server: Go-Learn
Date: Mon, 15 Sep 2025 17:57:49 GMT
Content-Length: 11
Content-Type: text/plain; charset=utf-8

/healthz OK
3. ServeMuxのインスタンスをhttp.HandleFuncメソッドに渡してルーティング登録
  • Handleとの違いは引数に関数を渡していること
  • ServeHTTPメソッドと値格納用の構造体を明示的に記述しなくても、暗黙的に処理を行ってくれる(http.Handlerを実装しなくてもよい)
  • 一般的にはhttp.Handlerを実装せずにhttp.HandleFuncを使うらしい
  • ビジネスロジックに依存する複雑なハンドラの場合は、http.Handlerを実装するとのこと
    • コードの管理や再利用性が増すため
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/ OK"))
	})
実行結果
curl -i http://localhost:8080/

HTTP/1.1 200 OK
Date: Mon, 15 Sep 2025 17:59:41 GMT
Content-Length: 4
Content-Type: text/plain; charset=utf-8

/ OK
4. ワイルドカードの利用
  • Go 1.22 オプションでHTTPのメソッド(GET, POSTなど)とパスに対するワイルドカード変数が利用可能となった
  • ワイルドカード変数の値を得るには、http.RequestのメソッドPathValueを利用する
	mux.HandleFunc("GET /content/{name}", func(w http.ResponseWriter, r *http.Request) {
		contentName := r.PathValue("name") // URL の {name} を取得
		fmt.Fprintf(w, "/content/%s OK", contentName)
	})
実行結果
curl -i http://localhost:8080/content/test

HTTP/1.1 200 OK
Date: Mon, 15 Sep 2025 18:24:57 GMT
Content-Length: 16
Content-Type: text/plain; charset=utf-8

/content/test OK
5. http.ServeMuxの階層化
  • http.ServeMuxは、http.Handlerのインスタンスにリクエストをディスパッチ(振り分け)する

  • http.ServeMuxhttp.Handlerを実装しているので、関連するリクエストをもつhttp.ServeMuxのインスタンスを複数個生成し、それを親のhttp.ServeMuxに登録することができる

  • mux.Handle("/blue/", http.StripPrefix("/blue", blueMux))

    • http.StripPrefix関数を使ってリクエストのURL/Pathを整形する
    • /blue/のパスを持つリクエストは子ルーターblueMuxにルーティングする。この際、/blueプレフィックスを削除して子ルーターに渡す
      1. 親ルーターが受け取ったリクエスト(Path):/blue/healthz
      2. (http.StripPrefixで整形して・・・)
      3. 子ルーターに渡す際のリクエスト(Path):/healthz
      4. となる
	// ルーターの階層化(blue/green)
	blueMux := http.NewServeMux()
	blueMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/blue/healthz OK"))
	})
	greenMux := http.NewServeMux()
	greenMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/green/healthz OK"))
	})

  // mux := http.NewServeMux()
	mux.Handle("/blue/", http.StripPrefix("/blue", blueMux))    // "/blue/"のパターンはusersMuxが処理する
	mux.Handle("/green/", http.StripPrefix("/green", greenMux)) // "/green/"のパターンはusersMuxが処理する
実行結果
curl -i http://localhost:8080/blue/healthz

HTTP/1.1 200 OK
Date: Mon, 15 Sep 2025 18:04:37 GMT
Content-Length: 16
Content-Type: text/plain; charset=utf-8

/blue/healthz OK
curl -i http://localhost:8080/green/healthz

HTTP/1.1 200 OK
Date: Mon, 15 Sep 2025 18:06:27 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

/green/healthz OK

例その2

	// ルーターの階層化(users)
	usersMux := http.NewServeMux()
	usersMux.HandleFunc("/mypage", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/users/mypage OK"))
	})
	usersMux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("/users/settings OK"))
	})

	// mux := http.NewServeMux()
	mux.Handle("/users/", http.StripPrefix("/users", usersMux)) // "/users/"のパターンはusersMuxが処理する
実行結果
curl -i http://localhost:8080/users/mypage

HTTP/1.1 200 OK
Date: Mon, 15 Sep 2025 18:07:10 GMT
Content-Length: 16
Content-Type: text/plain; charset=utf-8

/users/mypage OK
curl -i http://localhost:8080/users/settings

HTTP/1.1 200 OK
Date: Mon, 15 Sep 2025 18:08:16 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8

/users/settings OK

参考

GitHubで編集を提案

Discussion