Let's Go Further ارسال پاسخ‌های JSON › کدگذاری structها
قبلی · فهرست · بعدی
فصل ۳.۳.

کدگذاری structها

در این فصل دوباره سراغ متد showMovieHandler که قبلا ساختیم می‌رویم و آن را به‌روزرسانی می‌کنیم تا یک پاسخ JSON برگرداند که نماینده یک فیلم در سیستم ماست. چیزی شبیه این:

{
    "id": 123,
    "title": "Casablanca",
    "runtime": 102,
    "genres": [
        "drama",
        "romance",
        "war"
    ],
    "version": 1
}

این بار به جای encode کردن یک map برای ساخت این JSON object، کاری که در فصل قبل انجام دادیم، یک struct سفارشی به نام Movie را encode می‌کنیم.

پس اول از همه باید با تعریف یک struct سفارشی به نام Movie شروع کنیم. این کار را داخل یک پکیج جدید به نام internal/data انجام می‌دهیم؛ پکیجی که بعدا بزرگ‌تر می‌شود و همه typeهای داده سفارشی پروژه ما را همراه با منطق تعامل با database در خودش نگه می‌دارد.

اگر همراه کتاب جلو می‌روید، یک directory جدید به نام internal/data بسازید که یک فایل movies.go داخلش داشته باشد:

$ mkdir internal/data
$ touch internal/data/movies.go

و در این فایل جدید، struct سفارشی Movie را به این شکل تعریف می‌کنیم:

File: internal/data/movies.go
package data

import (
    "time"
)

type Movie struct {
    ID        int64     // Unique integer ID for the movie
    CreatedAt time.Time // Timestamp for when the movie is added to our database
    Title     string    // Movie title
    Year      int32     // Movie release year
    Runtime   int32     // Movie runtime (in minutes)
    Genres    []string  // Slice of genres for the movie (romance, comedy, etc.)
    Version   int32     // The version number starts at 1 and will be incremented each 
                        // time the movie information is updated
}

حالا که این کار انجام شد، showMovieHandler را به‌روزرسانی می‌کنیم تا یک instance از struct مربوط به Movie با مقداری داده ساختگی بسازد و بعد با استفاده از helper مربوط به writeJSON() آن را به عنوان پاسخ JSON ارسال کند.

در عمل کار نسبتا ساده است:

File: cmd/api/movies.go
package main

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

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

...

func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) {
    id, err := app.readIDParam(r)
    if err != nil {
        http.NotFound(w, r)
        return
    }

    // Create a new instance of the Movie struct, containing the ID we extracted from 
    // the URL and some dummy data. Also notice that we deliberately haven't set a
    // value for the Year field.
    movie := data.Movie{
        ID:        id,
        CreatedAt: time.Now(),
        Title:     "Casablanca",
        Runtime:   102,
        Genres:    []string{"drama", "romance", "war"},
        Version:   1,
    }

    // Encode the struct to JSON and send it as the HTTP response.
    err = app.writeJSON(w, http.StatusOK, movie, nil)
    if err != nil {
        app.logger.Error(err.Error())
        http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError)
    }
}

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

API را دوباره راه‌اندازی کنید و بعد در مرورگر به localhost:4000/v1/movies/123 بروید. باید یک پاسخ JSON شبیه این ببینید:

03.03-01.png

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

تغییر keyها در JSON object

یکی از چیزهای خوب درباره encode کردن structها در Go این است که می‌توانید با annotate کردن fieldها با struct tagها، JSON را سفارشی کنید.

احتمالا رایج‌ترین استفاده از struct tagها تغییر نام keyهایی است که در JSON object ظاهر می‌شوند. این زمانی مفید است که نام fieldهای struct شما برای پاسخ‌های public-facing مناسب نیست، یا می‌خواهید در خروجی JSON از یک سبک casing متفاوت استفاده کنید.

برای نشان دادن روش انجام این کار، struct مربوط به Movies را با struct tagها annotate می‌کنیم تا برای keyها از snake_case استفاده کند. به این شکل:

File: internal/data/movies.go
package data

...

// Annotate the Movie struct with struct tags to control how the keys appear in the 
// JSON-encoded output.
type Movie struct {
    ID        int64     `json:"id"`
    CreatedAt time.Time `json:"created_at"`
    Title     string    `json:"title"`
    Year      int32     `json:"year"`
    Runtime   int32     `json:"runtime"`
    Genres    []string  `json:"genres"`
    Version   int32     `json:"version"`
}

و اگر server را دوباره راه‌اندازی کنید و دوباره به localhost:4000/v1/movies/123 بروید، حالا باید پاسخی با keyهای snake_case شبیه این ببینید:

03.03-02.png

مخفی کردن fieldهای struct در JSON object

همچنین می‌توان با استفاده از directiveهای struct tag یعنی omitzero و -، قابل مشاهده بودن fieldهای جداگانه struct را در JSON کنترل کرد.

directive مربوط به - یا hyphen زمانی استفاده می‌شود که هرگز نمی‌خواهید یک field مشخص از struct در خروجی JSON ظاهر شود. این برای fieldهایی مفید است که شامل اطلاعات داخلی سیستم هستند و به کاربران شما مربوط نمی‌شوند، یا شامل اطلاعات حساسی هستند که نمی‌خواهید افشا کنید، مثل hash یک password.

در مقابل، directive مربوط به omitzero یک field را در خروجی JSON مخفی می‌کند اگر و فقط اگر مقدار آن برابر با zero value type همان field باشد. برای یادآوری، zero valueهای typeهای Go که می‌توانند به JSON encode شوند این‌ها هستند:

Zero value نوع Go
false bool
"" string
0 int*, uint*, float*, rune
nil slices, maps, pointers
هر field داخل struct مقدار zero value خودش را دارد structs
هر element داخل array روی zero value مربوط به type خودش تنظیم می‌شود arrays

برای نشان دادن نحوه استفاده از این directiveها، چند تغییر دیگر در struct مربوط به Movie ایجاد می‌کنیم. field مربوط به CreatedAt به کاربران نهایی ما ربطی ندارد، پس با استفاده از directive مربوط به - همیشه آن را در خروجی مخفی می‌کنیم. همچنین از directive مربوط به omitzero استفاده می‌کنیم تا fieldهای Year، Runtime و Genres را در خروجی مخفی کنیم، اگر و فقط اگر مقدارشان برابر با zero value مربوط به type خودشان باشد.

حالا struct tagها را به این شکل به‌روزرسانی کنید:

File: internal/data/movies.go
package data

...

type Movie struct {
    ID        int64     `json:"id"`
    CreatedAt time.Time `json:"-"` // Use the - directive
    Title     string    `json:"title"`
    Year      int32     `json:"year,omitzero"`    // Add the omitzero directive
    Runtime   int32     `json:"runtime,omitzero"` // Add the omitzero directive
    Genres    []string  `json:"genres,omitzero"`  // Add the omitzero directive
    Version   int32     `json:"version"`
}

حالا وقتی application را دوباره راه‌اندازی کنید و مرورگر را refresh کنید، باید پاسخی ببینید که دقیقا شبیه این است:

03.03-03.png

اینجا می‌بینیم که field مربوط به CreatedAt دیگر اصلا در JSON ظاهر نمی‌شود و field مربوط به Year هم که مقدار 0 داشت، به لطف directive مربوط به omitzero ظاهر نشده است. fieldهای دیگری که روی آن‌ها از omitzero استفاده کردیم، یعنی Runtime و Genres، تحت تاثیر قرار نگرفته‌اند.


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

directive مربوط به omitempty

directive مربوط به omitzero که در این فصل استفاده کردیم، در Go 1.24 اضافه شده و از نظر رفتار، همپوشانی قابل توجهی با directive قدیمی‌تری به نام omitempty دارد.

directive مربوط به omitzero درباره کاری که انجام می‌دهد بسیار روشن و سازگار است: وقتی مقدار دقیقا با zero value آن type برابر باشد، آن چیز را از JSON حذف می‌کند. و این تنها کاری است که انجام می‌دهد.

در مقابل، directive مربوط به omitempty شبیه omitzero است، اما رفتارش کمتر سازگار است. این directive از چند جهت مهم با omitzero فرق دارد:

حالا که omitzero وجود دارد، تنها زمانی که معمولا استفاده از directive مربوط به omitempty را پیشنهاد می‌کنم وقتی است که می‌خواهید sliceها یا mapهای خالی را کاملا از JSON حذف کنید، به جای اینکه به یک JSON array خالی مثل [] encode شوند.

برای مثال، اگر بخواهید هر وقت field مربوط به Genres هیچ مقداری ندارد، یا nil است، آن را کاملا از JSON حذف کنید، می‌توانید از directive مربوط به omitempty به این شکل استفاده کنید:

type Movie struct {
    ID        int64     `json:"id"`
    CreatedAt time.Time `json:"-"` 
    Title     string    `json:"title"`
    Year      int32     `json:"year,omitzero"`    
    Runtime   int32     `json:"runtime,omitzero"` 
    Genres    []string  `json:"genres,omitempty"` // Use the omitempty directive
    Version   int32     `json:"version"`
}

directive مربوط به string

آخرین directive struct tag که کمتر هم استفاده می‌شود، string است. می‌توانید از این directive روی fieldهای جداگانه struct استفاده کنید تا داده در خروجی JSON به صورت string نمایش داده شود.

برای مثال، اگر بخواهیم مقدار field مربوط به Runtime به جای number به صورت string در JSON نمایش داده شود، می‌توانیم از directive مربوط به string به این شکل استفاده کنیم:

type Movie struct {
    ID        int64     `json:"id"`
    CreatedAt time.Time `json:"-"`
    Title     string    `json:"title"`
    Year      int32     `json:"year,omitzero"`
    Runtime   int32     `json:"runtime,omitzero,string"` // Add the string directive
    Genres    []string  `json:"genres,omitzero"` 
    Version   int32     `json:"version"`
}

و خروجی JSON حاصل به این شکل خواهد بود:

{
  "id": 123,
  "title": "Casablanca",
  "runtime": "102",       ← This is now a string
  "genres": [
    "drama",
    "romance",
    "war"
  ],
  "version": 1
}

توجه کنید که directive مربوط به string فقط روی fieldهای struct کار می‌کند که type آن‌ها int*، uint*، float* یا bool باشد. برای هر type دیگری از fieldهای struct، اثری نخواهد داشت.