Time #

Waktu adalah salah satu konsep yang tampak sederhana tapi penuh jebakan di dunia pemrograman — zona waktu yang berbeda, format tanggal yang tidak konsisten, daylight saving time, presisi nanosecond, perbandingan waktu yang salah, dan scheduling yang tidak akurat. Package time di Go menangani semua kerumitan ini dengan API yang bersih dan konsisten. Ia menyediakan tipe time.Time untuk merepresentasikan momen tertentu, time.Duration untuk interval waktu, fungsi parsing dan formatting dengan pola yang unik, serta Timer dan Ticker untuk scheduling. Memahami package time dengan baik bukan hanya tentang cara mencetak tanggal — ini tentang menghindari bug zona waktu yang halus, mengelola timeout yang benar, dan membangun sistem yang berperilaku deterministik terhadap waktu.

Gambaran Besar Package time #

flowchart TD
    T["package time"] --> Repr["Representasi Waktu"]
    T --> Parse["Parsing & Formatting"]
    T --> Arith["Aritmetika Waktu"]
    T --> Sched["Scheduling"]
    T --> Zone["Zona Waktu"]

    Repr --> R1["time.Time — momen spesifik"]
    Repr --> R2["time.Duration — interval waktu"]
    Repr --> R3["time.Now() — waktu saat ini"]

    Parse --> P1["time.Parse — string → Time"]
    Parse --> P2["t.Format — Time → string"]
    Parse --> P3["time.RFC3339, time.Kitchen, dll"]

    Arith --> A1["t.Add(d) — tambah durasi"]
    Arith --> A2["t.Sub(t2) — selisih dua waktu"]
    Arith --> A3["t.Before / t.After / t.Equal"]

    Sched --> S1["time.Sleep — tunda eksekusi"]
    Sched --> S2["time.After — channel setelah durasi"]
    Sched --> S3["time.NewTimer — one-shot timer"]
    Sched --> S4["time.NewTicker — ticker periodik"]

    Zone --> Z1["time.LoadLocation — muat zona waktu"]
    Zone --> Z2["t.In(loc) — konversi zona"]
    Zone --> Z3["time.UTC / time.Local"]

    style T fill:#4f86c6,color:#fff
    style Repr fill:#e8f5e9
    style Parse fill:#e3f2fd
    style Arith fill:#fff3e0
    style Sched fill:#fce4ec
    style Zone fill:#f3e5f5

time.Time — Merepresentasikan Waktu #

time.Time adalah tipe utama package time. Ia merepresentasikan sebuah momen dalam waktu dengan presisi nanosecond, dan selalu menyimpan informasi zona waktu (timezone) bersama nilainya.

package main

import (
    "fmt"
    "time"
)

func main() {
    // Waktu saat ini — dengan zona waktu lokal
    sekarang := time.Now()
    fmt.Println(sekarang)
    // 2024-03-15 14:30:00.123456789 +0700 WIB

    // Membuat time.Time dari komponen tertentu
    ulangTahun := time.Date(1990, time.March, 15, 0, 0, 0, 0, time.Local)
    fmt.Println(ulangTahun)
    // 1990-03-15 00:00:00 +0700 WIB

    // Waktu dalam UTC
    sekarangUTC := time.Now().UTC()
    fmt.Println(sekarangUTC)
    // 2024-03-15 07:30:00.123456789 +0000 UTC

    // Zero value time.Time — January 1, year 1, 00:00:00 UTC
    var t time.Time
    fmt.Println(t)           // 0001-01-01 00:00:00 +0000 UTC
    fmt.Println(t.IsZero())  // true — berguna untuk memeriksa "belum diset"

    // Unix timestamp — detik sejak 1 Januari 1970 UTC
    sekarang = time.Now()
    fmt.Println(sekarang.Unix())      // 1710491400
    fmt.Println(sekarang.UnixMilli()) // 1710491400123 — milidetik
    fmt.Println(sekarang.UnixNano())  // 1710491400123456789 — nanosecond

    // Dari Unix timestamp
    dariUnix := time.Unix(1710491400, 0)
    fmt.Println(dariUnix)
    // 2024-03-15 14:30:00 +0700 WIB
}

Mengakses Komponen Waktu #

t := time.Now()

// Komponen tanggal
fmt.Println(t.Year())        // 2024
fmt.Println(t.Month())       // March (tipe time.Month)
fmt.Println(int(t.Month()))  // 3
fmt.Println(t.Day())         // 15
fmt.Println(t.Weekday())     // Friday (tipe time.Weekday)
fmt.Println(int(t.Weekday())) // 5 (0=Sunday, 6=Saturday)

// Komponen waktu
fmt.Println(t.Hour())        // 14
fmt.Println(t.Minute())      // 30
fmt.Println(t.Second())      // 0
fmt.Println(t.Nanosecond())  // 123456789

// Hari ke-berapa dalam tahun (1-365/366)
fmt.Println(t.YearDay())     // 75

// Week number ISO 8601
tahun, minggu := t.ISOWeek()
fmt.Printf("Tahun %d, Minggu ke-%d\n", tahun, minggu)

// Date dan Clock dalam satu panggilan
tahun2, bulan, hari := t.Date()
jam, menit, detik := t.Clock()
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n",
    tahun2, bulan, hari, jam, menit, detik)

Parsing dan Formatting #

Go menggunakan pendekatan unik untuk format waktu: alih-alih simbol seperti YYYY-MM-DD, Go menggunakan waktu referensi spesifikMon Jan 2 15:04:05 MST 2006. Setiap komponen format diwakili oleh nilai dari waktu referensi ini.

flowchart LR
    subgraph Ref["Waktu Referensi Go"]
        direction TB
        R1["2006 → tahun\n(tahun ke-6 dalam urutan 1 2 3 4 5 6)"]
        R2["01 → bulan\n(01=Januari)"]
        R3["02 → hari\n(02=tanggal 2)"]
        R4["15 → jam 24h\n(15:00 = 3pm)"]
        R5["04 → menit\n(04)"]
        R6["05 → detik\n(05)"]
        R7["MST → zona waktu\n(Mountain Standard Time)"]
    end

    subgraph Format["Contoh Format"]
        direction TB
        F1["2006-01-02 → YYYY-MM-DD"]
        F2["02/01/2006 → DD/MM/YYYY"]
        F3["15:04:05 → HH:MM:SS"]
        F4["2006-01-02T15:04:05Z07:00 → RFC3339"]
        F5["Jan 2, 2006 → 'Mar 15, 2024'"]
    end

    Ref --> Format
t := time.Now()

// Format ke string
fmt.Println(t.Format("2006-01-02"))
// 2024-03-15

fmt.Println(t.Format("02/01/2006"))
// 15/03/2024

fmt.Println(t.Format("2006-01-02 15:04:05"))
// 2024-03-15 14:30:00

fmt.Println(t.Format("Monday, 02 January 2006"))
// Friday, 15 March 2024

fmt.Println(t.Format("15:04:05.000"))
// 14:30:00.123 — milidetik

// Konstanta format bawaan
fmt.Println(t.Format(time.RFC3339))
// 2024-03-15T14:30:00+07:00

fmt.Println(t.Format(time.RFC3339Nano))
// 2024-03-15T14:30:00.123456789+07:00

fmt.Println(t.Format(time.RFC1123))
// Fri, 15 Mar 2024 14:30:00 WIB

fmt.Println(t.Format(time.Kitchen))
// 2:30PM

fmt.Println(t.Format(time.DateOnly))   // Go 1.20+
// 2024-03-15

fmt.Println(t.Format(time.TimeOnly))   // Go 1.20+
// 14:30:00

Parsing String ke time.Time #

// time.Parse — parse dengan zona waktu dari format string
t1, err := time.Parse("2006-01-02", "2024-03-15")
if err != nil {
    fmt.Println("parse error:", err)
    return
}
fmt.Println(t1) // 2024-03-15 00:00:00 +0000 UTC
// PERHATIAN: zona waktu adalah UTC jika tidak ada di format!

// time.ParseInLocation — parse dengan zona waktu eksplisit
loc, _ := time.LoadLocation("Asia/Jakarta")
t2, err := time.ParseInLocation("2006-01-02 15:04:05",
    "2024-03-15 14:30:00", loc)
if err != nil {
    fmt.Println("parse error:", err)
    return
}
fmt.Println(t2) // 2024-03-15 14:30:00 +0700 WIB

// Parse RFC3339 — format yang paling dianjurkan untuk API
t3, err := time.Parse(time.RFC3339, "2024-03-15T14:30:00+07:00")
fmt.Println(t3) // 2024-03-15 14:30:00 +0700 +0700

// ANTI-PATTERN: asumsikan format tanpa validasi
func parseHari(s string) time.Time {
    t, _ := time.Parse("2006-01-02", s) // abaikan error — berbahaya!
    return t // mengembalikan zero value jika parse gagal, tanpa peringatan
}

// BENAR: selalu periksa error parsing
func parseHariBaik(s string) (time.Time, error) {
    t, err := time.Parse("2006-01-02", s)
    if err != nil {
        return time.Time{}, fmt.Errorf("parseHari %q: %w", s, err)
    }
    return t, nil
}

Format untuk Indonesia #

// Nama bulan dan hari dalam Bahasa Indonesia
var namaBulan = []string{
    "", "Januari", "Februari", "Maret", "April", "Mei", "Juni",
    "Juli", "Agustus", "September", "Oktober", "November", "Desember",
}

var namaHari = []string{
    "Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu",
}

func formatIndonesia(t time.Time) string {
    return fmt.Sprintf("%s, %d %s %d",
        namaHari[t.Weekday()],
        t.Day(),
        namaBulan[t.Month()],
        t.Year(),
    )
}

func formatWaktuIndonesia(t time.Time) string {
    return fmt.Sprintf("%s pukul %02d.%02d WIB",
        formatIndonesia(t),
        t.Hour(),
        t.Minute(),
    )
}

t := time.Now()
fmt.Println(formatIndonesia(t))
// Jumat, 15 Maret 2024

fmt.Println(formatWaktuIndonesia(t))
// Jumat, 15 Maret 2024 pukul 14.30 WIB

time.Duration — Merepresentasikan Interval #

time.Duration adalah int64 yang merepresentasikan interval waktu dalam nanosecond. Go menyediakan konstanta untuk satuan yang umum digunakan.

// Konstanta durasi
fmt.Println(time.Nanosecond)   // 1ns
fmt.Println(time.Microsecond)  // 1µs
fmt.Println(time.Millisecond)  // 1ms
fmt.Println(time.Second)       // 1s
fmt.Println(time.Minute)       // 1m0s
fmt.Println(time.Hour)         // 1h0m0s

// Membuat durasi
setengan := 30 * time.Minute         // 30m0s
satuJamSetengah := 90 * time.Minute  // 1h30m0s
tigaHari := 3 * 24 * time.Hour       // 72h0m0s

// Konversi durasi ke satuan numerik
d := 2*time.Hour + 30*time.Minute + 15*time.Second
fmt.Println(d)                    // 2h30m15s
fmt.Println(d.Hours())            // 2.504166... (float64)
fmt.Println(d.Minutes())          // 150.25 (float64)
fmt.Println(d.Seconds())          // 9015 (float64)
fmt.Println(d.Milliseconds())     // 9015000 (int64)
fmt.Println(d.Nanoseconds())      // 9015000000000 (int64)

// Parsing durasi dari string
d2, err := time.ParseDuration("2h30m15s")
d3, err := time.ParseDuration("1.5h")
d4, err := time.ParseDuration("300ms")
d5, err := time.ParseDuration("2.5s")

// Pembulatan dan truncate
d6 := 2*time.Hour + 37*time.Minute + 42*time.Second
fmt.Println(d6.Round(time.Minute))    // 2h38m0s — dibulatkan ke menit
fmt.Println(d6.Truncate(time.Minute)) // 2h37m0s — dipotong ke menit
fmt.Println(d6.Abs())                 // 2h37m42s — nilai absolut (Go 1.19+)

Aritmetika Waktu #

Operasi aritmetika pada waktu — menambah, mengurangi, dan membandingkan — adalah hal yang sangat sering dilakukan dan mudah salah jika tidak hati-hati.

flowchart LR
    T1["time.Time\nt1"] 
    T2["time.Time\nt2"]
    D["time.Duration\nd"]

    T1 -- "t1.Add(d)" --> T3["time.Time\nt1 + d"]
    T1 -- "t1.Sub(t2)" --> D2["time.Duration\nt1 - t2"]
    T2 -- "t2.Add(-d)" --> T4["time.Time\nt2 - d"]
    T1 -- "t1.Before(t2)" --> B["bool"]
    T1 -- "t1.After(t2)" --> B
    T1 -- "t1.Equal(t2)" --> B

    style T1 fill:#e3f2fd
    style T2 fill:#e3f2fd
    style D fill:#fff3e0
    style D2 fill:#fff3e0
    style T3 fill:#e8f5e9
    style T4 fill:#e8f5e9
sekarang := time.Now()

// Add — tambahkan durasi ke waktu
besok := sekarang.Add(24 * time.Hour)
sejamLagi := sekarang.Add(time.Hour)
limaMenitLalu := sekarang.Add(-5 * time.Minute)

// AddDate — tambahkan tahun, bulan, hari (lebih natural untuk kalender)
bulanDepan := sekarang.AddDate(0, 1, 0)   // +1 bulan
tahunDepan := sekarang.AddDate(1, 0, 0)   // +1 tahun
semingguLalu := sekarang.AddDate(0, 0, -7) // -7 hari

// PERHATIAN: AddDate menangani overflow bulan secara otomatis
// 31 Januari + 1 bulan = 3 Maret (bukan 31 Februari)
akhirJan := time.Date(2024, time.January, 31, 0, 0, 0, 0, time.UTC)
fmt.Println(akhirJan.AddDate(0, 1, 0))
// 2024-03-02 00:00:00 +0000 UTC (bukan 2024-02-31!)

// Sub — selisih antara dua waktu, menghasilkan Duration
mulai := time.Now()
time.Sleep(100 * time.Millisecond)
selesai := time.Now()
elapsed := selesai.Sub(mulai)
fmt.Printf("Waktu eksekusi: %v\n", elapsed) // 100.123ms

// Since dan Until — shortcut yang sangat umum dipakai
fmt.Println(time.Since(mulai))   // setara dengan time.Now().Sub(mulai)
fmt.Println(time.Until(besok))   // setara dengan besok.Sub(time.Now())

// Perbandingan waktu
t1 := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC)

fmt.Println(t1.Before(t2)) // true
fmt.Println(t1.After(t2))  // false
fmt.Println(t1.Equal(t2))  // false

// ANTI-PATTERN: bandingkan dengan == — tidak memperhitungkan zona waktu
tJakarta := time.Date(2024, 1, 1, 7, 0, 0, 0, loc) // 07:00 WIB
tUTC := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) // 00:00 UTC
fmt.Println(tJakarta == tUTC)        // false! padahal momen yang sama
fmt.Println(tJakarta.Equal(tUTC))    // true  — ini yang benar

Zona Waktu #

Zona waktu adalah sumber bug yang paling sering tidak terdeteksi pada sistem yang berhubungan dengan waktu. Prinsip dasarnya sederhana: selalu simpan dan transfer waktu dalam UTC, konversi ke zona lokal hanya saat menampilkan ke pengguna.

flowchart TD
    subgraph Prinsip["Prinsip Zona Waktu"]
        direction LR
        P1["Simpan di DB\ndalam UTC"]
        P2["Transfer via API\ndalam RFC3339 + offset"]
        P3["Tampilkan ke user\ndalam zona lokal"]
        P1 --> P2 --> P3
    end

    subgraph Go["Di Go"]
        direction TB
        G1["time.Now().UTC()\nuntuk operasi internal"]
        G2["t.In(loc)\nuntuk konversi tampilan"]
        G3["time.LoadLocation('Asia/Jakarta')\nuntuk zona Indonesia"]
    end

    Prinsip --> Go
// Muat zona waktu dari database IANA
// Membutuhkan tzdata — tersedia di semua OS modern
loc, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
    fmt.Fprintf(os.Stderr, "gagal muat timezone: %v\n", err)
    return
}

// Zona waktu lain yang umum di Indonesia
locWITA, _ := time.LoadLocation("Asia/Makassar") // WIB+1
locWIT, _ := time.LoadLocation("Asia/Jayapura")  // WIB+2

// Zona tetap (fixed offset) — tidak terpengaruh DST
wib := time.FixedZone("WIB", 7*60*60)   // UTC+7
wita := time.FixedZone("WITA", 8*60*60) // UTC+8
wit := time.FixedZone("WIT", 9*60*60)   // UTC+9

// Konversi antar zona
sekarang := time.Now().UTC()
fmt.Println("UTC  :", sekarang.Format("15:04:05 MST"))
fmt.Println("WIB  :", sekarang.In(loc).Format("15:04:05 MST"))
fmt.Println("Tokyo:", sekarang.In(mustLoadLoc("Asia/Tokyo")).Format("15:04:05 MST"))
fmt.Println("NYC  :", sekarang.In(mustLoadLoc("America/New_York")).Format("15:04:05 MST"))

func mustLoadLoc(name string) *time.Location {
    loc, err := time.LoadLocation(name)
    if err != nil {
        panic(err)
    }
    return loc
}

// Membuat time.Time di zona tertentu
deadline := time.Date(2024, 3, 15, 17, 0, 0, 0, loc) // 17:00 WIB
fmt.Println(deadline.UTC()) // 2024-03-15 10:00:00 +0000 UTC
time.Local menggunakan zona waktu sistem operasi yang menjalankan program — ini bisa berbeda antara mesin developer dan server produksi. Di container yang berbasis Linux, time.Local biasanya UTC kecuali dikonfigurasi secara eksplisit. Selalu gunakan time.LoadLocation dengan nama zona yang eksplisit, atau time.UTC untuk operasi internal.

Menangani DST (Daylight Saving Time) #

Indonesia tidak menerapkan DST, tapi sistem yang berinteraksi dengan zona waktu lain perlu berhati-hati:

// DST menyebabkan "jam yang hilang" dan "jam yang ganda"
nyLoc, _ := time.LoadLocation("America/New_York")

// "Jam yang hilang" — 2:30 AM tidak ada saat spring forward
// Go menangani ini secara otomatis dengan menyesuaikan ke jam yang valid
jamHilang := time.Date(2024, 3, 10, 2, 30, 0, 0, nyLoc)
fmt.Println(jamHilang) // otomatis disesuaikan

// Hitung selisih waktu melintasi DST — gunakan Sub, bukan AddDate
sebelumDST := time.Date(2024, 3, 9, 12, 0, 0, 0, nyLoc)
setelahDST := time.Date(2024, 3, 11, 12, 0, 0, 0, nyLoc)
// Selisih adalah 47 jam, bukan 48 jam!
fmt.Println(setelahDST.Sub(sebelumDST)) // 47h0m0s

Timer dan Ticker — Scheduling #

time.Timer dan time.Ticker adalah mekanisme untuk mengeksekusi sesuatu di masa depan atau secara periodik. Keduanya menggunakan channel sebagai sinyal.

flowchart TD
    subgraph Timer["time.Timer — One-shot"]
        T1["time.NewTimer(d)"] --> T2["Tunggu..."]
        T2 --> T3["<-timer.C\nterima sekali setelah d"]
        T1 --> T4["timer.Stop()\nbatalkan sebelum fire"]
        T1 --> T5["timer.Reset(d)\natur ulang durasi"]
    end

    subgraph Ticker["time.Ticker — Periodik"]
        K1["time.NewTicker(d)"] --> K2["Tick..."]
        K2 --> K3["<-ticker.C\nterima setiap d"]
        K3 --> K2
        K1 --> K4["ticker.Stop()\nhentikan ticker"]
    end

    subgraph After["Shortcut"]
        A1["time.After(d)\nreturn <-chan Time"]
        A2["time.Sleep(d)\nblokir goroutine"]
        A3["time.AfterFunc(d, f)\njalankan f setelah d"]
    end

    style Timer fill:#e8f5e9
    style Ticker fill:#e3f2fd
    style After fill:#fff3e0

time.Sleep — Menunda Eksekusi #

// Tunda eksekusi goroutine saat ini
fmt.Println("Mulai...")
time.Sleep(2 * time.Second)
fmt.Println("2 detik berlalu")

// Sleep dengan durasi dari string
durasi, _ := time.ParseDuration("500ms")
time.Sleep(durasi)

time.Timer — One-shot Timer #

// Timer yang fire sekali setelah durasi tertentu
timer := time.NewTimer(3 * time.Second)

fmt.Println("Menunggu timer...")
<-timer.C // blokir sampai timer fire
fmt.Println("Timer fired!")

// Membatalkan timer sebelum fire
timer2 := time.NewTimer(10 * time.Second)
go func() {
    time.Sleep(2 * time.Second)
    // Hentikan timer — kembalikan true jika berhasil dihentikan
    if timer2.Stop() {
        fmt.Println("Timer dibatalkan")
    }
}()

select {
case <-timer2.C:
    fmt.Println("Timer fired")
case <-time.After(5 * time.Second):
    fmt.Println("Timeout menunggu timer")
}

// ANTI-PATTERN: Reset tanpa drain channel terlebih dahulu
timer3 := time.NewTimer(time.Second)
timer3.Stop()
timer3.Reset(2 * time.Second) // mungkin ada nilai di channel!

// BENAR: Stop lalu drain sebelum Reset
timer4 := time.NewTimer(time.Second)
if !timer4.Stop() {
    <-timer4.C // drain channel jika sudah fire
}
timer4.Reset(2 * time.Second)

time.Ticker — Eksekusi Periodik #

// Ticker yang berdetak setiap interval
ticker := time.NewTicker(time.Second)
defer ticker.Stop() // WAJIB: stop ticker saat selesai, hindari goroutine leak

batas := time.After(5 * time.Second)

for {
    select {
    case t := <-ticker.C:
        fmt.Println("Tick:", t.Format("15:04:05"))
    case <-batas:
        fmt.Println("Selesai")
        return
    }
}
// Output:
// Tick: 14:30:01
// Tick: 14:30:02
// Tick: 14:30:03
// Tick: 14:30:04
// Selesai
Selalu panggil ticker.Stop() setelah selesai menggunakan Ticker — biasanya dengan defer ticker.Stop(). Ticker yang tidak dihentikan akan terus berjalan dan goroutine yang membaca dari ticker.C tidak akan pernah selesai (goroutine leak). Ini adalah salah satu sumber memory leak yang paling umum di program Go yang menggunakan Ticker.

time.AfterFunc — Eksekusi Asinkron #

// Jalankan fungsi di goroutine baru setelah durasi tertentu
timer := time.AfterFunc(5*time.Second, func() {
    fmt.Println("Ini dijalankan di goroutine terpisah setelah 5 detik!")
    // Hati-hati dengan akses ke shared state — perlu sinkronisasi
})

// Batalkan jika perlu
timer.Stop()

// Pola: retry dengan backoff eksponensial
func retryDenganBackoff(fn func() error, maxRetry int) error {
    var err error
    for i := 0; i < maxRetry; i++ {
        if err = fn(); err == nil {
            return nil
        }
        if i < maxRetry-1 {
            backoff := time.Duration(1<<uint(i)) * 100 * time.Millisecond
            // cap backoff maksimal 30 detik
            if backoff > 30*time.Second {
                backoff = 30 * time.Second
            }
            fmt.Printf("Retry %d/%d setelah %v: %v\n", i+1, maxRetry, backoff, err)
            time.Sleep(backoff)
        }
    }
    return fmt.Errorf("gagal setelah %d retry: %w", maxRetry, err)
}

Timeout dengan time.After dan context #

Timeout adalah pola yang sangat umum di aplikasi Go — membatasi berapa lama sebuah operasi boleh berjalan. Ada dua cara utama: time.After untuk kasus sederhana, dan context.WithTimeout untuk integrasi yang lebih baik dengan stack Go.

sequenceDiagram
    participant Main as Goroutine Utama
    participant Op as Operasi (DB, HTTP, dll)
    participant Timer as time.After / context

    Main->>Op: Mulai operasi
    Main->>Timer: Set timeout (misalnya 5 detik)

    alt Operasi selesai tepat waktu
        Op-->>Main: Hasil
        Main->>Timer: Stop timer (jika Timer)
    else Timeout lebih dulu
        Timer-->>Main: Sinyal timeout
        Main->>Op: Cancel / abaikan hasil terlambat
        Main-->>Main: Return error timeout
    end
import (
    "context"
    "fmt"
    "time"
)

// Pola 1: time.After — untuk goroutine sederhana
func operasiDenganTimeout(durasi time.Duration) error {
    hasil := make(chan string, 1)

    go func() {
        // Simulasi operasi yang memakan waktu
        time.Sleep(3 * time.Second)
        hasil <- "selesai"
    }()

    select {
    case r := <-hasil:
        fmt.Println("Berhasil:", r)
        return nil
    case <-time.After(durasi):
        return fmt.Errorf("operasi timeout setelah %v", durasi)
    }
}

// Pola 2: context.WithTimeout — cara yang dianjurkan untuk produksi
func queryDenganTimeout(ctx context.Context, id int) (string, error) {
    // Buat context baru dengan timeout 5 detik
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel() // WAJIB: selalu cancel untuk bebaskan resource

    // Semua operasi yang mendukung context (DB, HTTP client, dll)
    // akan otomatis dibatalkan saat context timeout
    hasil := make(chan string, 1)
    go func() {
        // Simulasi DB query
        time.Sleep(2 * time.Second)
        hasil <- fmt.Sprintf("data untuk id %d", id)
    }()

    select {
    case r := <-hasil:
        return r, nil
    case <-ctx.Done():
        return "", fmt.Errorf("queryDenganTimeout: %w", ctx.Err())
    }
}

// Contoh penggunaan
func main() {
    ctx := context.Background()

    data, err := queryDenganTimeout(ctx, 42)
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: %v\n", err)
        return
    }
    fmt.Println(data)
}

Mengukur Waktu Eksekusi #

Mengukur berapa lama sebuah fungsi atau blok kode berjalan adalah kebutuhan umum untuk profiling dan optimasi.

// Pola 1: manual dengan time.Now dan time.Since
func ukurWaktu(nama string, fn func()) {
    mulai := time.Now()
    fn()
    fmt.Printf("%s selesai dalam %v\n", nama, time.Since(mulai))
}

// Pola 2: defer untuk automatic timing
func prosesData(data []int) {
    defer func(mulai time.Time) {
        fmt.Printf("prosesData(%d items) took %v\n",
            len(data), time.Since(mulai))
    }(time.Now()) // time.Now() dievaluasi saat defer dideklarasikan!

    // ... logika pemrosesan
    time.Sleep(100 * time.Millisecond)
}

// Pola 3: untuk benchmark dalam test
func BenchmarkProses(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // Fungsi yang diukur
        prosesSesuatu()
    }
}

// Contoh penggunaan manual timer
mulai := time.Now()

for i := 0; i < 1000000; i++ {
    // operasi
}

elapsed := time.Since(mulai)
fmt.Printf("1 juta iterasi: %v (%.2f ns/op)\n",
    elapsed,
    float64(elapsed.Nanoseconds())/1000000)

Pola Penggunaan di Produksi #

Scheduler Sederhana #

// Jalankan tugas setiap hari pukul 02:00
func jadwalkanHarian(jam, menit int, tugas func()) {
    for {
        sekarang := time.Now()
        // Hitung waktu berikutnya
        berikutnya := time.Date(
            sekarang.Year(), sekarang.Month(), sekarang.Day(),
            jam, menit, 0, 0, sekarang.Location(),
        )

        // Jika jam target sudah lewat hari ini, jadwalkan untuk besok
        if berikutnya.Before(sekarang) {
            berikutnya = berikutnya.Add(24 * time.Hour)
        }

        selisih := time.Until(berikutnya)
        fmt.Printf("Tugas berikutnya dalam %v (pukul %02d:%02d)\n",
            selisih.Round(time.Minute), jam, menit)

        <-time.After(selisih)
        go tugas() // jalankan di goroutine agar tidak blokir scheduler
    }
}

Rate Limiter Sederhana dengan Ticker #

// Batasi eksekusi ke N operasi per detik
func buatRateLimiter(opsPerDetik int) <-chan time.Time {
    return time.NewTicker(time.Second / time.Duration(opsPerDetik)).C
}

func main() {
    // Maksimal 5 request per detik
    rateLimiter := buatRateLimiter(5)

    permintaan := []string{"req1", "req2", "req3", "req4", "req5", "req6", "req7"}

    for _, req := range permintaan {
        <-rateLimiter // tunggu giliran
        go prosesPermintaan(req)
    }
}

Cache dengan Expiry #

import (
    "sync"
    "time"
)

type CacheItem struct {
    Nilai     any
    Kadaluarsa time.Time
}

type Cache struct {
    mu   sync.RWMutex
    data map[string]CacheItem
}

func NewCache() *Cache {
    c := &Cache{data: make(map[string]CacheItem)}
    // Bersihkan item kadaluarsa setiap menit
    go c.bersihkanPeriodik()
    return c
}

func (c *Cache) Set(key string, nilai any, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = CacheItem{
        Nilai:     nilai,
        Kadaluarsa: time.Now().Add(ttl),
    }
}

func (c *Cache) Get(key string) (any, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    item, ada := c.data[key]
    if !ada {
        return nil, false
    }
    // Periksa apakah sudah kadaluarsa
    if time.Now().After(item.Kadaluarsa) {
        return nil, false
    }
    return item.Nilai, true
}

func (c *Cache) bersihkanPeriodik() {
    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()

    for range ticker.C {
        c.mu.Lock()
        sekarang := time.Now()
        for key, item := range c.data {
            if sekarang.After(item.Kadaluarsa) {
                delete(c.data, key)
            }
        }
        c.mu.Unlock()
    }
}

Mengukur Umur Sesuatu #

// Hitung umur dalam bahasa manusia
func umurManusiawi(t time.Time) string {
    selisih := time.Since(t)

    switch {
    case selisih < time.Minute:
        return "baru saja"
    case selisih < time.Hour:
        menit := int(selisih.Minutes())
        return fmt.Sprintf("%d menit yang lalu", menit)
    case selisih < 24*time.Hour:
        jam := int(selisih.Hours())
        return fmt.Sprintf("%d jam yang lalu", jam)
    case selisih < 7*24*time.Hour:
        hari := int(selisih.Hours() / 24)
        return fmt.Sprintf("%d hari yang lalu", hari)
    case selisih < 30*24*time.Hour:
        minggu := int(selisih.Hours() / 24 / 7)
        return fmt.Sprintf("%d minggu yang lalu", minggu)
    case selisih < 365*24*time.Hour:
        bulan := int(selisih.Hours() / 24 / 30)
        return fmt.Sprintf("%d bulan yang lalu", bulan)
    default:
        tahun := int(selisih.Hours() / 24 / 365)
        return fmt.Sprintf("%d tahun yang lalu", tahun)
    }
}

// Contoh penggunaan
postTime := time.Now().Add(-3 * time.Hour)
fmt.Println(umurManusiawi(postTime)) // 3 jam yang lalu

postTime2 := time.Now().Add(-2 * 24 * time.Hour)
fmt.Println(umurManusiawi(postTime2)) // 2 hari yang lalu

Kapan Beralih ke Alternatif #

Tetap gunakan time jika:
  ✓ Semua operasi dasar tanggal, waktu, dan durasi
  ✓ Parsing dan formatting waktu dalam berbagai format
  ✓ Scheduling periodik dengan Ticker dan one-shot dengan Timer
  ✓ Timeout dan deadline dengan time.After atau context.WithTimeout
  ✓ Konversi antar zona waktu dengan LoadLocation

Pertimbangkan library eksternal jika:
  ✗ Kalkulasi kalender yang kompleks (bulan Hijriyah, Jawa, dll)
     → tidak ada library standard untuk ini
  ✗ Parsing tanggal dari berbagai format yang tidak diketahui
     → dateparse (github.com/araddon/dateparse)
  ✗ Human-readable duration parsing ("in 2 hours", "yesterday")
     → naturaldate atau library sejenis
  ✗ Cron expression scheduling ("0 2 * * *")
     → robfig/cron
  ✗ Manipulasi kalender bisnis (hari kerja, hari libur)
     → tidak ada di stdlib, perlu logika kustom atau library

Ringkasan #

  • time.Time selalu menyimpan zona waktu — dua time.Time yang berbeda zona tapi momen yang sama akan Equal tapi tidak ==. Selalu gunakan .Equal() untuk perbandingan, bukan ==.
  • Format Go menggunakan waktu referensi Mon Jan 2 15:04:05 MST 2006 — bukan YYYY-MM-DD. Hafalkan: 2006 (tahun), 01 (bulan), 02 (hari), 15 (jam), 04 (menit), 05 (detik).
  • time.RFC3339 adalah format terbaik untuk API — gunakan ini saat serialisasi waktu ke JSON atau HTTP response agar mudah di-parse oleh sistem manapun.
  • Selalu periksa error dari time.Parse — parse yang gagal mengembalikan zero value tanpa panik, menyebabkan bug tersembunyi yang sulit dilacak.
  • time.ParseInLocation bukan time.Parse saat input tidak menyertakan zona waktu — time.Parse mengasumsikan UTC, yang sering bukan yang diinginkan.
  • defer ticker.Stop() wajib setelah time.NewTicker — Ticker yang tidak dihentikan adalah goroutine leak yang lambat tapi pasti menguras memori.
  • time.Since(t) adalah shortcut untuk time.Now().Sub(t) — gunakan ini untuk mengukur elapsed time dengan ringkas.
  • Simpan waktu dalam UTC di database dan transfer via API — konversi ke zona lokal hanya saat menampilkan ke pengguna.
  • context.WithTimeout lebih baik dari time.After untuk timeout di produksi karena timeout di-propagate ke semua operasi downstream yang mendukung context.
  • AddDate menangani overflow bulan secara otomatis — 31 Januari + 1 bulan menghasilkan 3 Maret, bukan error atau tanggal tidak valid.

← Sebelumnya: Os   Berikutnya: Strconv →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact