Go Programming

اجرای جنریک‌ها در گو: ساخت مبدل‌های JSON ایمن از نظر نوع و ابزارهای برش سفارشی

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

در این پست، بررسی می‌کنیم که چگونه می‌توان از جنریک‌ها برای حل دو مشکل رایج در توسعه گو استفاده کرد: تبدیل ساختارهای داده JSON ناهمگن و ایجاد ابزارهای برش (Slice) قابل استفاده مجدد و ایمن از نظر نوع. این مثال‌ها نه تنها نحو جنریک‌ها، بلکه مزایای معماری استفاده از آن‌ها در کدهای آماده تولید را نشان می‌دهند.

مشکل مبدل‌های مبتنی بر بازتاب

قبل از جنریک‌ها، مدیریت تبدیل JSON معمولاً شامل یکی از دو رویکرد زیر بود: نوشتن دستی منطق سریال‌سازی برای هر ساختار (struct)، یا استفاده از بازتاب برای بررسی ساختارها در زمان اجرا. رویکرد دوم، اگرچه انعطاف‌پذیر است، کند و نامفهوم است. اگر نام یک فیلد اشتباه تایپ شود یا یک تبدیل نوع شکست بخورد، شما تنها در زمان اجرا متوجه باگ می‌شوید، اغلب در لایه‌های عمیق‌تر پشته برنامه شما.

جنریک‌ها به ما اجازه می‌دهند توابعی را تعریف کنیم که روی هر نوعی که محدودیت‌های خاصی را برآورده کند، عمل می‌کنند. با ترکیب جنریک‌ها با بسته encoding/json گو، می‌توانیم مبدل‌هایی بسازیم که در زمان کامپایل بررسی می‌شوند. این تضمین می‌کند که اگر ساختار داده‌های شما تغییر کند، کامپایلر بلافاصله شما را مطلع می‌کند، بسیار پیش از آنکه کد شما به محیط تولید برسد.

ساخت یک مبدل JSON ایمن از نظر نوع

بیایید یک تابع جنریک بسازیم که یک برش از هر نوعی را به یک برش از نوع دیگر تبدیل می‌کند، به شرطی که هر دو نوع قابلیت مارشال (marshal) شدن به JSON را داشته باشند. این موضوع به ویژه در معماری‌های میکروسرویس مفید است، جایی که ممکن است نیاز به تبدیل مدل‌های دامنه داخلی به پاسخ‌های API خارجی داشته باشید.

package transformer

import "encoding/json"

// TransformSlice یک برش از نوع S را به یک برش از نوع T تبدیل می‌کند.
// فرض می‌شود که هر دو S و T می‌توانند به JSON مارشال و آنمارشال شوند.
func TransformSlice[S any, T any](source []S) ([]T, error) {
	// برش منبع را به بایت‌های JSON مارشال می‌کند
	jsonBytes, err := json.Marshal(source)
	if err != nil {
		return nil, err
	}

	// بایت‌های JSON را به نوع هدف T آنمارشال می‌کند
	var target []T
	err = json.Unmarshal(jsonBytes, &target)
	if err != nil {
		return nil, err
	}

	return target, nil
}

// مثال استفاده:
// type UserInternal struct { ID int64; Email string; CreatedAt time.Time }
// type UserExternal struct { UserID string; EmailAddress string; Timestamp string }
//
// نکته: این مبدل ساده به تطابق نام فیلدها یا 
// تگ‌های سفارشی json متکی است. برای نگاشت‌های پیچیده، یک مپر دستی با استفاده از جنریک‌ها 
// ممکن است برای جلوگیری از سربار سریال‌سازی مناسب‌تر باشد.

اگرچه مثال بالا مختصر است، اما هزینه مارشال و آنمارشال JSON را به همراه دارد. برای سناریوهای با عملکرد بالا، می‌توانیم یک مپر جنریک بنویسیم که فیلدها را مستقیماً کپی کند. با این حال، رویکرد مبتنی بر JSON برای تکامل طرحواره و interoperability بین سرویس‌هایی با طرحواره‌های شل‌بسته (loosely coupled) بی‌نظیر است.

ایجاد ابزارهای برش قابل استفاده مجدد

کتابخانه استاندارد گو در نسخه ۱.۲۱ به بعد، بسته slices را ارائه می‌دهد، اما درک نحوه ساخت این ابزارها به ایجاد انتزاعات خاص دامنه کمک می‌کند. بیایید یک تابع جنریک Filter و یک تابع Map را پیاده‌سازی کنیم که می‌توانند روی هر برشی اعمال شوند.

package utils

// Filter یک برش جدید شامل عناصری از 's' را برمی‌گرداند که برای آن‌ها 'predicate' مقدار true را برمی‌گرداند.
func Filter[S any, T any](s []S, predicate func(S) bool) []T {
	var result []T
	for _, item := range s {
		if predicate(item) {
			// اگر T با S متفاوت است، آیتم را تبدیل یا پرتاب کنید
			// در اینجا فرض می‌شود که T می‌تواند از S مشتق شود یا همان نوع است
			if val, ok := any(item).(T); ok {
				result = append(result, val)
			} else {
				// حالت جایگزین برای موارد ساده‌ای که S == T
				result = append(result, any(item).(T))
			}
		}
	}
	return result
}

// Map یک تابع را روی هر عنصر در 's' اعمال می‌کند و یک برش جدید از نوع 'T' را برمی‌گرداند.
func Map[S any, T any](s []S, f func(S) T) []T {
	result := make([]T, len(s))
	for i, item := range s {
		result[i] = f(item)
	}
	return result
}

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

بهترین شیوه‌ها و ملاحظات عملکرد

اگرچه جنریک‌ها انعطاف‌پذیری ارائه می‌دهند، اما به دلیل حل پارامترهای نوع، مقداری سربار ایجاد می‌کنند. در حلقه‌های فشرده، این گاهی اوقات گران‌تر از استفاده از رابط‌ها (interfaces) است، اگرچه زمان‌های اجرای مدرن گو این موضوع را به طور قابل توجهی بهینه کرده‌اند. همیشه کد خود را پروفایل کنید. برای تبدیل‌های JSON، اگر عملکرد حیاتی است، استفاده از jsoniter یا کپی دستی فیلدها با جنریک‌ها را در نظر بگیرید به جای مارشال کردن کامل JSON.

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

نتیجه‌گیری

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

کوچک شروع کنید. چند حلقه تکراری را با توابع جنریک Map و Filter جایگزین کنید و بهبود وضوح و قابلیت اطمینان در پایگاه کد خود را مشاهده نمایید.

Share: