
Paket konteks di Go berguna untuk interaksi dengan API dan proses yang lambat, terutama dalam sistem tingkat produksi yang menangani permintaan web. Dengan bantuannya, goroutine dapat diberi tahu tentang perlunya menyelesaikan pekerjaan mereka.
Di bawah ini adalah panduan kecil untuk membantu Anda menggunakan paket ini dalam proyek Anda, serta beberapa praktik terbaik dan perangkap.
(Catatan: Konteksnya digunakan dalam banyak paket, misalnya, dalam bekerja dengan Docker ).
Sebelum Anda mulai
Untuk menggunakan konteks, Anda harus memahami apa itu goroutine dan saluran. Saya akan mencoba mempertimbangkannya secara singkat. Jika Anda sudah terbiasa dengan mereka, langsung ke bagian Konteks.
Gorutin
Dokumentasi resmi mengatakan bahwa "Gorutin adalah aliran eksekusi yang ringan." Goroutin lebih ringan dari utas, jadi mengelolanya relatif kurang intensif sumber daya.
โ Kotak Pasir
package main import "fmt" // , Hello func printHello() { fmt.Println("Hello from printHello") } func main() { // // go func(){fmt.Println("Hello inline")}() // go printHello() fmt.Println("Hello from main") }
Jika Anda menjalankan program ini, Anda akan melihat bahwa hanya Hello from main
dicetak. Sebenarnya, kedua goroutine mulai, tetapi main
selesai lebih awal. Jadi, Goroutine perlu cara untuk memberi tahu main
tentang akhir eksekusi mereka, dan agar dia menunggu ini. Di sini saluran datang untuk membantu kami.
Saluran
Saluran adalah cara komunikasi antara goroutine. Mereka digunakan ketika Anda ingin mentransfer hasil, kesalahan atau informasi lain dari satu goroutine ke yang lain. Saluran adalah tipe yang berbeda, misalnya, saluran tipe int
menerima bilangan bulat, dan saluran tipe error
menerima kesalahan, dll.
Katakanlah kita memiliki saluran ch
tipe int
. Jika Anda ingin mengirim sesuatu ke saluran, sintaksinya adalah ch <- 1
. Anda bisa mendapatkan sesuatu dari saluran seperti ini: var := <- ch
, i.e. ambil nilainya dari saluran dan simpan dalam variabel var
.
Kode berikut menggambarkan cara menggunakan saluran untuk mengkonfirmasi bahwa goroutine telah menyelesaikan pekerjaan mereka dan mengembalikan nilainya ke main
.
Catatan: Grup menunggu juga dapat digunakan untuk sinkronisasi, tetapi dalam artikel ini saya memilih saluran untuk contoh kode, karena kita akan menggunakannya nanti di bagian konteks.
โ Kotak Pasir
package main import "fmt" // int func printHello(ch chan int) { fmt.Println("Hello from printHello") // ch <- 2 } func main() { // . make // : // ch := make(chan int, 2), . ch := make(chan int) // . , . // go func(){ fmt.Println("Hello inline") // ch <- 1 }() // go printHello(ch) fmt.Println("Hello from main") // // , i := <- ch fmt.Println("Received ",i) // // , <- ch }
Konteks
Paket konteks di mana saja memungkinkan Anda untuk meneruskan data ke program Anda dalam semacam "konteks". Konteksnya, seperti batas waktu, tenggat waktu atau saluran, memberi sinyal penutupan dan panggilan kembali.
Misalnya, jika Anda membuat permintaan web atau menjalankan perintah sistem, sebaiknya gunakan batas waktu untuk sistem tingkat produksi. Karena jika API yang Anda akses lambat, Anda tidak mungkin ingin mengakumulasi permintaan di sistem Anda, karena hal ini dapat menyebabkan peningkatan beban dan penurunan kinerja saat memproses permintaan Anda sendiri. Hasilnya adalah efek kaskade.
Dan di sini konteks batas waktu atau batas waktu mungkin tepat.
Pembuatan konteks
Paket konteks memungkinkan Anda untuk membuat dan mewarisi konteks dengan cara berikut:
context.Background () ctx Konteks
Fungsi ini mengembalikan konteks kosong. Ini harus digunakan hanya pada level tinggi (di utama atau di handler permintaan level tertinggi). Ini dapat digunakan untuk mendapatkan konteks lain, yang akan kita bahas nanti.
ctx, cancel := context.Background()
Catatan trans .: Ada ketidakakuratan dalam artikel asli, contoh yang benar dalam menggunakan context.Background
akan menjadi sebagai berikut:
ctx := context.Background()
context.TODO () Konteks ctx
Fungsi ini juga menciptakan konteks kosong. Dan itu juga harus digunakan hanya pada level tinggi, baik ketika Anda tidak yakin konteks mana yang digunakan, atau jika fungsi belum menerima konteks yang diinginkan. Ini berarti bahwa Anda (atau seseorang yang mendukung kode) berencana untuk menambahkan konteks ke fungsi nanti.
ctx, cancel := context.TODO()
Catatan trans .: Ada ketidakakuratan dalam artikel asli, contoh yang benar untuk menggunakan context.TODO
akan menjadi sebagai berikut:
ctx := context.TODO()
Menariknya, lihat kodenya , sama sekali sama dengan latar belakang. Satu-satunya perbedaan adalah bahwa dalam kasus ini, Anda dapat menggunakan alat analisis statis untuk memeriksa validitas transfer konteks, yang merupakan detail penting, karena alat ini membantu mengidentifikasi potensi kesalahan pada tahap awal dan dapat dimasukkan dalam pipa CI / CD.
Dari sini :
var ( background = new(emptyCtx) todo = new(emptyCtx) )
context.Dengan Nilai (Konteks induk, kunci, antarmuka val {}) (Konteks CTX, batalkan CancelFunc)
Catatan Lane: Ada ketidakakuratan dalam artikel asli, tanda tangan yang benar untuk context.WithValue
akan sebagai berikut:
context.WithValue(parent Context, key, val interface{}) Context
Fungsi ini mengambil konteks dan mengembalikan konteks yang berasal darinya di mana nilai val
dikaitkan dengan key
dan melewati seluruh pohon konteks. Yaitu, segera setelah Anda membuat konteks WithValue
, setiap konteks turunan akan menerima nilai ini.
Tidak disarankan untuk melewatkan parameter kritis menggunakan nilai konteks, sebagai gantinya, fungsi harus mengambilnya secara eksplisit dalam tanda tangan.
ctx := context.WithValue(context.Background(), key, "test")
context.DenganCancel (Konteks induk) (Konteks CTX, batalkan CancelFunc)
Menjadi sedikit lebih menarik di sini. Fungsi ini menciptakan konteks baru dari orang tua yang diteruskan ke sana. Orang tua dapat menjadi konteks latar belakang atau konteks yang diteruskan sebagai argumen ke fungsi.
Konteks yang diturunkan dan fungsi undo dikembalikan. Hanya fungsi yang membuatnya harus memanggil fungsi untuk membatalkan konteks. Anda dapat meneruskan fungsi undo ke fungsi lain jika diinginkan, tetapi ini sangat tidak disarankan. Biasanya keputusan ini dibuat dari kesalahpahaman tentang pembatalan konteks. Karena itu, konteks yang dihasilkan dari orangtua ini dapat memengaruhi program, yang akan mengarah pada hasil yang tidak terduga. Singkatnya, lebih baik untuk TIDAK PERNAH melewati fungsi batal.
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
Catatan Lane: Dalam artikel aslinya, penulis, kelihatannya, keliru untuk context.WithCancel
memberi contoh dengan context.WithDeadline
. Contoh yang benar untuk context.WithCancel
ctx, cancel := context.WithCancel(context.Background())
context.DenganDeadline (Konteks induk, d waktu. Waktu) (Konteks ctx, batalkan CancelFunc)
Fungsi ini mengembalikan konteks turunan dari induknya, yang dibatalkan setelah batas waktu atau panggilan ke fungsi batal. Misalnya, Anda dapat membuat konteks yang secara otomatis dibatalkan pada waktu tertentu dan meneruskannya ke fungsi anak. Ketika konteks ini dibatalkan setelah batas waktu, semua fungsi yang memiliki konteks ini harus diberitahukan melalui pemberitahuan.
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
context.Dengan Timeime (Konteks induk, batas waktu time. Durasi) (Konteks CTX, batalkan CancelFunc)
Fungsi ini mirip dengan konteks. Perbedaannya adalah bahwa lamanya waktu digunakan sebagai input. Fungsi ini mengembalikan konteks turunan yang dibatalkan ketika fungsi batal dipanggil atau setelah beberapa saat.
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
Catatan Per.: Dalam artikel asli, penulis, tampaknya, keliru untuk context.WithTimeout
memberi contoh dengan context.WithDeadline
. Contoh yang benar untuk context.WithTimeout
adalah:
ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
Penerimaan dan penggunaan konteks dalam fungsi Anda
Sekarang kita tahu cara membuat konteks (Latar Belakang dan TODO) dan cara membuat konteks (WithValue, WithCancel, Tenggat Waktu, dan Batas Waktu), mari kita bahas cara menggunakannya.
Dalam contoh berikut, Anda dapat melihat bahwa fungsi yang mengambil konteks meluncurkan goroutine dan mengharapkannya untuk mengembalikan atau membatalkan konteks. Pernyataan pilih membantu kita menentukan apa yang terjadi terlebih dahulu dan menghentikan fungsi.
Setelah menutup saluran Selesai <-ctx.Done()
, case case <-ctx.Done():
dipilih. Segera setelah ini terjadi, fungsi harus mengganggu pekerjaan dan bersiap untuk kembali. Ini berarti Anda harus menutup koneksi terbuka, membebaskan sumber daya, dan kembali dari fungsi. Ada saat-saat ketika pelepasan sumber daya dapat menunda pengembalian, misalnya, pembersihan terhenti. Anda harus mengingat hal ini.
Contoh yang mengikuti bagian ini adalah program yang sudah jadi yang menggambarkan fungsi timeout dan undo.
// , - // , - func sleepRandomContext(ctx context.Context, ch chan bool) { // (. .: ) // // , defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() // sleeptimeChan := make(chan int) // // go sleepRandom("sleepRandomContext", sleeptimeChan) // select select { case <-ctx.Done(): // , // , - // , ( ) // - , // , // fmt.Println("Time to return") case sleeptime := <-sleeptimeChan: // , fmt.Println("Slept for ", sleeptime, "ms") } }
Contoh
Seperti yang kita lihat, menggunakan konteks Anda dapat bekerja dengan tenggat waktu, batas waktu, dan juga memanggil fungsi batal, dengan demikian memperjelas semua fungsi menggunakan konteks turunan yang Anda perlukan untuk menyelesaikan pekerjaan Anda dan menjalankan pengembalian. Pertimbangkan sebuah contoh:
fungsi main
:
- Membuat konteks fungsi batal
- Memanggil fungsi batal setelah batas waktu sewenang-wenang
Fungsi doWorkContext
:
- Menciptakan konteks turunan dengan batas waktu
- Konteks ini dibatalkan ketika fungsi utama memanggil cancelFunction, batas waktu habis, atau doWorkContext memanggil cancelFunction.
- Menjalankan goroutine untuk melakukan beberapa tugas lambat, melewati konteks yang dihasilkan
- Menunggu goroutine selesai atau konteksnya dibatalkan dari main, mana yang lebih dulu
Fungsi sleepRandomContext
:
- Meluncurkan goroutine untuk melakukan beberapa tugas lambat
- Menunggu goroutine selesai, atau
- Menunggu konteksnya dibatalkan oleh fungsi utama, batas waktu, atau panggil Fungsi batal sendiri
Fungsi sleepRandom
:
Contoh ini menggunakan mode tidur untuk mensimulasikan waktu pemrosesan acak, tetapi dalam kenyataannya, Anda dapat menggunakan saluran untuk memberi sinyal fungsi ini tentang dimulainya pembersihan dan menunggu konfirmasi dari saluran bahwa pembersihan selesai.
Sandbox (Sepertinya waktu acak yang saya gunakan di sandbox praktis tidak berubah. Coba ini di komputer lokal Anda untuk melihat keacakan)
โ Github
package main import ( "context" "fmt" "math/rand" "Time" ) // func sleepRandom(fromFunction string, ch chan int) { // defer func() { fmt.Println(fromFunction, "sleepRandom complete") }() // // , // ยซยป seed := time.Now().UnixNano() r := rand.New(rand.NewSource(seed)) randomNumber := r.Intn(100) sleeptime := randomNumber + 100 fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms") time.Sleep(time.Duration(sleeptime) * time.Millisecond) fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms") // , if ch != nil { ch <- sleeptime } } // , // , - func sleepRandomContext(ctx context.Context, ch chan bool) { // (. .: ) // // , defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() // sleeptimeChan := make(chan int) // // go sleepRandom("sleepRandomContext", sleeptimeChan) // select select { case <-ctx.Done(): // , // , doWorkContext // doWorkContext main cancelFunction // , - // , ( ) // - , // , // fmt.Println("sleepRandomContext: Time to return") case sleeptime := <-sleeptimeChan: // , fmt.Println("Slept for ", sleeptime, "ms") } } // , // // , main func doWorkContext(ctx context.Context) { // - // 150 // , , 150 ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond) // defer func() { fmt.Println("doWorkContext complete") cancelFunction() }() // // , // , ch := make(chan bool) go sleepRandomContext(ctxWithTimeout, ch) // select select { case <-ctx.Done(): // , // , main cancelFunction fmt.Println("doWorkContext: Time to return") case <-ch: // , fmt.Println("sleepRandomContext returned") } } func main() { // background ctx := context.Background() // ctxWithCancel, cancelFunction := context.WithCancel(ctx) // // defer func() { fmt.Println("Main Defer: canceling context") cancelFunction() }() // - // , go func() { sleepRandom("Main", nil) cancelFunction() fmt.Println("Main Sleep complete. canceling context") }() // doWorkContext(ctxWithCancel) }
Perangkap
Jika fungsi menggunakan konteks, pastikan pemberitahuan pembatalan ditangani dengan benar. Misalnya, exec.CommandContext
itu tidak menutup saluran baca sampai perintah menyelesaikan semua garpu yang dibuat oleh proses ( Github ), yaitu bahwa membatalkan konteks tidak segera kembali dari fungsi jika Anda menunggu dengan cmd.Wait (), sampai semua percabangan dari perintah eksternal menyelesaikan pemrosesan.
Jika Anda menggunakan batas waktu atau tenggat waktu dengan runtime maksimum, itu mungkin tidak berfungsi seperti yang diharapkan. Dalam kasus seperti itu, lebih baik menerapkan timeout menggunakan time.After
.
Praktik terbaik
- konteks. Latar belakang hanya boleh digunakan pada level tertinggi, sebagai akar dari semua konteks turunan.
- context.TODO harus digunakan ketika Anda tidak yakin apa yang harus digunakan, atau jika fungsi saat ini akan menggunakan konteks di masa depan.
- Pembatalan konteks disarankan, tetapi fungsi-fungsi ini mungkin membutuhkan waktu untuk dihapus dan keluar.
- context.Value harus digunakan sesedikit mungkin, dan tidak boleh digunakan untuk melewati parameter opsional. Ini membuat API tidak dapat dipahami dan dapat menyebabkan kesalahan. Nilai-nilai seperti itu harus dilewatkan sebagai argumen.
- Jangan menyimpan konteks dalam struktur, sampaikan secara eksplisit dalam fungsi, lebih baik sebagai argumen pertama.
- Jangan pernah melewatkan konteks nol sebagai argumen. Jika ragu, gunakan TODO.
- Struktur
Context
tidak memiliki metode pembatalan, karena hanya fungsi yang memunculkan konteks yang harus membatalkannya.
Dari penerjemah
Di perusahaan kami, kami secara aktif menggunakan paket Konteks ketika mengembangkan aplikasi server untuk penggunaan internal. Tetapi aplikasi seperti itu untuk fungsi normal, selain Konteks, memerlukan elemen tambahan, seperti:
- Penebangan
- Pemrosesan sinyal untuk penghentian aplikasi, pemuatan ulang, dan pencatatan log
- Bekerja dengan file pid
- Bekerja dengan file konfigurasi
- Dan lainnya
Oleh karena itu, pada titik tertentu, kami memutuskan untuk merangkum semua pengalaman kami dan membuat paket tambahan yang sangat menyederhanakan aplikasi penulisan (terutama aplikasi yang memiliki API). Kami telah memposting perkembangan kami di domain publik dan siapa pun dapat menggunakannya. Berikut ini adalah beberapa tautan ke paket yang berguna untuk memecahkan masalah seperti itu:
Baca juga artikel lain di blog kami: