Bài 3 đã nói về go.mod cơ bản. Bài này về bốn công cụ bạn cần khi dự án lớn lên.

replace: trỏ sang bản khác

// trong go.mod
replace github.com/ai-do/thuvien => ../thuvien-cua-toi
replace github.com/ai-do/thuvien => github.com/toi/thuvien v1.2.4

Dùng khi: đang sửa một thư viện cùng lúc với dự án dùng nó, hoặc cần một bản vá chưa được merge ngược lên.

Một quy tắc quan trọng và hay gây bất ngờ:

replace chỉ có tác dụng ở module CHÍNH. Nếu thư viện của bạn có replace trong go.mod, người dùng thư viện đó sẽ không nhận được nó — và build của họ hỏng nếu bạn thật sự cần bản thay thế.

Nên với thư viện xuất bản, đừng bao giờ để replace trỏ vào đường dẫn cục bộ. Với ứng dụng thì thoải mái.

exclude để loại một phiên bản cụ thể, nhưng hiếm dùng — thường require một bản mới hơn là đủ.

Nâng cấp phụ thuộc

go list -m -u all              # xem có gì mới, KHÔNG đổi gì
go get -u ./...                # nâng minor và patch
go get -u=patch ./...          # chỉ patch, an toàn hơn
go get github.com/x/y@v1.5.0   # ghim một phiên bản
go get github.com/x/y@latest
go mod tidy                    # dọn sau mỗi lần

Nhớ Minimal Version Selection ở bài 3: Go chọn phiên bản thấp nhất thoả mãn, nên nâng cấp là hành động tường minh. Đây là điểm khác Maven và npm, và nó làm build tái lập được.

Tôi gặp đúng tình huống này khi viết bài 41: go get golang.org/x/sync mới nhất từ chối cài vì đòi Go ≥ 1.25 trong khi môi trường chạy 1.23:

  golang.org/x/sync@v0.22.0 requires go >= 1.25.0 (running go 1.23.12)

Phải ghim @v0.8.0. Thông báo lỗi của Go rất rõ, và đó là hành vi đúng — thà từ chối còn hơn build ra binary hỏng.

Đường dẫn module và phiên bản major

Từ v2 trở đi, đường dẫn module phải đổi:

module github.com/toi/thuvien/v2

Và người dùng import github.com/toi/thuvien/v2. Nghe phiền, nhưng nó cho phép v1 và v2 cùng tồn tại trong một binary — thứ Maven không làm được, và là lý do "địa ngục phụ thuộc" ít gặp hơn trong Go.

vendor: chép phụ thuộc vào kho mã

go mod vendor          # tạo thư mục vendor/
go build -mod=vendor   # tự động nếu có vendor/ và go 1.14+

Toàn bộ mã nguồn phụ thuộc nằm trong dự án. Build không cần mạng, không cần proxy.

Khi nào đáng: môi trường build không có internet, yêu cầu kiểm toán phải thấy hết mã, hoặc muốn chắc chắn build tái lập được sau nhiều năm.

Cái giá: kho mã phình to, và mọi lần nâng cấp là một diff khổng lồ.

Với đa số dự án, proxy module cộng go.sum đã đủ — bài 3 đã nói proxy.golang.org giữ bản sao vĩnh viễn. Tôi chỉ dùng vendor khi có yêu cầu cụ thể.

Workspace: nhiều module cùng lúc

Từ Go 1.18:

go work init ./api ./thuvien

Tạo go.work, và các module trong đó thấy nhau trực tiếp mà không cần replace.

Đây là cách đúng để làm việc trên nhiều module cùng lúc, thay cho mẹo replace trỏ đường dẫn cục bộ mà người ta hay quên xoá trước khi commit.

Đừng commit go.work — thêm nó vào .gitignore. Nó là cấu hình của máy bạn, không phải của dự án.

Riêng tư và proxy

GOPRIVATE=github.com/congty/*     # không qua proxy công cộng, không kiểm checksum
GOPROXY=https://proxy.golang.org,direct
GONOSUMDB=...

GOPRIVATE là thứ bạn cần cho kho nội bộ công ty. Không đặt thì Go cố gọi proxy công cộng cho module riêng tư — chậm, và về lý thuyết là lộ tên kho.

Kiểm tra sức khoẻ phụ thuộc

go mod graph | wc -l           # số cạnh trong đồ thị phụ thuộc
go mod why github.com/x/y      # AI kéo module này vào
go list -m all | wc -l         # tổng số module

go mod why là công cụ tôi dùng nhiều nhất khi thấy một phụ thuộc lạ:

  # github.com/x/y
  vd/app
  github.com/a/b
  github.com/x/y

Nó in ra chuỗi import dẫn tới module đó. Tương đương mvn dependency:tree nhưng trả lời đúng câu hỏi bạn hỏi.

Thử ba mươi giây

go list -m -u all | grep '\[' | head -20

Mỗi dòng có ngoặc vuông là một phụ thuộc đã có bản mới hơn. Danh sách đó chính là đầu vào cho bài 59 về lỗ hổng bảo mật — và như bài 99 sê-ri Java đã đo, phần lớn lỗ hổng chỉ cần nâng phiên bản là xong.

Ngày mai: build và cross-compile — nhúng số phiên bản vào binary.