Gói time của Go thiết kế tốt hơn Date cũ của Java, nhưng vẫn có vài chỗ đặc trưng cần biết.
Duration là một kiểu
d := 90 * time.Second
90*time.Second = 1m30s | .Minutes()=1.5 | int64=90000000000
Duration là int64 đếm nano giây, có kiểu riêng nên đơn vị không lẫn được. In ra tự động thành dạng người đọc được.
Cái bẫy hay gặp:
n := 30
d := n * time.Second // LỖI BIÊN DỊCH
d := time.Duration(n) * time.Second // đúng
30 * time.Second chạy được vì 30 là hằng không kiểu (bài 4), còn n là biến int — và bài 5 đã nói Go không ép kiểu ngầm.
Layout định dạng: 2006-01-02
t.Format("02/01/2006 15:04")
13/08/2026 14:30
Go không dùng yyyy-MM-dd. Nó dùng một thời điểm tham chiếu cụ thể:
Mon Jan 2 15:04:05 MST 2006
1 2 3 4 5 6 7
Bạn viết thời điểm đó theo định dạng mong muốn, và Go suy ra quy tắc. Ban đầu rất khó nhớ, nhưng đổi lại là không cần thuộc bảng mã ký tự.
Dùng hằng có sẵn khi được: time.RFC3339 cho API, time.DateOnly và time.TimeOnly từ Go 1.20.
So sánh Time bằng == thì sai
t.Equal(t.In(vn)) = true <- cùng thời điểm
t == t.In(vn) = false <<< khác
== so cả thời điểm, múi giờ, và phần monotonic clock. Hai giá trị cùng chỉ một khoảnh khắc nhưng khác múi giờ sẽ không bằng nhau.
Luôn dùng Equal, Before, After. Đây đúng bài học ở bài 62 sê-ri Java với ZonedDateTime.
Cùng lý do, Time không nên dùng làm khoá map.
Cộng ngày khác cộng 24 giờ
gốc : 28/03 12:00 GMT
AddDate(0,0,1) : 29/03 12:00 BST
Add(24h) : 29/03 13:00 BST <<< lệch 1 giờ
Ngày 29/03 ở London chỉ có 23 giờ vì đổi sang giờ mùa hè.
AddDate làm việc theo lịch — "cùng giờ ngày mai". Add(24*time.Hour) làm việc theo thời gian tuyệt đối — đúng 86400 giây.
Chọn theo ý định: nhắc nhở "mỗi ngày 8 giờ sáng" là AddDate; token "hết hạn sau 24 giờ" là Add.
Việt Nam không có giờ mùa hè nên nhiều người Việt chưa từng gặp — cho tới khi làm sản phẩm cho thị trường châu Âu hoặc Mỹ.
Múi giờ
vn, err := time.LoadLocation("Asia/Ho_Chi_Minh")
Luôn dùng tên vùng IANA, không dùng offset cố định. time.FixedZone("GMT+7", 7*3600) tình cờ đúng cho Việt Nam nhưng sai về nguyên tắc.
Một cái bẫy triển khai: LoadLocation đọc cơ sở dữ liệu múi giờ của hệ điều hành. Ảnh Docker tối giản không có tzdata thì nó trả lỗi. Hai cách chữa: cài tzdata vào ảnh, hoặc nhúng vào binary:
import _ "time/tzdata"
Dòng đó thêm khoảng 450KB vào binary và bỏ hẳn phụ thuộc vào hệ điều hành — rất đáng cho ảnh scratch.
Và nhớ đặt TZ trong container, đúng bài học ở bài 97 sê-ri Java: mặc định là UTC, nên cron chạy 6 giờ sáng giờ Việt Nam sẽ thấy máy chủ đang là 23 giờ hôm trước.
Monotonic clock
time.Since(t0) = 21ms
Now() có phần m=+...
time.Now() mang theo hai đồng hồ: wall clock (có thể nhảy khi NTP chỉnh) và monotonic (chỉ tăng).
Sub, Since, Until dùng monotonic, nên đo khoảng thời gian không bao giờ ra số âm vì đồng hồ hệ thống bị chỉnh. Đây là thứ Java phải dùng System.nanoTime() riêng mới có.
Nhưng phần monotonic mất khi bạn Round, Truncate, In, hoặc serialize. Nên đừng đo khoảng thời gian bằng Time đã đi qua JSON.
Ba cái bẫy Timer
Quên Stop():
tk := time.NewTicker(time.Second)
defer tk.Stop()
Ticker không dừng là goroutine và timer rò rỉ. Timer cũng nên Stop nếu bạn thoát sớm.
time.After trong vòng lặp — bài 33 đã nói: mỗi vòng tạo một timer mới. Từ Go 1.23, GC dọn được timer chưa hết hạn nên nhẹ đi nhiều, nhưng thói quen vẫn nên giữ.
Ticker không bù nhịp bị lỡ. Nếu xử lý lâu hơn chu kỳ, nhịp bị bỏ qua chứ không dồn lại. Thường đó là hành vi bạn muốn — nhưng phải biết.
Test mã phụ thuộc thời gian
Đừng gọi time.Now() rải rác. Tiêm một hàm:
type Dich struct {
now func() time.Time
}
func New() *Dich { return &Dich{now: time.Now} }
// trong test: d.now = func() time.Time { return coDinh }
Đây là tương đương Clock của Java ở bài 61, và nó làm logic hết hạn test được mà không phải chờ.
Thử ba mươi giây
t := time.Now()
j, _ := json.Marshal(t)
var t2 time.Time
json.Unmarshal(j, &t2)
fmt.Println(t == t2, t.Equal(t2))
In ra false true. Qua JSON, phần monotonic mất và múi giờ có thể đổi — nhưng thời điểm vẫn đúng.
Ba mươi giây đó là lý do Equal tồn tại.
Ngày mai: viết test kiểu Go — bảng test và những gì thư viện chuẩn đã đủ.