إحدى الخصائص المهمة في البرامج الحديثة هو إمكانية التواصل مع البرامج الأخرى، سواءٌ كان برنامج جو يتحقق ما إذا كان لدى المستخدم حق الوصول إلى برنامج آخر، أو برنامج جافا سكريبت JavaScript يحصل على قائمة بالطلبات السابقة لعرضها على موقع ويب، أو برنامج رست Rust يقرأ نتائج اختبار من ملف، فهناك حاجة إلى طريقة نزوّد البرامج من خلالها بالبيانات. لدى أغلب لغات البرمجة طريقة خاصة في تخزين البيانات داخليًّا، والتي لا تفهمها اللغات البرمجية الأخرى. للسماح لهذه اللغات بالتفاعل مع بعضها، يجب تحويل البيانات إلى تنسيق أو صيغة مشتركة يمكنهم فهمها جميعًا. إحدى هذه الصيغ هي صيغة جسون JSON، إنها وسيلة شائعة لنقل البيانات عبر الإنترنت وكذلك بين البرامج في نفس النظام. تمتلك لغة جو والعديد من لغات البرمجة الأخرى طريقة لتحويل البيانات من وإلى صيغة جسون في مكتباتها القياسية.
سنُنشئ في هذا المقال برنامجًا يستخدم حزمة encoding/json
لتحويل صيغة البيانات من النوع map
إلى بيانات جسون، ثم من النوع struct
إلى جسون، كما سنتعلم كيفية إجراء تحويل عكسي.
المتطلبات
- إصدار مُثبّت من جو 1.16 أو أعلى، ويمكنك الاستعانة بمقال تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu لإعداده.
- تثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS.
- تثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز.
- (اختياري) معرفة بكيفية التعامل مع التاريخ والوقت في لغة جو. يمكنك الاطلاع على مقالة استخدام التاريخ والوقت في لغة جو Go.
- معرفة مسبقة بصيغةجسون.
- معرفة مسبقة بكيفية التعامل مع وسوم البنية Struct tags لتخصيص حقول البنية. يمكنك الاطلاع على مقال استخدام وسوم البنية Struct Tags في لغة جو.
استخدام الروابط 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.