اذهب إلى المحتوى

إحدى الخصائص المهمة في البرامج الحديثة هو إمكانية التواصل مع البرامج الأخرى، سواءٌ كان برنامج جو يتحقق ما إذا كان لدى المستخدم حق الوصول إلى برنامج آخر، أو برنامج جافا سكريبت JavaScript يحصل على قائمة بالطلبات السابقة لعرضها على موقع ويب، أو برنامج رست Rust يقرأ نتائج اختبار من ملف، فهناك حاجة إلى طريقة نزوّد البرامج من خلالها بالبيانات. لدى أغلب لغات البرمجة طريقة خاصة في تخزين البيانات داخليًّا، والتي لا تفهمها اللغات البرمجية الأخرى. للسماح لهذه اللغات بالتفاعل مع بعضها، يجب تحويل البيانات إلى تنسيق أو صيغة مشتركة يمكنهم فهمها جميعًا. إحدى هذه الصيغ هي صيغة جسون JSON، إنها وسيلة شائعة لنقل البيانات عبر الإنترنت وكذلك بين البرامج في نفس النظام. تمتلك لغة جو والعديد من لغات البرمجة الأخرى طريقة لتحويل البيانات من وإلى صيغة جسون في مكتباتها القياسية.

سنُنشئ في هذا المقال برنامجًا يستخدم حزمة encoding/json لتحويل صيغة البيانات من النوع map إلى بيانات جسون، ثم من النوع struct إلى جسون، كما سنتعلم كيفية إجراء تحويل عكسي.

المتطلبات

استخدام الروابط Maps لتوليد بيانات بصيغة JSON

توفر حزمة encoding/json بعض الدوال لتحويل البيانات من وإلى جسون JSON. الدالة الأولى هي json.Marshal. التنظيم Marshaling أو المعروف أيضًا باسم السلسلة Serialization، هو عملية تحويل بيانات البرنامج من الذاكرة إلى تنسيق يمكن نقله أو حفظه في مكان آخر، وهذا ما تفعله الدالة json.Marshal في لغة جو، إذ تحول بيانات البرنامج قيد التشغيل (الموجود في الذاكرة) إلى بيانات جسون. تقبل هذه الدالة أي قيمة من النوع واجهة {}interface لتنظيمها بصيغة جسون، لذلك يُسمح بتمرير أي قيمة مثل معامل، لتُعيد الدالة البيانات ممثلة بصيغة جسون في النتيجة.

سننشئ في هذا القسم برنامجًا يستخدم دالة json.Marshal لإنشاء ملف جسون يحتوي أنواعًا مختلفة من البيانات من قيم map، ثم سنطبع هذه القيم على شاشة الخرج. تُمثّل بيانات جسون غالبًا على شكل كائن مفاتيحه من سلاسل نصيّة وقيمه من أنواع مختلفة، وهذا يُشبه آلية تمثيل البيانات في روابط جو، لذا فإن الطريقة الأفضل لإنشاء بيانات جسون في لغة جو هي وضع البيانات ضمن رابطة map مع مفاتيح من النوع string وقيم من النوع {}interface. تٌفسّر مفاتيح map مباشرةً على أنها مفاتيح جسون، ويمكن أن تكون قيم النوع interface أي نوع بيانات في لغة جو، مثل int، أو string، أو حتى {}map[string]interface

لبدء استخدام الحزمة encoding/json، وكما هو معتاد، سنحتاج لبدء إنشاء برامجنا إلى إنشاء مجلد للعمل ووضع الملفات فيه، ويمكن وضع المجلد في أي مكان على الحاسب، إذ يكون للعديد من المبرمجين عادةً مجلدٌ يضعون داخله كافة مشاريعهم. سنستخدم في هذا المقال مجلدًا باسم "projects"، لذا فلننشئ هذا المجلد وننتقل إليه:

$ mkdir projects
$ cd projects

الآن، من داخل هذا المجلد، سنشغّل الأمر mkdir لإنشاء مجلد "jsondata" ثم سنستخدم cd للانتقال إليه:

$ mkdir jsondata
$ cd jsondata

يمكننا الآن فتح ملف "main.go" باستخدام محرر نانو nano أو أي محرر آخر تريده:

$ nano main.go

نضيف داخل ملف "main.go" دالة main لتشغيل البرنامج، ثم نضيف قيمة {}map[string]interface مع مفاتيح وقيم من أنواع مختلفة، ثم نستخدم الدالة json.Marshal لتحويل بيانات map إلى بيانات جسون:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    data := map[string]interface{}{
        "intValue":    1234,
        "boolValue":   true,
        "stringValue": "hello!",
        "objectValue": map[string]interface{}{
            "arrayValue": []int{1, 2, 3, 4},
        },
    }

    jsonData, err := json.Marshal(data)
    if err != nil {
        fmt.Printf("could not marshal json: %s\n", err)
        return
    }

    fmt.Printf("json data: %s\n", jsonData)
}

نلاحظ في المتغير data أن كل قيمة لديها مفتاح string، لكن قيم هذه المفاتيح تختلف، فأحدها int وأحدها bool والأخرى هي رابط {}map[string]interface مع قيم int بداخلها. عند تمرير المتغير data إلى json.Marshal، ستنظر الدالة إلى جميع القيم التي يتضمنها الرابط وتحدد نوعها وكيفية تمثيلها في جسون، وإذا حدثت مشكلة في تفسير بيانات الرابط، ستُعيد خطأ يصف المشكلة. إذا نجحت العملية، سيتضمّن المتغير jsonData بيانات من النوع byte[] تُمثّل البيانات التي جرى تنظيمها إلى صيغة جسون. بما أن byte[] يمكن تحويلها إلى قيمة string باستخدام (myString := string(jsonData أو العنصر النائب s% ضمن تنسيق سلسلة، يمكننا طباعة بيانات جسون على شاشة الخرج باستخدام دالة الطباعة fmt.Printf.

بعد حفظ وإغلاق الملف، لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

سيكون الخرج على النحو التالي:

json data: {"boolValue":true,"intValue":1234,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

نلاحظ من الخرج أن قيمة جسون هي كائن مُمثّل بأقواس معقوصة curly braces {} تحيط به، وأن جميع قيم المتغير data موجودة ضمن هذين القوسين. نلاحظ أيضًا أن رابط المفتاح objectValue الذي هو {}map[string]interface قد جرى تفسيره إلى كائن جسون آخر مُمثّل بأقواس معقوصة {} تحيط به أيضًا، ويتضمن أيضًا المفتاح arrayValue بداخله مع مصفوفة القيم المقابلة [1،2،3،4].

ترميز البيانات الزمنية في جسون

لا تقتصر قدرات الحزمة encoding/json على إمكانية تمثيل البيانات من النوع string و int، إذ يمكنها التعامل مع أنواع أعقد مثل البيانات الزمنية من النوع time.Time من الحزمة time.

لنفتح ملف البرنامج "main.go" مرةً أخرى ونضيف قيمة من النوع time.Time باستخدام الدالة time.Date:

package main

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

func main() {
    data := map[string]interface{}{
        "intValue":    1234,
        "boolValue":   true,
        "stringValue": "hello!",
        "dateValue":   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
        "objectValue": map[string]interface{}{
            "arrayValue": []int{1, 2, 3, 4},
        },
    }

    ...
}

يؤدي هذا التعديل إلى ضبط التاريخ على March 2, 2022 والوقت على ‎9:10:00 AM في المنطقة الزمنية UTC وربطهم بالمفتاح dateValue. لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

سيعطي الخرج التالي:

json data: {"boolValue":true,"dateValue":"2022-03-02T09:10:00Z","intValue":1234,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

نلاحظ هذه المرة في الخرج الحقل dateValue ضمن بيانات جسون، وأن الوقت مُنسّقٌ وفقًا لتنسيق RFC 3339، وهو تنسيق شائع يُستخدم لنقل التواريخ والأوقات على أنها قيم string.

ترميز قيم Null في جسون

قد نحتاج إلى التعامل مع قيم null، وتحويلها إلى صيغة جسون أيضًا. يمكن لحزمة encoding/json تولي هذه المهمة أيضًا، إذ يمكننا التعامل مع قيم nil (تُقابل قيم null) مثل أي قيمة من نوع آخر ضمن الرابط.

لنفتح ملف "main.go" ولنضع قيمتي null ضمن الرابط:

...

func main() {
    data := map[string]interface{}{
        "intValue":    1234,
        "boolValue":   true,
        "stringValue": "hello!",
                "dateValue":   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
        "objectValue": map[string]interface{}{
            "arrayValue": []int{1, 2, 3, 4},
        },
        "nullStringValue": nil,
        "nullIntValue":    nil,
    }

    ...
}

وضعنا في الشيفرة أعلاه قيمتي null مع مفتاحين مختلفين، هما nullStringValue و nullIntValue على التوالي، وعلى الرغم من أن أسماء المفاتيح تُشير إلى قيم string و int، لكن هي ليست كذلك (مجرد أسماء). طبعًا كل القيم ضمن الرابط مُشتقة من النوع {}interface والقيمة nil هي قيمة مُحتملة لهذا النوع وبالتالي تُفسّر على أنها null فقط، وهذا كل شيء. لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

سيكون الخرج كما يلي:

json data: {"boolValue":true,"dateValue":"2022-03-02T09:10:00Z","intValue":1234,"nullIntValue":null,"nullStringValue":null,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

نلاحظ في الخرج أن الحقلين nullIntValue و nullStringValue مُضمنان مع قيمة null لكل منهما، بالتالي تمكنا من استخدام {}map[string]interface مع قيم null دون أية مشاكل.

أنشأنا في هذا القسم برنامجًا يمكنه تحويل قيم بيانات من النوع {}map[string]interface إلى بيانات جسون. أضفنا بعد ذلك حقلًا يستخدم بيانات زمنية من النوع time.Time ضمن الرابط، وأخيرًا أضفنا حقلين يستخدمان القيمة null.

بالرغم من مرونة استخدام {}map[string]interface لتحويل البيانات إلى بيانات جسون، إلا أنه قد يكون عُرضةً لحدوث أخطاء غير مقصودة، ولا سيما إذا كنا بحاجة إلى إرسال نفس البيانات إلى عدة أماكن. إذا أرسلنا نُسخًا من هذه البيانات إلى أكثر من مكان ضمن الشيفرة، فربما نُغيّر عن طريق الخطأ اسم حقل أو نضع قيمة غير صحيحة ضمن حقل. في هكذا حالات ربما يكون من المفيد استخدام النوع struct لتمثيل البيانات التي نُريد تحويلها إلى جسون.

استخدام البنى Structs لتوليد بيانات بصيغة جسون

تُعَدّ جو لغةً ثابتة الأنواع statically-typed language مثل لغة C وجافا Java و ++C، وهذا يعني أن كل تعليمة في البرنامج تُفحَص في وقت التصريف. تتمثل فائدة ذلك في السماح للمُصرّف باستنتاج نوع المتغيرات والتحقق منها وفرض التناسق بين قيم المتغيرات. تستفيد الحزمة encoding/json من ذلك من خلال تعريف بنية struct تُمثّل بيانات جسون. يمكننا التحكم في كيفية تفسير البيانات التي تتضمنها البنية باستخدام وسوم البنية Struct tags. سنعدّل البرنامج السابق خلال هذا القسم، لاستخدام بنية بدلًا من رابط، لتوليد بيانات جسون.

عند استخدام struct لتعريف بيانات جسون، يجب علينا تصدير أسماء الحقول (وليس اسم النوع struct نفسه) التي نريد تحويلها إلى جسون، وذلك fأن نبدأ أسماء الحقول بحرف كبير (أي بدلًا من كتابة intValue نكتب IntValue) وإلا لن تكون الحزمة encoding/json قادرةً على الوصول إلى هذه الحقول لتحويلها إلى جسون.

الآن بالنسبة لأسماء الحقول، إذا لم نستخدم وسوم البنية للتحكم في تسمية هذه الحقول، ستُفسّر كما هي مباشرةً ضمن البنية. قد يكون استخدام الأسماء الافتراضية هو ما نريده في بيانات جسون، وذلك وفقًا للطريقة التي نرغب بها بتنسيق بياناتنا، وبذلك لن نحتاج في هذه الحالة إلى أية وسوم. يستخدم العديد من المبرمجين تنسيقات أسماء، مثل intValue، أو int_value مع حقول البيانات، وسنحتاج في هذه الحالة إلى وسوم البنية للتحكم في كيفية تفسير هذه الأسماء.

سيكون لدينا في المثال التالي بنية struct مع حقل وحيد اسمه IntValue وسنحول هذه البنية إلى صيغة جسون:

type myInt struct {
    IntValue int
}

data := &myInt{IntValue: 1234}

إذا حوّلنا المتغير data إلى صيغة جسون باستخدام الدالة json.Marshal، سنرى الخرج التالي:

{"IntValue":1234}

لكن لو كنا نريد أن يكون اسم الحقل في ملف جسون هو intValue بدلًا من IntValue، سنحتاج إلى إخبار encoding/json بذلك. بما أن json.Marshal لا تعرف ماذا نتوقع أن يكون اسم بيانات جسون، سنحتاج إلى إخبارها من خلال إضافة وسم البنية json بعد اسم الحقل مباشرةً مع إرفاقه بالاسم الذي نريد أن يظهر به في صيغة جسون. إذًا، من خلال إضافة هذا الوسم إلى الحقل IntValue مع الاسم الذي نريد يظهر به intValue، ستستخدم الدالة json.Marshal الاسم الذي نريده اسمًا للحقل ضمن صيغة جسون:

type myInt struct {
    IntValue int `json:"intValue"`
}

data := &myInt{IntValue: 1234}

إذا حوّلنا المتغير data إلى صيغة جسون باستخدام الدالة json.Marshal، سنرى الخرج التالي، وكما نلاحظ فإنه يستخدم اسم الحقل الذي نريده:

{"intValue":1234}

سنعدل البرنامج الآن لتعريف نوع بيانات struct يُمكن تحويله إلى بيانات جسون. سنضيف بنيةً باسم myJSON لتمثيل البيانات بطريقة يمكن تحويلها إلى جسون ونضيف البنية myObject التي ستكون قيمة للحقل ObjectValue ضمن البنية myJSON. سنضيف أيضًا وسمًا لكل اسم حقل ضمن البنية myJSON لتحديد الاسم الذي نريد أن يظهر به الحقل ضمن بيانات جسون. يجب أيضًا أن نحدّث الإسناد الخاص بالمتغير dataبحيث نسند له بنية myJSON مع التصريح عنه بنفس الطريقة التي نتعامل مع بنى جو الأخرى.

...

type myJSON struct {
    IntValue        int       `json:"intValue"`
    BoolValue       bool      `json:"boolValue"`
    StringValue     string    `json:"stringValue"`
    DateValue       time.Time `json:"dateValue"`
    ObjectValue     *myObject `json:"objectValue"`
    NullStringValue *string   `json:"nullStringValue"`
    NullIntValue    *int      `json:"nullIntValue"`
}

type myObject struct {
    ArrayValue []int `json:"arrayValue"`
}

func main() {
    otherInt := 4321
    data := &myJSON{
        IntValue:    1234,
        BoolValue:   true,
        StringValue: "hello!",
        DateValue:   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
        ObjectValue: &myObject{
            ArrayValue: []int{1, 2, 3, 4},
        },
        NullStringValue: nil,
        NullIntValue:    &otherInt,
    }

    ...
}

تُشبه معظم التغييرات في الشيفرة أعلاه ما فعلناه في المثال السابق مع الحقل IntValue، إلا أن هناك بعض الأشياء تستحق الإشارة إليها. أحد هذه الأشياء هو الحقل ObjectValue الذي يستخدم قيمةً مرجعية myObject* لإخبار دالة json.Marshal -التي تؤدي عملية التنظيم- إلى وجود قيمة مرجعية من النوع myObject أو قيمة nil. بهذه الطريقة نكون قد عرّفنا كائن جسون بأكثر من طبقة، وفي حال كانت هذه الطريقة مطلوبة، سيكون لدينا بنيةً أخرى من نوع struct داخل النوع myObject، وهكذا، وبالتالي نلاحظ أنه بإمكاننا تعريف كائنات جسون أعقد وأعقد باستخدام أنواع struct وفقًا لحاجتنا.

واحد من الأشياء الأخرى التي تستحق الذكر هي الحقلين NullStringValue و NullIntValue، وعلى عكس StringValue و IntValue؛ أنواع هذه القيم هي أنواع مرجعية int* و string*، وقيمها الافتراضية هي قيم صفريّة أي nil وهذا يُقابل القيمة 0 لنوع البيانات int والسلسلة الفارغة '' لنوع البيانات string. يمكننا من الكلام السابق أن نستنتج أنه في حال أردنا التعبير عن قيمة من نوع ما تحتمل أن تكون nil، فيجب أن نجعلها قيمةً مرجعية. مثلًا لو كنا نريد أن نعبر عن قيمة حقل تُمثّل إجابة مُستخدم عن سؤال ما، فهنا قد يُجيب المُستخدم عن السؤال أو قد لا يُجيب (نضع niil).

نُعدّل قيمة الحقل NullIntValue بضبطه على القيمة 4321 لنُظهر كيف يمكن إسناد قيمة لنوع مرجعي مثل int*. تجدر الإشارة إلى أنه في لغة جو، يمكننا إنشاء مراجع لأنواع البيانات الأولية primitive types فقط، مثل int و string باستخدام المتغيرات. إذًا، لإسناد قيمة إلى الحقل NullIntValue، نُسند أولًا قيمةً إلى متغير آخر otherInt، ثم نحصل على مرجع منه otherInt& (بدلًا من كتابة 4321& مباشرةً).

لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

سيكون الخرج على النحو التالي:

json data: {"intValue":1234,"boolValue":true,"stringValue":"hello!","dateValue":"2022-03-02T09:10:00Z","objectValue":{"arrayValue":[1,2,3,4]},"nullStringValue":null,"nullIntValue":4321}

نلاحظ أن هذا الناتج هو نفسه عندما استخدمنا {}map[string]interface، باستثناء أن قيمة nullIntValue هذه المرة هي 4321 لأن هذه هي قيمة otherInt.

يستغرق الأمر في البداية بعض الوقت لتعريف البنية struct وإعداد حقولها، ولكن يمكننا بعد ذلك استخدامها مرارًا وتكرارًا في الشيفرة، وستكون النتيجة هي نفسها بغض النظر عن مكان استخدامها، كما يمكننا تعديلها من مكان واحد بدلًا من تعديلها في كل مكان تتواجد نسخة منها فيه كما في حالة الروابط map.

تتيح لنا الدالة json.Marshal إمكانية تحديد الحقول التي نُريد تضمينها في جسون، في حال كانت قيمة تلك الحقول صفريّة (أي Null وهذا يُكافئ 0 في حالة int و false في حالة bool والسلسلة الفارغة في حالة string ..إلخ). قد يكون لدينا أحيانًا كائن جسون كبير أو حقول اختيارية لا نريد تضمينها دائمًا في بيانات جسون، لذا يكون تجاهل هذه الحقول مفيدًا. يكون التحكم في تجاهل هذه الحقول -عندما تكون قيمها صفريّة أو غير صفريّة- باستخدام الخيار omitempty ضمن وسم بنية json.

لنُحدّث البرنامج السابق لإضافة الخيار omitempty إلى حقل NullStringValue وإضافة حقل جديد يسمى EmptyString مع نفس الخيار:

...

type myJSON struct {
    ...

    NullStringValue *string   `json:"nullStringValue,omitempty"`
    NullIntValue    *int      `json:"nullIntValue"`
    EmptyString     string    `json:"emptyString,omitempty"`
}

...

بعد تحويل البنية myJSON إلى بيانات جسون، سنلاحظ أن الحقل EmptyString والحقل NullStringValue غير موجودان في بيانات جسون، لأن قيمهما صفريّة.

لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

سيكون الخرج على النحو التالي:

json data: {"intValue":1234,"boolValue":true,"stringValue":"hello!","dateValue":"2022-03-02T09:10:00Z","objectValue":{"arrayValue":[1,2,3,4]},"nullIntValue":4321}

نلاحظ أن الحقل nullStringValue لم يعد موجودًا في الخرج، لأنه يُعد حقلًا بقيمة nil، بالتالي فإن الخيار omitempty استبعده من الخرج. نفس الأمر بالنسبة للحقل emptyString، لأن قيمته صفريّة (القيمة الصفريّة تُكافئ nil كما سبق وذكرنا).

استخدمنا في هذا القسم أسلوب البُنى struct بدلًا من الروابط، لتمثيل كائن جسون في برنامجنا، ثم حولناها إلى بيانات جسون باستخدام الدالة json.Marshal. تعلمنا أيضًا كيفية تجاهل القيم الصفريّة من بيانات جسون.

هذه البيانات (بعد تحويلها لصيغة جسون) قد تُرسل إلى برامج أخرى، وبالتالي إذا كان البرنامج الآخر مكتوب بلغة جو، يجب أن نعرف كيف يمكننا قراءة هذا النوع من البيانات. يمكنك أن تتخيل الأمر على أنه برنامجي جو A و B أحدهما مُخدم والآخر عميل. يُرسل A طلبًا إلى B، فيعالجه ويرسله بصيغة جسون إلى A، ثم يقرأ A هذه البيانات. لأجل ذلك توفر الحزمة encoding/json طريقةً لفك ترميز بيانات جسون وتحويلها إلى أنواع جو المقابلة (مجرد عملية عكسية). سنتعلم في القسم التالي كيفية قراءة بيانات جسون وتحويلها إلى روابط.

تحليل بيانات جسون باستخدام الروابط

بطريقة مشابهة لما فعلناه عندما استخدمنا {}map[string]interface مثل طريقة مرنة لتوليد بيانات جسون، يمكننا استخدامها أيضًا مثل طريقة مرنة لقراءة بيانات جسون. تعمل الدالة json.Unmarshal بطريقة معاكسة للدالة json.Marshal، إذ تأخذ بيانات جسون وتحولها إلى بيانات جو، وتأخذ أيضًا متغيرًا لوضع البيانات التي جرى فك تنظيمها فيه، وتعيد إما خطأ error في حال فشل عملية التحليل أو nil في حال نجحت.

سنعدّل برنامجنا في هذا القسم، بحيث نستخدم الدالة json.Unmarshal لقراءة بيانات جسون من سلسلة وتخزينها في متغير من النوع map، وطباعة الخرج على الشاشة. لنعدّل البرنامج إذًا، بحيث نفك تنظيم البيانات باستخدام الدالة السابقة ونحولها إلى رابط {}map[string]interface. لنبدأ باستبدال المتغير data الأصلي بمتغير jsonData يحتوي على سلسلة جسون، ثم نُصرّح عن متغير data جديد على أنه {}map[string]interfac لتلقي بيانات جسون، ثم نستخدم الدالة json.Unmarshal مع هذه المتغيرات للوصول إلى بيانات جسون:

...

func main() {
    jsonData := `
        {
            "intValue":1234,
            "boolValue":true,
            "stringValue":"hello!",
            "dateValue":"2022-03-02T09:10:00Z",
            "objectValue":{
                "arrayValue":[1,2,3,4]
            },
            "nullStringValue":null,
            "nullIntValue":null
        }
    `

    var data map[string]interface{}
    err := json.Unmarshal([]byte(jsonData), &data)
    if err != nil {
        fmt.Printf("could not unmarshal json: %s\n", err)
        return
    }

    fmt.Printf("json map: %v\n", data)
}  

جرى إسناد قيمة المتغير jsonData في الشيفرة أعلاه من خلال سلسلة نصية أولية، وذلك للسماح بكتابة البيانات ضمن أسطر متعددة لتسهيل القراءة. بعد التصريح عن المتغير data على أنه متغير من النوع {}map[string]interface، نمرر jsonData والمتغير data إلى الدالة json.Unmarshal لفك تنظيم بيانات جسون وتخزين النتيجة في data. يُمرَّر المتغير jsonData إلى دالة فك التنظيم على شكل مصفوفة بايت byte[]، لأن الدالة تتطلب النوع byte[]، والمتغير jsonData عُرّف على أنه قيمة من نوع سلسلة نصية string. طبعًا هذا الأمر ينجح، لأنه في لغة جو، يمكن تحويل string إلى byte[] والعكس. بالنسبة للمتغير data، ينبغي تمريره مثل مرجع، لأن الدالة تتطلب معرفة موقع المتغير في الذاكرة. أخيرًا، يجري فك تنظيم البيانات وتخزين النتيجة في المتغير data، لنطبع النتيجة بعدها باستخدام دالة fmt.Printf.

لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

سيكون الخرج على النحو التالي:

json map: map[boolValue:true dateValue:2022-03-02T09:10:00Z intValue:1234 nullIntValue:<nil> nullStringValue:<nil> objectValue:map[arrayValue:[1 2 3 4]] stringValue:hello!]

يُظهر الخرج نتيجة تحويل بيانات جسون إلى رابط. نلاحظ أن جميع الحقول من بيانات جسون موجودة، بما في ذلك القيم الصفريّة null. بما أن بيانات جو الآن مُخزنة في قيمة من النوع {}map[string]interface، سيكون لدينا القليل من العمل مع بياناتها؛ إذ نحتاج إلى الحصول على القيمة من الرابط باستخدام قيمة مفتاح string معينة، وذلك للتأكد من أن القيمة التي تلقيناها هي القيمة التي نتوقعها، لأن القيمة المعادة هي قيمة من النوع {}interface.

لنفتح ملف "main.go" ونُحدّث البرنامج لقراءة حقل dateValue:

...

func main() {
    ...

    fmt.Printf("json map: %v\n", data)

    rawDateValue, ok := data["dateValue"]
    if !ok {
        fmt.Printf("dateValue does not exist\n")
        return
    }
    dateValue, ok := rawDateValue.(string)
    if !ok {
        fmt.Printf("dateValue is not a string\n")
        return
    }
    fmt.Printf("date value: %s\n", dateValue)
}

استخدمنا في الشيفرة أعلاه المفتاح dateValue لاستخراج قيمة من الرابط بكتابة ["data["dateValue، وخزّنا النتيجة في rawDateValue ليكون قيمةً من النوع {}interface، واستخدمنا المتغير ok للتأكد من أن الحقل ذو المفتاح dateValue موجود ضمن الرابط. استخدمنا بعدها توكيد النوع type assertion، للتأكد من أن rawDateValue هو قيمة string، وأسندناه إلى المتغير dateValue. استخدمنا بعدها المتغير ok للتأكد من نجاح عملية التوكيد. أخيرًا، طبعنا dateValue باستخدام دالة الطباعة fmt.Printf.

لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

سيكون الخرج كما يلي:

json map: map[boolValue:true dateValue:2022-03-02T09:10:00Z intValue:1234 nullIntValue:<nil> nullStringValue:<nil> objectValue:map[arrayValue:[1 2 3 4]] stringValue:hello!]
date value: 2022-03-02T09:10:00Z

يمكننا أن نلاحظ في السطر الأخير من الخرج استخراج قيمة الحقل dateValue من الرابط map وتغير نوعها إلى string.

استخدمنا في هذا القسم الدالة json.Unmarshal لفك تنظيم unmarshal بيانات جسون وتحويلها إلى بيانات في برنامج جو بالاستعانة بمتغير من النوع {}map[string]interface. بعد ذلك استخرجنا قيمة الحقل dateValue من الرابط الذي وضعنا فيه بيانات جسون وطبعناها على الشاشة.

الجانب السيء في استخدام النوع {}map[string]interface في عملية فك التنظيم، هو أن مُفسّر اللغة لا يعرف أنواع الحقول التي جرى فك تنظيمها؛ فكل ما يعرفه أنها من النوع {}interface، وبالتالي لا يمكنه أن يفعل شيئًا أكثر من تخمين الأنواع. بالتالي لن يجري فك تنظيم أنواع البيانات المعقدة، مثل time.Time في الحقل dateValue إلى بيانات من النوع time.Time، وإنما تُفسّر على أنها string. تحدث مشكلة مماثلة إذا حاولنا الوصول إلى أي قيمة رقمية number في الرابط بهذه الطريقة، لأن الدالة json.Unmarshal لا تعرف ما إذا كان الرقم من النوع int أو float أو int64 ..إلخ. لذا يكون التخمين الأفضل هو عدّ الرقم من النوع float64 لأنه النوع الأكثر مرونة.

نستنتج مما سبق جانب إيجابي للروابط وهو مرونة استخدامها، وجانب سيئ يتجلى بالمشكلات السابقة. هنا تأتي ميزة استخدام البنى لحل المشكلات السابقة. بطريقة مشابهة لآلية تنظيم البيانات من بنية struct باستخدام الدالة json.Marshal لإنتاج بيانات جسون، يمكن إجراء عملية معاكسة باستخدام json.Unmarshal أيضًا كما سبق وفعلنا مع الروابط. يمكننا باستخدام البنى الاستغناء عن تعقيدات توكيد النوع التي عانينا منها مع الروابط، من خلال تعريف أنواع البيانات في حقول البنية لتحديد أنواع بيانات جسون التي يجري فك تنظيمها. هذا ما سنتحدث عنه في القسم التالي.

تحليل بيانات جسون باستخدام البنى

عند قراءة بيانات جسون، هناك فرصة جيدة لمعرفة أنواع البيانات التي نتلقاها من خلال استخدام البُنى؛ فمن خلال استخدام البُنى، يمكننا منح مُفسّر اللغة تلميحات تُساعده في تحديد شكل ونوع البيانات التي يتوقعها. عرّفنا في المثال السابق البنيتين myJSON و myObject وأضفنا وسوم json لتحديد أسماء الحقول بعد تحويلها إلى جسون. يمكننا الآن استخدام قيم البنية struct نفسها لفك ترميز سلسلة جسون المُستخدمة، وهذا ما قد يكون مفيدًا لتقليل التعليمات البرمجية المكررة في البرنامج عند تنظم أو فك تنظيم بيانات جسون نفسها. فائدة أخرى لاستخدام بنية في فك تنظيم بيانات جسون هي إمكانية إخبار المُفسّر بنوع بيانات كل حقل، وهناك فائدة أخرى تأتي من استخدام مُفسّر اللغة للتحقق من استخدام الأسماء الصحيحة للحقول، وبالتالي تجنب أخطاء قد تحدث في أسماء الحقول (من النوع string) عند استخدام الروابط.

لنفتح ملف "main.go"، ونعدّل تصريح المتغير data لاستخدام مرجع للبنية myJSON ونضيف بعض تعليمات الطباعة fmt.Printf لإظهار بيانات الحقول المختلفة في myJSON:

...

func main() {
    ...

    var data *myJSON
    err := json.Unmarshal([]byte(jsonData), &data)
    if err != nil {
        fmt.Printf("could not unmarshal json: %s\n", err)
        return
    }

    fmt.Printf("json struct: %#v\n", data)
    fmt.Printf("dateValue: %#v\n", data.DateValue)
    fmt.Printf("objectValue: %#v\n", data.ObjectValue)
}

نظرًا لأننا عرّفنا سابقًا أنواع البُنى، فلن نحتاج إلا إلى تحديث نوع الحقل data لدعم عملية فك التنظيم في بنية. تُظهر بقية التحديثات بعض البيانات الموجودة في البنية نفسها. لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

ويظهر لدينا الخرج التالي:

json struct: &main.myJSON{IntValue:1234, BoolValue:true, StringValue:"hello!", DateValue:time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC), ObjectValue:(*main.myObject)(0x1400011c180), NullStringValue:(*string)(nil), NullIntValue:(*int)(nil), EmptyString:""}
dateValue: time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC)
objectValue: &main.myObject{ArrayValue:[]int{1, 2, 3, 4}}

هناك شيئان يجب أن نشير لهما في الخرج السابق، إذ نلاحظ أولًا في سطر json struct وسطر dateValue، أن قيمة التاريخ والوقت من بيانات جسون جرى تحويلها إلى قيمة من النوع time.Time (يظهر النوع time.Date عند استخدام العنصر النائب v#%). بما أن مُفسّر جو كان قادرًا على التعرّف على النوع time.Time في حقل DateValue، فهو قادر أيضًا على تحليل قيم النوع string.

الشيء الثاني الذي نلاحظه هو أن EmptyString يظهر على سطر json struct على الرغم من أنه لم يُضمّن في بيانات جسون الأصلية. إذا جرى تضمين حقل في بنية مُستخدمة في عملية فك تنظيم بيانات جسون، وكان هذا الحقل غير موجود في بيانات جسون الأصلية، فإنه هذا الحقل يُضبط بالقيمة الافتراضية لنوعه ويجري تجاهله. يمكننا بهذه الطريقة تعريف جميع الحقول المحتملة التي قد تحتوي عليها بيانات جسون بأمان، دون القلق بشأن حدوث خطأ إذا لم يكن الحقل موجودًا في أي من جانبي العملية.

ضُبط كل من الحقلين NullStringValue و NullIntValue على قيمتهما الافتراضية nil، لأن بيانات جسون تقول أن قيمهما null، لكن حتى لو لم يكونا ضمن بيانات جسون، سيأخذان نفس القيمة. على غرار الطريقة التي تجاهلت بها الدالة json.Unmarshal حقل EmptyString في البنية struct عندما كان حقل emptyString مفقودًا من بيانات جسون، فإن العكس هو الصحيح أيضًا؛ فإذا كان هناك حقل في بيانات جسون ليس له ما يقابله في البنية struct، سيتجاهل مفسّر اللغة هذا الحقل، وينتقل إلى الحقل التالي لتحليله. بالتالي إذا كانت بيانات جسون التي نقرأها كبيرة جدًا وكان البرنامج يحتاج عددًا صغيرًا من تلك الحقول، يمكننا إنشاء بنية تتضمن الحقول التي نحتاجها فقط، إذ يتجاهل مُفسّر اللغة أية حقول من بيانات جسون غير موجودة في البنية.

لنفتح ملف "main.go" ونعدّل jsonData لتضمين حقل غير موجود في myJSON:

...

func main() {
    jsonData := `
        {
            "intValue":1234,
            "boolValue":true,
            "stringValue":"hello!",
            "dateValue":"2022-03-02T09:10:00Z",
            "objectValue":{
                "arrayValue":[1,2,3,4]
            },
            "nullStringValue":null,
            "nullIntValue":null,
            "extraValue":4321
        }
    `

    ...
}

لنُشغّل ملف البرنامج "main.go" من خلال الأمر go run:

$ go run main.go

سيكون الخرج على النحو التالي:

json struct: &main.myJSON{IntValue:1234, BoolValue:true, StringValue:"hello!", DateValue:time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC), ObjectValue:(*main.myObject)(0x14000126180), NullStringValue:(*string)(nil), NullIntValue:(*int)(nil), EmptyString:""}
dateValue: time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC)
objectValue: &main.myObject{ArrayValue:[]int{1, 2, 3, 4}}

نلاحظ عدم ظهور الحقل الجديد extraValue الذي أضفناه إلى بيانات جسون ضمن الخرج، إذ تجاهله مُفسّر اللغة، لأنّه غير موجود ضمن البنية myJSON.

استخدمنا في هذا المقال أنواع البُنى struct المُعرّفة مسبقًا في عملية فك تنظيم بيانات جسون. رأينا كيف أن ذلك يُمكّن مُفسّر اللغة من تحليل قيم الأنواع المعقدة مثل time.Time، ويسمح بتجاهل الحقل EmptyString الموجود ضمن البنية وغير موجود ضمن بيانات جسون. رأينا أيضًا كيف يمكن التحكم في الحقول التي نستخرجها من بيانات جسون وتحديد ما نريده من حقول بدقة.

الخاتمة

أنشأنا في هذا المقال برنامجًا يستخدم الحزمة encoding/json من مكتبة لغة جو القياسية. استخدمنا بدايةً الدالة json.Marshal مع النوع {}map[string]interface لإنشاء بيانات جسون بطريقة مرنة. عدّلنا بعد ذلك البرنامج لاستخدام النوع struct مع وسوم json لإنشاء بيانات جسون بطريقة متسقة وموثوقة باستخدام الدالة json.Marshal. استخدمنا بعد ذلك الدالة json.Unmarshal مع النوع {}map[string]interface لفك ترميز سلسلة جسون وتحويلها إلى بيانات يمكن التعامل معها في برنامج جو. أخيرًا، استخدمنا نوع بنية struct مُعرّف مسبقًا في عملية فك تنظيم بيانات جسون باستخدام دالة json.Unmarshal للسماح لمُفسّر اللغة بإجراء التحليل واستنتاج أنواع البيانات وفقًا لحقول البنية التي عرّفناها.

يمكننا من خلال الحزمة encoding/json التفاعل مع العديد من واجهات برمجة التطبيقات APIs المتاحة على الإنترنت لإنشاء عمليات متكاملة مع مواقع الويب الأخرى. يمكننا أيضًا تحويل بيانات جو في برامجنا إلى تنسيق يمكن حفظه ثم تحميله لاحقًا للمتابعة من حيث توقف البرنامج (لأن عملية السلسلة Serialization تحفظ البيانات قيد التشغيل في الذاكرة). تتضمن الحزمة encoding/json أيضًا دوالًا أخرى مُفيدة للتعامل مع بيانات جسون، مثل الدالة json.MarshalIndent التي تساعدنا في عرض بيانات جسون بطريقة مُرتبة وأكثر وضوحًا للاطلاع عليها ومساعدتنا في استكشاف الأخطاء وإصلاحها.

ترجمة -وبتصرف- للمقال How To Use JSON in Go لصاحبه Kristin Davidson.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...