Log là công cụ gỡ lỗi phổ biến nhất, nhưng nó có giới hạn cố hữu: nó chỉ cho biết cái bạn nghĩ trước là cần in. Khi gặp một bug mà bạn không đoán được nguyên nhân, thêm log rồi deploy lại rồi đợi tái hiện là vòng lặp chậm chạp. Có lúc bạn cần xem trạng thái đầy đủ tại một điểm: giá trị mọi biến, ngăn xếp lời gọi, và bước qua từng dòng để thấy logic đi đâu.
Delve (lệnh dlv) là debugger chính thức cho Go — hiểu goroutine, kiểu Go, và runtime. Quan trọng hơn, chế độ headless cho phép gỡ lỗi từ xa: server dlv chạy cạnh tiến trình (kể cả bên trong container production), còn client (hoặc IDE) kết nối qua mạng. Bài này chạy Delve thật — đặt breakpoint, xem biến, cả cục bộ lẫn qua mạng — và nêu rõ rủi ro bảo mật của debug từ xa.
Build với cờ tắt tối ưu để debug chính xác
$ go build -gcflags="all=-N -l" -o dbgbin main.go
Cờ -N tắt tối ưu và -l tắt inline. Vì sao cần: trình tối ưu của Go làm biến "biến mất" (bị gộp vào thanh ghi, loại bỏ), và dòng thực thi nhảy lung tung so với mã nguồn. Với build tối ưu, debugger báo "optimized out" khi bạn xem biến, hoặc breakpoint dừng ở dòng lạ. Tắt tối ưu để trạng thái debug khớp chính xác mã nguồn (chỉ dùng cho build debug, không phải production).
Debug cục bộ: breakpoint + xem biến
$ dlv exec dbgbin
(dlv) break tinhTong # dừng khi vào hàm tinhTong
(dlv) continue # chạy tới breakpoint
(dlv) print d.Items # xem giá trị biến bất kỳ
(dlv) print gia
(dlv) next # bước qua từng dòng
(dlv) locals # mọi biến cục bộ hiện tại
Các lệnh cốt lõi: break đặt điểm dừng (theo tên hàm hoặc file:line), continue chạy tới đó, print xem bất kỳ biểu thức nào, next/step bước từng dòng (next bước qua lời gọi, step vào trong), locals liệt kê mọi biến cục bộ.

Hình 1: Delve trong Go — build với -gcflags="all=-N -l" để debug chính xác; debug cục bộ bằng dlv exec (break, print, next, locals); debug từ xa bằng headless server + dlv connect qua mạng, hoặc dlv attach <pid> vào tiến trình đang chạy.
Đo thật: cục bộ và từ xa
Cục bộ — breakpoint dừng đúng hàm, xem trạng thái đầy đủ:
(dlv) break tinhTong
Breakpoint 1 set at main.tinhTong() ./main.go:11
(dlv) continue
> [Breakpoint 1] main.tinhTong() ./main.go:11 (hits goroutine(1):1)
(dlv) print d.ID
1001
(dlv) print d.Items
[]string len: 3, cap: 3, ["ca-phe","banh","tra"]
(dlv) print gia
[]int len: 3, cap: 3, [45000,25000,5000]
(dlv) print len(d.Items) -> 3 print len(gia) -> 3
Breakpoint dừng đúng main.go:11, và ta xem được mọi biến: d.ID = 1001, d.Items = mảng 3 chuỗi, gia = mảng 3 số, kể cả gọi hàm len() trong biểu thức print. Đây là trạng thái đầy đủ tại điểm dừng — thứ log không cho được trừ khi bạn đoán trước phải in đúng những biến này.
Từ xa — server headless cạnh tiến trình, client kết nối qua mạng:
# server (trong container):
API server listening at: [::]:2345
warning: Listening for remote connections
(connections are not authenticated nor encrypted)
# client (dlv connect :2345):
Breakpoint 1 set at main.tinhTong() ./main.go:11
(dlv) print gia -> [45000,25000,5000]
(dlv) print d.Items[0] -> "ca-phe"
Server dlv lắng nghe trên :2345, client dlv connect tới nó qua mạng và đặt breakpoint, xem biến y như cục bộ. Đây chính là cách gỡ lỗi một tiến trình bên trong container hay trên máy chủ từ xa mà bạn không thể mở terminal trực tiếp — và IDE (VS Code, GoLand) cũng nối vào chính server dlv này để debug đồ họa. Ngoài exec, còn dlv attach <pid> để gắn vào một tiến trình đã đang chạy mà không cần khởi động lại.

Hình 2: Đo thật — cục bộ breakpoint dừng đúng main.go:11, print được d.ID=1001/Items/gia/len; từ xa server headless :2345 (kèm cảnh báo không mã hóa/xác thực) và client dlv connect xem biến qua mạng — debug tiến trình trong container từ máy dev.
Đánh đổi cần cân nhắc
dlv từ xa KHÔNG mã hóa và KHÔNG xác thực — rủi ro bảo mật lớn. Chú ý dòng cảnh báo thật ở trên: "connections are not authenticated nor encrypted". Bất kỳ ai kết nối được tới cổng dlv đều có toàn quyền trên tiến trình — đọc mọi biến (kể cả secret trong bộ nhớ), thậm chí gọi hàm, thay đổi trạng thái. TUYỆT ĐỐI không phơi cổng dlv ra Internet hay mạng không tin cậy. Chỉ dùng qua mạng riêng, hoặc bind localhost rồi tạo SSH tunnel từ máy dev tới container — đây là cách chuẩn để debug từ xa an toàn.
Breakpoint dừng tiến trình — cẩn thận trên production sống. Khi breakpoint được hit, toàn bộ tiến trình dừng lại cho tới khi bạn continue. Trên một dịch vụ production đang phục vụ người dùng, điều này nghĩa là mọi request đóng băng — timeout hàng loạt, có thể sập health check và bị restart. Debug từ xa trên production sống là biện pháp cuối cùng, làm trên một instance đã rút khỏi load balancer, không phải instance đang nhận traffic thật.
Build debug khác build production. Cờ -N -l tắt tối ưu làm code chậm hơn và binary lớn hơn — không dùng cho production. Nghĩa là để debug từ xa "đúng chuẩn", bạn cần build một binary debug riêng để deploy tạm, hoặc chấp nhận debug binary tối ưu với hạn chế (một số biến "optimized out"). Delve có debug được binary production, chỉ là kém chính xác hơn. Cân nhắc cái nào tùy tình huống.
Ba ý mang về
- Delve cho bạn xem trạng thái đầy đủ tại một điểm mà log không cho: breakpoint,
printbất kỳ biến/biểu thức,next/steptừng dòng,locals, stack — đo thật, breakpoint dừng đúngmain.go:11và in đượcd.ID=1001,d.Items,gia, cảlen(). - Chế độ headless cho phép debug từ xa: server dlv chạy cạnh tiến trình (kể cả trong container), client
dlv connectqua mạng đặt breakpoint và xem biến y như cục bộ (đo thật qua:2345);dlv attach <pid>gắn vào tiến trình đang chạy, IDE cũng nối được. - Nêu rõ rủi ro nghiêm trọng: dlv từ xa không mã hóa/xác thực (ai kết nối được có toàn quyền — chỉ qua mạng riêng/SSH tunnel), breakpoint dừng cả tiến trình nên nguy hiểm trên production sống (làm trên instance đã rút khỏi LB), và build debug (
-N -l) khác build production.
Phần sau ta xét gỡ lỗi sau khi sự cố đã xảy ra: core dump và phân tích sự cố trong Go — sinh core dump khi crash, và mổ xẻ nó bằng Delve để tìm nguyên nhân dù tiến trình đã chết.