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 را به این شکل بهروزرسانی میکنیم:
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) } ...
چند نکته مهم و جالب درباره این کد وجود دارد که باید به آنها اشاره کنیم:
هنگام صدا زدن
Decode()باید یک pointer غیر nil را به عنوان مقصد decode پاس بدهید. اگر از pointer استفاده نکنید، در runtime خطایjson.InvalidUnmarshalErrorبرمیگرداند.اگر مقصد decode یک struct باشد، مثل مورد ما، fieldهای struct باید exported باشند، یعنی با حرف بزرگ شروع شوند. درست مثل encoding، باید exported باشند تا برای پکیج
encoding/jsonقابل مشاهده باشند.هنگام decode کردن یک JSON object به struct، جفتهای key-value داخل JSON بر اساس نام struct tagها به fieldهای struct map میشوند. اگر struct tag منطبق وجود نداشته باشد، Go تلاش میکند مقدار را داخل fieldای decode کند که با key name منطبق است؛ match دقیق ترجیح داده میشود، اما اگر نبود سراغ match غیر حساس به بزرگی و کوچکی حروف میرود. هر جفت key-value در JSON که نتواند با موفقیت به fieldهای struct map شود، بیصدا نادیده گرفته میشود.
بعد از خوانده شدن
r.Bodyلازم نیست آن را close کنید. این کار به صورت خودکار توسطhttp.Serverدر Go انجام میشود، پس نیازی نیست شما انجامش دهید.
خب، بیایید امتحانش کنیم.
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 وجود دارد که دانستنشان مهم یا جالب است، اما بهخوبی در متن اصلی این کتاب جا نمیگیرند. این ضمیمه را اضافه کردهام که آنها را با جزئیات توضیح میدهد و نشان میدهد.