Go 1.21 đưa log/slog vào thư viện chuẩn, và nó thay thế được logrus, zap cho phần lớn nhu cầu.
Vì sao không dùng log cũ
2026/08/13 03:11:30 main.go:18: thông điệp kèm tham số
Gói log cũ chỉ ghi chuỗi. Muốn lọc theo mã đơn hàng, bạn phải viết biểu thức chính quy và cầu cho định dạng không đổi.
Nó cũng không có mức log — không có cách tắt debug trong sản xuất mà không sửa mã.
slog: hai handler
t := slog.New(slog.NewTextHandler(os.Stdout, nil))
t.Info("đơn hàng đã xử lý", "ma", "DH-9", "tien", 150.5, "thanhCong", true)
time=2026-08-13T03:11:30.805Z level=INFO msg="đơn hàng đã xử lý" ma=DH-9 tien=150.5 thanhCong=true
j := slog.New(slog.NewJSONHandler(os.Stdout, nil))
{"time":"2026-08-13T03:11:30.805Z","level":"INFO","msg":"đơn hàng đã xử lý","ma":"DH-9","tien":150.5}
Text cho môi trường phát triển, JSON cho sản xuất. JSON làm ma thành trường truy vấn được thay vì một chuỗi phải bóc bằng regex.
Cú pháp là cặp khoá–giá trị xen kẽ. Số lẻ đối số thì slog thêm khoá !BADKEY thay vì panic — nên lỗi này không làm sập dịch vụ nhưng cũng dễ lọt.
With: gắn ngữ cảnh cố định
l := j.With("dichVu", "don-hang", "phienBan", "1.2.3")
l.Info("nhận request", "duongDan", "/don/9")
{"...","msg":"nhận request","dichVu":"don-hang","phienBan":"1.2.3","duongDan":"/don/9"}
With trả về logger mới mang sẵn các trường đó. Đây là cách gắn mã tương quan cho toàn bộ vòng đời một request:
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
l := slog.With("maRequest", uuid.NewString())
ctx := context.WithValue(r.Context(), khoaLog{}, l)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
With cũng tính toán trước phần định dạng, nên logger đã With ghi nhanh hơn việc lặp lại các cặp khoá–giá trị ở mỗi lời gọi.
Có slog.Group để lồng:
{"...","msg":"có nhóm","http":{"method":"GET","ma":200}}
Cái bẫy: đối số vẫn được tính khi mức log đã tắt
gọi 1000 lần Debug (đang tắt) -> hàm đối số chạy 1000 lần
Đây là điểm quan trọng nhất của bài, và nó giống hệt kết luận ở bài 98 sê-ri Java.
slog.Debug("ẩn", "v", tonKem()) — tonKem() là đối số, nên Go tính nó trước khi gọi Debug. Mức DEBUG có tắt hay không cũng không cứu được.
Với đối số là biến sẵn có thì không sao. Với đối số cần tính toán — duyệt slice, serialize JSON, gọi hàm — hãy canh cổng:
if logger.Enabled(ctx, slog.LevelDebug) {
logger.Debug("chi tiết", "du", tinhToanTonKem())
}
có canh cổng Enabled() -> hàm chạy 0 lần
LogAttrs: bản không cấp phát
logger.LogAttrs(ctx, slog.LevelInfo, "dòng log",
slog.Int("i", i), slog.String("ma", "DH-9"))
Info (any...) : 70 ms (100.000 dòng)
LogAttrs (Attr) : 69 ms
Ở phép đo này hai cách gần bằng nhau — tôi mong LogAttrs nhanh hơn rõ rệt và nó không như vậy.
Lý do là slog đã tối ưu đường any... khá tốt cho ít đối số, và chi phí thật nằm ở việc định dạng JSON cùng ghi tệp chứ không ở việc đóng hộp đối số.
Nên lời khuyên trung thực: dùng Info(...) cho dễ đọc, và chỉ đổi sang LogAttrs ở đường chạy cực nóng sau khi đã đo. Đừng viết LogAttrs khắp nơi vì nghe nói nó nhanh hơn.
Cấu hình cho sản xuất
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true, // thêm tệp:dòng
})
slog.SetDefault(slog.New(h))
SetDefault làm slog.Info(...) ở mức package dùng handler của bạn — tiện cho thư viện không muốn nhận logger qua tham số.
Ghi ra stdout, không ghi tệp. Docker và Kubernetes đã thu log từ đó, đúng như bài 98 sê-ri Java.
AddSource tốn thêm một chút vì phải lấy thông tin ngăn xếp; bật nếu bạn thấy đáng.
Đổi mức lúc chạy bằng slog.LevelVar:
var muc slog.LevelVar
muc.Set(slog.LevelInfo)
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &muc})
// sau này: muc.Set(slog.LevelDebug)
Rất đáng có một endpoint đổi mức log — bật debug trong năm phút để điều tra rồi tắt, không cần triển khai lại.
Che dữ liệu nhạy cảm
Cài slog.LogValuer để kiểu của bạn tự che khi log:
type MatKhau string
func (m MatKhau) LogValue() slog.Value { return slog.StringValue("***") }
Giờ mọi chỗ log giá trị đó đều ra ***, kể cả khi lập trình viên quên. Đây là cách phòng tốt hơn nhiều so với dựa vào kỷ luật.
Thử ba mươi giây
grep -rn 'slog\.\(Debug\|Info\)(' --include='*.go' . | grep -E '\(\)|\.String\(\)|json\.Marshal'
Mỗi kết quả là một lời gọi hàm nằm trong đối số log. Nếu nó ở mức DEBUG và nằm trong vòng lặp nóng, bạn đang trả tiền cho những dòng log không ai nhìn thấy.
Ngày mai: graceful shutdown — nhận tín hiệu và đóng gọn.