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.

Ảnh chụp đoạn mã Go nền tối minh hoạ staticcheck và golangci-lint trong Go bắt lỗi go build bỏ qua, vì sao lint go build chỉ bắt lỗi cú pháp kiểu go build go vet bắt lỗi cú pháp và kiểu nhưng bỏ qua nhiều lỗi tinh vi so sánh thừa dead store dùng API sai cách code mùi lỗi hiệu năng phân tích tĩnh lint bắt chúng ngay khi viết trước cả khi chạy rẻ hơn tìm bug lúc chạy, code hợp lệ cú pháp nhưng đầy vấn đề tinh vi func XuLy ten string xs slice int string if len xs bằng 0 bằng true so sánh bool thừa S1002 return kq bằng bat dau gán rồi ghi đè ngay dead store kq bằng fmt Sprintf Xu ly phần trăm s ten tieude bằng fmt Sprintf KET QUA Sprintf vô ích S1039 sach bằng strings TrimLeft ten hai dấu cách cutset trùng ký tự SA1024 return kq, staticcheck bộ linter chất lượng cao staticcheck ./... hàng trăm luật SA S ST bug tiềm ẩn đơn giản hóa dùng API đúng cách chính xác cao ít dương tính giả, golangci-lint gom nhiều linter vào một golangci-lint run enable staticcheck ineffassign gosimple govet chạy nhiều linter song song gom kết quả khử trùng lặp 131 linter có sẵn bật tắt qua golangci.yml mỗi linter mạnh mảng riêng ineffassign dead store gosimple đơn giản hóa staticcheck bug govet nghi vấn

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.

Ảnh chụp bảng kết quả đo thật nền tối staticcheck và golangci-lint trong Go chạy bằng go build cộng staticcheck cộng golangci-lint Go 1.23 arm64, 1 go build không thấy vấn đề gì go build ./... go build OK không thấy vấn đề code hợp lệ cú pháp và kiểu go build cho qua, 2 staticcheck bắt 3 lỗi tinh vi code.go 12 S1002 should omit comparison to bool constant can be simplified to len xs bằng 0 code.go 21 S1039 unnecessary use of fmt Sprintf code.go 24 SA1024 cutset contains duplicate characters 3 vấn đề go build hoàn toàn bỏ qua staticcheck chỉ rõ dòng, 3 golangci-lint 4 linter bắt 4 lỗi cộng ngữ cảnh dòng code.go 12 S1002 gosimple if len xs bằng 0 bằng true code.go 21 S1039 gosimple tieude bằng fmt Sprintf code.go 17 ineffectual assignment to kq ineffassign kq bằng bat dau dead store staticcheck bỏ sót code.go 24 SA1024 staticcheck strings TrimLeft ten gom 4 linter bắt thêm dead store mà staticcheck 1 mình bỏ qua, golangci-lint có 131 linter sẵn golangci-lint help linters wc l 131 bật tắt qua golangci.yml mỗi linter mạnh 1 mảng, cốt lõi go build chỉ bắt cú pháp kiểu bỏ qua lỗi tinh vi code mùi staticcheck bộ linter chất lượng cao SA S ST ít dương tính giả golangci-lint gom nhiều linter 131 sẵn gom kết quả khử trùng đo thật build sạch staticcheck 3 lỗi golangci-lint 4 thêm dead store chạy khi ngay lúc viết cộng trong CI để chặn merge code có vấn đề đánh đổi bật nhiều linter nhiều nhiễu chọn bộ hợp đội chỉnh dần

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ề

  1. 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ả.
  2. 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.
  3. 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 //nolint tiế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.