context
context diterjemahkan sebagai konteks, tujuan awal desainnya adalah untuk mentransmisikan sinyal dan beberapa data sederhana lintas beberapa goroutine, terutama antara goroutine parent dan child. Context ini biasanya digunakan dalam skenario seperti menangani permintaan HTTP, penjadwalan tugas, kueri database, terutama dalam arsitektur microservice, gRPC menggunakan context untuk mentransmisikan metadata lintas proses dan jaringan, kontrol tautan, dan operasi lainnya.
package main
import (
"context"
"fmt"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
defer stop()
for {
select {
case <-ctx.Done():
fmt.Println("terminate")
return
default:
}
fmt.Println("running")
time.Sleep(100 * time.Millisecond)
}
}Dalam kasus di atas, context digunakan untuk mentransmisikan sinyal proses, ketika menerima sinyal, program akan keluar dengan sendirinya, ini juga merupakan salah satu skenario aplikasi context.
Struktur
context.Context bukan implementasi konkret, melainkan antarmuka yang mendefinisikan sekumpulan metode
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline(), mengembalikan tanggal deadline, dan nilai boolean menunjukkan apakah sudah berakhirDone(), mengembalikan channel, digunakan untuk mengirim notifikasi berakhirErr(), mengembalikan alasan penutupan contextValue(), mendapatkan nilai yang ditentukan berdasarkan key
Di pustaka standar total ada beberapa context yang dapat digunakan
Background, context kosong, biasanya digunakan sebagai node root contextWithCancel,WithCancelCause, context yang dapat dibatalkanWithDeadline,WithDeadlineCause, context dengan deadline waktuWithTimeout,WithTimeoutCause, context dengan waktu timeoutWithValue, context yang dapat mentransmisikan nilai
Untuk implementasi konkretnya terutama adalah
timerCtxcancelCtxemptyCtx
Jadi fungsi intinya ada empat poin
- Pembatalan
- Deadline
- Transmisi nilai
- Propagasi
Memahami beberapa poin ini, pada dasarnya sudah memahami cara kerja context.
Pembatalan
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
cause error // set to non-nil by the first cancel call
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}Inti dari cancelCtx terletak pada metode propagateCancel, yang bertanggung jawab untuk menyebarkan perilaku pembatalan ke context parent dan child.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}Pertama-tama akan memeriksa apakah parent context dapat dibatalkan, jika tidak maka langsung return
godone := parent.Done() if done == nil { return // parent is never canceled }Return memeriksa apakah parent context sudah dibatalkan, jika ya maka batalkan semua child context
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }Mencoba mengonversi parent context ke tipe
cancelCtx, jika berhasil maka akan menambahkan context saat ini kechildrenparent context.goif p, ok := parentCancelCtx(parent); ok { // parent is a *cancelCtx, or derives from one. p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err, p.cause) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() return }Mencoba mengonversinya ke tipe
afterFuncer, jika berhasil, akan mendaftarkan metode pembatalan context saat ini keAfterFuncparent contextgoif a, ok := parent.(afterFuncer); ok { // parent implements an AfterFunc method. c.mu.Lock() stop := a.AfterFunc(func() { child.cancel(false, parent.Err(), Cause(parent)) }) c.Context = stopCtx{ Context: parent, stop: stop, } c.mu.Unlock() return }Jika masih tidak berhasil, maka membuat goroutine terpisah untuk memantau channel
Done, ketika menerima sinyal, akan membatalkan child contextgogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
Kemudian metode cancelCtx.cancel akhirnya bertanggung jawab untuk membatalkan child context
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}Alurnya adalah sebagai berikut
Periksa apakah context saat ini sudah dibatalkan
goif err == nil { panic("context: internal error: missing cancel error") } if cause == nil { cause = err } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled }Tutup channel
done, kirim notifikasi penutupangoc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Loop untuk memberi tahu child context
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()Terakhir berdasarkan parameter yang传入 untuk menentukan apakah perlu menghapus dari parent context
if removeFromParent { removeChild(c.Context, c) }
Deadline
WithTimeout dan WithDeadline keduanya adalah context dengan deadline waktu, keduanya adalah tipe yang sama, hanya semantik penggunaannya yang berbeda, dan keduanya berbasis cancelCtx
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause bertanggung jawab untuk membuat context dengan deadline waktu
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Alurnya adalah sebagai berikut
Periksa tanggal deadline, jika parent deadline lebih awal dari deadline saat ini, maka parent context pasti akan dibatalkan lebih dulu dari context saat ini, maka langsung buat context tipe
cancelCtxgoif parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) }Bangun
timeCtx, dan sebarkan ke child contextgoc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Hitung deadline saat ini, jika sudah deadline langsung batalkan
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }Jika belum deadline, gunakan
time.AfterFuncuntuk mengatur pembatalan context saat ini pada waktu deadlinegoc.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded, cause) }) } return c, func() { c.cancel(true, Canceled, nil) }
Untuk timerCtx, metode pembatalan context-nya hanya menghentikan timer, dan顺便 menghentikan child context
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}Transmisi Nilai
valueCtx dapat mentransmisikan nilai dalam context, mendapatkan nilai
type valueCtx struct {
Context
key, val any
}
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}Intinya terletak pada fungsi value, implementasi context lainnya juga akan masuk ke fungsi ini untuk mendapatkan nilai, misalnya
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}Fungsi value adalah loop for besar, terus mencari nilai key yang ditentukan melalui rekursi ke atas
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}Penanganan tipe Context yang berbeda:
*valueCtx: Jikakeycocok dengankeyContext saat ini, kembalikanvalyang sesuai dengankey.*cancelCtx: Jikakeycocok dengancancelCtxKey, kembalikancancelCtxsaat ini, menunjukkan mengembalikan dirinya sendiri.withoutCancelCtx: MenunjukkanContexttanpa fungsi pembatalan, jikakeycocok dengancancelCtxKey, kembalikannil.*timerCtx: Jikakeycocok dengancancelCtxKey, kembalikancancelCtxyang terkait dengannya.backgroundCtxdantodoCtx: Biasanya adalah tipeContextkhusus tanpa membawa nilai tambahan apa pun, langsung kembalikannilketika遇到 kedua tipe ini.- Jika tipe tidak diketahui, lanjutkan memanggil metode
Valueuntuk mencari.
Ringkasan
Inti dari beberapa context ini adalah cancelCtx, melalui itu untuk menyebarkan sinyal pembatalan, tipe context lainnya serta tipe context pihak ketiga juga merupakan lapisan demi lapisan,在此基础上实现 berbagai fungsi.
