- read

Go Context Paketi

Onur Akköse 16

Go Context paketi aracılığı ile farklı işlemler boyunca iptal sinyalleri, veriler ve işlemlerin iptal edilmesi gereken tarihleri paylaşabiliriz. Örnek olarak server olarak çalıştırdığımız bir Go programına istekler atıldığını düşünelim. Bu isteklerin işlenerek geriye bir cevap döndürülmesine kadar çeşitli işlemler yapılıyor olabilir. Başka servislere istekler atılabilir ya da bir veritabanı ile iletişim kurulabilir. Fakat istemci bir istek gönderdikten sonrasında çalışmayı durdurmuş ve ulaşılamaz olabilir. Böyle bir durumda bir cevap dönülemeyeceği için arada yapılan her türlü işlem bir zaman kaybıdır. Go’da istemciye gelen her bir request kendi goroutine’ini yaratır ve bu gorotuineler veritabanı işlemleri ya da kullanıcının doğrulanmış olup olmaması ile ilgilenebilir. İstemcinin çevrim dışı kalması durumunda da tüm bu goroutinelerin çalışmayı durdurmasını bekleriz. Bunu sağlamak için ise Context paketini kullanırız.

Context paketinin temeli bir Context Interface’dir.

type Context interface { Done() <-chan struct{} Err() error Deadline() (deadline time.Time, ok bool) Value(key interface{}) interface{}}

Done metodu fonksiyonları durduran bir iptal sinyali göndermek için sadece değer okunan bir channel döndürür. Nil dönmesi durumunda bu contextin hiçbir zaman iptal edilemeceği anlaşılır. Yaygın olarak select ile kullanılır. Bu channel kapandığı zaman fonksiyonlar işlerini yarıda bırakırlar. Err metodu ise bir contextin neden iptal edildiği gösterir. Deadline metodu fonksiyonları çalışmaya başlayıp başlamayacağını bir zaman vererek belirlememizi sağlar. Value ise context üzerinden veri taşınmasını sağlar.

WithCancel, WithDeadline ve WithTimeout fonksiyonları bir parent context alır ve türetilmiş bir context ile bir CancelFunc döner. CancelFunc çağrıldığı zaman türetilen çocuk contexti ve ondan türeyen çocuk contextleri durdurur.

Contextlerin kullanıldığı fonksiyonlarda ilk parametre olması önerilir. Ayrıca contextler üzerinden taşınan veriler opsiyonel veriler olmamalıdır, istek tabanlı veriler taşınmalıdır.

Aynı contextin farklı goroutineler tarafından çalıştırılan farklı fonksiyonlara verilmesi sorun teşkil etmez. Contextler farklı goroutineler tarafında eş zamanlı kullanımlara uygundur.

Func Background() Context

Background iptal edilemeyen, bir deadline veya veri taşımayan boş bir context döner. Main, init gibi üst seviye fonksiyonlar da ayrıca testler de kullanılır. Birbirlerinden türeyen context zincirinin ilk elemanı olarak düşünülebilir.

func example(ctx context.Context) {
fmt.Println("example")
}
func main() {
ctx := context.Background()
example(ctx)
}

Func TODO() Context

TODO’da Background() gibi nil olmayan boş bir context döner. Gelecekte context eklenecekse fakat hangi context kullanılacağı henüz belli değilse kullanılır.

func example(ctx context.Context) {
fmt.Println("example")
}
func main() {
ctx := context.TODO()
example(ctx)
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

Bir parent context kendisine parametre olarak geçildiğinde bir Done Channel’a sahip türetilmiş bir context ve bir iptal fonksiyonu döner. Dönen iptal fonksiyonu çağrıldığında veya parent contextin Done Channel’ı kapatıldığında, mevcut contextin de Done Channel’ı kapanır. Kullanım örneği olarak 10 tane goroutinemiz var ise ve bir sinyal aldığımızda hepsi sonlandırmak istiyorsak , tüm goroutineler boyunca aynı contexti paylaşırsak kolayca yapabiliriz. Context türetildiği zaman contexte atanan tüm kaynakların serbest kalması için contexin iptal edilmesi önemlidir.

func example(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
go example2(ctx) cancel() time.Sleep(100 * time.Millisecond)
}
func example2(ctx context.Context) {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Println(err)
}
case <-time.After(1 * time.Second):
fmt.Printf("finished")
}
}
func main() {
ctx := context.Background()
example(ctx)
}

İkinci örnek :

func example(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
chanex := make(chan int)
go example2(ctx, chanex)
for i := 0; i < 3; i++ {
chanex <- i
}
cancel()
time.Sleep(100 * time.Millisecond)
}
func example2(ctx context.Context, chanex <-chan int) {
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Println(err)
}
return
case number := <-chanex:
fmt.Println(number)
}
}
}
func main() {
ctx := context.Background()
example(ctx)
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

Argüman olarak bir parent context ve bir tarih alır. Tarih aşıldığında dönen contextin Done Channel’ı kapanır. Ayrıca dönen iptal fonksiyonu çağrılırsa veya parent contextin Done Channel’ı kapanırsa yine context iptal edilir. Kısacası bir contextin ne zamana kadar yaşayacağına karar veririz.

func example(ctx context.Context) {
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
chanex := make(chan int)
go example2(ctx, chanex)
for i := 0; i < 3; i++ {
select {
case chanex <- i:
time.Sleep(1 * time.Second)
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Println("the error example => ", err)
}
break
}
}
cancel()
time.Sleep(100 * time.Millisecond)
}
func example2(ctx context.Context, chanex <-chan int) {
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Println("the error example2 => ", err)
}
return
case number := <-chanex:
fmt.Println("example2 => ", number)
}
}
}
func main() {
ctx := context.Background()
example(ctx)
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithDeadline() ile benzerdir. Belirli bir tarih yerine bir süre alır ve türetilen context ile bir iptal fonksiyonu döner. Örneğin fonksiyona 1 dakikalık bir süre verirsek, 1 dakika sonra context ve ona bağlı contextler iptal olacaktır. Şu şekilde de gösterilebilir: WithDeadline(parent, time.Now().Add(timeout)).

func example(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
chanex := make(chan int)
go example2(ctx, chanex)
for i := 0; i < 3; i++ {
select {
case chanex <- i:
time.Sleep(1 * time.Second)
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Println("the error example => ", err)
}
break
}
}
cancel()
time.Sleep(100 * time.Millisecond)
}
func example2(ctx context.Context, chanex <-chan int) {
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Println("the error example2 => ", err)
}
return
case number := <-chanex:
fmt.Println("example2 => ", number)
}
}
}
func main() {
ctx := context.Background()
example(ctx)
}

func WithValue(parent Context, key, val any) Context

Fonksiyonlarınız arasında veri paylaşmak istiyorsanız bunu context aracılığı ile yapabilirsiniz. Key ile veri ekleyebilir veya okuyabiliriz. Her veri tipini context içinde saklayabiliriz.

func example(ctx context.Context) {
fmt.Println(ctx.Value("myKey"))
}
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "myKey", "example")
example(ctx)
}

İkinci örnek:

func example(ctx context.Context) {
fmt.Println(ctx.Value("myKey"))
newCtx := context.WithValue(ctx, "myKey", "example2")
example2(newCtx)
fmt.Println(ctx.Value("myKey"))
}
func example2(ctx context.Context) {
fmt.Println(ctx.Value("myKey"))
}
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "myKey", "example")
example(ctx)
}

Kaynakça