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

ما الجديد في Go 1.27: دليل كامل

تطرح Go 1.27 التوابع العامة (generic methods)، وحزمة uuid ضمن المكتبة القياسية، و json/v2 كمحرك افتراضي. إليك ما يجدر تبنيه الآن، وما يتعطل بهدوء عند الترقية.

ما الجديد في Go 1.27: دليل كامل

تضيف Go 1.27 خمس حزم جديدة إلى المكتبة القياسية (encoding/json/v2 و encoding/json/jsontext و crypto/mldsa و uuid و simd) إضافة إلى ثلاثة تغييرات في اللغة. بعض ما فيها ستريده من اليوم الأول، مثل التوابع العامة (generic methods) وحزمة uuid التي تحذف اعتمادية من معظم ملفات go.mod. وبعضها سيُفشل مجموعة اختباراتك قبل أن تلاحظ وجود الميزة: تصدر compress/flate بايتات مختلفة، وتحصل الدوال المجهولة (function literals) على أسماء رموز مختلفة، وعدد من منافذ التراجع في GODEBUG تُفشل البناء الآن بدلًا من إعادة السلوك القديم.

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

التوابع العامة في Go والشيء الوحيد الذي ما زالت لا تستطيعه

منذ وصول الأنواع العامة (generics) في Go 1.18، كانت معاملات الأنواع تعمل في موضعين فقط: الدوال على المستوى الأعلى وتصريحات الأنواع. أما التوابع فبقيت خارج ذلك، فإذا أردت تحويلًا يغيّر نوع عناصر مجموعتك، كان عليك كتابته كدالة على مستوى الحزمة، والدوال على مستوى الحزمة لا تتسلسل.

على الأرجح كتبت شيئًا كهذا من قبل. خط معالجة للقياسات يحوّل قراءات المستشعرات إلى تسميات للعرض يحتاج إلى تحويلين، وكل تحويل يجب أن يغلّف الاستدعاء السابق:

example.gogo
type Metric struct {
    Sensor  string
    Celsius int
}

type List[T any] []T

// Package-level, because a method couldn't declare U.
func Map[T, U any](l List[T], f func(T) U) List[U] {
    out := make(List[U], len(l))
    for i, v := range l {
        out[i] = f(v)
    }
    return out
}

func main() {
    metrics := List[Metric]{
        {Sensor: "cpu0", Celsius: 61},
        {Sensor: "cpu1", Celsius: 58},
    }

    temps := Map(metrics, func(m Metric) int { return m.Celsius })
    labels := Map(temps, func(c int) string { return fmt.Sprintf("%dC", c) })

    fmt.Println(labels) // [61C 58C]
}

في Go 1.27، تصبح Map تابعًا حقيقيًا يصرّح بمعامل نوع خاص به، مستقل عن المستقبِل (receiver):

example.gogo
// U is the method's own type parameter, new in Go 1.27.
func (l List[T]) Map[U any](f func(T) U) List[U] {
    out := make(List[U], len(l))
    for i, v := range l {
        out[i] = f(v)
    }
    return out
}

func main() {
    metrics := List[Metric]{
        {Sensor: "cpu0", Celsius: 61},
        {Sensor: "cpu1", Celsius: 58},
    }

    labels := metrics.
        Map(func(m Metric) int { return m.Celsius }).
        Map(func(c int) string { return fmt.Sprintf("%dC", c) })

    fmt.Println(labels) // [61C 58C]
}

التسلسل هو التحسن الواضح. والتحسن الأقل وضوحًا هو التسمية: تندمج MapSlice و MapSet و MapBox في تابع واحد اسمه Map لكل نوع، لأن المستقبِل يقول أصلًا أي نوع تقصد. والمستقبِل يساعد المترجم (compiler) أيضًا: List[Metric] تثبّت T، فلا يبقى إلا نوع الخرج ليُستنتج. كما أن التوابع تظهر حين تكتب نقطة في محررك. أما الدالة المساعدة على مستوى الحزمة فلا تفيدك إلا إذا كنت تعرف بوجودها مسبقًا.

والمكتبة القياسية تستخدم الميزة بنفسها. تصرّح math/rand/v2 الآن بـ (*Rand) N[Int intType](Int) Int كتابع بجوار الدالة N على مستوى الحزمة، وهذا ما لم تكن القواعد القديمة تسمح به.

لكن هناك قيد يسهل أن يفوتك. من ملاحظات الإصدار:

لذلك لا يُترجم هذا:

example.gogo
type Transformer interface {
    // Compile error: interface methods can't declare type parameters.
    Map[U any](f func(Metric) U) any
}

توزيع الاستدعاء عبر interface يحدث وقت التشغيل، لذلك لا يستطيع المترجم أن يعرف أي تجسيدات (instantiations) لتابع عام يجب أن يولّدها لاستدعاء يُحدد وجهته ديناميكيًا. وإذا كانت واجهة برمجتك مبنية على interface، فستبقى التحويلات بحاجة إلى الدالة القديمة على مستوى الحزمة. وفائدة التوابع العامة لقاعدة كودك تتوقف في الغالب على هذه النقطة، فتحقق من interfaces لديك قبل أن تخطط لإعادة هيكلة. وإذا أردت التدريب العملي، تغطي دورة Go Generics Masterclass القيود (constraints) والاستنتاج على شكل تمارين في المتصفح.

الحكم: تبنَّها الآن، للأنواع الملموسة. واترك الواجهات المبنية على interface كما هي.

حزمة uuid في المكتبة القياسية

تكاد كل خدمة Go تتعامل مع قاعدة بيانات أن تستورد github.com/google/uuid. وتضيف Go 1.27 حزمة في المكتبة القياسية مسار استيرادها "uuid"، بناءً على المقترح #62026.

النوع UUID معرّف كـ [16]byte، ما يعني أن القيم قابلة للمقارنة بـ == وقابلة للاستخدام مباشرة كمفاتيح map. وتأتي معه ثلاث دوال توليد:

example.gogo
package main

import (
    "fmt"
    "uuid"
)

func main() {
    fmt.Println(uuid.New())   // reach for this when you don't care how it's made
    fmt.Println(uuid.NewV4()) // 128 bits, 122 of them random
    fmt.Println(uuid.NewV7()) // 48-bit timestamp first, so values sort by creation time

    requestID, err := uuid.Parse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
    if err != nil {
        return
    }
    fmt.Println(requestID, requestID == uuid.Nil()) // false, it parsed fine
}

الدالة New هي الافتراضية وتعيد حاليًا V4. و NewV4 عشوائية بالكامل، فلا يستطيع أحد تخمين التالية. وهذا ما تريده في معرّف طلب عام. أما NewV7 فتضع طابعًا زمنيًا بطول 48 بت في المقدمة، فتُرتَّب القيم الأحدث بعد الأقدم، ما يجعلها مفاتيح أساسية جيدة لأن عمليات الإدراج تهبط قرب نهاية الفهرس بدلًا من أن تتبعثر في أنحائه.

لاحظ أن Nil() و Max() دالتان، لذلك تُكتب المقارنة id == uuid.Nil() مع الأقواس. تصلح Nil() كقيمة حدّية تعني «لم تُضبط بعد». و Max() هي القيمة التي كل بتاتها آحاد، وهي مفيدة كحد أعلى عندما تمسح نطاقًا من مفاتيح V7 المرتبة زمنيًا.

الحكم: تبنَّها الآن في الكود الجديد. ونقل خدمة قائمة هو عملية بحث واستبدال، لكن راجع الأسئلة الشائعة قبل أن تحذف الاعتمادية.

هل تحتاج إلى تغيير أي شيء من أجل encoding/json/v2؟

لا. وهذا ما قصده التصميم. تعمل encoding/json الآن على تنفيذ v2، وملاحظات الإصدار صريحة في أن «سلوك Marshal و Unmarshal محفوظ، لكن النص الدقيق لرسائل الأخطاء قد يختلف.» فتحصل على سرعة المحرك الجديد دون تغيير أي كود. وبحسب ملاحظات الإصدار، «أداء Marshal مكافئ عمومًا للتنفيذ السابق، بينما أداء Unmarshal أسرع بشكل ملحوظ.»

لا تختار السلوك الجديد إلا باستيراد encoding/json/v2 بالاسم. تلك الحزمة لها إعدادات افتراضية أكثر صرامة، اختيرت لتطابق ما تتوقعه تنفيذات JSON الأخرى: ترفض UTF-8 غير الصالح داخل نصوص JSON، وترفض الأسماء المكررة داخل كائن JSON واحد، وتطابق أسماء الحقول مع مراعاة حالة الأحرف، وتحوّل slice قيمته nil إلى [] بدلًا من null، وتحوّل maps بترتيب غير حتمي بعد أن كانت v1 تضمن ترتيبًا حتميًا. وتغيير slice الذي قيمته nil هو التمييز بين nil والفراغ الوارد في مقال var مقابل make في Go، ومع v2 تستطيع أن تراه في الخرج.

example.gogo
import "encoding/json/v2"

type Event struct {
    ID     string `json:"id"`
    Action string `json:"action"`
}

// v1 would match the key "ID" against the tag `json:"id"`. v2 won't,
// and it skips the mismatched key silently rather than erroring.
var e Event
json.Unmarshal([]byte(`{"ID":"evt_01H","action":"checkout"}`), &e)
fmt.Println(e.ID, e.Action) // "" checkout

// v2 marshals maps in a non-deterministic order. Ask for a stable one
// explicitly when you hash or snapshot the output.
inventory := map[string]int{"widget": 12, "gadget": 3, "gizmo": 7}
b, _ := json.Marshal(inventory, json.Deterministic(true))

لاستعادة سلوك من v1 تحت v2، لا تبدّل الاستيرادات. بل تمرر خيارًا لذلك السلوك وحده: Deterministic(true) لترتيب map مستقر، و MatchCaseInsensitiveNames(true) لمطابقة حقول متسامحة، و FormatNilSliceAsNull(true) للحصول على null بدلًا من []. وقد حصلت حزمة v1 على الخيارات نفسها، فيمكنك تبني دلالات v2 سلوكًا واحدًا في كل مرة دون هجرة كاملة. والقائمة الكاملة موجودة تحت Migrating to v2 في توثيق حزمة v1.

أما الحزمة الثالثة، encoding/json/jsontext، فتتولى الصيغة (syntax) الأدنى مستوى. وهي تعرض JSON كسلسلة من الرموز (tokens) والقيم، مع آلة حالات تحافظ على صلاحية الخرج. وتُبنى عليها مرمّزات البث. ومعظم كود التطبيقات لا يستوردها مباشرة.

ولتقرر أيها تستورد:

flowchart TD
    A["Which JSON import?"] --> B{"Upgrading existing code?"}
    B -- Yes --> C["encoding/json<br/>v2 engine, v1 behavior"]
    B -- No --> D{"Want strict defaults<br/>and faster decoding?"}
    D -- No --> C
    D -- Yes --> E["encoding/json/v2"]
    E --> F{"Need raw tokens<br/>or streaming syntax?"}
    F -- Yes --> G["encoding/json/jsontext"]
    F -- No --> H["You're done"]

شجرة قرار للاختيار بين encoding/json و encoding/json/v2 و encoding/json/jsontext في Go 1.27.

الحكم: تبنَّ الإعداد الافتراضي الآن (وقد فعلت ذلك بالفعل)، وتبنَّ v2 بالاسم لاحقًا، بعد أن تقرأ ملاحظات الهجرة الخاصة بوسوم struct لديك. تغطي دورة Data Formats قواعد الوسوم التي تحدد مقدار تأثير هذا عليك.

التوقيعات المقاومة للحوسبة الكمية مع crypto/mldsa

تُعد RSA و ECDSA آمنتين اليوم لأن لا حاسوب يستطيع تفكيك الأعداد الكبيرة إلى عواملها أو حل اللوغاريتمات المتقطعة بسرعة كافية. أما حاسوب كمي كبير بما يكفي فيستطيع. وتنفّذ crypto/mldsa خوارزمية ML-DSA من معيار FIPS 204 بثلاث مجموعات معاملات، 44 و 65 و 87، بناءً على المقترح #77626.

وحزم أخرى تدعمها بالفعل. تحلل crypto/x509 مفاتيح وتوقيعات ML-DSA وتتحقق منها، وتقبل crypto/tls مجموعات المعاملات الثلاث كلها في مصافحة TLS 1.3. وتنضم MLKEM1024 إلى تبادلات المفاتيح المدعومة للاتفاق على مفتاح آمن كميًا، وتُفعَّل بإضافتها إلى Config.CurvePreferences.

توقيع وتحقق كاملان لإصدار برنامج ثابت (firmware):

example.gogo
sk, err := mldsa.GenerateKey(mldsa.MLDSA65())
if err != nil {
    return err
}

firmware, err := os.ReadFile("firmware-v2.1.0.bin")
if err != nil {
    return err
}

// Context is a domain-separation label. Sign and verify must pass the same one.
opts := &mldsa.Options{Context: "acme/firmware-release"}
sig, err := sk.Sign(nil, firmware, opts)
if err != nil {
    return err
}

fmt.Println(mldsa.Verify(sk.PublicKey(), firmware, sig, opts) == nil) // true
fmt.Println(mldsa.MLDSA65().SignatureSize())                         // 3309

firmware[0] ^= 1 // flip one bit
fmt.Println(mldsa.Verify(sk.PublicKey(), firmware, sig, opts) == nil) // false

تعيد Verify القيمة nil عندما يكون التوقيع سليمًا، لذلك تمنحك المقارنة مع nil قيمة منطقية. والعيب هو الحجم، والفرق كبير:

مخطط أعمدة أفقي يقارن أحجام التوقيعات الرقمية بالبايت. Ed25519 يساوي 64 بايت، و ECDSA P-256 نحو 71 بايت، و RSA-2048 يساوي 256 بايت، و ML-DSA-44 يساوي 2420 بايت، و ML-DSA-65 يساوي 3309 بايت، و ML-DSA-87 يساوي 4627 بايت.

المصدر: أحجام التوقيعات وفق FIPS 204 (ML-DSA) ومواصفات كل خوارزمية على حدة.

توقيع ML-DSA-65 أكبر بنحو 46 ضعفًا من توقيع Ed25519، لذلك لا يستحق الاستخدام إلا حيث يجب أن يدوم التوقيع. فالبرامج الثابتة ومخرجات الإصدار التي يجب أن يثق بها جهاز بعد عشر سنوات تستحق 3 KB. وكذلك جذور الشهادات المقصود أن تبقى بعد انتهاء عمر تشفير اليوم، وخوادم TLS التي تفضّل ألا تعيد توليد مفاتيحها حين تظهر الحواسيب الكمية. أما رموز الجلسات قصيرة العمر التي تُنتج آلافًا منها في الثانية، فالحجم الإضافي فيها حمل زائد لا تحتاجه بعد.

ويصل تغيير آخر غير مرتبط في crypto/x509 إلى المنطقة نفسها. فـ SystemCertPool تحترم الآن SSL_CERT_FILE و SSL_CERT_DIR على Windows و Darwin، وليس على Linux فقط. وعندما تكون هذه المتغيرات مضبوطة، تحمّل Go الجذور من القرص وتستخدم مدققها الخاص بدلًا من واجهات النظام. اضبط GODEBUG=x509sslcertoverrideplatform=0 للحفاظ على السلوك القديم.

الحكم: تبنَّها للتوقيعات طويلة العمر، وتجاهلها في كل ما عدا ذلك.

حزمة simd التجريبية

تعمل تعليمة المعالج العادية على قيمة واحدة. أما تعليمة SIMD فتعمل على متجه كامل من القيم في خطوة واحدة، فتحتاج النتيجة نفسها إلى تعليمات أقل بكثير. ويفيد هذا عندما تنفّذ العملية الحسابية نفسها على slice كبير من عينات الصوت أو بكسلات الصور، أو عندما تحسب جداءً قياسيًا. أما الكود الذي يتنقل في الغالب بين structs والنصوص فلن يستفيد.

وكل معمارية تعرض SIMD بطريقة مختلفة. حزمة simd الجديدة قابلة للنقل ولا تفترض حجمًا معينًا للمتجه. وهي متاحة على جميع المعماريات، وتستخدم تعليمات العتاد حيث تتوفر. فعّلها بـ GOEXPERIMENT=simd وقت البناء. وهي تجريبية، لذلك قد تتغير واجهتها البرمجية.

مزج مسارين صوتيين، بعدة عينات في كل خطوة:

example.gogo
//go:build goexperiment.simd

// out[i] = trackA[i] + trackB[i], several samples at a time.
out := make([]float32, len(trackA))

lanes := simd.LoadFloat32s(trackA).Len() // how many float32s fit in one vector
for i := 0; i+lanes <= len(trackA); i += lanes {
    va := simd.LoadFloat32s(trackA[i:])
    vb := simd.LoadFloat32s(trackB[i:])
    va.Add(vb).Store(out[i:])
}
// A plain scalar loop handles the leftover tail.

تخبرك Len بعدد العناصر التي تتسع في متجه واحد على الجهاز الذي تعمل عليه، فلا تحتاج الحلقة أبدًا إلى تثبيت عرض المتجه في الكود. وكل خطوة تحمّل هذا العدد من العينات من كلا المسارين، وتجمعها عنصرًا بعنصر في عملية واحدة، وتخزّن النتيجة.

الحكم: تجاهلها الآن إلا إذا كنت تقيس أداء حلقة عددية ساخنة بالفعل. فالواجهة البرمجية خلف GOEXPERIMENT وقابلة للتغيير.

أداء تحصل عليه بمجرد الترقية

بعض التسريعات لا تحتاج إلا إلى إعادة البناء. وأكبرها هو التخصيص المتخصص بالحجم، الذي تصفه ملاحظات الإصدار هكذا:

مخطط يوضح مخصص الذاكرة المتخصص بالحجم في Go 1.27. عمليات التخصيص الأصغر من 80 بايت تصبح أرخص بما يصل إلى 30 بالمئة، والبرامج الحقيقية كثيفة التخصيص تتحسن بنحو 1 بالمئة إجمالًا، وحجم الملف التنفيذي ينمو بمقدار ثابت هو 60 كيلوبايت.

المصدر: ملاحظات إصدار Go 1.27، تخصيص ذاكرة أسرع.

هذان الرقمان يقيسان أمرين مختلفين. فنسبة 30% هي كلفة استدعاء تخصيص صغير واحد. ونحو 1% هو ما تراه الخدمة بأكملها. وإذا واجهت تراجعًا في الأداء، فإن GOEXPERIMENT=nosizespecializedmalloc يلغي التفعيل، ومن المتوقع أن يختفي منفذ التراجع هذا في Go 1.28.

وهناك أيضًا ثلاث تحسينات في المترجم مفعّلة افتراضيًا. تمريرة تحليل تدفق البيانات للبتات المعروفة تتابع أي بتات القيمة يمكن إثبات أنها 0 أو 1، وتطوي ما ينتج عن ذلك من تكرار زائد. ونقل الكود الثابت خارج الحلقة يرفع الحسابات التي لا تتغير نتيجتها إلى خارج جسم الحلقة، فتُنفَّذ مرة واحدة بدلًا من كل دورة. كما تُترجم عبارات switch الآن إلى جداول بحث حين تسمح الحالات بذلك، بما في ذلك مع fallthrough، فتقفز مباشرة إلى الحالة المطابقة بدلًا من اختبار كل حالة على حدة.

وتصبح compress/flate أسرع أيضًا، مع تحذير واحد: قد يختلف الخرج المشفَّر بدقته عن Go 1.26. وتقع DEFLATE أسفل archive/zip و compress/gzip و compress/zlib و image/png، لذلك قد تفشل الاختبارات المرجعية المطابِقة بايتًا ببايت على أي منها. والخرج ما زال صحيحًا، فأعد اعتماد ملفات المقارنة.

الحكم: تبنَّه الآن. فستحصل عليه عند الترقية في كل الأحوال.

كشف تسريبات goroutine وقراءة آثار التتبع الموسومة

تخرّج الملف التعريفي goroutineleak من مرحلة التجربة إلى الإتاحة العامة، وحُذف المتغير goroutineleakprofile من GOEXPERIMENT. وهو متاح عبر runtime/pprof وكنقطة نهاية في net/http/pprof على المسار /debug/pprof/goroutineleak.

الـ goroutine المسرَّبة هي واحدة محجوبة على بنية أولية للتزامن (concurrency) لا يمكن أن تُفرَج أبدًا. وتجدها بيئة التشغيل عبر جامع المهملات: إذا كانت الـ goroutine المسماة G محجوبة على البنية P، وكانت P غير قابلة للوصول من أي goroutine قابلة للتشغيل أو من أي شيء يمكن لتلك أن تفرج عنه، فلن تستيقظ G أبدًا.

example.gogo
func startJob() {
    result := make(chan int) // unbuffered: the send waits for a receiver
    go func() {
        result <- expensiveWork() // blocks forever, nobody receives
    }()
    // returns without receiving, so result becomes unreachable
}

func main() {
    startJob()
    runtime.GC() // the detector scans during a GC cycle
    pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
    // goroutineleak profile: total 1
    //   ... main.startJob.func1 ... main.go:14
}

لهذا الأسلوب نقطة عمياء. قد تفوّت بيئة التشغيل تسريبات تكون فيها بنية الحجب قابلة للوصول عبر متغير عام أو عبر المتغيرات المحلية في goroutine قابلة للتشغيل. فالـ channel المخزَّن في سجل على مستوى الحزمة يُعد قابلًا للوصول، لذلك لن يبلّغ عنه الملف التعريفي. وتشرح دورة Go Concurrency Fundamentals أنماط التسريب التي تنتج هذه الحالات، وكيف تغلقها باستخدام context.

والتحسين الثاني في التنقيح لا يحتاج إلى أي كود. فبالنسبة إلى الوحدات (modules) التي يكون توجيه go فيها 1.27 أو أحدث، تضم آثار التتبع الآن وسوم goroutine الخاصة بـ runtime/pprof في سطر الترويسة. فالوسوم التي تضبطها أصلًا لقياس الأداء تظهر في تفريغات الانهيار، وفي آثار SIGQUIT، وفي خرج runtime.Stack. وحين يكون لـ goroutine وأخرى المكدس نفسه، يكون الوسم في الغالب الطريقة الوحيدة للتمييز بينهما:

example.gogo
pprof.Do(ctx, pprof.Labels("request", id), func(ctx context.Context) {
    work()
})

// goroutine 34 [running]:
// labels: {"request":"req-42"}
// main.handle.func1 ...

اضبط GODEBUG=tracebacklabels=0 لتعطيله إذا كانت وسومك تحمل أي شيء لا تريده في تفريغ انهيار.

الحكم: تبنَّهما الآن. فلا يكلفك أي منهما شيئًا.

مكاسب أصغر في اللغة والمكتبة

محددات الحقول داخل struct literal

يمكن أن يكون المفتاح في struct literal الآن أي محدد حقل صالح للنوع، لا مجرد اسم حقل على المستوى الأعلى (المقترح #9859). فإذا ضمّنت نموذجًا مشتركًا، لم تعد مضطرًا إلى كتابة القيمة المتداخلة كاملة:

example.gogo
type Model struct {
    ID        int64
    CreatedAt time.Time
}

type Post struct {
    Model
    Author string
    Likes  int
}

p := Post{Model: Model{ID: 42}, Author: "Patrik"} // before
p := Post{ID: 42, Author: "Patrik"}               // Go 1.27

تعميم استنتاج أنواع الدوال

يُطبَّق استنتاج نوع الدالة الآن في كل سياق تُسنَد فيه دالة عامة إلى نوع دالة مطابق أو تُحوَّل إليه (المقترح #77245). وسابقًا كان يعمل فقط في تصريح متغير محدد النوع:

example.gogo
func ascending[T cmp.Ordered](a, b T) int  { return cmp.Compare(a, b) }
func descending[T cmp.Ordered](a, b T) int { return cmp.Compare(b, a) }

orders := []func(int, int) int{ascending[int], descending[int]} // before
orders := []func(int, int) int{ascending, descending}           // Go 1.27

نوع عنصر الـ slice هو func(int, int) int، لذلك تستنتج Go أن T هو int. والأمر نفسه يعمل الآن في التحويلات وفي تمرير دالة عامة مجردة كوسيط.

strings.CutLast و bytes.CutLast

تقسم Cut عند أول فاصل. و CutLast تقسم عند آخر فاصل، فتعيد الجزء الذي قبله والجزء الذي بعده وما إذا كان الفاصل قد وُجد (المقترح #71151):

example.gogo
dir, file, found := strings.CutLast("internal/store/user.go", "/")
fmt.Println(dir, file, found) // internal/store user.go true

في السابق كنت تستدعي LastIndex، وتتحقق من -1، ثم تقطّع يدويًا مرتين.

Rand.N و maphash.Hasher و big.Int.Divide

تحصل math/rand/v2 على Rand.N كتابع عام (المقترح #77853)، فيمكنك سحب قيمة عشوائية محدودة من مصدر بذرته أنت بدلًا من المصدر العام، الذي لا يمكن بذره. وعندها يُعاد تشغيل الاختبار الفاشل بالمدخلات نفسها.

وتحصل hash/maphash على interface عام باسم Hasher[T] للبنى القائمة على التقطيع (hashing) مثل maps مخصصة ومرشحات Bloom (المقترح #70471). وهو يجمع دالة تقطيع مع فحص تساوٍ، ويجب أن تنتج القيم المتساوية التقطيع نفسه. و ComparableHasher[T] هو التنفيذ الجاهز للأنواع القابلة للمقارنة الذي يقارن بـ ==. وتستخدمه go/types بالفعل، إذ تقدم Hasher يحترم علاقة Identical.

ويحصل math/big.Int على تابع Divide يحسب ناتج القسمة والباقي معًا وفق نمط تقريب تختاره: Trunc أو Floor أو Round أو Ceil. أما Quo و Mod فتقتطعان دائمًا باتجاه الصفر، لذلك أصبح للكود المالي والعددي الذي يحتاج إلى تقريب آخر خيار مدمج.

Unicode 17 و database/sql وما تبقى

انتقلت حزمة unicode وكل ما يُبنى عليها من Unicode 15 إلى Unicode 17 مباشرة، لذلك تُصنَّف الآن تصنيفًا صحيحًا المحارف التي أُضيفت في الإصدارين الواقعين بينهما. وحصلت database/sql على ConvertAssign، التي تتيح لبرامج التشغيل (drivers) إعادة استخدام تحويلات الأنواع التي تنفذها Rows.Scan بدلًا من كتابة تحويلاتها الخاصة.

وما تبقى صغير:

  • crypto تضيف قيمة التقطيع MLDSAMu، وهي آلية إشارة للتوقيع بـ ML-DSA بنمط external-mu.
  • crypto/ecdsa تتحقق الآن من صحة طول التقطيع في PrivateKey.Sign عندما تمرر SignerOpts غير nil.
  • crypto/x509 تعرض RawSignatureAlgorithm على Certificate و CertificateRequest و RevocationList، فتمنحك AlgorithmIdentifier مشفَّرًا بـ DER حتى عندما تكون SignatureAlgorithm هي UnknownSignatureAlgorithm. كما يقبل التحليل إلى pkix.Name مدى أوسع من أنواع القيم، وتهبط الأنواع غير المعروفة في asn1.RawValue.
  • crypto/tls تضيف ConnectionState.LocalCertificate، وهي السلسلة التي قدمتها للطرف الآخر، إضافة إلى QUICConfig.ClientHelloInfoConn. وأصبحت Config.Rand مهملة لصالح testing/cryptotest.SetGlobalRandom في الاختبارات الحتمية.
  • net تجعل توابع القراءة في UnixConn تعيد io.EOF مباشرة بدلًا من تغليفه في net.OpError.
  • runtime/secret تنشر الوضع السري إلى goroutines التي تُنشأ داخله.
  • go/constant تضيف StringLen، و go/scanner تضيف Scanner.End، و go/token تمنح File تابع String.
  • على صعيد المنافذ، ينتقل منفذ PowerPC 64 بت ذو الترتيب big-endian على Linux إلى واجهة النظام الثنائية ELFv2، ما يفتح هناك إمكانية cgo والملفات التنفيذية المستقلة عن الموضع والربط الخارجي، ويتطلب نواة Linux 3.13 أو أحدث. وعلى Plan 9، أصبح syscall.Errno معرَّفًا وينفّذ error، فيُبنى الكود القابل للنقل الذي يشير إليه دون قيود بناء. كما يقبل الرابط الآن -macos و -macsdk لتحديد الإصدارات المكتوبة في أمر التحميل LC_BUILD_VERSION على macOS.

ما الذي تغيّر في سلسلة أدوات Go؟

حصلت go fix على أربعة محدِّثات. embedlit تبسّط الإشارات إلى الحقول المضمَّنة في القوالب المركبة، وهو ما تجعله قاعدة struct literal الجديدة ممكنًا. و atomictypes تستبدل الأنواع الأساسية في استدعاءات sync/atomic بالأنواع الذرية، و slicesbackward تعيد كتابة الحلقات العكسية باستخدام slices.Backward، و unsafefuncs تستبدل حسابات المؤشرات غير الآمنة باستدعاءات دوال. وجاء معها تغييران تنظيميان: حُذفت fmtappendf لأسباب تتعلق بالأسلوب، وأُعيدت تسمية waitgroup إلى waitgroupgo. شغّلها مرة واحدة بعد أن ترفع توجيه go:

example.bashbash
go fix -diff ./...   # preview
go fix ./...         # apply

وللاطلاع على القائمة الكاملة لما تعيد go fix كتابته، راجع كل محدِّث في go fix 1.26.

تشغّل go test الآن فحص stdversion من go vet افتراضيًا. وهو يبلّغ عن رموز المكتبة القياسية الأحدث من إصدار Go الساري على الملف، كما يحدده توجيه go.mod ووسوم البناء. فإذا كانت وحدتك تعلن go 1.25 واستخدم أحدهم strings.CutLast، يكتشف تشغيل الاختبارات ذلك قبل أن يكتشفه مستخدم يعمل على 1.25. كما يضيف go test -json إلى أسطر "Action":"output" حقلًا اختياريًا هو "OutputType"، وقيمه حاليًا error أو error-continue أو frame، وهذا مفيد إذا كنت تحلل خرج الاختبارات في CI.

تقبل go doc الآن صيغة package@version، لذلك يقرأ go doc rsc.io/[email protected] توثيق إصدار محدد دون سحبه. وهناك راية جديدة -ex تسرد الأمثلة القابلة للتشغيل في حزمة، وتسمية واحد منها تطبع مصدره.

تفرض go mod tidy تنسيقًا من كتلتين على الوحدات التي تعمل على go 1.27 أو أحدث، فتدمج كتل require المتفرقة في كتلة واحدة للاعتماديات المباشرة وأخرى لغير المباشرة. وتُحفظ كتل التعليقات المرتبطة بالاعتماديات، وينتقل التعليق الذي يغطي مجموعة مختلطة إلى الكتلة المباشرة.

يرتبط go tool trace -http=:6060 الآن بـ localhost فقط، مثل go tool pprof، لذلك مرر عنوانًا كاملًا مثل -http=0.0.0.0:6060 إذا احتجت الوصول إليه من جهاز آخر. كما تقبل الأدوات compile و link و asm و cgo و cover و pack ملفات استجابة (@file) بتنسيق متوافق مع GCC، وهذا يساعد أنظمة البناء التي تتجاوز حدود طول سطر الأوامر.

الاختبارات و net/http

تنشئ httptest.NewTestServer كائن Server على شبكة وهمية في الذاكرة، مصممة للاستخدام مع testing/synctest، فلا يوجد منفذ TCP حقيقي في الصورة. وشريكتها هي synctest.Sleep، التي تنفّذ time.Sleep و synctest.Wait في استدعاء واحد، فتقدّم الساعة الوهمية ثم تدع goroutines تستقر. وباستخدامهما معًا، لا تعود اختبارات HTTP معتمدة على التوقيت. وتغطي دورة Professional Go Testing نموذج synctest الذي تُبنى عليه هذه الأدوات.

example.gogo
srv := httptest.NewTestServer(t, handler) // in-memory, cleanup auto-registered

وعلى جانب net/http، يقبل خادم HTTP/2 الآن إشارات الأولوية من العميل كما يحددها RFC 9218، ويخدم التدفقات ذات الأولوية الأعلى أولًا. اضبط Server.DisableClientPriority = true للحصول على سلوك التوزيع الدوري القديم.

أما التغيير الذي يُرجَّح أن تلاحظه فهو في HTTP/1: إغلاق Response.Body المقروء جزئيًا يستنزف الآن المحتوى المتبقي حتى حد متحفظ لتتمكن من إعادة استخدام الاتصال. وفي معظم البرامج هذا بلا أثر أو تحسن بسيط في السرعة. وإذا كنت تغلق مبكرًا لإلغاء تنزيل كبير، فإن Transport.DisableKeepAlives = true يعطّل ذلك.

ويحدّ Server.MaxHeaderValueCount عدد القيم التي يمكن أن تحملها ترويسة واحدة، وهذا يحمي من الطلبات التي تُغرق ترويسة بالقيم. ويمكن لـ Transport و Server التفاوض على TLS ALPN عبر net.Conn تقدمه أنت، بشرط أن ينفّذ ConnectionState() tls.ConnectionState، فيُلتقط HTTP/2 عبر الاتصالات المنقولة في أنفاق أو عبر وكلاء. وحصلت net/url على URL.Clone و Values.Clone للنسخ العميق. وتغطي دورة HTTP and Networking إعدادات الخادم وطبقة النقل التي تُبنى عليها هذه الإضافات.

الحكم: تبنَّه الآن. إذا كانت لديك مجموعة اختبارات HTTP متقلبة، فإن httptest مع synctest سبب كافٍ للترقية.

ما الذي يتعطل عند الترقية إلى Go 1.27؟

توزّع ملاحظات الإصدار التغييرات الكاسرة على ثمانية أقسام. ويجمعها هذا الجدول، مرتبة بحسب احتمال أن تصطدم بها.

ما الذي يتغيرمن يتأثر بهما يجب فعله
compress/flate تصدر بايتات مختلفةالاختبارات المرجعية المطابِقة بايتًا ببايت على خرج gzip أو zlib أو zip أو pngأعد اعتماد ملفات المقارنة. الضغط صحيح، لكنه مختلف.
أسماء أبسط للدوال المجهولة (closures)الاختبارات التي تتحقق من أسماء الرموز، والكود الذي يقارن مؤشرات كود الدوال للتساويتوقف عن الاعتماد على أسماء الدوال المجهولة. مقارنة مؤشرات الدوال كانت موثقة أصلًا بأنها غير موثوقة.
حذف إعدادَي GODEBUG المسميين asynctimerchan و gotypesalias نهائيًاأي شيء يثبّتهما على القيمة القديمة في go.mod أو في سطر //go:debugنفّذ grep -r asynctimerchan . قبل الترقية. ضبطهما على القيمة الافتراضية النهائية ما زال يُبنى، أما ضبطهما على القيمة القديمة فيفشل.
حذف خمسة إعدادات GODEBUG خاصة بـ TLS و x509: tlsunsafeekm و tlsrsakex و tls3des و tls10server و x509keypairleafالخدمات التي ما زالت مثبّتة على سلوك TLS القديمالقاعدة نفسها المذكورة أعلاه: يقبل الأمر go القيمة الافتراضية النهائية ويرفض القيمة القديمة، لذلك تكتشف هذه وقت البناء.
macOS 13 Ventura هو الحد الأدنىمنفّذات CI وأجهزة التطوير التي تعمل على إصدارات macOS أقدمحدّث صورة المنفّذ. أُعلن عن ذلك في ملاحظات Go 1.26.
حذف دعم bzr من الأمر goالوحدات المستضافة على خواديم Bazaarانسخ الاعتمادية إلى مرآة، أو أدرجها ضمن vendor.
توجيه جديد //go:linknamestd يعلّم أسماء linkname المخصصة للمكتبة القياسية، والرابط يتحقق الآن من وصول linkname إلى رموز التجميع، وانتقلت واصفات الأنواع إلى قسم .go.typeالحزم التي تمتد إلى بيئة التشغيل عبر //go:linkname غير مصرَّح به، سواء كانت حزمك أو حزم اعتماديةحدّث الاعتمادية. هذه الأخطاء تظهر وقت البناء وبصوت عالٍ. ولا شيء منها مذكور في ملاحظات الإصدار، لذلك البناء هو وسيلتك لمعرفتها.
Response.Body.Close في HTTP/1 يستنزف المحتوى غير المقروءالكود الذي يغلق مبكرًا لإلغاء تنزيل كبيراضبط Transport.DisableKeepAlives = true في تلك العملاء.
نص رسائل أخطاء json/v2 مختلفالاختبارات التي تتحقق من نصوص أخطاء JSON حرفيًاطابق على نوع الخطأ أو على جزء من النص بدلًا من الرسالة الكاملة.

الصف الأخير هو الأصعب في الاكتشاف. فالسلوك محفوظ لكن النص ليس كذلك، لذلك تفشل مجموعة اختبارات تطابق نصوص أخطاء Unmarshal دون سبب واضح. اعزله بتشغيلين على فرع رفع go.mod فيه إلى go 1.27 بالفعل:

example.bashbash
go test ./... > default.txt 2>&1
GOEXPERIMENT=nojsonv2 go test ./... > nojsonv2.txt 2>&1
diff default.txt nojsonv2.txt

كل ما يظهر في الفرق مصدره تغيير JSON. وكل ما يفشل في التشغيلين هو شيء آخر من الجدول. ومن المتوقع حذف GOEXPERIMENT=nojsonv2 في إصدار قادم، لذلك استخدمه للتشخيص ثم أصلح الاختبارات.

الأسئلة الشائعة

هل يجب أن أعيد كتابة كود JSON من أجل Go 1.27؟

لا. حزمة encoding/json منفَّذة الآن فوق v2، لكن سلوك Marshal و Unmarshal محفوظ وواجهة v1 البرمجية ما زالت مدعومة. والفرق الوحيد الملحوظ هو أن النص الدقيق لرسائل الأخطاء قد يتغير. ولا تختار الإعدادات الافتراضية الأكثر صرامة في v2 إلا باستيراد encoding/json/v2 صراحة.

هل يمكن لتابع عام أن يحقق interface؟

لا. تنص ملاحظات الإصدار على أن توابع interface لا يجوز أن تصرّح بمعاملات أنواع، وأن توابع interface لا يمكن تنفيذها بتوابع عامة. فتوزيع الاستدعاء عبر interface يُحلّ وقت التشغيل، لذلك لا يستطيع المترجم أن يعرف أي تجسيدات يولّدها لاستدعاء يُحدد وجهته ديناميكيًا. وإذا كانت واجهة برمجتك مبنية على interfaces، فاستمر في استخدام الدوال العامة على مستوى الحزمة.

هل أتخلى عن github.com/google/uuid لصالح حزمة المكتبة القياسية؟

في الكود الجديد، نعم. أما في الكود القائم فتحقق من أمرين أولًا. النوع UUID في المكتبة القياسية هو [16]byte بمجموعة توابع خاصة به، لذلك يحتاج أي كود يستخدم التوابع الإضافية في نوع الطرف الثالث إلى مراجعة، وأي اعتمادية تُصدّر uuid.UUID الخاص بالطرف الثالث في واجهتها البرمجية تُبقي تلك الوحدة في مخطط اعتمادياتك على أي حال.

هل crypto/mldsa جاهزة لبيئة الإنتاج؟

هي حزمة مستقرة في المكتبة القياسية تنفّذ FIPS 204، وموصولة بـ crypto/x509 و crypto/tls. والقيد العملي هو حجم التوقيع لا النضج: توقيعات ML-DSA-65 بحجم 3309 بايت مقابل 64 بايت لـ Ed25519. استخدمها حيث يجب أن يبقى التوقيع صالحًا بعد انتهاء عمر تشفير اليوم، وتجاوزها في الرموز قصيرة العمر ذات الحجم الكبير.

ما أسرع طريقة لمعرفة ما تعطله Go 1.27 في قاعدة كودي؟

ارفع go.mod إلى go 1.27 على فرع، ثم ابحث بـ grep عن asynctimerchan و gotypesalias وعن إعدادات TLS الخمسة المحذوفة، لأن هذه تُفشل البناء تمامًا. ثم شغّل مجموعة اختباراتك مرتين، مرة بشكل عادي ومرة بـ GOEXPERIMENT=nojsonv2، وقارن الإخفاقات. هذا يفصل انحراف نصوص أخطاء JSON عن إخفاقات الملفات المرجعية وأسماء الرموز.

المصادر

المراجع الأساسية المشار إليها في هذا المقال (آخر تحقق في 23 أغسطس 2026):

واصل التعلم

يسهل استخدام التوابع العامة استخدامًا جيدًا حين تصبح القيود والاستنتاج مألوفة لديك، وهذان هما الجزءان اللذان يمر عليهما الناس سريعًا عادة حين يتعلمون الأنواع العامة لأول مرة. تشرح دورة Go Generics Masterclass القيود ومجموعات الأنواع (type sets) والاستنتاج على شكل تمارين في المتصفح تعمل على سلسلة الأدوات الحالية، فتستطيع تجربة صيغة التوابع في 1.27 في كود حقيقي.

هل أنت جديد على Go؟ ابدأ بـ مسار Go Fundamentals ثم عد إلى ملاحظات الإصدار. وإذا تجاوزت إصدار العام الماضي، يغطي مقال ما الجديد في Go 1.26 الدالة errors.AsType، وجامع المهملات Green Tea، و new(expr).

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

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

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