الكتاب الأساسي

نصائح Go 101

فهرس

كيف تُجبر مستخدمي الحزمة على استخدام القيم الحرفية التركيبية للبنى مع أسماء الحقول؟

يمكن لمطوّري الحزم وضع حقل بحجم صفري غير مُصدَّر في تعريف نوع بنية، بحيث تمنع المصرِّفات مستخدمي الحزمة من استخدام قيم حرفية تركيبية تحتوي بعض عناصر الحقول لكن دون أسماء الحقول لإنشاء قيم من نوع البنية.

مثال:


// foo.go
package foo

type Config struct {
	_    [0]int
	Name string
	Size int
}

// main.go
package main

import "foo"

func main() {
	//_ = foo.Config{[0]int{}, "bar", 123} // خطأ
	_ = foo.Config{Name: "bar", Size: 123} // التصريف ناجح
}

يرجى محاولة عدم وضع الحقل ذي الحجم الصفري غير المُصدَّر كحقل أخير في البنية، لأن القيام بذلك قد يكبّر حجم نوع البنية.

كيف تجعل نوع بنية غير قابل للمقارنة؟

أحيانًا نريد منع استخدام نوع بنية مخصص كأنواع مفاتيح خرائط، فيمكننا وضع حقل من نوع غير قابل للمقارنة بحجم صفري وغير مُصدَّر في نوع بنية لجعل نوع البنية غير قابل للمقارنة. على سبيل المثال:


package main

type T struct {
	dummy        [0]func()
	AnotherField int
}

var x map[T]int // خطأ تصريف: نوع مفتاح الخريطة T غير صالح

func main() {
	var a, b T
	_ = a == b // خطأ تصريف: عملية غير صالحة:
}

لا تستخدم إسنادات القيم مع تعبيرات يتفاعل بعضها مع بعض.

حاليًا (Go 1.25)، هناك بعض ترتيبات التقييم في الإسناد متعدد القيم غير محددة عندما تتفاعل التعبيرات المشاركة في الإسناد متعدد القيم مع بعضها. لذا حاول تقسيم الإسناد متعدد القيم إلى عدة إسنادات ذات قيمة واحدة إذا كانت هناك، أو لم تستطع التأكد من وجود، تبعيات بين التعبيرات المشاركة.

في الواقع، توجد أيضًا غموض في ترتيب تقييم التعبيرات في بعض الإسنادات سيئة الكتابة ذات القيمة الواحدة. على سبيل المثال، قد يطبع البرنامج التالي [7 0 9] أو [0 8 9] أو [7 8 9]، حسب تنفيذ المصرِّف.


package main

import "fmt"

var a = &[]int{1, 2, 3}
var i int
func f() int {
	i = 1
	a = &[]int{7, 8, 9}
	return 0
}

func main() {
	// ترتيب تقييم "a" و"i"
	// و"f()" غير محدد.
	(*a)[i] = f()
	fmt.Println(*a)
}

بعبارة أخرى، قد يؤثر استدعاء دالة في إسناد قيمة على نتائج تقييم التعبيرات غير الاستدعائية في الإسناد نفسه. يرجى قراءة ترتيبات التقييم في Go للتفاصيل.

كيف تحاكي for i in 0..N في بعض اللغات الأخرى؟

يمكننا التكرار على مصفوفة بعنصر بحجم صفري أو مؤشر مصفوفة nil لمحاكاة حلقة كهذه. على سبيل المثال:


package main

import "fmt"

func main() {
	const N = 5

	for i := range [N]struct{}{} {
		fmt.Println(i)
	}
	for i := range [N][0]int{} {
		fmt.Println(i)
	}
	for i := range (*[N]int)(nil) {
		fmt.Println(i)
	}
}

لاحظ أنه منذ Go 1.22، يمكننا ببساطة استخدام for i = range N.


package main

import "fmt"

func main() {
	const N = 5

	for i := range N {
		fmt.Println(i)
	}
}

ينبغي تصفير المؤشرات في خانات العناصر المحرَّرة في جميع أنواع التلاعبات بالشرائح لتجنب تسريب الذاكرة إذا لم نستطع التأكد من أن خانات العناصر المحرَّرة ستُعاد استخدامها لاحقًا.

يرجى قراءة كيفية حذف عناصر الشريحة والتسريب الشكلي للذاكرة الناتج عن عدم تصفير المؤشرات في عناصر الشرائح الميتة للتفاصيل.

قيم بعض الأنواع في الحزم القياسية ليس من المتوقع نسخها.

لا يُنصح بنسخ قيم النوع bytes.Buffer والنوع strings.Builder والأنواع في الحزمة القياسية sync. (بل حقًا لا ينبغي نسخها، رغم أنه لا مشكلة في نسخها في بعض الظروف المحددة.)

سيكتشف تنفيذ strings.Builder نسخ قيم strings.Builder غير الصالحة. وبمجرد العثور على نسخة كهذه، سيحدث panic. على سبيل المثال:


package main

import "strings"

func main() {
	var b strings.Builder
	b.WriteString("hello ")
	var b2 = b
	b2.WriteString("world!") // يحدث panic هنا
}

سيحذّر أمر go vet الموفَّر في سلسلة أدوات Go من نسخ قيم الأنواع في الحزمة القياسية sync.


// demo.go
package demo

import "sync"

func f(m sync.Mutex) { // تحذير
	m.Lock()
	defer m.Unlock()
	// افعل شيئًا ...
}

$ go vet demo.go
./demo.go:5: f passes lock by value: sync.Mutex

لن يُكتشف نسخ قيم bytes.Buffer أبدًا في وقت التشغيل ولا بواسطة أمر go vet. فقط كن حريصًا على عدم فعل ذلك.

يمكننا استخدام تحسين memclr لتصفير بعض العناصر المتجاورة في مصفوفة أو شريحة.

يرجى قراءة تحسين memclr للتفاصيل.

كيف تتحقق مما إذا كانت قيمة ما تملك طريقة دون استيراد حزمة reflect؟

استخدم الطريقة في المثال التالي. (افترض أن الطريقة المطلوب التحقق منها هي M(int) string.)


package main

import "fmt"

type A int
type B int
func (b B) M(x int) string {
	return fmt.Sprint(b, ": ", x)
}

func check(v interface{}) bool {
	_, has := v.(interface{M(int) string})
	return has
}

func main() {
	var a A = 123
	var b B = 789
	fmt.Println(check(a)) // false
	fmt.Println(check(b)) // true
}

كيف تنسخ شريحة بكفاءة وإتقان؟

يرجى قراءة مقالة الويكي هذه ومقالة الويكي هذه للتفاصيل.

ينبغي استخدام صيغة الشريحة الفرعية ثلاثية الفهارس في بعض السيناريوهات.

افترض أن حزمة توفر دالة func NewX(...Option) *X، وأن تنفيذ هذه الدالة سيدمج الخيارات المُدخَلة مع بعض الخيارات الافتراضية الداخلية، فإن التنفيذ التالي غير مُوصى به.


func NewX(opts ...Option) *X {
	options := append(opts, defaultOpts...)
	// استخدم الخيارات المدمجة لبناء X وإرجاعه.
	// ...
}

سبب عدم التوصية بالتنفيذ أعلاه هو أن استدعاء append قد يعدّل تسلسل Option الأساسي للمعطى opts. في معظم السيناريوهات، لا يمثل ذلك مشكلة. لكنه في بعض السيناريوهات الخاصة قد يسبب نتائج غير متوقعة.

لتجنب تعديل تسلسل Option الأساسي للمعطى المُدخَل، ينبغي استخدام الطريقة التالية بدلًا من ذلك.


func NewX(opts ...Option) *X {
	opts = append(opts[:len(opts):len(opts)], defaultOpts...)
	// استخدم الخيارات المدمجة لبناء X وإرجاعه.
	// ...
}

من ناحية أخرى، بالنسبة لمستدعي دالة NewX، ليس من الجيد الاعتقاد بأن دالة NewX لن تعدّل العناصر الأساسية لمعطيات الشرائح الممرَّرة والاعتماد على ذلك، لذا من الأفضل تمرير هذه المعطيات بصيغة الشريحة الفرعية ثلاثية الفهارس.

ذُكر سيناريو آخر ينبغي فيه استخدام صيغة الشريحة الفرعية ثلاثية الفهارس في مقالة الويكي هذه.

من عيوب صيغ الشرائح الفرعية ثلاثية الفهارس أنها مطوَّلة بعض الشيء. في الواقع، قدّمت اقتراحًا لجعلها أقل إطالة، لكنه رُفض.

استخدم الدوال المجهولة لجعل بعض استدعاءات الدوال المؤجَّلة تُنفَّذ أبكر.

يرجى قراءة هذا المقال للتفاصيل.

تأكد من أن نوعًا معرَّفًا مخصصًا يُنفِّذ نوع واجهة محدَّدًا، وأظهر ذلك.

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


package myreader

import "io"

type MyReader uint16

func NewMyReader() *MyReader {
	var mr MyReader
	return &mr
}

func (mr *MyReader) Read(data []byte) (int, error) {
	switch len(data) {
	default:
		*mr = MyReader(data[0]) << 8 | MyReader(data[1])
		return 2, nil
	case 2:
		*mr = MyReader(data[0]) << 8 | MyReader(data[1])
	case 1:
		*mr = MyReader(data[0])
	case 0:
	}
	return len(data), io.EOF
}

// أي من الأسطر الثلاثة التالية يضمن
// أن النوع *MyReader يُنفِّذ io.Reader.
var _ io.Reader = NewMyReader()
var _ io.Reader = (*MyReader)(nil)
func _() {_ = io.Reader(nil).(*MyReader)}

بعض حيل التحقق في وقت التصريف.

إلى جانب الحيلة أعلاه، توجد حيل أخرى للتحقق في وقت التصريف.

عدة طرق لضمان أن ثابتًا N ليس أصغر من ثابت آخر M في وقت التصريف:


// أي من الأسطر التالية يضمن N >= M
func _(x []int) {_ = x[N-M]}
func _(){_ = []int{N-M: 0}}
func _([N-M]int){}
var _ [N-M]int
const _ uint = N-M
type _ [N-M]int

// إذا كان مضمونًا أن M و N عددان صحيحان موجبان.
var _ uint = N/M - 1

طريقة أخرى مقتبسة من @lukechampine. وهي تستفيد من القاعدة التي تنص على أنه لا يمكن أن تظهر مفاتيح ثابتة مكررة في القيمة الحرفية التركيبية نفسها.


var _ = map[bool]struct{}{false: struct{}{}, N>=M: struct{}{}}

تبدو الطريقة أعلاه مطوَّلة بعض الشيء لكنها أكثر عمومية. يمكن استخدامها للتحقق من أي شروط. ويمكن أن تكون أقل إطالة لكنها تحتاج ذاكرة أكثر قليلًا (مُهمَلة):


var _ = map[bool]int{false: 0, N>=M: 1}

وبالمثل، طرق للتحقق من أن ثابتين صحيحين متساويان:


var _ [N-M]int; var _ [M-N]int
type _ [N-M]int; type _ [M-N]int
const _, _ uint = N-M, M-N
func _([N-M]int, [M-N]int) {}

var _ = map[bool]int{false: 0, M==N: 1}

var _ = [1]int{M-N: 0} // الفهرس الصالح الوحيد هو 0
var _ = [1]int{}[M-N]  // الفهرس الصالح الوحيد هو 0

var _ [N-M]int = [M-N]int{}

طرق للتحقق من أن نصًا ثابتًا ليس فارغًا:


type _ [len(aStringConstant)-1]int
var _ = map[bool]int{false: 0, aStringConstant != "": 1}
var _ = aStringConstant[:1]
var _ = aStringConstant[0]
const _ = 1/len(aStringConstant)

السطر الأخير مقتبس من الفكرة الذكية لـ Jan Mercl.

أحيانًا، لتجنب استهلاك المتغيرات على مستوى الحزمة ذاكرة كثيرة، يمكننا وضع شيفرة التحقق في دالة معرَّفة بالمعرّف الفارغ. على سبيل المثال،


func _() {
	var _ = map[bool]int{false: 0, N>=M: 1}
	var _ [N-M]int
}

كيف تعرّف ثابتَي القيمة العظمى لـ int و uint؟


const MaxUint = ^uint(0)
const MaxInt = int(^uint(0) >> 1)

كيف تكتشف حجم الكلمة الأصلي في وقت التصريف؟

هذه النصيحة لا علاقة لها بـ Go.


const Is64bitArch = ^uint(0) >> 63 == 1
const Is32bitArch = ^uint(0) >> 63 == 0
const WordBits = 32 << (^uint(0) >> 63) // 64 أو 32

كيف تضمن أن القيمة 64-بت التي تعمل عليها استدعاءة دالة ذرّية 64-بت تكون دائمًا محاذاة 64-بت على معماريات 32-بت؟

يرجى قراءة تخطيطات ذاكرة القيم في Go للتفاصيل.

تجنب تغليف القيم كبيرة الحجم داخل قيم الواجهات.

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

في المثال التالي، تكاليف استدعاءَي الطباعة الأخيرين أقل بكثير من الأولين.


package main

import "fmt"

func main() {
	var a [1000]int

	// تكلفة هذين السطرين عالية.
	fmt.Println(a)                   // تُنسخ a
	fmt.Printf("Type of a: %T\n", a) // تُنسخ a

	// تكلفة هذين السطرين منخفضة.
	fmt.Printf("%v\n", a[:])
	fmt.Println("Type of a:", fmt.Sprintf("%T", &a)[1:])
}

حول أحجام قيم الأنواع المختلفة، يرجى قراءة تكاليف نسخ القيم في Go.

حسّن شيفرة Go بالاستفادة من BCE (إزالة فحص الحدود).

يرجى قراءة هذا المقال لتعرف ما هو BCE ومدى دعم المصرِّف القياسي لـ Go له حاليًا.