Go言語入門:簡単なHTTPサーバとテストの実装を徹底解説

サムネイル

この記事は、DMMグループ Advent Calendar 2024の9日目の記事です。

はじめに

こんにちは! 24卒エンジニアの北松です!

現在、Go言語を学習中なので、その一環としてアドベントカレンダーにこの記事を投稿します。

Goには、HTTPリクエストやレスポンスの処理を簡単に行える net/httpという標準ライブラリがあります。これを使えば簡単にWebサーバを構築できます。 また、testing ライブラリを使うことで、テストコードの実装もシンプルに行えます。

今回は、/hello エンドポイントにアクセスすると、name クエリパラメータの値を含むメッセージを返すAPIとそのテストコードを解説します。

net/httpとは?

net/httpはhttp関連の処理を簡単に実現できるGoの標準ライブラリです。

このライブラリを使うと、わずか数行のコードでHTTPサーバを構築でき、Webアプリケーションの開発を迅速に始めることができます。

実装

サンプルコード

package main

import (
  "fmt"
  "net/http"
)

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

func HelloName(w http.ResponseWriter, r *http.Request) {
  if r.Method != "GET" {
      http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
      return
  }
  name := r.URL.Query().Get("name")
  fmt.Fprintf(w, "hello %s", name)
}

コードの各部分の解説

今回アクセスするURLは以下の通りです。

curl http:localhost/hello?name=dmm

これから先の内容は重複する内容を除き、コードを一行ずつ解説していきます。

  • func HelloName(w http.ResponseWriter, r *http.Request)

    • HelloName関数は、今回呼び出すHandlerになります。

    • 第一引数には、http.ResponseWriterで、HTTPレスポンスのデータをクライアントに送信するために使用するものです。

    • 第二引数には、*http.Requestで、リクエストデータを保持した変数を持っています。

  • http.HandleFunc("GET /hello", HelloName)

    • http.HandleFuncは、ServeMuxにエンドポイントが対応するHandlerを設定します。

    • 第一引数にリクエストメソッドとエンドポイントを指定します。

    • 第二引数に登録したいHandlerを設定します。今回はnilになっていますが、ミドルウェアを扱いたい場合は新しく定義するのが良いようです。

    • ※ Go1.22からリクエストメソッドを事前に定義できるようになりました。 それ以前のバージョンを使用している場合は、http.HandleFunc("/hello", HelloName)のように書いてください。詳細が気になる方は、公式のリリースノートをご覧ください。

    • また、Goのバージョンが1.22以降であるにも関わらずリクエストメソッドを指定できない場合は、環境変数にhttpmuxgo121が存在するかを判定しているため、GODEBUG=httpmuxgo121=0を追加してください。※ 私はこれでめっちゃ詰まりました...

下記は、HandleFuncの内部コードです。

// HandleFunc registers the handler function for the given pattern in [DefaultServeMux].
// The documentation for [ServeMux] explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  if use121 {
    DefaultServeMux.mux121.handleFunc(pattern, handler)
  } else {
    DefaultServeMux.register(pattern, HandlerFunc(handler))
  }
}


  • http.ListenAndServe(":8080", nil)

    • http.ListenAndServeは、HTTPサーバを立ち上げる関数です。

    • 第一引数に、立ち上げるためのアドレスを指定します。

    • 第二引数には、ServeMuxを指定します。nilの場合はDefaultのServeMuxを立ち上げる仕組みになっています。


  • r.URL.Query().Get("name")
    • このコードは、リクエストURLに含まれるnameクエリパラメータの値を取得します。


  • fmt.Fprintf(w, "hello %s", name)

    • このコードは、レスポンスボディにhello ${name}という文字列を出力します。

    • 第一引数は、ResponseWriterを設定します。

    • 第二引数は、レスポンスボディに書き込むフォーマットを指定します。

テスト

サンプルコード

package main

import (
  "net/http"
  "net/http/httptest"
  "testing"
)

func TestHelloName(t *testing.T) {
  t.Run("正常系", func(t *testing.T) {
    req, _ := http.NewRequest(http.MethodGet, "/hello?name=dmm", nil)
    resp := httptest.NewRecorder()
    HelloName(resp, req)
    got := resp.Body.String()
    expected := "hello dmm"
    if got != expected {
        t.Errorf("got %q, expected %q", got, expected)
    }
  })
  t.Run("nameが設定されていない場合", func(t *testing.T) {
    req, _ := http.NewRequest(http.MethodGet, "/hello", nil)
    resp := httptest.NewRecorder()
    HelloName(resp, req)
    got := resp.Body.String()
    expected := "Name parameter is missing\n"
    if got != expected {
        t.Errorf("got %q, expected %q", got, expected)
    }
  })
  t.Run("リクエストメソッドがGETでない場合", func(t *testing.T) {
    req, _ := http.NewRequest(http.MethodPost, "/hello?name=dmm", nil)
    resp := httptest.NewRecorder()
    HelloName(resp, req)
    // ステータスコードを確認
    if resp.Code != http.StatusMethodNotAllowed {
        t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, resp.Code)
    }
  })
}

各テストケースの解説

HelloName関数の動作確認のため、以下の簡単な3つのテストケースを用意しました。

  1. 正常系:正常にリクエストが処理される場合。
  2. クエリパラメータが設定されていない場合:必須のパラメータが欠落している場合。
  3. リクエストメソッドが異なる場合:GET以外のメソッドが送信された場合。

1. 正常系

問題なくリクエストが成功するテストです。

// テスト内容
t.Run("正常系", func(t *testing.T) {
  req, _ := http.NewRequest(http.MethodGet, "/hello?name=dmm", nil)
  resp := httptest.NewRecorder()
  HelloName(resp, req)
  got := resp.Body.String()
  expected := "hello dmm"
  if got != expected {
      t.Errorf("got %q, expected %q", got, expected)
  }
})
  • req, _ := http.NewRequest(http.MethodGet, "/hello", nil)

    • ここでは、リクエストするリクエストメソッド、エンドポイントとリクエストボディを設定します。

    • 第一引数に、リクエストメソッドを指定します。

    • 第二引数に、エンドポイントを指定します。

    • 第三引数には、リクエストボディを指定しますが、GETではリクエストボディでデータを送信することは一般的でないため、今回はnilにしています。


  • resp := httptest.NewRecorder()

    • これは、レスポンスを記録するためのモックを作成します。

    • 実際のHTTPレスポンスの代わりに、テストで結果を検証するために使用されます。


  • HelloName(resp, req)

    • 作成したリクエストとResponseRecorderをHandlerに渡して実行します。


  • got := resp.Body.String()

    • レスポンスボディの内容を文字列として取得します。


  • expected := "hello dmm"

    • 期待される結果を定義します。今回の場合は、dmmという名前を渡しているため、"hello dmm"という文字列を期待しています。


  • if got != expected { t.Errorf("got %q, expected %q", got, expected) }

    • 実際の結果と期待される結果を比較し、異なる場合はテストが失敗します。


2. クエリパラメータが設定されていない場合

t.Run("nameが設定されていない場合", func(t *testing.T) {
  req, _ := http.NewRequest(http.MethodGet, "/hello", nil)
  resp := httptest.NewRecorder()
  HelloName(resp, req)
  got := resp.Body.String()
  expected := "Name parameter is missing\n"
  if got != expected {
      t.Errorf("got %q, expected %q", got, expected)
  }
})
  • このテストケースでは、クエリパラメータにnameを指定しないリクエストを送信した場合の挙動を確認しています。

  • "Name parameter is missing\n"というエラーメッセージが帰ってきているか検証します。

3. リクエストメソッドが異なる場合

t.Run("リクエストメソッドがGETでない場合", func(t *testing.T) {
  req, _ := http.NewRequest(http.MethodPost, "/hello?name=dmm", nil)
  resp := httptest.NewRecorder()
  HelloName(resp, req)
  if resp.Code != http.StatusMethodNotAllowed {
      t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, resp.Code)
  }
})
  • POSTリクエストを作成し、Handlerを実行します。

  • resp.Codeでレスポンスのステータスコードを確認し、405 Method Not Allowedであることを検証します。

まとめ

この記事では、Go言語の net/http パッケージを使って簡単なHTTPサーバを構築し、そのテストコードを実装しました。

以下の2つのポイントを確認できました。

  1. HTTPサーバの構築:Goではnet/httpを用いて数行のコードでHTTPサーバを立ち上げられる。

  2. テストコードが簡単にかける:標準ライブラリの httptest を使用することで、HTTPHandlerの動作確認やエラーハンドリングを簡単にテストできる。

Go言語は、そのシンプルな構文と強力な標準ライブラリにより、効率的にWebアプリケーションを開発する環境を提供してくれます。

ぜひGo言語を活用したWeb開発に挑戦してみてください!