[TR] Golang - Context Nedir?
Tags: go, context, programming
Published: 2023-11-30 06:55:26

## 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](https://blog.golang.org/context) 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ış

```go
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.

```go
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.


![context](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l6ju8p8xz1gkmpvu6evd.png)


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.

```go
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.

```go
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.

```go
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.

```go
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.

```go
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

[Playground](https://play.golang.org/p/qd8RGe8vly1)

```go
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ı

[Playground](https://play.golang.org/p/-Xan2vcmnGq)

```go
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

[Playground](https://play.golang.org/p/4CJ3E8v1fMb)

```go
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

[Playground](https://play.golang.org/p/elyX-sOw_-Y)

```go
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ı
```

![Contextlerin ağaç görünümü](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bzyep13ekw15stl3abm1.png)

#### 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 :)

```go
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ı
```