العودة إلى المدونة

جميع أدوات التحديث (modernizers) في `go fix` 1.26

أعاد إصدار Go 1.26 بناء go fix على إطار عمل المحللات (analyzers). إليك كل أدوات التحديث (modernizers) المضمنة فيه: 26 مقارنة قبل وبعد تشمل اللغة والمكتبة القياسية.

جميع أدوات التحديث (modernizers) في `go fix` 1.26

أعاد إصدار Go 1.26 كتابة go fix على إطار العمل نفسه الذي يستخدمه go vet. يجمع الأمر الجديد كل أدوات التحديث (modernizers) التي بناها فريق Go ومؤلفو x/tools. كل واحدة منها محلل (analyzer) صغير يتعرّف على نمط قديم ويعيد كتابته بأسلوب Go الحديث.

أمر واحد يشغّلها جميعًا:

example.bashbash
go fix ./...

شغّله على فرع (branch) نظيف، وراجع الفروقات (diff)، وشغّل اختباراتك، ثم نفّذ commit. وإذا أردت معاينة التغييرات دون الكتابة إلى الملفات، فاستخدم go fix -diff ./....

فيما يلي كل أداة تحديث تأتي مع الإصدار 1.26، مصنفة حسب المجال. يذكر كل بند إصدار Go الذي قدّم الواجهة البرمجية (API) التي تعيد الأداة الكتابة إليها، إلى جانب مثال قبل وبعد. المقصود أن تتصفحه سريعًا أو تبحث فيه بـ Ctrl+F بقدر ما تقرؤه.

جدول المحتويات


اللغة والدوال المدمجة

توجد هنا سبع أدوات تحديث. يغطي معظمها ميزات في اللغة (any، والصيغة الجديدة range n، وتغيير متغير الحلقة في 1.22، وتوسيع new(expr))، إلى جانب عمليتي تنظيف تمتدان عبر عدة إصدارات.

any (Go 1.18): استبدال interface{} بـ any

any مجرد اسم بديل (alias)، لكنه أوضح عند القراءة في تواقيع الدوال ومعاملات الأنواع (type parameters).

قبل:

example.gogo
func log(args ...interface{}) {
    fmt.Println(args...)
}

var cache map[string]interface{}

بعد:

example.gogo
func log(args ...any) {
    fmt.Println(args...)
}

var cache map[string]any

minmax (Go 1.21): استبدال حصر القيم عبر if/else بـ min و max

الدالتان المدمجتان min و max تقبلان عددًا متغيرًا من الوسائط (variadic) وتعملان مع أي نوع قابل للترتيب.

قبل:

example.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

بعد:

example.gogo
x := max(0, min(100, f()))

rangeint (Go 1.22): حلقات for العددية بصيغة for i := range n

حلقة العد البسيطة، التي لا يقف خلفها slice أو array، يمكنها أن تمر على العدد الصحيح مباشرة باستخدام range.

قبل:

example.gogo
for i := 0; i < n; i++ {
    fmt.Println(i)
}

بعد:

example.gogo
for i := range n {
    fmt.Println(i)
}

forvar (Go 1.22): التخلص من حيلة الالتقاط x := x

قبل 1.22، كان متغير الحلقة مشتركًا بين كل الدورات. لذلك كانت الـ closures والـ goroutines التي تلتقطه تحتاج إلى x := x داخل جسم الحلقة. ومنذ 1.22 أصبحت كل دورة تحصل على متغير خاص بها، فلم يعد لهذا التظليل (shadowing) داعٍ.

قبل:

example.gogo
for _, x := range items {
    x := x // pre-1.22 capture trick
    go func() { use(x) }()
}

بعد:

example.gogo
for _, 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.gogo
func strPtr(s string) *string {
    p := s
    return &p
}

cfg.Name = strPtr("LevelUpGo")

بعد:

example.gogo
cfg.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.gogo
sort.Slice(s, func(i, j int) bool {
    return s[i] < s[j]
})

بعد:

example.gogo
slices.Sort(s)

slicescontains (Go 1.21): تحويل حلقات البحث اليدوية إلى slices.Contains

قبل:

example.gogo
func has(s []int, x int) bool {
    for _, v := range s {
        if v == x {
            return true
        }
    }
    return false
}

بعد:

example.gogo
func has(s []int, x int) bool {
    return slices.Contains(s, x)
}

slicesbackward (Go 1.23): التكرار العكسي باستخدام slices.Backward

تعيد slices.Backward مكررًا (iterator) ينتج أزواج الفهرس والقيمة من نهاية الـ slice إلى بدايته.

قبل:

example.gogo
for i := len(s) - 1; i >= 0; i-- {
    use(i, s[i])
}

بعد:

example.gogo
for i, v := range slices.Backward(s) {
    use(i, v)
}

mapsloop (Go 1.21): تحويل حلقات النسخ والاستنساخ والإدراج إلى استدعاءات من maps

توفر الحزمة maps دالة لكل حلقة شائعة على الـ maps، فيخبر اسم الاستدعاء القارئ بما كانت الحلقة تفعله.

قبل:

example.gogo
func 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.gogo
func 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.gogo
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
for _, k := range keys {
    use(k)
}

بعد:

example.gogo
for k := range maps.Keys(m) {
    use(k)
}

reflecttypefor (Go 1.22): التخلص من حيلة المؤشر nil مع reflect

reflect.TypeFor[T]() هي الطريقة المعممة والآمنة من حيث الأنواع للحصول على reflect.Type لنوع ساكن. أما الحل الالتفافي القديم TypeOf((*T)(nil)).Elem() فيعود إلى ما قبل ظهور generics.

قبل:

example.gogo
t := reflect.TypeOf((*MyType)(nil)).Elem()

بعد:

example.gogo
t := reflect.TypeFor[MyType]()

omitzero (Go 1.24): استبدال omitempty الهش بـ omitzero

يعتمد omitempty على كون القيمة صفرية بمفهوم Go، ما يعني أن time.Time{} ليست «فارغة» (فهي struct يحتوي على حقول)، لذلك لا تُحذف أبدًا. أما omitzero فيستدعي الدالة IsZero الخاصة بالقيمة إن وُجدت، فتختفي القيم الصفرية من time.Time من المخرجات كما تتوقع.

لا يغيّر المحلل إلا الحقول التي يكون فيها omitzero آمنًا. تبقى السلاسل النصية والـ slices والمؤشرات على omitempty لأن سلوكها سيتغير.

قبل:

example.gogo
type User struct {
    Name    string    `json:"name,omitempty"`
    Created time.Time `json:"created,omitempty"`
}

بعد:

example.gogo
type 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.gogo
b := []byte(fmt.Sprintf("%s=%d", key, val))

بعد:

example.gogo
b := fmt.Appendf(nil, "%s=%d", key, val)

stringscut (Go 1.18): تحويل strings.Index مع التقطيع إلى strings.Cut

تعيد strings.Cut الجزء الذي يسبق الفاصل والجزء الذي يليه وقيمة bool تدل على العثور عليه، كل ذلك في استدعاء واحد. كما تتعامل مع حالة عدم العثور بشكل صريح.

قبل:

example.gogo
i := strings.Index(s, "=")
if i >= 0 {
    key, val := s[:i], s[i+1:]
    use(key, val)
}

بعد:

example.gogo
if key, val, ok := strings.Cut(s, "="); ok {
    use(key, val)
}

stringscutprefix (Go 1.20): تحويل HasPrefix مع TrimPrefix إلى CutPrefix

تعيد CutPrefix السلسلة بعد اقتطاع البادئة وقيمة bool تدل على العثور عليها، فلا تفحص البادئة إلا مرة واحدة.

قبل:

example.gogo
if strings.HasPrefix(s, "go-") {
    rest := strings.TrimPrefix(s, "go-")
    handle(rest)
}

بعد:

example.gogo
if rest, ok := strings.CutPrefix(s, "go-"); ok {
    handle(rest)
}

تتعامل أداة التحديث نفسها مع CutSuffix أيضًا.

stringsseq (Go 1.24): تحويل Split و Fields داخل حلقات range إلى SplitSeq و FieldsSeq

تعيد SplitSeq و FieldsSeq مكررات بدلًا من تخصيص slice. إذا كنت تحتاج إلى المرور على الأجزاء مرة واحدة فقط، فإن صيغة المكرر تتجنب التخصيص تمامًا.

قبل:

example.gogo
for _, line := range strings.Split(s, "\n") {
    handle(line)
}

بعد:

example.gogo
for 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.gogo
s := ""
for _, p := range parts {
    s += p
}
return s

بعد:

example.gogo
var 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.gogo
addr := fmt.Sprintf("%s:%d", host, port)
net.Dial("tcp", addr)

بعد:

example.gogo
addr := 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.gogo
var myerr *MyErr
if errors.As(err, &myerr) {
    handle(myerr)
}

بعد:

example.gogo
if 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.gogo
p := unsafe.Pointer(uintptr(base) + uintptr(off))

بعد:

example.gogo
p := 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.gogo
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    fmt.Println("go!")
}()
wg.Wait()

بعد:

example.gogo
var wg sync.WaitGroup
wg.Go(func() {
    fmt.Println("go!")
})
wg.Wait()

atomictypes (Go 1.19): استبدال دوال atomic بتوابع على الأنواع

الأنواع atomic.Int64 و atomic.Uint64 و atomic.Pointer[T] وأخواتها تحمل قيمتها بنفسها. لذلك تتوقف عن تمرير المؤشرات هنا وهناك، ولا يعود حجم العملية قابلًا لأن يختلف عن نوع المتغير.

قبل:

example.gogo
var count int64
atomic.AddInt64(&count, 1)
n := atomic.LoadInt64(&count)

بعد:

example.gogo
var count atomic.Int64
count.Add(1)
n := count.Load()

testingcontext (Go 1.24): تحويل WithCancel اليدوي في الاختبارات إلى t.Context()

تعيد t.Context() قيمة context تُلغى تلقائيًا عند انتهاء الاختبار. لست مضطرًا إلى تذكّر defer cancel()، كما ينتقل الـ context إلى الاختبارات الفرعية (subtests).

قبل:

example.gogo
func TestThing(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    work(ctx)
}

بعد:

example.gogo
func 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.gogo
func BenchmarkThing(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        work()
    }
}

بعد:

example.gogo
func BenchmarkThing(b *testing.B) {
    for b.Loop() {
        work()
    }
}

تشغيل go fix بأمان

يعيد go fix كتابة ملفات مصدرية حقيقية. تعامل معه كما تتعامل مع أي إعادة هيكلة (refactoring) آلية أخرى.

ترتيب آمن للخطوات:

  1. ابدأ على فرع نظيف لا يحتوي على أي تغييرات أخرى قيد التنفيذ.
  2. شغّل go fix -diff ./... أولًا. يطبع هذا الخيار (flag) الفروقات بصيغة unified diff على stdout بدلًا من الكتابة إلى الملفات. تصفّحها سريعًا.
  3. شغّل go fix ./... لتطبيق التغييرات.
  4. شغّل git diff على التعديلات الفعلية. ابحث عن أي إعادة كتابة تفاجئك، خاصة في الكود الذي كُتب عمدًا بأسلوب أقدم.
  5. شغّل اختباراتك وأداة الفحص (linter) و go build ./....
  6. نفّذ commit.

تشغيل واحد لا يكفي في الغالب

قد يكشف تطبيق إصلاح واحد عن فرصة لإصلاح آخر، والمحللات لا تتتبع هذه السلسلة ضمن مرور واحد.

يوضح ذلك مثال الحصر من قسم minmax. ابدأ بهذا:

example.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

في التشغيل الأول تُعاد كتابة الحد الأدنى إلى max:

example.gogo
x := max(0, f())
if x > 100 {
    x = 100
}

وفي التشغيل الثاني ترى الأداة نمط if والإسناد المتبقي فتُكمل المهمة:

example.gogo
x := 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 إطار العمل الجديد.

اكتب Go كما يكتبها مهندس أول

دروس تفاعلية في متصفحك. الدروس الأولى مجانية.

جرّب درسًا مجانيًاأو أنشئ حسابًا مجانيًا