پکیج context که یک پکیج built-in هست یکی از پرکاربرد ترین پکیج ها می باشد. که اگر شما حتی نخواسته باشید سمت این پکیج بروید به مرور زمان وقتی که جلوتر بروید با این پکیج رو به رو خواهید شد و مجبور خواهید بود این پکیج را یاد بگیرید.
این پکیج چندان بزرگ نیست شاید بتوانید توابع و چیزهایی که داخلش هست را زود یادبگیرید اما دقت کنید یک روزی این پکیج میشه بنیادی ترین قسمت از کدهایتان که باهاش کار میکنید.
نگران نباشید در این بخش از کتاب شما با سادگی با این پکیج رو به رو خواهید شد و خیلی راحت می توانید درک کنید که باید با این پکیج چکار کنید.
3.8.1 context چیست؟ #
در واقع context مانند یک درخت می باشد که کلی شاخه دارد و هر شاخه به شاخه های ریزتری تقسیم شده و در نهایت به برگ ها و میوه های درخت منتهی می شوند. حال شما می توانید در هر ناحیه شاخه والد را قطع کنید تا شاخه های فرزند از بین برود.
در زبان گو context
یک اینترفیس است که یکسری متد دارد که هریک از متدها می تواند عملیاتی را انجام دهد و این امکان را فراهم می کند هر وقت یک درخواست از سمت کلاینت به سرور می آید این درخواست می تواند در لایه های مختلف منتهی شود و داخل context می تواند یکسری key/value های مهم باشد که شما می توانید در هر لایه دسترسی داشته باشید و هچنین می توانید سیگنال cancel بفرستید که درخواستی که تا هرجا رفته اس کنسل شود.
حال بزارید یک مثال ساده به زبان گو بزنیم و تا کمی راحتر در کنید.
شما فرض کنید یک کلاینت به سرور 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 می دهد.
- این context ایجاد شده هیچ مقداری داخلش ندارد.
- هیچ وقت نمی تواند کنسل شود.
- و هیچ 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 ایجاد می شود همانند یک درخت است که قرار است این درخت به شاخه های ریزتری تقسیم شود و هر یک از شاخه ها عملیات مختلفی کنترل شود و به شاخه های دیگر منتقل شود.
3.8.3.1 ایجاد یک فرزند برای context #
شما خیلی ساده مانند کد زیر می توانید یک فرزند برای درخت خود ایجاد کنید :
در کد فوق ما یک 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 که درخت است تشکیل شده
حالا اگر ما برای childCtx1 بیایم یک فرزند دیگر با نام childCtx4 اضافه کنیم بصورت زیر خواهد شد :
1childCtx4 := context.WithValue(childCtx1, "current_time", "some_time)
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}
در کد فوق ما یک context ایجاد کردیم و داخلش با استفاده از WithValue مقدار key/value قرار دادیم و سپس این context را تابع manager پاس دادیم و داخل تابع manager ما با استفاده از متد Value که داخل اینترفیس ctx هست مقدار کلید language را گرفتیم.
نکته کاربردی و مهم همیشه سعی کنید context را به عنوان اولین پارامتر برای توابع تعریف کنید. و بهتر است برای نام پارامتر ctx یا c بزارید.
3.8.5 تابع context.WithCancel #
زمانیکه شما با استفاده از تابع WithCancel یک context فرزند ایجاد می کنید ۲ تا خروجی به شما می دهد اولی context و دومی تابع cancel می باشد. که شما می توانید تابع cancel را برای لغو درخواستی که از سمت کلاینت یا لایه های بالاتر اومده را انجام دهید.
حالا سعی با استفاده از مثال زیر بحث لغو کردن را درک کنید :
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}
در کد فوق ما یک context فرزند با استفاده از WithCancel ایجاد کردیم که به عنوان خروجی cancelCtx و cancelFunc را داد. سپس cancelCtx را به تابع task منتقل کردیم تا عملیاتی را انجام دهد. حال در ادامه کد تابع main ما یک Sleep در حد ۳ ثانیه گذاشتیم و گفتیم تابع cancelFunc اجرا شود. اگر دقت کنید پس ۳ ثانیه سیگنال لغو به تابع task ارسال شده و خطای Gracefully exit را چاپ کردیم و پس از آن خطای context چاپ کردیم.
نکته کاربردی و مهم همیشه سعی کنید تابع cancelFunc را پس از اینکه context فرزند را با WithCancel ایجاد کردید داخل defer قرار دهید.
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}
در کد فوق ما یک 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}
در کد فوق یک context فرزند با استفاده از تابع WithDeadline ایجاد کردیم و سپس با توجه به زمان فعلی مدت زمان ۵ ثانیه بعد را درنظر گرفتیم که مثلا اگر الان ساعت است 10:45:30 درخواست را در 10:45:35 لغو کند.
3.8.8 نکات کاربردی #
- هیچوقت سعی نکنید اینترفیس context را داخل یک ساختار ذخیره کنید اما می توانید embed کنید.
- همیشه context باید بین لایه های خود منتقل کنید تا بتوانید کنترل بهتری برروی درخواست ها داشته باشید.
- همیشه سعی کنید context را به عنوان اولین پارامتر توابع قرار دهید.
- نام context به عنوان پارامتر توابع بهتر است ctx یا c باشد.
- اگر هنوز مطمئن نیستید که با context چکاری میخواهید انجام دهید بهتر است context را با context.ToDo ایجاد کنید.
- توجه کنید فقط تابعی که context والد را ایجاد کرده می تواند درخواست را لغو کند پس سعی نکنید تابع cancelFunc را به توابع زیرین پاس دهید.