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.
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.