الأنواع
في القسم السابق عن أنواع البيانات، تم تقديم جميع أنواع البيانات المدمجة في Go باختصار. هذه الأنواع الأساسية المدمجة هي أساس الأنواع المخصصة لاحقًا. Go لغة ثابتة النوع نموذجية، حيث يتم تحديد نوع جميع المتغيرات في مرحلة التجميع، ولا يتغير طوال دورة حياة البرنامج. في هذا القسم، سنقدم باختصار نظام أنواع Go والاستخدام الأساسي.
نوع ثابت قوي
Go لغة ثابتة النوع وقوية. الثبات يعني أن نوع جميع المتغيرات في Go يتم تحديده في مرحلة التجميع، ولا يتغير خلال دورة حياة البرنامج. على الرغم من أن تعريف المتغيرات القصيرة في Go يشبه قليلاً طريقة الكتابة في اللغات الديناميكية، إلا أن نوع المتغير يتم استنتاجه بواسطة المترجم، والفرق الجوهري هو أن نوعه بمجرد استنتاجه لن يتغير مرة أخرى، وهو عكس اللغات الديناميكية تمامًا. لذا الكود التالي لا يمكن أن يمر من التجميع، لأن a متغير من نوع int، ولا يمكن إسناد سلسلة نصية له.
func main() {
var a int = 64
a = "64"
fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}القوة تعني أن البرنامج ينفذ فحصًا صارمًا للأنواع، وإذا حدث عدم تطابق في الأنواع، يخبر المبرمج فورًا أنه لا ينبغي فعل ذلك، بدلاً من محاولة استنتاج النتيجة المحتملة كما في اللغات الديناميكية. لذا الكود التالي لا يمكن أن يمر من التجميع، لأن النوعين مختلفين ولا يمكن إجراء عملية حسابية.
func main() {
fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}النوع اللاحق
لماذا تضع Go تعريف النوع في الخلف وليس في المقدمة؟ هذا إلى حد كبير تعلم من دروس لغة C. خذ مثالًا رسميًا لإظهار التأثير، هذا مؤشر دالة:
int (*(*fp)(int (*)(int, int), int))(int, int)بصراحة، من الصعب معرفة ما هو هذا النوع بدون نظرة دقيقة. في Go، الكتابة المشابهة كالتالي:
f func(func(int,int) int, int) func(int, int) intطريقة تعريف Go تتبع دائمًا مبدأ الاسم في المقدمة والنوع في الخلف، وبالقراءة من اليسار إلى اليمين، يمكن معرفة أن هذه دالة من النظرة الأولى، وقيمتها المرجعة هي func(int,int) int. عندما يصبح النوع أكثر تعقيدًا، وضع النوع في الخلف أفضل بشكل كبير في القراءة، والعديد من جوانب تصميم Go تخدم القراءة.
تعريف النوع
في Go، من خلال تعريف النوع، يمكن تعريف نوع جديد باسم مخصص. تعريف نوع جديد عادة ما يتطلب اسم النوع ونوع أساسي. مثال بسيط:
type MyInt int64في تعريف النوع أعلاه، تم تعريف نوع باسم MyInt ونوعه الأساسي int64 من خلال الكلمة المفتاحية type. في Go، كل نوع جديد معرف يجب أن يكون له نوع أساسي مقابل، ولا يُنصح بتكرار اسم النوع مع المعرفات المدمجة الموجودة.
type MyInt int64
type MyFloat64 float64
type MyMap map[string]int
// يمكن أن يمر من التجميع، لكن لا يُنصح به، هذا سيغطي النوع الأصلي
type int int64الأنواع المعرفة من خلال تعريف النوع كلها أنواع جديدة، والأنواع المختلفة لا يمكنها إجراء عمليات حسابية، حتى لو كان النوع الأساسي هو نفسه.
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)invalid operation: f1 + f (mismatched types MyFloat64 and float64)الاسم المستعار للنوع
الاسم المستعار للنوع يختلف عن تعريف النوع، الاسم المستعار للنوع هو مجرد اسم مستعار، وليس إنشاء نوع جديد. مثال بسيط:
type Int = intكلاهما نفس النوع، فقط الاسم مختلف، لذا يمكن إجراء عملية حسابية، والمثال التالي بطبيعة الحال يمكن أن يمر من التجميع.
type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)3الاسم المستعار للنوع مفيد جدًا لبعض الأنواع المعقدة جدًا، مثلًا يوجد الآن نوع map[string]map[string]int، وهذا خريطة ثنائية الأبعاد. يوجد الآن دالة معاملها من نوع map[string]map[string]int، كالتالي:
func PrintMyMap(mymap map[string]map[string]int) {
fmt.Println(mymap)
}في هذه الحالة، لا داعي لاستخدام تعريف النوع، لأن الأول يعرف نوعًا جديدًا، ولا يمكن استخدامه كمعامل لهذه الدالة. مثال استخدام الاسم المستعار للنوع:
type TwoDMap = map[string]map[string]int
func PrintMyMap(mymap TwoDMap) {
fmt.Println(mymap)
}استخدام الاسم المستعار للنوع يجعله يبدو أكثر إيجازًا.
TIP
النوع المدمج any هو الاسم المستعار لـ interface{}، وكلاهما متكافئ تمامًا، فقط الاسم مختلف.
تحويل النوع
في Go، يوجد فقط تحويل النوع الصريح، لا يوجد تحويل النوع الضمني، لذلك المتغيرات من أنواع مختلفة لا يمكنها إجراء عمليات حسابية، ولا يمكن تمريرها كمعاملات. شرط تطبيق تحويل النوع هو معرفة نوع المتغير المحول والنوع المستهدف المراد التحويل إليه. مثال:
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)0.30000000000000004من خلال التحويل الصريح لـ MyFloat64 إلى نوع float64، يمكن إجراء عملية الجمع. شرط آخر لتحويل النوع هو: يجب أن يكون النوع المحول قابلاً للتمثيل بالنوع المستهدف (Representability)، مثلًا int يمكن تمثيله بنوع int64، ويمكن أيضًا تمثيله بنوع float64، لذا يمكن إجراء تحويل نوع صريح بينهما. لكن نوع int لا يمكن تمثيله بنوعي string و bool، لذلك لا يمكن إجراء تحويل النوع.
TIP
لمعرفة المزيد عن تعريف التمثيل (Representability)، انتقل إلى دليل المرجع - Representability
حتى إذا كان نوعان يمكن تمثيلهما ببعضهما، نتيجة تحويل النوع ليست دائمًا صحيحة. انظر المثال التالي:
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))1 0تم تحويل num1 بشكل صحيح إلى نوع int32، لكن num2 لم يتحول بشكل صحيح. هذه مشكلة فيضان رقمي نموذجية. int32 يمكنه تمثيل أعداد صحيحة 31 بت، بينما int8 يمكنه تمثيل أعداد صحيحة 7 بت فقط. الأعداد الصحيحة عالية الدقة عند تحويلها إلى أعداد صحيحة منخفضة الدقة تتخلص من البتات العالية وتحتفظ بالبتات المنخفضة، لذا نتيجة تحويل num1 هي 0. في تحويل النوع للأرقام، عادة يُنصح بالتحويل من الصغير إلى الكبير، ولا يُنصح بالتحويل من الكبير إلى الصغير.
عند استخدام تحويل النوع، يجب تجنب الغموض لبعض الأنواع، مثال:
*Point(p) // مكافئ لـ *(Point(p))
(*Point)(p) // تحويل p إلى نوع *Point
<-chan int(c) // مكافئ لـ <-(chan int(c))
(<-chan int)(c) // تحويل c إلى نوع <-chan int
(func())(x) // تحويل x إلى نوع func()
(func() int)(x) // تحويل x إلى نوع func() intتأكيد النوع
تأكيد النوع يستخدم عادة لتحديد ما إذا كان متغير من نوع واجهة معينة ينتمي إلى نوع معين. مثال:
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
fmt.Println(intVal)
} else {
fmt.Println("error type")
}1بما أن interface{} نوع واجهة فارغة، ونوع الواجهة الفارغة يمكنه تمثيل جميع الأنواع، لكن نوع int لا يمكنه تمثيل نوع interface{}، لذا لا يمكن استخدام تحويل النوع. بينما تأكيد النوع يمكنه تحديد ما إذا نوعه الأساسي هو النوع المطلوب. جملة تأكيد النوع لها قيمتان مرجعتان: الأولى هي القيمة بعد تحويل النوع، والثانية هي القيمة المنطقية لنتيجة التحويل.
تحديد النوع
في Go، جملة switch تدعم أيضًا طريقة كتابة خاصة، من خلال هذه الطريقة يمكن اتخاذ معالجات منطقية مختلفة بناءً على case المختلفة. شرط الاستخدام هو أن يكون المعامل الداخل من نوع واجهة. مثال:
var a interface{} = 2
switch a.(type) {
case int: fmt.Println("int")
case float64: fmt.Println("float")
case string: fmt.Println("string")
}intTIP
من خلال العمليات التي توفرها حزمة unsafe، يمكن تجاوز نظام أنواع Go، ويمكن القيام بعمليات تحويل النوع التي لا يمكن أن تمر من التجميع في الأصل.
