Pemrograman konkuren memiliki idiom tersendiri.
Salah satu contoh idiom tersebut yaitu penggunaan pewaktuan (timeout).
Walaupun channel pada Go tidak mendukung timeout, konsep timeout
sebenarnya cukup mudah diimplementasikan.
Katakanlah kita ingin menerima sebuah nilai dari channel ch
, namun ingin
menunggu hanya selama satu detik sebelum nilai sampai.
Kita bisa memulai dengan membuat sebuah channel yang memberi sinyal dan
meluncurkan sebuah goroutine yang tidur sebelum mengirim ke channel
tersebut:
timeout := make(chan bool, 1) go func() { time.Sleep(1 * time.Second) timeout <- true }()
Kemudian kita dapat menggunakan perintah select
untuk menerima antara ch
atau timeout
.
Jika tidak ada nilai yang diterima pada ch
setelah satu detik, maka pilihan
timeout akan dipilih dan pembacaan pada ch
ditinggalkan.
select { case <-ch: // pembacaan dari ch telah terjadi case <-timeout: // pembacaan dari ch waktunya telah habis }
Channel timeout
memiliki buffer dengan ruang 1 nilai, sehingga membolehkan
goroutine mengirim ke channel dan selesai.
Goroutine tersebut tidak tahu (atau tidak peduli) apakah nilai yang dikirimnya
diterima atau tidak.
Hal ini berarti goroutine tersebut tidak akan menunggu selamanya jika channel
ch
menerima nilai sebelum timeout terjadi.
Channel timeout
nantinya akan dibersihkan oleh garbage collector.
(Dalam contoh ini kita menggunakan time.Sleep
untuk memperlihatkan mekanisme
dari goroutine dan channel.
Dalam program sebenarnya, anda seharusnya menggunakan
time.After,
yaitu fungsi yang mengembalikan sebuah channel dan mengirim nilai ke channel
tersebut setelah durasi tertentu.)
Mari kita lihat variasi lain dari pola ini. Dalam contoh berikut kita memiliki sebuah program yang mencoba mendapatkan nilai dari beberapa basis data (database) replika secara simultan. Program tersebut hanya butuh satu jawaban, dan hanya menerima jawaban yang datang pertama kali.
Fungsi Query
menerima sebuah slice koneksi database dan sebuah string
query
.
Fungsi tersebut akan mengeksekusi query
di setiap koneksi database secara
paralel dan mengembalikan respons pertama yang sampai:
func Query(conns []Conn, query string) Result { ch := make(chan Result) for _, conn := range conns { go func(c Conn) { select { case ch <- c.DoQuery(query): default: } }(conn) } return <-ch }
Dalam contoh di atas, closure (fungsi tanpa nama, dalam konteks ini yaitu
fungsi di dalam pengulangan for
) melakukan pengiriman tanpa ditahan, dengan
menggunakan operasi pengiriman dalam perintah select
dengan default
.
Jika pengiriman tidak bisa langsung terjadi maka pilihan default
akan
dijalankan.
Dengan melakukan pengiriman tanpa ditahan, maka menjamin bahwa tidak ada
goroutine yang diluncurkan dalam pengulangan tersebut akan hang.
Namun, jika hasilnya eksekusi dari DoQuery
diterima sebelum fungsi Query
sampai ke perintah "return ←ch", maka pengiriman dapat gagal karena tidak ada
yang siap menerima dari channel ch
:
Permasalahan ini adalah contoh yang dikenal sebagai
kondisi berpacu
(race condition), namun cara memperbaiki cukup mudah.
Kita cukup membuat channel ch
memiliki buffer (dengan menambahkan panjang
buffer sebagai argumen dari
make),
sehingga menjamin bahwa pengiriman yang pertama memiliki ruang untuk menyimpan
nilai kembaliannya.
Hal ini supaya pengiriman selalu sukses, dan nilai pertama yang
diterima akan sampai tanpa memperhatikan urutan eksekusi.
Kedua contoh di atas memperlihatkan kesahajaan, yang mana Go dapat mengekspresikan interaksi yang kompleks antara goroutine.