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[string]interface{} و slice:

// تحويل الخريطة إلى JSON
myMap := map[string]interface{}{
    "name": "Bob",
    "age":  25,
}
jsonData, err := json.Marshal(myMap)
// ... تجاهل معالجة الأخطاء والناتج ...

// تحويل القطعة إلى JSON
mySlice := []string{"Apple", "Banana", "Cherry"}
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: "African Elephant",
        Description: "ثديي كبير ذو خرطوم ونابات.",
        Tag:         "مهدد بالانقراض", // لن يتم تسلسل هذا الحقل إلى JSON
    }
    jsonData, err := json.Marshal(animal)
    if err != nil {
        log.Fatalf("فشل تسلسل JSON: %s", err)
    }
    fmt.Println(string(jsonData)) // الناتج: {"species":"African Elephant","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 التحليل الديناميكي

في بعض الأحيان، الهيكل الذي نحتاج لتحليله ليس معروفاً مسبقاً، أو قد يتغير بشكل ديناميكي. في مثل هذه الحالات، يمكننا استخدام 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، تتوافق مع القطع.

فكر في المصفوفة JSON التالية:

[
    {"name": "دايف", "age": 34},
    {"name": "إيف", "age": 28}
]

في Go، نقوم بتعريف الهيكل المقابل والقطع على النحو التالي:

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 إلى قطع من نوع 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":"غير معروف"}`) // "العمر" يجب أن يكون عددًا، لكن تم توفير سلسلة هنا
    var u User

    err := json.Unmarshal(data, &u)
    if err != nil {
        log.Fatalf("فشل فك تسلسل JSON: %v", err)
    }
    
    fmt.Printf("%+v\n", u)
}

في هذا المثال، قمنا بتوفير نوع بيانات خاطئ لحقل "العمر" (سلسلة بدلاً من العدد المتوقع)، مما تسبب في رمي json.Unmarshal لخطأ. لذلك، نحتاج إلى التعامل مع هذا الوضع بشكل مناسب. الممارسة الشائعة هي تسجيل رسالة الخطأ وربما إرجاع كائن فارغ أو قيمة افتراضية أو رسالة خطأ تبعًا للسيناريو.

6 الميزات المتقدمة وتحسين الأداء

6.1 التخصيص في عمليات التسلسل والإزالة

بشكل افتراضي، يقوم الحزمة 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 أنواع مشفرة وفكريّة، والتي يمكن أن تعالج بيانات 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("Encoding error: %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("Decoding error: %v", err)
    }
    
    for _, u := range users {
        fmt.Printf("%+v\n", u)
    }
}

من خلال معالجة البيانات باستخدام المشفرات والفكّرات، يمكنك تنفيذ معالجة JSON أثناء القراءة، مما يقلل من استخدام الذاكرة ويحسن من كفاءة المعالجة، مما يكون خاصة مفيدًا لمعالجة عمليات النقل عبر الشبكة أو الملفات الكبيرة.