۰
همزمانی (Concurrency) یکی از ابعاد اساسی برنامهنویسی به زبان Go است و مدیریت مؤثر عملیات همزمان، کلید ساختن برنامههای مقاوم و بهینه است. یکی از ویژگیهای مهمی که به ما در این زمینه کمک میکند، پکیج Context در زبان Golang است. این پکیج ابزاری برای کنترل چرخهی عمر، لغو (cancellation) و انتشار درخواستها بین چند goroutine فراهم میکند. در این راهنمای جامع، به عمق مفهوم context در Golang میپردازیم، کاربردها و بهترین شیوهها را با مثالهای واقعی از صنعت نرمافزار بررسی میکنیم.
ویژگیهای جدیدی که در نسخه ۱.۲۱ افزوده شده، در این مقاله گنجانده شدهاند. این ویژگیها عبارتند از:
func AfterFunc
func WithDeadlineCause
func WithTimeoutCause
func WithoutCancel
پکیج context
بخشی از کتابخانه استاندارد Go است که ابزار قدرتمندی برای مدیریت عملیات همزمان ارائه میدهد. این پکیج اجازه میدهد سیگنالهای لغو، مهلتهای زمانی (deadlines) و مقادیر مورد نیاز بین goroutineها منتقل شده و عملیات وابسته به هم بتوانند در زمان مناسب بهصورت مرتب خاتمه یابند.
با استفاده از context، میتوانید سلسلهمراتبی از goroutineها بسازید و اطلاعات مهم را در طول این زنجیره منتقل کنید.
فرض کنید باید دادهها را همزمان از چند API مختلف دریافت کنید. با استفاده از context میتوانید اطمینان حاصل کنید که اگر هر یک از درخواستها بیش از زمان معین طول کشید، تمام درخواستها لغو شوند.
1package main 2 3import ( 4 "context" 5 "fmt" 6 "net/http" 7 "time" 8) 9 10func main() { 11 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 12 defer cancel() 13 14 urls := []string{ 15 "https://api.example.com/users", 16 "https://api.example.com/products", 17 "https://api.example.com/orders", 18 } 19 20 results := make(chan string) 21 22 for _, url := range urls { 23 go fetchAPI(ctx, url, results) 24 } 25 26 for range urls { 27 fmt.Println(<-results) 28 } 29} 30 31func fetchAPI(ctx context.Context, url string, results chan<- string) { 32 req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 33 if err != nil { 34 results <- fmt.Sprintf("Error creating request for %s: %s", url, err.Error()) 35 return 36 } 37 38 client := http.DefaultClient 39 resp, err := client.Do(req) 40 if err != nil { 41 results <- fmt.Sprintf("Error making request to %s: %s", url, err.Error()) 42 return 43 } 44 defer resp.Body.Close() 45 46 results <- fmt.Sprintf("Response from %s: %d", url, resp.StatusCode) 47}
خروجی ممکن است به شکل زیر باشد:
1Response from https://api.example.com/users: 200 2Response from https://api.example.com/products: 200 3Response from https://api.example.com/orders: 200
در این مثال، یک context با زمان تایماوت ۵ ثانیه ایجاد میکنیم و به هر درخواست API این context را اختصاص میدهیم. اگر هر یک از فراخوانیها بیش از ۵ ثانیه طول بکشد، سیگنال لغو از طریق context به تمام goroutineها ارسال میشود و درخواستهای باقیمانده لغو میشوند.
برای ساختن یک context، معمولاً از context.Background()
شروع میکنیم که یک context خالی و غیرقابل لغو را برمیگرداند و به عنوان ریشه در سلسلهمراتب contextها عمل میکند. سپس میتوانیم با استفاده از توابع context.WithTimeout()
یا context.WithDeadline()
contextهای جدید با محدودیت زمانی بسازیم.
در این مثال، یک context با تایماوت ۲ ثانیه ایجاد میکنیم و یک عملیات طولانی را شبیهسازی میکنیم:
1package main 2 3import ( 4 "context" 5 "fmt" 6 "time" 7) 8 9func main() { 10 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 11 defer cancel() 12 13 go performTask(ctx) 14 15 select { 16 case <-ctx.Done(): 17 fmt.Println("Task timed out") 18 } 19} 20 21func performTask(ctx context.Context) { 22 select { 23 case <-time.After(5 * time.Second): 24 fmt.Println("Task completed successfully") 25 } 26}
خروجی برنامه:
1Task timed out
در این نمونه، تابع performTask
یک کار طولانی مدت شبیهسازی شده انجام میدهد که ۵ ثانیه طول میکشد، اما به دلیل تایماوت ۲ ثانیهای context، عملیات قبل از اتمام کامل لغو میشود.
زمانی که یک context ساخته شد، میتوانید به تابعها یا goroutineهای پاییندستی آن را به عنوان آرگومان پاس بدهید تا عملیات مرتبط بتوانند در جریان لغو یا تغییراتی که در context رخ میدهد، قرار داشته باشند.
1package main 2 3import ( 4 "context" 5 "fmt" 6) 7 8func main() { 9 ctx := context.Background() 10 11 ctx = context.WithValue(ctx, "UserID", 123) 12 13 go performTask(ctx) 14 15 // ادامه عملیات دیگر 16} 17 18func performTask(ctx context.Context) { 19 userID := ctx.Value("UserID") 20 fmt.Println("User ID:", userID) 21}
خروجی:
1User ID: 123
در این مثال، ابتدا context زمینه را با context.Background()
میسازیم، سپس با context.WithValue()
یک مقدار به context اضافه میکنیم. این context به درون goroutine فرستاده شده و در آنجا مقدار بازیابی میشود.
علاوه بر انتشار context، میتوانید ارزشها یا دادههای ذخیره شده درون context را گرفته و در تابع یا goroutine استفاده کنید.
1package main 2 3import ( 4 "context" 5 "fmt" 6) 7 8func main() { 9 ctx := context.WithValue(context.Background(), "UserID", 123) 10 11 processRequest(ctx) 12} 13 14func processRequest(ctx context.Context) { 15 userID := ctx.Value("UserID").(int) 16 fmt.Println("Processing request for User ID:", userID) 17}
خروجی:
1Processing request for User ID: 123
در این نمونه، مقدار ذخیره شده با کلید "UserID"
با استفاده از type assertion به یک عدد صحیح تبدیل شده و استفاده میشود.
لغو (Cancellation) یک ویژگی اساسی در مدیریت context است که اجازه میدهد عملیات به شکلی مرتب و کنترلی پایان یابد و سیگنالهای لغو به goroutineهای مرتبط فرستاده شود.
1package main 2 3import ( 4 "context" 5 "fmt" 6 "time" 7) 8 9func main() { 10 ctx, cancel := context.WithCancel(context.Background()) 11 12 go performTask(ctx) 13 14 time.Sleep(2 * time.Second) 15 cancel() 16 17 time.Sleep(1 * time.Second) 18} 19 20func performTask(ctx context.Context) { 21 for { 22 select { 23 case <-ctx.Done(): 24 fmt.Println("Task cancelled") 25 return 26 default: 27 fmt.Println("Performing task...") 28 time.Sleep(500 * time.Millisecond) 29 } 30 } 31}
خروجی:
1Performing task... 2Performing task... 3Task cancelled
در این مثال، پس از ۲ ثانیه لغو context انجام میشود و goroutine که در حال اجرای performTask
است پیام لغو را دریافت و به صورت مرتب خارج میشود.
استفاده از محدودیتهای زمانی (Timeouts) و Deadlines برای اطمینان از اتمام عملیات در زمانی معقول اهمیت زیادی دارد و از ایجاد گلوگاه یا انتظار نامحدود جلوگیری میکند.
1package main 2 3import ( 4 "context" 5 "fmt" 6 "time" 7) 8 9func main() { 10 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second)) 11 defer cancel() 12 13 go performTask(ctx) 14 15 time.Sleep(3 * time.Second) 16} 17 18func performTask(ctx context.Context) { 19 select { 20 case <-ctx.Done(): 21 fmt.Println("Task completed or deadline exceeded:", ctx.Err()) 22 return 23 } 24}
خروجی:
1Task completed or deadline exceeded: context deadline exceeded
در این مثال، یک context با deadline ۲ ثانیه ایجاد شده و عملیات performTask
منتظر پایان یا لغو context میماند. پس از گذشت ۳ ثانیه، deadline تمام شده و عملیات لغو میشود.
استفاده از context در درخواستهای HTTP در Go اهمیت بالایی دارد و به کمک آن میتوان لغو درخواستها، تایماوت، و انتقال مقادیر مهم به handlerهای پاییندستی را مدیریت کرد.
1package main 2 3import ( 4 "context" 5 "fmt" 6 "net/http" 7 "time" 8) 9 10func main() { 11 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 12 defer cancel() 13 14 req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil) 15 if err != nil { 16 fmt.Println("Error creating request:", err) 17 return 18 } 19 20 client := http.DefaultClient 21 resp, err := client.Do(req) 22 if err != nil { 23 fmt.Println("Error making request:", err) 24 return 25 } 26 defer resp.Body.Close() 27 28 // پردازش پاسخ 29}
در اینجا، درخواست HTTP با context ساخته شده و در صورتی که پاسخ بیش از ۲ ثانیه زمان ببرد، لغو خواهد شد.
Context به خوبی میتواند در عملیات بانکهای اطلاعاتی به کار رود تا لغو کوئریها، تایماوت و انتقال دادههای مرتبط در تراکنشها کنترل شود.
1package main 2 3import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "time" 8 9 _ "github.com/lib/pq" 10) 11 12func main() { 13 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 14 defer cancel() 15 16 db, err := sql.Open("postgres", "postgres://username:password@localhost/mydatabase?sslmode=disable") 17 if err != nil { 18 fmt.Println("Error connecting to the database:", err) 19 return 20 } 21 defer db.Close() 22 23 rows, err := db.QueryContext(ctx, "SELECT * FROM users") 24 if err != nil { 25 fmt.Println("Error executing query:", err) 26 return 27 } 28 defer rows.Close() 29 30 // پردازش نتایج کوئری 31}
در این نمونه، queryها با استفاده از QueryContext
اجرا میشوند که از context استفاده کرده و به کمک آن عملیات میتواند در صورت تایماوت، لغو شود.
چند نکته مهم برای استفاده درست و بهینه از context در Golang:
context.TODO()
: زمانی که مطمئن نیستید چه contextی باید استفاده شود، این گزینه مناسب است ولی بهتر است بعدها context مناسب جایگزین آن شود.context.Background()
مستقیم استفاده نکنید: برای مدیریت درست lifecycle بهتر است از contextهای قابل لغو یا timeoutدار استفاده کنید.context.WithCancel()
بهره ببرید تا بتوانید لغو را به صورت برنامهای کنترل کنید. تایماوت بیشتر برای لغو خودکار کاربرد دارد.در معماری میکروسرویس، هر سرویس معمولا وابستگیهای خارجی مختلف دارد و با سایر سرویسها در ارتباط است. Context میتواند اطلاعات مهمی مانند توکنهای احراز هویت، متادیتاهای درخواست یا شناسههای ردیابی (tracing ID) را در سرتاسر تعاملات منتقل کند.
در وبسرورها که درخواستهای همزمان متعددی دریافت میکنند، context به مدیریت زمان پایان درخواستها، اعمال لغو و فرستادن مقادیر اختصاصی برای هر درخواست کمک میکند.
در تستها، context میتواند برای مدیریت تایماوت، کنترل پیکربندیهای خاص تست و پایان مرتب آزمونها استفاده شود.
context.Background
بدون deadline است.اگر یک goroutine همراه با context ایجاد شود اما به درستی هنگام لغو context خارج نشود، باعث نشت goroutine میگردد. در نتیجه منابع بیجهت مصرف میشوند.
1func main() { 2 ctx := context.Background() 3 4 go func(ctx context.Context) { 5 for { 6 select { 7 case <-ctx.Done(): 8 // رسیدگی به لغو 9 return 10 default: 11 // انجام کار 12 } 13 } 14 }(ctx) 15 16 time.Sleep(1 * time.Second) 17 18 cancel() // لغو context 19} 20 21func cancel() { 22 ctx, cancel := context.WithCancel(context.Background()) 23 cancel() 24}
در کد بالا، goroutine به درستی لغو نمیشود زیرا در واقع cancel()
در خارج کدی که context را ساخته نیست.
1func main() { 2 ctx, cancel := context.WithCancel(context.Background()) 3 4 go func(ctx context.Context) { 5 for { 6 select { 7 case <-ctx.Done(): 8 return 9 default: 10 // انجام کار 11 } 12 } 13 }(ctx) 14 15 time.Sleep(1 * time.Second) 16 17 cancel() 18}
اکنون goroutine به درستی با لغو context خارج میشود و نشت رخ نمیدهد.
اگرچه بسیار از کتابخانهها از context پشتیبانی دارند، اما در مواردی که نباشد، میتوانید:
1func APICall(ctx context.Context, args) { 2 select { 3 case <-ctx.Done(): // چک لغو 4 return 5 default: 6 } 7 8 go func() { 9 result := thirdPartyAPI(args) 10 11 select { 12 case <-ctx.Done(): 13 return 14 default: 15 } 16 17 handleResults(result) 18 }() 19}
func AfterFunc
این تابع امکان زمانبندی اجرای تابعی خاص را پس از پایان عمر context فراهم میکند و برای عملیات پاکسازی (cleanup) بسیار مفید است.
مثال:
1ctx, cancel := context.WithCancel(context.Background()) 2 3stop := context.AfterFunc(ctx, func() { 4 // پردازش باقیمانده صف 5}) 6 7go handleRequests(ctx) 8 9// هر زمان که بخواهیم: 10cancel() 11stop() // جلوگیری از اجرای cleanup
با این روش میتوان برنامههایی همزمان ساخت که عملیات پاکسازی را به صورت ایمن و تضمینی پس از اتمام عملیات اجرا کنند.
func WithDeadlineCause
این تابع امکان تعیین علت (error cause) سفارشی برای منقضی شدن deadline میدهد تا هنگام خطا، اطلاعات علت اصلی به جای پیام کلی “context deadline exceeded” قابل دسترسی باشد.
1ctx, cancel := context.WithDeadlineCause(ctx, time.Now().Add(100*time.Millisecond), 2 errors.New("RPC timeout")) 3defer cancel() 4 5time.Sleep(200 * time.Millisecond) 6 7fmt.Println(ctx.Err()) // prints "context deadline exceeded: RPC timeout"
func WithTimeoutCause
مانند بالا، اجازه افزودن علت سفارشی به خطای تایماوت را میدهد:
1ctx, cancel := context.WithTimeoutCause(ctx, 100*time.Millisecond, 2 errors.New("Backend RPC timed out"))
func WithoutCancel
در معماری contextها، لغو یک context والد باعث لغو تمام فرزندان میشود. اما گاهی لازم است کودکی ایجاد شود که مستقل از لغو والد باشد.
1func server(ctx context.Context) { 2 for { 3 req := waitForRequest(ctx) 4 handlerCtx := context.WithoutCancel(ctx) 5 go handleRequest(handlerCtx, req) // این دیگر لغو نمیشود 6 7 if ctx.Done() { 8 break 9 } 10 } 11}
با WithoutCancel
میتوان بخشهایی از context را جدا کرده تا در برابر لغو والد ایزوله باشند.
استفاده درست و کارآمد از context
در زبان Golang برای ساخت برنامههایی پایدار، مقیاسپذیر و پاسخگو بسیار حیاتی است. این ابزار قدرتمند به شما اجازه میدهد تا مدیریت همزمانی را ساده و مطمئن کنید، چرخهعمر درخواستها را کنترل کرده و عملیات لغو و تایماوت را به راحتی اعمال کنید.
با درک عمیق مفاهیم context و رعایت بهترین شیوهها، میتوانید چالشهای پیچیده همزمانی را به خوبی مدیریت کرده و نرمافزاری قابل اطمینان بسازید.
سوال ۱: آیا میتوان مقادیر سفارشی در context ذخیره کرد؟
پاسخ: بله، با استفاده از context.WithValue()
میتوانید جفتهای کلید-مقدار دلخواه را به context اضافه کنید.
سوال ۲: چگونه خطاهای لغو context را مدیریت کنم؟
پاسخ: تابع ctx.Err()
خطای مرتبط با لغو یا تایماوت را بازمیگرداند که میتوانید بر اساس آن رفتار مناسب را پیاده کنید.
سوال ۳: آیا میتوان سلسلهمراتب context ساخت؟
پاسخ: بله، با استفاده از context.WithValue()
و دیگر توابع، میتوان contextهای فرزند ساخت که از والد قبول میکنند، ولی باید مدیریت آنها به درستی انجام شود.
سوال ۴: آیا میتوان context را از طریق middleware در HTTP منتقل کرد؟
پاسخ: بله، middlewareها میتوانند context درخواستها را توسعه داده یا دستکاری کنند.
سوال ۵: Context چگونه به خاموش شدن مرتب (graceful shutdown) کمک میکند؟
پاسخ: لغو context باعث ارسال سیگنال به goroutineها میشود تا کارهای خود را به درستی خاتمه دهند و منابع آزاد شوند.
امیدوارم این راهنمای جامع به درک عمیق و کاربردی شما از context در Golang کمک کرده باشد.
موفق باشید و کد نویسی شاد!
۰
کد با می متعهد است که بالاترین سطح کیفی آموزش را در اختیار شما بگذارد. هدف به اشتراک گذاشتن دانش فناوری اطلاعات و توسعه نرم افزار در بالاترین سطح ممکن برای درستیابی به جامعه ای توانمند و قدرتمند است. ما باور داریم هر کسی میتواند با استمرار در یادگیری برنامه نویسی چالش های خود و جهان پیرامون خود را بر طرف کند و به موفقیت های چشم گیر برسد. با ما در این مسیر همراه باشید. کد با می اجتماع حرفه ای برنامه نویسان ایرانی.