ข้ามไปยังเนื้อหา

time

Package time มี type หลักสองตัว:

  • time.Time — จุดหนึ่งในเวลาที่มีความละเอียดระดับ nanosecond พกพา location (timezone) เสมอ
  • time.Duration — จำนวนเต็ม 64-bit แบบมีเครื่องหมายที่แทนช่วงเวลาเป็น nanosecond ค่าคงที่ duration อย่าง time.Hour, time.Minute, time.Second และ time.Millisecond ทำให้การคำนวณอ่านง่าย
d := 2*time.Hour + 30*time.Minute + 15*time.Second
fmt.Println(d) // 2h30m15s

ในโค้ด production เรียก time.Now() เพื่อได้เวลาปัจจุบัน สำหรับ test และตัวอย่างที่ deterministic ให้ใช้ time.Date:

t := time.Date(2024, 1, 2, 15, 4, 5, 0, time.UTC)
// year, month, day, hour, min, sec, nanosec, location

ค่า time.Month (time.January ถึง time.December) เป็น typed constant; สามารถส่ง integer literal (1–12) แทนได้เพราะ assign เข้า time.Month ได้

time.Now() อ่านทั้ง wall clock และในแพลตฟอร์มส่วนใหญ่ อ่าน monotonic clock ด้วย แล้วเก็บทั้งคู่ไว้ในค่า time.Time การอ่านค่า monotonic เดินหน้าอย่างเดียวเสมอ แม้ wall clock ของ OS จะถูกปรับด้วย NTP หรือแก้มือ — นี่คือสิ่งที่ Since และ Sub ใช้พอดี การวัด duration ที่ผ่านไปจึงปลอดภัยจาก clock ที่กระโดด

การอ่านค่าคู่นี้ก็เป็นเหตุผลที่ == กับ .Equal() อาจไม่ตรงกันแม้จะดูเหมือนเป็นจุดเวลาเดียวกัน:

a := time.Now()
b := a // identical copy — a == b is true
c := a.Round(0) // Round(0) strips the monotonic reading
fmt.Println(a == b, a.Equal(b)) // true true
fmt.Println(a == c, a.Equal(c)) // false true

== เทียบ struct ทั้งหมด — wall clock, monotonic reading และ location ทั้งหมด — ดังนั้นค่า Time สองตัวที่แทนจุดเวลาเดียวกันอาจเทียบไม่เท่ากันได้ถ้าตัวหนึ่งถูกถอด monotonic reading ออกไป (ผ่าน Round, ผ่าน round-trip ทาง Marshal/Unmarshal หรือผ่าน time.Parse) Equal เทียบแค่จุดเวลาเท่านั้นและไม่สนใจสิ่งเหล่านั้นเลย ใช้ Equal เพื่อเทียบค่า time.Time สองตัวเสมอ ไม่ใช้ == เว้นแต่ต้องการตรวจจับ monotonic reading ที่ถูกถอดออกโดยเฉพาะ

ค่าคงที่ duration (time.Second, time.Millisecond, …) มีไว้เพื่อไม่ให้ integer ธรรมดาถูกเข้าใจผิดว่าเป็น duration — แต่ความผิดพลาดที่เกี่ยวข้องกันสองแบบยังพบบ่อยในโค้ดที่ AI เขียน:

var n time.Duration = 2
d := n * time.Second // fine: n is already a Duration
fmt.Println(d) // 2s

bug จะโผล่เมื่อ n เป็น int ธรรมดาแทนที่จะเป็น time.Duration: n * time.Second เมื่อ n int จะ compile ไม่ผ่านเลยด้วยซ้ำ (“mismatched types”) ซึ่งเป็น Go ที่ปกป้องเราอยู่ — แต่ time.Sleep(n) compile ผ่านสำหรับค่าที่แปลงเป็น int ได้ทุกตัว และ time.Sleep(5) จะ sleep 5 nanosecond ไม่ใช่ 5 วินาที เพราะ Sleep รับ Duration และ 5 คือจำนวน nanosecond เปล่าๆ เมื่อไม่มีการคูณ:

time.Sleep(5) // sleeps 5 nanoseconds — almost certainly not what was intended
time.Sleep(5 * time.Second) // sleeps 5 seconds — the constant makes the unit explicit

เวลาอ่าน diff time.Sleep (หรือ context.WithTimeout, time.NewTimer, …) ที่ถูกเรียกด้วย integer literal เปล่าๆ โดยไม่มีค่าคงที่ time.Xxx คูณอยู่ด้วย ควรมองดูอีกครั้งเสมอ

Go ใช้เวลาอ้างอิงที่เป็นรูปธรรมแทน format code แบบนามธรรมอย่าง YYYY-MM-DD เวลาอ้างอิงคือ:

Mon Jan 2 15:04:05 MST 2006

การ format เวลา คือการเขียนเวลาอ้างอิงในรูปแบบที่ต้องการ:

t.Format("2006-01-02") // "2024-01-02"
t.Format("15:04:05") // "15:04:05"
t.Format("02 Jan 2006 15:04 MST") // "02 Jan 2024 15:04 UTC"
t.Format(time.RFC3339) // "2024-01-02T15:04:05Z"

Go 1.20 เพิ่มค่าคงที่ layout ที่มีชื่อสำหรับรูปแบบที่พบบ่อยที่สุด จึงไม่ต้องพิมพ์เวลาอ้างอิงซ้ำอีก: time.DateTime ("2006-01-02 15:04:05"), time.DateOnly ("2006-01-02") และ time.TimeOnly ("15:04:05")

time.Parse คือด้านตรงข้ามของ Format — รับ layout กับ string:

parsed, err := time.Parse("2006-01-02", "2024-06-15")
// parsed is midnight UTC on June 15, 2024

layout string ต้องตรงกับ component ของเวลาอ้างอิงที่ใช้ — ตัวอักษรเดียวกันในตำแหน่งเดียวกัน

time.Parse คืน Time เป็น UTC เสมอ เมื่อ layout ไม่มี component ของ timezone — มันไม่เคยสมมติ timezone ของเครื่อง เมื่อ input string แทนเวลา local ใน timezone ที่ระบุ (timestamp จาก log file ที่เขียนใน Asia/Bangkok เช่น) ให้ใช้ time.ParseInLocation(layout, value, loc) แทน ซึ่งตีความ string เป็น wall-clock time ใน loc แทนที่จะเป็น UTC:

loc, _ := time.LoadLocation("Asia/Bangkok")
p1, _ := time.Parse("2006-01-02 15:04:05", "2026-09-13 14:30:00") // UTC
p2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2026-09-13 14:30:00", loc) // Asia/Bangkok
// p1 and p2 represent different instants even though the digits are identical
deadline := t.Add(72 * time.Hour) // 3 days later
diff := deadline.Sub(t) // time.Duration: 72h0m0s
before := deadline.Before(t) // false
after := deadline.After(t) // true

Add รับ Duration แล้วคืน Time Sub รับ Time สองตัวแล้วคืน Duration ระหว่างสองค่านั้น

AddDate(years, months, days) เป็นการคำนวณแบบปฏิทิน ไม่ใช่การคำนวณ duration และมันสามารถ overflow เดือนในแบบที่ทำให้โค้ด “เพิ่มหนึ่งเดือน” ที่ AI เขียนแปลกใจได้: time.Date(2026, 1, 31, ...).AddDate(0, 1, 0) ไม่ได้ clamp ไปที่วันสุดท้ายของกุมภาพันธ์ — มันปรับ 31 กุมภาพันธ์ให้เลื่อนไปมีนาคมแทน กลายเป็นวันที่ 3 มีนาคม (2026 ไม่ใช่ปีอธิกสุรทิน) โค้ดใดที่เพิ่มเดือนให้วันที่ใกล้ปลายเดือนต้องจัดการเรื่องนี้อย่างชัดเจน

time.After, time.NewTimer และ time.NewTicker ล้วนส่งค่าผ่าน channel semantics สองอย่างเปลี่ยนไปเมื่อไม่นานมานี้และเป็นพฤติกรรมเดียวใน Go 1.27:

  • Timer channel ไม่มี buffer ตั้งแต่ Go 1.23 (สำหรับ module ที่ go 1.23+) channel ของ timer เป็นแบบ synchronous: ค่าจะถูกเห็นก็ต่อเมื่อ timer ทำงานแล้วเท่านั้น และ t.Stop() / t.Reset() การันตีว่าจะไม่มีค่าเก่าถูกส่งมาอีกหลังจากนั้น — พิธีกรรม “drain channel หลัง Stop” แบบเก่าหายไปแล้ว escape hatch asynctimerchan GODEBUG ถูกลบใน Go 1.27 ทุกโปรแกรมจึงได้พฤติกรรมนี้
  • timer ที่ไม่มีใครอ้างอิงถูก garbage collect time.After ข้างใน select ใน loop ไม่ leak จนกว่ามันจะ fire อีกต่อไป — เมื่อไม่มีอะไรอ้างอิง timer แล้วมันถูกเก็บได้
// given ctx context.Context
timer := time.NewTimer(50 * time.Millisecond)
select {
case <-timer.C:
fmt.Println("fired")
case <-ctx.Done():
timer.Stop() // no drain needed on Go 1.23+ semantics
}

ยังคงควรใช้ time.NewTicker แทน time.Tick ในโค้ดที่มีอายุยืนเพื่อจะได้ Stop() มันได้ และใช้ context.WithTimeout แทน timer ที่เขียนเองเมื่อ deadline เป็นของ request

ก่อน Go 1.23 time.Tick คือการ leak ที่การันตีได้เลย มันคืนแค่ channel โดยไม่มี handle ให้เรียก Stop() ดังนั้น ticker ข้างหลังมันไม่มีทาง garbage collector จะปล่อยคืนได้ — โค้ด idiomatic ก่อน 1.23 จึงใช้ time.NewTicker แทนเสมอ เพื่อให้มีอะไรให้ Stop() ได้ ตั้งแต่ Go 1.23 garbage collector สามารถเก็บคืน Ticker (หรือ Timer) ที่ไม่มีใครอ้างอิงได้แม้ไม่มี Stop() และ documentation ของ time.Tick เองตอนนี้บอกว่า “there is no longer any reason to prefer NewTicker when Tick will do” Stop() ยังสำคัญเมื่อต้องการหยุดการส่งค่าจริงๆ ในขณะที่ยังมี reference อื่นค้างอยู่ — แต่ไม่จำเป็นอีกต่อไปเพียงเพื่อกัน leak โค้ด (หรือ AI assistant) ที่ยังยืนยันว่า “ห้ามใช้ time.Tick มัน leak เสมอ” กำลังพูดถึงความเชื่อสมัยก่อน 1.23

โค้ดที่เรียก time.Now() ตรงๆ test แบบ deterministic ได้ยาก — คำตอบที่ “ถูก” เปลี่ยนทุกครั้งที่รัน วิธีแก้ทั่วไปคือรับ clock เป็น function value แทนที่จะเรียก time.Now() ตรงๆ ข้างใน เพื่อให้ test แทนที่ด้วยเวลาคงที่ได้:

func isExpired(now func() time.Time, deadline time.Time) bool {
return now().After(deadline)
}
// production: isExpired(time.Now, deadline)
// test: isExpired(func() time.Time { return fixedTime }, deadline)

นี่คือแนวคิด dependency-injection แบบเดียวกับที่ใช้กับ database connection หรือ HTTP client — ส่งสิ่งที่แปรผันเป็น parameter (หรือ interface เล็กๆ ที่มี method Now() time.Time) แทนที่จะใช้ global

package main
import (
"fmt"
"time"
)
func main() {
// Fixed time — deterministic in any interpreter
t := time.Date(2024, 1, 2, 15, 4, 5, 0, time.UTC)
fmt.Println(t)
// Formatting with the reference layout
fmt.Println(t.Format("2006-01-02"))
fmt.Println(t.Format("15:04:05"))
fmt.Println(t.Format("Mon, 02 Jan 2006 15:04:05 MST"))
fmt.Println(t.Format(time.RFC3339))
// Parsing
parsed, err := time.Parse("2006-01-02", "2024-06-15")
if err == nil {
fmt.Println(parsed.Format("January 2, 2006"))
}
// Arithmetic
deadline := t.Add(72 * time.Hour)
fmt.Println(deadline.Format("2006-01-02 15:04:05"))
diff := deadline.Sub(t)
fmt.Println(diff)
// Duration construction
d := 2*time.Hour + 30*time.Minute
fmt.Println(d)
// Accessing date parts
fmt.Println(t.Year(), t.Month(), t.Day())
fmt.Println(t.Hour(), t.Minute(), t.Second())
}
package main
import (
"fmt"
"time"
)
func main() {
// 1. monotonic clock: Since/Sub use it even if the wall clock jumps;
// Equal compares the instant, == compares the whole struct (including monotonic reading + location)
a := time.Now()
b := a // identical copy
fmt.Println("a == b:", a == b, "a.Equal(b):", a.Equal(b))
c := a.Round(0) // strips the monotonic reading
fmt.Println("a == c (monotonic stripped):", a == c, "a.Equal(c):", a.Equal(c))
// 2. the n*time.Second unit bug: n is already a Duration once multiplied,
// but a bare integer like 5 passed to Sleep means 5 nanoseconds, not 5 seconds
var n time.Duration = 2
d := n * time.Second
fmt.Println("2 * time.Second:", d)
// time.Sleep(5) sleeps 5 nanoseconds — a classic unit bug, not "5 seconds"
start := time.Now()
time.Sleep(5)
fmt.Println("Sleep(5) slept for less than a millisecond:", time.Since(start) < time.Millisecond)
// 3. time.DateTime and friends (Go 1.20)
t := time.Date(2026, 9, 13, 14, 30, 0, 0, time.UTC)
fmt.Println("time.DateTime:", t.Format(time.DateTime))
fmt.Println("time.DateOnly:", t.Format(time.DateOnly))
fmt.Println("time.TimeOnly:", t.Format(time.TimeOnly))
// 4. Parse returns UTC; ParseInLocation uses the given location
p1, _ := time.Parse("2006-01-02 15:04:05", "2026-09-13 14:30:00")
fmt.Println("Parse location:", p1.Location())
loc, _ := time.LoadLocation("Asia/Bangkok")
p2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2026-09-13 14:30:00", loc)
fmt.Println("ParseInLocation location:", p2.Location(), "same instant as UTC 07:30:", p2.UTC().Format(time.TimeOnly))
// 5. Ticker.Stop and AddDate month overflow
ticker := time.NewTicker(time.Hour)
ticker.Stop() // stops delivery now; not required for GC since Go 1.23, but still good practice
jan31 := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
overflowed := jan31.AddDate(0, 1, 0) // "add one month" overflows into March
fmt.Println("Jan 31 + 1 month:", overflowed.Format(time.DateOnly))
// 6. injecting a clock for tests: accept a func() time.Time instead of calling time.Now directly
fixedClock := func() time.Time { return time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) }
isExpired := func(now func() time.Time, deadline time.Time) bool { return now().After(deadline) }
fmt.Println("isExpired with injected clock:", isExpired(fixedClock, time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC)))
}

output จริงจากโปรแกรมนี้บน Go 1.26.5 (การเทียบค่าแบบ boolean ทำให้ deterministic แม้จะเรียก time.Now()):

a == b: true a.Equal(b): true
a == c (monotonic stripped): false a.Equal(c): true
2 * time.Second: 2s
Sleep(5) slept for less than a millisecond: true
time.DateTime: 2026-09-13 14:30:00
time.DateOnly: 2026-09-13
time.TimeOnly: 14:30:00
Parse location: UTC
ParseInLocation location: Asia/Bangkok same instant as UTC 07:30: 07:30:00
Jan 31 + 1 month: 2026-03-03
isExpired with injected clock: true

สังเกตว่าทุกการตรวจสอบข้างบนเป็น ความสัมพันธ์ (== เทียบกับ Equal, “น้อยกว่าหนึ่ง millisecond”, ชื่อ location, วันที่ปฏิทินคงที่) ไม่ใช่ค่าที่ผูกกับ wall-clock — เป็นวิธีเดียวที่จะทำให้ output ของตัวอย่างที่ใช้ time.Now() เหมือนเดิมทุกครั้งที่รัน

flowchart LR
  Now["time.Now()"] --> T["time.Time value"]
  T --> Wall["wall clock reading"]
  T --> Mono["monotonic reading"]
  Wall -- "affected by NTP / manual changes" --> Format["Format() / == comparison"]
  Mono -- "always moves forward" --> SinceSub["Since() / Sub()"]
  T -- "Round(0) strips monotonic" --> T2["Time value: wall clock only"]
Since/Sub อ่านจาก monotonic clock; Format กับ == เห็น wall clock ด้วย
สิ่งที่ได้ประโยชน์ต้นทุน
time.Time เป็น value typeimmutable, copy และแชร์ได้ปลอดภัยทุก Time พก timezone ที่ต้องคำนึงถึง
time.Duration (int64 nanosecond)คำนวณไม่กำกวม — 5*time.Secondต้องคูณด้วยค่าคงที่ที่มีชื่อ ไม่ใช่เขียน “5” เฉยๆ
Reference layout (“2006-01-02”)self-documenting เมื่อรู้ตัวอย่างแล้วต่างจาก YYYY/MM/DD ของภาษาอื่น — ต้องจำตัวเลขวิเศษ
  • time.Now() คืน UTC เสมอ — ขึ้นกับ timezone ท้องถิ่นของเครื่อง; เรียก time.Now().UTC() เมื่อต้องการ UTC ชัดเจน
  • time.Sleep(1 * time.Second) แม่นยำ — OS scheduler อาจ sleep นานกว่าที่ขอ เช่น 1.05s — มันไม่ใช่ hard real-time
  • time.Equal เหมือนกับ == — time.Equal เทียบจุดเวลา (ไม่สน timezone); == เทียบ memory layout ทั้งหมด รวม timezone ด้วย
  • time.Sleep(5) sleep 5 วินาที — มัน sleep 5 nanosecond; Sleep รับ time.Duration และ integer literal เปล่าๆ คือจำนวน nanosecond ไม่ใช่ “จำนวนเวลา” ที่ไม่สนหน่วย
  • time.Tick leak เสมอถ้าไม่เรียก Stop() — จริงก่อน Go 1.23 ตอนที่ Ticker ที่ไม่มีใครอ้างอิงไม่มีทาง GC ได้; ตั้งแต่ Go 1.23 garbage collector เก็บคืน Ticker หรือ Timer ที่ไม่มีใครอ้างอิงได้เอง จึงเป็นคำแนะนำที่ล้าสมัยแล้ว (Stop() ยังมีประโยชน์เพื่อหยุดการ tick ขณะที่ยังมี reference ค้างอยู่ แค่ไม่จำเป็นเพื่อกัน leak อีกต่อไป)
  • AddDate(0, 1, 0) ตกวันเดียวกันของเดือนถัดไปเสมอ — ใกล้ปลายเดือนมันอาจเลื่อนไปเดือนถัดไปอีกทีแทน (31 มกราคม + 1 เดือน ≠ 31 กุมภาพันธ์ ซึ่งไม่มีอยู่จริง)

💡 ตัวอย่างจากของจริง

net/http.Server: http.Server{ReadHeaderTimeout: 5 * time.Second} ตั้ง timeout เป็น time.Duration เพื่อหยุด client ที่ส่ง header ช้าไม่ให้ค้าง connection ไว้

context.WithTimeout: สร้าง deadline ที่ต้องส่งต่อข้าม function call — เช่น เรียก downstream service แล้วยกเลิกอัตโนมัติเมื่อหมดเวลา

net/http.TimeoutHandler: สร้าง context.WithTimeout จาก r.Context() แล้ว select บน ctx.Done() เพื่อตัด request เมื่อ handler ทำงานนานเกินไป

โค้ด time compile ผ่านไม่ว่าหน่วยหรือ timezone จะถูกหรือไม่ ดังนั้นควรตรวจด้วยตาเองทุกครั้ง:

  • time.Sleep, context.WithTimeout หรือ time.NewTimer รับ integer literal เปล่าๆ แทนที่จะเป็นค่าที่คูณด้วยค่าคงที่ time.Xxx หรือไม่?
  • โค้ดเทียบค่า time.Time สองตัวด้วย == แทนที่จะเป็น .Equal() หรือไม่ — โดยเฉพาะหลังจากตัวใดตัวหนึ่งผ่าน Parse, Round หรือ round-trip กับ database?
  • โค้ดเรียก time.Parse บน timestamp ที่รู้อยู่แล้วว่าเป็น local time ใน timezone ที่ระบุ ทั้งที่ควรใช้ ParseInLocation หรือไม่?
  • AddDate(0, n, 0) (หรือ AddDate(n, 0, 0)) ถูกรันใกล้ปลายเดือน (หรือ 29 กุมภาพันธ์) โดยไม่จัดการเรื่อง calendar overflow หรือไม่?
  • comment หรือคำอธิบายอ้างว่า time.Tick/Ticker ที่ไม่ได้ Stop “leak เสมอ” หรือไม่ — ข้อความนี้จริงก่อน Go 1.23 แต่ไม่ใช่เรื่องทั้งหมดอีกต่อไป?
  • โค้ด test เรียก time.Now() ตรงๆ ข้างใน function ที่กำลัง test แทนที่จะรับ clock ที่ inject เข้ามาหรือไม่?

🐛 หา bug ในโค้ดนี้

func retryWithBackoff(attempt int, do func() error) error {
for i := 0; i < attempt; i++ {
if err := do(); err == nil {
return nil
}
time.Sleep(5) // wait 5 seconds before retrying
}
return fmt.Errorf("failed after %d attempts", attempt)
}
เฉลย

comment บอกว่า “5 วินาที” แต่ time.Sleep รับ time.Duration — จำนวน nanosecond แบบ int64 — และ 5 ที่ไม่มีหน่วยแปลว่า 5 nanosecond ไม่ใช่ 5 วินาที loop retry นี้แทบไม่มี backoff delay เลย มันยิง do() เร็วเท่าที่ CPU จะทำได้ แก้ได้โดยคูณด้วยค่าคงที่ที่มีชื่อ:

time.Sleep(5 * time.Second)
เวลาอ้างอิงของ Go ที่ใช้สำหรับ formatting คืออะไร?
โค้ดนี้ sleep นานแค่ไหน?
time.Sleep(5)
fmt.Println("done")
t.Sub(other) คืนค่าอะไร?
โค้ดนี้พิมพ์อะไร?
a := time.Now()
b := a.Round(0)
fmt.Println(a == b, a.Equal(b))
time.Parse("2006-01-02 15:04:05", s) ถูกใช้กับ string ที่รู้อยู่แล้วว่าเป็น local time ใน Asia/Bangkok มีอะไรผิด?
ทำไมควรรับ parameter now func() time.Time แทนการเรียก time.Now() ข้างใน function ที่ต้องการ test?