Let's Go Further parse کردن درخواست‌های JSON › decode کردن JSON
قبلی · فهرست · بعدی
فصل ۴.۱.

decode کردن JSON

درست مثل encoding به JSON، برای decode کردن JSON به یک مقدار native در Go دو رویکرد دارید: استفاده از type مربوط به json.Decoder یا استفاده از function مربوط به json.Unmarshal().

هر دو رویکرد مزایا و معایب خودشان را دارند، اما برای decode کردن JSON از یک HTTP request body، استفاده از json.Decoder معمولا بهترین انتخاب است. این روش نسبت به json.Unmarshal() efficientتر است، کد کمتری لازم دارد، و چند setting مفید ارائه می‌کند که می‌توانید برای تنظیم رفتار آن استفاده کنید.

نشان دادن نحوه کار json.Decoder با کد از توضیح کلامی ساده‌تر است، پس مستقیم وارد کار می‌شویم و createMovieHandler را به این شکل به‌روزرسانی می‌کنیم:

File: cmd/api/movies.go
package main

import (
    "encoding/json" // New import
    "fmt"
    "net/http"
    "time"

    "greenlight.alexedwards.net/internal/data"
)

...

func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
    // Declare an anonymous struct to hold the information that we expect to be in the 
    // HTTP request body (note that the field names and types in the struct are a subset
    // of the Movie struct that we created earlier). This struct will be our *target 
    // decode destination*.
    var input struct {
        Title   string   `json:"title"`
        Year    int32    `json:"year"`
        Runtime int32    `json:"runtime"`
        Genres  []string `json:"genres"`
    }

    // Initialize a new json.Decoder instance which reads from the request body, and 
    // then use the Decode() method to decode the body contents into the input struct. 
    // Importantly, notice that when we call Decode() we pass a *pointer* to the input 
    // struct as the target decode destination. If there was an error during decoding,
    // we use our generic errorResponse() helper to send a 400 Bad Request response
    // with the error message to the client.
    err := json.NewDecoder(r.Body).Decode(&input)
    if err != nil {
        app.errorResponse(w, r, http.StatusBadRequest, err.Error())
        return
    }

    // Dump the contents of the input struct in an HTTP response.
    fmt.Fprintf(w, "%+v\n", input)
}

...

چند نکته مهم و جالب درباره این کد وجود دارد که باید به آن‌ها اشاره کنیم:

خب، بیایید امتحانش کنیم.

application را اجرا کنید، بعد یک terminal window دوم باز کنید و یک request به endpoint مربوط به POST /v1/movies با یک JSON request body معتبر شامل مقداری داده فیلم بفرستید. باید پاسخی شبیه این ببینید:

# Create a BODY variable containing the JSON data that we want to send.
$ BODY='{"title":"Moana","year":2016,"runtime":107, "genres":["animation","adventure"]}'

# Use the -d flag to send the contents of the BODY variable as the HTTP request body.
# Note that curl will default to sending a POST request when the -d flag is used.
$ curl -i -d "$BODY" localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 17:13:46 GMT
Content-Length: 65
Content-Type: text/plain; charset=utf-8

{Title:Moana Year:2016 Runtime:107 Genres:[animation adventure]}

عالی! به نظر می‌رسد خوب کار کرده است. از داده‌ای که در پاسخ dump شده می‌بینیم مقدارهایی که در request body فرستادیم، داخل fieldهای مناسب struct مربوط به input decode شده‌اند.

Zero valueها

بیایید سریع ببینیم اگر یک جفت key-value مشخص را از JSON request body حذف کنیم چه اتفاقی می‌افتد. برای مثال، یک request بدون year در JSON می‌فرستیم، به این شکل:

$ BODY='{"title":"Moana","runtime":107, "genres":["animation","adventure"]}'
$ curl -d "$BODY" localhost:4000/v1/movies
{Title:Moana Year:0 Runtime:107 Genres:[animation adventure]}

همان‌طور که احتمالا حدس زده‌اید، وقتی این کار را انجام می‌دهیم field مربوط به Year در struct مربوط به input با zero value خودش باقی می‌ماند؛ که در این مورد 0 است، چون field مربوط به Year از type نوع int32 است.

این ما را به یک سوال جالب می‌رساند: چطور می‌توان تفاوت بین حالتی را فهمید که client یک جفت key-value را اصلا ارائه نکرده، و حالتی که آن جفت key-value را ارائه کرده اما عمدا مقدارش را روی zero value تنظیم کرده است؟ مثل این:

$ BODY='{"title":"Moana","year":0,"runtime":107, "genres":["animation","adventure"]}'
$ curl -d "$BODY" localhost:4000/v1/movies
{Title:Moana Year:0 Runtime:107 Genres:[animation adventure]}

با وجود متفاوت بودن HTTP requestها، نتیجه نهایی یکسان است و فورا مشخص نیست چطور باید تفاوت این دو سناریو را تشخیص داد. بعدا در کتاب به این موضوع برمی‌گردیم، اما فعلا فقط خوب است از این رفتار آگاه باشید.


اطلاعات تکمیلی

typeهای مقصد پشتیبانی‌شده

مهم است اشاره کنیم که بعضی typeهای JSON فقط می‌توانند با موفقیت به بعضی typeهای Go decode شوند. برای مثال، اگر string در JSON یعنی "foo" را داشته باشید، می‌تواند به string در Go decode شود، اما تلاش برای decode کردن آن به int یا bool در Go باعث خطا در runtime می‌شود؛ همان‌طور که در فصل بعد نشان می‌دهیم.

جدول زیر مقصدهای decode پشتیبانی‌شده را برای typeهای مختلف JSON نشان می‌دهد:

نوع JSON typeهای Go پشتیبانی‌شده
boolean در JSON bool
string در JSON string
number در JSON int*, uint*, float*, rune
array در JSON array, slice
object در JSON struct, map

استفاده از function مربوط به json.Unmarshal

همان‌طور که در ابتدای این فصل گفتیم، می‌توان از function مربوط به json.Unmarshal() هم برای decode کردن یک HTTP request body استفاده کرد.

برای مثال، می‌توانید در یک handler به این شکل از آن استفاده کنید:

func (app *application) exampleHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Foo string `json:"foo"`
    }

    // Use io.ReadAll() to read the entire request body into a []byte slice.
    body, err := io.ReadAll(r.Body)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }
    
    // Use the json.Unmarshal() function to decode the JSON in the []byte slice to the
    // input struct. Again, notice that we are using a *pointer* to the input
    // struct as the decode destination.
    err = json.Unmarshal(body, &input)
    if err != nil {
        app.errorResponse(w, r, http.StatusBadRequest, err.Error())
        return
    }

    fmt.Fprintf(w, "%+v\n", input)
}

...

استفاده از این رویکرد مشکلی ندارد؛ کد کار می‌کند و روشن و ساده است. اما نسبت به رویکرد json.Decoder که همین حالا از آن استفاده می‌کنیم، مزیت اضافه‌ای ارائه نمی‌دهد.

کد نه تنها کمی verboseتر است، بلکه efficientتر هم نیست. اگر برای این use case مشخص performance نسبی را benchmark کنیم، می‌بینیم استفاده از json.Unmarshal() حدود ۸۰٪ memory بیشتری (B/op) نسبت به json.Decoder نیاز دارد و کمی هم کندتر است (ns/op).

$ go test -run=^$ -bench=. -benchmem -count=3 -benchtime=5s
goos: linux
goarch: amd64
BenchmarkUnmarshal-8      528088      9543 ns/op     2992 B/op     20 allocs/op
BenchmarkUnmarshal-8      554365     10469 ns/op     2992 B/op     20 allocs/op
BenchmarkUnmarshal-8      537139     10531 ns/op     2992 B/op     20 allocs/op
BenchmarkDecoder-8        811063      8644 ns/op     1664 B/op     21 allocs/op
BenchmarkDecoder-8        672088      8529 ns/op     1664 B/op     21 allocs/op
BenchmarkDecoder-8       1000000      7573 ns/op     1664 B/op     21 allocs/op

جزئیات بیشتر در decode کردن JSON

چند نکته ظریف درباره decode کردن JSON وجود دارد که دانستنشان مهم یا جالب است، اما به‌خوبی در متن اصلی این کتاب جا نمی‌گیرند. این ضمیمه را اضافه کرده‌ام که آن‌ها را با جزئیات توضیح می‌دهد و نشان می‌دهد.