> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# الأنواع والواجهات Interfaces والذاكرة والأخطاء في Go
- URL: https://arabroot.io/articles/الانواع-والواجهات-interfaces-والذاكرة-والاخطاء-في-go/
- Published: 2026-10-31T07:11:11.000Z
- Updated: 2026-10-31T08:10:02.000Z
- Description: لماذا لا ينقص المخزون رغم أن الدالة تعمل، ولماذا لا يساوي الخطأ nil وهو يطبع nil، وكيف تقيس ما يحجزه كودك من الذاكرة وتختار شكل الخطأ في خدمة حقيقية بلغة Go.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

بعد أسبوع من كتابة خدمة الطلبات بلغة Go، وصل إلى فريقنا في الشركة الناشئة التي تبيع القهوة والتمر والهيل ثلاث ملاحظات من الاختبار: الأولى أن حجز المخزون لا ينقص الكمية رغم أن الدالة تعمل ولا ترجع خطأ، والثانية أن طلباً سليماً تماماً يرجع للعميل خطأ 500 مع رسالة غريبة في السجل تقول `<nil>`، والثالثة أن طباعة إيصالات الطلبات اليومية تأخذ ست ثوان وتجعل جامع القمامة يعمل مئتي مرة. والمطور الذي كتب الكود جاء من Java، والكود يبدو صحيحاً تماماً لمن يقرؤه بعين Java، وهذا بالضبط ما يجعل هذه الأخطاء الثلاثة مهمة: كلها تأتي من مكان واحد، وهو أن نظام الأنواع وإدارة الذاكرة والأخطاء في Go تعمل بطريقة مختلفة عما تعودت عليه، والمترجم لن يعترض على أي منها.

وفي [المقال الأول من هذه المرحلة](https://arabroot.io/articles/go-%D9%84%D9%85%D9%86-%D9%8A%D8%B9%D8%B1%D9%81-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%88%D8%A7%D9%88%D9%84-%D9%85%D8%B4%D8%B1%D9%88%D8%B9/) ثبتنا Go 1.27.3 وتعرفنا على الأدوات وكتبنا حاسبة الطلب، وفي هذا المقال نتعمق في ثلاثة أسئلة يسألها كل قادم إلى اللغة: كيف تعمل الأنواع والواجهات Interfaces بلا كلاسات ولا وراثة؟ ومن يدير الذاكرة، وكيف تقيس ما يكلفه كودك منها؟ وكيف تبلغ عن الخطأ في لغة ليس فيها استثناءات؟ ولن نكرر هنا ما شرحناه في مقالين سابقين، فجامع القمامة نفسه ومقبضاه `GOGC` و`GOMEMLIMIT` قسناهما في مقال [إدارة الذاكرة يدوياً أم Garbage Collector أم Ownership](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%8A%D8%AF%D9%88%D9%8A%D8%A7-%D8%A7%D9%85-garbage-collector-%D8%A7%D9%85-ownership/)، وتغليف الخطأ بالفعل `%w` مع `errors.Is` و`errors.As` شرحناه في مقال [كيف تتعامل لغات البرمجة مع الأخطاء](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/)، وإنما نبني عليهما بما يحتاجه من يكتب خدمة حقيقية.

وسوف نناقش في هذا المقال ما يلي:

- نظام الأنواع Type System في Go: ثابت وصارم بلا تحويل ضمني، والقيمة الصفرية، والـ struct الذي ينسخ بالقيمة.
- المستقبل Receiver بالقيمة أو بالمؤشر، وهو سبب المخزون الذي لا ينقص.
- الواجهات Interfaces الضمنية، والتضمين Embedding بدل الوراثة، وتبديل الأنواع Type Switch، ثم الواجهة من الداخل وسبب الخطأ الذي يطبع `<nil>`.
- الأنواع العامة Generics، والجديد في Go 1.27: دوال مرتبطة بنوع تأخذ معاملات نوع.
- الذاكرة بالأرقام: تحليل الهروب Escape Analysis، وقياس الحجوزات بالـ Benchmarks، وما يراه جامع القمامة من `runtime.MemStats` و`gctrace`، ثم `pprof` ليدلك على السطر.
- الأخطاء: الخطأ الثابت ونوع الخطأ و`errors.Join`، وفحص جديد في `go vet`، ثم `panic` و`recover` وحدودهما.
- الأخطاء الشائعة، وجدول قرار للقيمة والمؤشر وشكل الخطأ.

## نظام الأنواع Type System في Go: ثابت وصارم

Go لغة ثابتة الأنواع Statically Typed، أي أن نوع كل متغير معروف وقت الترجمة، وصارمة Strongly Typed بمعنى أنها لا تحول بين الأنواع وحدها أبداً: لا تستطيع أن تجمع `int` مع `int64`، ولا أن تمرر `int32` لدالة تنتظر `int`، ولا أن تضع رقماً في شرط `if` بدل قيمة منطقية، وكل ذلك يحتاج تحويلاً صريحاً مثل `int64(x)`. وهذا يزعج القادم من JavaScript أو Python في الأيام الأولى، ولكنه يغلق باباً كاملاً من الأخطاء التي رأيناها في مقال [TypeScript للمطورين](https://arabroot.io/articles/typescript-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%85%D8%A7-%D8%A7%D9%84%D8%B0%D9%8A-%D8%AA%D9%83%D8%B4%D9%81%D9%87-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9/)، حيث تجمع اللغة نصاً مع رقم فتعطي نصاً دون أن يعترض أحد. وإذا أردت الصورة العامة لأنظمة الأنواع وأين تقع Go بين اللغات، فقد شرحناها في مقال [أنماط البرمجة وأنظمة الأنواع](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

وتستطيع أن تعرف نوعاً جديداً فوق نوع موجود، مثل `type Halalas int`، وهذا النوع الجديد مختلف عن `int` في نظر المترجم رغم أن تمثيله في الذاكرة واحد، فلا تستطيع أن تجمع `Halalas` مع `int` دون تحويل، وهذه طريقة رخيصة جداً لتمنع خلط الهللات بالكميات أو بالريالات في دالة واحدة. ولكي نرى كل ما في هذا القسم والقسمين التاليين بالمخرج، كتبنا برنامجاً واحداً اسمه `types/main.go` يمر على الأفكار بالترتيب، وأول جزء فيه النوع `Product` ودالتان لحجز المخزون:

```go
type Product struct {
	ID    int
	Name  string
	Price int // halalas
	Stock int
}

// Value receiver: works on a copy.
func (p Product) ReserveCopy(n int) { p.Stock -= n }

// Pointer receiver: works on the original.
func (p *Product) Reserve(n int) { p.Stock -= n }
```

وعند تشغيل البرنامج كاملاً بالأمر `go run ./types` يكون المخرج كما يلي، وسوف نقرؤه قسماً قسماً في هذا المقال:

```text
== zero values
{ID:0 Name: Price:0 Stock:0} | map nil? true len 0 | slice nil? true len 0
read from nil map: 0
== value vs pointer receiver
after ReserveCopy(4): 10
after Reserve(4):     6
== structs compare by value
a == b: false | a.Price: 1
== implicit interfaces and embedding
created by api | CreatedBy: api | total: 2490
== generics
2970 29.700000000000003 8300
== type switch
nil
int 42
string "قهوة" (len 8 bytes)
Priced main.Product = 990
other float64
== typed nil inside an interface
buggy: err == nil? false, value <nil>, type *main.StockError
fixed: err == nil? true
errors.As: true 6
```

### القيمة الصفرية Zero Value بدل null

السطر الأول من المخرج يبين ما يحدث عندما تعرف متغيرات دون أن تعطيها قيمة: الـ struct ظهر بكل حقوله أصفاراً ونصوصاً فارغة، والخريطة والشريحة كلاهما `nil` وطول كل منهما صفر، والقراءة من الخريطة `nil` أعادت صفراً دون أي خطأ. وهذا يعني أنك نادراً ما ترى في Go خطأً مثل `NullPointerException` عند قراءة حقل، لأن الـ struct نفسه ليس مؤشراً، ولكن انتبه إلى أن المؤشر `*Product` قيمته الصفرية `nil`، والقراءة منه توقف البرنامج بنفس الطريقة التي تعرفها من Java، والكتابة في خريطة `nil` توقفه أيضاً كما سوف نرى في قسم الأخطاء. والقاعدة العملية التي يكررها [دليل Effective Go](https://go.dev/doc/effective%5Fgo?ref=arabroot.io) أن تصمم أنواعك بحيث تكون قيمتها الصفرية صالحة للاستخدام، فالنوع `Totals` في حاسبة الطلب قيمته الصفرية «طلب فارغ» صحيح، ولا يحتاج دالة إنشاء.

### المستقبل Receiver بالقيمة أو بالمؤشر

وهنا سبب الملاحظة الأولى من الاختبار. السطر الثاني في المخرج يقول إن المخزون بقي 10 بعد `ReserveCopy(4)`، ثم صار 6 بعد `Reserve(4)`، والدالتان متطابقتان في كل شيء إلا النجمة قبل `Product`. والسبب أن كل شيء في Go يمرر بالقيمة Pass by Value، بلا استثناء، فالمستقبل بدون نجمة يستلم نسخة كاملة من الـ struct، والدالة عدلت هذه النسخة ثم اختفت النسخة مع نهاية الدالة. أما المستقبل بالنجمة فيستلم نسخة من المؤشر، وهي تشير إلى نفس المتغير، فوصل التعديل إليه. والصورة التالية تبين الحالتين:

![مقارنة بين مستقبل بالقيمة حيث تنسخ الدالة الـ struct فيبقى المخزون 10 في المتغير الأصلي ويصبح 6 في النسخة فقط، ومستقبل بالمؤشر حيث تستلم الدالة عنوان المتغير فيصبح المخزون 6 في الأصل، وتحتهما القاعدة العملية لاختيار المؤشر](https://arabroot.io/content/images/2026/10/go-types-memory-01-receivers.webp)

النجمة قبل اسم النوع هي الفرق بين تعديل الأصل وتعديل نسخة تختفي

والذي يجعل هذا الخطأ صعب الملاحظة أن المترجم يقبل الحالتين بنفس صيغة الاستدعاء `hail.Reserve(4)`، فإذا كانت الدالة بمؤشر والمتغير قيمة قابلة للعنونة كتب المترجم `(&hail).Reserve(4)` نيابة عنك، كما تشرح [الأسئلة الشائعة عن المؤشر أو القيمة في الدوال المرتبطة](https://go.dev/doc/faq?ref=arabroot.io#methods%5Fon%5Fvalues%5For%5Fpointers). والقادم من Java أو Python تعود على أن الكائنات تمرر بالمرجع دائماً، فيكتب الدالة بدون نجمة لأنها «أبسط»، ويعمل الكود دون أي رسالة. والقاعدة التي يذكرها نفس المرجع: إذا كانت الدالة تعدل المستقبل فيجب أن يكون مؤشراً، وإذا كان النوع كبيراً فالمؤشر أرخص من نسخه في كل استدعاء، وإذا كان فيه قفل `sync.Mutex` فيجب أن يكون مؤشراً لأن نسخ القفل يعطيك قفلاً آخر لا يحمي شيئاً، وإذا كانت بعض دوال النوع بمؤشر فاجعلها كلها بمؤشر حتى لا يحتار القارئ.

### الـ struct ينسخ بالقيمة Copy Semantics

والسطر الثالث في المخرج يكمل نفس الفكرة: نسخنا `a` إلى `b` بالسطر `b := a`، ثم غيرنا سعر `b`، فبقي سعر `a` 1 كما هو، والمقارنة `a == b` أعادت `false`. فالإسناد ينسخ الـ struct كاملاً، والمقارنة بالعلامة `==` تقارن الحقول حقلاً حقلاً، وهذا يعمل ما دامت كل الحقول قابلة للمقارنة Comparable، فإذا كان في الـ struct شريحة أو خريطة رفض المترجم المقارنة. ولاحظ الفخ هنا: الـ struct ينسخ، ولكن الشريحة داخله نافذة على مصفوفة، فنسخ struct فيه شريحة يعطيك نسختين تشيران إلى نفس البيانات، وتعديل عنصر في إحداهما يظهر في الأخرى، وهذا ما يشرحه مقال Go الرسمي عن [الشرائح Slices من الداخل](https://go.dev/blog/slices-intro?ref=arabroot.io).

## الواجهات Interfaces الضمنية

الواجهة في Go مجموعة دوال، وأي نوع له هذه الدوال يحققها تلقائياً، دون أن يكتب `implements` ودون أن يعرف أصلاً أن الواجهة موجودة. والجزء التالي من البرنامج يعرف واجهة صغيرة اسمها `Priced`، ونوعين يحققانها دون أن يذكراها:

```go
// Priced is satisfied by any type with a Halalas method; no "implements" keyword.
type Priced interface{ Halalas() int }

func (p Product) Halalas() int { return p.Price }

type Shipping struct{ Fee int }

func (s Shipping) Halalas() int { return s.Fee }

// Embedding: Audit's fields and methods are promoted into Order.
type Audit struct{ CreatedBy string }

func (a Audit) Who() string { return "created by " + a.CreatedBy }

type Order struct {
	Audit
	ID    int
	Items []Priced
}
```

والسطر الرابع في المخرج يبين أن الشريحة `[]Priced` حملت منتجاً ورسوم شحن معاً، والحلقة جمعت `990 + 1500 = 2490` دون أن تعرف نوع أي منهما. وقد يتساءل البعض: ما الفرق عن `interface` في Java سوى أنك لا تكتب الكلمة؟ والإجابة أن الفرق في من يعرف الواجهة: في Java يجب أن يعلن الكلاس عن الواجهات التي يحققها، فالواجهة يجب أن تكون موجودة قبله، أما في Go فتستطيع أن تكتب واجهة اليوم لنوع كتبه غيرك قبل سنوات في مكتبة لا تملكها، وسوف يحققها ما دامت دواله مطابقة. ولهذا السبب يعرف المستهلك الواجهة في Go وليس المنتج، والعرف المشهور «اقبل واجهات وارجع أنواعاً محددة» Accept Interfaces, Return Structs يأتي من هنا: الدالة في خدمة الطلبات التي تحتاج من قاعدة البيانات ثلاث عمليات فقط تعرف واجهة بهذه العمليات الثلاث، فتستطيع في الاختبار أن تمرر لها نسخة مزيفة بثلاث دوال، كما سوف نفعل في المقال الرابع.

وأشهر مثال على قوة الواجهات الصغيرة في المكتبة القياسية هو `io.Reader`، وهي واجهة بدالة واحدة `Read(p []byte) (n int, err error)`، ويحققها الملف والاتصال الشبكي وجسم طلب HTTP والنص في الذاكرة وملف مضغوط بـ gzip، فكل دالة تقبل `io.Reader` تعمل مع كل هذه الأشياء. ولذلك تجد أغلب الواجهات في Go بدالة أو دالتين، وكلما كبرت الواجهة قلت الأنواع التي تحققها وضعفت فائدتها.

### من الكلاسات Classes إلى Go: كيف ترجم الفريق تصميم Java؟

المطور القادم من Java في فريقنا بدأ تصميم الخدمة كما تعود: كلاس مجرد `AbstractRepository` فيه الاتصال بقاعدة البيانات، يرثه `ProductRepository` و`OrderRepository`، ثم واجهة `IOrderService` يحققها كلاس واحد `OrderServiceImpl`، ثم مصنع Factory ينشئ كل ذلك. وعندما حاول أن ينقل هذا الشكل إلى Go حرفياً وجد نفسه يكتب struct فارغاً يضمنه في كل نوع، وواجهة لكل struct لا يحققها إلا نوع واحد، ودالة `NewX` لكل شيء، والكود صار ضعف حجمه دون أي فائدة. والترجمة الصحيحة أبسط من ذلك بكثير، وهي ثلاث خطوات: الأولى أن الحالة المشتركة مثل مجمع الاتصالات تصبح حقلاً عادياً في struct واحد اسمه `Store`، والدوال المشتركة تصبح دوالاً مرتبطة به، فلا حاجة إلى أب مجرد. والثانية أن الواجهة لا تكتب بجانب النوع الذي يحققها، وإنما بجانب من يستخدمها، وبالدوال التي يحتاجها فقط، فطبقة HTTP تعرف واجهة بثلاث دوال من `Store` لأنها تستخدم ثلاثاً، وهذا ما سوف تراه في الحزمة `api` في المقال الرابع. والثالثة أن دالة الإنشاء `New` تكتب فقط عندما يحتاج النوع تجهيزاً حقيقياً، وإلا فالقيمة الصفرية أو القيمة الحرفية `Store{pool: p}` تكفي.

وقد يتساءل البعض: أليس هذا تراجعاً عن مبادئ التصميم التي تعلمناها؟ والإجابة أن المبادئ نفسها باقية، فالفصل بين الطبقات والاعتماد على واجهة بدل نوع محدد كلاهما موجود، ولكن الأداة تغيرت: في Java تحتاج الوراثة والإعلان الصريح لتحصل على هذه الفوائد، وفي Go تحصل عليها بالتركيب والواجهات الضمنية الصغيرة، ولذلك يبدو الكود أقل «هندسة» وهو يحقق نفس الأهداف. والقاعدة التي نصل إليها بعد عدة مشاريع: لا تكتب واجهة حتى يوجد لها مستخدمان، أو اختبار يحتاج نسخة مزيفة، لأن الواجهة التي لا يحققها إلا نوع واحد تضيف طبقة يقفز فيها القارئ دون سبب.

### التضمين Embedding بدل الوراثة Inheritance

في النوع `Order` أعلاه وضعنا `Audit` كحقل بلا اسم، وهذا هو التضمين Embedding، فصارت الدالة `Who()` والحقل `CreatedBy` متاحين مباشرة على الطلب، كما ظهر في المخرج: `created by api | CreatedBy: api`. وهذا يشبه الوراثة في شكله، ولكنه ليس وراثة في معناه، فالطلب ليس «نوعاً من» `Audit`، ولا تستطيع أن تمرر `Order` لدالة تنتظر `Audit`، وإنما المترجم يرفع Promote الحقول والدوال من الداخل إلى الخارج كاختصار للكتابة، وما زال `o.Audit.Who()` صحيحاً. ولا يوجد تجاوز Override بالمعنى الذي تعرفه، فإذا عرفت `Who()` على `Order` نفسه أخفت الدالة الداخلية، ولكن دوال `Audit` التي تستدعي `Who()` سوف تستدعي نسختها هي، وليس نسخة `Order`، وهذا يفاجئ كل من يحاول أن يبني نمط Template Method الذي تعرفه من Java. ويشرح [قسم التضمين في Effective Go](https://go.dev/doc/effective%5Fgo?ref=arabroot.io#embedding) هذه الآلية، والدرس العملي أن تفكر في التركيب Composition: الطلب يحتوي على بيانات تدقيق، ولا يرثها.

### النوع any وتبديل الأنواع Type Switch

النوع `any` اسم مختصر للواجهة الفارغة `interface{}`، وكل نوع يحققها لأنها لا تطلب أي دالة، فهي أقرب شيء في Go إلى `Object` في Java. وعندما تستلم قيمة من نوع `any` تستطيع أن تسأل عن نوعها الحقيقي بتبديل الأنواع Type Switch:

```go
func describe(v any) string {
	switch x := v.(type) {
	case nil:
		return "nil"
	case int:
		return fmt.Sprintf("int %d", x)
	case string:
		return fmt.Sprintf("string %q (len %d bytes)", x, len(x))
	case Priced:
		return fmt.Sprintf("Priced %T = %d", x, x.Halalas())
	default:
		return fmt.Sprintf("other %T", x)
	}
}
```

وفي المخرج لاحظ أمرين: الأول أن النص «قهوة» طوله 8 بايت وليس 4، لأن `len` على النص في Go تعد البايتات وليس الحروف، والحرف العربي في UTF-8 بايتان، فإذا أردت عدد الحروف فاستخدم `utf8.RuneCountInString` أو مرر على النص بالحلقة `range` التي تعطيك الحروف Runes واحداً واحداً، وهذا خطأ يقع فيه كل من يقص أسماء المنتجات العربية بعدد ثابت. والثاني أن الحالة `case Priced` طابقت المنتج لأنه يحقق الواجهة، فتبديل الأنواع يعمل مع الواجهات أيضاً، والترتيب مهم، فالحالة الأولى التي تطابق تفوز. ولا تجعل `any` عادة في الكود، لأنك تخسر كل فحص يقوم به المترجم وتنقله إلى وقت التشغيل، ومكانها الطبيعي الحدود مثل قراءة JSON لا تعرف شكله.

### الواجهة من الداخل: لماذا لا يساوي الخطأ nil؟

وهنا سبب الملاحظة الثانية من الاختبار، الطلب السليم الذي رجع بخطأ 500\. والكود الذي سببها في الدالة `checkStockBuggy`:

```go
type StockError struct{ ProductID, Want, Have int }

func (e *StockError) Error() string {
	return fmt.Sprintf("product %d: want %d, have %d", e.ProductID, e.Want, e.Have)
}

// checkStock returns a *StockError typed nil when everything is fine: a classic bug.
func checkStockBuggy(p Product, n int) error {
	var se *StockError
	if p.Stock < n {
		se = &StockError{p.ID, n, p.Stock}
	}
	return se
}

func checkStock(p Product, n int) error {
	if p.Stock < n {
		return &StockError{p.ID, n, p.Stock}
	}
	return nil
}
```

والمخرج في آخر البرنامج يقول: `buggy: err == nil? false, value <nil>, type *main.StockError`، أي أن الخطأ ليس `nil`، ولكن طباعته تعطي `<nil>`. ولكي تفهم ذلك يجب أن تعرف أن قيمة الواجهة في Go زوج من شيئين: النوع الحقيقي للقيمة التي بداخلها، والقيمة نفسها، كما يشرح Russ Cox في مقاله عن [تمثيل الواجهات Go Data Structures: Interfaces](https://research.swtch.com/interfaces?ref=arabroot.io). والواجهة تساوي `nil` فقط إذا كان الاثنان فارغين، والصورة التالية تبين الحالات الثلاث:

![ثلاث بطاقات تمثل قيمة الواجهة كزوج من النوع والقيمة: خطأ nil حقيقي نوعه nil وقيمته nil، ومؤشر nil داخل واجهة نوعه StockError وقيمته nil فلا تساوي الواجهة nil، وخطأ له قيمة، وتحتها مخرج التجربة الذي يبين false ثم true](https://arabroot.io/content/images/2026/10/go-types-memory-02-typed-nil.webp)

الواجهة لا تساوي nil إلا إذا كان النوع والقيمة كلاهما فارغين

فالمتغير `se` من النوع `*StockError`، وعندما أرجعناه من دالة نوعها المرجع `error`، وضع المترجم داخل الواجهة النوع `*StockError` والقيمة `nil`، فصار للواجهة نوع، ولم تعد تساوي `nil`. وهذا الخطأ مشهور لدرجة أن له سؤالاً خاصاً في [الأسئلة الشائعة: لماذا قيمة الخطأ nil لا تساوي nil؟](https://go.dev/doc/faq?ref=arabroot.io#nil%5Ferror). والحل في الدالة `checkStock`: لا تحفظ الخطأ في متغير من نوع محدد ثم ترجعه، وإنما ارجع `nil` صريحة في الطريق السليم، وارجع `&StockError{…}` مباشرة في طريق الخطأ، وعندها أعاد السطر الأخير `errors.As: true 6`، أي أنه استخرج المؤشر ومنه المخزون الحالي 6\. ولاحظ أن `staticcheck` لا يكتشف هذا الخطأ في كل أشكاله، فالحماية الحقيقية هي العادة: الدالة التي ترجع `error` ترجع `nil` حرفياً.

## الأنواع العامة Generics: من 1.18 إلى الدوال المرتبطة في 1.27

لعشر سنوات لم يكن في Go أنواع عامة، وكان الحل إما أن تكرر الدالة لكل نوع، أو أن تستخدم `any` وتخسر الفحص. ثم جاءت في Go 1.18 عام 2022 بعد نقاش طويل، ويشرحها [مقال مقدمة الأنواع العامة](https://go.dev/blog/intro-generics?ref=arabroot.io): الدالة تأخذ معامل نوع بين قوسين مربعين، ومعه قيد Constraint يحدد الأنواع المسموحة، والقيد نفسه واجهة:

```go
// Generics: one Sum for any numeric type.
type Number interface{ ~int | ~int64 | ~float64 }

func Sum[T Number](xs []T) T {
	var s T
	for _, x := range xs {
		s += x
	}
	return s
}

type Halalas int
```

والعلامة `~int` تعني «كل نوع تمثيله int»، ولذلك قبلت `Sum` النوع `Halalas` الذي عرفناه فوق `int`، وأعادت `8300`. ولكن انظر إلى السطر الخاص بها في المخرج مرة أخرى: `2970 29.700000000000003 8300`، فنفس الدالة مع `float64` جمعت 9.9 ثلاث مرات فأعطت 29.700000000000003، وهذا ليس خطأ في الأنواع العامة، وإنما سبب آخر لتخزين المبالغ بالهللة كأرقام صحيحة كما فعلنا في المقال الأول.

وفي الإصدار الحالي Go 1.27 أضيفت ميزة طال انتظارها، وهي أن الدالة المرتبطة بنوع Method تستطيع أن تأخذ معاملات نوع خاصة بها، كما تذكر [ملاحظات إصدار Go 1.27](https://go.dev/doc/go1.27?ref=arabroot.io). وقبلها كان يجب أن تكتب دالة عامة على مستوى الحزمة تأخذ الكائن كمعامل، والملف `generic/main.go` يجرب الميزة الجديدة:

```go
// Go 1.27: a method may declare its own type parameters.
package main

import "fmt"

type Product struct {
	ID    int
	Name  string
	Price int
}

type Catalog struct{ items []Product }

// Index builds a map from any comparable key the caller chooses.
func (c *Catalog) Index[K comparable](key func(Product) K) map[K]Product {
	m := make(map[K]Product, len(c.items))
	for _, p := range c.items {
		m[key(p)] = p
	}
	return m
}

func main() {
	c := &Catalog{items: []Product{{1, "قهوة", 4500}, {2, "تمر", 3800}, {3, "هيل", 990}}}
	byID := c.Index(func(p Product) int { return p.ID })
	byName := c.Index(func(p Product) string { return p.Name })
	fmt.Println(byID[3].Name, byName["تمر"].Price)
}
```

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

```text
هيل 3800
```

فالدالة `Index` بنت فهرساً بالرقم مرة وبالاسم مرة، والمترجم استنتج `K` من الدالة الممررة دون أن نكتبه. ولاحظ القيد الذي تذكره ملاحظات الإصدار: دوال الواجهات لا تستطيع أن تأخذ معاملات نوع، ولا يمكن أن تحقق دالة عامة دالة في واجهة. ومتى تستخدم الأنواع العامة؟ يجيب مقال [When To Use Generics](https://go.dev/blog/when-generics?ref=arabroot.io) من فريق Go بقاعدة بسيطة: إذا وجدت نفسك تكتب نفس الكود أكثر من مرة والفرق الوحيد هو النوع، فاستخدمها، أما إذا كان الفرق في السلوك فالواجهة هي الأداة الصحيحة. وفي خدمة الطلبات لن تحتاج أن تكتب أنواعاً عامة بنفسك إلا نادراً، ولكنك سوف تستخدمها كل يوم من المكتبات، مثل `slices.Sort` و`maps.Keys` و`pgx.CollectRows`.

## من يدير الذاكرة؟ جامع القمامة Garbage Collector

في Go يدير جامع قمامة Garbage Collector الذاكرة نيابة عنك، فلا يوجد `free` ولا `delete`، وقد شرحنا في مقال إدارة الذاكرة كيف يعمل الجامع المتتبع Tracing GC بمرحلتي التعليم والكنس، وأنه يعمل بالتوازي مع البرنامج ولا يوقفه إلا أجزاء من المللي ثانية، وقسنا هناك أثر المقبضين `GOGC` و`GOMEMLIMIT` على برنامج يحتفظ بثلاثة ملايين طلب. ومنذ Go 1.26 صار الجامع الافتراضي تصميماً جديداً اسمه Green Tea، ويقول الفريق في [ملاحظات إصدار Go 1.26](https://go.dev/doc/go1.26?ref=arabroot.io) إنه يحسن تعليم الكائنات الصغيرة وفحصها، ويتوقع انخفاضاً بين 10% و40% في عمل الجامع في البرامج الحقيقية. ولكن ما يهمك كمطور قبل أي ضبط للجامع سؤال أبسط: كم كائناً ينشئ كودك؟ لأن عمل الجامع يتناسب مع ما تحجزه، وأفضل طريقة لتقليل عمله أن تقلل الحجوزات في الكود الذي يعمل مع كل طلب، والدليل الرسمي [A Guide to the Go Garbage Collector](https://go.dev/doc/gc-guide?ref=arabroot.io) يقول ذلك صراحة.

وفي Go مكانان للذاكرة: المكدس Stack لكل Goroutine، وهو رخيص جداً لأن كل ما فيه يختفي عند خروج الدالة دون أن يراه الجامع، والكومة Heap المشتركة التي يجمعها الجامع. والفرق المهم عن C أنك لا تختار المكان بنفسك، فالمترجم هو الذي يقرر، وهذا ما سوف نقيسه الآن على كود من خدمة الطلبات.

ولاحظ أن مكدس كل Goroutine ليس ثابت الحجم كما في خيوط نظام التشغيل، وإنما يبدأ صغيراً بضعة كيلوبايتات، ويكبر الـ Runtime حجمه وينسخه إلى مكان أكبر عندما تحتاج الدالة مساحة أكثر، وهذا سبب أن تشغيل مئة ألف Goroutine ممكن على جهاز عادي، كما سوف نقيس في المقال التالي. ومن نتائج هذا التصميم أن عنوان متغير على المكدس قد يتغير عندما ينسخ المكدس، والـ Runtime يحدث كل المؤشرات إليه، ولذلك لا يسمح لك Go بأن تحتفظ بعنوان المكدس خارج الدالة، والمترجم يحل المشكلة بأن ينقل أي كائن قد يعيش بعد دالته إلى الـ Heap. وكل ذلك يحدث دون أن تراه، ولكنه يفسر لماذا لا تجد في Go كلمة مثل `new` تعني «في الـ Heap» كما في C++ أو Java، فحتى الدالة `new(T)` قد تضع الكائن على المكدس إذا أثبت المترجم أنه لا يهرب.

### تحليل الهروب Escape Analysis: قرار المترجم

كتبنا في الحزمة `alloc` دوالاً تمثل ما يحدث في الخدمة: إنشاء سطر طلب بقيمة أو بمؤشر، وبناء شريحة أسطر مع حجز مسبق أو بدونه، وبناء نص إيصال بالدمج أو بـ `strings.Builder`:

```go
// Package alloc compares allocation patterns that show up in an orders service.
package alloc

import (
	"fmt"
	"strconv"
	"strings"
)

type Line struct{ ProductID, Qty, Price int }

//go:noinline
func NewLinePtr(id, qty, price int) *Line { return &Line{id, qty, price} }

//go:noinline
func NewLineVal(id, qty, price int) Line { return Line{id, qty, price} }

func BuildAppend(n int) []Line {
	var out []Line
	for i := 0; i < n; i++ {
		out = append(out, Line{i, 1, 990})
	}
	return out
}

func BuildPrealloc(n int) []Line {
	out := make([]Line, 0, n)
	for i := 0; i < n; i++ {
		out = append(out, Line{i, 1, 990})
	}
	return out
}

func ReceiptConcat(lines []Line) string {
	s := ""
	for _, l := range lines {
		s += fmt.Sprintf("%d x %d = %d\n", l.ProductID, l.Qty, l.Qty*l.Price)
	}
	return s
}

func ReceiptBuilder(lines []Line) string {
	var b strings.Builder
	b.Grow(len(lines) * 16)
	for _, l := range lines {
		b.WriteString(strconv.Itoa(l.ProductID))
		b.WriteString(" x ")
		b.WriteString(strconv.Itoa(l.Qty))
		b.WriteString(" = ")
		b.WriteString(strconv.Itoa(l.Qty * l.Price))
		b.WriteByte('\n')
	}
	return b.String()
}
```

ثم طلبنا من المترجم أن يشرح قراره لكل حجز بالخيار `-gcflags=-m`، وأخذنا الأسطر التي تخص هذا الملف:

```bash
go build -gcflags=-m ./alloc 2>&1 | grep "alloc.go"
```

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

```text
alloc/alloc.go:18:6: can inline BuildAppend
alloc/alloc.go:26:6: can inline BuildPrealloc
alloc/alloc.go:46:29: inlining call to strconv.Itoa
alloc/alloc.go:46:16: inlining call to strings.(*Builder).WriteString
alloc/alloc.go:47:16: inlining call to strings.(*Builder).WriteString
alloc/alloc.go:48:29: inlining call to strconv.Itoa
alloc/alloc.go:48:16: inlining call to strings.(*Builder).WriteString
alloc/alloc.go:49:16: inlining call to strings.(*Builder).WriteString
alloc/alloc.go:46:29: inlining call to strconv.Itoa
alloc/alloc.go:46:16: inlining call to strings.(*Builder).copyCheck
alloc/alloc.go:47:16: inlining call to strings.(*Builder).copyCheck
alloc/alloc.go:48:29: inlining call to strconv.Itoa
alloc/alloc.go:48:16: inlining call to strings.(*Builder).copyCheck
alloc/alloc.go:49:16: inlining call to strings.(*Builder).copyCheck
alloc/alloc.go:46:16: inlining call to abi.NoEscape
alloc/alloc.go:47:16: inlining call to abi.NoEscape
alloc/alloc.go:48:16: inlining call to abi.NoEscape
alloc/alloc.go:49:16: inlining call to abi.NoEscape
alloc/alloc.go:13:52: &Line{...} escapes to heap
alloc/alloc.go:21:15: append escapes to heap
alloc/alloc.go:27:13: make([]Line, 0, n) escapes to heap
alloc/alloc.go:29:15: append escapes to heap
alloc/alloc.go:34:20: lines does not escape
alloc/alloc.go:37:19: ... argument does not escape
alloc/alloc.go:37:39: l.ProductID escapes to heap
alloc/alloc.go:37:52: l.Qty escapes to heap
alloc/alloc.go:37:63: l.Qty * l.Price escapes to heap
alloc/alloc.go:42:21: lines does not escape
alloc/alloc.go:46:16: "strings: illegal use of non-zero Builder copied by value" escapes to heap
alloc/alloc.go:46:16: append escapes to heap
```

في المخرج أعلاه لاحظ التالي:

- الأسطر التي تبدأ بـ `can inline` و`inlining call to` قرارات الدمج Inlining، أي أن المترجم يضع جسم الدالة الصغيرة في مكان استدعائها بدل استدعائها، وهذا شرط لكثير من قرارات الهروب، لأن المترجم لا يرى ما بعد حدود الدالة غير المدمجة. ولذلك وضعنا التعليق `//go:noinline` على الدالتين الأوليين، حتى يبقى القياس صادقاً.
- `alloc.go:13:52: &Line{...} escapes to heap`: الدالة `NewLinePtr` ترجع عنوان الكائن، فالكائن يجب أن يعيش بعدها، فذهب إلى الـ Heap. أما `NewLineVal` فلا يوجد لها سطر أصلاً، لأن القيمة تنسخ إلى من استدعاها ولا شيء يهرب.
- `append escapes to heap` و`make([]Line, 0, n) escapes to heap`: الشريحة ترجع من الدالة، فمصفوفتها في الـ Heap في الحالتين، والفرق كما سوف نرى في عدد المرات.
- `lines does not escape`: الدالة تقرأ الشريحة ولا تحفظها، فلا يحتاج المترجم أن ينقل شيئاً بسببها.
- `l.ProductID escapes to heap`: هذا مفاجئ، فالرقم يهرب لأن `fmt.Sprintf` تأخذ معاملاتها من النوع `any`، ووضع رقم داخل واجهة يحتاج غالباً حجزاً، وهذا سبب أن `fmt` في الكود الساخن مكلفة.

![مقارنة بين دالة ترجع قيمة يبقى كائنها على الـ Stack بزمن 5.975 نانوثانية وصفر بايت وصفر حجز، ودالة ترجع مؤشراً يهرب كائنها إلى الـ Heap بزمن 262 نانوثانية و24 بايت وحجز واحد، مع سطر escapes to heap من المترجم](https://arabroot.io/content/images/2026/10/go-types-memory-03-escape.webp)

نفس البيانات، والفرق الوحيد هو ما ترجعه الدالة

### قياس الحجوزات بالـ Benchmarks

قرار المترجم وحده لا يكفي، والسؤال الحقيقي كم يكلف كل نمط، والإجابة من قياس الأداء Benchmark في الحزمة `testing` نفسها، بملف `alloc_test.go`:

```go
package alloc

import "testing"

var sinkP *Line
var sinkV Line
var sinkS []Line
var sinkStr string

func BenchmarkNewLinePtr(b *testing.B) {
	for b.Loop() {
		sinkP = NewLinePtr(1, 2, 990)
	}
}
func BenchmarkNewLineVal(b *testing.B) {
	for b.Loop() {
		sinkV = NewLineVal(1, 2, 990)
	}
}
func BenchmarkBuildAppend(b *testing.B) {
	for b.Loop() {
		sinkS = BuildAppend(1000)
	}
}
func BenchmarkBuildPrealloc(b *testing.B) {
```

ولاحظ هنا أمرين: الحلقة `for b.Loop()` هي الطريقة الحديثة منذ Go 1.24 كما تشرح [تدوينة b.Loop](https://go.dev/blog/testing-b-loop?ref=arabroot.io)، وهي تحدد عدد الدورات وحدها وتمنع المترجم من حذف الكود الذي لا تستخدم نتيجته. والمتغيرات العامة `sink` تحفظ النتيجة لنفس السبب، فقد رأينا في مقال إدارة الذاكرة كيف حذف المترجم الحجز كله في تجربة لم تحفظ نتيجتها. ثم شغلنا القياس مع خيار الذاكرة:

```bash
go test -run '^$' -bench . -benchmem ./alloc
```

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

```text
goos: linux
goarch: amd64
pkg: example.com/typeslab/alloc
cpu: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
BenchmarkNewLinePtr-8       	 5282126	       262.0 ns/op	      24 B/op	       1 allocs/op
BenchmarkNewLineVal-8       	198585574	         5.975 ns/op	       0 B/op	       0 allocs/op
BenchmarkBuildAppend-8      	    6336	    224808 ns/op	   77802 B/op	      12 allocs/op
BenchmarkBuildPrealloc-8    	   16550	     89876 ns/op	   24576 B/op	       1 allocs/op
BenchmarkReceiptConcat-8    	    1098	   1007418 ns/op	  286648 B/op	     599 allocs/op
BenchmarkReceiptBuilder-8   	   22227	     48725 ns/op	    4160 B/op	     301 allocs/op
PASS
ok  	example.com/typeslab/alloc	7.871s
```

ولنقرأ المخرج سطراً سطراً:

- الرقم `-8` بعد اسم كل قياس هو `GOMAXPROCS`، أي عدد الأنوية التي استخدمها البرنامج، ثم عدد الدورات التي اختارها الإطار، ثم الزمن لكل دورة، ثم البايتات والحجوزات لكل دورة.
- القيمة مقابل المؤشر: صفر حجز وأقل من 6 نانوثانية للقيمة، وحجز واحد من 24 بايت للمؤشر. والزمن 262 نانوثانية للمؤشر أكبر مما يجب لحجز واحد، والسبب أن الجهاز كان يشغل أعمالاً أخرى أثناء هذا القياس بالذات، لذلك اعتمد على عمودي الحجوزات والبايتات فهما ثابتان، وكرر القياس بالخيار `-count` ثم قارن بأداة `benchstat` قبل أن تثق بأي زمن.
- بناء ألف سطر: بدون حجز مسبق 12 حجزاً و77,802 بايت، لأن `append` ضاعفت السعة عدة مرات ونسخت البيانات في كل مرة، ومع `make(…, 0, n)` حجز واحد بالضبط بحجم 24,576 بايت، أي ألف سطر في 24 بايت مقربة لأقرب حجم يستخدمه المخصص، والزمن نزل إلى أقل من النصف.
- الإيصال بمئتي سطر: الدمج بالعلامة `+=` مع `fmt.Sprintf` أخذ 599 حجزاً و286 كيلوبايت لكل إيصال، لأن النص في Go ثابت، فكل `+=` تنشئ نصاً جديداً وتنسخ فيه كل ما سبق، و`Sprintf` تحجز للمعاملات. أما `strings.Builder` مع `Grow` فأخذ 4,160 بايت فقط، وأسرع بعشرين ضعفاً تقريباً. وبقيت 301 حجزاً، وهي من `strconv.Itoa` التي تنشئ نصاً لكل رقم، ويمكن إزالتها بـ `strconv.AppendInt` إذا كان هذا الكود ساخناً فعلاً.

### ما يراه جامع القمامة: runtime.MemStats

الـ Benchmark يقيس دالة واحدة، أما ما يهم الخدمة فهو أثر هذه الحجوزات على الجامع خلال دقائق من العمل. لذلك كتبنا البرنامج `memstats` الذي يبني مئتي ألف إيصال بعشرين سطراً، ويقرأ إحصاءات الـ Runtime قبل الحلقة وبعدها بالدالة `runtime.ReadMemStats`، وحقولها موثقة في [توثيق MemStats](https://pkg.go.dev/runtime?ref=arabroot.io#MemStats):

```go
// memstats builds 200,000 receipts two ways and prints what the GC saw.
//   go run ./memstats concat|builder
package main

import (
	"fmt"
	"os"
	"runtime"
	"time"

	"example.com/typeslab/alloc"
)

var sink string

func main() {
	mode := os.Args[1]
	lines := alloc.BuildPrealloc(20)
	var before, after runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&before)
	start := time.Now()
	for i := 0; i < 200_000; i++ {
		if mode == "concat" {
			sink = alloc.ReceiptConcat(lines)
		} else {
			sink = alloc.ReceiptBuilder(lines)
		}
	}
	elapsed := time.Since(start)
	runtime.ReadMemStats(&after)
	fmt.Printf("mode=%s time=%v\n", mode, elapsed.Round(time.Millisecond))
	fmt.Printf("  allocated total: %d MiB in %d objects\n", (after.TotalAlloc-before.TotalAlloc)>>20, after.Mallocs-before.Mallocs)
	fmt.Printf("  gc cycles: %d, total pause: %v\n", after.NumGC-before.NumGC, time.Duration(after.PauseTotalNs-before.PauseTotalNs).Round(time.Microsecond))
	fmt.Printf("  heap in use now: %d KiB, from OS (Sys): %d MiB\n", after.HeapInuse>>10, after.Sys>>20)
}
```

والمخرج بالطريقتين سوف يكون كما يلي:

```text
mode=concat time=5.83s
  allocated total: 603 MiB in 11800973 objects
  gc cycles: 199, total pause: 674.126ms
  heap in use now: 2632 KiB, from OS (Sys): 21 MiB

mode=builder time=921ms
  allocated total: 73 MiB in 4200052 objects
  gc cycles: 22, total pause: 82.472ms
  heap in use now: 3328 KiB, from OS (Sys): 16 MiB
```

![أربعة رسوم بأعمدة أفقية تقارن الدمج بالعلامة += مع strings.Builder: الذاكرة المحجوزة 603 مقابل 73 ميجابايت، والكائنات 11.8 مقابل 4.2 مليون، ودورات الجامع 199 مقابل 22، ومجموع التوقف 674 مقابل 82 مللي ثانية والزمن الكلي 5.83 مقابل 0.92 ثانية](https://arabroot.io/content/images/2026/10/go-types-memory-04-alloc-gc.webp)

تقليل الحجوزات قلل عمل الجامع بنفس النسبة تقريباً، دون أي ضبط له

في المخرج أعلاه لاحظ التالي:

- `allocated total` هو مجموع ما حجزه البرنامج طوال الحلقة، 603 ميجابايت مقابل 73، وهذا الرقم لا يعني أن البرنامج استهلك 603 ميجابايت في لحظة واحدة، فالجامع كان يحرر وراءه باستمرار.
- `gc cycles` نزل من 199 إلى 22، أي تسع مرات أقل، وهو نفس نسبة الذاكرة المحجوزة تقريباً، لأن الجامع يبدأ دورة جديدة كلما تضاعف حجم الـ Heap الحي بالإعداد الافتراضي `GOGC=100`.
- `total pause` مجموع التوقفات القصيرة لكل الدورات، 674 مللي ثانية مقابل 82، وهذا وقت لم يخدم فيه البرنامج أي طلب.
- `Sys` هو ما أخذه البرنامج من نظام التشغيل، 21 و16 ميجابايت فقط، رغم مئات الميجابايتات المحجوزة، وهذا هو الجامع وهو يعمل: الذاكرة نفسها تستخدم مرة بعد مرة.

والدرس هنا أن مشكلة الملاحظة الثالثة من الاختبار، ست ثوان ومئتا دورة جمع، لم تكن مشكلة في الجامع، وإنما في الكود الذي يطعمه، وأن تغيير ثلاثة أسطر أنزل الزمن من 5.83 إلى 0.92 ثانية.

### السجل GODEBUG=gctrace=1 سطراً سطراً

وإذا أردت أن ترى كل دورة وحدها، فالمتغير `GODEBUG=gctrace=1` يجعل الـ Runtime يطبع سطراً عن كل دورة على الخطأ القياسي، وشغلنا به نفس البرنامج:

```bash
GODEBUG=gctrace=1 go run ./memstats concat 2> gc.txt
head -5 gc.txt; grep -c '^gc ' gc.txt
```

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

```text
mode=concat time=24.365s
  allocated total: 602 MiB in 11800776 objects
  gc cycles: 189, total pause: 214.247ms
  heap in use now: 672 KiB, from OS (Sys): 17 MiB
gc 1 @1.318s 0%: 0.19+14+0.026 ms clock, 1.5+14/10/0+0.20 ms cpu, 3->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 8 P
gc 2 @1.555s 0%: 11+1.7+0.005 ms clock, 94+2.0/1.3/0+0.041 ms cpu, 3->3->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 8 P
gc 3 @1.640s 1%: 7.9+17+0.006 ms clock, 63+0.87/0.84/14+0.053 ms cpu, 3->4->2 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 8 P
gc 4 @1.679s 1%: 1.9+4.4+0.012 ms clock, 15+1.0/2.1/0.11+0.10 ms cpu, 4->5->2 MB, 5 MB goal, 0 MB stacks, 0 MB globals, 8 P
gc 5 @1.697s 1%: 1.4+14+0.072 ms clock, 11+0/3.5/1.3+0.58 ms cpu, 4->8->5 MB, 5 MB goal, 0 MB stacks, 0 MB globals, 8 P
lines: 198
```

وقد شرحنا حقول هذا السطر واحداً واحداً في قسم جامع القمامة من مقال إدارة الذاكرة، والمهم هنا ثلاثة أشياء: الأول أن الهدف `4 MB goal` صغير جداً، لأن البيانات الحية بين الإيصالات قليلة، فالجامع يبدأ دورة كلما حجز البرنامج 4 ميجابايت جديدة، وهذا سبب العدد الكبير للدورات. والثاني أن الدورة الثانية فيها توقف أول 11 مللي ثانية، وهو رقم كبير لجامع Go، والدورات الأخرى أقل من 2 مللي ثانية، فالتوقف الطويل الوحيد جاء في بداية البرنامج والجهاز مشغول. والثالث أن البرنامج نفسه أخذ في هذا التشغيل 24 ثانية بدل 5.8، مع نفس عدد الدورات تقريباً، 189 مقابل 199، وهذه من المشكلات التي سوف نذكرها بعد قليل.

### أين تحجز الذاكرة؟ pprof يدلك على السطر

في مشروع حقيقي لا تعرف مسبقاً أي دالة هي المشكلة، والأداة التي تجيب هي `pprof`، وهي جزء من الأمر `go`، ويشرح [دليل التشخيص Diagnostics](https://go.dev/doc/diagnostics?ref=arabroot.io) أنواع ملفاتها. وأبسط طريقة أن تأخذ ملف الذاكرة من الـ Benchmark نفسه ثم تعرض الدوال الأكثر حجزاً:

```bash
go test -run '^$' -bench ReceiptConcat -benchmem -memprofile mem.out -o alloc.test ./alloc
go tool pprof -sample_index=alloc_space -top -nodecount=8 alloc.test mem.out
```

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

```text
File: alloc.test
Build ID: 22c472088bfbbe4f26ab288403949b9267715d31
Type: alloc_space
Time: 2026-10-31 07:25:40 UTC
Showing nodes accounting for 782.41MB, 99.62% of 785.41MB total
Dropped 28 nodes (cum <= 3.93MB)
      flat  flat%   sum%        cum   cum%
  776.41MB 98.85% 98.85%   782.91MB 99.68%  example.com/typeslab/alloc.ReceiptConcat
       6MB  0.76% 99.62%     6.50MB  0.83%  fmt.Sprintf
         0     0% 99.62%   782.91MB 99.68%  example.com/typeslab/alloc.BenchmarkReceiptConcat
         0     0% 99.62%   782.91MB 99.68%  testing.(*B).run1.func1
         0     0% 99.62%   783.41MB 99.74%  testing.(*B).runN
```

العمود `flat` هو ما حجزته الدالة نفسها، و`cum` ما حجزته مع كل ما تستدعيه، والدالة `ReceiptConcat` حجزت بنفسها 98.85% من كل البايتات، وهذه حجوزات `+=` نفسها، بينما `fmt.Sprintf` حجزت 6 ميجابايت فقط. فلو بدأنا بإزالة `Sprintf` لأننا سمعنا أنها بطيئة لأصلحنا أقل من 1% من المشكلة، وهذا هو سبب أن تقيس قبل أن تغير. وفي الخدمة الحقيقية لا تحتاج Benchmark، وإنما تضيف الحزمة `net/http/pprof` على منفذ داخلي، وتسحب الملف من السيرفر وهو يعمل، كما يشرح [مقال Profiling Go Programs](https://go.dev/blog/pprof?ref=arabroot.io).

## الأخطاء Errors في Go: قيم وليست استثناءات

شرحنا في مقال الأخطاء عبر اللغات لماذا اختارت Go أن يكون الخطأ قيمة ترجعها الدالة، وكيف يغلف كل مستوى الخطأ بسياقه بالفعل `%w`، وكيف تمر `errors.Is` و`errors.As` على السلسلة كلها. وما نضيفه هنا هو كيف تختار شكل الخطأ في خدمة حقيقية، لأن كل شكل يتحول في النهاية إلى رد HTTP مختلف. ويلخص Rob Pike الفلسفة في عنوان مقاله [Errors are values](https://go.dev/blog/errors-are-values?ref=arabroot.io): ما دام الخطأ قيمة فتستطيع أن تبرمج به كما تبرمج بأي قيمة، تجمعه وتغلفه وتقارنه.

### الخطأ الثابت Sentinel ونوع الخطأ Custom Type

البرنامج `errs/main.go` يمثل حجز المخزون في الخدمة، وفيه شكلان للخطأ:

```go
// Errors lab: sentinel errors, custom types, errors.Join, panic and recover.
package main

import (
	"errors"
	"fmt"
	"os"
	"strconv"
)

var ErrNotFound = errors.New("product not found")

type StockError struct{ ProductID, Want, Have int }

func (e *StockError) Error() string {
	return fmt.Sprintf("product %d: want %d, have %d", e.ProductID, e.Want, e.Have)
}

var stock = map[int]int{1: 100, 2: 100, 3: 10}

func reserve(id, qty int) error {
	have, ok := stock[id]
	if !ok {
		return fmt.Errorf("reserve %d: %w", id, ErrNotFound)
	}
	if have < qty {
		return fmt.Errorf("reserve: %w", &StockError{id, qty, have})
	}
	stock[id] = have - qty
	return nil
}

// in main():
	for _, c := range [][2]int{{3, 4}, {9, 1}, {3, 50}} {
		err := reserve(c[0], c[1])
		var se *StockError
		switch {
		case err == nil:
			fmt.Printf("reserve(%d,%d): ok, stock now %d\n", c[0], c[1], stock[c[0]])
		case errors.Is(err, ErrNotFound):
			fmt.Printf("reserve(%d,%d): 404 -> %v\n", c[0], c[1], err)
		case errors.As(err, &se):
			fmt.Printf("reserve(%d,%d): 409 -> %v (short by %d)\n", c[0], c[1], err, se.Want-se.Have)
		}
	}

```

والجزء الأول من المخرج:

```text
== sentinel and custom error types
reserve(3,4): ok, stock now 6
reserve(9,1): 404 -> reserve 9: product not found
reserve(3,50): 409 -> reserve: product 3: want 50, have 6 (short by 44)
```

فالخطأ الثابت `ErrNotFound` قيمة واحدة معروفة لكل البرنامج، تناسب حالة لا تحتاج تفاصيل: المنتج غير موجود، وتفحصه بـ `errors.Is` حتى بعد أن غلفته الدالة بالنص `reserve 9:`. أما نوع الخطأ `*StockError` فيحمل أرقاماً يحتاجها المستدعي ليبني رداً مفيداً للعميل، مثل الكمية المتوفرة، ويستخرجها بـ `errors.As`، وحسبنا منه النقص 44\. ولاحظ أننا لم نقارن نص الرسالة في أي مكان، فالنص للإنسان الذي يقرأ السجل، والنوع للكود الذي يتخذ القرار، ولو قارنت النص لتعطل الكود عندما يغير أحد صياغة الرسالة. ومنذ Go 1.26 توجد الدالة `errors.AsType`، وهي نسخة عامة من `As` تقول ملاحظات الإصدار إنها أسرع وأسلم في النوع، فتكتب `se, ok := errors.AsType[*StockError](err)` بدل تعريف المتغير ثم تمرير عنوانه.

### عدة أخطاء معاً: errors.Join

عندما يرسل العميل طلباً فيه ثلاثة أسطر خاطئة، فمن الأفضل أن ترد بالأخطاء الثلاثة مرة واحدة، بدل أن يصلح الأول ويرسل فيأتيه الثاني. والدالة `errors.Join` التي أضيفت في Go 1.20 تجمع عدة أخطاء في خطأ واحد:

```go
type ItemInput struct{ ProductID, Qty string }

// validate returns every problem at once with errors.Join.
func validate(items []ItemInput) error {
	var errs []error
	if len(items) == 0 {
		errs = append(errs, errors.New("items: must not be empty"))
	}
	for i, it := range items {
		if _, err := strconv.Atoi(it.ProductID); err != nil {
			errs = append(errs, fmt.Errorf("items[%d].product_id: %w", i, err))
		}
		if q, err := strconv.Atoi(it.Qty); err != nil || q <= 0 {
			errs = append(errs, fmt.Errorf("items[%d].qty: must be a positive integer, got %q", i, it.Qty))
		}
	}
	return errors.Join(errs...)
}
```

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

```text
== errors.Join collects all validation errors
items[1].product_id: strconv.Atoi: parsing "abc": invalid syntax
items[1].qty: must be a positive integer, got "0"
items[2].qty: must be a positive integer, got "-1"
is strconv.ErrSyntax inside? true
```

الخطأ المجمع يطبع كل خطأ في سطر، و`errors.Is` وجدت `strconv.ErrSyntax` داخل أحد الأخطاء المجمعة، لأنها تمر على كل الأخطاء وليس على الأول فقط، كما يشرح [توثيق errors.Join](https://pkg.go.dev/errors?ref=arabroot.io#Join). ولاحظ أن `errors.Join` ترجع `nil` إذا كانت القائمة فارغة، فلا تحتاج شرطاً قبلها، وهذا يتجنب فخ الـ `nil` داخل الواجهة الذي رأيناه في قسم الواجهات.

### ماذا يحدث إذا غلفت مؤشراً Pointer بالفعل %w؟

وهنا خطأ جديد يكتشفه `go vet` في Go 1.27 لأول مرة، كما تذكر ملاحظات الإصدار. لنفرض أن زميلك عرف نوع خطأ دالته `Error()` على القيمة وليس على المؤشر، ثم غلف عنوانه:

```go
// Go 1.27 vet: wrapping a pointer to a type whose VALUE implements error.
package main

import (
	"errors"
	"fmt"
)

type NotFound struct{ ID int }

func (e NotFound) Error() string { return fmt.Sprintf("product %d not found", e.ID) }

func find(id int) error {
	e := NotFound{id}
	return fmt.Errorf("find: %w", &e)
}

func main() {
	err := find(9)
	var nf NotFound
	fmt.Println(err, "| errors.As NotFound:", errors.As(err, &nf))
}
```

والمخرج عند التشغيل ثم الفحص:

```text
find: product 9 not found | errors.As NotFound: false
vetwrap/main.go:15:27: %w wants operand of error type NotFound, not pointer type *NotFound (defeats errors.Is)
exit=1
```

فالرسالة المطبوعة صحيحة تماماً، ولكن `errors.As` أعادت `false`، لأن السلسلة تحمل `*NotFound` والمستدعي يبحث عن `NotFound`، وهما نوعان مختلفان. وفي الخدمة يعني ذلك أن المنتج غير الموجود يرجع 500 بدل 404، ولا ترى السبب بعينك في السجل. والفحص الجديد في `go vet` أمسك الخطأ بالسطر والعمود وقال السبب صراحة: `defeats errors.Is`. والقاعدة لتتجنب الفخ من البداية: قرر لكل نوع خطأ هل دالة `Error()` على القيمة أم على المؤشر، ثم استخدم نفس الشكل في كل مكان تنشئه فيه وتبحث عنه، والأكثر شيوعاً المؤشر كما في `*StockError`.

### متى تستخدم panic، وأين يعمل recover؟

الدالة `panic` هي الأقرب إلى الاستثناء في Go، فهي توقف الدالة الحالية وتنفذ كل `defer` في طريق الصعود، فإذا لم يلتقطها أحد أوقفت البرنامج كله وطبعت مكدس الـ Goroutine. والـ Runtime نفسه يستخدمها للأخطاء البرمجية: الكتابة في خريطة `nil`، والفهرس خارج حدود الشريحة، والقراءة من مؤشر `nil`. والقاعدة في Go أن `panic` للأخطاء التي تعني أن الكود نفسه خطأ، وليست لملف ناقص أو مدخلات سيئة من العميل، كما يشرح مقال [Defer, Panic, and Recover](https://go.dev/blog/defer-panic-and-recover?ref=arabroot.io). ولكن سيرفر HTTP لا يجب أن يتوقف كله بسبب خطأ في معالج طلب واحد، لذلك تلتقط الـ panic في طبقة واحدة حول كل طلب بالدالة `recover`، والدالة `safe` في نفس البرنامج تمثل هذه الطبقة:

```go
// safe turns a panic inside handler into an error, like an HTTP middleware.
func safe(name string, handler func()) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("%s panicked: %v", name, r)
		}
	}()
	handler()
	return nil
}
```

والجزء الأخير من المخرج:

```text
== recover turns a panic into an error
write to nil map panicked: assignment to entry in nil map
index out of range panicked: runtime error: index out of range [3] with length 1
program still running
```

فالخطآن تحولا إلى قيمتين عاديتين من النوع `error`، واستمر البرنامج، ولاحظ أن رسالة الفهرس تقول بالضبط ما حدث: الفهرس 3 والطول 1\. ولاحظ أيضاً أن `recover` تعمل فقط داخل دالة مؤجلة بـ `defer`، وأن المتغير `err` اسم في توقيع الدالة Named Result، وهذا ما يسمح للدالة المؤجلة أن تغير القيمة المرجعة بعد الـ panic.

ولكن هناك حد مهم جداً لهذه الطبقة، والبرنامج `errs/panicgo` يبينه: فيه `recover` في `main`، ثم Goroutine أخرى تكتب في خريطة `nil`:

```go
// A panic in another goroutine is not caught by recover in main: the whole process exits.
package main

import (
	"fmt"
	"time"
)

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recovered in main:", r)
		}
	}()
	go func() {
		var orders map[int]string
		orders[1] = "paid" // panics inside the goroutine
	}()
	time.Sleep(100 * time.Millisecond)
	fmt.Println("this line never prints")
}
```

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

```text
panic: assignment to entry in nil map

goroutine 7 [running]:
main.main.func2()
	/lab/errs/panicgo/main.go:17 +0x25
created by main.main in goroutine 1
	/lab/errs/panicgo/main.go:15 +0x3b
exit status 2
exit=1
```

فالبرنامج توقف كله برمز الخروج 2، ولم تعمل `recover` في `main`، ولم يطبع السطر الأخير، والسبب أن `recover` تلتقط الـ panic في نفس الـ Goroutine فقط، والـ panic في Goroutine أخرى لا يصعد إلى من أنشأها. ولاحظ أن المكدس المطبوع يقول بوضوح `goroutine 7` و`created by main.main in goroutine 1`، أي أنه يدلك على السطر الذي أنشأها. والدرس للخدمة: سيرفر `net/http` يلتقط الـ panic في معالج الطلب وحده، ولكن أي Goroutine تنشئها أنت داخل المعالج، لإرسال بريد مثلاً، يجب أن تحمل `recover` خاصاً بها، وإلا فإن خطأً واحداً فيها يوقف الخدمة كلها مع كل الطلبات التي كانت تعالجها. والصورة التالية تجمع أشكال الخطأ الأربعة والرد المناسب لكل منها:

![أربع بطاقات: الخطأ الثابت Sentinel مع errors.Is ويقابله 404، ونوع الخطأ مع errors.As ويقابله 409، وerrors.Join للتحقق من المدخلات ويقابله 400، وpanic مع recover للأخطاء البرمجية ويقابله 500، وتحتها تنبيه بأن recover يعمل في نفس الـ Goroutine فقط](https://arabroot.io/content/images/2026/10/go-types-memory-05-errors.webp)

كل شكل للخطأ يتحول إلى رد HTTP مختلف في خدمة الطلبات

## مشكلات Problems واجهتنا أثناء التجربة

- رقم المؤشر في الـ Benchmark، 262 نانوثانية لحجز 24 بايت، أكبر بكثير مما يتوقعه أي شخص يعرف مخصص Go، والسبب أن الجهاز كان يشغل حاويات أخرى في نفس اللحظة. ولم نعد القياس حتى «يطلع الرقم أحسن»، وإنما اعتمدنا على عمودي البايتات والحجوزات لأنهما لا يتأثران بانشغال الجهاز، وهذا ما ننصحك به: الزمن من تشغيل واحد على جهاز مشترك لا يعتمد عليه، وأما عدد الحجوزات فثابت.
- تشغيل نفس البرنامج مع `gctrace` أخذ 24.4 ثانية بدل 5.83، مع نفس عدد الدورات تقريباً. وجزء من الفرق كتابة مئتي سطر إلى الخطأ القياسي، ولكن أغلبه انشغال الجهاز في تلك الدقيقة، ولذلك قارنا الدورات والحجوزات بين التشغيلين، ولم نقارن الأزمنة.
- مخرج `-gcflags=-m` الكامل مئات الأسطر، أغلبها قرارات دمج لدوال المكتبة القياسية، لذلك فلترناه بالملف ثم بأرقام الأسطر. وإذا أردت قراءة القرار لسطر واحد فابحث عن رقم السطر نفسه، ولا تقرأ المخرج من أوله.

## أخطاء شائعة Common Mistakes

- دالة تعدل الـ struct بمستقبل بالقيمة: لا خطأ ولا تحذير، والتعديل يضيع. اجعل المستقبل مؤشراً لكل دالة تعدل.
- نسخ struct فيه `sync.Mutex` بالقيمة، أو تمريره بالقيمة لدالة: صار لكل نسخة قفل مختلف. والأداة `go vet` تكتشف أغلب هذه الحالات بفحص `copylocks`.
- إرجاع مؤشر من نوع محدد قيمته `nil` كقيمة `error`: الخطأ لا يساوي `nil`. ارجع `nil` حرفياً.
- قص النصوص العربية بالبايت مثل `name[:10]`: قد تقطع حرفاً في منتصفه فيظهر رمز غريب. اعمل على الحروف بـ `[]rune` أو بالحلقة `range`.
- مقارنة نص رسالة الخطأ بـ `err.Error() == "…"` أو `strings.Contains`: يتعطل الكود عندما تتغير الصياغة. استخدم خطأً ثابتاً مع `errors.Is` أو نوعاً مع `errors.As`.
- تغليف الخطأ بالفعل `%v` بدل `%w`: النص المطبوع نفسه، والسلسلة مقطوعة، و`errors.Is` ترجع `false`.
- استخدام `panic` للمدخلات الخاطئة أو للملف الناقص: هذه أخطاء عادية ترجعها قيمة. احتفظ بـ `panic` للأخطاء البرمجية.
- Goroutine بلا `recover` داخل خدمة: panic واحد فيها يوقف العملية كلها.
- ضبط `GOGC` قبل قياس الحجوزات: أغلب مشكلات الجامع تحل بتقليل الحجوزات في الكود الساخن، وابدأ بـ `pprof`.
- `any` في كل مكان لتشبه الكود الديناميكي: تخسر فحص المترجم. استخدمها عند الحدود فقط، والأنواع العامة عندما يتكرر الكود لأنواع مختلفة.

## جدول قرار Decision Table: قيمة أم مؤشر، وأي شكل للخطأ؟

| الحالة                          | الاختيار                              | السبب                                      |
| ------------------------------- | ------------------------------------- | ------------------------------------------ |
| دالة تعدل النوع                 | مستقبل بمؤشر \*T                      | بالقيمة تعدل نسخة تختفي                    |
| نوع فيه قفل أو كبير الحجم       | مؤشر في كل الدوال                     | نسخ القفل يلغي الحماية، والنسخ الكبير مكلف |
| نوع صغير ثابت مثل Totals        | قيمة                                  | لا يحتاج حجزاً في الـ Heap، وآمن للمشاركة  |
| دالة تنشئ كائناً صغيراً كثيراً  | ارجع قيمة                             | صفر حجز بدل حجز في كل استدعاء              |
| شريحة تعرف حجمها مسبقاً         | make(\[\]T, 0, n)                     | حجز واحد بدل 12 في تجربتنا                 |
| بناء نص في حلقة                 | strings.Builder مع Grow               | من 599 حجزاً إلى أقل بكثير                 |
| حالة معروفة بلا تفاصيل          | خطأ ثابت مع errors.Is                 | مقارنة بسيطة تصمد مع التغليف               |
| المستدعي يحتاج أرقاماً من الخطأ | نوع خطأ مع errors.As أو errors.AsType | البيانات في حقول، لا في نص                 |
| عدة أخطاء في المدخلات           | errors.Join                           | العميل يصلح كل شيء مرة واحدة               |
| خطأ برمجي لا يجب أن يحدث        | panic مع recover في طبقة واحدة        | طلب واحد يفشل بـ 500 والخدمة تستمر         |

## تدريب عملي

- أضف إلى النوع `Product` حقلاً `sync.Mutex` واجعل `ReserveCopy` تقفله، ثم شغل `go vet ./types` واقرأ الرسالة.
- غير `ReceiptBuilder` لتستخدم `strconv.AppendInt` على شريحة بايتات بدل `strconv.Itoa`، وأعد القياس، وانظر كم حجزاً بقي من الـ 301.
- شغل `memstats concat` مرة مع `GOGC=400` ومرة مع `GOMEMLIMIT=8MiB`، وقارن عدد الدورات وقيمة `Sys` بالجدول في مقال إدارة الذاكرة.
- أعد كتابة الجزء الذي يستخدم `errors.As` في `errs/main.go` بالدالة `errors.AsType`، وقارن عدد الأسطر.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- كل شيء في Go يمرر بالقيمة، فالمستقبل بدون نجمة يعدل نسخة، وهذا كان سبب المخزون الذي بقي 10 بعد الحجز، والإصلاح نجمة واحدة.
- الواجهات ضمنية ويعرفها المستهلك، والواجهة زوج من نوع وقيمة، ولذلك المؤشر `nil` داخلها لا يساوي `nil`، وهذا كان سبب الطلب السليم الذي رجع بخطأ 500.
- الأنواع العامة منذ 1.18 تقلل التكرار، وفي 1.27 صارت الدوال المرتبطة بنوع تأخذ معاملات نوع، و`float64` في الأنواع العامة أعطتنا 29.700000000000003 كتذكير بالهللات.
- المترجم يقرر مكان الكائن بتحليل الهروب، والمؤشر المرجع يكلف حجزاً، والحجز المسبق أنزل الحجوزات من 12 إلى 1، و`strings.Builder` أنزل الإيصالات من 603 ميجابايت و199 دورة جمع إلى 73 ميجابايت و22 دورة، والزمن من 5.83 إلى 0.92 ثانية.
- الخطأ الثابت للحالات المعروفة، ونوع الخطأ عندما تحتاج بياناته، و`errors.Join` للتحقق، و`go vet` في 1.27 يكتشف تغليف المؤشر الخاطئ.
- `recover` يعمل في نفس الـ Goroutine فقط، فكل Goroutine في الخدمة تحتاج حمايتها، وإلا أوقف خطأ واحد فيها العملية كلها برمز 2.

وفي المقال التالي من مرحلة [لغة Go للمطورين](https://arabroot.io/start/#stage-lang-go) ندخل إلى أهم ما يميز اللغة، وهو التزامن بالـ Goroutines والقنوات، ونقيس نفس مهمة الشبكة ونفس مهمة المعالج بالتسلسل والتزامن:

[![](https://arabroot.io/content/images/2026/10/go-for-devs-cover-cover.webp)دليلGo لمن يعرف البرمجة: الأدوات Toolchain وأول مشروعتثبيت Go 1.27 والفرق في صيغتها عما تعرفه، ثم أدواتها في أمر واحد وشكل المشروع، مع تجربة كاملة تبني حاسبة طلب باختباراتها وفحصها وملف تنفيذي واحد.](https://arabroot.io/articles/go-%D9%84%D9%85%D9%86-%D9%8A%D8%B9%D8%B1%D9%81-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%88%D8%A7%D9%88%D9%84-%D9%85%D8%B4%D8%B1%D9%88%D8%B9/)[![](https://arabroot.io/content/images/2026/10/memory-management-cover-cover.webp)دليلإدارة الذاكرة: يدوياً أم Garbage Collector أم Ownership؟نقيس بأيدينا في C وGo وPython وRust من يحرر الذاكرة ومتى، وكم يكلف ذلك من الذاكرة والوقت، وما الأخطاء التي تبقى ممكنة في كل طريقة.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%8A%D8%AF%D9%88%D9%8A%D8%A7-%D8%A7%D9%85-garbage-collector-%D8%A7%D9%85-ownership/)[![](https://arabroot.io/content/images/2026/10/error-handling-cover-cover.webp)دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجارب بالإصدار Go 1.27.3 داخل الـ Image الرسمية golang:1.27.3-trixie على Windows 11.