لنفرض أنك تكتب الكود منذ سنوات بلغة Python أو Java أو JavaScript، وتعمل في شركة ناشئة تبيع القهوة والتمر والهيل عبر الإنترنت، والمتجر يعمل بخير، ثم قرر الفريق أن خدمة الطلبات الجديدة سوف تكتب بلغة Go، والسبب الذي سمعته في الاجتماع أنها تخرج ملفاً تنفيذياً واحداً، وتستهلك ذاكرة قليلة، وتتعامل مع آلاف الطلبات المتزامنة دون تعقيد. وأنت لا تريد دورة للمبتدئين تشرح لك ما هو المتغير وما هي الحلقة، فهذا تعرفه جيداً، وإنما تريد أن تعرف في يوم واحد كيف تثبت اللغة بالطريقة الصحيحة، وما الذي يختلف في صيغتها عما تعرفه، وما هي الأدوات التي سوف تستخدمها كل يوم، وكيف يبدو المشروع الحقيقي من الداخل، حتى تستطيع أن تفتح أول Pull Request دون أن يعلق عليه زميلك بأن هذا «ليس أسلوب Go».
وهذا المقال هو الأول في مرحلة لغة Go للمطورين من مسار لغات البرمجة عملياً، والمقالات الخمسة في هذه المرحلة تتبع نفس القصة: في هذا المقال نجهز الأدوات ونكتب أول برنامج صغير يحسب إجمالي الطلب مع ضريبة القيمة المضافة، ثم نتعمق في الأنواع والذاكرة والأخطاء، ثم في التزامن، ثم نبني خدمة الطلبات كاملة مع PostgreSQL والاختبارات وDocker، ثم نختم بالأخطاء التي يقع فيها القادمون من لغات أخرى. وإذا كنت لم تقرر بعد هل Go هي اللغة المناسبة لمشروعك أصلاً، فقد قارنا بين أربع لغات بنفس الخدمة في مقال كيف تختار لغة البرمجة لمشروعك، وإذا كنت جديداً على البرمجة نفسها فابدأ من أساسيات البرمجة ثم عد إلى هنا.
وسوف نناقش في هذا المقال ما يلي:
- لماذا صممت Go، وما المشكلة التي جاءت لحلها في Google عام 2007، وما الذي تركته عن قصد.
- تثبيت الإصدار الحالي Go 1.27.3 بالطريقة الصحيحة، وكيف يبدل الأمر
goالإصدار وحده بحسب المشروع. - جولة سريعة في الصيغة Syntax مقارنة بما تعرفه: المتغيرات والقيمة الصفرية، والدوال بقيمتين، والحلقة الوحيدة، والـ struct بدل الكلاسات، والشرائح والخرائط، و
defer، والحرف الكبير بدلpublic. - الأدوات Toolchain كلها في أمر واحد: البناء والاختبار والتنسيق والفحص وإدارة الحزم، وما تحتاجه من خارجه.
- شكل المشروع Project Layout: الوحدة Module والحزمة Package والمجلد
internal. - تجربة عملية كاملة: حاسبة طلب صغيرة باختباراتها، ثم التنسيق والفحص والبناء لنظامين مختلفين، وإضافة مكتبة، والمشكلات التي واجهتنا.
- الأخطاء الشائعة في الأيام الأولى، وقائمة تحقق قبل أن تكتب أول خدمة.
لماذا صممت Go؟ المشكلة Problem التي جاءت لحلها
أفضل طريقة لتفهم أي لغة أن تعرف المشكلة التي صنعت من أجلها، لأن كل قرار غريب في صيغتها سوف يصبح منطقياً عندما تعرف ما كان يزعج من صمموها. وفي حالة Go فالقصة موثقة بشكل ممتاز في محاضرة Rob Pike بعنوان Go at Google: Language Design in the Service of Software Engineering، حيث يشرح أن اللغة ولدت في أواخر 2007 كإجابة على مشكلات كانت Google تعيشها يومياً: برامج سيرفرات ضخمة بلغة C++ وJava، يعمل عليها آلاف المطورين، وتبنى على عناقيد من الأجهزة، والبناء نفسه صار يأخذ دقائق طويلة وأحياناً ساعات. ويذكر Pike رقماً يشرح المشكلة كلها: في 2007 قاس مهندسو البناء في Google ترجمة برنامج كبير بلغة C++، فوجدوا أن حوالي ألفي ملف حجمها مجتمعة 4.2 ميجابايت تحولت بعد توسيع ملفات #include إلى أكثر من 8 جيجابايت تصل إلى المترجم، أي أن كل بايت من الكود صار ألفي بايت، وأن بناء هذا البرنامج أخذ 45 دقيقة.
ولذلك جلس ثلاثة مهندسين هم Robert Griesemer وRob Pike وKen Thompson، والأخير أحد صانعي Unix ولغة B التي سبقت C، وصمموا لغة هدفها الأول ليس جمال الصيغة، وإنما أن تجعل الفرق الكبيرة سريعة: ترجمة سريعة جداً لأن كل ملف يذكر ما يستورده بالضبط ولا يوجد ملفات header، واستيراد غير مستخدم يعتبر خطأ ترجمة حتى لا تتراكم الاعتماديات بلا سبب، وصيغة صغيرة يقرؤها أي مطور في الفريق دون أن يحتاج إلى معرفة عشرين ميزة متقدمة، وتنسيق واحد للكود تفرضه أداة، وتزامن جزء من اللغة لأن البرامج التي كانوا يكتبونها هي سيرفرات تخدم آلاف الاتصالات في نفس الوقت. وتجد قصة الأسماء والتواريخ وكثيراً من «لماذا لا توجد هذه الميزة؟» في الأسئلة الشائعة في موقع Go، وهي من أفضل ما تقرأه في يومك الأول، وقد روينا قصص صانعي اللغات بشكل أوسع في مقال قصص لغات البرمجة ومن صنعها.
وأصبحت اللغة مفتوحة المصدر في نوفمبر 2009، ثم صدر Go 1 في مارس 2012 ومعه وثيقة مهمة جداً لمن يكتب كوداً سوف يعيش سنوات، وهي وعد التوافق Go 1 Compatibility، والتي تقول إن البرنامج المكتوب لإصدار Go 1 سوف يستمر في الترجمة والعمل مع كل إصدارات Go 1 اللاحقة. ولهذا الوعد أثر عملي تراه كل يوم: الكود الذي كتب قبل عشر سنوات يترجم اليوم في الغالب دون تعديل، والترقية من إصدار إلى آخر حدث عادي وليس مشروعاً. وبعد ذلك بعشر سنوات جاءت أكبر إضافة على اللغة، وهي الأنواع العامة Generics في Go 1.18 عام 2022، ثم في الإصدار الحالي Go 1.27، الذي صدر في 19 أغسطس 2026، صار بإمكان الدالة المرتبطة بنوع Method أن تأخذ معاملات نوع خاصة بها، كما تذكر ملاحظات إصدار Go 1.27. وتصدر Go إصداراً رئيسياً كل ستة أشهر تقريباً، في فبراير وأغسطس، ويدعم الفريق آخر إصدارين رئيسيين بالإصلاحات الأمنية بحسب صفحة سجل الإصدارات، ولذلك الإصدار المناسب لمشروع جديد اليوم هو 1.27.3 الصادر في 19 أكتوبر 2026، وهو الإصدار الذي استخدمناه في كل التجارب.
والصورة التالية تجمع ما أخذته Go وما تركته عن قصد، وهي خريطة مفيدة لما سوف تفتقده من لغتك في الأسابيع الأولى:

وقد يتساءل البعض: إذا كانت اللغة تركت كل هذا، فلماذا يحبها الناس؟ والإجابة أن ما تخسره في التعبير تكسبه في القراءة، فالكود في Go يشبه بعضه في كل الشركات، وتستطيع أن تفتح مشروع Kubernetes أو Docker أو Prometheus، وكلها مكتوبة بلغة Go، وتقرأ ملفاً منها دون أن تحتاج إلى معرفة «أسلوب» فريقها. ولكن هذه البساطة لها ثمن حقيقي سوف تشعر به، وهو الكود المتكرر، خصوصاً في معالجة الأخطاء، وسوف نتحدث عنه بصراحة في المقال الثاني.
تثبيت Go: الأداة الرسمية Toolchain بالإصدار 1.27.3
هناك ثلاث طرق شائعة لتثبيت Go، ولكل منها مكان مختلف:
- مدير حزم التوزيعة مثل
apt install golang: الأسهل، ولكنه في الغالب يعطيك إصداراً قديماً، فالتوزيعات المستقرة تجمد الإصدار عند بداية دورتها، وقد تجد نفسك على إصدار لم يعد يحصل على إصلاحات أمنية، وبعض المكتبات الحديثة تطلب في ملفgo.modإصداراً أحدث مما لديك. لذلك لا ينصح بهذه الطريقة لجهاز التطوير. - الملف الرسمي من صفحة التنزيل: ملف مضغوط لنظام Linux أو macOS تفكه في
/usr/local/go، أو ملف تثبيت لنظام Windows، والخطوات في دليل التثبيت الرسمي. وهذه الطريقة الأنسب لجهازك، لأنك تعرف بالضبط أي إصدار لديك. - الـ Docker Image الرسمية
golang: مناسبة للتجارب ولخطوة البناء في Dockerfile، وهي التي استخدمناها هنا، والإصدار في الوسم نفسه، مثلgolang:1.27.3-trixieالمبنية على Debian 13.
وعلى Linux تكون خطوات الطريقة الثانية كما يلي، مع التأكد من بصمة الملف من نفس صفحة التنزيل قبل فكه:
curl -LO https://go.dev/dl/go1.27.3.linux-amd64.tar.gz
sha256sum go1.27.3.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.27.3.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.profileولاحظ أن الدليل الرسمي يطلب حذف المجلد القديم قبل فك الجديد، والسبب أن فك إصدار فوق آخر يترك ملفات قديمة تخلط بين الإصدارين بطرق غريبة. والمجلد $HOME/go/bin هو المكان الذي يضع فيه الأمر go install الأدوات التي تثبتها لاحقاً مثل staticcheck، لذلك أضفناه إلى المسار من البداية. وفي تجربتنا شغلنا كل شيء داخل Container من الـ Image الرسمية مع Volume لحفظ الـ Cache بين التشغيلات، والأمر كما يلي:
docker run --rm -v slj-go-cache:/root/.cache -v slj-go-mod:/go/pkg/mod \
-v "$PWD:/lab" -w /lab golang:1.27.3-trixie bash run.shوأول ما يقوم به السكربت هو أن يسأل عن الإصدار والإعدادات:
go version
go env GOROOT GOPATH GOOS GOARCH GOTOOLCHAIN GOFLAGS CGO_ENABLEDوالمخرج سوف يكون كما يلي:
go version go1.27.3 linux/amd64
/usr/local/go
/go
linux
amd64
local
1في المخرج أعلاه لاحظ التالي:
GOROOTهو مكان اللغة نفسها: المترجم والمكتبة القياسية Standard Library بكودها المصدري كاملاً، وتستطيع أن تفتح أي ملف فيها وتقرأ كيف كتبت، وهذا من أفضل طرق التعلم في Go.GOPATHهو مجلد العمل الخاص بك، وفيه أهم مجلدين:pkg/modحيث تنزل المكتبات، وbinحيث تثبت الأدوات. وفي الإصدارات القديمة قبل الوحدات Modules كان يجب أن يكون كودك نفسه داخلGOPATH/src، وهذا لم يعد مطلوباً منذ سنوات، فإذا قرأت دليلاً يطلب ذلك فهو دليل قديم.GOOSوGOARCHهما النظام والمعالج الذي يبني له المترجم، وتغييرهما وحده يكفي لبناء ملف لنظام آخر، كما سوف نرى في التجربة.GOTOOLCHAIN=local، وهذا إعداد الـ Image الرسمية، ومعناه ألا ينزل الأمرgoإصداراً آخر وحده. والقيمة الافتراضية على جهازك هيauto، وسوف نشرحها حالاً.CGO_ENABLED=1: يعني أن الكود يستطيع استدعاء مكتبات مكتوبة بلغة C، وهذا يحتاج مترجم C على الجهاز، وسوف نعود إليه عند الحديث عن الملف التنفيذي الثابت.
الإصدار في go.mod والتبديل التلقائي Toolchain Switching
منذ Go 1.21 صار السطر go 1.27 في ملف go.mod حداً أدنى ملزماً، وليس ملاحظة، فإذا كان على جهازك Go 1.26 وفتحت مشروعاً يطلب 1.27، فإن الأمر go بالإعداد auto ينزل الإصدار المطلوب من سيرفرات Go ويشغله بدلاً منه، وتشرح وثيقة الـ Toolchains هذه الآلية بالتفصيل، والسبب في تدوينة الإعلان عنها أن الفريق أراد أن يتوقف عن «ترجمة الكود بإصدار أقدم مما كتب له» دون أن يعرف أحد. وجربنا العكس داخل الـ Container، أي أن نطلب إصداراً أقدم صراحة:
GOTOOLCHAIN=go1.26.9 go version
GOTOOLCHAIN=local go versionوالمخرج سوف يكون كما يلي:
go: downloading go1.26.9 (linux/amd64)
go version go1.26.9 linux/amd64
go version go1.27.3 linux/amd64فالأمر نزل الإصدار 1.26.9 كاملاً وشغله، ثم عاد إلى الإصدار المثبت عندما طلبنا local. وهذه الآلية مفيدة جداً في الفرق، فالمشروع نفسه يحدد إصداره، ولكن انتبه إلى أمرين: الأول أن جهاز CI الذي لا يصل إلى الإنترنت سوف يفشل عند هذه الخطوة، لذلك ثبت فيه الإصدار الصحيح من البداية. والثاني أن رفع السطر go في مكتبة يستخدمها غيرك يجبرهم على الترقية، فلا ترفعه إلا عندما تحتاج ميزة من الإصدار الجديد فعلاً.
جولة سريعة في الصيغة Syntax مقارنة بلغتك
الصيغة في Go صغيرة عن قصد، ومواصفات اللغة Language Specification كلها صفحة واحدة تقرؤها في جلسة، وهذا ليس مبالغة، فهي أقصر بكثير من مواصفات Java أو C++. ولكي لا نكرر ما شرحناه عن أنماط البرمجة وأنظمة الأنواع في مقال أنماط البرمجة وأنظمة الأنواع، سوف نمر هنا على ما يختلف فعلاً عن لغتك، والجدول التالي يضع الأفكار جنباً إلى جنب:

المتغيرات والقيمة الصفرية Zero Value
في Go تكتب النوع بعد اسم المتغير وليس قبله، فالمتغير var total int يقرأ «المتغير total من نوع int»، والسبب الذي يذكره الفريق أن هذا الترتيب يبقى مقروءاً من اليسار إلى اليمين حتى مع الأنواع المعقدة مثل دالة ترجع دالة. ولكنك نادراً ما تكتب النوع، لأن الصيغة القصيرة total := 0 تعرف المتغير وتستنتج نوعه من القيمة، وهي الأكثر استخداماً داخل الدوال. والأهم من الصيغة فكرة القيمة الصفرية Zero Value: كل متغير في Go له قيمة من لحظة تعريفه، فالأرقام صفر، والنص فارغ ""، والقيمة المنطقية false، والمؤشرات والخرائط والشرائح nil، ولا توجد قيمة «غير معرفة» كما في JavaScript، ولا خطأ «متغير لم يعط قيمة» كما في Java. ولهذه الفكرة أثر في التصميم سوف تراه في كل مكتبة جيدة، وهو أن تجعل القيمة الصفرية للنوع صالحة للاستخدام مباشرة، فالمتغير var b strings.Builder جاهز للكتابة فيه، وvar mu sync.Mutex قفل جاهز دون أي دالة إنشاء.
الدوال Functions بقيمتين والخطأ Error
الدالة في Go تستطيع أن ترجع أكثر من قيمة، وأشهر استخدام لذلك أن ترجع النتيجة والخطأ معاً، كما في func Compute(lines []Line) (Totals, error). ولا توجد استثناءات Exceptions في Go، وإنما الخطأ قيمة عادية من النوع error، تكون nil إذا نجحت العملية، ويجب على من يستدعي الدالة أن يفحصها بنفسه بالسطر الذي سوف تراه آلاف المرات: if err != nil { return err }. وقد شرحنا الفرق بين الطريقتين وقسنا ثمن كل منهما بأربع لغات في مقال كيف تتعامل لغات البرمجة مع الأخطاء، ومنه أن فريق Go أعلن في 2025 أنه توقف عن البحث عن صيغة مختصرة لهذا السطر، أي أنه باق. والمقال الثاني في هذه المرحلة يتعمق في أنواع الأخطاء ومتى تستخدم panic.
حلقة واحدة for والشرط switch
لا توجد while ولا do في Go، وإنما for وحدها بأربعة أشكال: for i := 0; i < n; i++ الحلقة المعروفة، وfor n > 0 وهي while بلا اسمها، وfor وحدها وهي حلقة لا تنتهي إلا بـ break أو return، وfor i, p := range items للمرور على الشرائح والخرائط والنصوص والقنوات. ومنذ Go 1.22 صار متغير الحلقة جديداً في كل دورة، وهذا أغلق خطأً قديماً مشهوراً كانت فيه الـ Goroutines داخل الحلقة ترى كلها آخر قيمة، كما تشرح تدوينة تغيير متغير الحلقة، لذلك إذا قرأت كوداً قديماً فيه السطر الغريب p := p داخل الحلقة فهذا هو سببه. أما switch فلا يحتاج break في نهاية كل حالة، لأن الحالة لا تنزل إلى التي تحتها إلا إذا كتبت fallthrough صراحة، وهذا عكس C وJava وJavaScript، ويستطيع أن يعمل بلا متغير أصلاً كبديل مرتب لسلسلة من if. والشرط if يقبل جملة قبل الشرط، مثل if err := f(); err != nil، فيبقى المتغير err محصوراً في هذا الشرط.
الـ struct والدوال المرتبطة Methods بدل الكلاسات
لا توجد كلاسات Classes في Go، وإنما تعرف نوعاً من البيانات بالكلمة struct، ثم تربط به دوالاً بكتابة المستقبل Receiver قبل اسم الدالة، مثل func (p *Product) Reserve(n int). والنجمة قبل النوع تعني أن الدالة تعمل على الأصل عبر مؤشر Pointer، وبدونها تعمل على نسخة، وهذا الفرق هو أول خطأ يقع فيه القادم من Java أو Python، لأن الدالة بدون النجمة تعدل النسخة وتترك الأصل كما هو دون أي رسالة، وسوف نراه بالمخرج في المقال الثاني. ولا توجد وراثة Inheritance، وبدلها التضمين Embedding، أي أن تضع نوعاً داخل نوع فتظهر حقوله ودواله في النوع الخارجي، والواجهات Interfaces تتحقق ضمنياً: أي نوع له الدوال المطلوبة يحقق الواجهة دون أن يعلن ذلك بكلمة مثل implements.
الشرائح Slices والخرائط Maps
المصفوفة Array في Go حجمها جزء من نوعها، فالنوع [3]int يختلف عن [4]int، ولذلك نادراً ما تستخدمها مباشرة، وإنما تستخدم الشريحة Slice، وهي نافذة على مصفوفة: مؤشر إلى بدايتها وطول Length وسعة Capacity، وتكبر بالدالة append التي تنسخ البيانات إلى مصفوفة أكبر عندما تمتلئ السعة. وهذا التصميم يفسر أشياء سوف تفاجئك، مثل أن شريحتين قد تشيران إلى نفس البيانات فيغير تعديل إحداهما الأخرى، أو أن شريحة صغيرة تمسك مصفوفة كبيرة في الذاكرة، وهي المشكلة التي قسناها بالأرقام في قسم التسرب من مقال إدارة الذاكرة. أما الخريطة map[string]int فهي جدول هاش Hash Table عادي، والقراءة من خريطة nil ترجع القيمة الصفرية دون خطأ، ولكن الكتابة فيها توقف البرنامج، ولذلك تنشئها بـ make أو بقيمة حرفية قبل الكتابة. وترتيب المرور على الخريطة عشوائي عن قصد في كل تشغيل، حتى لا يعتمد أحد على ترتيب لم تعد به اللغة.
الأمر defer للتنظيف Cleanup
بدلاً من finally في Java وJavaScript أو with في Python، تكتب في Go السطر defer f.Close() مباشرة بعد فتح الملف، فيؤجل التنفيذ إلى لحظة خروج الدالة، مهما كان طريق الخروج: return عادي أو خطأ أو حتى panic. وميزة هذا الأسلوب أن الفتح والإغلاق متجاوران في الكود، فلا تنسى الإغلاق عندما تضيف return جديداً في منتصف الدالة بعد سنة. وتنفذ الأوامر المؤجلة بترتيب عكسي، آخرها أولها، وقيم المعاملات تحسب لحظة كتابة defer وليس لحظة التنفيذ، وهذه النقطة الأخيرة مصدر خطأ كلاسيكي عندما تؤجل طباعة متغير ثم تغيره.
الحرف الكبير بدل public أو private Visibility
لا توجد كلمات public وprivate في Go، وإنما يحدد الحرف الأول من الاسم كل شيء: الاسم الذي يبدأ بحرف كبير مثل Compute مصدر Exported تراه الحزم الأخرى، والذي يبدأ بحرف صغير مثل round لا يراه أحد خارج حزمته. وهذا ينطبق على الدوال والأنواع والحقول والثوابت، وله أثر لا يتوقعه القادم من لغات أخرى: حقل الـ struct الذي يبدأ بحرف صغير لا تراه مكتبة encoding/json أيضاً، لأنها حزمة أخرى، فيخرج الكائن في JSON فارغاً دون أي خطأ، وهذا من أشهر أسئلة الأسبوع الأول.
وفي هذه الجولة تركنا عن قصد أهم ميزتين في اللغة، وهما الواجهات والتزامن، لأن لكل منهما مقالاً كاملاً في هذه المرحلة، فالأولى في المقال الثاني، والـ Goroutines والقنوات Channels في المقال الثالث، وفيه نقيس نفس المهمة بالتسلسل والتزامن بالأرقام.
أدوات اللغة Tools في أمر واحد
في Python تحتاج pip أو uv للحزم، وpytest للاختبار، وBlack أو Ruff للتنسيق، وMyPy للفحص، وفي Java تحتاج Maven أو Gradle، وفي JavaScript npm وJest وPrettier وESLint، وقد شرحنا هذا التنوع في مقالي مديرو الحزم وأدوات البناء. أما في Go فأغلب هذا كله أمر واحد اسمه go، ومعه أداة التنسيق gofmt، والصورة التالية تجمع الأوامر التي سوف تستخدمها كل يوم:

وأهم ما يجب أن تعرفه عن هذه الأدوات:
- المترجم Compiler:
go buildيترجم ويربط ويخرج ملفاً واحداً، وgo runيفعل نفس الشيء في مجلد مؤقت ثم يشغل البرنامج، وهو للتجربة فقط وليس للإنتاج. وكلاهما يحفظ نتيجة ترجمة كل حزمة في الـ Build Cache، فلا يعيد ترجمة ما لم يتغير، وقد رأينا في مقال كيف تختار لغة البرمجة ماذا يحدث داخل Dockerfile إذا ضاع هذا الـ Cache مع كل بناء. وإذا أردت أن تفهم ما يحدث بين الكود والملف التنفيذي، من التحليل إلى توليد كود الآلة، فقد شرحناه في مقال المترجم والمفسر وJIT. - الاختبار Test Runner:
go testيبحث عن الملفات التي تنتهي بـ_test.goويشغل كل دالة تبدأ بـTest، والإطار نفسه هو الحزمة القياسية testing، ومعه قياس الأداء Benchmarks وتغطية الكود Coverage وكاشف السباقات Race Detector. - المنسق Formatter:
gofmtله شكل واحد ولا يقبل أي إعدادات، ويقول Rob Pike في تدوينة gofmt إن أسلوب gofmt لا يعجب أحداً بالكامل، ولكنه يعجب الجميع لأنه أنهى الجدال، وهذا صحيح، فلن تقضي وقتاً في مراجعة الكود على المسافات والأقواس أبداً. - الفاحص Vet: go vet يبحث عن أخطاء يقبلها المترجم ولكنها في الغالب خطأ، مثل نوع خاطئ في
Printf، أو نسخ قفل بالقيمة، أو كود لا يصل إليه التنفيذ. ويشغلgo testجزءاً منه تلقائياً قبل كل اختبار. - الحزم Modules:
go mod initينشئ الوحدة، وgo getيضيف مكتبة أو يغير إصدارها، وgo mod tidyيجعل ملفgo.modمطابقاً لما يستورده الكود فعلاً، والمرجع الكامل في مرجع الوحدات Go Modules Reference.
وهناك أدوات خارج الأمر go لا يستغني عنها فريق جاد: gopls وهو خادم اللغة Language Server الرسمي الذي يعطي VS Code وNeovim وGoLand الإكمال والانتقال إلى التعريف وإعادة التسمية، وstaticcheck وهو أشهر فاحص ثابت Static Analyzer أعمق من go vet، وgolangci-lint الذي يجمع عشرات الفاحصات ومنها staticcheck في أمر واحد مع ملف إعداد، وهو الأكثر استخداماً في خطوط CI، وgovulncheck الأداة الرسمية التي تبحث في قاعدة بيانات الثغرات عن ثغرات معروفة في المكتبات التي يستدعيها كودك فعلاً، وليس في كل ما هو مذكور في go.mod، وهذا يقلل الإنذارات الكاذبة كثيراً.
شكل المشروع Project Layout: الوحدة Module والحزمة Package
أكثر ما يربك القادم من Java أن مشروع Go لا يحتاج أي بنية مجلدات مفروضة، لا يوجد src/main/java ولا setup.py، وإنما مفهومان فقط، والصورة التالية تبين مشروع التجربة:

- الوحدة Module: مجلد فيه ملف
go.mod، والسطر الأول فيه اسم الوحدة مثلmodule example.com/store، وهذا الاسم هو بداية مسار كل استيراد داخلها. وإذا كانت مكتبة ينشرها الآخرون فاسمها هو عنوان مستودعها مثلgithub.com/jackc/pgx/v5، والسبب أن الأمرgoينزل المكتبة من هذا العنوان نفسه. - الحزمة Package: كل مجلد حزمة واحدة، وكل الملفات فيه تبدأ بنفس السطر
package money، وترى بعضها مباشرة دون استيراد. والحزمةmainمع الدالةmain()هي التي تصبح برنامجاً تنفيذياً. - المجلد
internal: هذا ليس عرفاً، وإنما قاعدة يفرضها المترجم، فالحزم تحته لا يستطيع استيرادها إلا الكود الذي يقع داخل المجلد الأب لها، لذلك تضع فيه ما لا تريد أن يعتمد عليه غيرك. - المجلد
cmd: عرف شائع لوضع كل برنامج تنفيذي في مجلد باسمه، فإذا كان للمشروع خدمة وأداة سطر أوامر صار لديكcmd/ordersوcmd/migrate.
وقد يتساءل البعض: ألا يوجد شكل «قياسي» للمشروع؟ ستجد في البحث مستودعاً مشهوراً باسم golang-standards/project-layout فيه عشرات المجلدات مثل pkg وapi وconfigs، ولكن انتبه إلى أنه ليس من فريق Go رغم اسمه، ويذكر هو نفسه في أوله أنه «ليس معياراً رسمياً». والمرجع الرسمي هو صفحة تنظيم وحدة Go التي تبدأ بأبسط شكل، ملف واحد في مجلد واحد، ثم تضيف internal وcmd عندما تحتاجهما. والقاعدة العملية أن تبدأ بأقل عدد من الحزم، وأن تقسم عندما يصبح للحزمة اسم واضح ومسؤولية واحدة، وليس قبل ذلك، والسبب أن كل حزمة في Go حدود حقيقية: ما يبدأ بحرف صغير لا يعبرها، والاعتماد الدائري Import Cycle بين حزمتين خطأ ترجمة، فالتقسيم المبكر يجبرك على تصدير أشياء لم تكن تريد تصديرها.
التجربة العملية Lab: حاسبة الطلب pricecalc
الآن نعود إلى قصتنا، فأول قطعة يحتاجها فريق خدمة الطلبات هي الدالة التي تحسب إجمالي الطلب مع ضريبة القيمة المضافة 15%، وهي نفس الدالة التي سوف تعيش داخل الخدمة الحقيقية في المقال الرابع. والقاعدة الأولى في أي حساب مالي أن تخزن المبالغ بأصغر وحدة كأرقام صحيحة، أي بالهللة، والسبب أن الأرقام العشرية Floating Point لا تمثل 0.1 بدقة، وقد شرحنا ذلك من الداخل في مقال الأرقام العشرية Floating Point، ورأينا في مقال اختيار اللغة كيف اختلفت هللة واحدة بين لغتين بسبب التقريب. لذلك نحسب الضريبة على المجموع بالهللات، ونقرب النصف إلى الأعلى بمعادلة صحيحة: نضرب في 15، ونضيف 50، ثم نقسم على 100.
الكود: الحزمة money والبرنامج pricecalc
أنشأنا الوحدة بالأمر go mod init example.com/store، فأصبح ملف go.mod كما يلي:
module example.com/store
go 1.27ثم الحزمة money في الملف internal/money/money.go:
// Package money keeps every amount in integer halalas (1 SAR = 100 halalas).
package money
import "fmt"
// VATRate is the Saudi VAT rate in percent.
const VATRate = 15
// Line is one product line of an order.
type Line struct {
PriceHalalas int
Qty int
}
// Totals is the result of Compute.
type Totals struct {
Subtotal int
VAT int
Total int
}
// VAT returns 15% of subtotal in halalas, rounded half up.
func VAT(subtotal int) int {
return (subtotal*VATRate + 50) / 100
}
// Compute adds the lines, then the VAT on the subtotal.
func Compute(lines []Line) (Totals, error) {
var t Totals
for i, l := range lines {
if l.Qty <= 0 {
return Totals{}, fmt.Errorf("line %d: qty must be > 0, got %d", i, l.Qty)
}
t.Subtotal += l.PriceHalalas * l.Qty
}
t.VAT = VAT(t.Subtotal)
t.Total = t.Subtotal + t.VAT
return t, nil
}
// Format prints halalas as riyals: 3416 -> "34.16 SAR".
func Format(h int) string {
return fmt.Sprintf("%d.%02d SAR", h/100, h%100)
}في الكود أعلاه لاحظ التالي:
- التعليق فوق كل اسم مصدر يبدأ باسمه، مثل
// VAT returns…، وهذا عرف تعتمد عليه أداةgo docوموقع pkg.go.dev لتوليد التوثيق، ويطلبه دليل Effective Go. - الدالة
Computeترجع قيمتين، وعند الخطأ ترجعTotals{}وهي القيمة الصفرية للنوع، مع خطأ يحمل رقم السطر والقيمة الخاطئة، لأن رسالة مثل «invalid input» وحدها لا تساعد أحداً في السجل. - الحلقة
for i, l := range linesتعطي الموقع والقيمة، والقيمةlنسخة من العنصر وليست مرجعاً إليه. - الدالة
Formatتستخدم%02dلتطبع الهللات برقمين دائماً، فيظهر 34.06 وليس 34.6.
ثم البرنامج نفسه في cmd/pricecalc/main.go، ويأخذ أسطر الطلب من سطر الأوامر بصيغة السعر بالهللة ثم الكمية:
// pricecalc prints the total of an order given as price:qty pairs.
//
// pricecalc 990:3 4500:1
package main
import (
"fmt"
"os"
"strconv"
"strings"
"example.com/store/internal/money"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: pricecalc price:qty ...")
os.Exit(2)
}
var lines []money.Line
for _, arg := range os.Args[1:] {
price, qty, ok := strings.Cut(arg, ":")
if !ok {
fmt.Fprintf(os.Stderr, "bad item %q, want price:qty\n", arg)
os.Exit(2)
}
p, err1 := strconv.Atoi(price)
q, err2 := strconv.Atoi(qty)
if err1 != nil || err2 != nil {
fmt.Fprintf(os.Stderr, "bad numbers in %q\n", arg)
os.Exit(2)
}
lines = append(lines, money.Line{PriceHalalas: p, Qty: q})
}
t, err := money.Compute(lines)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Println("subtotal:", money.Format(t.Subtotal))
fmt.Println("vat 15%: ", money.Format(t.VAT))
fmt.Println("total: ", money.Format(t.Total))
}ولاحظ هنا أمرين: الأول أن الاستيراد يكون بالمسار الكامل example.com/store/internal/money ثم يستخدم باسم الحزمة الأخير money، والثاني أن الدالة strings.Cut ترجع ثلاث قيم، والثالثة منطقية تقول هل وجد الفاصل أم لا، وهذا نمط شائع في المكتبة القياسية بدل رمي الاستثناء.
التشغيل go run والقيمة الخاطئة
الآن نجرب الطلب الذي سوف نعود إليه في كل مقالات المسار: ثلاثة أكياس هيل سعر الواحد 9.90 ريال:
go run ./cmd/pricecalc 990:3
go run ./cmd/pricecalc 990:0; echo "exit=$?"والمخرج سوف يكون كما يلي:
subtotal: 29.70 SAR
vat 15%: 4.46 SAR
total: 34.16 SAR
error: line 0: qty must be > 0, got 0
exit status 1
exit=1المجموع 29.70 ريال، والضريبة الدقيقة 4.455 ريال، أي 445.5 هللة، ومعادلة التقريب إلى الأعلى أعطت 446 هللة، فالإجمالي 34.16 ريال. وفي التشغيل الثاني رجعت الدالة الخطأ، فطبع البرنامج الرسالة وخرج بالرمز 1، ولاحظ السطر exit status 1، وهو من الأمر go run نفسه وليس من برنامجنا، فهو يخبرك أن البرنامج الذي شغله خرج برمز غير صفري.
الاختبار go test والتغطية Coverage
والاختبار في نفس المجلد في ملف money_test.go، بنمط اسمه الاختبارات الجدولية Table-Driven Tests، وهو الأسلوب الأكثر شيوعاً في Go:
package money
import "testing"
func TestCompute(t *testing.T) {
tests := []struct {
name string
lines []Line
want Totals
}{
{"three cardamom bags", []Line{{990, 3}}, Totals{2970, 446, 3416}},
{"coffee and dates", []Line{{4500, 1}, {3800, 2}}, Totals{12100, 1815, 13915}},
{"empty order", nil, Totals{0, 0, 0}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := Compute(tc.lines)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tc.want {
t.Errorf("Compute() = %+v, want %+v", got, tc.want)
}
})
}
}
func TestComputeRejectsZeroQty(t *testing.T) {
if _, err := Compute([]Line{{990, 0}}); err == nil {
t.Fatal("expected an error for qty 0")
}
}
func TestFormat(t *testing.T) {
if got := Format(3416); got != "34.16 SAR" {
t.Errorf("Format(3416) = %q", got)
}
}ثم نشغله بالخيار -v لنرى كل حالة:
go test -v ./...
go test -cover ./...والمخرج سوف يكون كما يلي:
? example.com/store/cmd/pricecalc [no test files]
=== RUN TestCompute
=== RUN TestCompute/three_cardamom_bags
=== RUN TestCompute/coffee_and_dates
=== RUN TestCompute/empty_order
--- PASS: TestCompute (0.00s)
--- PASS: TestCompute/three_cardamom_bags (0.00s)
--- PASS: TestCompute/coffee_and_dates (0.00s)
--- PASS: TestCompute/empty_order (0.00s)
=== RUN TestComputeRejectsZeroQty
--- PASS: TestComputeRejectsZeroQty (0.00s)
=== RUN TestFormat
--- PASS: TestFormat (0.00s)
PASS
ok example.com/store/internal/money 0.132s
example.com/store/cmd/pricecalc coverage: 0.0% of statements
ok example.com/store/internal/money 1.078s coverage: 100.0% of statementsفي المخرج أعلاه لاحظ التالي:
- النمط
./...يعني هذه الحزمة وكل ما تحتها، لذلك ظهرت الحزمةcmd/pricecalcمع الملاحظة[no test files]. - كل حالة في الجدول ظهرت كاختبار فرعي Subtest باسمها، والمسافات في الاسم تحولت إلى شرطة سفلية، وتستطيع تشغيل حالة واحدة بالخيار
-run 'TestCompute/three_cardamom'. - الرقم 0.132s هو زمن تشغيل ملف الاختبار كاملاً، والاختبارات نفسها أخذت أقل من جزء من المللي ثانية كما يظهر في
(0.00s)، وارتفع الرقم إلى 1.078s مع-coverلأن الكود صار يحمل عدادات التغطية والجهاز كان مشغولاً. - التغطية 100% للحزمة
money، وصفر للبرنامج لأنه لا يوجد له اختبار. ولا تجعل 100% هدفاً في كل مكان، ولكن الحساب المالي من الأماكن التي يستحق فيها كل سطر اختباراً، وقد شرحنا أنواع الاختبارات ومتى تستخدم كلاً منها في مقال أنواع الاختبارات.
ولاحظ أن هذا المقال لا يحتاج أي مكتبة خارجية للاختبار، فلا يوجد assert ولا expect، وإنما if got != want ثم t.Errorf، وهذا مقصود في ثقافة Go، والسبب الذي يذكره الفريق أن رسالة الخطأ التي تكتبها بنفسك تقول بالضبط ماذا كان متوقعاً وماذا ظهر، كما في Compute() = %+v, want %+v. ومع ذلك ستجد مكتبة testify في كثير من المشاريع، ولا بأس بها إذا اتفق عليها الفريق.
التنسيق gofmt والفحص go vet
لكي نرى ما يفعله المنسق كتبنا ملفاً بشكل سيء عن قصد، بلا مسافات ولا إزاحة، ثم طلبنا من gofmt أن يعرض الفرق دون أن يغير الملف:
gofmt -d fmtme/main.goوالمخرج سوف يكون كما يلي:
diff fmtme/main.go.orig fmtme/main.go
--- fmtme/main.go.orig
+++ fmtme/main.go
@@ -1,6 +1,10 @@
package main
+
import "fmt"
-func main(){
-prices:=[]int{990,4500}
-for i,p:=range prices{ fmt.Println(i,p) }
+
+func main() {
+ prices := []int{990, 4500}
+ for i, p := range prices {
+ fmt.Println(i, p)
+ }
}المنسق أضاف سطراً فارغاً بعد package وبعد الاستيراد، ووضع المسافات حول := وبعد الفواصل، وأزاح الأسطر بعلامة Tab وليس بمسافات، وفتح القوس في نفس سطر الحلقة. والأخيرة ليست ذوقاً، وإنما ضرورة، لأن المترجم يضيف الفاصلة المنقوطة تلقائياً في نهاية السطر، فلو وضعت القوس في السطر التالي لفهمه المترجم بشكل خاطئ. وفي المشروع الحقيقي لا تشغل هذا الأمر بيدك، وإنما تجعل المحرر يشغله عند كل حفظ، وتجعل خطوة في CI تفشل إذا أخرج gofmt -l . أي ملف.
ثم الفاحص، وكتبنا له خطأً شائعاً، وهو الدالة Printf مع ترتيب معاملات خاطئ، اسم المنتج مكان الرقم والكمية مكان النص:
package main
import "fmt"
func main() {
name := "هيل حب 100 غرام"
qty := 3
fmt.Printf("product %d qty %s\n", name, qty)
}والمترجم يقبل هذا الكود دون اعتراض، لأن Printf تقبل أي قيم، وعند التشغيل ثم الفحص يكون المخرج كما يلي:
product %!d(string=هيل حب 100 غرام) qty %!s(int=3)
vet/main.go:8:22: fmt.Printf format %d has arg name of wrong type string
exit=1فالسطر الأول هو ما طبعه البرنامج فعلاً، والعلامة %!d(string=…) هي طريقة Go لتقول إن الفعل %d جاءه نص، فهي لا توقف البرنامج، وإنما تطبع هذا الشكل الغريب في السجل، وقد يمر أسابيع قبل أن يلاحظه أحد. والسطر الثاني من go vet، ويحدد الملف والسطر والعمود والسبب، ويخرج برمز غير صفري فيوقف خط CI.
الفحص الأعمق staticcheck ثم govulncheck
وهناك أخطاء لا يراها go vet، مثل هذا الملف:
package main
import (
"fmt"
"strings"
)
func main() {
sku := " hl-100 "
strings.TrimSpace(sku)
if sku == "hl-100" {
fmt.Println("found")
}
fmt.Println(strings.Title(sku))
}والخطأ هنا أن strings.TrimSpace لا تعدل النص، وإنما ترجع نصاً جديداً، لأن النصوص في Go ثابتة Immutable، فالشرط بعدها لن يتحقق أبداً. وشغلنا go vet فخرج برمز صفر دون أي ملاحظة، ثم ثبتنا staticcheck وشغلناه:
go install honnef.co/go/tools/cmd/[email protected]
staticcheck ./staticوالمخرج سوف يكون كما يلي:
go: downloading golang.org/x/sync v0.23.0
go: downloading golang.org/x/mod v0.41.0
staticcheck 2026.2.2 (0.8.2)
static/main.go:10:2: TrimSpace doesn't have side effects and its return value is ignored (SA4017)
static/main.go:14:14: strings.Title has been deprecated since Go 1.18 and an alternative has been available since Go 1.0: The rule Title uses for word boundaries does not handle Unicode punctuation properly. Use golang.org/x/text/cases instead. (SA1019)
exit=1فالأداة وجدت مشكلتين، لكل منهما رمز تبحث عنه في قائمة الفحوصات: SA4017 للقيمة المرجعة المهملة من دالة ليس لها أثر جانبي، وSA1019 لاستخدام دالة قديمة Deprecated، فالدالة strings.Title لا تتعامل مع حدود الكلمات في Unicode بشكل صحيح، والبديل في الحزمة golang.org/x/text/cases. وعندما شغلنا البرنامج طبع Hl-100 بالمسافات، ولم يطبع found، وهذا تأكيد للخطأ الأول.
ثم govulncheck، وهي الأداة الرسمية للثغرات، وشغلناها على مشروعنا:
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...والمخرج كان No vulnerabilities found.، وهذا متوقع في مشروع لا يستخدم إلا المكتبة القياسية من أحدث إصدار. ولكن قيمة هذه الأداة تظهر عندما يصبح للمشروع عشرات المكتبات، فهي لا تكتفي بأن تقول إن المكتبة فيها ثغرة، وإنما تتبع الاستدعاءات وتقول هل الدالة المصابة يستدعيها كودك فعلاً، كما يشرح توثيق إدارة الثغرات في Go، وهذا يجعل تقريرها قصيراً وقابلاً للتنفيذ، بدل قائمة بمئات الإنذارات لا يقرؤها أحد.
البناء go build: ملف واحد بلا مكتبات Static Binary
الآن نبني البرنامج كما سوف نوزعه، ثم نبنيه مرة ثانية مع حذف رموز التصحيح، ثم نسأل هل يعتمد على مكتبات النظام، ثم نبنيه لنظام Windows على معالج ARM من نفس الجهاز:
go build -o /tmp/pricecalc ./cmd/pricecalc
go build -ldflags="-s -w" -o /tmp/pricecalc-s ./cmd/pricecalc
ldd /tmp/pricecalc
GOOS=windows GOARCH=arm64 go build -o /tmp/pricecalc.exe ./cmd/pricecalcوالصورة التالية تجمع ما خرج من هذه الأوامر:

والأرقام كما ظهرت في المخرج:
real 0m9.664s
user 0m0.515s
sys 0m0.964s
-rwxr-xr-x 1 root root 2399204 Oct 31 07:14 /tmp/pricecalc
-rwxr-xr-x 1 root root 1548448 Oct 31 07:14 /tmp/pricecalc-s
not a dynamic executable
-rwxr-xr-x 1 root root 2391552 Oct 31 07:15 /tmp/pricecalc.exeفي المخرج أعلاه لاحظ التالي:
- الملف 2.4 ميجابايت تقريباً لبرنامج من خمسين سطراً، والسبب أنه يحمل داخله الـ Runtime كاملاً: جامع القمامة، والمجدول Scheduler الذي يدير الـ Goroutines، وكود الحزم
fmtوstrconvوos. وهذا الحجم ثابت تقريباً، فالخدمة الكاملة في المقال الرابع لن تكون أكبر بعشرة أضعاف. - الخيار
-s -wيحذف جدول الرموز ومعلومات التصحيح DWARF، فنزل الحجم إلى 1.55 ميجابايت، ولكنك تخسر أسماء الدوال عند استخدام المصحح Debugger، أما رسائلpanicفتبقى بأسماء الدوال لأن الـ Runtime يحتفظ بجدول خاص بها. not a dynamic executableتعني أن الملف لا يحتاج أي مكتبة من النظام، ولا حتىlibc، فتستطيع أن تنسخه إلى أي جهاز Linux على نفس المعالج وتشغله، أو تضعه وحده في Docker Image فارغة تقريباً. ولكن انتبه إلى أن هذا صحيح لأن برنامجنا لا يستخدم حزماً تستدعي C، فإذا استخدمت مكتبة تعتمد على cgo، أو بعض الحالات في الحزمتينnetوos/user، صار الملف يعتمد علىlibc، والحل المعتاد في البناء للحاويات هوCGO_ENABLED=0.- البناء لنظام Windows على ARM خرج من نفس الـ Container دون أي أداة إضافية، فقط بالمتغيرين
GOOSوGOARCH، وهذه من أكبر الأسباب التي جعلت أدوات سطر الأوامر مثل Docker CLI وTerraform وkubectl تكتب بلغة Go.
أما الأمر go list std | wc -l فأعاد 383، وهو عدد الحزم في المكتبة القياسية لهذا الإصدار، وفيها سيرفر HTTP كامل وتشفير وJSON وقواعد بيانات بواجهة database/sql واختبار وقياس أداء، وهذا سبب أن كثيراً من خدمات Go لا تستخدم إطار عمل Framework أصلاً، كما سوف نرى في المقال الرابع.
إضافة مكتبة Dependency: go get ثم go mod tidy
الآن نضيف مكتبة خارجية لنرى ما يحدث في الملفات، وهي golang.org/x/text بإصدار محدد:
go get golang.org/x/[email protected]
cat go.mod; head -4 go.sum
go mod tidy
cat go.modوالمخرج سوف يكون كما يلي:
go: downloading golang.org/x/text v0.30.0
go: added golang.org/x/text v0.30.0
module example.com/store
go 1.27
require golang.org/x/text v0.30.0 // indirect
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
# go mod tidy; cat go.mod
module example.com/store
go 1.27
go.sumفي المخرج أعلاه لاحظ التالي:
- أضاف
go getالسطرrequireمع العلامة// indirect، والسبب أن كودنا لا يستورد هذه المكتبة بعد، فهي في نظر الأداة اعتمادية غير مباشرة. - الملف
go.sumفيه بصمتان لكل إصدار: بصمة الكود كاملاً، وبصمة ملفgo.modالخاص بالمكتبة. وهذه البصمات تتحقق منها الأداة مع قاعدة بصمات عامة يديرها فريق Go، كما يشرح قسم قاعدة البصمات Checksum Database، فإذا غير أحد كود إصدار منشور على GitHub بعد نشره رفضت الأداة تنزيله. وهذه حماية لا تجدها افتراضياً في كل مديري الحزم. go mod tidyحذف السطر كله، لأن الكود لا يستورد المكتبة، وهذا ما تريده دائماً قبل الـ Commit، وبقي الملفgo.sumفي مكانه كما يظهر في السطر الأخير.
وقد يتساءل البعض: أين ملف القفل Lock File مثل package-lock.json؟ والإجابة أن Go لا تحتاجه، لأنها تستخدم خوارزمية اسمها أقل إصدار ممكن Minimal Version Selection، فإذا طلبت مكتبتك الإصدار 1.2 من مكتبة، وطلبت مكتبة أخرى 1.4 منها، اختارت الأداة 1.4 بالضبط، وليس أحدث إصدار منشور، والنتيجة نفسها في كل جهاز وفي كل يوم ما دام go.mod لم يتغير. ويشرح Russ Cox الفكرة وسببها في مقاله Minimal Version Selection، ومن نتائجها أن مكتبة تنشر إصداراً جديداً سيئاً لا تصل إلى مشروعك وحدها، وإنما عندما ترفع الإصدار بنفسك.
مشكلات Problems واجهتنا أثناء التجربة
ثلاث مشكلات صغيرة ظهرت أثناء التجربة، وكل واحدة منها سوف تقابلك في أسبوعك الأول:
- عندما أردنا أن نرى رسالة المترجم عن الاستيراد غير المستخدم، كتبنا
go build ./unusedفظهر خطأ لا علاقة له بالكود:go: build output "unused" already exists and is a directory. والسبب أنgo buildلحزمةmainواحدة يكتب الملف التنفيذي في المجلد الحالي باسم آخر جزء من المسار، وهنا اسم المجلد نفسه. والحل أن تحدد المخرج بالخيار-o، ونحن كتبنا-o /dev/nullلأننا نريد رسائل الترجمة فقط. - ثبتنا
staticcheckبالإصدار2026.1.1من الذاكرة، فرفض الأمر بالرسالةinvalid version: unknown revision 2026.1.1، لأن هذا الإصدار لم يصدر أصلاً. ولاحظنا بعدها أن المشروع يعطي إصداراته أسماء بالسنة مثل 2026.2.2، بينما رقم الوحدة الحقيقي v0.8.2، كما ظهر في المخرج. لذلك خذ رقم الإصدار من صفحة الإصدار وثبته في ملف إعداد CI، ولا تستخدم@latestهناك حتى لا يتغير الفاحص وحده في يوم لم يغير فيه أحد شيئاً. - البناء الأول أخذ 9.66 ثانية بالوقت الحقيقي، بينما وقت المعالج كله 0.52 ثانية، ثم أخذ البناء الثاني 0.24 ثانية. فالفرق ليس ترجمة، وإنما انتظار القرص، لأن الكود في مجلد مشترك بين Windows والـ Container، والكتابة إلى
/tmpوقراءة المجلد المشترك بطيئة في Docker Desktop، والجهاز كان يشغل أعمالاً أخرى. والدرس أن تقرأuserوsysبجانبrealقبل أن تحكم على سرعة أي أداة.
ماذا يحدث إذا تركت متغيراً أو استيراداً غير مستخدم Unused؟
في أغلب اللغات يكون المتغير غير المستخدم تحذيراً يتجاهله الجميع، أما في Go فهو خطأ يوقف البناء، والملف التالي فيه الحالتان:
package main
import (
"fmt"
"os"
)
func main() {
total := 3416
fmt.Println("done")
}وعند بنائه يكون المخرج كما يلي:
# example.com/broken/unused
unused/main.go:5:2: "os" imported and not used
unused/main.go:9:2: declared and not used: total
exit=1والسبب كما يذكر الفريق في الأسئلة الشائعة أن الاستيراد غير المستخدم يبطئ الترجمة ويضيف اعتماديات بلا سبب، والمتغير غير المستخدم في الغالب علامة على خطأ، مثل نتيجة حسبتها ونسيت أن ترجعها. وهذا مزعج في البداية عندما تعلق سطراً لتجربة شيء، فيرفض المترجم البناء بسبب استيراد صار غير مستخدم، والحل العملي أن تجعل المحرر يشغل goimports أو gopls عند الحفظ، فيضيف الاستيرادات ويحذفها وحده. ولاحظ أن الشرطة السفلية _ تتجاوز هذه القاعدة، فالسطر _ = total يرضي المترجم، ولا تقم بذلك في كود حقيقي إلا لسبب مكتوب، لأنه يخفي بالضبط ما أرادت القاعدة كشفه.
أخطاء شائعة Common Mistakes في الأيام الأولى
- تثبيت Go من مدير حزم التوزيعة ثم الاستغراب من رسالة
go.mod requires go >= 1.27. ثبت الإصدار الرسمي، واتركGOTOOLCHAINعلىautoفي جهازك. - نسخ بنية مجلدات ضخمة من مستودع مشهور قبل أن يكون لديك ملف واحد. ابدأ بحزمة واحدة، ثم أضف
internalوcmdعندما تحتاجهما. - تسمية الحزم بأسماء عامة مثل
utilsوcommonوhelpers. اسم الحزمة جزء من كل استدعاء، فـmoney.VATتقرأ جملة مفهومة، وutils.VATلا تقول شيئاً. - تكرار اسم الحزمة في أسماء ما بداخلها، مثل
money.MoneyFormat. اكتبmoney.Format، لأن الاسم الكامل عند الاستدعاء يحمل اسم الحزمة دائماً. - حقل في struct يبدأ بحرف صغير ثم الاستغراب من أن JSON خرج فارغاً. الحقول التي تريدها خارج الحزمة يجب أن تبدأ بحرف كبير، والاسم في JSON تحدده بالوسم
`json:"name_ar"`. - تجاهل الخطأ بالشرطة السفلية لتسكت المترجم، مثل
p, _ := strconv.Atoi(s). الرقم الخاطئ يصبح صفراً بهدوء، وهذا سعر صفر في فاتورة. - تشغيل الإنتاج بالأمر
go run. هذا الأمر يترجم في كل تشغيل في مجلد مؤقت، وابن الملف بـgo buildثم شغله. - عدم تشغيل
gofmtوgo vetفي CI، ثم مراجعات كود تدور حول المسافات. اجعلها خطوتين تفشلان البناء من اليوم الأول.
قائمة تحقق Checklist قبل أن تكتب أول خدمة
| البند | كيف تتحقق |
|---|---|
| الإصدار الرسمي الحالي مثبت | go version يطبع go1.27.x |
السطر go في go.mod مقصود | لا يزيد عن الإصدار الذي يحتاجه الكود فعلاً |
| المحرر يستخدم gopls وينسق عند الحفظ | احفظ ملفاً سيء التنسيق وراقب التغيير |
| CI يشغل التنسيق | test -z "$(gofmt -l .)" |
| CI يشغل الفحص والاختبار | go vet ./... ثم go test -race ./... |
| فاحص أعمق بإصدار مثبت | staticcheck أو golangci-lint برقم إصدار في ملف CI |
| فحص الثغرات | govulncheck ./... في كل Pull Request وكل أسبوع |
go.mod وgo.sum نظيفان | go mod tidy ثم git diff --exit-code |
| المبالغ بالهللة كأرقام صحيحة | لا يوجد float64 في أي حساب مالي |
تدريب عملي
- أضف إلى
pricecalcخياراً-vatبالحزمة القياسيةflagيغير نسبة الضريبة، ثم أضف حالة إلى جدول الاختبار تتحقق من نسبة 5%. - شغل
go build -gcflags=-m ./internal/moneyواقرأ ما يقوله المترجم عن كل دالة: هل استطاع أن يدمجها Inline في مكان استدعائها؟ - غير السطر
go 1.27إلىgo 1.28فيgo.mod، ثم شغلgo versionداخل المجلد، واقرأ رسالة الخطأ، لأن هذا الإصدار لم يصدر بعد. - استبدل
strings.Titleفي الملف الذي فحصه staticcheck بالحزمةgolang.org/x/text/cases، وشاهد كيف تتغيرgo.modوgo.sumهذه المرة دون العلامةindirect.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- صممت Go لمشكلة فرق كبيرة في Google: بناء بطيء، واعتماديات متشابكة، وكود يصعب قراءته، ولذلك كل ما تركته من الكلاسات والاستثناءات وحلقة while كان قراراً لصالح البساطة والقراءة.
- ثبت الإصدار الرسمي 1.27.3، فالسطر
goفيgo.modحد أدنى ملزم، والأمرgoينزل الإصدار المطلوب وحده بالإعدادauto. - أهم اختلافات الصيغة: النوع بعد الاسم، والقيمة الصفرية لكل متغير، والدالة بقيمتين مع
error، وforوحدها، والـ struct مع الدوال المرتبطة، وdefer، والحرف الكبير للتصدير. - الأمر
goوحده يبني ويختبر ويفحص ويدير الحزم، ومعهgofmt، وتضيف إليهstaticcheckأوgolangci-lintوgovulncheckوgopls. - في التجربة حسبت الحاسبة 29.70 ريال مع ضريبة 4.46 فالإجمالي 34.16، ونجحت الاختبارات بتغطية 100% للحزمة، وكشف
go vetخطأPrintfوكشفstaticcheckخطأين لم يرهما. - الملف التنفيذي 2.4 ميجابايت بلا أي مكتبة من النظام، و1.55 مع حذف الرموز، والبناء لنظام Windows على ARM كان متغيرين فقط.
وفي المقالات التالية من مرحلة لغة Go للمطورين نكمل نفس القصة، من الأنواع والواجهات والأخطاء، إلى التزامن بالـ Goroutines، ثم خدمة الطلبات كاملة، ثم الأخطاء الشائعة وخطة القراءة:
دليلكيف تختار لغة البرمجة Programming Language لمشروعك؟نختار لغة خدمة حقيقية بأسئلة الفريق والتوظيف والمكتبات والرخصة، ثم نكتبها بأربع لغات ونقيس البناء والحجم والذاكرة والطلبات في الثانية بأنفسنا.
دليلالبرمجة بعدة لغات Polyglot: متى تفيد ومتى تعقد مشروعك؟نستدعي نفس الدالة من Python بخمس طرق، عبر FFI إلى C وGo وعبر HTTP وgRPC، ونقيس كلفة كل حد بين لغتين وتعقيد البناء وما يحدث عندما يتعطل الطرف الآخر.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة بالإصدار Go 1.27.3 داخل الـ Image الرسمية golang:1.27.3-trixie على Windows 11، مع staticcheck 2026.2.2 وgovulncheck v1.9.0.