time
time.Time และ time.Duration
หัวข้อที่มีชื่อว่า “time.Time และ time.Duration”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.Secondfmt.Println(d) // 2h30m15sการสร้าง time.Time
หัวข้อที่มีชื่อว่า “การสร้าง time.Time”ในโค้ด 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 ได้
Monotonic clock: Equal เทียบกับ ==
หัวข้อที่มีชื่อว่า “Monotonic clock: Equal เทียบกับ ==”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 truec := a.Round(0) // Round(0) strips the monotonic readingfmt.Println(a == b, a.Equal(b)) // true truefmt.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 ที่ถูกถอดออกโดยเฉพาะ
กับดักหน่วยของ n คูณ time.Second และ time.Sleep(5)
หัวข้อที่มีชื่อว่า “กับดักหน่วยของ n คูณ time.Second และ time.Sleep(5)”ค่าคงที่ duration (time.Second, time.Millisecond, …) มีไว้เพื่อไม่ให้ integer ธรรมดาถูกเข้าใจผิดว่าเป็น duration — แต่ความผิดพลาดที่เกี่ยวข้องกันสองแบบยังพบบ่อยในโค้ดที่ AI เขียน:
var n time.Duration = 2d := n * time.Second // fine: n is already a Durationfmt.Println(d) // 2sbug จะโผล่เมื่อ 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 intendedtime.Sleep(5 * time.Second) // sleeps 5 seconds — the constant makes the unit explicitเวลาอ่าน diff time.Sleep (หรือ context.WithTimeout, time.NewTimer, …) ที่ถูกเรียกด้วย integer literal เปล่าๆ โดยไม่มีค่าคงที่ time.Xxx คูณอยู่ด้วย ควรมองดูอีกครั้งเสมอ
Formatting — reference layout
หัวข้อที่มีชื่อว่า “Formatting — reference layout”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")
Parsing
หัวข้อที่มีชื่อว่า “Parsing”time.Parse คือด้านตรงข้ามของ Format — รับ layout กับ string:
parsed, err := time.Parse("2006-01-02", "2024-06-15")// parsed is midnight UTC on June 15, 2024layout 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") // UTCp2, _ := 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การคำนวณ — Add และ Sub
หัวข้อที่มีชื่อว่า “การคำนวณ — Add และ Sub”deadline := t.Add(72 * time.Hour) // 3 days laterdiff := deadline.Sub(t) // time.Duration: 72h0m0sbefore := deadline.Before(t) // falseafter := deadline.After(t) // trueAdd รับ 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 ไม่ใช่ปีอธิกสุรทิน) โค้ดใดที่เพิ่มเดือนให้วันที่ใกล้ปลายเดือนต้องจัดการเรื่องนี้อย่างชัดเจน
Timer, ticker และ channel ของมัน
หัวข้อที่มีชื่อว่า “Timer, ticker และ channel ของมัน”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 hatchasynctimerchanGODEBUG ถูกลบใน Go 1.27 ทุกโปรแกรมจึงได้พฤติกรรมนี้ - timer ที่ไม่มีใครอ้างอิงถูก garbage collect
time.Afterข้างในselectใน loop ไม่ leak จนกว่ามันจะ fire อีกต่อไป — เมื่อไม่มีอะไรอ้างอิง timer แล้วมันถูกเก็บได้
// given ctx context.Contexttimer := 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
การ inject clock สำหรับ test
หัวข้อที่มีชื่อว่า “การ inject clock สำหรับ test”โค้ดที่เรียก 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): truea == c (monotonic stripped): false a.Equal(c): true2 * time.Second: 2sSleep(5) slept for less than a millisecond: truetime.DateTime: 2026-09-13 14:30:00time.DateOnly: 2026-09-13time.TimeOnly: 14:30:00Parse location: UTCParseInLocation location: Asia/Bangkok same instant as UTC 07:30: 07:30:00Jan 31 + 1 month: 2026-03-03isExpired with injected clock: trueสังเกตว่าทุกการตรวจสอบข้างบนเป็น ความสัมพันธ์ (== เทียบกับ Equal, “น้อยกว่าหนึ่ง millisecond”, ชื่อ location, วันที่ปฏิทินคงที่) ไม่ใช่ค่าที่ผูกกับ wall-clock — เป็นวิธีเดียวที่จะทำให้ output ของตัวอย่างที่ใช้ time.Now() เหมือนเดิมทุกครั้งที่รัน
Monotonic reading เทียบกับ wall clock reading
หัวข้อที่มีชื่อว่า “Monotonic reading เทียบกับ wall clock reading”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"]
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| time.Time เป็น value type | immutable, 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 ทำงานนานเกินไป
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ด 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)