go build và go vet bắt được lỗi cú pháp và lỗi kiểu — nhưng một khối lượng lớn lỗi tinh vi lọt qua chúng hoàn toàn: so sánh bool thừa, gán giá trị rồi ghi đè ngay (dead store), dùng API thư viện chuẩn sai cách, code mùi, lỗi hiệu năng tiềm ẩn. Những lỗi này biên dịch sạch và có thể chạy đúng hầu hết thời gian, nhưng là bug chờ nổ hoặc code khó bảo trì.
Phân tích tĩnh (static analysis / lint) bắt chúng ngay khi viết, trước cả khi chạy. Trong hệ sinh thái Go, staticcheck là bộ linter chất lượng cao được kính trọng nhất, và golangci-lint là công cụ gom nhiều linter (bao gồm staticcheck) chạy cùng lúc. Bài này chạy cả hai thật trên code có lỗi tinh vi, và cho thấy chúng bắt được gì mà go build bỏ qua — cùng lý do nên gom nhiều linter.
Code hợp lệ cú pháp nhưng đầy vấn đề
func XuLy(ten string, xs []int) string {
if len(xs) == 0 == true { // so sánh bool thừa (S1002)
return ""
}
kq := "bat dau" // gán rồi ghi đè ngay (dead store)
kq = fmt.Sprintf("Xu ly %s", ten)
tieude := fmt.Sprintf("=== KET QUA ===") // Sprintf vô ích (S1039)
sach := strings.TrimLeft(ten, " ") // cutset trùng ký tự (SA1024)
_ = sach; _ = tieude; return kq
}
Bốn vấn đề: so sánh == true thừa, gán kq rồi ghi đè ngay (lãng phí), Sprintf không có tham số định dạng (nên dùng chuỗi thẳng), và TrimLeft với cutset " " (hai dấu cách trùng nhau — gần như chắc là lỗi ý định, người viết tưởng nó cắt tiền tố chuỗi chứ không phải tập ký tự). Tất cả biên dịch sạch.

Hình 1: go build chỉ bắt cú pháp/kiểu, bỏ qua lỗi tinh vi; staticcheck là bộ linter chất lượng cao (SA/S/ST), golangci-lint gom nhiều linter (131 sẵn) chạy cùng lúc và gom kết quả.
Đo thật: go build sạch, staticcheck 3 lỗi, golangci-lint 4
go build không thấy gì:
$ go build ./...
go build: OK (không thấy vấn đề)
staticcheck bắt 3 lỗi tinh vi, mỗi lỗi kèm mã luật và dòng:
code.go:12: S1002: should omit comparison to bool constant, can be simplified to len(xs) == 0
code.go:21: S1039: unnecessary use of fmt.Sprintf
code.go:24: SA1024: cutset contains duplicate characters
Mã luật (S1002, S1039, SA1024) tra được trên trang staticcheck để hiểu vì sao — mỗi luật có lý do rõ ràng. staticcheck nổi tiếng chính xác cao, ít dương tính giả, nên khi nó báo, gần như chắc là vấn đề thật.
golangci-lint (bật 4 linter) bắt 4 lỗi — nhiều hơn staticcheck một:
code.go:12: S1002 ... (gosimple) if len(xs) == 0 == true
code.go:21: S1039 ... (gosimple) tieude := fmt.Sprintf(...)
code.go:17: ineffectual assignment to kq (ineffassign) // staticcheck bỏ sót!
code.go:24: SA1024 ... (staticcheck) strings.TrimLeft(ten, " ")
Điểm mấu chốt: golangci-lint bắt thêm dead store (ineffectual assignment to kq dòng 17) mà staticcheck một mình không báo — vì đây là thế mạnh của linter ineffassign, không phải staticcheck. Đây chính là lý do gom nhiều linter: mỗi linter mạnh ở một mảng, và golangci-lint chạy chúng song song, gom kết quả, khử trùng lặp. golangci-lint có 131 linter sẵn, bật/tắt qua tệp .golangci.yml.

Hình 2: Đo thật — go build sạch; staticcheck bắt 3 lỗi (S1002, S1039, SA1024); golangci-lint (4 linter) bắt 4 — thêm cả dead store ineffectual assignment to kq mà staticcheck một mình bỏ sót; 131 linter sẵn để chọn.
Đánh đổi cần cân nhắc
Bật nhiều linter dễ ngập trong nhiễu — chọn bộ hợp đội. Có 131 linter không nghĩa là bật hết. Nhiều linter chồng chéo, một số quá khắt khe hoặc mang tính phong cách gây tranh cãi (độ dài dòng, đặt tên, thứ tự import). Bật hết trên một codebase lớn cho ra hàng nghìn cảnh báo, đội bỏ cuộc. Chiến lược tốt: bắt đầu với bộ mặc định của golangci-lint (hoặc chỉ staticcheck + vài linter cốt lõi như ineffassign, gosimple, govet, errcheck), chạy được, rồi bật thêm từ từ khi đội đồng thuận. Chất lượng hơn số lượng.
Linter bắt code mùi, không bắt lỗi logic sâu. Static analysis mạnh với các mẫu cục bộ, cú pháp (dead store, so sánh thừa, dùng API sai). Nhưng nó không hiểu ý định code, nên không bắt được lỗi logic sâu (thuật toán sai, điều kiện nghiệp vụ nhầm) — đó là việc của test (property-based, golden file như các bài trước) và code review. Linter là một lớp, không phải toàn bộ phòng thủ chất lượng.
Chạy trong CI để chặn hồi quy, không chỉ lúc viết. Chạy linter cục bộ lúc viết là tốt, nhưng con người quên. Đặt golangci-lint run trong pipeline CI (chặn merge khi có lỗi) đảm bảo mọi code vào nhánh chính đều sạch theo chuẩn đội. Kết hợp với //nolint:linter // lý do cho các ngoại lệ hiếm có chủ đích (kèm lý do, để người sau hiểu) — nhưng dùng tiết kiệm, đừng lạm dụng để tắt cảnh báo thật.
Ba ý mang về
- Phân tích tĩnh bắt lỗi tinh vi go build bỏ qua: đo thật, cùng file biên dịch sạch nhưng staticcheck bắt 3 lỗi (S1002 so sánh thừa, S1039 Sprintf vô ích, SA1024 cutset trùng) — mỗi lỗi kèm mã luật và dòng, chính xác cao ít dương tính giả.
- golangci-lint gom nhiều linter mạnh hơn một linter đơn: đo thật, nó bắt 4 lỗi — thêm cả dead store (
ineffectual assignment) mà staticcheck một mình bỏ sót, vì đó là thế mạnh của ineffassign; 131 linter sẵn, mỗi cái mạnh một mảng, gom kết quả và khử trùng lặp. - Dùng lint đúng cách: chọn bộ linter hợp đội (đừng bật hết 131 gây nhiễu), hiểu lint bắt code mùi chứ không bắt lỗi logic sâu (cần test + review), và chạy trong CI để chặn hồi quy — dùng
//nolinttiết kiệm kèm lý do.
Phần sau ta chuyển sang quan sát hiệu năng liên tục trong sản xuất: continuous profiling trong Go — thu thập hồ sơ CPU/bộ nhớ từ dịch vụ đang chạy thật, để tìm điểm nóng mà benchmark cục bộ không lộ ra.