Go Programming

غلبه بر قدرت و ایمنی: مثال‌های عملی از جنریک‌های Go برای توسعه مدرن

از زمان معرفی Go 1.18، جنریک‌ها به یکی از موردانتظارترین ویژگی‌ها در تاریخ این زبان تبدیل شده‌اند. برای سال‌ها، توسعه‌دهندگان Go برای دستیابی به ایمنی نوع بدون کدهای تکراری به ابزارهای تولید کد مانند stringer یا اطمینان‌های دستی اینترفیس متکی بودند. اگرچه این روش‌ها مؤثر بودند، اما اغلب قصد برنامه را مخفی می‌کردند و پیچیدگی را به فرآیند ساخت اضافه می‌کردند.

جنریک‌ها به ما امکان می‌دهند کدی بنویسیم که روی هر نوعی عمل کند، با محدودیت‌های خاص، بدون قربانی کردن ایمنی نوع یا عملکرد. برای توسعه‌دهندگان متوسط و پیشرفته، درک نحوه استفاده مؤثر از این محدودیت‌ها برای نوشتن کدی قابل نگهداری و DRY (تکرار نکنید) حیاتی است. در این پست، ما مثال‌های عملی از جنریک‌های Go را کاوش خواهیم کرد و از قطعه‌کدهای ساده «Hello World» فراتر رفته و به سناریوهای دنیای واقعی می‌پردازیم.

حذف کدهای تکراری با توابع جنریک

رایج‌ترین مورد استفاده برای جنریک‌ها، حذف توابع تکراری است که تنها از نظر نوعشان متفاوت هستند. فرض کنید در سنیاریویی قرار دارید که نیاز به یافتن مقدار حداقل در یک مجموعه از اعداد دارید. بدون جنریک‌ها، شما به توابع جداگانه برای int، float64 و int64 نیاز خواهید داشت.

با جنریک‌ها، می‌توانیم یک تابع واحد را با استفاده از یک پارامتر نوع T و یک محدودیت تعریف کنیم. کتابخانه استاندارد comparable را ارائه می‌دهد که هر نوعی که از بررسی‌های برابری پشتیبانی می‌کند (== و !=) را مطابقت می‌دهد. با این حال، برای یافتن حداقل‌ها، اغلب به ترتیب‌بندی نیاز داریم. کتابخانه استاندارد Go یک محدودیت مرتب‌سازی داخلی ارائه نمی‌دهد، اما می‌توانیم خودمان یکی بسازیم.

package main

import (
	"fmt"
)

// Ordered یک محدودیت است که ما برای انواعی که از مقایسه پشتیبانی می‌کنند تعریف می‌کنیم
type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
	~float32 | ~float64 | ~string
}

// FindMin کوچکترین مقدار را در یک برش از هر نوع مرتب‌شده برمی‌گرداند.
func FindMin[T Ordered](s []T) (T, bool) {
	if len(s) == 0 {
		var zero T
		return zero, false
	}
	min := s[0]
	for _, v := range s[1:] {
		if v < min {
			min = v
		}
	}
	return min, true
}

func main() {
	ints := []int{3, 1, 4, 1, 5, 9}
	if min, ok := FindMin(ints); ok {
		fmt.Printf("Min int: %d\n", min)
	}

	floats := []float64{1.1, 2.2, 0.5}
	if min, ok := FindMin(floats); ok {
		fmt.Printf("Min float: %.1f\n", min)
	}
}

به استفاده از علامت مد (~) در محدودیت توجه کنید. این نشان می‌دهد که پارامتر نوع می‌تواند هر نوعی باشد که نوع پایه‌ای آن یکی از انواع مشخص‌شده باشد (برای مثال، یک نوع سفارشی بر اساس int). این انعطاف‌پذیری کلید اصلی برای قابل استفاده مجدد بودن واقعی جنریک‌ها است.

انواع جنریک برای مجموعه‌ها

یک کاربرد قدرتمند دیگر جنریک‌ها، ایجاد ساختارهای داده جنریک است. بیایید یک پشته (Stack) ساده و ایمن از نظر نوع را پیاده‌سازی کنیم. برخلاف برش‌ها که می‌توانند به صورت پویا رشد کنند، یک پشته اغلب ظرفیت ثابت یا سمانتیک‌های خاص push/pop دارد.

package main

import (
	"errors"
	"fmt"
)

type Stack[T any] struct {
	items []T
}

// NewStack یک پشته جدید با ظرفیت مشخص‌شده ایجاد می‌کند.
func NewStack[T any](capacity int) *Stack[T] {
	return &Stack[T]{
		items: make([]T, 0, capacity),
	}
}

// Push یک آیتم را به بالای پشته اضافه می‌کند.
func (s *Stack[T]) Push(item T) {
	s.items = append(s.items, item)
}

// Pop آیتم بالایی را حذف و برمی‌گرداند. در صورت خالی بودن خطا برمی‌گرداند.
func (s *Stack[T]) Pop() (T, error) {
	if len(s.items) == 0 {
		var zero T
		return zero, errors.New("stack is empty")
	}
	index := len(s.items) - 1
	item := s.items[index]
	s.items = s.items[:index]
	return item, nil
}

func main() {
	// ایجاد یک پشته از رشته‌ها
	stringStack := NewStack[string](10)
	stringStack.Push("First")
	stringStack.Push("Second")

	item, err := stringStack.Pop()
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Printf("Popped: %s\n", item) // خروجی: Popped: Second
	}
}

در این مثال، ما از [T any] استفاده می‌کنیم. نوع any یک نام مستعار برای interface{} است، به این معنی که هر نوعی را می‌پذیرد. اگرچه این محدودیت کمتری نسبت به اینترفیس Ordered در مثال قبلی دارد، اما به ما امکان می‌دهد یک پشته جنریک ایجاد کنیم که می‌تواند اعداد صحیح، رشته‌ها یا حتی ساختارهای سفارشی را بدون نیاز به پیاده‌سازی‌های جداگانه نگه دارد.

مشاوره عملی: چه زمانی از جنریک‌ها استفاده کنیم

اگرچه جنریک‌ها قدرتمند هستند، اما راه‌حل جادویی نیستند. در اینجا چند راهنمایی وجود دارد که باید در نظر بگیرید:

  • از مهندسی بیش‌ازحد پرهیز کنید: اگر فقط به تابعی نیاز دارید که با دو نوع کار کند، به جای آن از اینترفیس استفاده کنید. جنریک‌ها زمانی درخشان می‌شوند که چندین نوع داشته باشید یا نیاز به پرهیز از بازتاب (reflection) داشته باشید.
  • خوانایی مهم است: کد جنریک گاهی اوقات می‌تواند سخت‌تر از کد عادی خوانده شود. همیشه تلاش کنید تا محدودیت‌ها و پارامترهای نوع خود را ساده نگه دارید. از محدودیت‌های تو در تو و عمیق پرهیز کنید.
  • به کتابخانه استاندارد پایبند باشید: در صورت امکان، از محدودیت‌های کتابخانه استاندارد (مانند comparable) یا محدودیت‌های سفارشی به خوبی درک‌شده استفاده کنید تا ثبات را در کل کدبیس خود تضمین کنید.

نتیجه‌گیری

جنریک‌ها در Go گام بزرگی در جهت امکان‌پذیر کردن استفاده مجدد از کد در حالی که فلسفه اصلی زبان یعنی سادگی و ایمنی نوع را حفظ می‌کند، محسوب می‌شوند. با تسلط بر محدودیت‌ها و پارامترهای نوع، می‌توانید برنامه‌های تمیزتر و قوی‌تری بنویسید که با نیازهای تیم شما مقیاس‌پذیر باشند. کوچک شروع کنید: یک تابع تکراری را بازسازی کنید یا یک نوع ابزار جنریک ایجاد کنید و به تدریج این الگوها را در پروژه‌های خود ادغام کنید.

همان‌طور که اکوسیستم Go بالغ‌تر می‌شود، احتمالاً کتابخانه‌ها و اصطلاحات جنریک پیشرفته‌تری ظاهر خواهند شد. آگاه ماندن و تمرین با این ابزارهای جدید، شما را در خط مقدم توسعه مدرن Go نگه می‌دارد.

Share: