9.4.11.1 توضیحات #
الگوی Future (یا Promise) یکی از الگوهای مهم و کاربردی در طراحی سیستمهای ناهمزمان (asynchronous) است که در زبان Go نیز، اگرچه به صورت مستقیم در کتابخانه استاندارد وجود ندارد، اما میتوان با استفاده از ابزارهای زبان مانند goroutine و channel، بهسادگی آن را پیادهسازی کرد. هدف این الگو این است که یک “آبجکت” یا واسط به برنامهنویس داده شود که نماینده نتیجه یک عملیات (مانند درخواست شبکه یا محاسبه سنگین) است—حتی اگر آن عملیات هنوز به پایان نرسیده باشد.
در عمل، وقتی عملیاتی به صورت asynchronous آغاز میشود، به جای اینکه فوراً منتظر نتیجه بمانیم (و اجرای برنامه را بلاک کنیم)، یک مقدار از نوع Future دریافت میکنیم. این Future به عنوان placeholder یا وعدهای برای تحویل نتیجه نهایی به کار میرود. در پسزمینه، عملیات اصلی (مثلاً دریافت داده از API یا خواندن از دیسک) با یک goroutine انجام میشود و زمانی که به پایان رسید، نتیجه در Future ذخیره و آماده دسترسی میشود. هر زمان که برنامه به نتیجه نیاز داشته باشد، میتواند روی Future فراخوانی انجام دهد (مثلاً با خواندن از یک channel یا متد Get/Result)؛ اگر نتیجه هنوز آماده نشده باشد، برنامه به طور بلاک تا تکمیل عملیات منتظر میماند و بلافاصله پس از آمادهشدن داده، ادامه اجرا انجام میشود.
مزیت کلیدی الگوی Future در Go، جداسازی منطق اجرای عملیات ناهمزمان از منطق مصرفکننده آن است. این کار خوانایی و مدیریت خطا را سادهتر، مدیریت منابع را بهینهتر و کد را مقیاسپذیرتر میکند. با استفاده از این الگو میتوان معماریهای مدرن با پردازش موازی و کارآمد ساخت، بدون آنکه درگیر callback hell یا کد پیچیده شوید. همچنین Future پایه بسیاری از فریمورکها و ابزارهای concurrent در زبانهای دیگر (مانند Java, Rust, JavaScript) نیز هست و در Go، idiomatic ترین پیادهسازی معمولاً مبتنی بر channel و goroutine است.
9.4.11.2 دیاگرام #
9.4.11.3 نمونه کد #
1package main
2
3import (
4 "errors"
5 "fmt"
6 "sync"
7 "time"
8)
9
10type FutureInt struct {
11 once sync.Once
12 result int
13 err error
14 done chan struct{}
15}
16
17// Get با بلاک تا تکمیل شدن عملیات صبر میکند و نتیجه و خطا را برمیگرداند
18func (f *FutureInt) Get() (int, error) {
19 <-f.done
20 return f.result, f.err
21}
22
23// GetWithTimeout با تایماوت مشخص منتظر نتیجه میماند
24func (f *FutureInt) GetWithTimeout(timeout time.Duration) (int, error) {
25 select {
26 case <-f.done:
27 return f.result, f.err
28 case <-time.After(timeout):
29 return 0, errors.New("timeout waiting for future")
30 }
31}
32
33func longRunningTask() *FutureInt {
34 f := &FutureInt{done: make(chan struct{})}
35 go func() {
36 defer close(f.done)
37 // شبیهسازی کار زمانبر و گاهی بروز خطا
38 time.Sleep(time.Second)
39 if time.Now().Unix()%2 == 0 {
40 f.result = 42
41 f.err = nil
42 } else {
43 f.result = 0
44 f.err = errors.New("unexpected error")
45 }
46 }()
47 return f
48}
49
50func main() {
51 f := longRunningTask()
52 fmt.Println("Do something else while waiting for result...")
53 // دریافت نتیجه با مدیریت خطا
54 result, err := f.Get()
55 if err != nil {
56 fmt.Println("Future failed:", err)
57 return
58 }
59 fmt.Println("The answer is:", result)
60
61 // نمونه با timeout
62 f2 := longRunningTask()
63 result2, err2 := f2.GetWithTimeout(500 * time.Millisecond)
64 if err2 != nil {
65 fmt.Println("Timeout error:", err2)
66 } else {
67 fmt.Println("Result with timeout:", result2)
68 }
69}
در این مثال از الگوی Future در Go، ما یک ساختار کامل و قابل اطمینان برای مدیریت نتیجهی عملیات ناهمزمان (asynchronous) و دریافت امن و حرفهای نتیجه، همراه با مدیریت خطا و قابلیت timeout پیادهسازی کردهایم.
در ساختار FutureInt
، یک کانال از نوع chan struct{}
با نام done
وجود دارد که سیگنال اتمام عملیات را ارسال میکند. مقدار نتیجه (result
) و خطا (err
) به صورت فیلدهای struct نگهداری میشوند. زمانی که عملیات ناهمزمان (در goroutine مربوط به تابع longRunningTask
) به پایان میرسد، کانال done
بسته میشود تا هر goroutine منتظر یا فراخوانی کنندهی Get یا GetWithTimeout متوجه آمادهشدن نتیجه شود.
متد Get
تا زمانی که عملیات کامل نشده منتظر میماند و پس از اتمام، مقدار نهایی و خطا را بازمیگرداند. این کار با خواندن از کانال done
انجام میشود، که هم thread-safe و هم idiomatic است. متد GetWithTimeout
علاوه بر انتظار برای تکمیل، این امکان را میدهد که اگر نتیجه طی زمان معینی آماده نشد، با پیغام خطای timeout عملیات را مدیریت کنید—این قابلیت در سناریوهای real-time و حساس به تاخیر اهمیت زیادی دارد.
در تابع main
، ابتدا یک Future ساخته میشود و قبل از فراخوانی نتیجه میتوان هر کار دیگری انجام داد (این همان مزیت کلیدی Future است). سپس با صدا زدن Get، نتیجه و خطا را دریافت و مدیریت میکنیم. همچنین نمونهای از دریافت نتیجه با timeout هم آورده شده است تا نحوهی مدیریت عملیات طولانی یا گیر افتاده نیز مشخص باشد.
این معماری علاوه بر ایمنی همزمانی، جداسازی وظایف (تولید و مصرف نتیجه)، پشتیبانی از خطا و timeout، به سادگی قابل توسعه و استفاده در پروژههای واقعی و تولیدی است و تجربه برنامهنویسی concurrent را بسیار حرفهایتر و قابل کنترلتر میکند.
9.4.11.4 کاربردها #
- درخواستهای شبکه (Asynchronous Network Requests):
در زمانی که نیاز به ارسال درخواست به یک سرویس خارجی یا API دارید، الگوی Future کمک میکند که بتوانید درخواست را به صورت ناهمزمان ارسال و به محض آماده شدن پاسخ، آن را دریافت کنید. این کار باعث میشود بتوانید بدون مسدود کردن برنامه، کارهای دیگری انجام دهید تا زمانی که نتیجه آماده شود. این تکنیک برای توسعه کلاینتهای HTTP، REST، GraphQL و حتی WebSocket بسیار کاربردی است. - کوئریهای پایگاه داده (Async Database Querying):
هنگام اجرای کوئریهای سنگین یا زمانبر روی دیتابیس، Future اجازه میدهد کوئری را به صورت ناهمزمان آغاز کنید و هر زمان که نتیجه واقعاً لازم بود، آن را دریافت کنید. این رویکرد برای برنامههایی که باید همزمان چند کوئری مختلف را به دیتابیس ارسال کنند (مانند جمعآوری داده از چند جدول یا سرور متفاوت) بسیار مفید است و latency کلی برنامه را کاهش میدهد. - محاسبات سنگین و پردازش موازی:
اگر در برنامه نیاز به انجام محاسبات CPU-intensive (مانند تجزیه داده، پردازش تصویر، رمزنگاری و …) دارید، میتوانید هر task را در یک Future قرار دهید و نتایج را در صورت نیاز، به صورت همزمان و بدون بلاک شدن منتظر بمانید. این کار باعث بهبود performance و پاسخگویی سیستم خواهد شد. - ترکیب و همگامسازی عملیات مستقل (Composition and Synchronization):
میتوانید چندین Future ایجاد کنید و به طور موازی آنها را اجرا نمایید، سپس به صورت هماهنگ (مثلاً با WaitGroup یا channel) منتظر دریافت همه نتایج باشید (pattern معروف به Fan-In). این رویکرد برای جمعآوری نتایج عملیاتهای موازی (مانند دانلود چند فایل یا جمعآوری داده از چند سرویس) ایدهآل است. - پردازش رویداد و message queue:
در معماریهایی مانند صف پیام یا پردازش رویداد (event-driven)، هر message را میتوان به عنوان یک Future مدیریت کرد و پس از پایان پردازش، نتیجه یا پاسخ را برای ادامه کار استفاده نمود.