Menu

Golang Context: İptal, Timeout'lar ve Değerler

context.Context'in iptali, deadline'ları ve istek kapsamlı değerleri bir Go programı boyunca nasıl taşıdığı: Background, WithCancel, WithTimeout, WithValue, select içinde ctx.Done ve HTTP sunucu ve istemcilerinde context.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

On satırda bir timeout

context.Context'in işi koda ne zaman durması gerektiğini söylemektir. Burada yavaş bir işleme 50 ms verilir ve context öyle dediğinde vazgeçer:

İlk çağrı 10 ms'de biter ve rows <nil> döndürür. İkincisi 200 ms'ye ihtiyaç duyardı, ama context 50 ms'de (oluşturulduğu andan itibaren sayılarak) süresini doldurur, bu yüzden context deadline exceeded döndürür.

Hiçbir şey zorla durdurulmaz. Go'da bir goroutine'i dışarıdan öldürmenin bir yolu yoktur. Bir context bir sinyaldir ve kodun onu kontrol etmesi gerekir: ctx.Done() üzerinde select yaparak, adımlar arasında ctx.Err()'ü kontrol ederek ya da ctx'i onu sizin yerinize kontrol eden kütüphane çağrılarına (http.NewRequestWithContext, db.QueryContext, exec.CommandContext) aktararak.

Context interface'i

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}
MetotDöndürdüğü
Done()context iptal edildiğinde ya da süresi dolduğunda kapatılan bir channel (hiç iptal edilemeyen bir context için nil)
Err()aktifken nil, sonra context.Canceled ya da context.DeadlineExceeded
Deadline()deadline ve true, ya da yoksa ok == false
Value(key)bu context'te ya da bir atasında key altında saklanan değer, ya da nil

Context'ler değiştirilemezdir. Birini asla değiştirmezsiniz; With fonksiyonlarından biriyle ondan bir çocuk türetirsiniz ve çocuk bir iptal sinyali, bir deadline ya da bir değer ekler.

Bir context nereden gelir

Her context ağacı bir kökten başlar:

  • main, init, testler ve sunucuların üst seviye kurulumu için context.Background().
  • Bir fonksiyonun context alması gerektiği ama çağıranın henüz bir tane sahibi olmadığı durumlar için context.TODO(). Tam olarak Background gibi davranır; ad, sonraki yeniden düzenleme için bir işarettir.

Bir HTTP handler'ının içinde kök oluşturmazsınız. Sunucunun istemci bağlantıyı kestiğinde ya da handler döndüğünde iptal ettiği r.Context()'i kullanırsınız.

WithCancel: istendiğinde durmak

context.WithCancel bir çocuk context ve bir cancel fonksiyonu döndürür. cancel'ı çağırmak çocuğun Done channel'ını ve ondan türetilen her şeyin Done channel'larını kapatır.

Üreticinin gönderimi ctx.Done()'un yanında bir select içinde durur. Onun durabilmesini sağlayan budur: tüketici okumayı bıraktığında çıplak bir out <- i sonsuza kadar bloklanır ve goroutine sızardı. Sondaki for range nums, üretici channel'ı kapatana kadar bekler. O boşalırken üretici bir iki değer daha göndermeyi başarabilir, çünkü select'inin iki case'i de hazır olduğunda Go rastgele birini seçer; iptal hızlıdır ama anlık değildir.

cancel'ı birden fazla kez ve herhangi bir goroutine'den çağırmak güvenlidir. Yalnızca ilk çağrı bir şey yapar.

WithTimeout ve WithDeadline

WithTimeout(parent, d), WithDeadline(parent, time.Now().Add(d))'dir. "En fazla bu kadar" için bir timeout, mutlak bir zamanınız olduğunda bir deadline kullanın.

Süre geçtiğinde Done kapanır ve Err, context.DeadlineExceeded döndürür. Önce cancel çağrılırsa Err, context.Canceled döndürür. Kütüphaneler genellikle hatayı sarmaladığı için hangisi olduğunu errors.Is ile kontrol edin:

Kendiliğinden tetiklenecek bir timeout için bile cancel'ı her zaman çağırın. Context bunlardan biri olana kadar bir timer ve ebeveyninde bir yuva tutar; defer cancel() fonksiyon döner dönmez ikisini de serbest bırakır. go vet atılan bir cancel fonksiyonunu raporlar: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak.

Çocuklar ebeveynlerinden uzun yaşayamaz

Context'ler bir ağaç oluşturur. Bir ebeveyni iptal etmek her torunu iptal eder. Bir çocuğun deadline'ı ebeveyninkinden kısa olabilir, asla uzun olamaz: her zaman daha erken deadline kazanır.

Context'leri katmanlar arasında kullanışlı yapan budur. Bir HTTP handler'ı istekle birlikte ölen bir context alır; üç katman aşağıdaki bir veritabanı çağrısı ondan 2 saniyelik bir timeout türetir. İstemci 100 ms sonra kapatırsa sorgu iki saniye sonra değil, o anda iptal edilir.

Bloklandığınızda her zaman ctx.Done() üzerinde select yapın

Bekleyen her goroutine (bir channel gönderiminde, bir alımda, bir timer'da) aynı anda ctx.Done()'u da beklemelidir. Hiç bloklanmayan CPU yoğun döngüler için ara sıra ctx.Err()'ü kontrol edin:

for i, item := range items {
	if i%1000 == 0 {
		if err := ctx.Err(); err != nil {
			return err
		}
	}
	process(item)
}

Basit bir bekleme için select içinde time.After kullanın, ama bekleme sık sık iptal edilebilecekse durdurabileceğiniz bir timer'ı (ya da bir context timeout'unu) tercih edin.

İptal nedenleri (Go 1.20 ve 1.21)

ctx.Err() yalnızca canceled ya da deadline exceeded der. Nedenini kaydetmek için Cause çeşitlerini kullanın:

WithCancelCause Go 1.20'de, WithTimeoutCause ve WithDeadlineCause Go 1.21'de geldi. Var olan kontroller çalışmaya devam etsin diye Err standart değerleri döndürmeye devam eder; ayrıntıyı context.Cause verir.

WithValue, ölçülü kullanın

context.WithValue(parent, key, value) tek bir değer ekler. ctx.Value(key) onu ebeveyn zinciri boyunca arar.

Değer kuralları:

  • Anahtarlar için asla düz bir string değil, dışa kapalı bir tip kullanın. İkisi de "user" kullanan iki paket birbirinin üzerine yazardı. (go vet bunu yakalamaz; staticcheck yakalar.)
  • Erişimi WithRequestID ve RequestID gibi tipli yardımcı fonksiyonlarla sarın, böylece çağıranlar asla any'yi ya da anahtarı görmez.
  • Yalnızca API'lerden geçen istek kapsamlı verileri saklayın: trace ve istek ID'leri, kimliği doğrulanmış kullanıcı, bir logger. İsteğe bağlı parametreleri, veritabanı tanıtıcılarını ya da yapılandırmayı asla saklamayın. Bunlar, derleyicinin kontrol edebileceği ve okuyanların görebileceği fonksiyon argümanlarına ya da struct alanlarına aittir.
  • Arama zinciri her seferinde bir ebeveyn dolaşır, bu yüzden eklediğiniz her değer diğerlerinin aranmasını bir adım uzatır.

Gelenekler

  • ctx context.Context, G/Ç yapan, bloklanan ya da bunları yapan bir şeyi çağıran her fonksiyonun ilk parametresidir: func Fetch(ctx context.Context, url string) error.
  • Bir context'i bir struct içinde saklamayın. Onu her metot çağrısına aktarın. Bir context tek bir işleme aittir ve bir struct genellikle ondan uzun yaşar. (İstisna, http.Request gibi tek bir işlemi temsil eden bir tiptir.)
  • Context olarak asla nil aktarmayın. Daha iyisi yoksa context.TODO() kullanın.
  • Context nedeniyle durduğunuzda ctx.Err()'ü döndürün ya da %w ile sarmalayın, böylece çağıranlar bir timeout'u gerçek bir başarısızlıktan ayırt edebilir.

HTTP sunucularında ve istemcilerinde context

Sunucu tarafı: r.Context() istemci bağlantıyı kestiğinde, handler döndüğünde ya da bir HTTP/2 stream'i sıfırlandığında iptal edilir. İstemci tarafı: http.NewRequestWithContext isteğin bir timeout'a ya da iptale uymasını sağlar. Bu program iki ucu da httptest üzerinden çalıştırır:

İstemci 50 ms'de vazgeçer ve bağlantıyı kapatır. Sunucu bunu fark eder, istek context'i iptal edilir ve handler kimsenin okumayacağı bir rapor için 450 milisaniye daha harcamak yerine durur. Gerçek bir handler'da r.Context()'i her veritabanı ve HTTP çağrısına aktarırsınız ve hepsi birlikte durur.

Diğer yardımcılar (Go 1.21)

  • context.WithoutCancel(ctx), aynı değerlere sahip ama ctx iptal edildiğinde iptal edilmeyen bir context döndürür. Bir denetim logu yazmak gibi istek bittikten sonra da tamamlanması gereken işler için kullanın.
  • context.AfterFunc(ctx, f), ctx bittiğinde f'yi kendi goroutine'inde çalıştırır ve kaydı kaldırmak için bir stop fonksiyonu döndürür.

Sık yapılan hatalar

  • cancel'ı çağırmamak. WithCancel, WithTimeout ya da WithDeadline'dan hemen sonra her zaman defer cancel().
  • ctx'i yok sayan bir goroutine başlatmak. ctx.Done() üzerinde select yapmadan bloklanıyorsa iptal hiçbir şey yapmaz ve goroutine sızar.
  • Bir çağrı zincirinin derinliklerinde yeni bir context.Background() oluşturmak. Çağıranın deadline'ı ve iptaliyle bağı keser. Size verilen ctx'i aktarın.
  • Hataları == ile karşılaştırmak. errors.Is(err, context.DeadlineExceeded) kullanın; çoğu kütüphane onu sarmalar.
  • Bağımlılıklar için WithValue kullanmak. Bir context içinde gizlenmiş bir veritabanı tanıtıcısı, derleyicinin artık kontrol edemediği bir parametredir.
  • İptalin anlık olmasını beklemek. Kod bunu yalnızca bir sonraki kontrolünde fark eder. Kontrolü olmayan uzun bir döngü çalışmaya devam eder.

Sıkça Sorulan Sorular

Go'da context ne için kullanılır?

Bir context.Context, bir fonksiyona ve onun çağırdığı her şeye ne zaman vazgeçeceğini söyler: çağıran iptal ettiği için, bir deadline geçtiği için ya da istemci bağlantıyı kestiği için. Bir istek ID'si gibi istek kapsamlı değerler de taşıyabilir. Geleneğe göre ilk parametredir ve adı ctx'tir.

context.Background ile context.TODO arasındaki fark nedir?

İkisi de hiç iptal edilmeyen, deadline'ı ya da değeri olmayan boş bir context döndürür. Aynı şekilde davranırlar. Background(), main, testler ve üst seviye kurulum için köktür. TODO() ise gerçek bir context'in aktarılması gereken ama çevreleyen kodun henüz bir tane sahibi olmadığı bir yeri işaretler; bu da onu sonradan bulmayı kolaylaştırır.

context.WithTimeout'tan sonra neden cancel çağırmam gerekiyor?

WithTimeout, WithDeadline ve WithCancel yeni context'i ebeveynine kaydeder ve bir timer başlatabilir. cancel'ı çağırmak bu kaynakları timeout tetiklendiğinde ya da ebeveyn iptal edildiğinde değil, işiniz biter bitmez serbest bırakır. Oluşturduktan hemen sonra defer cancel() yazın; bir cancel fonksiyonu atıldığında go vet uyarır.

Go'da "context deadline exceeded" ne anlama gelir?

Bu, bir context'in deadline'ı geçtikten sonra ctx.Err()'ün döndürdüğü context.DeadlineExceeded hatasının metnidir. HTTP istemcileri ve veritabanı sürücüleri gibi context'e uyan fonksiyonlar, süreleri bittiğinde onu (çoğu zaman sarmalanmış olarak) döndürür. errors.Is(err, context.DeadlineExceeded) ile kontrol edin.

Parametre aktarmak için context.WithValue kullanmalı mıyım?

Hayır. Onu yalnızca API sınırlarını geçen ve aradaki fonksiyonların bilmesi gerekmeyen istek kapsamlı veriler için kullanın; bir trace ID'si ya da kimliği doğrulanmış bir kullanıcı gibi. Bir fonksiyonun işini yapmak için ihtiyaç duyduğu her şey, derleyicinin kontrol ettiği parametrelerine aittir.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA