Go's Killer Feature: Concurrency
Jika ada satu alasan kenapa saya memilih Go untuk backend SaaS, itu adalah concurrency model-nya. Goroutines yang ringan dan channels yang elegant membuat menulis concurrent code menjadi menyenangkan.
Saya pernah membangun service yang memproses 10K+ events per detik secara concurrent dengan mudah di Go.
Goroutines: Lebih Ringan dari yang Anda Pikir
Goroutine itu lightweight thread yang di-manage oleh Go runtime. Satu goroutine hanya butuh ~2KB stack dibandingkan ~1MB untuk OS thread. Anda bisa menjalankan ratusan ribu goroutines bersamaan.
package main
import (
"fmt"
"time"
)
func main() {
go func() {
fmt.Println("Hello from goroutine!")
}()
for i := 0; i < 5; i++ {
go func(id int) {
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished\n", id)
}(i)
}
time.Sleep(2 * time.Second)
}
Channels: Communication Between Goroutines
Channels adalah cara goroutines berkomunikasi. Ingat filosofi Go: "Don't communicate by sharing memory; share memory by communicating."
func main() {
ch := make(chan string)
go func() {
ch <- "data from worker"
}()
msg := <-ch
fmt.Println(msg)
buffered := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
buffered <- i
}
close(buffered)
}()
for val := range buffered {
fmt.Println(val)
}
}
Real-World Pattern: Fan-Out Fan-In
Ini pattern yang saya pakai untuk parallel processing di SaaS:
func processItems(items []Item) []Result {
results := make(chan Result, len(items))
for i := 0; i < 4; i++ {
go func(workerID int) {
for _, item := range items {
result := process(item)
results <- result
}
}(i)
}
var allResults []Result
for i := 0; i < len(items); i++ {
allResults = append(allResults, <-results)
}
return allResults
}
Common Pitfalls
- Data race Selalu pakai go run -race saat development
- Deadlock Pastikan semua goroutine bisa mengirim/menerima dari channel
- Resource leak Close channels when done, cancel contexts properly
- Unnecessary concurrency Tidak semua masalah butuh goroutine
Concurrency di Go itu powerful, tapi bukan magic. Gunakan sync.WaitGroup, sync.Mutex, dan context.Context untuk patterns yang lebih complex. Dan selalu: test with race detector enabled.
Context: Mengontrol Goroutine Lifecycle
Context adalah kunci untuk manage goroutine lifecycle di Go. Tanpa proper context handling, Anda akan mengalami goroutine leak yang memakan memory server:
func FetchData(ctx context.Context, urls []string) []string {
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return // Context cancelled, early exit
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
results <- string(body)
}(url)
}
var data []string
for i := 0; i < len(urls); i++ {
select {
case d := <-results:
data = append(data, d)
case <-ctx.Done():
return data // Timeout or cancelled
}
}
return data
}
Dengan context, semua goroutine akan otomatis berhenti ketika timeout atau request dibatalkan. Ini pattern yang harus selalu Anda pakai di production Go code.
sync.Pool: Mengurangi GC Pressure
Di high-throughput applications, object allocation yang berlebihan bisa bikin GC bekerja terlalu keras. Gunakan sync.Pool untuk reuse objects:
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func ProcessRequest(data []byte) []byte {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
buf.Write(data)
return buf.Bytes()
}