Bạn đang sửa một thư viện dùng chung lib và ứng dụng app dùng nó — cả hai là module riêng, cùng nằm trên máy bạn, và lib chưa được đẩy lên remote. Làm sao để app thấy bản lib bạn vừa sửa mà không phải publish sau mỗi lần đổi một dòng? Trước Go 1.18, câu trả lời là thêm replace vào go.mod — một hack dễ commit nhầm và làm bẩn lịch sử. Go 1.18 giới thiệu workspace qua file go.work, giải bài toán này sạch sẽ. Bài này đo thật cách workspace hoạt động và vì sao nó hơn hẳn replace.
Vấn đề: module local chưa publish
Xét hai module riêng nằm cạnh nhau:
/work/
├── app/ module example.com/app (import example.com/lib)
└── lib/ module example.com/lib (chưa đẩy lên remote)
app/main.go import example.com/lib, nhưng lib mới chỉ ở đĩa local. Đo thật trong go-lab (Go 1.23), build app cho lỗi:
$ go build ./app
main.go:4:2: no required module provides package example.com/lib;
to add it: go get example.com/lib
Go module cố tải example.com/lib từ mạng — nó không biết bản local ngay bên cạnh. go get cũng vô ích vì module chưa tồn tại trên remote. Đây chính là ma sát mà mọi người phát triển đa module gặp phải.

Hình 1: app dùng lib local chưa publish → build lỗi. go work init ./app ./lib sinh file go.work với danh sách use, ánh xạ nhiều module local — khác cách cũ phải thêm replace vào go.mod.
Giải pháp: go work init và use
Workspace được dựng bằng lệnh go work init với danh sách các module tham gia:
go work init ./app ./lib # sinh go.work ở thư mục cha
Lệnh này tạo file go.work ở thư mục cha, chứa:
go 1.23
use (
./app
./lib
)
Mệnh đề use liệt kê các thư mục module thuộc workspace. Khi build từ bất kỳ đâu trong cây workspace, Go ưu tiên các module local này thay vì tải từ mạng. Đo thật, sau khi có go.work:
$ go build . # từ thư mục app
build OK (dùng lib local qua go.work)
$ go run ./app
Cong(2,3) = 5 # app gọi hàm Cong trong lib local
app giờ thấy lib local ngay, gọi được lib.Cong(2, 3) trả về 5. Thêm module mới vào workspace về sau bằng go work use ./tool.
Điểm mấu chốt: go.mod vẫn sạch
Đây là lý do go.work hơn hẳn replace. Đo thật, sau khi dựng workspace, app/go.mod không có một dòng replace nào:

Hình 2: Từ thư mục app — GOWORK=off build lỗi (mất lib local), workspace bật mặc định thì Cong(2,3)=5. app/go.mod có 0 dòng replace; go env GOWORK cho thấy file workspace đang dùng. go work use thêm module, go work edit -json cho tooling.
replace (cách cũ) sửa thẳng go.mod — file này được commit, nên đường dẫn local kiểu replace example.com/lib => ../lib dễ lọt vào git và làm hỏng build của người khác (đường dẫn ../lib không tồn tại trên máy họ). go.work cố ý không nên commit (thêm vào .gitignore): mỗi lập trình viên tự dựng workspace của mình, còn go.mod giữ nguyên sạch sẽ cho mọi người. Người khác clone repo về vẫn build bình thường qua module remote thật.
Workspace điều khiển bằng biến GOWORK: go env GOWORK cho biết file đang dùng (/work/t080/go.work), GOWORK=off go build tắt workspace để build như module thuần (kiểm tra xem code có build đúng ngoài workspace không — quan trọng trước khi push). go work edit -json xuất cấu hình dạng JSON cho công cụ đọc.
Đánh đổi cần cân nhắc
go.work cho phát triển local, không cho build sản xuất. Workspace là công cụ lúc phát triển: nó ghi đè việc phân giải module để bạn sửa nhiều module cùng lúc. Nhưng khi build/CI cho sản xuất, bạn muốn dùng đúng phiên bản trong go.mod/go.sum (có kiểm tra checksum) — nên CI thường chạy GOWORK=off hoặc đơn giản không có go.work. Đừng dựa vào workspace cho reproducible build.
Đừng commit go.work trong repo đa người dùng. Vì đường dẫn use là đường dẫn local (./app, ../lib), commit go.work làm build của người khác lệ thuộc vào bố cục thư mục của bạn. Ngoại lệ: một monorepo nơi mọi module nằm cố định trong cùng cây thư mục — khi đó commit go.work có thể hợp lý vì đường dẫn nhất quán với mọi người. Cân theo bố cục dự án.
Vẫn phải cập nhật go.mod khi hoàn tất. Workspace chỉ là cầu tạm. Khi bạn sửa xong lib và publish nó, phải cập nhật require trong app/go.mod sang phiên bản mới thật (go get example.com/lib@v1.2.0), rồi mới bỏ khỏi workspace. go.work không thay thế việc quản lý phụ thuộc thật — nó chỉ giúp giai đoạn phát triển song song mượt hơn.
Ba ý mang về
go.workcho phép làm việc nhiều module local cùng lúc:go work init ./app ./libsinh file workspace với danh sáchuse, đểappdùng ngayliblocal chưa publish — đo thật, không go.work thì build lỗino required module provides package, có go.work thìCong(2,3)=5.- go.mod vẫn sạch — hơn hẳn replace thủ công: đo thật
app/go.modcó 0 dòng replace;go.workgiữ ánh xạ local riêng và không nên commit (mỗi dev tự dựng), khácreplacesửa thẳng go.mod dễ commit nhầm làm hỏng build người khác. - Workspace là công cụ phát triển, không cho build sản xuất: điều khiển bằng
GOWORK(=offđể tắt,go env GOWORKđể xem), CI nên build không workspace để dùng đúng phiên bản có checksum — và nhớ cập nhậtgo.modthật khi module đã publish.
Bài tập làm thử
Bài 1 (đọc hiểu). Bạn có hai module local nằm cạnh nhau: /work/app (import example.com/lib) và /work/lib (chưa đẩy lên remote). Nếu chạy go build ./app mà chưa có file go.work, kết quả là gì? Giải thích tại sao go get example.com/lib cũng không giải quyết được vấn đề.
Đáp án
Build sẽ báo lỗi no required module provides package example.com/lib; to add it: go get example.com/lib — vì hệ thống module Go cố tải example.com/lib từ mạng, nó không tự biết có một bản local ngay thư mục bên cạnh. go get example.com/lib cũng vô ích vì module này chưa tồn tại trên remote (chưa được publish) — không có gì để tải về cả, lệnh sẽ thất bại vì không tìm thấy module đó ở bất kỳ registry nào.
Bài 2 (vận dụng — thực hiện lệnh). Viết các lệnh cần thiết để dựng một workspace cho hai module ./app và ./lib nằm trong thư mục /work, sao cho app dùng được lib local ngay mà không cần sửa go.mod của app.
Đáp án
cd /work
go work init ./app ./lib
Lệnh này sinh ra file go.work ở thư mục /work (thư mục cha) với nội dung dạng:
go 1.23
use (
./app
./lib
)
Sau đó, chạy go build . hoặc go run ./app từ bất kỳ đâu trong cây workspace sẽ ưu tiên dùng module lib local thay vì tải từ mạng, mà không cần thêm bất kỳ dòng nào vào app/go.mod. Muốn thêm module thứ ba sau này, dùng go work use ./tool.
Bài 3 (đọc hiểu / so sánh). Giải thích tại sao cách cũ (thêm replace example.com/lib => ../lib vào go.mod của app) rủi ro hơn cách dùng go.work, dựa vào việc file nào được commit vào git.
Đáp án
go.mod là file được commit vào git — nó là một phần chính thức của module, mọi người clone repo đều nhận được nó. Nếu ai đó thêm replace example.com/lib => ../lib (một đường dẫn local, chỉ đúng trên máy người viết) rồi lỡ commit, mọi đồng nghiệp clone repo về sẽ build lỗi vì đường dẫn ../lib không tồn tại trên máy họ. Ngược lại, go.work được thiết kế để không commit (thêm vào .gitignore) — mỗi lập trình viên tự dựng workspace riêng theo bố cục thư mục của mình, trong khi go.mod được giữ hoàn toàn sạch sẽ (0 dòng replace), nên người khác clone repo về vẫn build bình thường qua module remote thật.
Bài 4 (vận dụng thực tế). Đội DevOps của bạn thắc mắc: "Nếu go.work ưu tiên dùng module local, làm sao CI đảm bảo build đúng bằng phiên bản module đã khai trong go.mod/go.sum (có kiểm tra checksum)?" Trả lời dựa theo bài học.
Đáp án
CI nên build với biến môi trường GOWORK=off (hoặc đơn giản là không có file go.work trong môi trường CI, vì nó không được commit), để build hoạt động như module thuần — dùng đúng phiên bản module khai trong go.mod/go.sum và kiểm tra checksum như bình thường. Bài học nói rõ: "workspace là công cụ lúc phát triển... khi build/CI cho sản xuất, bạn muốn dùng đúng phiên bản trong go.mod/go.sum — nên CI thường chạy GOWORK=off... Đừng dựa vào workspace cho reproducible build." go env GOWORK cũng giúp kiểm tra xem một môi trường có đang ở chế độ workspace hay không.
Bài 5 (bẫy/đánh đổi). Đội của bạn làm việc theo mô hình monorepo, nơi mọi module luôn nằm cố định ở cùng vị trí tương đối trong cây thư mục cho mọi lập trình viên. Có nên commit go.work trong trường hợp này không? Còn nếu module lib cuối cùng được publish thành công lên remote, bạn cần làm gì tiếp theo với workspace và go.mod?
Đáp án
Trong mô hình monorepo với bố cục thư mục nhất quán cho mọi người, commit go.work có thể hợp lý — đây là ngoại lệ được bài nêu rõ: "một monorepo nơi mọi module nằm cố định trong cùng cây thư mục — khi đó commit go.work có thể hợp lý vì đường dẫn nhất quán với mọi người." Về phần thứ hai: khi lib đã publish thành công, cần cập nhật require trong app/go.mod sang phiên bản thật vừa publish (ví dụ go get example.com/lib@v1.2.0), sau đó có thể bỏ lib khỏi workspace (go work use chỉnh sửa hoặc xóa file go.work nếu không còn cần phát triển song song nữa) — vì "go.work không thay thế việc quản lý phụ thuộc thật, nó chỉ giúp giai đoạn phát triển song song mượt hơn."
Phần sau ta chuyển từ công cụ sang kiến trúc phần mềm: Phần sau mổ xẻ Clean Architecture trong Go — cách tổ chức tầng, hướng phụ thuộc, và những chỗ triết lý "đơn giản" của Go va chạm với các lớp trừu tượng.