Catatan: Artikel ini membutuhkan pengalaman dan pengetahuan tentang membuat layanan peladen (server), seperti peladen HTTP.
Pendahuluan
Dalam peladen (server) yang dibuat dengan Go, setiap permintaan yang masuk ditangani oleh goroutine-nya sendiri. Fungsi yang menangani permintaan (disebut juga handler) terkadang menjalankan goroutine tambahan untuk mengakses backend lainnya seperti layanan basis-data dan Remote Procedure Call (RPC). Kumpulan goroutine yang bekerja dalam sebuah handler biasanya membutuhkan akses ke nilai tertentu seperti identitas pengguna, token otorisasi, dan tenggat (deadline) permintaan. Saat sebuah permintaan dibatalkan atau kehabisan waktu, semua goroutine yang sedang bekerja pada permintaan tersebut seharusnya segera berhenti supaya sistem dapat mengambil alih kembali sumber daya yang mereka gunakan.
Di Google, kami mengembangkan sebuah paket context
yang mempermudah
mengirim nilai sesuai skop-permintaan, sinyal pembatalan, dan tenggat ke semua
goroutine yang ikut serta dalam menangani sebuah permintaan.
Paket tersebut tersedia secara publik sebagai
context.
Artikel ini menjelaskan bagaimana menggunakan paket tersebut dan menyediakan
sebuah contoh kerjanya.
Context
Inti dari paket context
adalah tipe Context
:
// Sebuah Context membawa sebuah tenggat (Deadline), sinyal pembatalan (Done), // dan nilai-nilai (Value) sesuai skop-permintaan. // Method-method nya aman digunakan secara simultan oleh banyak _goroutine_. type Context interface { // Done mengembalikan sebuah kanal yang tertutup saat Context ini // dibatalkan atau waktunya telah habis. Done() <-chan struct{} // Err mengindikasikan kenapa context dibatalkan, setelah kanal Done // tertutup. Err() error // Deadline mengembalikan waktu saat Context ini akan dibatalkan, jika // ada. Deadline() (deadline time.Time, ok bool) // Value mengembalikan nilai yang berasosiasi dengan key, atau nil jika // key tidak ada. Value(key interface{}) interface{} }
(Deskripsi dari tipe Context di atas telah diringkas; lihat godoc untuk lebih lengkapnya.)
Method Done
mengembalikan sebuah kanal yang berlaku sebagai sinyal pembatalan
terhadap fungsi-fungsi yang berjalan dengan Context
: saat kanal tersebut
tertutup, fungsi-fungsi tersebut sebaiknya berhenti bekerja.
Method Err
mengembalikan sebuah error
yang mengindikasikan kenapa
Context
tersebut dibatalkan.
Artikel
pipeline dan pembatalan
mendiskusikan idiom dari kanal Done
lebih detail.
Sebuah Context
tidak memiliki method Cancel
dengan alasan yang sama
kenapa kanal Done
hanya menerima-saja: fungsi yang menerima sinyal
pembatalan biasanya bukan yang mengirim sinyal.
Pada khususnya, saat sebuah induk operasi memulai beberapa goroutine untuk
sub-operasi, maka sub-operasi tersebut tidak bisa membatalkan induk.
Namun, fungsi WithCancel
(yang dijelaskan di bawah) menyediakan cara untuk
membatalkan sebuah nilai Context
yang baru.
Sebuah Context
aman digunakan secara simultan oleh beberapa goroutine.
Kode dapat mengirim sebuah Context
ke sejumlah goroutine dan membatalkan
Context
tersebut untuk mengirim sinyal ke semua goroutine.
Method Deadline
membolehkan fungsi menentukan apakah mereka harus mulai
bekerja atau tidak;
jika waktu yang tersedia tinggal sedikit, maka pekerjaan mungkin sebaiknya
tidak dilakukan.
Kode juga bisa menggunakan sebuah tenggat untuk men-set batas waktu untuk
operasi input/output (I/O).
Method Value
membolehkan sebuah Context
membawa data sesuai
skop-permintaan.
Data tersebut haruslah aman untuk digunakan secara simultan oleh beberapa
goroutine.
Context turunan
Paket context
menyediakan fungsi-fungsi untuk menurunkan nilai Context
baru dari yang sudah ada.
Nilai-nilai Context
tersebut membentuk sebuah pohon: saat sebuah Context
dibatalkan, semua Context
turunannya juga akan ikut dibatalkan.
Fungsi Background
adalah akar dari semua pohon Context
; ia tidak pernah
dibatalkan:
// Background mengembalikan sebuah Context kosong. // Ia tidak pernah dibatalkan, dan tidak punya tenggat, dan tidak memiliki // nilai. // Fungsi Background biasanya digunakan dalam main, init, dan tes, dan sebagai // Context induk pada penanganan permintaan yang masuk. func Background() Context
Fungsi WithCancel
dan WithTimeout
mengembalikan turunan dari nilai
Context
yang dapat dibatalkan lebih awal dari Context
induk-nya.
Context
yang berasosiasi dengan permintaan masuk biasanya dibatalkan saat
handler selesai.
Fungsi WithCancel
berguna untuk membatalkan permintaan yang duplikat saat
menggunakan beberapa replika.
Fungsi WithTimeout
berguna untuk men-set sebuah tenggat saat mengirim
permintaan ke peladen backend lain.
// WithCancel mengembalikan sebuah salinan dari Content `parent` dengan kanal // Done tertutup setelah parent.Done ditutup atau saat cancel dipanggil. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // CancelFunc membatalkan sebuah Context. type CancelFunc func() // WithTimeout mengembalikan salinan dari Context `parent` dengan kanal Done // ditutup setelah parent.Done ditutup, atau cancel dipanggil, atau timeout // telah lewat. // Tenggat dari Context yang baru yaitu lebih kecil dari now+timeout dan dari // tenggat `parent`, jika ada. // Jika timer masih tetap berjalan, fungsi cancel melepaskan sumber daya // mereka. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
Fungsi WithValue
menyediakan sebuah cara untuk mengasosikan nilai-nilai
skop-permintaan dengan sebuah Context
:
// WithValue mengembalikan sebuah salinan dari Context parent yang mana method // `Value`-nya mengembalikan `val` dari `key`. func WithValue(parent Context, key interface{}, val interface{}) Context
Cara paling baik untuk melihat bagaimana paket context
digunakan yaitu lewat
sebuah contoh kode, seperti yang akan kita bahas di bawah.
Contoh: Pencarian Web Google
Contoh ini yaitu sebuah peladen HTTP yang menangani URL seperti
/search?q=golang&timeout=1s
dengan meneruskan kueri "golang" ke
API Google Web Search
dan menampilkan hasilnya.
Parameter timeout
memberitahu peladen untuk membatalkan permintaan tersebut
setelah durasi habis.
Kode ini dibagi dalam tiga paket:
Program peladen
Program
peladen
menangani permintaan seperti /search?q=golang
dengan
mengembalikan beberapa hasil pencarian pertama dari Google untuk kata
golang
.
Peladen tersebut memiliki fungsi handleSearch
untuk menangani permintaan
ke /search
.
Fungsi tersebut membuat sebuah Context
induk bernama ctx
yang mengatur
supaya dibatalkan saat fungsi selesai.
Jika permintaan mengikutkan parameter timeout
pada kueri URL, maka Context
akan dibatalkan secara otomatis saat timeout
telah habis:
func handleSearch(w http.ResponseWriter, req *http.Request) { // ctx adalah Context dari fungsi ini. // Memanggil cancel akan menutup kanal ctx.Done, yang merupakan sinyal // pembatalan untuk permintaan yang dimulai oleh fungsi ini. var ( ctx context.Context cancel context.CancelFunc ) timeout, err := time.ParseDuration(req.FormValue("timeout")) if err == nil { // Permintaan memiliki batas waktu, jadi buatlah sebuah context // yang dibatalkan secara otomatis saat timeout selesai. ctx, cancel = context.WithTimeout(context.Background(), timeout) } else { ctx, cancel = context.WithCancel(context.Background()) } defer cancel() // Batalkan ctx saat handleSearch selesai.
Fungsi handleSearch
mengekstrak kueri dan alamat IP klien dari HTTP
request dengan memanggil paket userip
.
Alamat IP dari klien dibutuhkan untuk permintaan ke backend, jadi
handleSearch
memasukkan-nya ke dalam ctx
:
// Periksa kueri pencarian. query := req.FormValue("q") if query == "" { http.Error(w, "no query", http.StatusBadRequest) return } // Simpan alamat pengguna dalam ctx untuk digunakan oleh kode dalam paket // lain. userIP, err := userip.FromRequest(req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } ctx = userip.NewContext(ctx, userIP)
Fungsi handleSearch
kemudian memanggil google.Search
dengan mengirim ctx
dan query
:
// Jalankan pencarian Google dan cetak hasilnya. start := time.Now() results, err := google.Search(ctx, query) elapsed := time.Since(start)
Jika pencarian sukses, fungsi tersebut menampilkan hasilnya:
if err := resultsTemplate.Execute(w, struct { Results google.Results Timeout, Elapsed time.Duration }{ Results: results, Timeout: timeout, Elapsed: elapsed, }); err != nil { log.Print(err) return }
Paket userip
Paket
userip
menyediakan fungsi-fungsi untuk mengekstrak alamat IP pengguna
dari sebuah permintaan dan menanamnya dalam sebuah Context.
Sebuah Context
menyediakan pemetaan kunci-nilai, yang mana kunci dan nilai
bertipe interface{}
.
Tipe dari kunci haruslah mendukung
ekualitas,
dan tipe dari nilai haruslah aman digunakan secara simultan oleh beberapa
goroutine.
Paket seperti userip
menyembunyikan detail dari pemetaan ini dan menyediakan
akses ke nilai Context
tertentu.
Untuk menghindari bentrok dengan kunci yang lain, userip
mendefinisikan tipe
key
yang tidak diekspor dan menggunakan nilai dari tipe tersebut sebagai
kunci dari context:
// Tipe key tidak diekspor untuk mencegah bentrok dengan kunci-kunci dari // context yang didefinisikan dalam paket yang lain. type key int // userIPkey adalah kunci context untuk alamat IP pengguna. // Nilainya bisa 0 atau nilai integer lain. // Jika paket ini mendefinisikan kunci-kunci context lainnya, maka nilai // setiap kunci tersebut haruslah memiliki nilai integer yang berbeda. const userIPKey key = 0
Fungsi FromRequest
mengekstrak sebuah nilai userIP
dari http.Request
:
func FromRequest(req *http.Request) (net.IP, error) { ip, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr) }
Fungsi NewContext
mengembalikan sebuah Context
baru yang membawa nilai
userIP
:
func NewContext(ctx context.Context, userIP net.IP) context.Context { return context.WithValue(ctx, userIPKey, userIP) }
Fungsi FromContext
mengekstrak sebuah userIP
dari sebuah Context
:
func FromContext(ctx context.Context) (net.IP, bool) { // ctx.Value mengembalikan nil jika ctx tidak memiliki nilai sesuai key; // konversi tipe net.IP mengembalikan ok=false jika kunci tidak ada atau // nilai IP adalah nil. userIP, ok := ctx.Value(userIPKey).(net.IP) return userIP, ok }
Paket google
Fungsi
google.Search
membuat permintaan HTTP ke
Google Web Search API
dan mengurai kembalian dalam bentuk JSON.
Fungsi tersebut menerima sebuah Context
parameter ctx
dan selesai bila
ctx.Done
ditutup walau permintaan masih tetap berjalan.
Permintaan untuk Google Web Search API mengikutkan query
pencarian dan
alamat IP pengguna sebagai parameter kueri:
func Search(ctx context.Context, query string) (Results, error) { // Persiapkan permintaan untuk Google Search API. req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil) if err != nil { return nil, err } q := req.URL.Query() q.Set("q", query) // Jika ctx membawa alamat IP pengguna, teruskan ke peladen. // Google API menggunakan alamat IP pengguna untuk membedakan permintaan // yang diinisiasi oleh server dengan permintaan dari user. if userIP, ok := userip.FromContext(ctx); ok { q.Set("userip", userIP.String()) } req.URL.RawQuery = q.Encode()
Fungsi Search
menggunakan fungsi pembantu, httpDo
, untuk membuat dan
membatalkan permintaan HTTP bila ctx.Done
ditutup saat permintaan atau
respon masih dalam proses.
Fungsi Search
mengirim sebuah closure ke httpDo
untuk menangani respon
HTTP:
var results Results err = httpDo(ctx, req, func(resp *http.Response, err error) error { if err != nil { return err } defer resp.Body.Close() // Urai hasil pencarian dalam bentuk JSON. // https://developers.google.com/web-search/docs/#fonje var data struct { ResponseData struct { Results []struct { TitleNoFormatting string URL string } } } if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return err } for _, res := range data.ResponseData.Results { results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL}) } return nil }) // httpDo menunggu closure yang kita berikan selesai, jadi aman untuk // membaca hasilnya di sini. return results, err
Fungsi httpDo
menjalankan permintaan HTTP dan memproses respon dalam sebuah
goroutine yang baru.
Ia membatalkan permintaan jika ctx.Done
ditutup sebelum goroutine selesai:
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { // Jalankan permintaan HTTP dalam sebuah goroutine dan kirim respon-nya ke // f. c := make(chan error, 1) req = req.WithContext(ctx) go func() { c <- f(http.DefaultClient.Do(req)) }() select { case <-ctx.Done(): <-c // Tunggu sampai f selesai. return ctx.Err() case err := <-c: return err } }
Adaptasi kode untuk Context
Banyak kerangka peladen menyediakan paket dan tipe untuk membawa nilai-nilai
sesuai-nilai sesuai-nilai sesuai-nilai sesuai skop-permintaan.
Kita dapat mendefinisikan implementasi baru dari interface Context
untuk
menjembatani antara kode yang menggunakan kerangka yang telah ada dan kode
yang mengharapkan sebuah parameter Context
.
Misalnya, paket
github.com/gorilla/context
pada kerangka peladen HTTP Gorilla membolehkan fungsi-fungsi mengasosiasikan
data dengan permintaan yang masuk dengan menyediakan sebuah pemetaan dari
permintaan HTTP ke pasangan kunci-nilai.
Dalam
gorilla.go,
kami menyediakan sebuah implementasi Context
dengan method Value
mengembalikan nilai-nilai yang diasosiasikan dengan permintaan HTTP tertentu
dalam paket Gorilla.
Paket-paket lain telah menyediakan dukungan pembatalan yang mirip dengan
Context
.
Contohnya,
Tomb
menyediakan sebuah method Kill
yang mengirim sinyal pembatalan dengan
menutup kanal Dying
.
Tomb
juga menyediakan method-method untuk menunggu goroutine selesai,
mirip dengan sync.WaitGroup
.
Dalam
tomb.go,
kami menyediakan sebuah implementasi Context
yang dibatalkan saat Context
induk-nya dibatalkan atau saat Tomb
dihentikan.
Kesimpulan
Di Google, kita mengharuskan programmer Go mengirim sebuah parameter Context
sebagai argumen pertama pada semua fungsi antara permintaan masuk dan keluar.
Hal ini membolehkan kode Go yang dikembangkan oleh banyak tim yang berbeda
saling terhubung dengan baik.
Ia menyediakan kontrol sederhana terhadap batas waktu dan pembatalan dan
memastikan nilai-nilai penting seperti kredensial keamanan terkirim dalam
program Go dengan benar.
Kerangka peladen yang ingin dibangun dengan Context
sebaiknya menyediakan
implementasi Context
untuk menjembatani antara paket mereka dengan paket
yang mengharapkan sebuah parameter Context
.
Pustaka klien mereka kemudian menerima sebuah Context
dari kode yang
dipanggil.
Dengan menjalin sebuah antarmuka umum untuk data dengan skop-permintaan dan
pembatalan, Context
mempermudah pengembang paket berbagi kode untuk membuat
layanan-layanan yang mudah dikembangkan.