Open11
goのライブラリ agnosticなリクエストのハンドリングを考えてみる

以前にも考えた気がするけれどもう一度考えてみる

昔考えた式指向のやつの記録が見つからない
(後で貼る)

このbindingを使えないことはないのか。errors.Joinでつなげる。

こんな感じ
// @deriving:binding
type AnotherModel struct {
ItemName string `in:"query" query:"item_name" required:"true"`
Quantity *int `in:"query" query:"quantity"`
IsSpecial bool `in:"header" header:"X-Special"`
}
func (s *AnotherModel) Bind(req *http.Request, pathVar func(string) string) error {
b := binding.New(req, pathVar)
return errors.Join(
binding.One(b, &s.ItemName, binding.Query, "item_name", parser.String, binding.Required), // Field: ItemName (string)
binding.OnePtr(b, &s.Quantity, binding.Query, "quantity", parser.Int, binding.Optional), // Field: Quantity (*int)
binding.One(b, &s.IsSpecial, binding.Header, "X-Special", parser.Bool, binding.Optional), // Field: IsSpecial (bool)
)
}

parse, don't validate的な感じにしたい気もする

ここに貼る

大雑把に必要なもの
Go 1.22以降の net/http
パッケージを使用して、OpenAPIで定義されるような各種パラメータをバインディング(取得・割り当て)する方法には、以下の種類があります。
-
クエリパラメータ (Query Parameters)
- HTTPリクエストのURLに含まれる
?key=value
形式のパラメータを取得します。リクエストオブジェクトのURL.Query()
メソッドを使って値を取得できます。
- HTTPリクエストのURLに含まれる
-
パスパラメータ (Path Parameters)
- URLのパスの一部(例:
/users/{id}
の{id}
部分)をパラメータとして取得します。Go 1.22からhttp.ServeMux
がパスのワイルドカードをサポートし、リクエストオブジェクトのPathValue()
メソッドで値を取得できるようになりました。
- URLのパスの一部(例:
-
リクエストヘッダ (Header Parameters)
- HTTPリクエストヘッダ(例:
X-API-Key
)に含まれる値を取得します。リクエストオブジェクトのHeader.Get()
メソッドなどを使用します。
- HTTPリクエストヘッダ(例:
-
リクエストボディ (Request Body)
- 主にJSONやXML形式で送信されるリクエストの本体部分を、構造体などにデコード(バインド)します。
json.NewDecoder
などをリクエストのBody
と組み合わせて使用します。
- 主にJSONやXML形式で送信されるリクエストの本体部分を、構造体などにデコード(バインド)します。
-
フォームデータ (Form Data)
- HTMLのフォームから送信される
application/x-www-form-urlencoded
やmultipart/form-data
形式のデータを取得します。リクエストのParseForm()
やParseMultipartForm()
を呼び出した後、FormValue()
やFormFile()
などのメソッドで値やファイルを取得します。
- HTMLのフォームから送信される
-
クッキー (Cookie Parameters)
- リクエストヘッダに含まれるCookieの値を取得します。リクエストオブジェクトの
Cookie()
メソッドで特定の名前のクッキーを取得できます。
- リクエストヘッダに含まれるCookieの値を取得します。リクエストオブジェクトの

パスパラメーター以外はリクエストに生えてる

全部試したい

はい、承知いたしました。
アプリケーションコードとhttptest
を使ったテストコードを、Go Playgroundで直接実行できる単一のファイルにまとめます。
Go Playgroundでは go test
コマンドを実行できるため、このコードを貼り付けて "Run" ではなく "Test" を選択すると、テストが実行されます。
Go Playground用 統合ソースコード
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// --- アプリケーションコード ---
// UserData はリクエストボディのJSONをバインドするための構造体
type UserData struct {
Age int `json:"age"`
}
// HandlerParams はアダプターからビジネスロジックへ渡す、全パラメータを格納する構造体
type HandlerParams struct {
UserID string
QueryName string
APIKey string
SessionID string
// ボディの形式によってどちらか一方に値が入る
JSONBody UserData
FormData string // デモのためフォームの "email" フィールドを格納
}
// businessLogicHandler はHTTPの詳細に依存しないビジネスロジックを担当する
func businessLogicHandler(w http.ResponseWriter, r *http.Request, params HandlerParams) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := map[string]interface{}{
"message": "Successfully bound all parameters!",
"path_parameter": params.UserID,
"query_parameter": params.QueryName,
"header_parameter": params.APIKey,
"cookie_parameter": params.SessionID,
"json_body_received": params.JSONBody,
"form_data_received": params.FormData,
}
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("レスポンスの書き込みに失敗しました: %v", err)
}
}
// handlerAdapter は net/http のハンドラで、リクエストからパラメータを抽出し、
// ビジネスロジックを呼び出すアダプターの役割を担う
func handlerAdapter(w http.ResponseWriter, r *http.Request) {
params := HandlerParams{}
var err error
params.UserID = r.PathValue("id")
params.QueryName = r.URL.Query().Get("name")
params.APIKey = r.Header.Get("X-API-Key")
cookie, err := r.Cookie("session_id")
if err == nil {
params.SessionID = cookie.Value
}
contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/json") {
if err := json.NewDecoder(r.Body).Decode(¶ms.JSONBody); err != nil {
http.Error(w, "Invalid JSON body", http.StatusBadRequest)
return
}
} else if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
if err := r.ParseForm(); err != nil {
http.Error(w, "Could not parse form", http.StatusBadRequest)
return
}
params.FormData = r.FormValue("email")
}
businessLogicHandler(w, r, params)
}
// main関数は、Go Playgroundではサーバーとして実行できません。
// ローカル環境で `go run .` を実行する際に使用します。
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /users/{id}", handlerAdapter)
fmt.Println("サーバーをポート 8080 で起動します。")
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
if err := server.ListenAndServe(); err != nil {
log.Fatalf("サーバーの起動に失敗しました: %v", err)
}
}
// --- テストコード ---
// TestHandlerAdapterWithJSON はJSONボディを持つリクエストをテストする
func TestHandlerAdapterWithJSON(t *testing.T) {
// 1. リクエストボディの組み立て
reqBody := `{"age": 30}`
// 2. HTTPリクエストの作成
req := httptest.NewRequest(http.MethodPost, "/users/123?name=gopher", strings.NewReader(reqBody))
// 3. パスパラメータの設定 (Go 1.22+ の機能)
req.SetPathValue("id", "123")
// 4. ヘッダーとクッキーの組み立て
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", "my-secret-key")
req.AddCookie(&http.Cookie{Name: "session_id", Value: "abcdef12345"})
// 5. レスポンスを記録するための ResponseRecorder を作成
rr := httptest.NewRecorder()
// 6. テスト対象のハンドラを直接呼び出す
handlerAdapter(rr, req)
// 7. 結果の検証
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
var resp map[string]interface{}
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("Could not decode response body: %v", err)
}
if resp["path_parameter"] != "123" {
t.Errorf("unexpected path parameter: got %v want %v", resp["path_parameter"], "123")
}
if resp["query_parameter"] != "gopher" {
t.Errorf("unexpected query parameter: got %v want %v", resp["query_parameter"], "gopher")
}
if resp["header_parameter"] != "my-secret-key" {
t.Errorf("unexpected header parameter: got %v want %v", resp["header_parameter"], "my-secret-key")
}
if resp["cookie_parameter"] != "abcdef12345" {
t.Errorf("unexpected cookie parameter: got %v want %v", resp["cookie_parameter"], "abcdef12345")
}
jsonBody := resp["json_body_received"].(map[string]interface{})
if jsonBody["age"].(float64) != 30 {
t.Errorf("unexpected json body value: got %v want %v", jsonBody["age"], 30)
}
}
// TestHandlerAdapterWithForm はフォームデータを持つリクエストをテストする
func TestHandlerAdapterWithForm(t *testing.T) {
// 1. リクエストボディの組み立て
formData := "email=gopher@example.com"
// 2. HTTPリクエストの作成
req := httptest.NewRequest(http.MethodPost, "/users/456?name=engineer", strings.NewReader(formData))
// 3. パスパラメータの設定
req.SetPathValue("id", "456")
// 4. ヘッダーとクッキーの組み立て
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("X-API-Key", "my-secret-key-2")
req.AddCookie(&http.Cookie{Name: "session_id", Value: "fedcba54321"})
// 5. レスポンスレコーダーの作成
rr := httptest.NewRecorder()
// 6. ハンドラの呼び出し
handlerAdapter(rr, req)
// 7. 結果の検証
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
var resp map[string]interface{}
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("Could not decode response body: %v", err)
}
if resp["form_data_received"] != "gopher@example.com" {
t.Errorf("unexpected form data: got %v want %v", resp["form_data_received"], "gopher@example.com")
}
}

このハンドルアダプターは生成できる(やらない)