Menu

Thời gian trong Golang: Now, Sleep, Format, Parse, Duration

Cách làm việc với ngày giờ trong Go: time.Now và time.Sleep, tính toán với Duration, layout mẫu 2006-01-02 15:04:05 cho Format và Parse, múi giờ, Unix timestamp, timer và ticker.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Kiến thức cơ bản trong một chương trình

time.Now() trả về thời gian địa phương hiện tại, time.Sleep tạm dừng goroutine hiện tại, time.Since đo thời gian đã trôi qua, và time.Date dựng một thời điểm cụ thể. Phần còn lại của trang lần lượt đi qua từng phần. Các ví dụ dùng ngày cố định và múi giờ UTC để đầu ra giống nhau dù bạn chạy ở đâu.

Sleep và Duration

time.Duration là một int64 đếm số nano giây. Package này định nghĩa sẵn các hằng số để tạo giá trị dễ đọc:

Hằng sốGiá trị
time.Nanosecond1
time.Microsecond1000 ns
time.Millisecond1000 µs
time.Second1000 ms
time.Minute60 s
time.Hour60 phút

Không có time.Day, vì một ngày không phải lúc nào cũng dài 24 giờ khi múi giờ đổi sang giờ mùa hè. Dùng AddDate cho ngày theo lịch (xem bên dưới).

Hai bug thường gặp xuất phát từ việc Duration là số nguyên:

  • time.Sleep(5) chỉ ngủ 5 nano giây. Luôn nhân với một đơn vị: time.Sleep(5 * time.Second).
  • time.Sleep(n * time.Second) không biên dịch được khi n là biến kiểu int. Hãy chuyển đổi: time.Duration(n) * time.Second. Một hằng số như 5 dùng được mà không cần chuyển đổi, vì hằng số không có kiểu tự thích nghi với kiểu Duration.

ParseDuration chấp nhận ns, us (hoặc µs), ms, s, m và h, kết hợp như "2h45m" hay "1.5s". Nó không có đơn vị cho ngày.

Định dạng: layout mẫu 2006-01-02

Go không dùng %Y-%m-%d hay yyyy-MM-dd. Layout là thời điểm mẫu

Mon Jan 2 15:04:05 MST 2006

được viết theo đúng hình dạng bạn muốn đầu ra trông như thế. Các giá trị được chọn sao cho mỗi giá trị là duy nhất: tháng 1, ngày 2, giờ 3 (hoặc 15), phút 4, giây 5, năm 6 (2006), độ lệch múi giờ -7 (-0700). Hãy đọc nó là 01/02 03:04:05PM '06 -0700.

Các token layout bạn sẽ cần nhiều nhất:

TokenÝ nghĩaVí dụ
2006 / 06năm, 4 hoặc 2 chữ số2026 / 26
01 / 1 / Jan / Januarytháng03 / 3 / Mar / March
02 / 2 / _2ngày trong tháng (đệm số 0, không đệm, đệm khoảng trắng)05 / 5 / " 5"
Mon / Mondaythứ trong tuầnThu / Thursday
15giờ, đồng hồ 24 giờ09
03 / 3giờ, đồng hồ 12 giờ09 / 9
04 / 4phút07 / 7
05 / 5giây03 / 3
PM / pmký hiệu AM hoặc PMAM
.000 / .999phần lẻ của giây (cố định / bỏ số 0 ở cuối).250 / .25
MSTtên viết tắt của múi giờUTC
-0700 / -07:00độ lệch múi giờ dạng số+0000 / +00:00
Z07:00giống -07:00, nhưng in Z cho UTCZ

Các layout định nghĩa sẵn:

Hằng sốLayout
time.RFC33392006-01-02T15:04:05Z07:00 (dùng cho API và JSON)
time.RFC3339Nano2006-01-02T15:04:05.999999999Z07:00
time.DateTime (Go 1.20)2006-01-02 15:04:05
time.DateOnly (Go 1.20)2006-01-02
time.TimeOnly (Go 1.20)15:04:05
time.Kitchen3:04PM
time.RFC1123Mon, 02 Jan 2006 15:04:05 MST (ngày tháng trong HTTP thì dùng http.TimeFormat)

Lỗi kinh điển là viết layout với số sai, ví dụ "2023-01-01". Go không từ chối nó. Go chép nguyên các ký tự nó không nhận ra và thay thế những ký tự nó nhận ra: mỗi 2 là ngày, 3 là giờ theo đồng hồ 12 giờ và cả hai 01 là tháng, nên ngày 5 tháng 3 lúc 9:07 sẽ được định dạng thành 5059-03-03. Một layout như "YYYY-MM-DD" không chứa token nào và in ra y nguyên chính nó. Nếu ngày được định dạng trông lạ, hãy kiểm tra layout có dùng đúng các giá trị mẫu không.

Phân tích chuỗi thành thời gian

time.Parse(layout, value) dùng cùng các layout đó, và trả về một lỗi bạn phải kiểm tra:

Nếu đầu vào không có múi giờ, Parse trả về UTC. Cùng một chuỗi giờ đồng hồ có thể là những thời điểm khác nhau tùy theo múi giờ, vì thế mới có ParseInLocation. Thông báo lỗi nêu tên phần không khớp, rất có ích khi bạn gỡ lỗi một layout.

Múi giờ

time.Time là một thời điểm cộng với một location dùng khi hiển thị. Đổi location bằng In chỉ thay đổi cách nó được in ra, không thay đổi thời điểm.

  • time.UTC luôn có sẵn. Hãy lưu trữ và truyền thời gian ở UTC (hoặc RFC 3339 kèm độ lệch) và chỉ chuyển đổi khi hiển thị.
  • time.Local là múi giờ của máy. Trên máy chủ và trong container nó thường là UTC, còn trên laptop thì không, nên code phụ thuộc vào nó sẽ chạy khác nhau ở mỗi nơi.
  • time.LoadLocation("Europe/Berlin") đọc cơ sở dữ liệu IANA từ hệ điều hành. Các image container tối giản thường không có nó, và khi đó lời gọi trả về lỗi. Import trống _ "time/tzdata" nhúng cơ sở dữ liệu này vào tệp nhị phân của bạn (khoảng 450 KB) để nó luôn chạy được. Luôn xử lý lỗi.
  • time.FixedZone(name, offsetSeconds) tạo một múi giờ có độ lệch cố định. Nó không có quy tắc giờ mùa hè, nên hãy dùng nó cho các độ lệch bạn nhận được, không phải cho các vùng có tên.

Tính toán và so sánh

Những điều cần chú ý trong đầu ra:

  • AddDate(0, 1, 0) vào ngày 31 tháng 1 cho ra ngày 3 tháng 3, không phải ngày 28 tháng 2. Go cộng một tháng để có "ngày 31 tháng 2" rồi chuẩn hóa phần tràn. Nếu bạn cần "cùng ngày tháng sau, giới hạn ở cuối tháng", hãy tự viết logic đó.
  • Sub trả về một Duration, tối đa khoảng 292 năm. Để tính chênh lệch theo ngày lịch, hãy so sánh các ngày lúc nửa đêm UTC rồi chia cho 24 giờ.
  • Để lấy đầu ngày, hãy dựng lại thời gian từ Date(). t.Truncate(24 * time.Hour) làm tròn so với thời điểm zero ở UTC, nên cho kết quả sai với mọi múi giờ khác UTC.

So sánh bằng Equal, Before, After, không bao giờ bằng ==. time.Now() có kèm giá trị đọc từ đồng hồ đơn điệu (monotonic clock), dùng để time.Since vẫn đúng khi đồng hồ hệ thống bị chỉnh. == so sánh cả giá trị đó lẫn location, nên hai giá trị Time cùng một thời điểm vẫn có thể không bằng nhau. Cũng vì lý do này, đừng dùng time.Time làm khóa của map khi chưa chuẩn hóa (t.UTC().Round(0) bỏ đi giá trị đồng hồ đơn điệu).

time.Time zero là ngày 1 tháng 1 năm 1, 00:00 UTC. Kiểm tra nó bằng t.IsZero().

Đo thời gian đã trôi qua

start := time.Now()
doWork()
log.Printf("doWork took %v", time.Since(start))

time.Since(start) chính là time.Now().Sub(start), và time.Until(deadline) là deadline.Sub(time.Now()). Cả hai dùng đồng hồ đơn điệu khi có, nên an toàn trước những lần đồng hồ hệ thống thay đổi. Để benchmark code, hãy dùng tính năng benchmark của Go trong package testing thay vì tự bấm giờ.

Timer và ticker

time.After(d) trả về một channel nhận đúng một giá trị sau d. time.Timer cũng vậy nhưng có thêm phương thức Stop. time.Ticker gửi một giá trị sau mỗi chu kỳ cho đến khi bạn dừng nó.

Ba tick lúc 20, 40 và 60 ms đến sớm hơn nhiều so với timer 110 ms, nên chương trình dừng sau tick thứ ba. time.AfterFunc(d, f) chạy f trong goroutine riêng sau d, rất tiện cho việc trì hoãn chạy một lần.

Từ Go 1.23, timer và ticker không còn được tham chiếu sẽ bị thu gom rác kể cả khi bạn không gọi Stop, và channel của timer đã dừng hoặc đã đặt lại không còn gửi giá trị cũ nữa. Gọi Stop bằng defer vẫn là cách rõ ràng để nói vòng đời của ticker đã kết thúc. Khi giới hạn thời gian áp dụng cho cả một thao tác chứ không phải một lần chờ, context.WithTimeout thường dễ đọc hơn timer.

Thời gian trong JSON

time.Time tự động được chuyển sang và từ chuỗi RFC 3339 trong JSON, với độ chính xác đến nano giây:

type Event struct {
	Name string    `json:"name"`
	At   time.Time `json:"at"`
}
// {"name":"deploy","at":"2026-09-23T14:30:00Z"}

Chuỗi JSON ở định dạng khác sẽ không unmarshal được. Với Unix timestamp hoặc định dạng tùy chỉnh, hãy lưu một int64 hoặc một chuỗi rồi chuyển đổi, hoặc định nghĩa một kiểu có UnmarshalJSON riêng.

Lỗi thường gặp

  • time.Sleep(1) hoặc time.Sleep(n) với một số trần. Đó là nano giây.
  • Layout với chữ số sai. "2023-01-01" hay "YYYY-MM-DD" không phải layout. Hãy dùng "2006-01-02".
  • Nhầm phút với tháng. Phút là 04, tháng là 01. "15:01" in ra giờ rồi đến tháng, không phải phút.
  • So sánh bằng ==. Hãy dùng Equal.
  • Dựa vào time.Local. Nó khác nhau giữa các máy. Hãy ghi rõ time.UTC hoặc một location đã nạp.
  • Bỏ qua lỗi từ LoadLocation hoặc Parse. Cả hai đều thất bại với đầu vào thực tế. Parse thất bại trả về thời điểm zero, in ra là năm 1, còn LoadLocation thất bại trả về location nil khiến t.In(loc) bị panic.

Câu hỏi thường gặp

Sleep trong Go như thế nào?

Gọi time.Sleep với một time.Duration: time.Sleep(2 * time.Second) hoặc time.Sleep(500 * time.Millisecond). Một số trần như time.Sleep(2) vẫn biên dịch được nhưng chỉ ngủ 2 nano giây, vì Duration đếm theo nano giây. Sleep chỉ chặn goroutine hiện tại.

Vì sao Go dùng 2006-01-02 15:04:05 để định dạng ngày?

Go định dạng ngày bằng ví dụ. Layout là thời điểm mẫu Mon Jan 2 15:04:05 MST 2006 được viết theo đúng hình dạng bạn muốn đầu ra trông như thế. Các phần của nó tăng dần theo thứ tự kiểu Mỹ: tháng 1, ngày 2, giờ 3 (15 theo đồng hồ 24 giờ), phút 4, giây 5, năm 6 (2006), độ lệch múi giờ 7 (-0700). Vì vậy "2006-01-02" nghĩa là năm-tháng-ngày và "02/01/2006" nghĩa là ngày/tháng/năm.

Phân tích chuỗi ngày tháng trong Go như thế nào?

Dùng time.Parse(layout, value) với layout viết theo thời điểm mẫu: t, err := time.Parse("2006-01-02", "2026-09-23"). Luôn kiểm tra err. Nếu chuỗi không có thông tin múi giờ, kết quả ở múi giờ UTC; dùng time.ParseInLocation để hiểu chuỗi theo một múi giờ khác.

Lấy Unix timestamp trong Go như thế nào?

time.Now().Unix() trả về số giây tính từ ngày 1 tháng 1 năm 1970 UTC dưới dạng int64. UnixMilli(), UnixMicro() và UnixNano() cho đơn vị nhỏ hơn. Để làm chiều ngược lại, dùng time.Unix(sec, 0) hoặc time.UnixMilli(ms).

So sánh hai thời điểm trong Go như thế nào?

Dùng t1.Before(t2), t1.After(t2) và t1.Equal(t2). Đừng dùng ==: nó còn so sánh cả location và giá trị đồng hồ đơn điệu, nên hai giá trị cùng một thời điểm vẫn có thể không bằng nhau. t2.Sub(t1) cho khoảng chênh lệch dưới dạng Duration.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu