تجمع الكلمة المفتاحية struct حقولًا مُسمّاة في نوع واحد. والتصريح type Order struct { ... } هو الطريقة التي تصف بها في Go طلبًا أو مستخدمًا أو جسم طلب HTTP أو ملف إعدادات. ليس للـ structs دوال إنشاء (constructors) ولا وراثة. تضيف السلوك بالدوال التابعة (methods)، وتعيد استخدام الكود بتضمين struct داخل أخرى، وتتحكم في الترميز بوسوم struct (struct tags) (مواصفات Go).
الخلاصة
- الصيغة
type Order struct { ... }تصرّح عن نوع struct. لكل حقل اسم ونوع، ويمكن للحقول التي تشترك في النوع نفسه أن تُكتب في سطر واحد. - في القيمة الصفرية للـ struct يأخذ كل حقل قيمته الصفرية الخاصة، لذا يصبح
var o Orderجاهزًا للاستخدام دون أي تهيئة. - اكتب القيم المركّبة الحرفية (composite literals) بأسماء الحقول، كما في
Order{ID: 1001, Total: 4999}. ويُنبّهgo vetعلى القيم الحرفية التي لا تذكر أسماء الحقول حين يكون نوعها من حزمة أخرى. - الحقول المُصدَّرة (التي تبدأ بحرف كبير) وحدها مرئية لـ
encoding/jsonوللحزم الأخرى. أما الحقل الذي يبدأ بحرف صغير فيُتجاهَل دون أي خطأ. - تتحكم وسوم struct مثل
json:"email,omitempty"في الترميز. استخدمomitzero(Go 1.24) معtime.Timeوغيره من الـ structs، لأنomitemptyلا يحذفها أبدًا. - الدوال التابعة ذات المستقبِل بالمؤشر (pointer receiver) يمكنها تعديل الـ struct. أما ذات المستقبِل بالقيمة (value receiver) فتعمل على نسخة.
- تضمين نوع ما يُرقّي (promote) حقوله ودواله التابعة إلى الـ struct الخارجية. وبهذا تعيد Go استخدام الكود دون وراثة.
- لا يمكن مقارنة الـ structs باستخدام
==إلا إذا كانت كل حقولها قابلة للمقارنة. وإسناد struct ينسخها، لكن الـ slices والـ maps بداخلها تبقى مشتركة.
كيف تصرّح عن struct في Go؟
اكتب type ثم الاسم ثم struct ثم الحقول بين قوسين معقوفين. كل حقل هو اسم يليه نوع:
example.gogopackage main import ( "fmt" "time" ) type Order struct { ID int64 CustomerID int64 Status string Total int64 // cents CreatedAt time.Time } type Address struct { Street, City, Country string } func main() { var o Order fmt.Printf("%+v\n", o) fmt.Println(o.Status == "", o.CreatedAt.IsZero()) }
example.texttext{ID:0 CustomerID:0 Status: Total:0 CreatedAt:0001-01-01 00:00:00 +0000 UTC} true true
يوضّح Address الصيغة المختصرة. فالحقول التي لها النوع نفسه يمكن أن تشترك في سطر واحد، مفصولة بفواصل. ومع ذلك تضع معظم قواعد الكود حقلًا واحدًا في كل سطر، لأن هذا يُبقي الفروقات (diffs) صغيرة ويترك مكانًا لوسم أو تعليق.
لا يحتاج var o Order إلى دالة إنشاء. فكل حقل يبدأ بقيمته الصفرية: 0 للأرقام، و "" للسلاسل النصية، و nil للمؤشرات والـ slices والـ maps، وقيمة time.Time الصفرية لـ CreatedAt. ويشرح مقال الكلمة المفتاحية var في Go القيم الصفرية بالتفصيل.
وكأي نوع آخر، يمكن التصريح عن struct داخل دالة حين لا يحتاجها غير تلك الدالة. ويعرض مقال الكلمة المفتاحية type في Go: الأنواع المُعرَّفة والأسماء البديلة والدوال التابعة struct محلية باسم loginRequest تُستخدم لفك ترميز جسم JSON.
كيف تنشئ قيمة struct؟
ستستخدم في الغالب القيمة الصفرية من var، أو قيمة مركّبة حرفية بأسماء الحقول، أو مؤشرًا إلى قيمة حرفية باستخدام &:
example.gogokeyed := Order{ID: 1001, CustomerID: 42, Status: "paid", Total: 4999} partial := Order{ID: 1002, Status: "pending"} positional := Order{1003, 42, "paid", 1250} p := &Order{ID: 1004} p.Status = "shipped"
example.texttext{ID:1001 CustomerID:42 Status:paid Total:4999} {ID:1002 CustomerID:0 Status:pending Total:0} {ID:1003 CustomerID:42 Status:paid Total:1250} {ID:1004 CustomerID:0 Status:shipped Total:0}
القيمة الحرفية ذات الأسماء يمكن أن تذكر أي مجموعة من الحقول بأي ترتيب، وتأخذ بقية الحقول قيمتها الصفرية. أما القيمة الحرفية الموضعية (positional) فيجب أن تذكر كل الحقول بترتيب التصريح عنها، وهذا ما يجعلها هشّة. فإذا أضفت حقل Currency إلى Order توقفت كل قيمة حرفية موضعية عن الترجمة، بينما تستمر القيم ذات الأسماء في العمل.
ومع أنواع struct القادمة من حزم أخرى، يُبلغ go vet عن القيم الحرفية الموضعية عبر المحلّل composites:
example.gogoaddr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttextmain.go:9:10: net.TCPAddr struct literal uses unkeyed fields
يعمل p.Status على المؤشر دون أن تكتب (*p).Status. فـ Go تتبع المؤشر إلى الـ struct تلقائيًا حين تصل إلى أحد حقولها.
كيف تضبط حقل مؤشر اختياريًا في سطر واحد؟
استخدم new مع قيمة، وهذا يعمل منذ Go 1.26. حقول المؤشرات هي الطريقة المعتادة للتمييز بين «لم يُرسَل» و«أُرسل بقيمة صفرية» في جسم طلب PATCH، وقبل Go 1.26 كنت تحتاج إلى متغير مؤقت أو دالة مساعدة لكل حقل منها:
example.gogotype UpdateUser struct { Name *string `json:"name,omitempty"` Email *string `json:"email,omitempty"` Age *int `json:"age,omitempty"` } func main() { patch := UpdateUser{ Email: new("[email protected]"), Age: new(31), } out, _ := json.Marshal(patch) fmt.Println(string(out)) }
example.texttext{"email":"[email protected]","age":31}
يحجز new(31) قيمة int ويخزّن فيها 31 ثم يعيد المؤشر إليها (ملاحظات إصدار Go 1.26). ومع go 1.25 في go.mod يرفضه المترجم:
example.texttext./main.go:16:10: new("[email protected]") requires go1.26 or later (-lang was set to go1.25; check go.mod)
لماذا تختفي حقول struct التي تبدأ بحرف صغير من JSON؟
لأنها غير مُصدَّرة، ولا تستطيع الحزم الأخرى رؤية الحقول غير المُصدَّرة. و encoding/json حزمة أخرى، لذا تتجاهلها دون أي خطأ:
example.gogotype User struct { ID int64 Email string password string role string } func main() { u := User{ID: 7, Email: "[email protected]", password: "hunter2", role: "admin"} out, err := json.Marshal(u) fmt.Println(string(out), err) var in User err = json.Unmarshal([]byte(`{"ID":8,"Email":"[email protected]","role":"admin"}`), &in) fmt.Printf("%+v %v\n", in, err) }
example.texttext{"ID":7,"Email":"[email protected]"} <nil> {ID:8 Email:[email protected] password: role:} <nil>
يعيد الاستدعاءان كلاهما خطأ nil، ويضيع role الموجود في المدخلات دون أي تنبيه. فإذا أعادت واجهة API مكتوبة بـ Go القيمة {} أو بقي حقل فارغًا بعد فك الترميز، فابحث عن هذا السبب أولًا. والحل أن تجعل الحرف الأول من اسم الحقل كبيرًا، وتستخدم وسمًا للإبقاء على الاسم بحروف صغيرة في JSON.
وتنطبق القاعدة نفسها على كل حزمة تقرأ الحقول عبر الانعكاس (reflection): encoding/xml، وأدوات القراءة في database/sql، و GORM، ومكتبات YAML، وتنفيذ القوالب. ويمكنك أيضًا الاستفادة منها عن قصد. فالحقل غير المُصدَّر مكان آمن للبيانات التي يجب ألا تُرمَّز أبدًا، مثل مقبض قاعدة بيانات أو ذاكرة تخزين مؤقت.
ما وسوم struct في Go؟
وسم struct سلسلة نصية حرفية تأتي بعد نوع الحقل. تتجاهله Go نفسها، وتقرؤه الحزم عبر الانعكاس لتقرر كيف تتعامل مع ذلك الحقل. ووسم JSON هو أكثر ما ستكتبه:
example.gogotype User struct { ID int64 `json:"id"` Email string `json:"email"` Nickname string `json:"nickname,omitempty"` PasswordHash string `json:"-"` DeletedAt time.Time `json:"deleted_at,omitempty"` LastLoginAt time.Time `json:"last_login_at,omitzero"` } func main() { u := User{ID: 7, Email: "[email protected]", PasswordHash: "$2a$10$..."} enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(u) }
example.texttext{ "id": 7, "email": "[email protected]", "deleted_at": "0001-01-01T00:00:00Z" }
وإليك المخرجات حقلًا بحقل:
- يغيّر
json:"id"اسم الحقل في المخرجات. - يحذف
omitemptyالحقل حين تكون قيمتهfalseأو0أو""أوnilأو slice أو map فارغة. والحقلNicknameفارغ، لذا اختفى. - لا يرمّز
json:"-"الحقل أبدًا. فيبقىPasswordHashخارج كل استجابة. - لا يعمل
omitemptyمع الـ structs. فالحقلDeletedAtقيمةtime.Timeصفرية، ومع ذلك يظهر على شكل0001-01-01T00:00:00Z، وتميل تطبيقات العملاء إلى قراءته على أنه تاريخ حقيقي. - يحذف
omitzero، الذي أُضيف في Go 1.24، الحقل حين يحمل القيمة الصفرية لنوعه. ومع الأنواع التي لها دالة تابعةIsZero() bool، مثلtime.Time، يستدعي تلك الدالة. لذا اختفىLastLoginAt(ملاحظات إصدار Go 1.24).
في الكود الجديد، استخدم omitzero مع حقول struct ومع time.Time. أما omitempty فما زال مناسبًا للسلاسل النصية والأرقام والـ slices والـ maps.
يمكن أن يحمل الحقل وسومًا لعدة حزم، مفصولة بمسافات: `json:"email" db:"email" validate:"required,email"`. وكل حزمة لا تقرأ إلا مفتاحها الخاص باستخدام reflect.StructTag.Get:
example.gogof, _ := reflect.TypeFor[User]().FieldByName("Nickname") fmt.Println(f.Tag.Get("json"))
example.texttextnickname,omitempty
لماذا يُتجاهَل وسم struct الذي كتبته؟
السبب في الغالب مسافة أو علامة اقتباس ناقصة. فالصيغة صارمة: key:"value"، دون مسافة بعد النقطتين. يبدو json: "email" سليمًا للقارئ، لكن Tag.Get("json") يعيد سلسلة فارغة، فيُرمَّز الحقل باسم Email. ويقبل المترجم أي سلسلة نصية وسمًا، لذا لن يعترض. أما go vet فسيعترض عبر المحلّل structtag، ويكتشف أيضًا حقلين لهما اسم JSON نفسه:
example.gogotype User struct { Email string `json: "email"` Name string `json:"name" db:"name"` ID int64 `json:"id"` Ref int64 `json:"id"` }
example.texttextmain.go:4:2: struct field tag `json: "email"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value main.go:7:2: struct field Ref repeats json tag "id" also at main.go:6
والمحررات التي تشغّل gopls تعرض التحذيرات نفسها أثناء الكتابة.
كيف تعمل الدوال التابعة مع الـ structs؟
الدالة التابعة دالة لها مستقبِل (receiver) يُكتب قبل اسمها. ويكون المستقبِل إما قيمة (c Cart) أو مؤشرًا (c *Cart)، وهذا الاختيار يحدد هل تستطيع الدالة تعديل الـ struct:
example.gogotype Cart struct { Items []string Total int64 } func (c Cart) AddValue(item string, price int64) { c.Items = append(c.Items, item) c.Total += price } func (c *Cart) Add(item string, price int64) { c.Items = append(c.Items, item) c.Total += price } func main() { var c Cart c.AddValue("keyboard", 4999) fmt.Println(len(c.Items), c.Total) c.Add("keyboard", 4999) fmt.Println(len(c.Items), c.Total) }
example.texttext0 0 1 4999
تُترجم AddValue وتعمل، لكن السلة تبقى فارغة. فالمستقبِل بالقيمة نسخة من الـ struct، لذا تعدّل الدالة النسخة ثم تتخلص منها. ويقع كود Go الجديد في هذا الخطأ كثيرًا، لأنه لا يوجد خطأ أو تحذير يشير إليه. أما Add فتأخذ مؤشرًا وتعدّل سلة المستدعي.
لا تحتاج إلى كتابة (&c).Add(...). فحين يكون c قابلًا لأخذ عنوانه، تأخذ Go العنوان نيابةً عنك. والعكس يعمل أيضًا: يمكن للمؤشر أن يستدعي الدوال التابعة ذات المستقبِل بالقيمة.
استخدم المستقبِل بالمؤشر حين تعدّل الدالة الـ struct، أو حين تكون الـ struct كبيرة، أو حين تحتوي على sync.Mutex أو حقل مشابه يجب ألا يُنسخ. واستخدم المستقبِل بالقيمة للقيم الصغيرة الثابتة مثل Money أو Point. وإذا احتاجت دالة تابعة واحدة على نوع ما إلى مستقبِل بالمؤشر، فاجعل كل دواله التابعة بمستقبِل بالمؤشر. فالخلط بين النوعين يجعل فهم مجموعة الدوال التابعة أصعب، لأن *Cart وحده عندها يملك كل الدوال، و *Cart وحده يحقق الـ interfaces التي تحتاج إليها جميعًا (مواصفات Go).
كيف يعمل تضمين الـ structs في Go؟
الحقل الذي يُكتب بنوعه فقط دون اسم هو حقل مُضمَّن (embedded field). وتُرقّى حقوله ودواله التابعة، أي يمكنك استخدامها كأنها مُصرَّح عنها في الـ struct الخارجية:
example.gogotype Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } func (t *Timestamps) Touch(now time.Time) { if t.CreatedAt.IsZero() { t.CreatedAt = now } t.UpdatedAt = now } type Order struct { ID int64 Timestamps } type User struct { ID int64 Email string Timestamps } func main() { now := time.Date(2026, 9, 26, 9, 0, 0, 0, time.UTC) var o Order o.Touch(now) fmt.Println(o.CreatedAt.Format(time.DateOnly), o.Timestamps.UpdatedAt.Format(time.Kitchen)) u := User{ID: 7, Email: "[email protected]", Timestamps: Timestamps{CreatedAt: now}} fmt.Println(u.CreatedAt.Year()) }
example.texttext2026-09-26 9:00AM 2026
الاستدعاء o.Touch(now) اختصار لـ o.Timestamps.Touch(now). فالحقل المُضمَّن ما زال موجودًا تحت اسم نوعه، وبهذا الاسم أيضًا تضبطه في القيمة الحرفية: Timestamps: Timestamps{...}. وتُسطّح encoding/json الـ structs المُضمَّنة كذلك، لذا يُرمَّز Order أعلاه على شكل {"ID":0,"CreatedAt":"2026-09-26T09:00:00Z","UpdatedAt":"2026-09-26T09:00:00Z"}.
التضمين ليس وراثة. فالنوع Order ليس Timestamps، ولا يمكنك تمرير Order إلى دالة تأخذ Timestamps. وحين تعمل Touch يكون مستقبِلها هو Timestamps الداخلية. ولا سبيل لها إلى الوصول إلى Order المحيطة بها أو استدعاء دوال تابعة يعرّفها Order. أما الصنف الفرعي (subclass) في Java أو Python فيستطيع ذلك، ومن هنا تأتي أسئلة «أي دالة مُعاد تعريفها ستعمل؟». وهذه الأسئلة لا تُطرح في Go.
وحين تصرّح الـ struct الخارجية عن حقل أو دالة تابعة بالاسم نفسه، تكون لها الأولوية، ويبقى الحقل أو الدالة المُضمَّنة متاحًا عبر اسم النوع:
example.gogotype Base struct{ ID int64 } func (Base) Describe() string { return "base" } type Order struct { Base ID string } func (Order) Describe() string { return "order" } func main() { o := Order{Base: Base{ID: 42}, ID: "ord_42"} fmt.Println(o.ID, o.Base.ID) fmt.Println(o.Describe(), o.Base.Describe()) }
example.texttextord_42 42 order base
هل يمكنك تضمين interface داخل struct؟
نعم، وهذه طريقة شائعة لتغليف نوع مع إعادة تعريف دالة تابعة واحدة. فالـ middleware في HTTP الذي يسجّل رمز الحالة يضمّن http.ResponseWriter، فيحصل على Header و Write دون أي جهد، ولا يستبدل إلا WriteHeader:
example.gogotype statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } func logStatus(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, req) slog.Info("request", "path", req.URL.Path, "status", rec.status) }) }
example.texttext2026/09/26 09:04:44 INFO request path=/orders/99 status=404
ولأن *statusRecorder يملك الدوال التابعة الثلاث كلها، فهو يحقق http.ResponseWriter ويمكن تمريره إلى المعالج التالي. وإذا كانت الـ interface المُضمَّنة nil، فإن استدعاء إحدى دوالها التابعة يسبب panic، لذا اضبطها دائمًا في القيمة الحرفية.
هل ينبغي تضمين sync.Mutex داخل struct؟
ليس في نوع مُصدَّر. فالتضمين يُرقّي Lock و Unlock إلى الواجهة العامة للنوع، فيستطيع أي مستدعٍ أن يقفل ذاكرتك المؤقتة من الخارج:
example.gogotype Cache struct { sync.Mutex items map[string]string }
أما الحقل المُسمّى غير المُصدَّر فيُبقي القفل تفصيلًا داخليًا في التنفيذ:
example.gogotype SafeCache struct { mu sync.Mutex items map[string]string } func (c *SafeCache) Get(key string) (string, bool) { c.mu.Lock() defer c.mu.Unlock() v, ok := c.items[key] return v, ok }
القيمة الصفرية لـ sync.Mutex قفل غير مقفول، لذا لا تحتاج SafeCache إلى أي تهيئة للقفل. لكنها ما زالت تحتاج إلى إنشاء الـ map قبل أول عملية كتابة.
هل يمكنك مقارنة الـ structs ونسخها في Go؟
يمكنك مقارنة struct بأخرى باستخدام == حين تكون كل الحقول قابلة للمقارنة. وتجري المقارنة حقلًا بحقل. وهذا يجعل الـ structs الصغيرة أنواع قيم جيدة ومفاتيح map جيدة:
example.gogotype Money struct { Amount int64 Currency string } type RouteKey struct { Method string Path string } func main() { fmt.Println(Money{4999, "EUR"} == Money{4999, "EUR"}) hits := map[RouteKey]int{} hits[RouteKey{"GET", "/orders"}]++ hits[RouteKey{"GET", "/orders"}]++ hits[RouteKey{"POST", "/orders"}]++ fmt.Println(hits[RouteKey{"GET", "/orders"}], len(hits)) }
example.texttexttrue 2 2
المفتاح المركّب مثل RouteKey أنظف من لصق السلاسل النصية معًا على شكل "GET /orders"، ولا يمكن أن يتصادم مع مفتاح آخر حين يحتوي المسار على مسافة. والقيم الحرفية الموضعية مناسبة هنا، لأن RouteKey مُصرَّح عنه في الحزمة نفسها وله حقلان واضحان.
الـ slices والـ maps والدوال غير قابلة للمقارنة، لذا فالـ struct التي تحتوي على أحدها غير قابلة للمقارنة أيضًا. ويخبرك المترجم بذلك:
example.gogotype Order struct { ID int64 Items []string } func main() { a := Order{ID: 1} b := Order{ID: 1} fmt.Println(a == b) }
example.texttext./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)
قارن مثل هذه الـ structs حقلًا بحقل، مع slices.Equal للـ slice، أو باستخدام reflect.DeepEqual في الاختبارات.
هل يؤدي إسناد struct إلى نسخها؟
نعم، لكن النسخة سطحية. فكل حقل يُنسخ، والحقل من نوع slice أو map يُنسخ على شكل ترويسة (header) ما زالت تشير إلى البيانات نفسها:
example.gogotype Order struct { ID int64 Items []string Meta map[string]string } func main() { original := Order{ID: 1, Items: []string{"keyboard"}, Meta: map[string]string{"source": "web"}} copied := original copied.ID = 2 copied.Items[0] = "mouse" copied.Meta["source"] = "api" fmt.Println(original.ID, original.Items, original.Meta) }
example.texttext1 [mouse] map[source:api]
تغيير copied.ID لم يمسّ الأصل. أما تغيير عنصر في copied.Items أو مفتاح في copied.Meta فقد غيّر الأصل أيضًا. وللحصول على نسخة مستقلة، انسخ هذه الحقول بنفسك باستخدام slices.Clone و maps.Clone.
ويحدث النسخ نفسه حين تمرّر struct بالقيمة إلى دالة. ومع struct تحتوي على sync.Mutex يكون هذا النسخ خطأً برمجيًا، لأن النسخة تحصل على قفل خاص بها. ويُبلغ go vet عن ذلك عبر المحلّل copylocks:
example.gogotype Counter struct { mu sync.Mutex hits int } func report(c Counter) { fmt.Println(c.hits) }
example.texttextmain.go:13:15: report passes lock by value: shop.Counter contains sync.Mutex main.go:19:9: call of report copies lock value: shop.Counter contains sync.Mutex
والحل هو func report(c *Counter).
ما الـ struct المجهولة (anonymous struct) في Go؟
هي نوع struct يُكتب في مكانه دون اسم. تستخدمها حين تحتاج إلى شكل ما مرة واحدة. وأكثر مكانين ستراها فيهما هما الاختبارات المبنية على الجداول (table-driven tests) وفك ترميز جزء من استجابة JSON:
example.gogovar resp struct { Data struct { Status string `json:"status"` } `json:"data"` } body := `{"data":{"status":"shipped","carrier":"DHL"},"meta":{"request_id":"abc"}}` if err := json.Unmarshal([]byte(body), &resp); err != nil { panic(err) } fmt.Println(resp.Data.Status) tests := []struct { name string input string want string }{ {"trims spaces", " [email protected] ", "[email protected]"}, {"already clean", "[email protected]", "[email protected]"}, } for _, tt := range tests { got := strings.ToLower(strings.TrimSpace(tt.input)) fmt.Println(tt.name, got == tt.want) }
example.texttextshipped trims spaces true already clean true
لا يملأ فك الترميز إلا الحقول التي تصرّح عنها الـ struct، لذا يُتجاهَل carrier و meta. ولا تحتاج إلى نوع مُسمّى لاستجابة لا تقرأ منها إلا حقلًا واحدًا. وحين يظهر الشكل نفسه في مكانين، أعطه اسمًا.
ما استخدام struct{}؟
النوع struct{} هو struct بلا حقول. حجمها صفر بايت، لذا فهي نوع القيمة المفضّل حين لا يهم إلا وجود الشيء. والحالتان الشائعتان هما مجموعة (set) مبنية على map، وقناة (channel) تُستخدم للإشارة فقط:
example.gogoseen := map[string]struct{}{} for _, id := range []string{"evt_1", "evt_2", "evt_1"} { if _, dup := seen[id]; dup { fmt.Println("skipping duplicate", id) continue } seen[id] = struct{}{} } fmt.Println(len(seen), unsafe.Sizeof(struct{}{})) done := make(chan struct{}) go func() { defer close(done) }() <-done fmt.Println("worker finished")
example.texttextskipping duplicate evt_1 2 0 worker finished
يقول map[string]struct{} «مجموعة من السلاسل النصية» بوضوح أكبر من map[string]bool، التي يضطر القارئ معها إلى السؤال عن معنى false. ويقول chan struct{} إنه لا توجد بيانات تنتقل عبر القناة، وإنما مجرد حقيقة أنها أُغلقت. وللسبب نفسه تعيد ctx.Done() القيمة <-chan struct{}.
هل يغيّر ترتيب الحقول حجم الـ struct؟
نعم. فكل حقل يُحاذى (aligned) بحسب حجمه، لذا يُدخل المترجم حشوًا (padding) بين حقل صغير وحقل أكبر يليه. والحقول الخمسة نفسها تشغل ضعف الذاكرة إذا رُتّبت ترتيبًا سيئًا على جهاز 64 بت:
example.gogotype EventLoose struct { Active bool ID int64 Retried bool Attempts int32 Urgent bool } type EventPacked struct { ID int64 Attempts int32 Active bool Retried bool Urgent bool } func main() { fmt.Println(unsafe.Sizeof(EventLoose{}), unsafe.Sizeof(EventPacked{})) }
example.texttext32 16
يضيف EventLoose حشوًا من 7 بايتات بعد Active كي يبدأ ID عند حدّ من 8 بايتات، ثم حشوًا إضافيًا بعد Retried و Urgent. أما EventPacked فيرتّب الحقول من الأكبر إلى الأصغر ولا يهدر إلا بايتًا واحدًا في النهاية.
في معظم الـ structs لا يهم هذا، وتجميع الحقول بحسب معناها أسهل في القراءة. لكنه يبدأ في الأهمية حين تحتفظ بملايين القيم في slice أو في ذاكرة تخزين مؤقت. ويُبلغ المحلّل fieldalignment في golang.org/x/tools عن الـ structs التي يمكن أن تكون أصغر.
الـ struct في Go مقابل الصنف (class): ما الفرق؟
تحمل الـ struct في Go البيانات ويمكن أن تكون لها دوال تابعة، وهذا يغطي معظم ما يفعله الصنف في Java أو C# أو Python. وما عدا ذلك مختلف عن قصد:
| الـ struct في Go | الصنف (Java و C# و Python) | |
|---|---|---|
| دالة الإنشاء | لا توجد. القيمة الصفرية، أو دالة NewX بحكم العُرف | دالة إنشاء خاصة |
| إعادة الاستخدام | التضمين (التركيب) | الوراثة، إضافةً إلى التركيب |
| تعدد الأشكال | الـ interfaces، وتتحقق ضمنيًا | الأصناف الأساسية والـ interfaces المُصرَّح عنها |
| الظهور | على مستوى الحزمة، بحسب حالة الحرف الأول | public و private و protected لكل عضو |
| الدوال التابعة | يُصرَّح عنها خارج النوع، في أي مكان من الحزمة | يُصرَّح عنها داخل جسم الصنف |
| دلالة النسخ | بالقيمة افتراضيًا، و & للحصول على مؤشر | بالمرجع افتراضيًا |
حين تحتاج struct إلى تحقق من صحة القيم أو إلى قيم افتراضية، اكتب دالة عادية تعيدها. والعُرف أن تسمّيها NewX، ومن الطبيعي أن تعيد معها خطأً:
example.gogotype Client struct { baseURL string http *http.Client retries int } func NewClient(baseURL string) (*Client, error) { if baseURL == "" { return nil, errors.New("baseURL is required") } return &Client{ baseURL: baseURL, http: &http.Client{Timeout: 10 * time.Second}, retries: 3, }, nil }
الحقول غير المُصدَّرة تعني أن المستدعين خارج الحزمة لا يستطيعون الحصول على Client إلا عبر NewClient، مع تطبيق القيم الافتراضية. ولأن Go لا تعرف super ولا إعادة تعريف الدوال، فلن تضطر إلى صعود شجرة أصناف لتعرف ما تفعله دالة ما. فسلوك الـ struct هو دوالها التابعة الخاصة إضافةً إلى الدوال التابعة التي تضمّنها، ويمكنك قراءتها كلها في حزمة واحدة. وهذا يساعد كثيرًا حين تكبر قاعدة كود مكتوبة بـ Go.
أين يأتي دور LevelUpGo
يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تبني دورة Composite Types أول struct لك ودوال الإنشاء (constructor functions) والدوال التابعة والتضمين خطوة بخطوة، ثم تستخدمها في مشروع صغير. وتغطي دورة Pointers & Memory المؤشرات إلى الـ structs و new والمستقبِل بالقيمة مقابل المستقبِل بالمؤشر، وتنتهي بمخزن لمفاتيح الميزات (feature flags) يعدّل الـ structs في مكانها. وتوضّح دورة Interfaces & Polymorphism كيف تحقق الـ structs الـ interfaces دون التصريح بذلك. ويقدّم Training Ground تمارين قصيرة مستقلة للتدرّب خارج أي دورة. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.
الأسئلة الشائعة
هل struct كلمة مفتاحية في Go؟
نعم. struct واحدة من الكلمات المفتاحية المحجوزة الـ 25 في Go، لذا لا يمكنك استخدامها اسمًا لمتغير أو دالة. وهي تبدأ نوع struct، يُسمّى في أغلب الأحوال باستخدام type، كما في type Order struct { ... }. وتظهر أيضًا في الـ structs المجهولة وفي الـ struct الفارغة struct{}.
هل في Go أصناف (classes)؟
لا. في Go بدلًا منها structs مع دوال تابعة، و interfaces، وتضمين. تحمل الـ struct البيانات، وتضيف الدوال التابعة السلوك، وتوفّر الـ interfaces تعدد الأشكال، ويعيد التضمين استخدام الكود عبر التركيب. لا توجد وراثة ولا كلمة مفتاحية لدالة الإنشاء. وتؤدي دالة NewX هذا الدور بحكم العُرف.
هل أستخدم المستقبِل بالقيمة أم المستقبِل بالمؤشر؟
استخدم المستقبِل بالمؤشر إذا كانت الدالة تعدّل الـ struct، أو إذا كانت الـ struct كبيرة، أو إذا كانت تحتوي على sync.Mutex. واستخدم المستقبِل بالقيمة للقيم الصغيرة التي لا تتغير أبدًا، مثل Money أو Point. وإذا احتاجت دالة تابعة واحدة على نوع ما إلى مستقبِل بالمؤشر، فاجعل كل الدوال التابعة بمستقبِل بالمؤشر كي يتصرف النوع بشكل متّسق.
لماذا يعيد json.Marshal كائنًا فارغًا للـ struct الخاصة بي؟
لأن حقول الـ struct غير مُصدَّرة. فالحزمة encoding/json لا ترى إلا الحقول التي تبدأ بحرف كبير، وتتجاهل غيرها دون أي خطأ. غيّر اسم email إلى Email وأضف `json:"email"` كي يبقى الاسم بحروف صغيرة في JSON.
ما الفرق بين omitempty و omitzero؟
يحذف omitempty القيم false و 0 و "" و nil والـ slices والـ maps الفارغة، لكنه لا يحذف struct أبدًا، لذا تظهر قيمة time.Time الصفرية على شكل 0001-01-01T00:00:00Z. أما omitzero، الذي أُضيف في Go 1.24، فيحذف أي حقل يحمل القيمة الصفرية لنوعه، ويستخدم الدالة التابعة IsZero الخاصة بالنوع إن وُجدت.
المصادر
- The Go Programming Language Specification, Struct types: https://go.dev/ref/spec#Struct_types
- The Go Programming Language Specification, Composite literals: https://go.dev/ref/spec#Composite_literals
- The Go Programming Language Specification, Method sets: https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Comparison operators: https://go.dev/ref/spec#Comparison_operators
- encoding/json, Marshal: https://pkg.go.dev/encoding/json#Marshal
- reflect, StructTag: https://pkg.go.dev/reflect#StructTag
- Go 1.24 Release Notes, omitzero: https://go.dev/doc/go1.24
- Go 1.26 Release Notes, new with an expression: https://go.dev/doc/go1.26
- go vet analyzers (composites, structtag, copylocks): https://pkg.go.dev/cmd/vet
