مدونة

بعض التغييرات غير الموثّقة في Go 1.18 و1.19

عند إصدار نسخة جديدة من Go، تُدرَج التغييرات في هذه النسخة عادةً في ملاحظات الإصدار الخاص بها. لكن تغيب أحياناً بعض التغييرات عن ملاحظات الإصدار.

تسرد هذه المقالة عدة تغييرات غائبة عن ملاحظات إصداري Go 1.18 و1.19.

GoTV

نستخدم GoTV [1] لعرض التغييرات غير المذكورة في هذه المقالة. GoTV (اختصار Go Toolchain Version) أداة تُستخدم لإدارة عدة تنصيبات متجاورة لنسخ سلسلة أدوات Go الرسمية واستخدامها معاً بانسجام ويسر.

إن لم تكن قد نصّبته بعد، يمكنك تنصيب GoTV بتشغيل


go install go101.org/gotv@latest

ثم شغّل الأوامر التالية لتنصيب أحدث الإصدارات من سلاسل أدوات Go 1.17 وGo 1.18 وGo 1.19.


gotv 1.17. version
gotv 1.18. version
gotv 1.19. version

يحتاج أول تنفيذ للأمر gotv إلى سحب مشروع لغة Go، لذا قد يستغرق إتمامه عدة دقائق.

توضيح تعريف مصطلح شريحة البايتات (slice of bytes)

تنص مواصفة Go على أنه يمكن تحويل قيمة غير ثابتة x إلى النوع T إذا كانت x نصاً وكان T شريحة بايتات، والعكس صحيح.

لكن ما هي شريحة البايتات؟ هناك تفسيران:

  1. أي نوع يكون نوعه الكامن هو []byte.
  2. أي نوع شريحة يكون النوع الكامن لنوع عناصره هو byte.

قبل Go 1.18، كان المصرِّف Go القياسي الرسمي يتبنّى التفسير الأول أحياناً والتفسير الثاني أحياناً أخرى. ومنذ Go 1.18، أصبح المصرِّف Go القياسي الرسمي يتبنّى التفسير الثاني دائماً. وقد اعترفت Go 1.19 رسمياً بأن تنفيذ الإصدارات 1.18+ هو الصحيح.

على سبيل المثال، يرى المصرِّف Go القياسي الرسمي 1.17 أن الدالة f في الشيفرة التالية صالحة، لكنه يرى أن الدالة g غير صالحة.


package main

type T byte

//go:noinline
func f(s string) []T {
	return []T(s)
}

//go:noinline
func g(x []T) string {
	return string(x) // خطأ (السطر 12)
}

func main() {
	_ = f("Go")
	_ = g([]T{71, 111})
}

منذ سلسلة أدوات Go 1.18، تُصرَّف كلتا الدالتين بلا مشكلة. فيما يلي مخرجات المصرِّف Go القياسي الرسمي 1.17 و1.18 للبرنامج أعلاه.


$ gotv 1.17. run main.go
[Run]: $HOME/.cache/gotv/tag_go1.17.13/bin/go run main.go
# command-line-arguments
./main.go:12:15: cannot use <node SPTR> (type *T) as type *byte in argument to runtime.slicebytetostring

$ gotv 1.18. run main.go
[Run]: $HOME/.cache/gotv/tag_go1.18.7/bin/go main main.go

يرى المصرِّف Go القياسي الرسمي 1.17 أحياناً أن []T شريحة بايتات (في الدالة f)، وأحياناً لا يراها كذلك (في الدالة g). وإذا غُيّر تعريف النوع type T byte إلى تعريف الاسم المستعار للنوع type T = byte، فستُعتبر الدالة g أيضاً صالحة من قِبل المصرِّف Go القياسي الرسمي 1.17.

وحدث الوضع نفسه مع شرائح rune (أو شريحة rune).

لاحظ أنه حتى الآن (Go 1.19)، لم تُحدَّث الحزمة القياسية reflect وفقاً لذلك. على سبيل المثال، يطبع البرنامج التالي false مرتين، لكن ينبغي أن يطبع true مرتين بدلاً من ذلك.


package main

import "reflect"

type T byte

func main() {
	var s string
	var x []T
	var sType = reflect.TypeOf(s)
	var xType = reflect.TypeOf(x)
	println(sType.ConvertibleTo(xType))
	println(xType.ConvertibleTo(sType))
}

المراجع:

تفصيل دقيق في تعريفات الثوابت المحلية

ماذا ينبغي أن يطبع البرنامج التالي؟


package main

func main() {
	const (
		iota = iota
		Y
	)
	println(Y)
}

يعتقد بعض الـ gophers أنه ينبغي أن يطبع 1، وهو في الواقع جواب المصرِّف Go القياسي الرسمي 1.17.13-. لكن منذ المصرِّف Go القياسي الرسمي 1.18، أصبح الجواب 0. فيما يلي المخرجات مع نسخ مختلفة من المصرِّف:


$ gotv 1.17. run main.go
[Run]: $HOME/.cache/gotv/tag_go1.17.13/bin/go run main.go
1

$ gotv 1.18. run main.go
[Run]: $HOME/.cache/gotv/tag_go1.18.7/bin/go run main.go
0

أي الجوابين صحيح؟

تقول مواصفة Go:

داخل قائمة تعريف ثوابت محاطة بأقواس، يمكن حذف قائمة التعبيرات من أي ConstSpec عدا الأول. وتكافئ هذه القائمة الفارغة الاستبدال النصي بأول قائمة تعبيرات غير فارغة تسبقها مع نوعها إن وُجد. لذا، فإن حذف قائمة التعبيرات يكافئ تكرار القائمة السابقة.

يشير الوصف إلى أن جواب الإصدارات 1.18+ هو الصحيح، لأن البرنامج أعلاه يكافئ


package main

func main() {
	const (
		iota = iota
		Y    = iota
	)
	println(Y)
}

آخر iota هو iota المعرَّف محلياً، وقيمته 0.

أكّدت Go 1.19 أنه كان خطأً في نسخ 1.17.13-.

المرجع: https://github.com/golang/go/issues/49157

إصلاح خطأ طويل الأمد متعلق بالأسماء المستعارة للأنواع

مع إدخال الاسم المستعار للنوع في Go 1.8، أُدخل أيضاً خطأ في المقارنة.

ينبغي أن يطبع البرنامج التالي false، لكنه يطبع true عند استخدام المصرِّف Go القياسي الرسمي من الإصدار 1.8 إلى 1.17.13.


package main

type T struct{}
type S = T

type A = struct{ T }
type B = struct{ S }

func main() {
	var a A
	var b B
	println(interface{}(b) == interface{}(a))
}

يشير الاسمان المستعاران A وB إلى نوعين مختلفين (بأسماء حقول مختلفة)، ولهذا ينبغي أن تكون نتيجة المقارنة false.

أُصلح الخطأ في Go 1.18.

فيما يلي المخرجات مع نسخ مختلفة من المصرِّف:


$ gotv 1.17. run main.go
[Run]: $HOME/.cache/gotv/tag_go1.17.13/bin/go run main.go
true

$ gotv 1.18. run main.go
[Run]: $HOME/.cache/gotv/tag_go1.18.7/bin/go run main.go
false

المرجع: https://github.com/golang/go/issues/24721

عودة مصطلح النوع المسمّى إلى مواصفة Go

مع إدخال الاسم المستعار للنوع في Go 1.8، أُزيل مصطلح النوع المسمّى من مواصفة Go. وقُدّم مصطلح جديد هو النوع المعرَّف لسدّ الفراغ الناتج عن إزالة النوع المسمّى. غير أن ذلك جعل العديد من دروس ومقالات Go القديمة عتيقة وجلب عدة مساوئ أخرى.

ومع إدخال الأجناس العامة المخصّصة، عاد مصطلح النوع المسمّى منذ Go 1.18، وهو ما يساعد على شرح العديد من قواعد التحويل والإسناد في Go بوضوح.

الآن، قد يكون النوع المسمّى

  • نوعاً مُعلناً مسبقاً؛
  • نوعاً معرَّفاً (غير عام مخصّص)؛
  • نوعاً مجسَّداً (لنوع عام)؛
  • نوع وسيط نوع (يُستخدم في الأجناس العامة المخصّصة).

لاحظ أن الاسم المستعار لنوع غير مسمّى ليس نوعاً مسمّى.

قد تُلغي إشارات Goexit حالات panic التي حدثت بالفعل (القابلة للاستعادة)

هل ينبغي أن ينهار البرنامج التالي بسبب الـ panic أم أن ينتهي طبيعياً؟


package main

import "runtime"

func main() {
	c := make(chan struct{})
	go func() {
		defer close(c)
		defer runtime.Goexit()
		panic("bye")
	}()
	<-c
}

قبل Go 1.19، لم يكن هناك جواب واضح. يعتقد بعض الـ gophers أنه ينبغي أن ينهار، لأن حالة panic "bye" لا تُستعاد أبداً. لكن في تطبيق وقت تشغيل Go القياسي الرسمي، تُلغي إشارة Goexit حالات panic التي حدثت بالفعل (القابلة للاستعادة) في الـ goroutine الحالية. لذا، مع المصرِّف القياسي الرسمي، سينتهي البرنامج أعلاه طبيعياً. قُرِّر في Go 1.19 أن هذا السلوك صحيح.

في رأيي الشخصي، لا ينبغي لإشارات Goexit أن تُلغي حالات panic، لذا ينبغي أن ينهار البرنامج أعلاه بدلاً من ذلك. لكنني أحترم القرار النهائي لفريق Go الأساسي.

يجعل القرار النهائي استدعاءات runtime.Goexit تعمل عمل استدعاءات recover فائقة. على سبيل المثال، تتصرف دالتا الـ goroutine التاليتان بالطريقة نفسها.


func goroutine1() {
	defer runtime.Goexit()
	panic(1)
}

func goroutine2() {
	defer func() {
		recover()
	}()
	panic(1)
}

تشرح وثائق Go الرسمية آلية panic/recover ببساطة شديدة بحيث تبقى عدة تفاصيل أخرى غير مغطّاة، ولهذا يتضمن كتاب Go 101 مقالاً خاصاً لشرح آلية panic/recover بالتفصيل.

المزيد من التغييرات الصغيرة في المصرِّف ووقت التشغيل

هناك أيضاً بعض التحسينات الصغيرة غير الموثّقة في المصرِّف ووقت التشغيل والتي أُجريت في نسخ سلسلة أدوات Go الأخيرة. وقد ذُكر بعض هذه التحسينات الصغيرة في كتاب Go Optimizations 101.

  1. GoTV (https://go101.org/apps-and-libs/gotv.html) أداة تُستخدم لتنصيب نسخ متجاورة من سلسلة أدوات Go واستخدامها معاً بانسجام. ↩︎