Go の Gin でのリクエスト・レスポンス処理

Gin ではリクエストデータの取得とレスポンスの返却が直感的に行える。JSON、フォーム、ファイルなど様々な形式に対応している。

リクエストボディの取得(JSON)

JSON リクエストは構造体にバインドできる。

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

r.POST("/users", func(c *gin.Context) {
    var req CreateUserRequest
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(201, gin.H{
        "message": "User created",
        "user":    req,
    })
})

ShouldBindJSON はエラーを返すが、BindJSON はエラー時に自動で 400 レスポンスを返す。柔軟なエラーハンドリングには ShouldBindJSON を使う。

フォームデータの取得

HTML フォームからのデータも同様にバインドできる。

type LoginForm struct {
    Username string `form:"username"`
    Password string `form:"password"`
}

r.POST("/login", func(c *gin.Context) {
    var form LoginForm
    
    if err := c.ShouldBind(&form); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // 認証処理...
})

ShouldBind は Content-Type を見て自動的に適切なバインダーを選択する。

個別にパラメータを取得

構造体を使わず、個別に値を取得することもできる。

r.POST("/upload", func(c *gin.Context) {
    // フォーム値
    name := c.PostForm("name")
    description := c.DefaultPostForm("description", "No description")
    
    // クエリパラメータ
    page := c.Query("page")
    
    // パスパラメータ
    id := c.Param("id")
    
    // ヘッダー
    token := c.GetHeader("Authorization")
})

ファイルアップロード

単一ファイルのアップロードは FormFile で処理する。

r.POST("/upload", func(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.JSON(400, gin.H{"error": "File required"})
        return
    }
    
    // ファイルを保存
    dst := "./uploads/" + file.Filename
    if err := c.SaveUploadedFile(file, dst); err != nil {
        c.JSON(500, gin.H{"error": "Failed to save file"})
        return
    }
    
    c.JSON(200, gin.H{
        "filename": file.Filename,
        "size":     file.Size,
    })
})

複数ファイルの場合は MultipartForm を使う。

r.POST("/uploads", func(c *gin.Context) {
    form, _ := c.MultipartForm()
    files := form.File["files"]
    
    for _, file := range files {
        c.SaveUploadedFile(file, "./uploads/"+file.Filename)
    }
    
    c.JSON(200, gin.H{"count": len(files)})
})

JSON レスポンス

最も一般的なレスポンス形式だ。

// gin.H を使う
c.JSON(200, gin.H{
    "status": "success",
    "data":   someData,
})

// 構造体を使う
type Response struct {
    Status string      `json:"status"`
    Data   interface{} `json:"data"`
}
c.JSON(200, Response{Status: "success", Data: someData})

その他のレスポンス形式

// 文字列
c.String(200, "Hello, %s!", name)

// XML
c.XML(200, gin.H{"message": "hello"})

// YAML
c.YAML(200, gin.H{"message": "hello"})

// ファイルダウンロード
c.File("./files/report.pdf")

// ファイル添付(ダウンロードを促す)
c.FileAttachment("./files/report.pdf", "monthly-report.pdf")

// リダイレクト
c.Redirect(301, "https://example.com")

レスポンスヘッダーの設定

r.GET("/api/data", func(c *gin.Context) {
    c.Header("X-Custom-Header", "custom-value")
    c.Header("Cache-Control", "no-cache")
    
    c.JSON(200, data)
})

ストリーミングレスポンス

大きなデータを少しずつ送信する場合は Stream を使う。

r.GET("/stream", func(c *gin.Context) {
    c.Stream(func(w io.Writer) bool {
        w.Write([]byte("chunk of data\n"))
        time.Sleep(time.Second)
        return true // true を返すと継続、false で終了
    })
})

Raw ボディの取得

バインドせずに生のリクエストボディを取得したい場合もある。

r.POST("/webhook", func(c *gin.Context) {
    body, err := io.ReadAll(c.Request.Body)
    if err != nil {
        c.JSON(400, gin.H{"error": "Failed to read body"})
        return
    }
    
    // body を処理...
})

Gin のリクエスト・レスポンス処理は、Go の net/http をラップして使いやすくしたものだ。必要に応じて c.Request や c.Writer から生の http.Request と http.ResponseWriter にアクセスすることもできる。