GoのHTTPサーバーをゼロから理解する:net/httpパッケージの完全ガイド

GoのHTTPサーバーをゼロから理解する:net/httpパッケージの完全ガイド | mohablog

シンプルで高速なWebサーバーを構築するなら、Goは本当に良い選択肢だと思います。標準ライブラリのnet/httpパッケージを使えば、複雑なフレームワークに依存することなく、わずか数行のコードで本格的なHTTPサーバーを立ち上げられるんですよね。この記事では、Goでのサーバー開発の基本から応用まで、実装しながら一緒に学んでいきましょう。

目次

Goでサーバーを選ぶ理由

Goがサーバーサイド開発に向いているのは、言語設計そのものに理由があります。実際のプロジェクトで感じた優位性も含めて、以下の特徴が挙げられます:

  • 軽量で高速:コンパイル型言語で実行速度が速く、メモリ効率も優れているため、本番環境のリソース消費を抑えられます
  • 並行処理が得意:ゴルーチンにより数千の同時接続を効率的に処理できるので、スケーラビリティに優れています
  • 標準ライブラリが充実:net/httpだけで本格的なサーバーが構築可能で、余分な依存関係を減らせます
  • デプロイが簡単:単一のバイナリファイルとしてコンパイルされるため、サーバーへのデプロイが非常にスムーズです

PythonのFlaskやNode.jsのExpressも確かに優秀ですが、Goは性能と開発効率のバランスに優れていると実感しています。

最小限のHTTPサーバーを構築する

まず、Goでの最もシンプルなサーバー実装から始めてみましょう。

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    http.HandleFunc("/", helloHandler)
    http.ListenAndServe(":8080", nil)
}

このコードの流れを説明していきますね:

  • helloHandler:クライアントからのリクエストに対応するハンドラー関数。http.ResponseWriterにデータを書き込みます
  • http.HandleFunc():指定したパスとハンドラーを紐付ける関数。複数回呼び出して複数のエンドポイントを登録できます
  • http.ListenAndServe():8080番ポートでサーバーを起動し、接続待ち受け状態になります

実行するには go run main.go を実行し、ブラウザで http://localhost:8080 にアクセスすれば「Hello, World!」が表示されます。シンプルですが、これが全ての基礎になるんです。

複数のエンドポイントを処理する

実際のプロジェクトでは、複数のパスに対応する必要が出てきます。以下の例を見てください:

package main

import (
    "fmt"
    "net/http"
)

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "ホームページです")
}

func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "このサイトについて")
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"message": "API response"}`)
}

func main() {
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/about", aboutHandler)
    http.HandleFunc("/api/data", apiHandler)
    http.ListenAndServe(":8080", nil)
}

複数のエンドポイントを登録することで、それぞれのパスに異なるレスポンスを返せます。ここで気をつけるべきは、JSONレスポンスの場合はContent-Typeヘッダーを適切に設定すること。これを忘れると、クライアント側で正しく解析されない場合があります。調べてみたら、ブラウザはこのヘッダーを参考にしてレスポンスの扱いを判断しているんです。

HTTPメソッドの判定

RESTful APIを構築する際は、GET、POST、PUT、DELETEなどのメソッドを区別する必要があります。ここが結構重要なポイントです。

package main

import (
    "fmt"
    "net/http"
)

func dataHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case "GET":
        fmt.Fprintf(w, "データを取得しました")
    case "POST":
        fmt.Fprintf(w, "データを保存しました")
    case "PUT":
        fmt.Fprintf(w, "データを更新しました")
    case "DELETE":
        fmt.Fprintf(w, "データを削除しました")
    default:
        http.Error(w, "メソッドが許可されていません", http.StatusMethodNotAllowed)
    }
}

func main() {
    http.HandleFunc("/data", dataHandler)
    http.ListenAndServe(":8080", nil)
}

リクエストのr.MethodプロパティでHTTPメソッドを判定し、適切なレスポンスを返します。この辺りは初心者がうっかり見落とすんですが、無効なメソッドに対してはhttp.StatusMethodNotAllowed(405エラー)を返すのがベストプラクティスです。こうすることで、クライアント側もサーバーの意図を正確に理解できるようになります。

クエリパラメータとボディの処理

現場のサーバーでは、クライアントから送られてくるデータを処理する場面が避けられません。

package main

import (
    "fmt"
    "net/http"
)

func searchHandler(w http.ResponseWriter, r *http.Request) {
    // クエリパラメータを取得
    query := r.URL.Query().Get("q")
    if query == "" {
        http.Error(w, "クエリパラメータが必要です", http.StatusBadRequest)
        return
    }
    fmt.Fprintf(w, "検索キーワード: %s", query)
}

func formHandler(w http.ResponseWriter, r *http.Request) {
    // フォームデータを解析
    r.ParseForm()
    name := r.FormValue("name")
    email := r.FormValue("email")
    fmt.Fprintf(w, "名前: %s, メール: %s", name, email)
}

func main() {
    http.HandleFunc("/search", searchHandler)
    http.HandleFunc("/form", formHandler)
    http.ListenAndServe(":8080", nil)
}

r.URL.Query().Get()でクエリパラメータを、r.FormValue()でPOSTフォームデータを取得できます。最初ここで失敗したんですが、常にバリデーションを行い、必要なデータが実際に存在するか確認することが重要です。空の値がきた場合の処理も想定しておくと、本番環境での思わぬエラーを防げます。

静的ファイルの配信

HTMLやCSSなどの静的ファイルをサーブする場合は、http.FileServer()を使用します。

package main

import (
    "net/http"
)

func main() {
    // 静的ファイルの配信
    http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./public"))))
    
    // 動的コンテンツ
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/html; charset=utf-8")
        http.ServeFile(w, r, "./index.html")
    })
    
    http.ListenAndServe(":8080", nil)
}

http.StripPrefix()を使うことで、URLパスから接頭辞を削除し、正しいファイルパスにマッピングできるんですね。これにより /public/style.css./public/style.css から読み込まれます。公式ドキュメントを読んでみたら、このアプローチは一般的で、多くのプロジェクトで採用されているパターンのようです。

エラーハンドリングとステータスコード

適切なHTTPステータスコードを返すことは、APIの信頼性に関わってくるポイントです。

package main

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

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

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    
    // 例: IDが見つからない場合
    id := r.URL.Query().Get("id")
    if id == "" {
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(map[string]string{"error": "IDが必要です"})
        return
    }
    
    // 成功時
    w.WriteHeader(http.StatusOK)
    user := User{ID: 1, Name: "Taro"}
    json.NewEncoder(w).Encode(user)
}

func main() {
    http.HandleFunc("/user", getUserHandler)
    http.ListenAndServe(":8080", nil)
}

w.WriteHeader()でステータスコードを明示的に設定できます。一般的なコードは以下の通りです:

  • 200 OK:成功した場合に返す基本的なコード
  • 400 Bad Request:不正なリクエストであることをクライアントに伝える
  • 404 Not Found:リソースが見つからないことを示す
  • 500 Internal Server Error:サーバー側のエラーが発生した時に使用

ロギングとデバッグ

本番環境ではリクエストログを記録することが本当に重要です。以下はシンプルなロギング機能の例になります:

package main

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

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

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello!")
}

func main() {
    http.HandleFunc("/", loggingMiddleware(helloHandler))
    log.Println("サーバーが起動しました: http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

このアプローチでは、ミドルウェアを使用してすべてのリクエストをログに記録し、実行時間も測定しています。デバッグや性能改善に非常に役立つんです。実際のプロジェクトで本当にお世話になった機能で、どの処理が遅いのか、どのエンドポイントがよくアクセスされるのか、これで一目瞭然になります。

まとめ

GoのHTTPサーバー開発について、ゼロから実装できるレベルまで学習しました。以下が要点です:

  • net/httpパッケージを使えば、複雑なフレームワークなしでサーバーを構築可能だと分かりました
  • http.HandleFunc()でエンドポイントを定義し、複数のパスに対応できます
  • HTTPメソッドの判定でRESTful APIを実装する際の基本となります
  • クエリパラメータとフォームデータの処理はGoの標準機能で十分対応可能です
  • 静的ファイルの配信はhttp.FileServer()を活用することで簡潔に実装できます
  • 適切なステータスコードを返し、エラーハンドリングを意識することが重要です
  • ロギング機能でデバッグと性能監視を実現し、本番環境での問題解決に役立ちます

Goでのサーバー開発は習得が容易でありながら、スケーラビリティにも優れているんですよね。これらの基本を習得すれば、本番レベルのWebサーバーを構築できるようになります。

よくある質問

複数のハンドラーを同じパスに登録するにはどうしたらいい?

Goの標準ライブラリでは、同じパスに複数のハンドラーを登録することはできません。ただし、1つのハンドラーの中でr.Methodを使ってメソッドを判定し、異なる処理を実行する方法があります。より複雑なルーティングが必要な場合は、Gorilla Muxなどのルーティングライブラリを導入するのがおすすめです。

サーバーを停止するにはどうしたらいい?

ターミナルで Ctrl+C を押すことで、http.ListenAndServe()が停止します。本番環境でより制御が必要な場合は、http.Server構造体を直接使用し、Shutdown()メソッドで計画的にサーバーをシャットダウンできます。

JSONのマーシャリング時に、特定のフィールドを除外したい場合は?

構造体のフィールドにjson:"-"というタグを付けることで、そのフィールドはJSONの出力から除外されます。また、json:"fieldName,omitempty"のようにomitemptyを指定すれば、ゼロ値の場合は出力されません。

大きなファイルのアップロードを処理するときに注意することは?

r.ParseForm()を呼ぶ前に、r.ParseMultipartForm(maxSize)を使ってメモリに読み込むサイズを制限することが重要です。これにより、メモリ使用量を抑制し、サーバーの安定性を保つことができます。

複数のサーバーを同時に起動するにはどうしたらいい?

ゴルーチンを使用して、複数のhttp.ListenAndServe()を並行実行できます。例えば、go http.ListenAndServe(":8080", nil)go http.ListenAndServe(":8081", nil)を同時に実行し、複数ポートでサーバーを起動することが可能です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次