(هذه أسئلة شائعة غير رسمية حول Go. الأسئلة الرسمية موجودة هنا.)
الفهرس:
- المصرِّف ووقت التشغيل - ماذا تعني رسالة خطأ التصريف
non-name ** - on left side of :=؟ - ماذا تعني رسالة خطأ التصريف
unexpected newline, expecting { after if clause؟ - ماذا تعني رسالة خطأ المصرِّف
declared and not used؟ - هل يحافظ وقت تشغيل Go على ترتيب التكرار في الخرائط؟
- هل تقوم مصرِّفات Go بالحشو (padding) لضمان محاذاة الحقول في أنواع البنى؟
- لماذا يسهم الحقل الأخير من نوع صفري الحجم في بنية في حجم البنية أحيانًا؟
- هل
new(T)مجرد سكر نحوي لـvar t T; (&t)؟ - ماذا تعني رسالة خطأ وقت التشغيل
all goroutines are asleep - deadlock؟ - هل الأعداد الصحيحة ذات 64 بت مضمونة المحاذاة على 64 بت حتى يمكن الوصول إليها ذريًّا؟
- هل إسنادات القيم عمليات ذرية؟
- هل تتكوّن كل قيمة صفرية من سلسلة بايتات صفرية في الذاكرة؟
- هل يدعم مصرِّف Go القياسي تضمين الدوال؟
- هل يمكنني استخدام الدوال النهائية (finalizers) كمُدمِّرات للكائنات؟
- الحزم القياسية - كيف تحصل على عدد أيام أي شهر بأقل عدد ممكن من أسطر الشيفرة؟
- ما الفرق بين استدعاء الدالة
time.Sleep(d)وعملية استقبال القناة<-time.After(d)؟ - غالبًا ما تعيد استدعاءات الدالتين
TrimLeftوTrimRightفي الحزمتين القياسيتينstringsوbytesنتائج غير متوقعة، فهل توجد أخطاء في تطبيقات هذه الدوال؟ - ما الفروق بين الدالتين
fmt.Printوfmt.Println؟ - هل يوجد أي فرق بين الدالتين
log.Printوlog.Println؟ - هل الدوال
fmt.Printوfmt.Printlnوfmt.Printfمتزامنة؟ - ما الفروق بين الدالتين المدمجتين
print/printlnودوال الطباعة المقابلة لهما في الحزمتين القياسيتينfmtوlog؟ - ما الفرق بين الأعداد العشوائية التي تنتجها الحزمة القياسية
math/randوالحزمة القياسيةcrypto/rand؟ - لماذا لا توجد دالة
math.Round؟ - نظام الأنواع - ما الأنواع التي لا تدعم المقارنات؟
- لماذا لا تكون قيمتا
nilمتساويتين أحيانًا؟ - لماذا لا يتشارك النوعان
[]T1و[]T2النوع الأساسي نفسه حتى إذا كان النوعان المختلفانT1وT2يتشاركان النوع الأساسي نفسه؟ - ما القيم التي يمكن أخذ عنوانها وما القيم التي لا يمكن؟
- لماذا لا يمكن أخذ عنوان عناصر الخريطة؟
- لماذا تكون عناصر شريحة غير nil قابلة لأخذ العنوان دائمًا، حتى لو كانت الشريحة نفسها غير قابلة لأخذ العنوان؟
- لأي نوع
Tغير مؤشر وغير واجهة، لماذا تكون مجموعة طرق*Tدائمًا مجموعة فائقة لمجموعة طرقT، وليس العكس؟ - ما الأنواع التي يمكننا تنفيذ طرق لها؟
- كيف تعرّف قيمًا غير قابلة للتغيير في Go؟
- لماذا لا يوجد نوع حاوية
setمدمج؟ - ما هو byte؟ وما هو rune؟ وكيف تحوّل قيم
[]byteو[]runeإلى نصوص؟ - كيف تتعامل مع قيم المؤشرات ذريًّا؟
- أخرى - ماذا يعني
iota؟ - لماذا لا توجد دالة مدمجة
closedللتحقق مما إذا كانت القناة مغلقة أم لا؟ - هل من الآمن أن تعيد دالة مؤشرات إلى متغيرات محلية؟
- ماذا تعني كلمة gopher في مجتمع Go؟
المصرِّف ووقت التشغيل
ماذا تعني رسالة خطأ التصريف non-name *** on left side of :=؟
حتى الآن (Go 1.25)، توجد قاعدة إلزامية لتعريفات المتغيرات القصيرة:
يجب أن تكون جميع العناصر على الجانب الأيسر من := معرّفات خالصة، ويجب أن يكون واحد منها على الأقل اسم متغير جديد.
وهذا يعني أن عناصر الحاويات (x[i]) وحقول البنى (x.f) وإشارات إلغاء المؤشر (*p) والمعرّفات المؤهَّلة (aPackage.Value) لا يمكن أن تظهر على الجانب الأيسر من :=.
حاليًا، توجد مسألة مفتوحة (دُمجت مع أخرى أكثر ارتباطًا) لهذه المشكلة. يبدو أن الفريق الأساسي لـ Go يريد ترك هذه المشكلة دون حل حاليًا.
ماذا تعني رسالة خطأ التصريف unexpected newline, expecting { ...؟
في Go، لا يمكننا كسر سطر شيفرة عند أي موضع عشوائي. اقرأ قواعد كسر الأسطر في Go للتفاصيل. ووفقًا لهذه القواعد، عمومًا، لا يجوز كسر أسطر الشيفرة قبل الأقواس المعقوفة المفتوحة مباشرة.
على سبيل المثال، الشيفرة التالية
if true
{
}
for i := 0; i < 10; i++
{
}
var _ = []int
{
1, 2, 3
}
ستُفسَّر على أنها
if true;
{
}
for i := 0; i < 10; i++;
{
}
var _ = []int;
{
1, 2, 3;
}
ستُبلّغ مصرِّفات Go عن خطأ عند كل قوس معقوف مفتوح {. لتجنّب هذه الأخطاء، ينبغي أن نعيد كتابة الشيفرة أعلاه كما يلي.
if true {
}
for i := 0; i < 10; i++ {
}
var _ = []int {
1, 2, 3,
}
ماذا تعني رسالة خطأ المصرِّف declared and not used؟
بالنسبة لمصرِّف Go القياسي، يجب استخدام كل متغير معرَّف في كتل الشيفرة المحلية كقيمة r-value (أي قيمة على الجانب الأيمن) مرة واحدة على الأقل.
لذا تفشل الشيفرة التالية في التصريف.
func f(x bool) {
var y = 1 // y معرَّف لكنه غير مستخدم (كقيمة r-value)
if x {
y = 2 // هنا يُستخدم y كقيمة على الجانب الأيسر
}
}
هل يحافظ وقت تشغيل Go على ترتيب التكرار في الخرائط؟
لا. تقول مواصفة Go 1 إن ترتيب التكرار على خريطة غير محدَّد وليس مضمونًا أن يكون نفسه من تكرار إلى آخر. وبالنسبة لمصرِّف Go القياسي، فإن ترتيبات تكرار الخرائط عشوائية جزئيًّا دائمًا بدرجات متفاوتة. وإذا كنت تحتاج إلى ترتيب تكرار مستقر لخريطة ما، فيجب أن تحافظ على الترتيب بنفسك. اقرأ خرائط Go عمليًّا لمزيد من المعلومات.
ومع ذلك، لاحظ أنه منذ Go 1.12، أصبح ترتيب المدخلات في نتيجة الطباعة لدوال الطباعة في الحزم القياسية مرتبًا دائمًا.
هل تقوم مصرِّفات Go بالحشو لضمان محاذاة الحقول في أنواع البنى؟
على الأقل بالنسبة لمصرِّف Go القياسي وgccgo، الجواب نعم. وعدد البايتات التي ستُحشى يعتمد على نظام التشغيل والمصرِّف. اقرأ تخطيطات الذاكرة للتفاصيل.
لن تعيد مصرِّفات Go ترتيب حقول البنية لتقليل أحجام قيم البنى. فقد يسبب فعل ذلك بعض النتائج غير المتوقعة. ومع ذلك، يمكن للمبرمجين تقليل الحشو بإعادة ترتيب الحقول يدويًّا.
لماذا يسهم الحقل الأخير من نوع صفري الحجم في بنية في حجم البنية أحيانًا؟
في التطبيق الحالي لوقت تشغيل Go القياسي، ما دامت كتلة ذاكرة ما مُشارًا إليها من مؤشر نشط واحد على الأقل، فلن تُعتبر تلك الكتلة نفايات ولن تُجمع.
يمكن أخذ عناوين جميع حقول قيمة بنية قابلة لأخذ العنوان. وإذا كان حجم الحقل الأخير في قيمة بنية غير صفرية الحجم صفرًا، فإن أخذ عنوان الحقل الأخير في قيمة البنية سيعيد عنوانًا يقع خارج كتلة الذاكرة المخصَّصة لقيمة البنية. وقد يشير العنوان المُعاد إلى كتلة ذاكرة مخصَّصة أخرى تلي مباشرة الكتلة المخصَّصة لقيمة البنية غير صفرية الحجم. وما دام العنوان المُعاد مخزَّنًا في قيمة مؤشر نشطة، فلن تُجمع كتلة الذاكرة المخصَّصة الأخرى كنفايات، ما قد يسبب تسرّبًا في الذاكرة.
لتجنّب هذه الأنواع من مشكلات تسرّب الذاكرة، سيضمن مصرِّف Go القياسي أن أخذ عنوان الحقل الأخير في بنية غير صفرية الحجم لن يعيد أبدًا عنوانًا يقع خارج كتلة الذاكرة المخصَّصة للبنية. ويحقّق مصرِّف Go القياسي ذلك بحشو بعض البايتات بعد الحقل الأخير صفري الحجم عند الحاجة.
إذا كانت أنواع جميع الحقول في نوع بنية ما صفرية الحجم (بحيث تكون البنية أيضًا نوعًا صفري الحجم)، فلا حاجة إلى حشو بايتات في البنية، لأن مصرِّف Go القياسي يعامل كتل الذاكرة صفرية الحجم معاملة خاصة.
مثال:
package main
import (
"unsafe"
"fmt"
)
func main() {
type T1 struct {
a struct{}
x int64
}
fmt.Println(unsafe.Sizeof(T1{})) // 8
type T2 struct {
x int64
a struct{}
}
fmt.Println(unsafe.Sizeof(T2{})) // 16
}
انظر أيضًا هذا البند في الأسئلة الشائعة الرسمية.
هل new(T) مجرد سكر نحوي لـ var t T; (&t)؟
عمومًا يمكننا التفكير بذلك، رغم أنه قد تكون هناك بعض الفروق الدقيقة بين الاثنين حسب تطبيقات المصرِّفات. وقد تكون كتلة الذاكرة التي يخصّصها new على المكدس أو على الكومة.
ماذا تعني رسالة خطأ وقت التشغيل all goroutines are asleep - deadlock؟
كلمة asleep ليست دقيقة هنا، فهي تعني في الواقع حالة الحجب.
وبما أن الـ goroutine المحجوبة لا يمكن تحريرها إلا بواسطة goroutine أخرى، فإذا دخلت جميع الـ goroutines في برنامج ما حالة الحجب، فستبقى كلها في حالة الحجب إلى الأبد. وهذا يعني أن البرنامج في حالة جمود. ولا يُتوقّع أبدًا أن يكون برنامج يعمل بشكل طبيعي في حالة جمود، لذا يجعل وقت تشغيل Go القياسي البرنامج ينهار ويخرج.
هل الأعداد الصحيحة ذات 64 بت مضمونة المحاذاة على 64 بت حتى يمكن الوصول إليها ذريًّا؟
يجب أن تكون العناوين التي تُمرَّر إلى دوال 64 بت في حزمة sync/atomic محاذاة على 64 بت، وإلا فقد تُسبّب استدعاءات هذه الدوال panic في وقت التشغيل.
بالنسبة لمصرِّف Go القياسي ومصرِّف gccgo، على معماريات 64 بت، الأعداد الصحيحة ذات 64 بت مضمونة المحاذاة على 64 بت. لذا يمكن الوصول إليها ذريًّا دائمًا دون أي مشكلات.
على معماريات 32 بت، الأعداد الصحيحة ذات 64 بت مضمونة المحاذاة على 32 بت فقط. لذا قد يسبب الوصول ذريًّا إلى كثير من الأعداد الصحيحة ذات 64 بت حدوث panics. ومع ذلك، توجد بعض الطرق لضمان إمكانية الاعتماد على محاذاة بعض الأعداد الصحيحة ذات 64 بت على 64 بت. اقرأ تخطيطات الذاكرة في Go للتفاصيل.
هل إسنادات القيم عمليات ذرية؟
لا بالنسبة لمصرِّف Go القياسي، حتى لو كانت أحجام القيم المُسنَدة بحجم الكلمات الأصلية للمعالج.
اقرأ السؤال الرسمي لمزيد من التفاصيل.
هل تتكوّن كل قيمة صفرية من سلسلة بايتات صفرية في الذاكرة؟
بالنسبة لمعظم الأنواع، هذا صحيح. لكن ينبغي أن نعلم أن هذا يعتمد على المصرِّف. فمثلاً، بالنسبة لمصرِّف Go القياسي، العبارة خاطئة في بعض القيم الصفرية لأنواع النصوص وأنواع الأعداد العشرية.
الدليل:
package main
import (
"unsafe"
"fmt"
)
func main() {
// الحالة 1:
var s = "abc"[0:0]
fmt.Println(s == "") // true
var addr = unsafe.StringData(s)
fmt.Println(addr) // <a-non-zero-value>
// الحالة 2:
var x = 0.0
var y = -x
fmt.Println(y == 0) // true
var n = *(*uintptr)(unsafe.Pointer(&y))
fmt.Println(n) // 9223372036854775808
}
وبالعكس، بالنسبة لجميع المعماريات التي يدعمها مصرِّف Go القياسي حاليًا، إذا كانت جميع البايتات في قيمة ما صفرية، فيجب أن تكون القيمة قيمة صفرية لنوعها. ومع ذلك، لا تضمن مواصفة Go ذلك. سمعت أنه على بعض المعالجات القديمة جدًّا، لا تكون المؤشرات nil صفرية في الذاكرة.
هل يدعم مصرِّف Go القياسي تضمين الدوال؟
نعم، يدعم مصرِّف Go القياسي تضمين الدوال. وسيقوم المصرِّف تلقائيًّا بتضمين بعض الدوال القصيرة جدًّا التي تستوفي متطلبات معينة. وقد تتغيّر متطلبات التضمين المحددة من إصدار إلى آخر.
حاليًا (سلسلة أدوات Go 1.25)، بالنسبة لمصرِّف Go القياسي:
- لا توجد طرق صريحة لتحديد الدوال التي ينبغي تضمينها في برامج المستخدمين.
- يعطّل خيار البناء
-gcflags "-l"التضمين عالميًّا، ما يمنع توسيع جميع الدوال بالتضمين. - في برامج المستخدمين، لا توجد طرق رسمية لمنع تضمين دوال معينة. يمكنك إضافة توجيه
//go:noinlineفي سطر قبل تعريف دالة لمنع تضمينها، لكن هذه الطريقة غير مضمونة العمل دائمًا في المستقبل.
هل يمكنني استخدام الدوال النهائية كمُدمِّرات للكائنات؟
في برامج Go، يمكننا تعيين دالة نهائية (finalizer) لكائن باستخدام الدالة runtime.SetFinalizer. عمومًا، ستُستدعى الدالة النهائية قبل جمع الكائن كنفايات. لكن الدوال النهائية لم يُقصد بها أبدًا أن تُستخدم كمُدمِّرات للكائنات. فالدوال النهائية المعيَّنة بواسطة runtime.SetFinalizer ليس مضمونًا أن تعمل. لذا لا ينبغي أن تعتمد على الدوال النهائية في صحة برنامجك.
الغرض الرئيسي من الدوال النهائية هو السماح لمسؤولي صيانة مكتبة ما بتعويض المشكلات الناتجة عن استخدام خاطئ للمكتبة. فمثلاً، إذا استخدم مبرمج os.Open لفتح ملفات كثيرة لكنه نسي إغلاقها بعد استخدامها، فسيحتفظ البرنامج بكثير من واصفات الملفات حتى يخرج البرنامج. وهذا مثال كلاسيكي على تسرّب الموارد. ولتجنّب احتفاظ البرنامج بعدد كبير جدًّا من واصفات الملفات، سيعيّن مسؤولو صيانة حزمة os دالة نهائية على كل كائن os.File يُنشأ. وستُغلق الدالة النهائية واصف الملف المخزَّن في كائن os.File. وكما ذُكر أعلاه، ليس مضمونًا استدعاء الدوال النهائية. فهي تُستخدم فقط لجعل نطاق تسرّب الموارد أصغر ما يمكن.
لإضافة أفضل الممارسات ومزيد من الحالات التي لا تعمل فيها الدالة النهائية.
لاحظ أن بعض الدوال النهائية لن تُستدعى أبدًا بالتأكيد، وأن تعيين الدوال النهائية بشكل غير صحيح سيمنع أحيانًا جمع بعض الكائنات كنفايات. اقرأ توثيق دالة runtime.SetFinalizer للحصول على مزيد من التفاصيل.
الحزم القياسية
كيف تحصل على عدد أيام أي شهر بأقل عدد ممكن من الأسطر؟
افترض أن السنة والشهر المُدخَلين من التقويم الميلادي (يناير هو 1).
days := time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
بالنسبة لواجهات وقت Go البرمجية، النطاق المعتاد للشهر هو [1, 12] ويوم بداية كل شهر هو 1. ووقت بداية الشهر m في السنة y هو time.Date(y, m, 1, 0, 0, 0, 0, time.UTC). ويمكن أن تكون المعطيات المُمرَّرة إلى time.Date خارج نطاقاتها المعتادة وستُطبَّع أثناء التحويل. فمثلاً، سيُحوَّل 32 يناير إلى 1 فبراير.
إليك بعض أمثلة استخدام time.Date في Go:
package main
import (
"time"
"fmt"
)
func main() {
// 2017-02-01 00:00:00 +0000 UTC
fmt.Println(time.Date(2017, 1, 32, 0, 0, 0, 0, time.UTC))
// 2017-01-31 23:59:59.999999999 +0000 UTC
fmt.Println(time.Date(2017, 1, 32, 0, 0, 0, -1, time.UTC))
// 2017-01-31 00:00:00 +0000 UTC
fmt.Println(time.Date(2017, 2, 0, 0, 0, 0, 0, time.UTC))
// 2016-12-31 00:00:00 +0000 UTC
fmt.Println(time.Date(2016, 13, 0, 0, 0, 0, 0, time.UTC))
// 2017-02-01 00:00:00 +0000 UTC
fmt.Println(time.Date(2016, 13, 32, 0, 0, 0, 0, time.UTC))
}
ما الفرق بين استدعاء الدالة time.Sleep(d) وعملية استقبال القناة <-time.After(d)؟
كلا الاثنين سيوقفان تنفيذ الـ goroutine الحالية لمدة معينة. والفرق هو أن استدعاء الدالة time.Sleep(d) سيجعل الـ goroutine الحالية تدخل الحالة الفرعية للنوم، لكنها تبقى في حالة التشغيل، في حين أن عملية استقبال القناة <-time.After(d) ستجعل الـ goroutine الحالية تدخل حالة الحجب.
غالبًا ما تعيد استدعاءات الدالتين TrimLeft وTrimRight في الحزمتين القياسيتين strings وbytes نتائج غير متوقعة، فهل توجد أخطاء في تطبيقات هذه الدوال؟
آها، ربما توجد أخطاء في التطبيقات، لكن لم يُؤكَّد أي منها حتى الآن. وإذا كانت النتائج المُعادة غير متوقعة، فالأرجح أن توقعاتك غير صحيحة.
توجد دوال تقليم (trim) كثيرة في الحزمتين القياسيتين strings وbytes. ويمكن تصنيف هذه الدوال إلى مجموعتين:
TrimوTrimLeftوTrimRightوTrimSpaceوTrimFuncوTrimLeftFuncوTrimRightFunc. ستقوم هذه الدوال بتقليم جميع نقاط شيفرة Unicode (المعروفة أيضًا بـ runes) المشفَّرة بـ UTF-8 البادئة أو اللاحقة التي تستوفي الشروط المحدَّدة أو الضمنية (TrimSpaceتعني ضمنًا تقليم جميع أنواع المسافات البيضاء). وسيُفحص كل rune بادئ أو لاحق حتى يُعثر على rune لا يستوفي الشروط المحدَّدة أو الضمنية.TrimPrefixوTrimSuffix. ستقوم الدالتان بتقليم النصوص الفرعية (أو الشرائح الفرعية) للبادئة أو اللاحقة المحدَّدة كوحدة واحدة.
بعض المبرمجين أساؤوا استخدام الدالتين TrimLeft وTrimRight كدالتَي TrimPrefix وTrimSuffix عند استخدام دوال التقليم لأول مرة. وبالتأكيد، من المرجح جدًّا ألا تكون النتائج المُعادة كما هو متوقع.
مثال:
package main
import (
"fmt"
"strings"
)
func main() {
var s = "abaay森z众xbbab"
o := fmt.Println
o(strings.TrimPrefix(s, "ab")) // aay森z众xbbab
o(strings.TrimSuffix(s, "ab")) // abaay森z众xbb
o(strings.TrimLeft(s, "ab")) // y森z众xbbab
o(strings.TrimRight(s, "ab")) // abaay森z众x
o(strings.Trim(s, "ab")) // y森z众x
o(strings.TrimFunc(s, func(r rune) bool {
return r < 128 // تقليم جميع أحرف ascii
})) // 森z众
}
ما الفروق بين الدالتين fmt.Print وfmt.Println؟
ستكتب الدالة fmt.Println دائمًا مسافة بين معطيين متجاورين، في حين أن الدالة fmt.Print ستكتب مسافة بين معطيين متجاورين فقط إذا لم يكن كلا المعطيين المتجاورين (أي قيمتيهما الملموستين) من نوع النص.
وثمة فرق آخر هو أن fmt.Println ستكتب محرف سطر جديد في النهاية، لكن الدالة fmt.Print لن تفعل ذلك.
هل يوجد أي فرق بين الدالتين log.Print وlog.Println؟
الفرق بين الدالتين log.Print وlog.Println هو نفسه الفرق الأول بين الدالتين fmt.Print وfmt.Println المذكور في السؤال السابق.
ستكتب كلتا الدالتين محرف سطر جديد في النهاية.
هل الدوال fmt.Print وfmt.Println وfmt.Printf متزامنة؟
لا، هذه الدوال ليست متزامنة. استخدم بدلاً منها الدوال المقابلة في الحزمة القياسية log عند الحاجة إلى التزامن. ويمكنك استدعاء log.SetFlags(0) لإزالة البادئة من كل سطر سجل.
ما الفروق بين الدالتين المدمجتين print/println ودوال الطباعة المقابلة لهما في الحزمتين القياسيتين fmt وlog؟
إلى جانب الفرق المذكور في السؤال السابق، توجد بعض الفروق الأخرى بين مجموعات الدوال الثلاث.
- ستكتب الدالتان المدمجتان
print/printlnإلى الخطأ القياسي. وستكتب دوال الطباعة في الحزمة القياسيةfmtإلى الخرج القياسي. وستكتب دوال الطباعة في الحزمة القياسيةlogإلى الخطأ القياسي افتراضيًّا، رغم أنه يمكن تغيير ذلك باستخدام الدالةlog.SetOutput. - لا يمكن لاستدعاءات الدالتين المدمجتين
print/printlnأن تأخذ معطيات من نوع مصفوفة أو بنية. - بالنسبة لمعطٍ من نوع مركّب، ستكتب الدالتان المدمجتان
print/printlnعناوين أجزاء القيمة الأساسية للمعطى، في حين تحاول دوال الطباعة في الحزمتين القياسيتينfmtوlogكتابة القيمة الحرفية للقيم الديناميكية لمعطيات الواجهة. - لن تجعل استدعاءات الدالتين المدمجتين
print/printlnالقيم المُشار إليها بواسطة معطيات الاستدعاءات تهرب إلى الكومة، في حين أن دوال الطباعة في الحزمتين القياسيتينfmtوlogستفعل ذلك. - إذا كان لمعطٍ ما طريقة
String() stringأوError() string، فستحاول دوال الطباعة في الحزمتين القياسيتينfmtوlogاستدعاء تلك الطريقة عند كتابة المعطى، في حين أن الدالتين المدمجتينprint/printlnستتجاهلان طرق المعطيات. - ليس مضمونًا أن توجد الدالتان المدمجتان
print/printlnفي إصدارات Go المستقبلية.
ما الفرق بين الأعداد العشوائية التي تنتجها الحزمة القياسية math/rand والحزمة القياسية crypto/rand؟
الأعداد شبه العشوائية (pseudo random) التي تنتجها الحزمة القياسية math/rand حتمية بالنسبة لبذرة (seed) معطاة. والأعداد العشوائية المنتَجة ليست جيدة للسياقات الحساسة أمنيًّا. ولأغراض الأمان التشفيري، ينبغي أن نستخدم الأعداد شبه العشوائية التي تنتجها الحزمة القياسية crypto/rand.
لماذا لا توجد دالة math.Round؟
توجد دالة math.Round، لكن منذ Go 1.10 فقط. فقد أُضيفت دالتان جديدتان، math.Round وmath.RoundToEven، منذ Go 1.10.
قبل Go 1.10، أُنفق الكثير من الوقت والجهد في مناقشة ما إذا كان ينبغي إضافة دالة math.Round إلى الحزمة القياسية أم لا. وفي النهاية، اعتُمد الاقتراح.
نظام الأنواع
ما الأنواع التي لا تدعم المقارنات؟
الأنواع التالية لا تدعم المقارنات:
- map
- slice
- function
- أنواع البنى التي تحتوي على حقول غير قابلة للمقارنة
- أنواع المصفوفات التي تكون أنواع عناصرها غير قابلة للمقارنة
لا يمكن استخدام الأنواع التي لا تدعم المقارنات كنوع مفتاح لأنواع الخرائط.
لاحظ أنه:
- رغم أن أنواع الخرائط والشرائح والدوال لا تدعم المقارنات، يمكن مقارنة قيمها بالمعرّف المجرّد
nil. - مقارنة قيمتي واجهة باستخدام
==ستُسبّب panic في وقت التشغيل إذا كان النوعان الديناميكيان لقيمتي الواجهة متطابقين وغير قابلين للمقارنة.
ولمعرفة سبب عدم دعم أنواع الشرائح والخرائط والدوال للمقارنة، اقرأ هذا الجواب في الأسئلة الشائعة الرسمية لـ Go.
لماذا لا تكون قيمتا nil متساويتين أحيانًا؟
(الجواب في الأسئلة الشائعة الرسمية لـ Go قد يجيب أيضًا عن هذا السؤال.)
يمكن النظر إلى قيمة واجهة كصندوق يُستخدم لتغليف القيم غير الواجهية. ولا يمكن تغليف (تضمين) سوى القيم التي تنفّذ أنواعها نوع قيمة الواجهة داخل قيمة الواجهة. وفي Go، توجد عدة أنواع من الأنواع التي تُمثَّل قيمها الصفرية بالمعرّف المُعرَّف مسبقًا nil. وقيمة الواجهة التي لا تغلّف شيئًا هي قيمة واجهة صفرية، أي قيمة واجهة nil. غير أن قيمة الواجهة التي تغلّف قيمة غير واجهية تساوي nil تغلّف شيئًا ما، لذا فهي ليست قيمة واجهة nil ولا تساويها.
وعند مقارنة قيمة واجهة nil وقيمة غير واجهية تساوي nil (بافتراض إمكانية مقارنتهما)، ستُحوَّل القيمة غير الواجهية التي تساوي nil إلى نوع قيمة الواجهة nil قبل إجراء المقارنة. ونتيجة التحويل هي قيمة واجهة تغلّف نسخة من القيمة غير الواجهية. وقيمة الواجهة الناتجة تغلّف شيئًا ما، لذا فهي ليست قيمة الواجهة nil ولا تساويها.
اقرأ الواجهات في Go وقيم nil في Go للحصول على شروح مفصلة.
على سبيل المثال:
package main
import "fmt"
func main() {
var pi *int = nil
var pb *bool = nil
var x interface{} = pi
var y interface{} = pb
var z interface{} = nil
fmt.Println(x == y) // false
fmt.Println(x == nil) // false
fmt.Println(y == nil) // false
fmt.Println(x == z) // false
fmt.Println(y == z) // false
}
لماذا لا يتشارك النوعان []T1 و[]T2 النوع الأساسي نفسه حتى إذا كان النوعان المختلفان T1 وT2 يتشاركان النوع الأساسي نفسه؟
(يبدو أن الأسئلة الشائعة الرسمية لـ Go أضافت أيضًا سؤالاً مشابهًا منذ وقت ليس ببعيد.)
في Go، لا يمكن تحويل قيم نوع شريحة إلى نوع شريحة آخر دون استخدام آليات unsafe إلا إذا كان نوعا الشريحتين يتشاركان النوع الأساسي نفسه. (هذا المقال يسرد القائمة الكاملة لقواعد تحويل القيم.)
النوع الأساسي لنوع مركّب غير مسمّى هو النوع المركّب نفسه. لذا حتى إذا كان نوعان مختلفان T1 وT2 يتشاركان النوع الأساسي نفسه، فالنوعان []T1 و[]T2 ما زالا نوعين مختلفين، لذا فإن نوعيهما الأساسيين مختلفان أيضًا، ما يعني أنه لا يمكن تحويل قيم أحدهما إلى الآخر.
وأسباب عدم تطابق النوعين الأساسيين لـ []T1 و[]T2 هي:
- أن الحاجة إلى تحويل قيم
[]T1و[]T2إحداهما إلى الأخرى ليست قوية عمليًّا. - لجعل قواعد تتبّع النوع الأساسي أبسط.
والأسباب نفسها تنطبق أيضًا على الأنواع المركّبة الأخرى. فمثلاً، النوعان map[T]T1 وmap[T]T2 لا يتشاركان النوع الأساسي نفسه أيضًا حتى إذا كان T1 وT2 يتشاركان النوع الأساسي نفسه.
من الممكن تحويل قيم من النوع []T1 إلى []T2 باستخدام آليات unsafe، لكن لا يُوصى بذلك عمومًا:
package main
import (
"fmt"
"unsafe"
)
func main() {
type MyInt int
var a = []int{7, 8, 9}
var b = *(*[]MyInt)(unsafe.Pointer(&a))
b[0]= 123
fmt.Println(a) // [123 8 9]
fmt.Println(b) // [123 8 9]
fmt.Printf("%T \n", a) // []int
fmt.Printf("%T \n", b) // []main.MyInt
}
ما القيم التي يمكن أخذ عنوانها وما القيم التي لا يمكن؟
لا يمكننا أخذ عنوان القيم التالية:
- البايتات في النصوص
- عناصر الخرائط
- القيم الديناميكية لقيم الواجهات (المكشوفة بواسطة تأكيدات النوع)
- القيم الثابتة (بما فيها الثوابت المسمّاة والقيم الحرفية)
- دوال مستوى الحزمة
- الطرق (المستخدمة كقيم دوال)
- القيم الوسيطة - استدعاءات الدوال
- تحويلات القيم الصريحة
- جميع أنواع العمليات، باستثناء عمليات إلغاء المؤشر، ولكن بما فيها: - عمليات استقبال القنوات
- عمليات النصوص الفرعية
- عمليات الشرائح الفرعية
- الجمع والطرح والضرب والقسمة، إلخ.
لاحظ أن هناك سكرًا نحويًّا في Go، &T{}، وهو صيغة مختصرة لـ tmp := T{}; (&tmp). ومع ذلك، رغم أن &T{} قانوني، فإن القيمة الحرفية T{} ما زالت غير قابلة لأخذ العنوان.
ويمكن أخذ عنوان القيم التالية:
- المتغيرات
- حقول البنى القابلة لأخذ العنوان
- عناصر المصفوفات القابلة لأخذ العنوان
- عناصر أي شرائح (سواء كانت الشرائح قابلة لأخذ العنوان أم لا)
- عمليات إلغاء المؤشر
لماذا لا يمكن أخذ عنوان عناصر الخريطة؟
السبب الأول هو أنه سيتعارض مع عملية إدارة الذاكرة الداخلية للخرائط. في Go، صُمِّمت الخريطة كحاوية يمكن أن تحتوي على عدد غير محدود من المدخلات إذا توافرت الذاكرة. ولضمان كفاءة جيدة في فهرسة عناصر الخريطة، يحافظ كل قيمة خريطة في تطبيق وقت تشغيل Go الرسمي على قطعة ذاكرة متصلة واحدة فقط لكامل المدخلات المخزَّنة في تلك الخريطة. لذلك، يحتاج وقت تشغيل Go إلى تخصيص قطع ذاكرة أكبر للخريطة من وقت لآخر عندما يُضاف المزيد والمزيد من المدخلات إلى الخريطة. وفي هذه العملية، ستُنقل المدخلات المخزَّنة على قطع الذاكرة الأقدم إلى قطع ذاكرة أحدث. وقد تكون هناك أيضًا بعض الأسباب الأخرى التي تسبب تحركات للمدخلات في الذاكرة. بعبارة أخرى، ستتغيّر عناوين عناصر الخريطة من وقت لآخر عند الحاجة. ولو كان مسموحًا بأخذ عناوين عناصر الخريطة، فعند نقل بعض مدخلات الخريطة، سيتعيّن على وقت تشغيل Go تحديث جميع المؤشرات التي تخزّن عناوين العناصر المنقولة، ما يجلب صعوبات كثيرة في تطبيق مصرِّفات Go وأوقات تشغيلها ويقلّل أداء التنفيذ تقليلًا كبيرًا. لذا، حاليًا، لا يمكن أخذ عناوين عناصر الخرائط.
ثانيًا، قد يعيد تعبير فهرسة الخريطة aMap[key] عنصرًا مخزَّنًا في الخريطة aMap أو لا، ما يعني أن aMap[key] قد يظل ناتجًا في قيمة صفرية بعد استدعاء (&aMap[key]).Modify(). وهذا من شأنه أن يحير كثيرًا من الناس. (هنا تشير Modify() إلى طريقة افتراضية ستعدّل القيمة aMap[key]).
لماذا تكون عناصر شريحة غير nil قابلة لأخذ العنوان دائمًا، حتى لو كانت الشريحة نفسها غير قابلة لأخذ العنوان؟
النوع الداخلي للشرائح هو بنية مثل
struct {
// elements يشير إلى متتالية عناصر.
elements unsafe.Pointer
length int
capacity int
}
تشير كل شريحة داخليًّا إشارة غير مباشرة إلى متتالية عناصر أساسية. ورغم أن شريحة غير nil ليست قابلة لأخذ العنوان، فإن متتالية عناصرها الداخلية مخصَّصة دائمًا في مكان ما ويجب أن تكون قابلة لأخذ العنوان. وأخذ عناوين عناصر شريحة هو في الواقع أخذ عناوين عناصر المتتالية الداخلية. ولهذا تكون عناصر الشرائح غير nil غير القابلة لأخذ العنوان قابلة لأخذ العنوان دائمًا.
لأي نوع T غير مؤشر وغير واجهة، لماذا تكون مجموعة طرق *T دائمًا مجموعة فائقة لمجموعة طرق T، وليس العكس؟
كلا الحالتين تتضمنان سكرًا نحويًّا، لكن واحدة فقط منهما قاعدة جوهرية.
- يمكن لقيمة من النوع
Tأن تستدعي طرق النوع*T، لكن فقط إذا كانت قيمةTقابلة لأخذ العنوان. وستأخذ المصرِّفات عنوان قيمةTتلقائيًّا قبل استدعاء طرق المستقبِل المؤشري. ولأن النوعTيمكن أن يمتلك قيمًا غير قابلة لأخذ العنوان، فليست كل قيم النوعTقادرة على استدعاء طرق النوع*T. وهذه الميزة مجرد سكر نحوي، وليست قاعدة جوهرية. - يمكن لقيمة من النوع
*Tأن تستدعي طرق النوعTدائمًا. وذلك لأن إلغاء الإشارة إلى قيمة مؤشر قانوني دائمًا. وهذه الميزة ليست سكرًا نحويًّا فحسب، بل هي أيضًا قاعدة جوهرية.
لذا، فمن المنطقي أن تكون مجموعة طرق *T دائمًا مجموعة فائقة لمجموعة طرق T، وليس العكس.
في الواقع، يمكنك التفكير في أنه لكل طريقة معرَّفة على النوع T، تُعرَّف تلقائيًّا على النوع *T طريقة ضمنية بالاسم نفسه والتوقيع نفسه. اقرأ الطرق للتفاصيل.
func (t T) MethodX(v0 ParamType0, ...) (ResultType0, ...) {
...
}
// تُعرَّف تلقائيًّا طريقة ضمنية للنوع *T كما يلي
func (pt *T) MethodX(v0 ParamType0, ...) (ResultType0, ...) {
return (*pt).MethodX(v0, ...)
}
اقرأ هذا الجواب في الأسئلة الشائعة الرسمية لـ Go للحصول على مزيد من الشروح.
ما الأنواع التي يمكننا تنفيذ طرق لها؟
اقرأ الطرق في Go للتفاصيل.
كيف تعرّف قيمًا غير قابلة للتغيير في Go؟
توجد ثلاثة تعريفات للقيم غير القابلة للتغيير:
- القيم التي لا عناوين لها (لذا فهي غير قابلة لأخذ العنوان).
- القيم التي لها عناوين لكنها غير قابلة لأخذ العنوان (لا يُسمح بأخذ عناوينها في الصيغة).
- القيم القابلة لأخذ العنوان لكن لا يُسمح بتعديل قيمها في الصيغة.
في Go، حتى الآن (Go 1.25)، لا توجد قيم تستوفي التعريف الثالث. بعبارة أخرى، التعريف الثالث غير مدعوم. ومع ذلك، قد يُنظر إلى متغيرات الأنواع صفرية الحجم فعليًّا كقيم (قابلة لأخذ العنوان) غير قابلة للتغيير.
تستوفي قيم الثوابت المسمّاة التعريف الأول.
يمكن أيضًا النظر إلى الطرق ودوال مستوى الحزمة كقيم معرَّفة غير قابلة للتغيير. وهي تستوفي التعريف الثاني. وتستوفي عناصر النصوص (البايتات) وعناصر مدخلات الخرائط أيضًا التعريف الثاني.
لا توجد طرق لتعريف قيم مسمّاة مخصَّصة أخرى غير قابلة للتغيير في Go.
لماذا لا يوجد نوع حاوية set مدمج؟
المجموعات (sets) ليست سوى خرائط لكنها لا تهتم بقيم العناصر. وفي Go، كثيرًا ما يُستخدم map[Tkey]struct{} كنوع مجموعة.
ما هو byte؟ وما هو rune؟ وكيف تحوّل قيم []byte و[]rune إلى نصوص؟
في Go، byte اسم مستعار للنوع uint8. بعبارة أخرى، byte وuint8 هما النوع نفسه بعينه. والعلاقة نفسها تنطبق على rune وint32.
كثيرًا ما يُستخدم rune لتخزين نقطة شيفرة Unicode.
يمكن تحويل قيم []byte و[]rune صراحةً ومباشرة إلى نصوص، والعكس صحيح.
package main
import "fmt"
func main() {
var s0 = "Go"
var bs = []byte(s0)
var s1 = string(bs)
var rs = []rune(s0)
var s2 = string(rs)
fmt.Println(s0 == s1) // true
fmt.Println(s0 == s2) // true
}
لمزيد من المعلومات عن النصوص، اقرأ النصوص في Go.
كيف تتعامل مع قيم المؤشرات ذريًّا؟
اقرأ العمليات الذرية للمؤشرات.
أخرى
ماذا يعني iota؟
Iota هو الحرف التاسع من الأبجدية اليونانية. وفي Go، يُستخدم iota في تعريفات الثوابت. وفي كل مجموعة تعريف ثوابت، تكون قيمته N في المواصفة رقم N للثوابت في مجموعة تعريف الثوابت تلك. وهذا يسمح بـتعريف الثوابت المرتبطة بسهولة.
لماذا لا توجد دالة مدمجة closed للتحقق مما إذا كانت القناة مغلقة أم لا؟
السبب هو أن نفع مثل هذه الدالة سيكون محدودًا جدًّا، في حين أن احتمال إساءة استخدامها مرتفع. وقد لا تكون نتيجة الإرجاع لاستدعاء مثل هذه الدالة قادرة على عكس أحدث حالة للقناة المعطى بها. لذا، ليس من الجيد اتخاذ قرارات بالاعتماد على نتيجة الإرجاع.
وإذا كنت تحتاج فعلاً إلى مثل هذه الدالة، فسيكون كتابتها بنفسك أمرًا سهلًا. اقرأ هذا المقال لتعرف كيف تكتب دوال closed وكيف تتجنّب استخدام مثل هذه الدالة.
هل من الآمن أن تعيد دالة مؤشرات إلى متغيرات محلية؟
نعم، إنه آمن تمامًا في Go.
ستجري مصرِّفات Go التي تدعم المكدس تحليل الهروب (escape analysis). وبالنسبة لمصرِّف Go القياسي، إذا رأى محلّل الهروب أن كتلة ذاكرة ما ستُستخدم في الـ goroutine الحالية فقط بالتأكيد، فسيخصّص كتلة الذاكرة على المكدس. وإلا فستُخصَّص كتلة الذاكرة على الكومة. اقرأ كتلة الذاكرة لمزيد من المعلومات.
ماذا تعني كلمة gopher في مجتمع Go؟
في مجتمع Go، تعني كلمة gopher مبرمج Go. وقد يكون هذا اللقب نابعًا من أن لغة Go اتخذت فأر حقل كرتونيًّا تميمةً لها. بالمناسبة، صمّمت فأر الحقل الكرتوني هذا رينيه فرنش (Renee French)، وهي زوجة قائد مشروع Go (الأول)، روب بايك (Rob Pike).