مفهوم Context در زبان Go

منبع

دوستان من توی ترجمه Context واژی خوبی واقعا پیدا نکردم که بشه ازش استفاده کرد پس از خود همین واژه استفاده میکنم شما هم به بزرگی خودتون ببخشید :)

از کانتکس ها در برنامه هایی که با گولنگ توسعه داده شده است برای مدیریت و کنترل هرچه بهتر مواردی همچون کنسل کردن، اشتراک گذاری دیتاها در برنامه نویسی اصطلاحا همروند به کار گرفته شده است.

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

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

داکیومنت این پکیج از اینجا قابل دسترس هستش

Context With Value

یکی از بیشتری استفاده کانتکس ها برای Share Data هستش و یا Use Request Scoped Value. وقتیکه تعداد خیلی زیادی فانکشن دارین و میخواین دیتا بین اونها پخش یا توزیع کنید اینجا میتونید از کانتکس ها استفاده کنید. بهترین و ساده ترین روش استفاده از فانکشن context.WithValue هستش. این فانکشن یک کانتکس جدید بر اساس کانتکس والد می سازه و یک مقداری به کلید داده شده اختصاص میده.

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

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

package main
import (
	&quotcontext&quot
	&quotfmt&quot
)
func main() {
	ctx := context.Background()
	ctx = addValue(ctx)
	readValue(ctx)
}
func addValue(ctx context.Context) context.Context {
	return context.WithValue(ctx, &quotkey&quot, &quottest-value&quot)
}
func readValue(ctx context.Context) {
	val := ctx.Value(&quotkey&quot)
	fmt.Println(val)
}

چیز مهمی که پشت دیزاین این پکیج کانتکس وجود داره اینه که همه چیز یک context.Context جدید از نوع struct برمیگردونند.

خوب این دیگه چیه ؟

شما مجبور هستین که یادتون باشه با مقدار برگشتی باید یه کاری به هر حال انجام بدید و حتی کانتکس های قدیمی با جدیدها جایگزین کنید. این یک امر مهم در دیزاین با ثبات هستش.

استفاده از این تکنیک که شما میاین با استفاده از context.Context مقداری ارسال میکنین به فانکشن های همروند. تا وقیکه مدیریت خوبی روی کانتکس ارسالی دارین خیلی روش خوبیه که scoped value ها را بین فانکشن های همروند تقسیم کنید.

چی شد !؟

یعنی هر کانتکس، مقدار خودش داخل اسکوپ خودش نگهداری میکنه. دقیقا کاری که پکیج net/http داره انجام میده برای هندل کردن HTTP Requests.

باید مثال بزنیم تا روشن بشه چون همچنان گنگ هستش.

میدلورها

بهترین مثالی که میشه اینجا آورد برای این کیس میدلورهایی هستند که درخواست های ورودی هندل میکنند.

تایپ http.Request شامل کانتکس هم هستش که میتونه با خودش scoped values داخل پایپلاین HTTP بیاره.

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

package main
import (
	&quotcontext&quot
	&quotlog&quot
	&quotnet/http&quot
	&quotgithub.com/google/uuid&quot
	&quotgithub.com/gorilla/mux&quot
)
func main() {
	router := mux.NewRouter()
	router.Use(guidMiddleware)
	router.HandleFunc(&quot/ishealthy&quot, handleIsHealthy).Methods(http.MethodGet)
	http.ListenAndServe(&quot:8080&quot, router)
}
func handleIsHealthy(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	uuid := r.Context().Value(&quotuuid&quot)
	log.Printf(&quot[%v] Returning 200 - Healthy&quot, uuid)
	w.Write([]byte(&quotHealthy&quot))
}
func guidMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		uuid := uuid.New()
		r = r.WithContext(context.WithValue(r.Context(), &quotuuid&quot, uuid))
		next.ServeHTTP(w, r)
	})
}

همانطور که ملاحظه میکنین میبینین که فانکشن guidMiddleware به درخواست attach شده و هر چیزی که ارسال بشه این کد بهش اعمال یا اضافه میشه.

Context Cancellation

فیچر خیلی خوبی که گولنگ در اختیار ما قرار ماده همین Context Cancellation هستش، خوب این دوستمون چی هست و چی کار میکنه برامون ؟

دقیقا برای موقعی خوبه که شما میخواین تعداد خیلی زیادی از گوروتین هارو بهشون بگین کنسل بشن. برای مثال فرض کنین یک فانکشن دارین که ده ها گوروتین استارت میکنه. اون فانکشن اصلی صبر میکنه تا تمام گوروتین ها کارشون تموم بشه و یا یک سیگنال کنسل ایجاد بشه قبل از اینکه بقیه پروسه ادامه پیدا کنه.اگر سیگنا کنسل دریافت بشه ما میخوایم اینو پخشش کنیم به همه ی گوروتین ها

خوب که چی بشه !؟

شما دیگه منابع کامپیوتر هدر ندادین اگر یک کانتکس مشترک بین همه گوروتین های خودتون تقسیم کرده باشین.

خیلی ساده برای ایجاد یک کانتکس با ویژگی کنسل شدن فقط کافیه (ctx)context.WithCancel صدا زده بشه و کانتکس به عنوان ورودی پاس داده بشه.

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

package main
import (
	&quotcontext&quot
	&quotfmt&quot
	&quotio/ioutil&quot
	&quotnet/http&quot
	neturl &quotnet/url&quot
	&quottime&quot
)

func queryWithHedgedRequestsWithContext(urls []string) string {
	ch := make(chan string, len(urls))
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	for _, url := range urls {
		go func(u string, c chan string) {
			c <- executeQueryWithContext(u, ctx)
		}(url, ch)

		select {
		case r := <-ch:
			cancel()
			return r
		case <-time.After(21 * time.Millisecond):
		}
	}
	return <-ch
}

func executeQueryWithContext(url string, ctx context.Context) string {
	start := time.Now()
	parsedURL, _ := neturl.Parse(url)
	req := &http.Request{URL: parsedURL}
	req = req.WithContext(ctx)

	response, err := http.DefaultClient.Do(req)

	if err != nil {
		fmt.Println(err.Error())
		return err.Error()
	}

	defer response.Body.Close()
	body, _ := ioutil.ReadAll(response.Body)
	fmt.Printf(&quotRequest time: %d ms from url%s\n&quot, 
        time.Since(start).Nanoseconds()/time.Millisecond.Nanoseconds(), url)
	return fmt.Sprintf(&quot%s from %s&quot, body, url)
}

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

Context Timeout

این پترن خیلی مرسوم هستش برای ارسال درخواست خای خارجی مثل کوعری های دیتابیس یا گرفتن دیتا از یک سرویس دیگر چه از طریق HTTP و یا gRPC. مدیریت این سناریوها با استفاده از این پکیج خیلی راحت انجام میشه. تنها کاری که لازم دارین انجان بدین صدا زدن فانکشن context.WithTimeout(ctx, time) در واقع کانتکس و زمان باید پاس داده بشه.

ctx, cancel := context.WithTimeout(context.Background(), 100 * time.Millisecond)

با این کار شما فانکشن کنسل دریافت میکنید برای موقعی که بخواین صداش کنین. دقیقا مثل Context Cancellation قبلی کار میکنه