Memanggil fungsi secara biasa membuat pemanggil berhenti menunggu fungsi tersebut selesai. Bayangkan program unduhan yang harus menunggu satu berkas selesai sebelum mulai berikutnya — total waktu jumlah dari seluruh penungguan. Go mengganti model itu dengan goroutine: fungsi yang dijalankan secara concurrent oleh runtime sehingga pemanggil bisa langsung melanjutkan pekerjaan lain. Chapter ini mempraktikkan keyword go, menyaksikan apa yang terjadi ketika program keluar tanpa menunggu, lalu memakai sync.WaitGroup untuk menyinkronkan penyelesaian sekumpulan goroutine.

Menjalankan Fungsi dengan keyword go #

Keyword go ditempatkan sebelum pemanggilan fungsi. Runtime kemudian menjalankan fungsi itu sebagai goroutine baru dan sekaligus mengembalikan kendali ke pemanggil, sehingga baris setelah go dieksekusi tanpa menunggu:

go
go sapa("Budi")          // bentuk pemanggilan fungsi bernama
go func() {              // bentuk fungsi anonim (anonymous function)
        fmt.Println("dari goroutine")
}()

Kedua bentuk sama sahnya. Fungsi anonim sering dipakai ketika tugas goroutine cukup pendek dan tidak perlu nama. Parameter dievaluasi pada saat statement go dieksekusi, bukan saat goroutine benar-benar mulai berjalan.

Program Keluar Tanpa Menunggu Goroutine #

Perilaku paling penting yang harus dipahami: fungsi main adalah goroutine juga, dan ketika main kembali, runtime Go menghentikan seluruh program termasuk semua goroutine yang masih berjalan. Berkas goroutine-demo.go membuktikannya:

go
package main

import (
	"fmt"
	"time"
)

func sapa(nama string) {
	time.Sleep(500 * time.Millisecond) // simulasi kerja yang lama
	fmt.Println("Halo,", nama)
}

func main() {
	go sapa("Budi")

	fmt.Println("program selesai")
}

Jalankan dengan perintah berikut dari folder chapter:

bash
go run goroutine-demo.go

Output yang Dihasilkan:

text
program selesai

Mari kita telaah baris demi baris program di atas:

  1. Baris 3–6 (import ...): mengimpor package fmt untuk mencetak dan package time yang dipakai untuk jeda simulasi kerja.
  2. Baris 8–11 (func sapa): fungsi yang akan dijalankan sebagai goroutine; time.Sleep di dalamnya hanya mensimulasikan kerja yang lama — durasinya tidak berperan dalam sinkronisasi.
  3. Baris 14 (go sapa("Budi")): memulai goroutine sapa dan langsung melanjutkan; pemanggilan ini kembali dalam hitungan mikrodetik.
  4. Baris 16 (fmt.Println(...)): main mencetak pesan penutup sementara goroutine sapa masih tertidur 500 milidetik.
  5. Baris 17 (akhir main): main kembali, runtime menghentikan program, dan goroutine sapa terbunuh sebelum sempat mencetak sapaan — itulah sebabnya output hanya berisi satu baris.

Output satu baris tersebut stabil karena program keluar jauh sebelum tidur 500 milidetik selesai. Tanpa jeda itu, goroutine kadang sempat mencetak dan kadang tidak — urutan eksekusi goroutine tidak boleh dijadikan dasar logika program.

Ketika hasil kerja goroutine dibutuhkan, program harus menunggu secara eksplisit. Alat standarnya adalah sync.WaitGroup: sebuah counter tugas. Method Add(n) menambah n tugas, Done() milik tiap goroutine menandai satu tugas selesai, dan Wait() memblokir pemanggil sampai counter mencapai nol. Berkas goroutine-tunggu-demo.go memakai pola lengkapnya:

go
package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

func main() {
	fmt.Println("goroutine di awal    :", runtime.NumGoroutine())

	hasil := make([]string, 3)
	var wg sync.WaitGroup
	wg.Add(3)

	for i := range 3 {
		go func() {
			defer wg.Done()
			time.Sleep(50 * time.Millisecond) // simulasi kerja
			hasil[i] = fmt.Sprintf("tugas %d selesai", i+1)
		}()
	}

	fmt.Println("goroutine saat kerja :", runtime.NumGoroutine())

	wg.Wait()

	for _, h := range hasil {
		fmt.Println(h)
	}
}

Output yang Dihasilkan:

text
goroutine di awal    : 1
goroutine saat kerja : 4
tugas 1 selesai
tugas 2 selesai
tugas 3 selesai

Mari kita telaah baris demi baris program di atas:

  1. Baris 11 (runtime.NumGoroutine()): menghitung goroutine yang sedang ada; di awal program hanya ada main, hasilnya 1.
  2. Baris 14–15 (var wg ... wg.Add(3)): counter WaitGroup dinaikkan menjadi 3 sebelum satu pun goroutine dimulai, sesuai posisi Add yang wajib mendahului go.
  3. Baris 18 (go func() {...}()): memulai goroutine anonim; closure menangkap variabel loop i yang sejak Go 1.22 dibuat ulang pada setiap iterasi sehingga tiap goroutine memegang nilai i miliknya.
  4. Baris 19 (defer wg.Done()): menjadwalkan Done() agar counter berkurang apa pun jalannya goroutine, sekaligus memastikan pemanggil tidak lupa memanggilnya.
  5. Baris 21 (hasil[i] = ...): tiap goroutine menulis pada slot indeksnya sendiri — tidak ada dua goroutine menulis lokasi memori yang sama, jadi tidak terjadi data race.
  6. Baris 27 (wg.Wait()): memblokir main sampai ketiga goroutine selesai; setelah baris ini aman membaca slice hasil secara utuh.
  7. Baris 29–31 (for _, h := range hasil): hasil dicetak terurut menurut urutan slice, bukan urutan goroutine selesai.

Perhatikan bahwa ketiga hasil tercetak terurut: goroutine boleh selesai dalam urutan apa pun, tetapi program mengumpulkan hasil ke slot indeks masing-masing lalu mencetaknya setelah Wait. Setelah wg.Wait() kembali, seluruh pekerjaan tuntas dan slice hasil terisi penuh. Kami sengaja tidak mencetak runtime.NumGoroutine() tepat setelah Wait — goroutine yang baru memanggil Done() boleh masih berada di tengah proses keluarnya, sehingga angka pada titik itu belum pasti. Pembahasan sync.WaitGroup yang lebih dalam menanti di chapter sync.WaitGroup .

Goroutine Ringan, Jumlahnya Bebas #

Satu goroutine memulai hidup dengan stack kecil — beberapa kilobyte — yang tumbuh otomatis ketika dibutuhkan, jauh lebih murah daripada thread sistem operasi. Penjadwalannya juga terjadi di dalam runtime Go (model M:N pada chapter Concurrency dan Parallelism ), bukan lewat sistem operasi yang mahal. Jumlah goroutine yang wajar karenanya berada di orde ribuan, bukan lusinan; pertumbuhan stack goroutine dibedah di chapter Stack dan Heap .

Referensi #

Goroutine sudah berjalan, tetapi program contoh di atas masih memakai slot memori untuk menampung hasil. Go menawarkan alat komunikasi yang lebih idiomatik — channel — yang menjadi fokus chapter berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

goroutine-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"time"
)

func sapa(nama string) {
	time.Sleep(500 * time.Millisecond) // simulasi kerja yang lama
	fmt.Println("Halo,", nama)
}

func main() {
	go sapa("Budi")

	fmt.Println("program selesai")
}

goroutine-tunggu-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

func main() {
	fmt.Println("goroutine di awal    :", runtime.NumGoroutine())

	hasil := make([]string, 3)
	var wg sync.WaitGroup
	wg.Add(3)

	for i := range 3 {
		go func() {
			defer wg.Done()
			time.Sleep(50 * time.Millisecond) // simulasi kerja
			hasil[i] = fmt.Sprintf("tugas %d selesai", i+1)
		}()
	}

	fmt.Println("goroutine saat kerja :", runtime.NumGoroutine())

	wg.Wait()

	for _, h := range hasil {
		fmt.Println(h)
	}
}