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

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

دليل شامل لميزات Go 1.26 الجديدة: معالجة أخطاء أقوى مع errors.AsType، وواجهات تشفير مبسّطة، وكشف تسريبات goroutine، وتحسينات الأداء في جامع المهملات Green Tea GC، والمزيد. يتضمن أمثلة كود قابلة للتشغيل.

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

معظم ما في Go 1.26 يظهر في الكود الذي تكتبه كل يوم. ستجد كودًا متكررًا (boilerplate) أقل حول المؤشرات وأنواع الأخطاء، وطريقة جديدة لالتقاط تسريبات goroutine، وبيئة تشغيل أسرع تحصل عليها بمجرد إعادة البناء.

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


تهيئة أنظف للمؤشرات باستخدام new(expr)

إذا استخدمت واجهة API في Go تعبّر عن الحقول الاختيارية بالمؤشرات، فقد كتبت هذا من قبل:

example.gogo
port := 8080
config := &Config{Port: &port}  // Can't use &8080 directly!

Go لا تسمح لك بأخذ عنوان قيمة حرفية، لذلك تحتاج إلى متغير مؤقت أولًا. وتضيف بعض قواعد الكود دوالًا مساعدة مثل intPtr(n int) *int، لكن هذا ينقل المشكلة إلى مكان آخر فقط.

تعالج Go 1.26 هذا بتوسيع الدالة المضمّنة new(). كانت تقبل الأنواع فقط، فتعطيك new(int) قيمة *int تؤشر إلى الصفر. أما الآن فهي تقبل أي تعبير:

example.gogo
package main

import "fmt"

type ServerConfig struct {
    Host    string
    Port    *int
    Enabled *bool
}

func main() {
    config := ServerConfig{
        Host:    "localhost",
        Port:    new(8080),
        Enabled: new(true),
    }

    fmt.Printf("Port: %d, Enabled: %t\n", *config.Port, *config.Enabled)
}

عندما تكتب new(8080)، تُقيّم Go التعبير، وتحجز ذاكرة لنوعه (وهو int هنا)، وتخزن القيمة، وتعيد مؤشرًا. وأي تعبير يصلح: new(x + y) لقيمة محسوبة، و new(time.Now()) لنتيجة دالة، و new("default") لنص.

ستستخدمها أكثر ما تستخدمها مع واجهات JSON ورسائل Protobuf، وأي struct آخر يفرّق فيه حقل المؤشر بين «اختياري وله قيمة» و«غير مُقدَّم».

فحص الأخطاء بأمان الأنواع مع errors.AsType

لاستخراج نوع خطأ محدد من خطأ مُغلَّف، كنت تستخدم errors.As():

example.gogo
var appErr *AppError
if errors.As(err, &appErr) {
    fmt.Printf("Code: %d\n", appErr.Code)
}

هذا يعمل، لكنه ثقيل. فالمتغير يتسرب إلى النطاق الخارجي مع أنك لا تستخدمه إلا داخل كتلة if. وأنت تمرر &appErr مع أن appErr مؤشر أصلًا، وهذا يربك كثيرين. كما أن errors.As تعتمد على الانعكاس (reflection) في الخلفية.

تضيف Go 1.26 الدالة errors.AsType[T]()، وهي دالة عامة تؤدي المهمة نفسها بتكلّف أقل:

example.gogo
package main

import (
    "errors"
    "fmt"
)

type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("app error %d: %s", e.Code, e.Message)
}

func main() {
    err := &AppError{Code: 404, Message: "not found"}

    if appErr, ok := errors.AsType[*AppError](err); ok {
        fmt.Printf("Code: %d, Message: %s\n", appErr.Code, appErr.Message)
    }

    // Works with wrapped errors too
    wrappedErr := fmt.Errorf("operation failed: %w", err)
    if appErr, ok := errors.AsType[*AppError](wrappedErr); ok {
        fmt.Printf("Unwrapped code: %d\n", appErr.Code)
    }
}

يخبر معامل النوع [*AppError] المترجم (compiler) بدقة عما تبحث عنه، والنتيجة محصورة في نطاق كتلة if. لا يوجد مؤشر إلى مؤشر. ولأن النوع معروف وقت الترجمة، فلا يوجد انعكاس أيضًا، لذلك تعمل أسرع في مسارات معالجة الأخطاء الساخنة.

قيود generics ذاتية المرجعية

تضيف Go 1.26 دعمًا محدودًا لقيود الأنواع التكرارية (recursive type constraints). وهذا يجعل بعض الأنماط قابلة للتعبير عنها لأول مرة، مثل نوع يقارن نفسه بقيم أخرى من النوع نفسه:

example.gogo
package main

import "fmt"

type Comparable[T any] interface {
    CompareTo(other T) int
}

type Integer int

func (i Integer) CompareTo(other Integer) int {
    if i < other {
        return -1
    } else if i > other {
        return 1
    }
    return 0
}

func Max[T Comparable[T]](a, b T) T {
    if a.CompareTo(b) > 0 {
        return a
    }
    return b
}

func main() {
    result := Max(Integer(5), Integer(3))
    fmt.Printf("Max: %d\n", result)
}

يشير القيد Comparable[T] إلى معامل نوعه الخاص، ويشترط Max[T Comparable[T]] أن يكون T قابلًا للمقارنة بنفسه. قبل Go 1.26 لم يكن هذا النوع من القيود العامة ذاتية المرجعية يُترجَم. أما الآن فيمكنك بناء واجهات سلسة تعتمد على تسلسل الدوال، وأشجار تشير عقدها إلى نوعها ذاته، وبناة (builders) آمنين من ناحية الأنواع يعيدون النوع الصحيح في كل خطوة.

واجهات API أبسط للتشفير

معظم مطوري Go كتبوا هذا السطر:

example.gogo
key, err := rsa.GenerateKey(rand.Reader, 2048)

الوسيط rand.Reader بقية من الماضي. فقد كانت إصدارات Go الأولى تسمح بمصادر عشوائية مخصصة لأغراض الاختبار. أما عمليًا فيجب أن تمرر crypto/rand.Reader دائمًا، وتمرير أي شيء آخر، مثل math/rand، يفتح ثغرة أمنية.

تجعل Go 1.26 الخيار الآمن هو الافتراضي. مرر nil وستستخدم الدالة crypto/rand.Reader داخليًا:

example.gogo
package main

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
)

func main() {
    key := make([]byte, 32)
    rand.Read(key)

    fmt.Printf("Generated key: %s\n", hex.EncodeToString(key))
}

يشمل هذا توليد المفاتيح في حزم التشفير كلها: rsa.GenerateKey(nil, bits)، و ecdsa.GenerateKey(curve, nil)، و ecdh.GenerateKey(curve, nil). والسبب الوحيد الباقي لتمرير reader خاص بك هو اختبار يحتاج إلى مخرجات حتمية.

HPKE للتشفير الحديث

تنفّذ الحزمة الجديدة crypto/hpke معيار RFC 9180، وهو معيار حديث للتشفير الهجين بالمفتاح العام. RSA وحده بطيء ولا يستطيع تشفير إلا رسائل صغيرة. والحل المعتاد هو تشفير مفتاح متماثل عشوائي باستخدام RSA، ثم استخدام ذلك المفتاح للبيانات الفعلية. هذا يعمل، لكن عليك أن تنفّذه بعناية.

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

example.gogo
suite := hpke.NewSuite(
    hpke.DHKEM_X25519,
    hpke.KDF_HKDF_SHA256,
    hpke.AEAD_ChaCha20Poly1305,
)

publicKey, privateKey, _ := suite.GenerateKeyPair(nil)

sender, _ := suite.NewSender(publicKey, nil)
ciphertext, _ := sender.Seal(plaintext, nil)
encapsulated := sender.EncapsulatedKey()

recipient, _ := suite.NewRecipient(privateKey, encapsulated)
decrypted, _ := recipient.Open(ciphertext, nil)

يستخدم HPKE مفاتيح بطول 256 بت بدلًا من 2048-4096 بت في RSA، ويتعامل مع رسائل بأي حجم، وهو أسرع بكثير.

كشف تسريبات goroutine

من السهل ألا تنتبه إلى تسريبات goroutine. فتسريب الذاكرة يُسقط برنامجك في النهاية. أما تسريب goroutine فيجعله أبطأ مع الوقت فقط، إذ تتراكم آلاف من goroutines المعلّقة وتحتفظ بالذاكرة والمعالج دون أن تؤدي أي عمل مفيد.

إليك طريقة شائعة لتسريب واحدة:

example.gogo
func handleRequest(ctx context.Context) error {
    results := make(chan Result)

    go func() {
        result := expensiveOperation()
        results <- result  // Blocks forever if context cancels
    }()

    select {
    case <-ctx.Done():
        return ctx.Err()  // Goroutine still blocked on send
    case r := <-results:
        return processResult(r)
    }
}

إذا أُلغي context قبل أن تعود expensiveOperation()، تتوقف الـ goroutine إلى الأبد وهي تحاول الإرسال على channel غير مُخزَّن لن يقرأ منه أحد أبدًا.

تضيف Go 1.26 ملف التحليل goroutineleak والراية go test -goroutineleak اللذين يلتقطان هذه الحالات تلقائيًا. ويمكنك أيضًا أن ترى التسريب بعدّ goroutines قبل العملية وبعدها:

example.gogo
package main

import (
    "fmt"
    "runtime"
    "time"
)

func createLeak() {
    ch := make(chan int)
    go func() {
        <-ch  // Blocks forever - no sender
    }()
}

func main() {
    before := runtime.NumGoroutine()
    createLeak()
    time.Sleep(50 * time.Millisecond)
    after := runtime.NumGoroutine()

    fmt.Printf("Before: %d, After: %d\n", before, after)
    if after > before {
        fmt.Println("Goroutine leak detected!")
    }
}

للحل ثلاثة أجزاء: channel مُخزَّن حتى لا يتوقف الإرسال، و select داخل الـ goroutine لتخرج عند إلغاء context، وفحص لـ context في المستدعي:

example.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

func expensiveWork() string {
    time.Sleep(50 * time.Millisecond)
    return "completed"
}

func safeHandler(ctx context.Context) (string, error) {
    ch := make(chan string, 1)

    go func() {
        result := expensiveWork()
        select {
        case ch <- result:
        case <-ctx.Done():
            return
        }
    }()

    select {
    case <-ctx.Done():
        return "", ctx.Err()
    case result := <-ch:
        return result, nil
    }
}

func main() {
    ctx1 := context.Background()
    result1, err1 := safeHandler(ctx1)
    fmt.Printf("Normal: %s, err: %v\n", result1, err1)

    ctx2, cancel := context.WithCancel(context.Background())
    cancel()
    result2, err2 := safeHandler(ctx2)
    fmt.Printf("Canceled: %s, err: %v\n", result2, err2)
}

بفضل الـ channel المُخزَّن لا يمكن أن يتوقف الإرسال حتى إن لم يستقبل أحد. وتعطي select الـ goroutine مخرجًا عند إلغاء context. وسواء نجح العمل أو أُلغي، تنتهي الـ goroutine.

كما تضيف Go 1.26 مقاييس goroutines إلى runtime/metrics، فتستطيع لوحات المراقبة في بيئة الإنتاج تتبّع العدد الكلي لـ goroutines، والمحجوب منها، والقابل للتشغيل.

تحسينات المكتبة القياسية

Buffer.Peek

القراءة من bytes.Buffer تستهلك البيانات. فإذا أردت أن تلقي نظرة أولًا، كأن تفحص نوع رسالة قبل أن تختار المحلل، أو تتحقق من صيغة قبل قراءتها، كان عليك أن تنسخ الـ buffer.

تضيف Go 1.26 الدالة Peek():

example.gogo
package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("Hello, World!")

    peeked := buf.Bytes()[:5]
    fmt.Printf("Peeked: %s\n", peeked)

    fmt.Printf("Full content: %s\n", buf.String())
    fmt.Printf("Length: %d\n", buf.Len())
}

مقارنة بادئات IP

كان ترتيب بادئات IP يعني مقارنة البايتات يدويًا. أما الآن فلدى netip.Prefix دالة Compare() تُمرَّر مباشرة إلى slices.SortFunc:

example.gogo
package main

import (
    "fmt"
    "net/netip"
    "slices"
)

func main() {
    prefixes := []netip.Prefix{
        netip.MustParsePrefix("192.168.0.0/16"),
        netip.MustParsePrefix("10.0.0.0/8"),
        netip.MustParsePrefix("172.16.0.0/12"),
    }

    slices.SortFunc(prefixes, func(a, b netip.Prefix) int {
        return a.Compare(b)
    })

    for _, p := range prefixes {
        fmt.Println(p)
    }
}

سبب الإلغاء في context الإشارات

إذا كنت تستخدم signal.NotifyContext() للإيقاف السلس، فيمكنك الآن معرفة الإشارة التي سببت الإلغاء:

example.gogo
ctx, stop := signal.NotifyContext(
    context.Background(),
    os.Interrupt,
    syscall.SIGTERM,
)
defer stop()

<-ctx.Done()

if cause := context.Cause(ctx); cause != nil {
    switch cause {
    case os.Interrupt:
        log.Println("User interrupted (Ctrl+C)")
    case syscall.SIGTERM:
        log.Println("Graceful shutdown requested")
    }
}

الاختبارات والتسجيل

slog.MultiHandler

كثيرًا ما تكتب خدمات بيئة الإنتاج سجلاتها إلى أكثر من مكان: JSON إلى ملف لتجميع السجلات، ونص إلى stdout للتنقيح، والأخطاء إلى خدمة مراقبة. تتولى slog.MultiHandler هذا التوزيع عنك، فلا تحتاج إلى كتابته بنفسك:

example.gogo
package main

import (
    "log/slog"
    "os"
)

func main() {
    jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
    logger := slog.New(jsonHandler)
    logger.Info("Server started", "port", 8080)
}

كل handler يُرشّح حسب مستواه الخاص. فيمكن أن يستقبل الملف كل شيء بمستوى debug، بينما لا تصل إلى الطرفية إلا التحذيرات والأخطاء.

مجلدات مخرجات الاختبارات

كثيرًا ما تكتب اختبارات التكامل ملفات مخرجات مثل لقطات الشاشة، ومخرجات SQL، والتقارير المولّدة. كانت هذه الملفات تنتهي مبعثرة في /tmp أو في مجلد آخر، ولا شيء ينظّفها.

تعطيها t.ArtifactDir() موقعًا قياسيًا. يُنظَّف عند نجاح الاختبار ويُحفظ عند فشله:

example.gogo
func TestGenerateReport(t *testing.T) {
    artifactDir := t.ArtifactDir()
    reportPath := filepath.Join(artifactDir, "report.txt")
    
    err := os.WriteFile(reportPath, []byte("Test Report\n"), 0644)
    if err != nil {
        t.Fatalf("Failed to write: %v", err)
    }
}

الاحتفاظ بالملفات عند الفشل هو ما يجعل هذا مفيدًا. فتستطيع فتح مخرجات اختبار فاشل دون أن تبحث عنها.

تحسينات httptest

كان اختبار كود HTTPS الذي يتصل بمستضيفات خارجية يعني تعطيل التحقق من الشهادات (وهي فكرة سيئة حتى في الاختبارات) أو إعدادًا مزعجًا للشهادات. في Go 1.26 توجّه httptest.Server.Client() طلبات example.com إلى خادم الاختبار الخاص بك وتتولى TLS عنك:

example.gogo
func TestExternalAPIClient(t *testing.T) {
    server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"status": "ok"}`))
    }))
    defer server.Close()

    client := server.Client()
    resp, err := client.Get("https://example.com/api/status")
    // Request goes to test server, not real example.com
}

الأداء: Green Tea GC

لا يحتاج أي من هذه التحسينات إلى تغيير في الكود. أعد البناء باستخدام Go 1.26 وسيعمل برنامجك أسرع.

يخفض جامع المهملات (garbage collector) الجديد Green Tea زمن الاستجابة عند P99 بنسبة 30-40% وكلفة الذاكرة بنسبة 10-15%، ويرفع معدل النقل بنسبة 5-10%. والبرامج التي تحجز كثيرًا من الكائنات قصيرة العمر تكسب أكثر من غيرها. وتأتي المكاسب من جدولة أفضل لعمل جامع المهملات ومن كلفة أقل لتتبع عمليات الحجز.

وأصبحت نداءات النظام ونداءات cgo أسرع بنحو 30% لأن بيئة التشغيل تخلّت عن حالة المعالج _Psyscall التي كانت تضيف كلفة على كل نداء نظام. كما صار حجز الكائنات الصغيرة (1-512 بايت) أسرع بنحو 30% أيضًا، بفضل إجراءات حجز متخصصة تستخدم جداول قفز بدلًا من المسارات العامة.

وأصبحت بعض دوال المكتبة القياسية أسرع كذلك. fmt.Errorf أسرع بنحو 30% و io.ReadAll أسرع بنسبة 28%. وفي اختبار على بيئة الإنتاج، انخفض زمن الاستجابة عند P99 من 45ms إلى 32ms في خادم HTTP API يعالج 10,000 طلب في الثانية.

ميزات بيئة التشغيل

الاتصال بالشبكة مع مراعاة context

تطبّق الحزمة net الآن المواعيد النهائية في context على تحليل DNS إلى جانب الاتصال نفسه:

example.gogo
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var dialer net.Dialer
conn, err := dialer.DialContext(ctx, "tcp", "example.com:443")
// Timeout now covers DNS + connect time combined

حماية البيانات الحساسة

تساعد الحزمة التجريبية runtime/secret في إبقاء البيانات الحساسة خارج تفريغات الذاكرة:

example.gogo
secretKey := secret.New([]byte("sk_live_secret"))
defer secretKey.Destroy()

apiKey := secretKey.Bytes()
makeAuthenticatedRequest(apiKey)

تمسح Destroy() الذاكرة، فتقصر المدة التي قد يظهر فيها السر في تفريغ النواة أو ملف التبديل.

عمليات SIMD

تعطيك الحزمة الجديدة simd/archsimd تعليمات المتجهات العتادية على amd64:

example.gogo
func vectorAdd(a, b [4]float32) [4]float32 {
    va := archsimd.Float32x4FromArray(a)
    vb := archsimd.Float32x4FromArray(b)
    return va.Add(vb).ToArray()
}

تعالج SIMD عدة قيم في التعليمة الواحدة، وهذا يعطي معدل نقل أعلى بمقدار 4-16 ضعفًا في العمليات الرقمية في معالجة الصور، وبرامج ترميز الصوت والفيديو، والحوسبة العلمية. وهي مرتبطة بالمنصة، لذلك اكتب بديلًا قياسيًا (scalar) إذا كان كودك سيعمل على معماريات أخرى.

ترحيل كودك

تحسّن Go 1.26 الأداة go fix لتتمكن من أتمتة عمليات الترحيل الشائعة:

example.bashbash
go fix -diff ./...   # Preview changes
go fix ./...         # Apply all fixes

تعيد الأداة كتابة rsa.GenerateKey(rand.Reader, 2048) إلى rsa.GenerateKey(nil, 2048)، وتحوّل أنماط errors.As إلى errors.AsType حيث أمكن. راجع الـ diff، وشغّل اختباراتك، ثم ثبّت التغييرات.

القائمة الكاملة لعمليات إعادة الكتابة موجودة في كل أداة تحديث في go fix 1.26.

ماذا يعني هذا لكودك

أول ما ستلمسه من التغييرات هو new(expr) و errors.AsType، و go fix يتولى جزءًا من هذا الترحيل عنك. شغّل go test -goroutineleak على كودك المتزامن لتجد التسريبات قبل أن تصل إلى بيئة الإنتاج. وصارت دوال توليد مفاتيح التشفير تختار مصدر العشوائية الآمن حين تمرر nil. أما تسريعات جامع المهملات وبيئة التشغيل فلا تطلب منك شيئًا سوى إعادة البناء.

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

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

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