Skip to content

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.

go
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

go
// 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 berakhir
  • Done(), mengembalikan channel, digunakan untuk mengirim notifikasi berakhir
  • Err(), mengembalikan alasan penutupan context
  • Value(), mendapatkan nilai yang ditentukan berdasarkan key

Di pustaka standar total ada beberapa context yang dapat digunakan

  • Background, context kosong, biasanya digunakan sebagai node root context
  • WithCancel, WithCancelCause, context yang dapat dibatalkan
  • WithDeadline, WithDeadlineCause, context dengan deadline waktu
  • WithTimeout, WithTimeoutCause, context dengan waktu timeout
  • WithValue, context yang dapat mentransmisikan nilai

Untuk implementasi konkretnya terutama adalah

  • timerCtx
  • cancelCtx
  • emptyCtx

Jadi fungsi intinya ada empat poin

  • Pembatalan
  • Deadline
  • Transmisi nilai
  • Propagasi

Memahami beberapa poin ini, pada dasarnya sudah memahami cara kerja context.

Pembatalan

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

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. Pertama-tama akan memeriksa apakah parent context dapat dibatalkan, jika tidak maka langsung return

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. Return memeriksa apakah parent context sudah dibatalkan, jika ya maka batalkan semua child context

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. Mencoba mengonversi parent context ke tipe cancelCtx, jika berhasil maka akan menambahkan context saat ini ke children parent context.

    go
    if 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
    }
  4. Mencoba mengonversinya ke tipe afterFuncer, jika berhasil, akan mendaftarkan metode pembatalan context saat ini ke AfterFunc parent context

    go
    if 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
    }
  5. Jika masih tidak berhasil, maka membuat goroutine terpisah untuk memantau channel Done, ketika menerima sinyal, akan membatalkan child context

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

go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	...
}

Alurnya adalah sebagai berikut

  1. Periksa apakah context saat ini sudah dibatalkan

    go
    if 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
    }
  2. Tutup channel done, kirim notifikasi penutupan

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. Loop untuk memberi tahu child context

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. 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

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause bertanggung jawab untuk membuat context dengan deadline waktu

go
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	...
}

Alurnya adalah sebagai berikut

  1. 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 cancelCtx

    go
    if 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)
    }
  2. Bangun timeCtx, dan sebarkan ke child context

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. Hitung deadline saat ini, jika sudah deadline langsung batalkan

    go
    dur := time.Until(d)
    if dur <= 0 {
    	c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
    	return c, func() { c.cancel(false, Canceled, nil) }
    }
  4. Jika belum deadline, gunakan time.AfterFunc untuk mengatur pembatalan context saat ini pada waktu deadline

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

go
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

go
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

go
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

go
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: Jika key cocok dengan key Context saat ini, kembalikan val yang sesuai dengan key.
  • *cancelCtx: Jika key cocok dengan cancelCtxKey, kembalikan cancelCtx saat ini, menunjukkan mengembalikan dirinya sendiri.
  • withoutCancelCtx: Menunjukkan Context tanpa fungsi pembatalan, jika key cocok dengan cancelCtxKey, kembalikan nil.
  • *timerCtx: Jika key cocok dengan cancelCtxKey, kembalikan cancelCtx yang terkait dengannya.
  • backgroundCtx dan todoCtx: Biasanya adalah tipe Context khusus tanpa membawa nilai tambahan apa pun, langsung kembalikan nil ketika遇到 kedua tipe ini.
  • Jika tipe tidak diketahui, lanjutkan memanggil metode Value untuk 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.

Golang by www.golangdev.cn edit