context.WithDeadline untuk timeout di Go
Pattern untuk batasi waktu eksekusi function di Go. Cancel propagate ke nested goroutine, DB query, HTTP call. Idiom yang harus dihafal.
Dipublikasikan 18 Mei 2026
Go gak punya try/finally seperti Java. Cara cancel long-running operation: context.Context. Pattern timeout adalah idiom yang harus jadi reflex.
Kode
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
"time"
)
// Process dengan timeout 5 detik
func processWithTimeout(parentCtx context.Context) error {
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel() // PENTING: release resource
return doWork(ctx)
}
func doWork(ctx context.Context) error {
// Cek timeout di mid-operation
select {
case <-ctx.Done():
return ctx.Err() // context.DeadlineExceeded atau context.Canceled
default:
}
// HTTP call dengan ctx
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("http: %w", err)
}
defer resp.Body.Close()
// DB query dengan ctx
var db *sql.DB
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE created_at > $1", time.Now().Add(-time.Hour))
if err != nil {
return fmt.Errorf("db: %w", err)
}
defer rows.Close()
// Nested goroutine
errCh := make(chan error, 1)
go func() {
errCh <- expensiveCalc(ctx, rows)
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
return ctx.Err()
}
}
func expensiveCalc(ctx context.Context, rows *sql.Rows) error {
for rows.Next() {
// Cek cancellation setiap iterasi
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// ... process row
}
return rows.Err()
}
Pemakaian
func main() {
// Top-level context — biasanya dari main atau HTTP handler
ctx := context.Background()
if err := processWithTimeout(ctx); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Operation timeout")
} else {
fmt.Println("Error:", err)
}
}
}
Cara kerja
WithTimeoutderive context baru dengan deadline.- Setelah 5 detik, deadline reached → ctx.Done() channel di-close.
- Semua function yang receive
ctxcekctx.Done()— kalau closed, return early denganctx.Err(). defer cancel()release resource walaupun timeout belum tercapai.
Kapan dipakai
- HTTP handler: batasi total processing time per request
- Background worker: cancel job kalau parent process shutdown
- DB transaction: rollback kalau timeout
- External API call: don’t wait forever
- Goroutine fan-out: cancel pending worker kalau salah satu fail
Pattern: combine multiple cancel sources
func processJob(parentCtx context.Context, jobID string) error {
// Inherit parent cancellation + add own timeout
ctx, cancel := context.WithTimeout(parentCtx, 30*time.Second)
defer cancel()
// Listen juga ke shutdown signal
sigCtx, sigCancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer sigCancel()
return doJob(sigCtx, jobID)
}
Cancel salah satu dari:
- Parent context canceled
- 30 detik timeout reached
- SIGTERM diterima (graceful shutdown)
Catatan
- defer cancel() SELALU. Tanpa ini, context tetap “live” sampai garbage collected — memory leak di high-throughput service.
- Pass ctx as first parameter by convention (
func F(ctx context.Context, ...) error). - Don’t store context in struct — pass as parameter. Exception: HTTP request, di mana ctx attached ke
*http.Request. - WithValue sparingly — context value bukan untuk passing dependency. Pakai struct field untuk dep.
Gotcha: leak goroutine yang gak check ctx
// BAD — goroutine ini tidak akan stop walaupun ctx canceled
go func() {
for {
doSomething()
time.Sleep(time.Second)
}
}()
// GOOD — periodic check ctx
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
doSomething()
}
}
}()
Selalu beri exit path untuk goroutine.
Go 1.23+ punya
context.WithCancelCause(parent, cause)yang attach reason ke cancellation. Berguna untuk distinguish “timeout vs user-canceled vs upstream-failed”.
# tags
gocontexttimeoutcancellation
Ditulis oleh Asti Larasati · 18 Mei 2026