DEV Community

Furkan Aksoy
Furkan Aksoy

Posted on

[TR] Golang - Context Nedir?

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{}
}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
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.

context

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Kod Örnekleri

Context - İptal

Playground

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ı")
}
Enter fullscreen mode Exit fullscreen mode
>> a çalışmaya başladı
>> context iptal edildi
>> program sonlandı
Enter fullscreen mode Exit fullscreen mode

Context - Zaman Aşımı

Playground

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ı")
}
Enter fullscreen mode Exit fullscreen mode
>> a çalışmaya başladı
>> context iptal edildi
>> program sonlandı
Enter fullscreen mode Exit fullscreen mode

Context - Bilgi Gömmek

Playground

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ı")
}
Enter fullscreen mode Exit fullscreen mode
>> a çalışmaya başladı
>> context içinden okunan değer: Merhaba Dünya
>> program sonlandı
Enter fullscreen mode Exit fullscreen mode

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

Playground

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ı")
}
Enter fullscreen mode Exit fullscreen mode
>> 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ı
Enter fullscreen mode Exit fullscreen mode

Contextlerin ağaç görünümü

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ı")
}
Enter fullscreen mode Exit fullscreen mode
>> istek atılıyor
>> sendReq err: Get "https://context.free.beeceptor.com/": context deadline exceeded
>> program sonlandı
Enter fullscreen mode Exit fullscreen mode

Top comments (0)