13 KiB
Dưới đây là phân tích chi tiết về 5 quy ước quan trọng nhất:
1. Bắt buộc Định dạng Mã bằng gofmt/goimports
-
Quy ước: Tất cả mã nguồn Go trong dự án phải được định dạng bằng công cụ
gofmt. Khuyến khích mạnh mẽ sử dụnggoimports(bao gồmgofmtvà tự động quản lý imports) thay thế. -
Giải thích:
-
gofmtlà công cụ định dạng mã nguồn tiêu chuẩn của Go. Việc sử dụng nó đảm bảo một phong cách định dạng thống nhất (thụt lề bằng tab, cách dòng, sắp xếp, v.v.) trên toàn bộ codebase, loại bỏ hoàn toàn các cuộc tranh luận không cần thiết về định dạng. -
goimportslà một bản nâng cấp củagofmt, ngoài việc định dạng mã, nó còn tự động sắp xếp, thêm và xóa các khai báoimportkhông cần thiết, giúp giữ cho phần import luôn gọn gàng và chính xác.
-
-
Tại sao quan trọng cho đội ngũ:
-
Tính nhất quán tuyệt đối: Mọi người đọc và viết mã theo cùng một định dạng, giảm thiểu sự phân tâm và giúp tập trung vào logic nghiệp vụ.
-
Giảm thời gian review: Người review không cần phải bình luận về các lỗi định dạng cơ học.
-
Dễ đọc hơn: Mã được định dạng chuẩn dễ đọc và dễ theo dõi hơn đáng kể.
-
-
Cách thực thi:
- Tích hợp vào IDE/Editor: Cấu hình trình soạn thảo mã (VS Code, GoLand, v.v.) để tự động chạy
goimportsmỗi khi lưu file. - Pre-commit Hooks: Sử dụng các công cụ như
pre-commitđể tự động chạygoimportstrước khi commit mã nguồn. - CI/CD Pipeline: Đảm bảo có một bước kiểm tra định dạng bằng
gofmthoặcgoimportstrong quy trình tích hợp liên tục (CI).
- Tích hợp vào IDE/Editor: Cấu hình trình soạn thảo mã (VS Code, GoLand, v.v.) để tự động chạy
2. Xử lý Lỗi Nhất quán và Rõ ràng
-
Quy ước:
-
Luôn kiểm tra lỗi: Nếu một hàm trả về giá trị kiểu
error, phải kiểm tra giá trị đó. Không bao giờ bỏ qua lỗi bằng định danh trống (_) trừ khi có lý do cực kỳ chính đáng và được ghi chú rõ ràng. -
Thêm ngữ cảnh khi cần: Khi trả về một lỗi từ tầng dưới, hãy sử dụng
fmt.Errorfvới động từ%wđể "wrap" lỗi gốc và thêm ngữ cảnh mô tả thao tác đang thực hiện. Ví dụ:return fmt.Errorf("opening config file %s: %w", filename, err). -
Xử lý lỗi một lần: Một lỗi chỉ nên được xử lý (ví dụ: ghi log) tại một điểm duy nhất trong chuỗi gọi, thường là ở tầng cao nhất có đủ ngữ cảnh.
-
Tránh
paniccho lỗi thông thường: Chỉ sử dụngpaniccho các lỗi nghiêm trọng, không thể phục hồi (ví dụ: lỗi lập trình, điều kiện không thể xảy ra). Luôn ưu tiên trả vềerrorcho các lỗi có thể dự đoán được. -
Chuỗi lỗi: Chuỗi lỗi trả về (qua phương thức
Error()) không nên viết hoa chữ cái đầu và không kết thúc bằng dấu chấm câu.
-
-
Giải thích:
-
Go sử dụng kiểu
errorvà trả về nhiều giá trị để xử lý lỗi một cách tường minh. Việc bỏ qua kiểm tra lỗi có thể dẫn đến các hành vi không mong muốn hoặc crash chương trình. -
Wrapping lỗi bằng
%wcho phép các tầng trên kiểm tra lỗi gốc bằngerrors.Ishoặcerrors.As, đồng thời cung cấp thông tin về đường dẫn lỗi xảy ra. -
Xử lý lỗi nhiều lần (ví dụ: log ở nhiều tầng) gây ra nhiễu thông tin và khó theo dõi.
-
-
Tại sao quan trọng cho đội ngũ:
- Độ tin cậy: Đảm bảo các trạng thái lỗi được xử lý đúng cách, giúp ứng dụng ổn định hơn.
- Khả năng gỡ lỗi: Lỗi được wrap với ngữ cảnh rõ ràng giúp xác định nguyên nhân gốc rễ nhanh hơn nhiều.
- Tính nhất quán: Một cách tiếp cận xử lý lỗi thống nhất giúp mã dễ hiểu và dễ bảo trì hơn.
-
Cách thực thi:
- Code Review: Chú trọng kiểm tra việc xử lý lỗi trong quá trình review mã.
- Linters: Sử dụng các công cụ như
errcheck(thường có tronggolangci-lint) để tự động phát hiện các lỗi chưa được kiểm tra.
3. Quy ước Đặt tên Rõ ràng và Nhất quán
-
Quy ước:
-
MixedCaps: Sử dụngMixedCapshoặcmixedCaps(camelCase), không dùng dấu gạch dưới (_). -
Ngắn gọn nhưng Mô tả: Độ dài tên tỷ lệ thuận với phạm vi của nó. Tên ngắn cho biến cục bộ, tên mô tả hơn cho các biến/hàm có phạm vi lớn hơn.
-
Từ viết tắt (Initialisms): Đối xử như một từ, viết hoa nhất quán (ví dụ:
userID,ServeHTTP,apiClient,URL). Không dùngUserId,ServeHttp,apiUrl. -
Tên Package: Ngắn gọn, một từ, chữ thường, không dấu gạch dưới, không lặp lại trong định danh của package (ví dụ: trong package
http, dùngClientthay vìHttpClient). Tránh các tên chung chung nhưutil,common. -
Tên Hàm/Phương thức: Không tiền tố
Getcho các hàm/phương thức chỉ trả về giá trị (ví dụ:Counts()thay vìGetCounts()). -
Tên Interface: Thường có hậu tố
-ercho interface một phương thức (ví dụ:Reader,Writer). -
Tên Receiver: Ngắn (1-2 chữ cái), viết tắt của loại, nhất quán trong cùng một loại (ví dụ:
choặcclcho*Client).
-
-
Giải thích:
- Tên gọi nhất quán và có ý nghĩa giúp mã nguồn dễ đọc và dễ hiểu hơn rất nhiều.
- Quy tắc về từ viết tắt và việc không lặp lại tên package là những điểm đặc trưng của Go cần tuân thủ.
-
Tại sao quan trọng cho đội ngũ:
- Khả năng đọc: Giảm thời gian cần thiết để hiểu mục đích của biến, hàm, package.
- Khả năng bảo trì: Dễ dàng tìm kiếm, thay đổi tên và hiểu tác động của thay đổi.
- Giảm lỗi: Tên rõ ràng giúp tránh nhầm lẫn và sử dụng sai.
-
Cách thực thi:
- Code Review: Đây là cách chính để đảm bảo tuân thủ quy ước đặt tên.
- Linters: Một số linter (ví dụ:
revive,stylechecktronggolangci-lint) có thể giúp kiểm tra một phần các quy ước đặt tên.
4. Quản lý Đồng thời Cẩn thận (Vòng đời Goroutine & context.Context)
-
Quy ước:
-
Quản lý Vòng đời Goroutine: Khi khởi tạo một goroutine bằng từ khóa
go, phải đảm bảo có cách rõ ràng để nó kết thúc. Tránh goroutine bị rò rỉ (leak) do bị chặn vô thời hạn. -
Sử dụng
sync.WaitGroup: DùngWaitGroupđể chờ một nhóm các goroutine con hoàn thành trước khi goroutine cha tiếp tục. -
Truyền Biến Vòng lặp: Khi tạo goroutine trong vòng lặp, luôn truyền biến vòng lặp làm tham số cho goroutine để tránh lỗi closure phổ biến (bắt giá trị cuối cùng).
-
Sử dụng
context.Context:-
Truyền
context.Contextlàm tham số đầu tiên (thường đặt tên làctx) cho các hàm thực hiện I/O, gọi RPC, hoặc các tác vụ có thể bị hủy bỏ hoặc có deadline. -
Các hàm nhận
ctxnên lắng nghe kênhctx.Done()(thường trongselect) để dừng công việc sớm khi có tín hiệu hủy bỏ hoặc hết hạn. -
Không lưu trữ
Contexttrong struct. Truyền nó qua các tham số hàm/phương thức.
-
-
-
Giải thích:
-
Goroutine rất nhẹ và dễ tạo, nhưng nếu không quản lý cẩn thận, chúng có thể bị leak, tiêu tốn tài nguyên và gây ra các lỗi khó gỡ.
-
context.Contextlà cơ chế tiêu chuẩn và thành ngữ của Go để xử lý việc hủy bỏ, timeout và truyền dữ liệu theo phạm vi yêu cầu một cách an toàn qua các tầng và goroutine.
-
-
Tại sao quan trọng cho đội ngũ:
- Độ tin cậy: Ngăn chặn rò rỉ tài nguyên và các lỗi liên quan đến đồng thời khó gỡ lỗi (race conditions, deadlocks).
- Khả năng kiểm soát: Cho phép hủy bỏ các tác vụ không cần thiết hoặc đã quá hạn, giải phóng tài nguyên hệ thống.
- Khả năng mở rộng: Các hệ thống xử lý nhiều yêu cầu đồng thời cần cơ chế quản lý hủy bỏ và deadline hiệu quả.
-
Cách thực thi:
-
Code Review: Đặc biệt chú ý đến việc khởi tạo goroutine và sử dụng
context. -
Race Detector: Luôn chạy kiểm thử với cờ
-race(go test -race) để phát hiện data races. -
Linters: Một số linter có thể phát hiện các vấn đề liên quan đến
context(ví dụ:contextcheck).
-
5. Thiết kế Package Hợp lý (Tránh util, Sử dụng internal)
-
Quy ước:
-
Nhóm theo Chức năng: Tổ chức mã vào các package dựa trên chức năng hoặc trách nhiệm nghiệp vụ, không phải theo loại (ví dụ: tránh các package
models,controllers,helpers). -
Tránh Package Chung chung: Không tạo các package có tên mơ hồ như
util,common,shared,helpers,base. Nếu cần chia sẻ mã, hãy tìm một tên package cụ thể hơn, mô tả rõ trách nhiệm của nó. -
Sử dụng
internal: Đặt mã nguồn không dành cho việc sử dụng bởi các module bên ngoài vào thư mụcinternal. Trình biên dịch Go sẽ thực thi quy tắc này, ngăn chặn việc import mã từinternalbởi các dự án khác. -
Thư mục
pkg(Thận trọng): Chỉ đặt mã vào thư mụcpkgở gốc nếu bạn chắc chắn muốn cung cấp nó như một thư viện công khai, ổn định cho các dự án khác import. Việc đặt mã ở đây tạo ra một cam kết về API. Cân nhắc kỹ lưỡng, nhiều trường hợp đặt thư viện ở gốc hoặc tronginternallà đủ. -
Bắt đầu Đơn giản: Không cần áp dụng cấu trúc phức tạp ngay từ đầu. Bắt đầu với cấu trúc phẳng và chỉ thêm các thư mục như
internalhaycmdkhi dự án phát triển và thực sự cần thiết.
-
-
Giải thích:
-
Thiết kế package tốt giúp mã dễ hiểu, dễ tìm kiếm và giảm thiểu sự phụ thuộc không cần thiết.
-
Các package chung chung thường trở thành nơi chứa mã không liên quan, làm tăng sự phức tạp và khó bảo trì.
-
Thư mục
internallà một công cụ mạnh mẽ của Go để thực thi đóng gói ở cấp độ module, giúp phân định rõ ràng API công khai và chi tiết triển khai nội bộ, cho phép tái cấu trúc an toàn hơn.
-
-
Tại sao quan trọng cho đội ngũ:
- Khả năng bảo trì: Mã được tổ chức tốt dễ hiểu và dễ sửa đổi hơn.
- Khả năng tái sử dụng: Các package được thiết kế tốt, tập trung vào chức năng cụ thể có khả năng tái sử dụng cao hơn.
- Kiểm soát API: Thư mục
internalgiúp kiểm soát chặt chẽ những gì được coi là API công khai, giảm rủi ro phá vỡ các dự án phụ thuộc khi thay đổi mã nội bộ. - Giảm xung đột: Tên package cụ thể giúp tránh xung đột tên.
-
Cách thực thi:
- Thảo luận Thiết kế: Thảo luận về cấu trúc package khi bắt đầu dự án hoặc các tính năng lớn.
- Code Review: Đảm bảo các package mới tuân thủ nguyên tắc nhóm theo chức năng và sử dụng
internalđúng cách.