ستسرد هذه المقالة جميع أنواع استثناءات الصياغة والدلالات في Go. بعض هذه الاستثناءات سكر نحوي يجعل البرمجة أكثر سهولة، وبعضها ناتج عن امتيازات مدمجة تخص الأجناس العامة، وبعضها موجود لأسباب تاريخية، وبعضها موجود لأسباب أخرى في المنطق.
استدعاءات الدوال المتداخلة
القاعدة الأساسية:
إذا كان عدد نتائج الإرجاع لاستدعاء دالة ليس صفرًا، وكانت نتائج الإرجاع قابلة للاستخدام كمعطيات كاملة لاستدعاء دالة أخرى، فيمكن حينئذٍ تداخل استدعاء الدالة الأول داخل استدعاء الدالة الثاني، ولا يمكن للاستدعاء المتداخل الأول أن يختلط مع معطيات أخرى للاستدعاء المُتداخِل الثاني.
السكر النحوي:
إذا أرجع استدعاء دالة نتيجة واحدة بالضبط، فيمكن دائمًا استخدام استدعاء الدالة كمعطى أحادي القيمة في استدعاءات دوال أخرى، ويمكن لاستدعاء الدالة أحادي النتيجة أن يختلط مع معطيات أخرى لاستدعاءات الدوال المتداخلة.
مثال:
package main
import (
"fmt"
)
func f0() float64 {return 1}
func f1() (float64, float64) {return 1, 2}
func f2(float64, float64) {}
func f3(float64, float64, float64) {}
func f4()(x, y []int) {return}
func f5()(x map[int]int, y int) {return}
type I interface {m()(float64, float64)}
type T struct{}
func (T) m()(float64, float64) {return 1, 2}
func main() {
// هذه الأسطر تُصرَّف بنجاح.
f2(f0(), 123)
f2(f1())
fmt.Println(f1())
_ = complex(f1())
_ = complex(T{}.m())
f2(I(T{}).m())
// هذه الأسطر لا تُصرَّف.
/*
f3(123, f1())
f3(f1(), 123)
*/
// لا يُصرَّف هذا السطر بنجاح إلا بدءًا من
// Go Toolchain 1.15.
println(f1())
// لا تُصرَّف الأسطر الثلاثة التالية بنجاح
// إلا بدءًا من Go Toolchain 1.13.
copy(f4())
delete(f5())
_ = complex(I(T{}).m())
}
اختيار حقول البنية
القاعدة الأساسية:
قيم المؤشرات ليس لها حقول.
السكر النحوي:
يمكننا اختيار حقول قيمة بنية عبر مؤشرات تلك القيمة.
مثال:
package main
type T struct {
x int
}
func main() {
var t T
var p = &t
p.x *= 2
// السطر أعلاه سكر نحوي للسطر التالي.
(*p).x *= 2
}
معطيات المستقبِل في استدعاءات الطرائق
القاعدة الأساسية:
الطرائق المُعرَّفة صراحةً للنوع *T ليست بالتأكيد طرائق للنوع T.
السكر النحوي:
رغم أن الطرائق المُعرَّفة صراحةً للنوع *T ليست طرائق للنوع T، يمكن استخدام القيم القابلة للعنونة من النوع T كمعطيات المستقبِل في استدعاءات هذه الطرائق.
مثال:
package main
type T struct {
x int
}
func (pt *T) Double() {
pt.x *= 2
}
func main() {
// T{3}.Double() // يفشل هذا السطر في التصريف.
var t = T{3}
t.Double() // أصبح t.x == 6 الآن
// السطر أعلاه سكر نحوي للسطر التالي.
(&t).Double() // أصبح t.x == 12 الآن
}
أخذ عناوين القيم الحرفية المركّبة
القاعدة الأساسية:
القيم الحرفية غير قابلة للعنونة، ولا يمكن أخذ عناوين القيم غير القابلة للعنونة.
السكر النحوي:
رغم أن القيم الحرفية المركّبة غير قابلة للعنونة، يمكن أخذ عناوينها صراحةً.
يُرجى قراءة البنى والحاويات للتفاصيل.
المُحدِّدات على المؤشرات المسمّاة أحادية المستوى
القاعدة الأساسية:
عمومًا، لا يمكن استخدام المُحدِّدات على قيم أنواع المؤشرات المسمّاة.
السكر النحوي:
إذا كانت x قيمة من نوع مؤشر مسمّى أحادي المستوى، وكان المُحدِّد (*x).f مُحدِّدًا صالحًا، فإن x.f مُحدِّد صالح أيضًا، ويمكن اعتباره اختصارًا لـ (*x).f.
لا يمكن أبدًا استخدام المُحدِّدات على قيم أنواع المؤشرات متعددة المستويات، سواء كانت أنواع المؤشرات متعددة المستويات مسمّاة أم لا.
استثناء على السكر النحوي:
لا يكون السكر النحوي صالحًا إلا إذا كان f يشير إلى حقل بنية، ولا يكون صالحًا إذا كان f يشير إلى طريقة.
مثال:
package main
type T struct {
x int
}
func (T) y() {
}
type P *T
type PP **T // نوع مؤشر متعدد المستويات
func main() {
var t T
var p P = &t
var pt = &t // نوع pt هو *T
var ppt = &pt // نوع ppt هو **T
var pp PP = ppt
_ = pp
_ = (*p).x // صالح
_ = p.x // صالح أيضًا (لأن x حقل)
_ = (*p).y // صالح
// _ = p.y // غير صالح (لأن y طريقة)
// جميع ما يلي غير صالح.
/*
_ = ppt.x
_ = ppt.y
_ = pp.x
_ = pp.y
*/
}
قابلية عنونة الحاوية وعناصرها
القاعدة الأساسية:
إذا كانت الحاوية قابلة للعنونة، فإن عناصرها قابلة للعنونة أيضًا.
استثناء:
عناصر الخريطة غير قابلة للعنونة دائمًا، حتى لو كانت الخريطة نفسها قابلة للعنونة.
السكر النحوي:
عناصر الشريحة قابلة للعنونة دائمًا، حتى لو كانت الشريحة نفسها غير قابلة للعنونة.
مثال:
package main
func main() {
var m = map[string]int{"abc": 123}
_ = &m // حسن
// الاستثناء:
// p = &m["abc"] // خطأ: عناصر الخريطة غير قابلة للعنونة
// السكر النحوي:
f := func() []int { // نتائج الإرجاع غير قابلة للعنونة
return []int{0, 1, 2}
}
// _ = &f() // خطأ: f() غير قابلة للعنونة
_ = &f()[2] // حسن
}
تعديل القيم غير القابلة للعنونة
القاعدة الأساسية:
لا يمكن تعديل القيم غير القابلة للعنونة. بعبارة أخرى، لا ينبغي أن تظهر القيم غير القابلة للعنونة في الإسنادات كقيم وجهة.
استثناء:
رغم أن قيم عناصر الخريطة غير قابلة للعنونة، يمكن تعديلها وأن تظهر في الإسنادات كقيم وجهة. (لكن لا يمكن تعديل عناصر الخريطة جزئيًا، بل يمكن الكتابة فوقها كاملة فقط، أي استبدالها.)
مثال:
package main
func main() {
type T struct {
x int
}
var mt = map[string]T{"abc": {123}}
// عناصر الخريطة غير قابلة للعنونة.
// _ = &mt["abc"] // خطأ
// التعديل الجزئي غير مسموح.
// mt["abc"].x = 456 // خطأ
// لا بأس في استبدال عنصر خريطة كاملًا.
mt["abc"] = T{x: 789}
}
وسائط الدوال
القاعدة الأساسية:
كل وسيط هو قيمة من نوع ما.
استثناء:
الوسائط الأولى للدالتين المدمجتين make وnew هي أنواع.
أسماء الدوال في الحزمة الواحدة
القاعدة الأساسية:
لا يمكن تكرار أسماء الدوال المُعرَّفة في الحزمة الواحدة.
استثناء:
يمكن تعريف عدة دوال بأسماء مثل init (وبأنواع مثل func()).
استدعاءات الدوال
القاعدة الأساسية:
يمكن استدعاء الدوال التي ليست أسماؤها المعرّف الفارغ في شيفرة المستخدم.
استثناء:
لا يمكن استدعاء دوال init في شيفرة المستخدم.
استخدام الدوال كقيم
القاعدة الأساسية:
يمكن استخدام الدوال المُعرَّفة كقيم دوال.
استثناء:
لا يمكن استخدام دوال init كقيم دوال.
مثال:
package main
import "fmt"
import "unsafe"
func init() {}
func main() {
// هذه الأسطر حسنة.
var _ = main
var _ = fmt.Println
// يفشل هذا السطر في التصريف.
var _ = init
}
طرق تمرير معطيات النوع
القاعدة الأساسية:
في قائمة معطيات النوع، تُحاط جميع معطيات النوع بأقواس مربعة وتُفصل بفواصل في القائمة.
استثناء:
تختلف طرق قوائم معطيات النوع المُمرَّرة إلى تجسيدات الأنواع العامة المدمجة باختلاف الصيغ. يُحاط معطى نوع المفتاح لكل نوع خريطة مُجسَّد بأقواس مربعة على حدة. ولا تُحاط معطيات النوع الأخرى. ويُحاط معطى النوع لتجسيد الدالة العامة المدمجة new بأقواس هلالية. أما معطى النوع لتجسيد الدالة العامة المدمجة make فيختلط مع معطيات القيم، وتُحاط معطيات النوع والقيم هذه بأقواس هلالية.
تجاهل قيم الإرجاع لاستدعاءات الدوال
القاعدة الأساسية:
يمكن تجاهل قيم الإرجاع لاستدعاء دالة كليًا.
استثناء:
لا يمكن تجاهل قيم الإرجاع لاستدعاءات الدوال المدمجة الموثّقة في الحزمتين المعياريتين builtin وunsafe، إذا كانت الدالة المستدعاة لها نتائج إرجاع.
استثناء داخل استثناء:
يمكن تجاهل جميع قيم الإرجاع لاستدعاء الدالتين المدمجتين copy وrecover، حتى لو كان للدالتين نتائج إرجاع.
المتغيرات المُعرَّفة
القاعدة الأساسية:
المتغيرات المُعرَّفة قابلة للعنونة دائمًا.
استثناء:
المتغير nil المُعرَّف مسبقًا غير قابل للعنونة.
لذا فإن nil متغير غير قابل للتغيير.
تمرير المعطيات
القاعدة الأساسية:
لا يمكن تمرير معطى إلى وسيط الدالة المقابل إلا إذا كان المعطى قابلًا للإسناد إلى نوع وسيط الدالة المقابل.
السكر النحوي:
إذا كان معطى الشريحة الأول لاستدعاء الدالة copy أو append شريحة بايتات، فيمكن أن يكون المعطى الثاني نصًا، مع أن قيمة النص غير قابلة للإسناد إلى نوع الوسيط الثاني (وهو أيضًا شريحة بايتات). (في استدعاء append، افترض أن المعطى الثاني مُمرَّر بالصيغة arg....)
مثال:
package main
func main() {
var bs = []byte{1, 2, 3}
var s = "xyz"
copy(bs, s)
// السطر أعلاه سكر نحوي (وتحسين)
// للسطر التالي.
copy(bs, []byte(s))
bs = append(bs, s...)
// السطر أعلاه سكر نحوي (وتحسين)
// للسطر التالي.
bs = append(bs, []byte(s)...)
}
المقارنات
القاعدة الأساسية:
لا تدعم أنواع الخرائط والشرائح والدوال المقارنة.
استثناء:
يمكن مقارنة قيم الخرائط والشرائح والدوال بالمعرّف غير المُنمّط المُعرَّف مسبقًا nil.
مثال:
package main
func main() {
var s1 = []int{1, 2, 3}
var s2 = []int{7, 8, 9}
//_ = s1 == s2 // خطأ: لا يمكن مقارنة قيم الشرائح
_ = s1 == nil // حسن
_ = s2 == nil // حسن
var m1 = map[string]int{}
var m2 = m1
// _ = m1 == m2 // خطأ: لا يمكن مقارنة قيم الخرائط
_ = m1 == nil
_ = m2 == nil
var f1 = func(){}
var f2 = f1
// _ = f1 == f2 // خطأ: لا يمكن مقارنة الدوال
_ = f1 == nil
_ = f2 == nil
}
المقارنات 2
القاعدة الأساسية:
إذا كان يمكن تحويل قيمة ضمنيًا إلى نوع قابل للمقارنة، فيمكن مقارنة القيمة بقيم ذلك النوع القابل للمقارنة.
استثناء:
لا يمكن مقارنة قيم نوع غير واجهي غير قابل للمقارنة بقيم نوع واجهي، حتى لو كان النوع غير الواجهي غير القابل للمقارنة يطبّق النوع الواجهي (بحيث يمكن تحويل قيم النوع غير الواجهي غير القابل للمقارنة ضمنيًا إلى النوع الواجهي).
يُرجى قراءة قواعد المقارنة للأمثلة.
القيم الحرفية المركّبة الفارغة
القاعدة الأساسية:
إذا كانت قيم النوع T يمكن تمثيلها بقيم حرفية مركّبة، فإن T{} هي قيمته الصفرية.
استثناء:
بالنسبة لنوع خريطة أو شريحة T، فإن T{} ليست قيمته الصفرية. بل تُمثَّل قيمته الصفرية بـ nil.
مثال:
package main
import "fmt"
func main() {
// تُرجع new(T) عنوان القيمة الصفرية للنوع T.
type T0 struct {
x int
}
fmt.Println( T0{} == *new(T0) ) // true
type T1 [5]int
fmt.Println( T1{} == *new(T1) ) // true
type T2 []int
fmt.Println( T2{} == nil ) // false
type T3 map[int]int
fmt.Println( T3{} == nil ) // false
}
تكرار عناصر الحاوية
القاعدة الأساسية:
لا يمكن استخدام range إلا على قيم الحاويات، والقيم المُكرَّرة هي عناصر الحاوية. ويُرجَع مفتاح/فهرس العنصر أيضًا إلى جانب كل عنصر مُكرَّر.
الاستثناء 1:
تكون القيم المُكرَّرة من نوع rune إذا كانت الحاويات المُكرَّرة نصوصًا، بدلًا من عناصر البايتات للنصوص.
الاستثناء 2:
لا يُرجَع فهرس العنصر (ترتيبه) إلى جانب كل عنصر مُكرَّر عند التكرار على القنوات.
السكر النحوي:
يمكن أيضًا استخدام range على مؤشرات المصفوفات لتكرار عناصر المصفوفة، رغم أن المؤشرات ليست حاويات.
طرائق الأنواع المدمجة
القاعدة الأساسية:
عمومًا، لا تمتلك الأنواع المدمجة طرائق.
استثناء:
النوع المدمج error له طريقة Error() string.
أنواع القيم
القاعدة الأساسية:
لكل قيمة إما نوع أو نوع افتراضي.
استثناء:
القيمة nil غير المُنمّطة ليس لها نوع ولا نوع افتراضي.
القيم الثابتة
القاعدة الأساسية:
القيم الثابتة لا تتغير أبدًا. ويمكن إسناد الثوابت إلى المتغيرات.
استثناء:
iota المُعرَّف مسبقًا ثابت مدمج مرتبط بـ 0، لكن قيمته ليست ثابتة. ستبدأ قيمته من 0 وتزداد مواصفة ثابت تلو مواصفة ثابت في تعريف ثوابت، مع أن الزيادات تحدث في وقت التصريف.
الاستثناء 2:
لا يمكن استخدام iota إلا داخل تعريفات الثوابت. ولا يمكن إسناده إلى متغيرات في تعريفات المتغيرات.
تغيّر السلوك الناتج عن تجاهل نتائج التقييم الاختيارية للتعبيرات
القاعدة الأساسية:
لن يؤثر وجود نتيجة التقييم الاختيارية لتعبير ما أو عدمه على سلوك البرنامج.
استثناء:
سيؤدي إغفال قيمة النتيجة الاختيارية في تأكيد النوع إلى حدوث panic في الـ goroutine الحالية إذا فشل تأكيد النوع.
مثال:
package main
func main() {
var ok bool
var m = map[int]int{}
_, ok = m[123] // لن يحدث panic
_ = m[123] // لن يحدث panic
var c = make(chan int, 2)
c <- 123
close(c)
_, ok = <-c // لن يحدث panic
_ = <-c // لن يحدث panic
var v interface{} = "abc"
_, ok = v.(int) // لن يحدث panic
_ = v.(int) // سيحدث panic!
}
الكلمة المفتاحية else متبوعة بكتلة شيفرة
القاعدة الأساسية:
يجب أن تتبع الكلمة المفتاحية else كتلة شيفرة صريحة {...}.
السكر النحوي:
يمكن أن تتبع الكلمة المفتاحية else كتلة شيفرة if (وهي ضمنية).
على سبيل المثال، في الشيفرة التالية، تُصرَّف الدالة foo بنجاح، أما الدالة bar فلا.
func f() []int {
return nil
}
func foo() {
if vs := f(); len(vs) == 0 {
} else if len(vs) == 1 {
}
if vs := f(); len(vs) == 0 {
} else {
switch {
case len(vs) == 1:
default:
}
}
if vs := f(); len(vs) == 0 {
} else {
for _, _ = range vs {
}
}
}
func bar() {
if vs := f(); len(vs) == 0 {
} else switch { // خطأ
case len(vs) == 1:
default:
}
if vs := f(); len(vs) == 0 {
} else for _, _ = range vs { // خطأ
}
}