أعاد إصدار Go 1.26 كتابة go fix على إطار العمل نفسه الذي يستخدمه go vet. يجمع الأمر الجديد كل أدوات التحديث (modernizers) التي بناها فريق Go ومؤلفو x/tools. كل واحدة منها محلل (analyzer) صغير يتعرّف على نمط قديم ويعيد كتابته بأسلوب Go الحديث.
أمر واحد يشغّلها جميعًا:
example.bashbashgo fix ./...
شغّله على فرع (branch) نظيف، وراجع الفروقات (diff)، وشغّل اختباراتك، ثم نفّذ commit. وإذا أردت معاينة التغييرات دون الكتابة إلى الملفات، فاستخدم go fix -diff ./....
فيما يلي كل أداة تحديث تأتي مع الإصدار 1.26، مصنفة حسب المجال. يذكر كل بند إصدار Go الذي قدّم الواجهة البرمجية (API) التي تعيد الأداة الكتابة إليها، إلى جانب مثال قبل وبعد. المقصود أن تتصفحه سريعًا أو تبحث فيه بـ Ctrl+F بقدر ما تقرؤه.
جدول المحتويات
- اللغة والدوال المدمجة
- الـ slices والـ maps والتكرار
- السلاسل النصية والتنسيق
- الشبكات والأخطاء والانعكاس (reflection)
- التزامن والاختبارات
- تشغيل go fix بأمان
اللغة والدوال المدمجة
توجد هنا سبع أدوات تحديث. يغطي معظمها ميزات في اللغة (any، والصيغة الجديدة range n، وتغيير متغير الحلقة في 1.22، وتوسيع new(expr))، إلى جانب عمليتي تنظيف تمتدان عبر عدة إصدارات.
any (Go 1.18): استبدال interface{} بـ any
any مجرد اسم بديل (alias)، لكنه أوضح عند القراءة في تواقيع الدوال ومعاملات الأنواع (type parameters).
قبل:
example.gogofunc log(args ...interface{}) { fmt.Println(args...) } var cache map[string]interface{}
بعد:
example.gogofunc log(args ...any) { fmt.Println(args...) } var cache map[string]any
minmax (Go 1.21): استبدال حصر القيم عبر if/else بـ min و max
الدالتان المدمجتان min و max تقبلان عددًا متغيرًا من الوسائط (variadic) وتعملان مع أي نوع قابل للترتيب.
قبل:
example.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
بعد:
example.gogox := max(0, min(100, f()))
rangeint (Go 1.22): حلقات for العددية بصيغة for i := range n
حلقة العد البسيطة، التي لا يقف خلفها slice أو array، يمكنها أن تمر على العدد الصحيح مباشرة باستخدام range.
قبل:
example.gogofor i := 0; i < n; i++ { fmt.Println(i) }
بعد:
example.gogofor i := range n { fmt.Println(i) }
forvar (Go 1.22): التخلص من حيلة الالتقاط x := x
قبل 1.22، كان متغير الحلقة مشتركًا بين كل الدورات. لذلك كانت الـ closures والـ goroutines التي تلتقطه تحتاج إلى x := x داخل جسم الحلقة. ومنذ 1.22 أصبحت كل دورة تحصل على متغير خاص بها، فلم يعد لهذا التظليل (shadowing) داعٍ.
قبل:
example.gogofor _, x := range items { x := x // pre-1.22 capture trick go func() { use(x) }() }
بعد:
example.gogofor _, x := range items { go func() { use(x) }() }
لا يعمل المحلل إلا عندما يصرّح go.mod بـ go 1.22 أو أحدث، لأن الوحدات (modules) الأقدم ما زالت تتبع دلالة المتغير المشترك.
newexpr (Go 1.26): مؤشر إلى قيمة باستخدام new(expr)
يوسّع الإصدار 1.26 الدالة المدمجة new لتقبل تعبيرًا، لا نوعًا فقط. تعيد new(s) قيمة من النوع *string تشير إلى نسخة من s. وبهذا لا تعود هناك حاجة إلى الدوال المساعدة المعتادة مثل strPtr/intPtr/boolPtr.
قبل:
example.gogofunc strPtr(s string) *string { p := s return &p } cfg.Name = strPtr("LevelUpGo")
بعد:
example.gogocfg.Name = new("LevelUpGo")
إذا كانت لديك دوال مساعدة لا يمكنك حذفها بعد (لوجود مستدعين آخرين، أو لأنها جزء من API مُصدَّرة)، فراجع inline أدناه لتتعرف على التوجيه الذي يتيح لك الترحيل تدريجيًا.
inline (توجيه): تضمين الدوال والثوابت الموسومة بـ //go:fix inline
أضف التوجيه إلى دالة مغلِّفة (wrapper)، وسيعيد go fix كتابة كل موضع استدعاء ليستخدم التعبير الداخلي مباشرة. أجده مفيدًا عند إعادة التسمية وعند التخلص من الدوال المغلِّفة الرفيعة. ويمكنك أيضًا استخدامه لنشر أدوات تحديث خاصة بك عبر قاعدة كود خاصة.
قبل:
example.gogo//go:fix inline func Square(x int) int { return Pow(x, 2) } a := Square(5) b := Square(10)
بعد:
example.gogo//go:fix inline func Square(x int) int { return Pow(x, 2) } a := Pow(5, 2) b := Pow(10, 2)
يعمل التوجيه نفسه مع الثوابت، مما يجعله مفيدًا عند الترحيل بين قيم enum أُعيدت تسميتها.
plusbuild (Go 1.17): استبدال // +build بـ //go:build
صيغة وسوم البناء (build tags) القديمة مُهملة منذ 1.17. يستخدم التوجيه الجديد تعبيرات منطقية عادية بصيغة Go.
قبل:
example.gogo// +build linux,amd64 package sys
بعد:
example.gogo//go:build linux && amd64 package sys
الـ slices والـ maps والتكرار
خمس من أدوات التحديث السبع في هذه المجموعة تنقل الحلقات المكتوبة يدويًا إلى الحزمتين slices و maps. أما الأداتان الأخريان فتتعاملان مع reflect.TypeFor ووسوم JSON مع omitzero.
slicessort (Go 1.21): تحويل sort.Slice إلى slices.Sort
في الحالة الشائعة، أي ترتيب slice من نوع قابل للترتيب ترتيبًا تصاعديًا، تكون slices.Sort دالة معممة (generic) تستغني تمامًا عن closure المقارنة.
قبل:
example.gogosort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
بعد:
example.gogoslices.Sort(s)
slicescontains (Go 1.21): تحويل حلقات البحث اليدوية إلى slices.Contains
قبل:
example.gogofunc has(s []int, x int) bool { for _, v := range s { if v == x { return true } } return false }
بعد:
example.gogofunc has(s []int, x int) bool { return slices.Contains(s, x) }
slicesbackward (Go 1.23): التكرار العكسي باستخدام slices.Backward
تعيد slices.Backward مكررًا (iterator) ينتج أزواج الفهرس والقيمة من نهاية الـ slice إلى بدايته.
قبل:
example.gogofor i := len(s) - 1; i >= 0; i-- { use(i, s[i]) }
بعد:
example.gogofor i, v := range slices.Backward(s) { use(i, v) }
mapsloop (Go 1.21): تحويل حلقات النسخ والاستنساخ والإدراج إلى استدعاءات من maps
توفر الحزمة maps دالة لكل حلقة شائعة على الـ maps، فيخبر اسم الاستدعاء القارئ بما كانت الحلقة تفعله.
قبل:
example.gogofunc copyMap(src map[string]int) map[string]int { dst := make(map[string]int, len(src)) for k, v := range src { dst[k] = v } return dst }
بعد:
example.gogofunc copyMap(src map[string]int) map[string]int { dst := make(map[string]int, len(src)) maps.Copy(dst, src) return dst }
تغطي أداة التحديث نفسها maps.Clone و maps.Equal ونمط الإدراج من أزواج المفاتيح والقيم.
stditerators (Go 1.23): استخدام دوال المكررات من المكتبة القياسية في حلقات range
تعيد maps.Keys و maps.Values و slices.All و slices.Values وأخواتها مكررات يمكنك المرور عليها مباشرة باستخدام range، فلا تحتاج إلى بناء slice وسيط أولًا.
قبل:
example.gogokeys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } for _, k := range keys { use(k) }
بعد:
example.gogofor k := range maps.Keys(m) { use(k) }
reflecttypefor (Go 1.22): التخلص من حيلة المؤشر nil مع reflect
reflect.TypeFor[T]() هي الطريقة المعممة والآمنة من حيث الأنواع للحصول على reflect.Type لنوع ساكن. أما الحل الالتفافي القديم TypeOf((*T)(nil)).Elem() فيعود إلى ما قبل ظهور generics.
قبل:
example.gogot := reflect.TypeOf((*MyType)(nil)).Elem()
بعد:
example.gogot := reflect.TypeFor[MyType]()
omitzero (Go 1.24): استبدال omitempty الهش بـ omitzero
يعتمد omitempty على كون القيمة صفرية بمفهوم Go، ما يعني أن time.Time{} ليست «فارغة» (فهي struct يحتوي على حقول)، لذلك لا تُحذف أبدًا. أما omitzero فيستدعي الدالة IsZero الخاصة بالقيمة إن وُجدت، فتختفي القيم الصفرية من time.Time من المخرجات كما تتوقع.
لا يغيّر المحلل إلا الحقول التي يكون فيها omitzero آمنًا. تبقى السلاسل النصية والـ slices والمؤشرات على omitempty لأن سلوكها سيتغير.
قبل:
example.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitempty"` }
بعد:
example.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitzero"` }
السلاسل النصية والتنسيق
تستبدل هذه الأدوات الخمس أنماطًا قديمة من strings و fmt بواجهات API أحدث. بعضها يوفّر تخصيصًا للذاكرة (allocation)، وبعضها أسهل في القراءة فحسب.
fmtappendf (Go 1.19): تحويل []byte(fmt.Sprintf(...)) إلى fmt.Appendf
تكتب fmt.Appendf المخرجات المنسقة مباشرة في []byte تمرره أنت (أو nil لتخصيص جديد)، وتتجاوز السلسلة النصية الوسيطة.
قبل:
example.gogob := []byte(fmt.Sprintf("%s=%d", key, val))
بعد:
example.gogob := fmt.Appendf(nil, "%s=%d", key, val)
stringscut (Go 1.18): تحويل strings.Index مع التقطيع إلى strings.Cut
تعيد strings.Cut الجزء الذي يسبق الفاصل والجزء الذي يليه وقيمة bool تدل على العثور عليه، كل ذلك في استدعاء واحد. كما تتعامل مع حالة عدم العثور بشكل صريح.
قبل:
example.gogoi := strings.Index(s, "=") if i >= 0 { key, val := s[:i], s[i+1:] use(key, val) }
بعد:
example.gogoif key, val, ok := strings.Cut(s, "="); ok { use(key, val) }
stringscutprefix (Go 1.20): تحويل HasPrefix مع TrimPrefix إلى CutPrefix
تعيد CutPrefix السلسلة بعد اقتطاع البادئة وقيمة bool تدل على العثور عليها، فلا تفحص البادئة إلا مرة واحدة.
قبل:
example.gogoif strings.HasPrefix(s, "go-") { rest := strings.TrimPrefix(s, "go-") handle(rest) }
بعد:
example.gogoif rest, ok := strings.CutPrefix(s, "go-"); ok { handle(rest) }
تتعامل أداة التحديث نفسها مع CutSuffix أيضًا.
stringsseq (Go 1.24): تحويل Split و Fields داخل حلقات range إلى SplitSeq و FieldsSeq
تعيد SplitSeq و FieldsSeq مكررات بدلًا من تخصيص slice. إذا كنت تحتاج إلى المرور على الأجزاء مرة واحدة فقط، فإن صيغة المكرر تتجنب التخصيص تمامًا.
قبل:
example.gogofor _, line := range strings.Split(s, "\n") { handle(line) }
بعد:
example.gogofor line := range strings.SplitSeq(s, "\n") { handle(line) }
لا يعيد المحلل الكتابة إلا في الحالات التي يُستهلك فيها الـ slice مرة واحدة ثم يُهمل. إذا خزّنت الـ slice أو وصلت إلى عناصره بالفهرس، تبقى صيغة Split الأصلية كما هي.
stringsbuilder (Go 1.10): تحويل الدمج التربيعي داخل الحلقات إلى strings.Builder
داخل الحلقة، تعيد s += p تخصيص الذاكرة والنسخ في كل دورة، فيصبح العمل من رتبة O(n²). أما strings.Builder فينمو كما ينمو الـ slice.
قبل:
example.gogos := "" for _, p := range parts { s += p } return s
بعد:
example.gogovar sb strings.Builder for _, p := range parts { sb.WriteString(p) } return sb.String()
المحلل متحفظ هنا. فهو لا يعمل إلا مع نمط المُراكِم (accumulator) الواضح، وليس مع كل += داخل حلقة. إذا كانت حلقتك تدمج ثابتين أو ثلاثة ثوابت صغيرة، فالصيغة الأبسط مقبولة.
الشبكات والأخطاء والانعكاس (reflection)
تضم هذه المجموعة ثلاث أدوات: فخ قديم في net، والدالة المعممة الجديدة errors.AsType، ودوال unsafe المساعدة الأكثر أمانًا.
hostport (Go 1.0): تحويل Sprintf لبناء العناوين إلى net.JoinHostPort
تبني fmt.Sprintf("%s:%d", host, port) عنوانًا معطوبًا عندما يكون host عنوان IPv6 حرفيًا، لأن عناوين IPv6 تحتوي هي نفسها على نقطتين رأسيتين. تضع net.JoinHostPort مضيفات IPv6 بين أقواس مربعة، وهي الطريقة القياسية لبناء سلسلة host:port.
قبل:
example.gogoaddr := fmt.Sprintf("%s:%d", host, port) net.Dial("tcp", addr)
بعد:
example.gogoaddr := net.JoinHostPort(host, strconv.Itoa(port)) net.Dial("tcp", addr)
errorsastype (Go 1.26): تحويل errors.As إلى النسخة المعممة errors.AsType
يضيف الإصدار 1.26 الدالة errors.AsType[T]، التي تعيد الخطأ بنوعه المحدد وقيمة bool، بدلًا من أن تطلب منك التصريح عن متغير هدف وتمرير عنوانه.
قبل:
example.gogovar myerr *MyErr if errors.As(err, &myerr) { handle(myerr) }
بعد:
example.gogoif myerr, ok := errors.AsType[*MyErr](err); ok { handle(myerr) }
unsafefuncs (Go 1.17): تحويل حسابات unsafe إلى unsafe.Add و unsafe.Slice
أضاف الإصدار 1.17 الدالتين unsafe.Add و unsafe.Slice، لتتمكن من إجراء العمليات الحسابية على المؤشرات وبناء slices فوق أي منطقة من الذاكرة دون كتابة التحويلات المتتالية عبر uintptr يدويًا.
قبل (base من النوع unsafe.Pointer):
example.gogop := unsafe.Pointer(uintptr(base) + uintptr(off))
بعد:
example.gogop := unsafe.Add(base, off)
تعيد أداة التحديث نفسها كتابة الأنماط اليدوية التي تجمع unsafe.Pointer مع reflect.SliceHeader لتستخدم unsafe.Slice.
التزامن والاختبارات
الأدوات الأربع الأخيرة تنظّف الكود الذي يستخدم sync و atomic والحزمة testing.
waitgroupgo (Go 1.25): تحويل Add(1) مع defer Done() إلى wg.Go
أضاف الإصدار 1.25 الدالة WaitGroup.Go، التي تجمع Add وإطلاق الـ goroutine و defer Done في استدعاء واحد.
قبل:
example.gogovar wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() fmt.Println("go!") }() wg.Wait()
بعد:
example.gogovar wg sync.WaitGroup wg.Go(func() { fmt.Println("go!") }) wg.Wait()
atomictypes (Go 1.19): استبدال دوال atomic بتوابع على الأنواع
الأنواع atomic.Int64 و atomic.Uint64 و atomic.Pointer[T] وأخواتها تحمل قيمتها بنفسها. لذلك تتوقف عن تمرير المؤشرات هنا وهناك، ولا يعود حجم العملية قابلًا لأن يختلف عن نوع المتغير.
قبل:
example.gogovar count int64 atomic.AddInt64(&count, 1) n := atomic.LoadInt64(&count)
بعد:
example.gogovar count atomic.Int64 count.Add(1) n := count.Load()
testingcontext (Go 1.24): تحويل WithCancel اليدوي في الاختبارات إلى t.Context()
تعيد t.Context() قيمة context تُلغى تلقائيًا عند انتهاء الاختبار. لست مضطرًا إلى تذكّر defer cancel()، كما ينتقل الـ context إلى الاختبارات الفرعية (subtests).
قبل:
example.gogofunc TestThing(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() work(ctx) }
بعد:
example.gogofunc TestThing(t *testing.T) { ctx := t.Context() work(ctx) }
bloop (Go 1.24): تحويل for i := 0; i < b.N; i++ إلى for b.Loop()
b.Loop() هي حلقة اختبارات الأداء (benchmarks) الجديدة. كما أنها تتولى إعادة ضبط المؤقت وإيقافه تلقائيًا. لذلك تختفي في معظم اختبارات الأداء الاستدعاءات اليدوية لـ b.ResetTimer() و b.StopTimer() حول مرحلة التهيئة.
قبل:
example.gogofunc BenchmarkThing(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { work() } }
بعد:
example.gogofunc BenchmarkThing(b *testing.B) { for b.Loop() { work() } }
تشغيل go fix بأمان
يعيد go fix كتابة ملفات مصدرية حقيقية. تعامل معه كما تتعامل مع أي إعادة هيكلة (refactoring) آلية أخرى.
ترتيب آمن للخطوات:
- ابدأ على فرع نظيف لا يحتوي على أي تغييرات أخرى قيد التنفيذ.
- شغّل
go fix -diff ./...أولًا. يطبع هذا الخيار (flag) الفروقات بصيغة unified diff على stdout بدلًا من الكتابة إلى الملفات. تصفّحها سريعًا. - شغّل
go fix ./...لتطبيق التغييرات. - شغّل
git diffعلى التعديلات الفعلية. ابحث عن أي إعادة كتابة تفاجئك، خاصة في الكود الذي كُتب عمدًا بأسلوب أقدم. - شغّل اختباراتك وأداة الفحص (linter) و
go build ./.... - نفّذ commit.
تشغيل واحد لا يكفي في الغالب
قد يكشف تطبيق إصلاح واحد عن فرصة لإصلاح آخر، والمحللات لا تتتبع هذه السلسلة ضمن مرور واحد.
يوضح ذلك مثال الحصر من قسم minmax. ابدأ بهذا:
example.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
في التشغيل الأول تُعاد كتابة الحد الأدنى إلى max:
example.gogox := max(0, f()) if x > 100 { x = 100 }
وفي التشغيل الثاني ترى الأداة نمط if والإسناد المتبقي فتُكمل المهمة:
example.gogox := min(100, max(0, f()))
يحتاج الأمر إلى مرورين لأن كل أداة تحديث محلل مستقل لا يفحص الكود إلا مرة واحدة في كل تشغيل لـ go fix. فهو يمسح شجرة الصياغة (syntax tree)، ويجد الأنماط التي يعرفها، ثم يُصدر الإصلاحات الخاصة بها.
في مثال الحصر، يعيد المرور الأول كتابة الكتلة < 0. بعد هذا التعديل تأتي الكتلة > 100 بعد استدعاء max(...) بدلًا من x := f() مجردة، وهذه فرصة جديدة لـ minmax. لكن minmax أنهت بالفعل مرورها الوحيد، لذلك لن ترى الشكل الجديد حتى تشغّل go fix مرة أخرى.
يحدث الأمر نفسه بين المحللات المختلفة. قد تكشف إعادة كتابة تجريها mapsloop عن فرصة لـ stditerators، لأن محلل المكررات كان يبحث عن شكل مثل range maps.Keys(m) لم يكن موجودًا قبل إعادة كتابة الحلقة.
الحل هو أن تعيد تشغيل go fix ./... حتى تعود الفروقات فارغة. يكفي مروران لمعظم قواعد الكود، والحاجة إلى مرور ثالث نادرة.
ملاحظات أخرى
- أدوات التحديث المرتبطة بالإصدار (
forvarوrangeintوnewexprوerrorsastype) لا تعمل إلا إذا صرّحgo.modبالحد الأدنى الصحيح للإصدار. إذا لم تظهر إعادة الكتابة التي توقعتها، فتحقق من توجيهgoلديك. - لعمليات الترحيل الخاصة بقاعدة الكود لديك، استخدم التوجيه
//go:fix inline. ضع الوسم على دالة مغلِّفة أو ثابت، ثم شغّلgo fix، فيُحدَّث كل موضع استدعاء في مكانه. - إذا غيّرت إعادة كتابة ما السلوك بطريقة لم يكتشفها المحلل، فهذا خطأ (bug) في المحلل ويستحق أن تفتح بلاغًا عنه. يتعامل فريق Go مع هذه البلاغات بسرعة.
مصادر للتعمق أكثر
للاطلاع على التغطية الكاملة لإصدار Go 1.26 (واجهات API جديدة للتشفير، و Green Tea GC، واكتشاف تسرّب الـ goroutines، وغير ذلك)، راجع دليل الجديد في Go 1.26.
أما في المصادر الأصلية، فيشرح توثيق x/tools/go/analysis/passes/modernize كل محلل بالتفصيل، ويشرح مقال مدونة Go عن go fix إطار العمل الجديد.
