Ba bài trước ta dựng Clean Architecture và Hexagonal — cả hai đều cần một chỗ nối dây (wiring): tạo các thành phần và tiêm chúng vào nhau theo đúng thứ tự phụ thuộc. Với một hệ nhỏ, viết tay là đủ. Nhưng khi đồ thị phụ thuộc lớn — chục service, mỗi cái cần vài thứ, mỗi thứ lại cần thứ khác — nối dây thủ công thành một hàm dài và mong manh. Java giải bằng framework DI dùng reflection lúc chạy (Spring). Go, đúng triết lý của nó, chọn cách khác: sinh mã lúc biên dịch qua wire. Bài này đo thật cả hai lối và điểm mạnh của wire.
Chuỗi phụ thuộc và nối dây thủ công
Xét một chuỗi điển hình: Config → DB → Repo → Service. Mỗi thành phần có một constructor nhận đúng thứ nó cần:
func NewConfig() *Config { ... }
func NewDB(c *Config) *DB { ... }
func NewRepo(db *DB) *Repo { ... }
func NewService(r *Repo) *Service { ... }
Nối dây thủ công nghĩa là tự gọi các constructor này theo đúng thứ tự, truyền kết quả cái trước vào cái sau:
func khoiTaoThuCong() *Service {
cfg := NewConfig()
db := NewDB(cfg) // phải tự biết DB cần Config
repo := NewRepo(db)
return NewService(repo)
}
Đo thật (Go 1.23): chạy ra mở DB: postgres://demo rồi Service chạy với DSN: postgres://demo. Cách này rõ ràng và không phụ thuộc thư viện ngoài — bạn đọc là hiểu mọi thứ nối vào nhau ra sao. Nhược điểm chỉ lộ khi hệ lớn: một hàm khởi tạo có 50 dòng, và thêm một phụ thuộc mới vào giữa chuỗi buộc bạn sửa tay đúng chỗ.

Hình 1: Chuỗi Config → DB → Repo → Service. Nối dây thủ công gọi constructor đúng thứ tự. wire chỉ liệt kê provider trong wire.Build, để wire tự suy thứ tự từ kiểu tham số và sinh ra wire_gen.go — đúng mã bạn viết tay.
wire: khai báo provider, để công cụ nối
wire (google/wire) đảo ngược công việc: bạn khai báo tập provider và để công cụ sinh mã nối dây. Trong một file có build tag đặc biệt wireinject, bạn viết một hàm "injector" chỉ liệt kê các constructor:
//go:build wireinject
func InitService() *Service {
wire.Build(NewConfig, NewDB, NewRepo, NewService) // chỉ LIỆT KÊ, không cần thứ tự
return nil
}
Chú ý: bạn không viết thứ tự gọi — chỉ liệt kê provider. Chạy wire ./..., công cụ đọc chữ ký các constructor, khớp kiểu (ai trả *DB, ai cần *DB) để suy ra đồ thị phụ thuộc, rồi sinh file wire_gen.go:
func InitService() *Service {
config := NewConfig()
db := NewDB(config) // wire tự suy: DB cần Config
repo := NewRepo(db)
service := NewService(repo)
return service
}
Đây chính xác là mã bạn sẽ viết tay. wire không phải phép màu runtime — nó là trình sinh mã: kết quả là Go thường, đọc và debug được như code người viết.
Đo thật: sinh mã, an toàn lúc biên dịch, 0 reflection
Ba đo thật làm rõ vì sao wire hợp với Go:

Hình 2: Cả hai cách cho cùng kết quả. go list cho thấy wire.go (tag wireinject) bị loại, chỉ wire_gen.go được build; grep reflect trả 0. Bỏ NewRepo khỏi provider set → wire báo lỗi no provider found for *di.Repo ngay lúc sinh mã.
Một, wire là sinh mã compile-time. File wire.go có tag wireinject nên bị loại khỏi build thật; chỉ wire_gen.go được biên dịch (đo thật go list cho [components.go main.go wire_gen.go]). Quan trọng: grep reflect wire_gen.go trả 0 — không một chút reflection. Khác hẳn framework DI runtime (dùng reflection để dựng đồ thị lúc khởi động), wire không tốn một chu kỳ CPU nào lúc chạy.
Hai, wire bắt lỗi lúc sinh mã. Bỏ NewRepo khỏi wire.Build, chạy wire báo ngay: inject InitService: no provider found for *di.Repo needed by *di.Service in provider "NewService" rồi generate failed. Đây là điểm mạnh cốt lõi: một phụ thuộc thiếu bị bắt lúc sinh mã, trước khi build, không phải panic lúc server khởi động trên production như các framework DI dựa reflection.
Ba, kết quả nhất quán. Cả khoiTaoThuCong() thủ công và InitService() do wire sinh cho ra output y hệt — vì wire chỉ sinh đúng mã bạn sẽ viết tay.
Đánh đổi cần cân nhắc
Dự án nhỏ và vừa: nối dây thủ công thường đủ. Đừng thêm wire chỉ vì nó "chuyên nghiệp". Với dưới chục thành phần, một hàm khoiTao() viết tay rõ ràng hơn, không cần cài công cụ, không có file sinh ra cần commit và regenerate. Cộng đồng Go phần lớn nối dây thủ công cho service cỡ vừa — và điều đó hoàn toàn ổn.
wire trả cổ tức khi đồ thị phụ thuộc lớn và hay đổi. Khi bạn có 30 provider và thường xuyên thêm/bớt thành phần, viết tay dễ sai thứ tự và mệt mỏi. wire tự suy đồ thị, nên thêm một provider chỉ là thêm một tên vào wire.Build rồi chạy lại wire. Nó cũng bắt phụ thuộc vòng và thiếu provider mà mắt người dễ bỏ sót.
wire là sinh mã, nên phải commit và regenerate. Giống stringer, wire_gen.go phải commit vào repo (kèm // Code generated ... DO NOT EDIT), và CI nên chạy wire rồi kiểm git diff sạch. Quên regenerate sau khi đổi provider là nguồn lỗi. Đây là chi phí kỷ luật mà framework DI runtime không có — đổi lại wire cho an toàn compile-time và 0 chi phí runtime.
Ba ý mang về
- DI trong Go có hai lối: nối dây thủ công (tự gọi constructor đúng thứ tự — rõ ràng, không phụ thuộc ngoài) và wire (khai báo tập provider, để công cụ suy đồ thị từ kiểu tham số và sinh mã nối) — đo thật cả hai cho cùng kết quả.
- wire là sinh mã compile-time, không reflection: đo thật
wire_gen.gocó 0 lần dùngreflect(khác framework DI runtime), và file khai báowire.gobị loại khỏi build nhờ tagwireinject— DI không tốn chu kỳ CPU nào lúc chạy. - wire bắt lỗi lúc sinh mã, không panic runtime: thiếu một provider →
no provider foundngay khi chạywire, trước khi build — nhưng chỉ đáng dùng khi đồ thị phụ thuộc lớn; dự án nhỏ nối dây thủ công là đủ, và phải commit + regeneratewire_gen.go.
Bài tập làm thử
Bài 1 (đọc hiểu). Cho chuỗi constructor:
func NewConfig() *Config { ... }
func NewDB(c *Config) *DB { ... }
func NewRepo(db *DB) *Repo { ... }
func NewService(r *Repo) *Service { ... }
Với nối dây thủ công, đoạn mã khoiTaoThuCong phải tự biết điều gì mà wire.Build(NewConfig, NewDB, NewRepo, NewService) không cần biết?
Đáp án
Nối dây thủ công phải tự biết thứ tự gọi đúng (Config trước, rồi DB, rồi Repo, rồi Service) và tự truyền tay kết quả bước trước vào bước sau. wire.Build chỉ cần liệt kê tập provider — không cần khai thứ tự — vì wire tự suy đồ thị phụ thuộc bằng cách khớp kiểu tham số (constructor nào trả *DB, constructor nào cần *DB) rồi sinh đúng thứ tự gọi vào wire_gen.go.
Bài 2 (sửa lỗi). Một injector wire được viết như sau nhưng khi chạy wire ./... báo lỗi no provider found for *di.Repo needed by *di.Service in provider "NewService":
//go:build wireinject
func InitService() *Service {
wire.Build(NewConfig, NewDB, NewService)
return nil
}
Sửa lỗi này.
Đáp án
Thiếu NewRepo trong danh sách provider — NewService cần một *Repo nhưng không có constructor nào trong wire.Build tạo ra nó. Sửa: thêm NewRepo vào danh sách:
wire.Build(NewConfig, NewDB, NewRepo, NewService)
Đúng như bài viết mô tả: wire bắt thiếu provider ngay lúc sinh mã (wire generate failed), trước khi build, chứ không phải panic lúc runtime như framework DI dùng reflection.
Bài 3 (vận dụng). Giả sử bạn thêm một thành phần mới Cache mà Service cũng cần, với constructor func NewCache(c *Config) *Cache. Viết lại chữ ký NewService để nhận thêm Cache, và sửa wire.Build cho phù hợp.
Đáp án
func NewService(r *Repo, ca *Cache) *Service { ... }
//go:build wireinject
func InitService() *Service {
wire.Build(NewConfig, NewDB, NewRepo, NewCache, NewService)
return nil
}
Chỉ cần thêm NewCache vào danh sách provider — wire tự khớp kiểu *Cache do NewCache trả về với tham số ca *Cache mà NewService cần, không cần sửa tay thứ tự gọi. Sau đó chạy lại wire ./... để tái sinh wire_gen.go.
Bài 4 (bẫy/đánh đổi). Đo thật cho thấy grep -c reflect wire_gen.go trả về 0. Vì sao con số này quan trọng khi so sánh wire với các framework DI kiểu Spring (Java)?
Đáp án
Framework DI kiểu Spring dùng reflection lúc chạy để dựng đồ thị phụ thuộc (đọc annotation, tạo bean động), tốn chi phí CPU lúc khởi động và lỗi thiếu phụ thuộc chỉ lộ ra bằng panic lúc runtime. wire ngược lại là trình sinh mã compile-time: wire_gen.go là Go thường không dùng reflect một lần nào (0 reflection), nên không tốn chi phí runtime nào để nối dây, và lỗi thiếu provider bị bắt ngay lúc chạy lệnh wire (sinh mã), trước khi build — an toàn sớm hơn và nhanh hơn lúc chạy.
Bài 5 (đọc hiểu/đánh đổi). Vì sao bài viết khuyên "dự án nhỏ và vừa: nối dây thủ công thường đủ", và cái giá phải trả khi dùng wire là gì?
Đáp án
Với dưới chục thành phần, một hàm khởi tạo viết tay đã đủ rõ ràng, không cần cài thêm công cụ ngoài, không sinh ra file cần commit và regenerate. wire chỉ trả cổ tức khi đồ thị phụ thuộc lớn (hàng chục provider) và hay thay đổi — lúc đó viết tay dễ sai thứ tự. Cái giá của wire: wire_gen.go là mã sinh ra nên phải commit vào repo (kèm chú thích "DO NOT EDIT"), và CI nên chạy lại wire rồi kiểm git diff sạch để tránh quên regenerate sau khi đổi provider — một kỷ luật thêm mà nối dây thủ công không cần.
Phần sau ta xét một mẫu cấu hình thanh lịch mà Go dùng khắp nơi để khởi tạo linh hoạt: Phần sau mổ xẻ functional options nâng cao — cách dùng closure làm tham số cấu hình, và các kỹ thuật ít người biết để làm API mở rộng mà vẫn tương thích ngược.