Golang - Context Nedir?
Merhaba, uzun süredir haşır neşir olduğum go context ile ilgili hem Türkçe kaynağın az olması, hem de kendi bilgilerimi pekiştirme amacıyla bu içeriği hazırlamak istedim. Umarım faydalı olur, keyifli okumalar.
Eğer Go ile bir web framework, routine, veritabanı bağlantısı, HTTP isteği vs gibi bir işlemle haşır neşir olduysanız, fonksiyonların içinde context.Context
diye bir parametre mutlaka görmüşssünüzdür. Bazı framework'ler veya kütüphaneler bu context'in çevresini sararak kendi context'lerini oluşturabilir pek tabii. Fakat işlev 3 aşağı 5 yukarı aynıdır.
Context 101
Örnek: İptal Sinyali
Şöyle bir senaryomuz düşünelim; Veritabanına sorgu yapan bir fonksiyonumuz var. Bu fonksiyon çağırıldığında sorgu atılırken bir problem oldu ve çok fazla zaman aldı. Eğer bir sorgu 2 saniyeden fazla sürerse iptal olması gerekiyor. Bir diğer deyişle 2 saniye sonra bizim bu fonksiyona bir iptal et sinyali yollamamız gerekiyor. İşte bu durumda yardımımıza context yapısı koşuyor
Örnek: İstek Tabanlı Veriler
Bir web servisimiz ve 1 endpointimiz var. Fakat bu endpointimize gelen istekte bu isteğin hangi kullanıcı tarafından yapıldığını bilmek istiyoruz. Bu yüzden endpointimizin önüne bir middleware yazmaya karar verdik. Bu middleware istekte gelen token'ı parse edip kullanıcı bilgilerini endpoint'e iletmekle görevli.
Bu senaryoda da yardımımıza context
yapısı koşuyor. Servisimize istek ilk geldiğinde bir context oluşuyor ve bu context bir sonra çalışcak olan fonksiyona yani middleware'e geçiliyor. Middleware içinde kullanıcı bilgileri context'e ekleniyor. Sonrasında da yine aynı context endpoint'e iletiliyor. Bu sayede endpoint içinde kullanıcı bilgilerini context üzerinden çekebiliyoruz.
Uzun lafın kısası Google context paketi için şöyle bir açıklama yapıyor:
Context paketi; istek tabanlı verileri, iptal sinyallerini, deadline bilgilerini bir API kapsamı içindeki tüm fonksiyonlara, goroutine'lere geçirebilmek için tasarlandı.
Context'e Yakından Bakış
type Context interface {
// Done metodu, context tamamlandığında veya zaman aşımına uğradığında
// kapanan bir channel döndürür.
Done() <-chan struct{}
// Err metodu, context'in neden kapandığını anlatan bir error döndürür.
// Genellikle Done kanalı kapandıktan sonra bunu kullanırız.
Err() error
// Deadline metodu, eğer context herhangi bir deadline'a sahipse deadline zamanını döndürür.
// `ok` değeri context bir deadline'a sahipse true, değilse false olur.
Deadline() (deadline time.Time, ok bool)
// Value metodu, context'in içerisinde saklanan bilgilere ulaşmak için kullanılır.
Value(key interface{}) interface{}
}
Context'in içinde bulunan metodlar eşzamanlı olarak ayrı ayrı goroutine'lerde kullanılmaya uygundur. Bir context'i alıp birden fazla fonksiyon, goroutine vb yapıların içine yollayıp, bunların hepsine aynı anda iptal sinyali yollayabilirisiniz.
Yukarıda context'i tanımladık ve context'in eşzamanlı işlemlerde kullanılmasının uygun olduğunu söyledik. Fakat burada dikkat etmemiz gereken bir nokta var. Context içerisinde veri saklayabiliyoruz ve bu verilerin kontrolü bize ait. İçeri koyduğumuz veriler de eşzamanlı kullanıma uygun olmalı.
Hazır Context Tipleri
Background
Background context'i Go içinde tanımlı bulunan context.Background()
içinde bir değer bulundurmayan, deadline'ı olmayan ve iptal edilemeyen bir context'tir. Aslında boş bir context'tir. Yeni üreteceğimiz contextler'de taban olarak kullanılır.
Go kodu içinde ise aşağıdaki gibi tanımlanmıştır.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
background = new(emptyCtx)
func Background() Context {
return background
}
TODO
TODO context'i yapı itibariyle background
ile aynıdır. Aralarında anlamsal bi fark vardır. context.TODO()
ile okuyucuya:
Buraya anlamlı bir context gelecek, şu an bu context'in ne olduğunu konusunda emin değilim ya da bu context'i sen belirlemelisin
mesajını vermiş oluyoruz.
Context Oluşturma
Yeni bir context sadece mevcut bir context üzerinden oluşturalabilir. Bunu bir ağaç yapısı gibi hayal edebiliriz.
Yukarıdaki ağaç yapısında C1, C2, C3, C4, C5 bizim contextlerimiz. C3 C2'den, C2 C1'den oluşturulmuş. Aynı şekilde C5 C4'den, C4 ise C1 den oluşturulmuş. Burada önemli olan bilgi ise şu:
Bir context iptal edildiğinde (cancel), o context kullanılarak oluşturulmuş tüm contextler iptal olur.
Yukarıdaki örnekten yola çıkacak olursak, C2'yi iptal ettiğimiz zaman otomatik olarak C3 iptal olur.
Context ile ilgili bütün işlemlerimizi context
paketini kullanarak yapıyoruz. Bu paket içinde yeni contextler oluşturmak için bazı fonksiyonlar var. Şimdi bunları inceleyelim.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel içine verdiğimiz context'den yeni bir context üretir. Aynı zamanda bu context'i iptal edebilmemiz için bir fonksiyon döndürür. Bu fonksiyon çağırıldığı zaman yeni oluşturulan context iptal edilir.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout içine verdiğimiz context'den yeni bir context üretir. Aynı zamanda yeni oluşturulan context'i iptal edebilmemiz için bir fonksiyon döndürür. Bu kısma kadar yaptığı işlem WithCancel
ile aynı. Aradaki tek fark timeout
parametresi. timeout
parametresiyle birlikte yeni oluşturulacak context'in ömrünü belirlemiş oluyoruz. Örneğin timeout değeri olarak 30 saniye verirsek, yeni oluşturulan context 30 saniye sonra iptal olacaktır.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDeadline 'i WithTimeout
gibi düşünebiliriz. WithTimeout ie yeni oluşturalacak context'in ne kadar süre yaşayacağını belirtirken, WithDeadline ile yeni oluşturalacak context'in ne zamana kadar yaşayacağını belirtiriz.
func WithValue(parent Context, key interface{}, val interface{}) Context
WithValue içine verdiğimiz context'den yeni bir context üretir. Aynı zamanda yeni üretilen context'in içine key
ve val
yerine verdiğimiz değerleri koyar. Bu sayede context içinde bilgi saklayabiliriz.
Cancel
Fonksiyonu
Yukarıdaki örneklerin hemen hemen hepsinde yeni bir context oluşturduktan sonra elimizde Cancel
fonksiyonu oluyor. Bu Cancel
fonksiyonu ile oluşturulan yeni context'i iptal edebiliyoruz. Buradaki standart ise context'le işimiz bittiğinde o context'i iptal etmektir.
Context iptal edildiği zaman o context'e atanan bütün kaynaklar serbest kalır. Bu nedenle işimiz bittiğinde context'i iptal etmeyi unutmamalıyız.
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // işimiz bittiğinde context'i iptal ediyoruz. İptal ettiğimiz zaman `ctx` kullanılarak üretilen bütün context'ler iptal olur.
Kod Örnekleri
Context - İptal
func a(ctx context.Context) {
fmt.Println("a çalışmaya başladı")
select {
case <-time.After(1 * time.Second): // işlemler 1 saniye sürüyor
fmt.Println("a çalışmasını bitirdi")
case <-ctx.Done():
fmt.Println("context iptal edildi")
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go a(ctx) // a()'yı ayrı bi goroutine içinde başlattık.
time.Sleep(500 * time.Millisecond) // context'i iptal etmeden önce 500ms uyuyoruz.
cancel() // context'i iptal ediyoruz.
time.Sleep(1 * time.Second) // a()'nın çıktısını görebilmek için biraz bekliyoruz.
fmt.Println("program sonlandı")
}
>> a çalışmaya başladı
>> context iptal edildi
>> program sonlandı
Context - Zaman Aşımı
func a(ctx context.Context) {
fmt.Println("a çalışmaya başladı")
select {
case <-time.After(2 * time.Second): // işlemler 2 saniye sürüyor.
fmt.Println("a çalışmasını bitirdi")
case <-ctx.Done():
fmt.Println("context iptal edildi")
}
}
func main() {
// 1 saniye sonra otomatik olarak iptal olacak bir context oluşturduk.
ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second)
defer cancel()
go a(ctx) // a()'yı ayrı bi goroutine içinde başlattık.
time.Sleep(5 * time.Second) // Sonuçları görebilmek için biraz uyuyoruz.
fmt.Println("program sonlandı")
}
>> a çalışmaya başladı
>> context iptal edildi
>> program sonlandı
Context - Bilgi Gömmek
func a(ctx context.Context) {
fmt.Println("a çalışmaya başladı")
select {
case <-ctx.Done():
fmt.Println("context iptal edildi")
default:
if yeniDeger := ctx.Value("YeniDeger"); yeniDeger != nil {
fmt.Println("context içinden okunan değer:", yeniDeger)
}
}
}
func main() {
// Context'in içine bilgi gömdük, yeni bir context oluşturduk.
ctx := context.WithValue(context.Background(), "YeniDeger", "Merhaba Dünya")
go a(ctx) // a()'yı ayrı bi goroutine içinde başlattık.
time.Sleep(1 * time.Second) // Sonuçları görebilmek için biraz uyuyoruz.
fmt.Println("program sonlandı")
}
>> a çalışmaya başladı
>> context içinden okunan değer: Merhaba Dünya
>> program sonlandı
Eğer aradığımız key context içinde yoksa, ctx.Value(key)'in cevabı nil olur. Bu yüzden dönen değerin nil olup olmadığını kontrol etmeliyiz.
Context - Toplu İptal
func a(ctx context.Context, idx int) {
fmt.Println(idx, "çalışmaya başladı")
select {
case <-time.After(2 * time.Second):
fmt.Println(idx, "çalışma sonlandı")
case <-ctx.Done():
fmt.Println(idx, "context iptal edildi")
}
}
func main() {
// Context'in içine bilgi gömdük, yeni bir context oluşturduk.
baseCtx, cancel := context.WithCancel(context.Background())
for i := 1; i <= 5; i++ {
newCtx := context.WithValue(baseCtx, "YeniDeger", i) // baseCtx'den yeni bir context üretilir.
go a(newCtx, i) // a() yeni context ile çağırılır.
}
cancel() // baseCtx iptal edilir. Böylece baseCtx'den türetilen bütün context'ler iptal edilmiş olur.
time.Sleep(5 * time.Second) // Sonuçları görebilmek için biraz uyuyoruz.
fmt.Println("program sonlandı")
}
>> 1 çalışmaya başladı
>> 1 context iptal edildi
>> 2 çalışmaya başladı
>> 2 context iptal edildi
>> 3 çalışmaya başladı
>> 3 context iptal edildi
>> 4 çalışmaya başladı
>> 4 context iptal edildi
>> 5 çalışmaya başladı
>> 5 context iptal edildi
>> program sonlandı
Context - Http İsteği
Context'in yaygın olarak kullanıldığı senaryolardan biri. Bir kaynağa istek atarken Deadline'ı olan context oluşturup, harcanan zamanın maksimum ne kadar olabileceğini sınırlıyoruz. İstek atılan endpoint 2 saniye sonra cevap verecek şekilde ayarlandı, bizde context'imizin timeout değerine 1 saniye verdik. 1 saniye geçtikten sonra context iptal oldu ve hata olarak context deadline exceeded
mesajını gördük. Eğer context'in timeout süresini 3 saniye verseydik göreceğimiz çıktı cevap alındı
mesajı olacaktı. Denemesi bedava :)
func sendReq(ctx context.Context) error {
fmt.Println("istek atılıyor")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://context.free.beeceptor.com/", nil)
if err != nil {
return err
}
_, err = http.DefaultClient.Do(req)
if err != nil {
return err
}
fmt.Println("cevap alındı")
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := sendReq(ctx); err != nil {
fmt.Println("sendReq err:", err)
}
fmt.Println("program sonlandı")
}
>> istek atılıyor
>> sendReq err: Get "https://context.free.beeceptor.com/": context deadline exceeded
>> program sonlandı
Top comments (0)