راهنمای جامع Context در Golang: مدیریت بهینه Concurrency


۰


معرفی

همزمانی (Concurrency) یکی از ابعاد اساسی برنامه‌نویسی به زبان Go است و مدیریت مؤثر عملیات همزمان، کلید ساختن برنامه‌های مقاوم و بهینه است. یکی از ویژگی‌های مهمی که به ما در این زمینه کمک می‌کند، پکیج Context در زبان Golang است. این پکیج ابزاری برای کنترل چرخه‌ی عمر، لغو (cancellation) و انتشار درخواست‌ها بین چند goroutine فراهم می‌کند. در این راهنمای جامع، به عمق مفهوم context در Golang می‌پردازیم، کاربردها و بهترین شیوه‌ها را با مثال‌های واقعی از صنعت نرم‌افزار بررسی می‌کنیم.

ویژگی‌های جدیدی که در نسخه ۱.۲۱ افزوده شده، در این مقاله گنجانده شده‌اند. این ویژگی‌ها عبارتند از:

  • func AfterFunc
  • func WithDeadlineCause
  • func WithTimeoutCause
  • func WithoutCancel

فهرست مطالب

  1. Context چیست؟
  2. ایجاد Context
  3. انتشار Context
  4. گرفتن مقادیر از Context
  5. لغو Context
  6. تایم‌اوت و Deadline
  7. استفاده از Context در درخواست‌های HTTP
  8. استفاده از Context در عملیات بانک اطلاعاتی
  9. بهترین شیوه‌ها در استفاده از Context
  10. مشکلات رایج در استفاده از Context
  11. Context و نشت Goroutine
  12. استفاده از Context با کتابخانه‌های شخص ثالث
  13. ویژگی‌های جدید Context (نسخه go1.21.0)
  14. نتیجه‌گیری
  15. سوالات متداول (FAQs)

1. Context چیست؟

پکیج context بخشی از کتابخانه استاندارد Go است که ابزار قدرتمندی برای مدیریت عملیات همزمان ارائه می‌دهد. این پکیج اجازه می‌دهد سیگنال‌های لغو، مهلت‌های زمانی (deadlines) و مقادیر مورد نیاز بین goroutine‌ها منتقل شده و عملیات وابسته به هم بتوانند در زمان مناسب به‌صورت مرتب خاتمه یابند.

با استفاده از context، می‌توانید سلسله‌مراتبی از goroutine‌ها بسازید و اطلاعات مهم را در طول این زنجیره منتقل کنید.


مثال: مدیریت درخواست‌های API همزمان

فرض کنید باید داده‌ها را همزمان از چند 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‌ها ارسال می‌شود و درخواست‌های باقی‌مانده لغو می‌شوند.


2. ایجاد Context

برای ساختن یک context، معمولاً از context.Background() شروع می‌کنیم که یک context خالی و غیرقابل لغو را برمی‌گرداند و به عنوان ریشه در سلسله‌مراتب context‌ها عمل می‌کند. سپس می‌توانیم با استفاده از توابع context.WithTimeout() یا context.WithDeadline() contextهای جدید با محدودیت زمانی بسازیم.


مثال: ایجاد Context با Timeout

در این مثال، یک 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، عملیات قبل از اتمام کامل لغو می‌شود.


3. انتشار Context

زمانی که یک context ساخته شد، می‌توانید به تابع‌ها یا goroutine‌های پایین‌دستی آن را به عنوان آرگومان پاس بدهید تا عملیات مرتبط بتوانند در جریان لغو یا تغییراتی که در context رخ می‌دهد، قرار داشته باشند.


مثال: انتشار Context به goroutine‌ها

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 فرستاده شده و در آنجا مقدار بازیابی می‌شود.


4. گرفتن مقادیر از Context

علاوه بر انتشار context، می‌توانید ارزش‌ها یا داده‌های ذخیره شده درون context را گرفته و در تابع یا goroutine استفاده کنید.


مثال: گرفتن اطلاعات کاربر از Context

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 به یک عدد صحیح تبدیل شده و استفاده می‌شود.


5. لغو Context

لغو (Cancellation) یک ویژگی اساسی در مدیریت context است که اجازه می‌دهد عملیات به شکلی مرتب و کنترلی پایان یابد و سیگنال‌های لغو به goroutineهای مرتبط فرستاده شود.


مثال: لغو Context

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 است پیام لغو را دریافت و به صورت مرتب خارج می‌شود.


6. تایم‌اوت و Deadlines

استفاده از محدودیت‌های زمانی (Timeouts) و Deadlines برای اطمینان از اتمام عملیات در زمانی معقول اهمیت زیادی دارد و از ایجاد گلوگاه یا انتظار نامحدود جلوگیری می‌کند.


مثال: تعیین Deadline برای Context

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 تمام شده و عملیات لغو می‌شود.


7. Context در درخواست‌های HTTP

استفاده از context در درخواست‌های HTTP در Go اهمیت بالایی دارد و به کمک آن می‌توان لغو درخواست‌ها، تایم‌اوت، و انتقال مقادیر مهم به handlerهای پایین‌دستی را مدیریت کرد.


مثال: ایجاد درخواست HTTP با 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(), 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 ساخته شده و در صورتی که پاسخ بیش از ۲ ثانیه زمان ببرد، لغو خواهد شد.


8. Context در عملیات بانک اطلاعاتی

Context به خوبی می‌تواند در عملیات بانک‌های اطلاعاتی به کار رود تا لغو کوئری‌ها، تایم‌اوت و انتقال داده‌های مرتبط در تراکنش‌ها کنترل شود.


مثال: استفاده از Context در عملیات PostgreSQL

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 استفاده کرده و به کمک آن عملیات می‌تواند در صورت تایم‌اوت، لغو شود.


9. بهترین شیوه‌ها در استفاده از Context

چند نکته مهم برای استفاده درست و بهینه از context در Golang:

  1. پاس دادن context به صورت صریح: همیشه context را به‌عنوان آرگومان به توابع یا goroutine‌ها ارسال کنید، نه به شکل متغیرهای سراسری. این کمک می‌کند مدیریت چرخه عمر context بهتر باشد و از شرایط رقابتی جلوگیری شود.
  2. استفاده از context.TODO(): زمانی که مطمئن نیستید چه contextی باید استفاده شود، این گزینه مناسب است ولی بهتر است بعدها context مناسب جایگزین آن شود.
  3. از context.Background() مستقیم استفاده نکنید: برای مدیریت درست lifecycle بهتر است از contextهای قابل لغو یا timeoutدار استفاده کنید.
  4. ترجیح لغو صریح بر تایم‌اوت: وقتی ممکن است، از context.WithCancel() بهره ببرید تا بتوانید لغو را به صورت برنامه‌ای کنترل کنید. تایم‌اوت بیشتر برای لغو خودکار کاربرد دارد.
  5. حجم context را کوچک نگه دارید: از درج داده‌های بزرگ یا غیرضروری خودداری کنید و فقط داده‌های مورد نیاز را در context بگذارید.
  6. از زنجیروار کردن context‌ها بپرهیزید: انتشار context بین ایجادکننده‌ها باید به صورت یک context صورت گیرد تا از پیچیدگی‌ها و ابهام‌ها جلوگیری شود.
  7. حواستان به نشت goroutine باشد: همیشه اطمینان یابید که goroutineهایی که روی context متکی هستند، بعد از لغو context به درستی بسته می‌شوند.

Context در سناریوهای واقعی

Context در میکروسرویس‌ها

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


Context در وب‌سرورها

در وب‌سرورها که درخواست‌های همزمان متعددی دریافت می‌کنند، context به مدیریت زمان پایان درخواست‌ها، اعمال لغو و فرستادن مقادیر اختصاصی برای هر درخواست کمک می‌کند.


Context در تست‌ها

در تست‌ها، context می‌تواند برای مدیریت تایم‌اوت‌، کنترل پیکربندی‌های خاص تست و پایان مرتب آزمون‌ها استفاده شود.


10. اشتباهات رایج در استفاده از Context

  1. عدم انتشار context: توابع فرزند باید context را دریافت کنند تا لغو را مدنظر قرار دهند. context را در یک تابع نگه ندارید.
  2. فراموش کردن فراخوانی cancel: پس از اتمام کار با context قابل لغو، باید تابع cancel حتما فراخوانی شود.
  3. نشت goroutine: goroutineهای متکی باید کانال Done را چک کرده و به موقع خارج شوند.
  4. استفاده صرف از context.Background: این نوع context فاقد لغو و تایم‌اوت است.
  5. ارسال context نا معتبر (nil): همیشه context معتبر ارسال کنید، nil باعث panic می‌شود.
  6. بررسی زود هنگام: بررسی کانال Done نباید خیلی زود و قبل از شروع کار انجام شود چون باعث لغو ناخواسته می‌شود.
  7. استفاده از عملیات بلوکه‌کننده بدون کنترل: عملیات I/O باید با چک کردن context پوشش داده شوند تا از معلق ماندن جلوگیری شود.
  8. استفاده بیش از حد از context: Context مناسب برای عملیات محدود به درخواست است نه منابع عمومی.
  9. فرض وجود تایم‌اوت برای context: context.Background بدون deadline است.
  10. فراموش کردن انقضای context: فرض نکنید goroutine‌ها همیشه اجرا می‌شوند، ممکن است context منقضی شود.

11. Context و نشت Goroutine

اگر یک goroutine همراه با context ایجاد شود اما به درستی هنگام لغو context خارج نشود، باعث نشت goroutine می‌گردد. در نتیجه منابع بی‌جهت مصرف می‌شوند.


مثال نشت 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 خارج می‌شود و نشت رخ نمی‌دهد.


12. استفاده از Context با کتابخانه‌های شخص ثالث

اگرچه بسیار از کتابخانه‌ها از context پشتیبانی دارند، اما در مواردی که نباشد، می‌توانید:

  • فراخوانی کتابخانه را در یک تابع با پارامتر context بسته‌بندی کنید
  • قبل و بعد از فراخوانی API کنترل لغو context را پیاده کنید
  • عملیات‌های بلندمدت را در goroutine جدا اجرا کنید
  • مقادیر پیش‌فرض مناسبی برای timeout و deadline در نظر بگیرید

قالب نمونه

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}

13. ویژگی‌های جدید Context در golang 1.21.0

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 و رعایت بهترین شیوه‌ها، می‌توانید چالش‌های پیچیده همزمانی را به خوبی مدیریت کرده و نرم‌افزاری قابل اطمینان بسازید.


سوالات متداول (FAQs)

سوال ۱: آیا می‌توان مقادیر سفارشی در context ذخیره کرد؟
پاسخ: بله، با استفاده از context.WithValue() می‌توانید جفت‌های کلید-مقدار دلخواه را به context اضافه کنید.


سوال ۲: چگونه خطاهای لغو context را مدیریت کنم؟
پاسخ: تابع ctx.Err() خطای مرتبط با لغو یا تایم‌اوت را بازمی‌گرداند که می‌توانید بر اساس آن رفتار مناسب را پیاده کنید.


سوال ۳: آیا می‌توان سلسله‌مراتب context ساخت؟
پاسخ: بله، با استفاده از context.WithValue() و دیگر توابع، می‌توان contextهای فرزند ساخت که از والد قبول می‌کنند، ولی باید مدیریت آن‌ها به درستی انجام شود.


سوال ۴: آیا می‌توان context را از طریق middleware در HTTP منتقل کرد؟
پاسخ: بله، middlewareها می‌توانند context درخواست‌ها را توسعه داده یا دست‌کاری کنند.


سوال ۵: Context چگونه به خاموش شدن مرتب (graceful shutdown) کمک می‌کند؟
پاسخ: لغو context باعث ارسال سیگنال به goroutineها می‌شود تا کارهای خود را به درستی خاتمه دهند و منابع آزاد شوند.


امیدوارم این راهنمای جامع به درک عمیق و کاربردی شما از context در Golang کمک کرده باشد.

موفق باشید و کد نویسی شاد!

گو

۰


نظرات


author
نویسنده مقاله: امیر محمد محمدی

کد با می متعهد است که بالاترین سطح کیفی آموزش را در اختیار شما بگذارد. هدف به اشتراک گذاشتن دانش فناوری اطلاعات و توسعه نرم افزار در بالاترین سطح ممکن برای درستیابی به جامعه ای توانمند و قدرتمند است. ما باور داریم هر کسی میتواند با استمرار در یادگیری برنامه نویسی چالش های خود و جهان پیرامون خود را بر طرف کند و به موفقیت های چشم گیر برسد. با ما در این مسیر همراه باشید. کد با می اجتماع حرفه ای برنامه نویسان ایرانی.

تمام حقوق این سایت متعلق به وبسایتcodebymeمیباشد.