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

استخدام وسوم البنية Struct Tags في لغة جو Go


هدى جبور

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

كيف يبدو شكل وسم البنية؟

وسم البنية في لغة جو هو "توضيح" يُكتب بعد نوع الحقل داخل البنية، ويتكون كل وسم من زوج "key:"value أي مفتاح مرتبط بقيمة مقابلة ويوضع ضمن علامتين اقتباس مائلة (`) كما يلي:

type User struct {
    Name string `example:"name"`
}

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

package main

import "fmt"

type User struct {
    Name string `example:"name"`
}

func (u *User) String() string {
    return fmt.Sprintf("Hi! My name is %s", u.Name)
}

func main() {
    u := &User{
        Name: "Sammy",
    }

    fmt.Println(u)
}

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

Hi! My name is Sammy

يعرّف هذا المثال نوع بيانات باسم User يمتلك حقلًا اسمه Name، وأعطينا هذا الحقل وسم بنية "example:"name مُتمثّل بالمفتاح example والقيمة "name" للحقل Name. عرّفنا التابع ()String في البنية User، والذي تحتاجه الواجهة fmt.Stringer، وبالتالي سيُستدعى تلقائيًا عندما نُمرّر هذا النوع إلى fmt.Println وبالتالي نحصل على طباعة مُرتبة للبنية.

نأخذ في الدالة main متغيرًا من النوع User ونمرّره إلى دالة الطباعة fmt.Println. على الرغم من أنه لدينا وسم بنية، إلا أننا لن نلحظ أي فرق في الخرج، أي كما لو أنها غير موجودة. لكي نحصل على تأثير وسم البنية، يجب كتابة شيفرة أخرى تفحص البنية في وقت التشغيل runtime. تحتوي المكتبة القياسية على حزم تستخدم وسوم البنية كأنها جزء من عملها، وأكثرها شيوعًا هي حزمة encoding/json.

ترميز JSON

جسون JSON هي اختصار إلى "ترميز كائن باستخدام جافا سكريبت JavaScript Object Notation"، وهي تنسيق نصي لترميز مجموعات البيانات المنظمة وفق أسماء مفاتيح مختلفة. يُشاع استخدام جسون لربط البيانات بين البرامج المختلفة، إذ أن التنسيق بسيط ويوجد مكتبات جاهزة لفك ترميزه في العديد من اللغات البرمجية. فيما يلي مثال على جسون:

{
  "language": "Go",
  "mascot": "Gopher"
}

يتضمن كائن جسون أعلاه مفتاحين؛ الأول language والثاني mascot، ولكل منهما قيمة مرتبطة به هما Go و Gopher.

يستخدم مُرمّز encoder جسون في المكتبة القياسية وسوم البنية مثل "توصيفات annotations" تشير إلى الكيفية التي تريد بها تسمية الحقول الخاصة بك في خرج جسون. يمكن العثور على آليات ترميز وفك ترميز جسون في حزمة encoding/json من هنا.

جرب هذا المثال لترى كيف يكون ترميز جسون دون وسوم البنية:

package main

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

type User struct {
    Name          string
    Password      string
    PreferredFish []string
    CreatedAt     time.Time
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

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

{
  "Name": "Sammy the Shark",
  "Password": "fisharegreat",
  "CreatedAt": "2019-09-23T15:50:01.203059-04:00"
}

عرّفنا بنية تمثّل مُستخدم مع حقول تُدل على اسمه Name وكلمة المرور Password وتاريخ إنشاء الحساب CreatedAt. أخذنا داخل الدالة main متغيرًا من هذه البنية وأعطينا قيمًا لجميع حقوله عدا PreferredFish (يحب Sammy جميع الأسماك). بعد ذلك، مرّرنا البنية إلى الدالة json.MarshalIndent، إذ تمكننا هذه الدالة من رؤية خرج جسون بسهولة ودون استخدام أي أداة خارجية. يمكن استبدال الاستدعاء السابق لهذه الدالة بالاستدعاء (json.Marshal(u وذلك لطباعة جسون بدون أي فراغات إضافية، إذ يتحكم الوسيطان الإضافيان للدالة json.MarshalIndent في بادئة الخرج، التي أهملناها مع السلسلة الفارغة، وبالمحارف المُراد استخدامها من أجل تمثيل المسافة البادئة (هنا فراغين).

سُجّلت الأخطاء الناتجة عن json.MarshalIndent وأُنهي البرنامج باستخدام (os.Exit(1، وأخيرًا حوّلنا المصفوفة byte[] المُعادة من json.MarshalIndent إلى سلسلة ومررنا السلسلة الناتجة إلى fmt.Println للطباعة على الطرفية.

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

package main

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

type User struct {
    name          string
    password      string
    preferredFish []string
    createdAt     time.Time
}

func main() {
    u := &User{
        name:      "Sammy the Shark",
        password:  "fisharegreat",
        createdAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

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

{}

استبدلنا هذه المرة أسماء الحقول، بحيث تتوافق مع تنسيق سنام الجمل؛ فبدلًا من Name وضعنا name وبدلًا من Password وضعنا password وبدلًا من CreatedAt وضعنا createdAt، وعدّلنا داخل متن الدالة main أسماء الحقول أيضًا بحيث تتوافق مع الأسماء الجديدة، ومرّرنا البنية إلى الدالة json.MarshalIndent كما في السابق. الخرج كان عبارة عن كائن جسون فارغ {}.

يفرض نمط سنام الجمل أن يكون أول حرف صغير دومًا، بينما لا تهتم جسون بذلك، ولغة جو صارمة مع حالة اﻷحرف؛ إذ تدل البداية بحرف كبير على أن الحقل غير مُصدّر وتدل البداية بحرف صغير على أنّه مُصدّر، وبما أن الحزمة encoding/json هي حزمة منفصلة عن حزمة main التي نستخدمها، يجب علينا كتابة الحرف الأول بأحرف كبيرة لجعله مرئيًا للحزمة encoding/json. حسنًا، يبدو أننا في طريق مسدود، فنحن بحاجة إلى طريقة ما لننقل إلى ترميز جسون ما نود تسمية هذا الحقل به.

استخدام وسوم البنية للتحكم بالترميز

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

إذًا، من خلال استخدام تنسيق سنام الجمل لقيم المفاتيح، سيستخدم المرمّز هذه القيم مثل أسماء للحقول مع الإبقاء على الحقول مُصدّرة، وبالتالي نكون أنهينا المشاكل السابقة:

package main

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

type User struct {
    Name          string    `json:"name"`
    Password      string    `json:"password"`
    PreferredFish []string  `json:"preferredFish"`
    CreatedAt     time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

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

{
  "name": "Sammy the Shark",
  "password": "fisharegreat",
  "preferredFish": null,
  "createdAt": "2019-09-23T18:16:17.57739-04:00"
}

لاحظ أننا تركنا أسماء الحقول تبدأ بأحرف كبيرة من أجل السماح بعملية التصدير، ولحل مشكلة تنسيق سنام الجمل استخدمنا وسوم بنية من الشكل "json:"name، إذ أن "name" هي القيمة التي نريد من json.MarshalIndent أن تعرضها عند طباعة كائن جسون.

لاحظ أن الحقل PreferredFish لم نُعطه أي قيمة، وبالتالي قد لا نرغب بظهوره عند طباعة كائن جسون. فيما يلي سنتحدث حول هذا الموضوع.

حذف حقول جسون الفارغة

يُعد حذف حقول الخرج التي ليس لها قيمة في جسون أمرًا شائعًا، وبما أن جميع الأنواع في جو لها "قيمة صفرية" أو قيمة افتراضية مُهيّأة بها، تحتاج حزمة encoding/json إلى معلومات إضافية لتتمكن من معرفة أن بعض الحقول ينبغي عدّها غير مضبوطة، أي قيمتها صفرية في جو. هذه المعلومات هي في الحقيقة مجرد كلمة واحدة نضيفها إلى نهاية القيمة المرتبطة بمفتاح الحقل في وسم البنية؛ وهذه الكلمة هي omitempty,، إذ نُخبر جسون من خلال إضافة هذه الكلمة إلى الحقل بأننا لا نريد ظهوره عندما تكون قيمته صفرية. يوضح المثال التالي الأمر:

package main

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

type User struct {
    Name          string    `json:"name"`
    Password      string    `json:"password"`
    PreferredFish []string  `json:"preferredFish,omitempty"`
    CreatedAt     time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

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

{
  "name": "Sammy the Shark",
  "password": "fisharegreat",
  "createdAt": "2019-09-23T18:21:53.863846-04:00"
}

عدّلنا الأمثلة السابقة بحيث أصبح حقل PreferredFish يحتوي على وسم البنية "json:"preferredFish,omitempty، إذ سيمنع وجود الكلمة omitempty, ظهور هذا الحقل في خرج كائن جسون.

أصبحت الآن الأمور أفضل، لكن هناك مشكلة أخرى واضحة، وهي ظهور كلمة المرور، ولحل المشكلة تؤمن encoding/json طريقةً لتجاهل الحقول الخاصة تمامًا.

منع عرض الحقول الخاصة في خرج كائنات جسون

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

يعمل هذا المثال على إصلاح مشكلة عرض كلمة مرور المستخدم.

package main

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

type User struct {
    Name      string    `json:"name"`
    Password  string    `json:"-"`
    CreatedAt time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

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

{
  "name": "Sammy the Shark",
  "createdAt": "2019-09-23T16:08:21.124481-04:00"
}

الشيء الوحيد الذي تغير في هذا المثال عن المثال السابق هو أن حقل كلمة المرور يستخدم الآن القيمة الخاصة "-" لوسم البنية :json. يمكنك أن تلاحظ من الخرج السابق اختفاء كلمة المرور من الخرج.

ميزتا التجاهل والإخفاء، أو حتى باقي الخيارات في حزمة encoding/json التي استخدمناها مع حقل PreferredFish و Password، ليستا قياسيتين، أي ليست كل الحزم تستخدم نفس الميزات ونفس بنية القواعد، لكن حزمة encoding/json هي حزمة مُضمّنة عمومًا في المكتبة القياسية، وبالتالي سيكون لدى الحزم الأخرى نفس الميزات ونفس العرض convention. مع ذلك، من المهم قراءة التوثيق الخاص بأي حزمة تابعة لجهة خارجية تستخدم وسوم البنية لمعرفة ما هو مدعوم وما هو غير مدعوم.

خاتمة

توفر وسوم البنية وسيلةً قويةً لتسهيل التعامل مع دوال الشيفرات التي تعمل مع البنى، كما توفر العديد من الحزم القياسية والخارجية طرقًا لتخصيص عملياتها من خلال استخدام هذه الوسوم. يوفر استخدام الوسوم بفعالية في التعليمات البرمجية الخاصة بك سلوك تخصيص ممتاز ويوثّق بإيجاز كيفية استخدام هذه الحقول للمطوّرين المستقبليين.

ترجمة -وبتصرف- للمقال How To Use Struct Tags in Go لصاحبه Gopher Guides.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...