1. مروری بر کتابخانه استاندارد encoding/json

زبان Go یک کتابخانه قدرتمند به نام encoding/json را برای برخورد با فرمت داده‌های JSON فراهم می‌کند. با استفاده از این کتابخانه، می‌توانید به راحتی انواع داده‌های Go را به فرمت JSON تبدیل کرده (سریال‌سازی) یا داده‌های JSON را به انواع داده‌های Go تبدیل کنید (غیرسریال‌سازی). این کتابخانه امکانات زیادی مانند رمزنگاری، رمزگشایی، ورودی/خروجی جریانی و پشتیبانی از منطق تجزیه و تحلیل JSON سفارشی را فراهم می‌کند.

انواع داده‌ها و توابع مهم‌تری که در این کتابخانه وجود دارند عبارتند از:

  • Marshal و MarshalIndent: برای سریال‌سازی انواع داده‌های Go به رشته‌های JSON استفاده می‌شود.
  • Unmarshal: برای غیرسریال‌سازی رشته‌های JSON به انواع داده‌های Go استفاده می‌شود.
  • Encoder و Decoder: برای ورودی/خروجی جریانی داده‌های JSON استفاده می‌شود.
  • Valid: برای بررسی اینکه آیا یک رشته داده به عنوان یک فرمت JSON معتبر است یا خیر.

ما به طور خاص در فصول آتی نحوه استفاده از این توابع و انواع را یاد خواهیم گرفت.

2. سریال‌سازی ساختارهای داده‌ای Go به JSON

2.1. استفاده از json.Marshal

json.Marshal یک تابع است که انواع داده‌های Go را به رشته‌های JSON سریال‌سازی می‌کند. این تابع انواع داده را از زبان Go بعنوان ورودی می‌گیرد، آن‌ها را به فرمت JSON تبدیل می‌کند، و یک برش بایت همراه با خطاهای احتمالی برمی‌گرداند.

در ادامه یک مثال ساده آمده است که نحوه تبدیل یک ساختار داده‌ای Go به یک رشته JSON را نشان می‌دهد:

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    person := Person{"Alice", 30}
    jsonData, err := json.Marshal(person)
    if err != nil {
        log.Fatalf("خطا در سریال‌سازی JSON: %s", err)
    }
    fmt.Println(string(jsonData)) // خروجی: {"name":"Alice","age":30}
}

به علاوه از ساختارها، تابع json.Marshal همچنین می‌تواند انواع داده‌ای دیگر مانند map و slice را نیز سریال‌سازی کند. دو مثال زیر نحوه استفاده از map[string]interface{} و slice را نشان می‌دهند:

// تبدیل map به JSON
myMap := map[string]interface{}{
    "name": "Bob",
    "age":  25,
}
jsonData, err := json.Marshal(myMap)
// ... بدست آوردن خطا و خروجی حذف شده است ...

// تبدیل slice به JSON
mySlice := []string{"سیب", "موز", "گیلاس"}
jsonData, err := json.Marshal(mySlice)
// ... بدست آوردن خطا و خروجی حذف شده است ...

2.2. برچسب‌های ساختار

در زبان Go، برچسب‌های ساختاری برای ارائه داده‌های فراداده برای فیلدهای ساختاری مورد استفاده قرار می‌گیرند و رفتار سریال‌سازی JSON را کنترل می‌کنند. پرکاربردترین موارد استفاده شامل تغییر نام فیلدها، نادیده‌گرفتن فیلدها و سریال‌سازی شرطی می‌باشد.

به عنوان مثال، شما می‌توانید از برچسب json:"<نام>" برای مشخص کردن نام فیلد JSON استفاده کنید:

type Animal struct {
    SpeciesName string `json:"species"`
    Description string `json:"desc,omitempty"`
    Tag         string `json:"-"` // افزودن برچسب "-" نشان می‌دهد که این فیلد سریال‌سازی نخواهد شد
}

در مثال فوق، برچسب json:"-" در ابتدای فیلد Tag به json.Marshal می‌گوید که این فیلد را نادیده بگیرد. گزینه omitempty برای فیلد Description نشان می‌دهد که اگر فیلد خالی باشد (مقدار صفر، مانند یک رشته خالی)، در JSON سریال‌سازی نخواهد شد.

در ادامه یک مثال کامل با استفاده از برچسب‌های ساختاری آمده است:

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Animal struct {
    SpeciesName string `json:"species"`
    Description string `json:"desc,omitempty"`
    Tag         string `json:"-"`
}

func main() {
    animal := Animal{
        SpeciesName: "گاج آفریقایی",
        Description: "یک پستاندار بزرگ با خرطوم و دندان‌های خیسلان.",
        Tag:         "متاثر شده از انقراض", // این فیلد به JSON سریال‌سازی نخواهد شد
    }
    jsonData, err := json.Marshal(animal)
    if err != nil {
        log.Fatalf("خطا در سریال‌سازی JSON: %s", err)
    }
    fmt.Println(string(jsonData)) // خروجی: {"species":"گاج آفریقایی","desc":"یک پستاندار بزرگ با خرطوم و دندان‌های خیسلان."}
}

به این ترتیب، شما می‌توانید با اطمینان از ساختار داده‌ها اطمینان حاصل کرده و همزمان نحوه نمایش JSON را کنترل نمایید و نیاز‌های مختلف سریال‌سازی را با انعطاف مدیریت کنید.

3. غیرسریال‌سازی JSON به ساختار داده‌ای Go

3.1 استفاده از json.Unmarshal

تابع json.Unmarshal به ما اجازه می‌دهد که رشته‌های JSON را به ساختارهای داده‌ای Go مانند ساختارها، نقشه‌ها و غیره تجزیه کنیم. برای استفاده از json.Unmarshal، ابتدا باید یک ساختار داده‌ای Go را تعریف کنیم که با داده‌های JSON مطابقت داشته باشد.

با فرض داشتن داده JSON زیر:

{
    "name": "Alice",
    "age": 25,
    "emails": ["[email protected]", "[email protected]"]
}

برای تجزیه این داده به یک ساختار Go، باید یک ساختار سازگار تعریف کنیم:

type User struct {
    Name   string   `json:"name"`
    Age    int      `json:"age"`
    Emails []string `json:"emails"`
}

حال می‌توانیم از json.Unmarshal برای توسعه کاربردی استفاده کنیم:

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonData := `{
        "name": "Alice",
        "age": 25,
        "emails": ["[email protected]", "[email protected]"]
    }`

    var user User
    err := json.Unmarshal([]byte(jsonData), &user)
    if err != nil {
        fmt.Println("Error unmarshalling JSON:", err)
        return
    }

    fmt.Printf("User: %+v\n", user)
}

در مثال بالا، از تگ‌های مانند json:"name" برای اطلاع‌رسانی به تابع json.Unmarshal در مورد نگاشت فیلدهای JSON به فیلدهای ساختار استفاده شده است.

3.2 تجزیه پویا

گاهی اوقات ساختار JSONی که باید آن‌ را تجزیه کنیم از قبل شناور نیست یا ساختار داده JSON ممکن است به‌طور پویا تغییر کند. در چنین مواردی، می‌توانیم از interface{} یا json.RawMessage برای تجزیه استفاده کنیم.

استفاده از interface{} به ما اجازه می‌دهد بدون دانستن ساختار JSON، تجزیه کنیم:

func main() {
    jsonData := `{
        "name": "Alice",
        "details": {
            "age": 25,
            "job": "Engineer"
        }
    }`

    var result map[string]interface{}
    json.Unmarshal([]byte(jsonData), &result)

    fmt.Println(result)

    // ادعای نوع، پیش از استفاده، اطمینان از مطابقت نوع
    name := result["name"].(string)
    fmt.Println("Name:", name)
    details := result["details"].(map[string]interface{})
    age := details["age"].(float64) // توجه: اعداد در `interface{}` به عنوان float64 به حساب می‌آیند
    fmt.Println("Age:", age)
}

استفاده از json.RawMessage به ما اجازه می‌دهد که در حالی که بخش‌های مورد نظر آن را تجزیه می‌کنیم، JSON اصلی را حفظ کنیم:

type UserDynamic struct {
    Name    string          `json:"name"`
    Details json.RawMessage `json:"details"`
}

func main() {
    jsonData := `{
        "name": "Alice",
        "details": {
            "age": 25,
            "job": "Engineer"
        }
    }`

    var user UserDynamic
    json.Unmarshal([]byte(jsonData), &user)

    var details map[string]interface{}
    json.Unmarshal(user.Details, &details)

    fmt.Println("Name:", user.Name)
    fmt.Println("Age:", details["age"])
    fmt.Println("Job:", details["job"])
}

این رویکرد برای کنترل ساختارهای JSON منعطف و مواردی که ممکن است فیلدهای خاصی دارای انواع داده‌ای مختلف باشند، مفید است.

4. پردازش ساختارهای تودرتو و آرایه‌ها

4.1 اشیاء JSON تودرتو

داده‌های معمول JSON معمولاً صاف نیستند، بلکه حاوی ساختارهای تودرتو هستند. در Go، می‌توانیم این وضعیت را با تعریف ساختارهای تودرتو مدیریت کنیم.

فرض کنید داده JSON تودرتو زیر را داریم:

{
    "name": "Bob",
    "contact": {
        "email": "[email protected]",
        "address": "123 Main St"
    }
}

می‌توانیم ساختار Go را به شکل زیر تعریف کنیم:

type ContactInfo struct {
    Email   string `json:"email"`
    Address string `json:"address"`
}

type UserWithContact struct {
    Name    string      `json:"name"`
    Contact ContactInfo `json:"contact"`
}

عملیات تجزیه شبیه به ساختارهای تودرتو نیست:

func main() {
    jsonData := `{
        "name": "Bob",
        "contact": {
            "email": "[email protected]",
            "address": "123 Main St"
        }
    }`

    var user UserWithContact
    err := json.Unmarshal([]byte(jsonData), &user)
    if err != nil {
        fmt.Println("Error unmarshalling JSON:", err)
    }

    fmt.Printf("%+v\n", user)
}

4.2 آرایه‌های JSON

در JSON، آرایه‌ها یک ساختار داده متداول هستند. در Go، آن‌ها متناظر با slice ها می‌باشند.

به آرایه JSON زیر توجه کنید:

[
    {"name": "دیوید", "age": 34},
    {"name": "ایو", "age": 28}
]

در Go، ما ساختار متناظر و slice را به صورت زیر تعریف می‌کنیم:

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    jsonData := `[
        {"name": "دیوید", "age": 34},
        {"name": "ایو", "age": 28}
    ]`

    var people []Person
    json.Unmarshal([]byte(jsonData), &people)
    
    for _, person := range people {
        fmt.Printf("%+v\n", person)
    }
}

از این روش برای تجزیه‌ی هر عنصر در آرایه JSON به یک slice از ساختارهای Go برای پردازش و دسترسی بعدی استفاده می‌کنیم.

5. رفع خطاها

در هنگام برخورد با داده‌های JSON، سهولت (تبدیل داده‌های ساختاری به فرمت JSON) یا تجزیه (تبدیل JSON به داده‌های ساختاری)، خطاها ممکن است رخ دهند. در ادامه، ما به بحث در مورد خطاهای متداول و چگونگی رفع آن‌ها می‌پردازیم.

5.1 رفع خطاهای سهولت

معمولاً خطاهای سهولت هنگام فرآیند تبدیل یک ساختار یا سایر انواع داده به یک رشته JSON رخ می‌دهد. به عنوان مثال، اگر تلاشی برای سهولت یک ساختار حاوی فیلدهای غیرقانونی (مانند یک نوع کانال یا تابعی که نمی‌تواند در JSON نمایان شود) صورت گیرد، json.Marshal یک خطا برمی‌گرداند.

import (
    "encoding/json"
    "fmt"
    "log"
)

type User struct {
    Name string
    Age  int
    // فرض کنید که یک فیلد غیرقانونی در اینجا وجود دارد
    // Data chan struct{} // کانال‌ها نمی‌توانند در JSON نمایان شوند
}

func main() {
    u := User{
        Name: "الیس",
        Age:  30,
        // Data: make(chan struct{}),
    }

    bytes, err := json.Marshal(u)
    if err != nil {
        log.Fatalf("سهولت JSON ناموفق بود: %v", err)
    }
    
    fmt.Println(string(bytes))
}

در مثال بالا، ما به‌طور ارادی نظریه کانال Data را کامنت‌گذاری کردیم. اگر آن را کامنت نبریم، سهولت ناموفق خواهد بود و برنامه پیام خطا را لاگ کرده و اجرا را متوقف خواهد کرد. رفع اینگونه خطاها معمولاً شامل بررسی خطاها و پیاده‌سازی راهکارهای مربوط به رفع خطا (مانند ثبت خطاها، بازگشت دادن داده‌های پیش‌فرض و غیره) می‌شود.

5.2 رفع خطاهای تجزیه

خطاهای تجزیه ممکن است در فرآیند تبدیل یک رشته JSON به ساختار Go یا سایر انواع داده رخ دهند. به عنوان مثال، اگر فرمت رشته JSON نادرست یا ناسازگار با نوع مقصد باشد، json.Unmarshal یک خطا برمی‌گرداند.

import (
    "encoding/json"
    "fmt"
    "log"
)

func main() {
    var data = []byte(`{"name":"الیس","age":"نامعلوم"}`) // "age" باید یک عدد صحیح باشد، اما اینجا یک رشته ارائه شده است
    var u User

    err := json.Unmarshal(data, &u)
    if err != nil {
        log.Fatalf("تجزیه JSON ناموفق بود: %v", err)
    }
    
    fmt.Printf("%+v\n", u)
}

در این مثال کد، ما به طور ارادی نوع داده نادرست را برای فیلد age (یک رشته به جای عدد صحیح مورد انتظار) فراهم کردیم که باعث می‌شود json.Unmarshal یک خطا ایجاد کند. بنابراین، ما باید این موقف را به‌طور مناسب رفع کنیم. روش متداول این است که پیام خطا را لاگ نموده و بسته به سناریو، احتمالاً یک شیء خالی، مقدار پیش‌فرض یا پیام خطا بازگردانده شود.

6.1 سفارشی کردن Marshal و Unmarshal

به طور پیش‌فرض، بسته encoding/json در Go از طریق بازتاب، JSON را سریالیزه و از سریالیزه کردن توسط بازتاب استفاده می‌کند. با این حال، می‌توانیم این فرآیندها را با پیاده‌سازی رابط‌های json.Marshaler و json.Unmarshaler سفارشی‌سازی کنیم.

import (
    "encoding/json"
    "fmt"
)

type Color struct {
    Red   uint8
    Green uint8
    Blue  uint8
}

func (c Color) MarshalJSON() ([]byte, error) {
    hex := fmt.Sprintf("\"#%02x%02x%02x\"", c.Red, c.Green, c.Blue)
    return []byte(hex), nil
}

func (c *Color) UnmarshalJSON(data []byte) error {
    _, err := fmt.Sscanf(string(data), "\"#%02x%02x%02x\"", &c.Red, &c.Green, &c.Blue)
    return err
}

func main() {
    c := Color{Red: 255, Green: 99, Blue: 71}
    
    jsonColor, _ := json.Marshal(c)
    fmt.Println(string(jsonColor))

    var newColor Color
    json.Unmarshal(jsonColor, &newColor)
    fmt.Println(newColor)
}

در اینجا، نوع Color را تعریف کرده‌ایم و متدهای MarshalJSON و UnmarshalJSON را پیاده‌سازی کرده‌ایم تا رنگ‌های RGB را به رشته‌های ششگانه تبدیل کرده و سپس دوباره به رنگ‌های RGB بازگردانیم.

6.2 رمزنگارها و رمزگشاها

هنگام کار با داده‌های JSON بزرگ، استفاده مستقیم از json.Marshal و json.Unmarshal ممکن است منجر به مصرف زیاد حافظه یا عملیات ورودی/خروجی نامناسب شود. به همین دلیل، بسته encoding/json در Go انواع Encoder و Decoder را فراهم کرده است که می‌توانند داده‌های JSON را به صورت استریمینگ پردازش کنند.

6.2.1 استفاده از json.Encoder

json.Encoder می‌تواند به طور مستقیم داده‌های JSON را به هر شیء که رابط io.Writer را پیاده سازی می‌کند، بنویسد. این به معنای این است که می‌توانید داده‌های JSON را مستقیماً به یک فایل، اتصال شبکه و غیره رمزنگاری کنید.

import (
    "encoding/json"
    "os"
)

func main() {
    users := []User{
        {Name: "Alice", Age: 30},
        {Name: "Bob", Age: 25},
    }
    
    file, _ := os.Create("users.json")
    defer file.Close()
    
    encoder := json.NewEncoder(file)
    if err := encoder.Encode(users); err != nil {
        log.Fatalf("خطای رمزنگاری: %v", err)
    }
}

6.2.2 استفاده از json.Decoder

json.Decoder می‌تواند به طور مستقیم داده‌های JSON را از هر شیء که رابط io.Reader را پیاده سازی می‌کند، بخواند و شیء‌ها و آرایه‌های JSON را جستجو و تجزیه کند.

import (
    "encoding/json"
    "os"
)

func main() {
    file, _ := os.Open("users.json")
    defer file.Close()
    
    var users []User
    decoder := json.NewDecoder(file)
    if err := decoder.Decode(&users); err != nil {
        log.Fatalf("خطای رمزگشا: %v", err)
    }
    
    for _, u := range users {
        fmt.Printf("%+v\n", u)
    }
}

با پردازش داده با استفاده از رمزنگارها و رمزگشاها می‌توانید در حین خواندن فرآیند رمزنگاری JSON را انجام دهید، مصرف حافظه را کاهش دهید و کارایی پردازش را بهبود بخشید که به ویژه برای مدیریت انتقال‌های شبکه یا فایل‌های بزرگ مفید است.