3.8 پکیج context

3.8 پکیج context

context

پکیج context یک پکیج built-in است که یکی از پرکاربرد ترین پکیج های زبان گو می باشد. کار اصلی این پکیج فراهم کردن بستری است که بتوان به داده های مشترک دسترسی داشت و یا بتوان آنها را به اشتراک گذاشت و مدیریت کرد. حتی اگر شما نخواسته باشید سمت این پکیج بروید به مرور زمان که جلوتر می روید با این پکیج رو به رو خواهید شد و مجبور خواهید بود این پکیج را یاد بگیرید.

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

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

3.8.1 context چیست؟ #

در واقع context مانند یک درخت می باشد که کلی شاخه دارد و هر شاخه به شاخه های ریزتری تقسیم شده و در نهایت به برگ ها و میوه های درخت منتهی می شوند. حال شما می توانید در هر ناحیه شاخه والد را قطع کنید تا شاخه های فرزند از بین برود. در زبان گو context یک اینترفیس است که یکسری متد دارد که هریک از متدها می تواند عملیاتی را انجام دهد و این امکان را فراهم می کند هر وقت یک درخواست از سمت کلاینت به سرور می آید این درخواست می تواند در لایه های مختلف منتهی شود و داخل context می تواند یکسری key/value های مهم باشد که شما بتوانید در هر لایه، به آنها دسترسی داشته باشید و همچنین در صورت لزوم می توانید سیگنال cancel بفرستید که درخواستی که تا هرجا رفته است کنسل شود.

در ادامه با طرح یک مثال ساده به درک بهتر این موضوع کمک می کنیم :

context diagram

فرض کنید یک سرور http راه اندازی کردید (در فصل ۵ آشنا خواهید شد) که یکسری آدرس API دارد که کلاینت می تواند با استفاده از این آدرس ها با سرور شما ارتباط برقرار کند و عملیات مشخصی را انجام دهد. حال وقتی کلاینت درخواست می دهد. درخواست تا زمانیکه کامل شود و خروجی به کاربر نمایش داده شود می توانید این درخواست را بواسطه context در لایه های مختلف پروژه خود منتهی کنید و یکسری عملیات یا اطلاعات را در هر لایه از context بگیرید. اگر به دیاگرام فوق نگاه کنید اگر کلاینت درخواستش را لغو کند و درخواست کاربر به واسط context تا لایه Manager رفته باشد می تواند این درخواست در همان لایه متوقف شود و عملیات تکمیل نشود.

برای درک بهتر مثال فوق بهتره فایل صوتی زیر را گوش دهید تا بهتر بتوانید درک کنید :

دانلود فایل صوت

3.8.1.1 کاربردهای context #

  • لغو یک درخواستی که منتهی شده به لایه های مختلف پروژه بواسطه تابع cancel در پکیج context
  • انتقال داده های حساس به لایه های مختلف بواسطه تابع WithValue در پکیج context
  • گذاشتن timeout برروی context جهت لغو درخواستی که خیلی باعث منتظر ماندن می شود بواسطه تابع WithTimeout در پکیج context

3.8.1.2 معرفی اینترفیس context #

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

 1type Context interface {
 2    //It retures a channel when a context is cancelled, timesout (either when deadline is reached or timeout time has finished)
 3    Done() <-chan struct{}
 4
 5    //Err will tell why this context was cancelled. A context is cancelled in three scenarios.
 6    // 1. With explicit cancellation signal
 7    // 2. Timeout is reached
 8    // 3. Deadline is reached
 9    Err() error
10
11    //Used for handling deallines and timeouts
12    Deadline() (deadline time.Time, ok bool)
13
14    //Used for passing request scope values
15    Value(key interface{}) interface{}
16}
  • متد Done : بواسطه این متد که یک کانال فقط دریافت است شما می توانید سیگنال توقف درخواست را دریافت کنید و خطا برگردانید.
  • متد Err : داخل این متد اینترفیس خطا وجود دارد که خطاهای مربوط به context را می توانید دریافت و مدیریت کنید.
  • متد Deadline : با استفاده از این متد می توانید context هایی که از نوع Deadline هستند را مدیریت کنید.
  • متد Value : با استفاده از این می توانید مقادیری که بصورت key/value داخل context ذخیره شده را دریافت کنید که بصورت اینترفیس یک key میگیرد و به صورت اینترفیس مقدار داخل key را برمیگرداند.

3.8.2 ایجاد یک context #

شما با استفاده از ۲ تابع داخل پکیج context می توانید اولین context خام را ایجاد کنید و در واقع این context ایجاد شده می تواند والد تمامی context هایی که در لایه مختلف ایجاد کردید باشد.

برای ایجاد context گفتیم ۲ تابع وجود دارد که به شرح زیر می باشد :

context.Background() : #

داخل پکیج context ما یک تابع داریم به نام Background که اولین context خام و والد را میسازد و به شما یک اینترفیس از نوع Context می دهد.

  1. این context ایجاد شده هیچ مقداری داخلش ندارد.
  2. هیچ وقت نمی تواند کنسل شود.
  3. و هیچ deadline ندارد.

در هر صورت بدانید ریشه اصلی context شما با این تابع ایجاد می شود و نقطه شروع انتقال یک درخواست بین لایه هایتان با این context والد خواهد بود.

1func Background() Context

context.ToDo() : #

داخل پکیج context ما یک تابع داریم به نام ToDo که یک context خالی ایجاد می کند و هدف از این context ایجاد شده با ToDo این است هنوز برایمان مشخص نیست چکار میخوایم انجام بدیم با context می توانیم از این تابع استفاده کنیم. و معمولا برای تست ها و اعتبارسنجی و آنالیز کد خیلی کاربردی هست.

و دقت کنید در پایه اصلی پروژه اتون بهتره از Background همیشه استفاده کنید.

1func TODO() Context

3.8.3 درخت Context #

در واقع context خام یا ریشه که بواسطه تابع Background یا ToDo ایجاد می شود همانند یک درخت است که قرار است این درخت به شاخه های ریزتری تقسیم شود و هر یک از شاخه ها عملیات مختلفی کنترل شود و به شاخه های دیگر منتقل شود.

context diagram

3.8.3.1 ایجاد یک فرزند برای context #

شما خیلی ساده مانند کد زیر می توانید یک فرزند برای درخت خود ایجاد کنید :

1rootCtx := context.Background()
2childCtx := context.WithValue(rootCtx, "key", "value")

در کد فوق ما یک rootCtx ایجاد کردیم که همان درخت است و سپس اومدیم با استفاده از تابع WithValue یک شاخه ایجاد کردیم که داخل این شاخه یک key/value قرار دارد. که این key/value در لایه های دیگر که منتقل می شود قرار دارد.

3.8.3.2 ایجاد دو فرزند برای context #

1rootCtx := context.Background()
2childCtx := context.WithValue(rootCtx, "key", "value")
3childOfChildCtx, cancelFunc := context.WithCancel(childCtx)

در کد فوق :

  • rootCtx درخت است
  • childCtx فرزند اول است که با استفاده از WithValue ایجاد شده و یک مقدار key/value را نگه داری می کند.
  • childOfChildCtx برای فرزند اول context ما یک فرزند دیگری ایجاد کردیم با استفاده از تابع WithCancel که این تابع به شما یک context و یک تابع از نوع cancelFunc برمیگرداند.

3.8.3.3 درخت چند سطحی #

1rootCtx := context.Background()
2childCtx1 := context.WithValue(rootCtx, "key1", "value1")
3childCtx2, cancelFunc := context.WithCancel(childCtx1)
4childCtx3 := context.WithValue(rootCtx, "user_id", "some_user_id")

در کد فوق :

  • rootCtx درخت است
  • childCtx1 فرزند اول است که با استفاده از WithValue ایجاد شده و یک مقدار key/value را نگه داری می کند.
  • childCtx2 برای فرزند اول context ما یک فرزند دیگری ایجاد کردیم با استفاده از تابع WithCancel که این تابع به شما یک context و یک تابع از نوع cancelFunc برمیگرداند.
  • childCtx3 با استفاده از WithValue از rootCtx که درخت است تشکیل شده
context multi-level

حالا اگر ما برای childCtx1 بیایم یک فرزند دیگر با نام childCtx4 اضافه کنیم بصورت زیر خواهد شد :

1childCtx4 := context.WithValue(childCtx1, "current_time", "some_time)
context multi-level

3.8.4 تابع context.WithValue #

همانطور که گفتیم شما با استفاده از تابع WithValue می توانید مقادیری را بصورت key/value به context اضافه کنید و سپس این مقادیر را با استفاده از context به لایه های مختلف منتقل کنید.

1withValue(parent Context, key, val interface{}) (ctx Context)
دقت کنید شما می توانید بواسطه context.WithValue مقادیر خیلی مهم و حساس نظیر توکن ها و … را به لایه های مختلف خود منتقل کنید و این مورد خیلی قابل اهمیت است با استفاده از context انجام دهید.
1// Root Context
2ctxRoot := context.Background()
3
4// Below ctxChild has acess to only one pair {"a":"x"}
5ctxChild := context.WithValue(ctxRoot, "a", "x") 
6
7// Below ctxChildofChild has access to both pairs {"a":"x", "b":"y"} as it is derived from ctxChild
8ctxChildofChild := context.WithValue(ctxChild, "b", "y") 

در بالا ما یک ctxRoot ایجاد کردیم و سپس یک فرزند با استفاده از تابع WithValue ایجاد کردیم که یک مقدار از نوع key/value با نام a را داخل context فرزند قرار دادیم. حالا برای context فرزند مجدد با استفاده از WithValue یک فرزند دیگری ایجاد کردیم که یک مقدار دیگر از نوع key/value با نام b قرار دادیم حالا اگر دقت کنید ctxChildofChild دارای ۲ مقدار a و b هستش.

بزارید یک مثال ساده بزنیم :

 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6)
 7
 8func main() {
 9	ctx := context.WithValue(context.Background(), "language", "Go")
10
11	fmt.Println(manager(ctx, "language"))
12}
13
14func manager(ctx context.Context, key string) string {
15	if v := ctx.Value(key); v != nil {
16		return v.(string)
17	}
18	return "not found value"
19}
1$ go run main.go
2Go

در کد فوق ما یک context ایجاد کردیم و داخلش با استفاده از WithValue مقدار key/value قرار دادیم و سپس این context را تابع manager پاس دادیم و داخل تابع manager ما با استفاده از متد Value که داخل اینترفیس ctx هست مقدار کلید language را گرفتیم.

نکته کاربردی و مهم همیشه سعی کنید context را به عنوان اولین پارامتر برای توابع تعریف کنید. و بهتر است برای نام پارامتر ctx یا c بزارید.

3.8.5 تابع context.WithCancel #

زمانیکه شما با استفاده از تابع WithCancel یک context فرزند ایجاد می کنید ۲ تا خروجی به شما می دهد اولی context و دومی تابع cancel می باشد. که شما می توانید تابع cancel را برای لغو درخواستی که از سمت کلاینت یا لایه های بالاتر اومده را انجام دهید.

1type CancelFunc func()
2
3func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

حالا با استفاده از مثال زیر میتوانید بحث لغو کردن را بهتر درک کنید :

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "time"
 7)
 8
 9func main() {
10    ctx := context.Background()
11    cancelCtx, cancelFunc := context.WithCancel(ctx)
12    go task(cancelCtx)
13    time.Sleep(time.Second * 3)
14    cancelFunc()
15    time.Sleep(time.Second * 1)
16}
17
18func task(ctx context.Context) {
19    i := 1
20    for {
21        select {
22        case <-ctx.Done():
23            fmt.Println("Gracefully exit")
24            fmt.Println(ctx.Err())
25            return
26        default:
27            fmt.Println(i)
28            time.Sleep(time.Second * 1)
29            i++
30        }
31    }
32}
1$ go run main.go
21
32
43
5Gracefully exit
6context canceled

در کد فوق ما یک context فرزند با استفاده از WithCancel ایجاد کردیم که به عنوان خروجی cancelCtx و cancelFunc را داد. سپس cancelCtx را به تابع task منتقل کردیم تا عملیاتی را انجام دهد. حال در ادامه کد تابع main ما یک Sleep در حد ۳ ثانیه گذاشتیم و گفتیم تابع cancelFunc اجرا شود. اگر دقت کنید پس ۳ ثانیه سیگنال لغو به تابع task ارسال شده و خطای Gracefully exit را چاپ کردیم و پس از آن خطای context چاپ کردیم.

نکته کاربردی و مهم همیشه سعی کنید تابع cancelFunc را پس از اینکه context فرزند را با WithCancel ایجاد کردید داخل defer قرار دهید.

1ctx := context.Background()
2cancelCtx, cancelFunc := context.WithCancel(ctx)
3defer cancelFunc()

3.8.6 تابع context.WithTimeout #

تابع WithTimeout یکی از کاربردی ترین context ها را برای ما ایجاد میکند و باعث می شود جلوی طول کشیدن یک درخواست خارجی یا عملیاتی را بگیرد و درخواست را لغو کند. این تابع همانند تابع WithCancel به شما تابع cancelFunc را می دهد و در عوض از شما یک مدت زمان را میگیرد.

1func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

بزارید یک مثال ساده بزنیم :

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "time"
 7)
 8
 9func main() {
10    ctx := context.Background()
11    cancelCtx, cancel := context.WithTimeout(ctx, time.Second*3)
12    defer cancel()
13    go task1(cancelCtx)
14    time.Sleep(time.Second * 4)
15}
16
17func task1(ctx context.Context) {
18    i := 1
19    for {
20        select {
21        case <-ctx.Done():
22            fmt.Println("Gracefully exit")
23            fmt.Println(ctx.Err())
24            return
25        default:
26            fmt.Println(i)
27            time.Sleep(time.Second * 1)
28            i++
29        }
30    }
31}
1$ go run main.go
21
32
43
5Gracefully exit
6context deadline exceeded

در کد فوق ما یک context فرزند با استفاده از تابع WithTimeout ایجاد کردیم و مدت زمان ۳ ثانیه به این تابع پاس دادیم و پس از آن context فرزند به همراه تابع cancelFunc دریافت کردیم. حالا تابع cancel را داخل defer قرار دادیم و cancelCtx را به تابع task1 که داخل گوروتین است پاس داده ایم سپس و یک Sleep به مدت ۴ ثانیه گذاشتیم تا، تابع main کارش تمام نشود. حال پس از اینکه ۳ ثانیه گذشت داخل select سیگنال cancel را دریافت کردیم و خطای context deadline exceeded که نشان دهنده اتمام شدن مدت زمان هست را چاپ کرده ایم. همانطور که متوجه شدید درخواست کلی ما لغو شده.

3.8.7 تابع context.WithDeadline #

تابع WithDeadline تا حدی شبیه به WithTimeout است اما با این تفاوت که پارامتر زمانی که میگیرد از نوع time.Time است و مدت زمانی که میگیرد براساس تایم هست مثلا شما میگید ۵ ثانیه بعد از زمان الان درخواست را لغو کند در صورتیکه withTimeout مدت زمان میگیرد که درخواست ۵ ثانیه مهلت دارد کارش را انجام دهد.

1func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

به مثال زیر توجه کنید :

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "time"
 7)
 8
 9func main() {
10    ctx := context.Background()
11    cancelCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*5))
12    defer cancel()
13    go task(cancelCtx)
14    time.Sleep(time.Second * 6)
15}
16
17func task(ctx context.Context) {
18    i := 1
19    for {
20        select {
21        case <-ctx.Done():
22            fmt.Println("Gracefully exit")
23            fmt.Println(ctx.Err())
24            return
25        default:
26            fmt.Println(i)
27            time.Sleep(time.Second * 1)
28            i++
29        }
30    }
31}
1$ go run main.go
21
32
43
54
65
7Gracefully exit
8context deadline exceeded

در کد فوق یک context فرزند با استفاده از تابع WithDeadline ایجاد کردیم و سپس با توجه به زمان فعلی مدت زمان ۵ ثانیه بعد را درنظر گرفتیم که مثلا اگر الان ساعت است 10:45:30 درخواست را در 10:45:35 لغو کند.

3.8.8 نکات کاربردی #

  1. هیچوقت سعی نکنید اینترفیس context را داخل یک ساختار ذخیره کنید اما می توانید embed کنید.
  2. همیشه context باید بین لایه‌های خود منتقل کنید تا بتوانید کنترل بهتری برروی درخواست ها داشته باشید.
  3. همیشه سعی کنید context را به عنوان اولین پارامتر توابع قرار دهید.
  4. نام context به عنوان پارامتر توابع بهتر است ctx یا c باشد.
  5. اگر هنوز مطمئن نیستید که با context چکاری میخواهید انجام دهید بهتر است context را با context.ToDo ایجاد کنید.
  6. توجه کنید فقط تابعی که context والد را ایجاد کرده می تواند درخواست را لغو کند پس سعی نکنید تابع cancelFunc را به توابع زیرین پاس دهید.