Bekerja dengan Goroutines adalah sebuah momok yang membingungkan saya di awal development dengan Go. Goroutines tidak hanya menjadi andalan Go dalam melakukan proses secara simultan, namun juga sebuah skill yang wajib dikuasai dalam perancangan sebuah program menggunakan Go.

Channel

Pada dasarnya goroutines hanyalah sebutan dari sebuah fungsi yang terdapat penggalan kata go di depannya

go func(name string) {
    fmt.Printf("Hello %s", name)
}("Kresna")

Contoh Goroutines

Sepenggal baris kode diatas merupakan contoh dari goroutines yang melakukan proses cetak ke stdout sistem. Kode program tersebut akan berjalan, beriringan dengan proses utama hingga prosesnya selesai. Singkat kata, fungsi tersebut tidak lagi tergantung pada block operation yang terjadi pada proses utamanya.

Kesulitan baru akan muncul ketika main proses atau proses lainnya ingin berinterakasi dengan goroutines tersebut. Karena groroutines memiliki jalurnya sendiri dalam proses simultan, diperlukan sebuah mekanisme yang dapat menjembatani proses goroutines dengan proses lainnya. Mekanisme tersebut bernama Channel.

err := make(chan error)

Contoh Deklarasi Channel dengan tipe Error

Penggalan kode diatas merupakan contoh dari sebuah channel dengan tipe Error di Go.

Channel merupakan sebuah interface dari sebuah tipe data atau objek dimana seolah-olah menjadi terowongan atau jembatan yang terhubung antara pengirim dan penerimanya.

func main() {
    result := make(chan int)
    
    go func (res chan<- int, num1, num2 int) {
        time.Sleep(time.Second * 5) // ilustrasi waktu proses
        res<- num1 + num2
		close(res)
    }(result, 10, 20)
    
    fmt.Printf("10 + 20 = %v", <-result)
}

play.golang.org/p/c2dDZ0smjmP

Kutipan kode diatas merupakan ilustrasi channel dengan tipe integer melakukan komunikasi dengan mengirimkan hasil dari penjumlahan ke dalam blok proses utama.

Jika program diatas dijalankan, maka akan proses utama akan menunggu proses dari fungsi goroutines untuk selesai (ilustrasinya membutuhkan waktu 5 detik), setelahnya proses utama akan mencetak hasil ke console / terminal. Dari sini terlihat bahwa proses utama atau proses yang menerima hasil akan terkena blocking (pause) sampai hasil dari goroutines dikirimkan melalui channel result.

Alur Data pada Channel

...
    res<- num1 + num2
    ...
fmt.Printf("10 + 20 = %v", <-result)
...

Alur data flow pada channel ditandai dengan penulisan anak panah ke variabelnya. Mengutip contoh kode sebelumnya, penulisan res<- merupakan statemen untuk mengirimkan data (send) hasil penjumlahan untuk masuk melalui channel (terowongan), lalu pada main kode program terdapat <-result (receive) yang merupakan proses pengambilan data penjumlahan.

Buffered Channel

Buffered Channel dapat dianalogikan seperti sebuah terowongan yang memiliki kuota mengenai berapa banyak jumlah objek yang dapat muat masuk sekaligus kedalamnya. Pada dasarnya channel tidak memiliki buffer (unbuffered channel), alias hanya satu objek saja yang dapat dikirimkan melalui channel tersebut.

Namun dengan penggunaan Buffered Channel kita dapat melakukan proses pengiriman data sekaligus sebanyak jumlah buffer (capacity) yang disiapkan sebelumnya.

func add(res chan<- int, num1, num2 int) {
	res <- num1 + num2
	fmt.Println("sent", num1+num2)
}

func printer(t <-chan int) {
	for c := 0; c < 3; c++ {
		time.Sleep(time.Second)
		fmt.Printf("processing %v \n", <-t)
	}
}

func main() {
	results := make(chan int)

	go add(results, 3, 5)
	go add(results, 7, 9)
	go add(results, 2, 12)

	go printer(results)

	time.Sleep(time.Second * 5)
	close(results)
}

play.golang.org/p/JRy7-Qc2Utj

Dalam contoh program diatas, channel results merupakan unbuffered channel, dimana menjadikan fungsi add() hanya dapat mengirimkan hasil penjumlahan begitu channel results tersedia atau telah melakukan proses receive.

Fungsi printer() sebagai receiver memiliki waktu proses selama 1 detik, karena melakukan proses menggunakan unbuffered channel, maka antar goroutines add() harus saling menunggu untuk dapat mengirimkan hasil berikutnya.

processing 16 
sent 16
processing 8 
sent 8
processing 14 
sent 14

Program exited.

Hasil ekseskusi Unbuffered Channel

Terlihat dari hasil eksekusi kode program diatas, fungsi add() hanya akan dapat mengirimkan data ke channel ketika proses receive telah dilakukan, karena itu proses cetak ke terminal selalu berdampingan antara pengiriman dan pemrosesan data hasil.

Mengambil dari ilustrasi kode sebelumnya, dengan merubah sebaris kode seperti dibawah ini, akan mengganti prosses unbuffered channel menjadi buffered channel dimana channel akan memiliki kuota sebanyak 3x int.

...
results := make(chan int, 3)
...

play.golang.org/p/T80d9Zrjs6G

Dengan menggunakan buffered channel, proses pengiriman hasil dari fungsi add() dapat langsung dilakukan tanpa harus menunggu channel results tersedia. Hal ini terlihat dari hasil output di bawah ini.

sent 16
sent 14
sent 8
processing 16 
processing 14 
processing 8 

Program exited.

Hasil ekseskusi Buffered Channel

Closing Channel

Channel yang telah selesai digunakan harus ditutup dengan keyword close, hal ini menandakan proses transmisi data melalui channel tersebut tidak lagi dapat dilakukan. Selanjutnya channel yang sudah tidak terpakai ini akan dibersihkan melalui proses garbage collection.

...
close(results)
...

Closing channel

GOMAXPROCS

Dalam prakteknya setelah versi go 1.5, default value dari GOMAXPROCS akan diset dengan sejumlah CPU yang tersedia pada komputer yang menjalankannya. Hal ini tentunya merupakan hal yang positif mengingat dalam versi sebelumnya, developer harus mendeteksi berapa jumlah CPU core yang ada dan melakukan set GOMAXPROCS secara manual.

Mengapa hal ini sangat berpengaruh? Gorotines pada dasarnya tidak selalu membutuhkan multi CPU untuk bekerja, beberapa developer mungkin berfikir dengan menambah jumlah CPU, jalannya sebuah program akan lebih maksimal, sayangnya tidak selalu seperti itu.

Goroutines mirip seperti Thread pada Java, pada dasarnya menggunakan Memory (RAM) pada komputer untuk dapat beroperasi, dia tidak selalu harus berjalan pada CPU yang berbeda meskipun CPU realitanya hanya dapat melakukan satu pekerjaan dalam satu waktu. Hal ini dapat dicapai karena satuan waktu yang dibutuhkan CPU untuk memproses satu task sangatlah kecil, sehingga ketika sejumlah goroutines dijalankan akan seperti terlihat simultan bagi kita.

Selain itu dalam prosesnya Go secara otomatis akan mengatur berapa dan bagaimana goroutines akan bekerja di satu core atau multi-core, karena setelah Go versi 1.5, developer tidak perlu lagi memikirkan bagaimana kolaborasi gorotines dalam memanfaatkan multi-core CPU agar proses dapat bekerja lebih cepat dan efisien.

Berkomunikasilah dengan Data bukan Pointer

Don’t communicate by sharing memory; share memory by communicating. (R. Pike)

Kutipan diatas menjelaskan kepada kita tentang bagaimana sebaiknya sebuah proses concurrency berinteraksi antara satu dengan yang lainnya. Meskipun pada Go kita dapat mengirimkan pointer (alamat memory) namun hal itu sebaiknya dihindari ketika menggunakan goroutines.

Melakukan komunikasi antar goroutines dengan cara sharing memory adalah hal yang patut dihindari, karena dapat berpotensi abuse pada alamat memory yang di share ke beberapa proses, dimana memungkinkan munculnya error bila terdapat ketidak konsistenan program dalam memanipulasi memory address. Oleh karena itu dalam goroutines sebaiknya hanya mengirimkan objek statis atau value saja.

Semoga tulisan saya di atas cukup membantu kalian dalam memahami cara kerja dan aturan-aturan yang ada pada Goroutines.