כיצד להשתמש בתגי Struct ב־Go

הקדמה

מבנים, או structs, משמשים לאיסוף מספר רב של פרטים ביחד ביחידה אחת. אוספים אלו של מידע משמשים לתיאור מושגים ברמה גבוהה יותר, כמו כתובת המורכבת מ־רחוב, עיר, מדינה, ו־מיקוד. כאשר אתה קורא את המידע הזה ממערכות כמו מסדי נתונים או APIs, אתה יכול להשתמש ב־תגי structs כדי לשלוט באיזו דרך המידע הזה מוקצה לשדות של struct. תגי structs הם חתיכות קטנות של מטא־נתונים מצורפות לשדות של struct שמספקות הוראות לקוד Go אחר שעובד עם ה־struct.

איך נראה תג Struct?

תגי struct של Go הם הערות שמופיעות אחרי הסוג בהצהרת struct של Go. כל תג מורכב ממחרוזות קצרות המשויכות לערך תואם מסוים.

A struct tag looks like this, with the tag offset with backtick ` characters:

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

קוד Go אחר מסוגל לבדוק את ה־structs האלו ולחלץ את הערכים שהוקצו למפתחות מסוימים שהוא מבקש. תגי structs אין להם השפעה על פעולת הקוד שלך ללא קוד נוסף שבודק אותם.

נסה את הדוגמה הזו כדי לראות איך תגי struct נראים, ושללא קוד מחבילה אחרת, הם לא יהיו לו כל השפעה.

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)
}

זה יפלט:

Output
Hi! My name is Sammy

דוגמה זו מגדירה סוג User עם שדה של Name. לשדה Name ניתנה תגית struct של example:"name". אנו נפנה לתג המסוים הזה בשיח כ "תג struct דוגמה" מכיוון שהוא משתמש במילה "דוגמה" כמפתח שלו. לתג ה- example יש את הערך "name" עבור השדה Name. בסוג User, אנו גם מגדירים את השיטה String() הנדרשת על ידי ממשק ה- fmt.Stringer. זה ייקרא אוטומטית כאשר אנו מעבירים את הסוג ל- fmt.Println ונותן לנו הזדמנות לייצר גרסה מעוצבת של ה- struct שלנו.

בתוך גוף הפונקציה main, אנו יוצרים מופע חדש של הסוג שלנו User ומעבירים אותו ל- fmt.Println. גם אם הסטרקט כילה תג struct, אנו רואים שהוא אין לו שום השפעה על פעולת קוד Go זו. הוא יתנהג בדיוק באותו אופן אם התג struct לא היה נוכח.

כדי להשתמש בתגי struct כדי להשיג משהו, יש לכתוב קוד Go אחר המבדוק את struct בזמן ריצה. בספריית התקנים יש חבילות שמשתמשות בתגי struct כחלק מפעולתן. הפופולרית ביותר מביניהן היא חבילת encoding/json.

קידוד JSON

מסמך העברת מידע (JSON) הוא פורמט טקסטואלי לקידוד אוספים של נתונים שמאורגנים במפתחות מחרוזת שונים. בדרך כלל משמש לתקשורת של נתונים בין תוכניות שונות מאחר והפורמט הוא פשוט מספיק כך שספריות קיימות לקידוד שלו בשפות תכנות שונות. הנה דוגמה ל-JSON:

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

אובייקט ה-JSON הזה מכיל שני מפתחות, language ו-mascot. לאחר מכן מופיעים הערכים המשויכים. כאן המפתח language מקבל ערך של Go והמפתח mascot מקבל את הערך Gopher.

מקודד ה-JSON בספריית הסטנדרט משתמש בתגיות מבנה כהערות שמציינות למקודד כיצד ברצונך שיהיה שמך לשדות בפלט ה-JSON. המנגנון הזה של הקידוד והפיענוח של JSON ניתן למציאה בחבילת encoding/json של.

נסה דוגמה זו כדי לראות כיצד נעשה קידוד של 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))
}

הפלט יראה כך:

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

הגדרנו מבנה המתאר משתמש עם שדות כוללים את שמו, הסיסמה שלו, והזמן שבו נוצר המשתמש. בתוך פונקציית main, אנו יוצרים מופע של משתמש זה על ידי ספק ערכים עבור כל השדות חוץ מ־PreferredFish (סמי אוהב את כל הדגים). לאחר מכן, אנו מעבירים את מופע ה־User לפונקציית json.MarshalIndent. זה משמש כדי שנוכל לראות את הפלט של JSON בצורה יותר נוחה מבלי להשתמש בכלי עיצוב חיצוני. קריאה זו יכולה להיות מוחלפת ב־json.Marshal(u) כדי להדפיס JSON בלי שום רווחים נוספים. שני הארגומנטים הנוספים ל־json.MarshalIndent שולטים על תחילת הפלט (שלא ציינו כאן באמצעות מחרוזת ריקה) והתווים לשימוש בכניסה, שכאן הם שני תווים רווח. כל שגיאות שיוצרות מ־json.MarshalIndent מוזנות ליומן והתוכנית נסגרת באמצעות os.Exit(1). לבסוף, אנו ממירים את ה־[]byte שחזר מ־json.MarshalIndent למחרוזת ומעבירים את המחרוזת התוצאה ל־fmt.Println להדפסה בטרמינל.

שדות המבנה מופיעים בדיוק כמו שקרויים. זה אינו הסגנון התקני של JSON שאתה עשוי לצפות בו, אך, שימוש בכתיבת גומרים לשמות השדות. כפי שתראה בעת הפעלת הדוגמה הזו, זה לא יעבוד מכיוון ששמות השדות הרצויים נקלעים לחוקי Go אודות שמות שדות מיוצאים.

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))
}

זה יציג את הפלט הבא:

Output
{}

בגרסה זו, שינינו את שמות השדות לקייס קמל. כעת Name הוא name, Password הוא password, ולבסוף CreatedAt הוא createdAt. בתוך גוף ה־main, שינינו את היצירה של המבנה שלנו כך שישמשו את השמות החדשים הללו. לאחר מכן, אנו מעבירים את המבנה לפונקציית json.MarshalIndent כפי שעשינו בעבר. הפלט, הפעם, הוא אובייקט JSON ריק, {}.

הקיום המדויק של קייס קמל דורש שהתו הראשון יהיה באות קטנה. בעוד ש־JSON לא משנה איך אתה מכנה את השדות שלך, Go כן, מכיוון שזה מציין את הנראות של השדה מחוץ לחבילה. מאחר שחבילת encoding/json היא חבילה נפרדת מהחבילה הראשית main שאנו משתמשים בה, עלינו לעשות את התו הראשון באות ריקעת כדי להפוך אותו לגלוי עבור encoding/json. נראה שאנו במבוי סתום. אנו זקוקים לאמץ להעביר למקודד JSON איזה שם אנו רוצים שהשדה יקרא.

שימוש בתגיות מבנה כדי לשלוט בקידוד

אתה יכול לשנות את הדוגמה הקודמת כך שיהיו לשדות שהוצאו מעוצבים עם שמות שדות בצורה נכונה עם תוויות מבנה על כל שדה עם תווית מבנה. התווית מבנה שencoding/json מזהה כוללת מפתח של 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))
}

זה יוציא:

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

שינינו את שדות המבנה כך שיהיו גלויים לחבילות אחרות על ידי כתיבת האותיות הראשונות של השמות שלהם. אך הפעם הוספנו תגי מבנה בצורת json:"name", כאשר "name" היה השם שרצינו שjson.MarshalIndent ישתמש בו בעת הדפסת המבנה שלנו כ-JSON.

עכשיו הצלחנו לעצב את JSON שלנו כראוי. שים לב, אך, שהשדות לערכים מסוימים הודפסו גם אם לא הגדרנו את הערכים הללו. מקודד ה-JSON יכול להסיר גם שדות אלו, אם תרצה.

הסרת שדות JSON ריקים

נפוץ להסיר את השדות שאינם מוגדרים ב־JSON. מאחר וכל סוג בִּשְׂפַת Go מכיל "ערך ריק" – ערך ברירת מחדל שהוא מוגדר אליו, החבילה encoding/json דורשת מידע נוסף כדי להבדיל בין ערך ריק לבין שדה שאינו מוגדר. בתוך הערך של התגית ה־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))
}

דוגמה זו תפליט:

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

עשינו שינוי בדוגמאות הקודמות כך שלשדה של PreferredFish יש כעת את התגית הבאה: json:"preferredFish,omitempty". קיום ההוספה ,omitempty גורם ל־JSON encoder לדלג על השדה הזה, מאחר שהחלטנו להשאיר אותו לא מוגדר. בדוגמאות הקודמות שלנו, הערך של השדה הזה היה null.

הפלט נראה הרבה יותר טוב, אך אנו עדיין מדפיסים את הסיסמה של המשתמש. החבילה encoding/json מספקת דרך נוספת להתעלם משדות פרטיים לחלוטין.

התעלמות משדות פרטיים

כמה תחומים חייבים להיות מיוצאים ממבני נתונים כדי שחבילות אחרות תוכל להתממשק כראוי עם הסוג. אף על פי שטבעם של השדות הללו עשוי להיות רגיש, כך שבמקרים אלה, נרצה שמקודד ה־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))
}

כאשר אתה מפעיל דוגמה זו, תראה את פלט זה:

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

הדבר היחיד ששינינו בדוגמה זו מהדוגמאות הקודמות הוא ששדה הסיסמה משתמש כעת בערך מיוחד "-" עבור תג המבנה שלו json:. בפלט מדוגמה זו אינו נמצא עוד שדה password.

תכונות אלו של חבילת encoding/json,omitempty, "-", ו־אפשרויות אחרות — אינן סטנדרטיות. מה שחבילה מחליטה לעשות עם ערכים של תג מבנה תלוי ביישום שלה. מאחר וחבילת encoding/json היא חלק מספריית הסטנדרטית, חבילות אחרות גם הן מיישמות את התכונות הללו באותה הדרך כדי שיהיה מוגדר כמקובל. עם זאת, חשוב לקרוא את התיעוד של כל חבילה של צד שלישי שמשתמשת בתגי מבנה כדי ללמוד מה נתמך ומה לא.

מסקנה

תגי מבנה מציעים אמצעי חזק להעשרת פונקציונליות של קוד העובד עם המבנים שלך. חבילות רבות של הספרייה הסטנדרטית וגורמים צד שלישי מציעות דרכים להתאים אישית את פעולתן באמצעות שימוש בתגי מבנה. השימוש היעיל בהם בקוד שלך מספק הן את התנהגות ההתאמה האישית הזו וגם מתעד בצורה תמציתית איך שדות אלו נעשים בשימוש למפתחים עתידיים.

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-struct-tags-in-go