Bài trước thấy reflect mạnh: xử lý kiểu chưa biết lúc chạy. Nhưng "mạnh" đi kèm "chậm" — reflection nổi tiếng tốn kém, và người ta thường tránh nó ở đường nóng. Câu hỏi là: chậm bao nhiêu? Câu trả lời gây bất ngờ vì nó rất khác nhau tùy thao tác: đọc một field chỉ chậm gấp đôi (chấp nhận được), nhưng gọi một hàm qua reflect chậm gần trăm lần. Bài này đo cả hai bằng benchmark thật, và chỉ cách giảm chi phí.
Hai loại thao tác reflect
reflect có hai thao tác chính, chi phí rất khác nhau:
// ĐỌC FIELD — trực tiếp vs reflect
sink += t.A // trực tiếp: một phép đọc bộ nhớ
v := reflect.ValueOf(t) // reflect: tạo Value rồi đọc
sink += int(v.Field(0).Int())
// GỌI HÀM — trực tiếp vs reflect.Call
sink += cong(n, 1) // trực tiếp
args := []reflect.Value{reflect.ValueOf(n), reflect.ValueOf(1)}
out := fnVal.Call(args) // reflect: đóng hộp tham số vào slice
Điểm mấu chốt: đọc field chỉ là "offset bộ nhớ + kiểm kiểu", còn reflect.Call phải đóng hộp tất cả tham số vào một []reflect.Value (cấp phát), gọi gián tiếp, rồi bóc kết quả ra. Sự khác biệt cơ chế này dẫn tới chênh lệch hiệu năng khổng lồ.

Hình 1: Hai loại thao tác reflect — đọc field (trực tiếp/reflect/cache) và gọi hàm (trực tiếp/reflect.Call), cùng cách giảm chi phí.
Đo thật: đọc field chậm ~2x, 0 cấp phát

Hình 2: Đọc field — trực tiếp 0,998 ns, reflect cache 1,677 ns (~1,7x), reflect ngây thơ 2,237 ns (~2,2x), tất cả 0 cấp phát. Gọi hàm — trực tiếp 1,318 ns vs reflect.Call 112,5 ns (~85x, 2 cấp phát).
- Trực tiếp (
t.A): 0,998 ns, 0 cấp phát. - Reflect cache (tạo Value một lần ngoài vòng): 1,677 ns (~1,7x), 0 cấp phát.
- Reflect ngây thơ (
ValueOfmỗi lần): 2,237 ns (~2,2x), 0 cấp phát.
Đọc field qua reflect chỉ chậm ~2x và không cấp phát — chấp nhận được cho nhiều trường hợp (đây là lý do json.Marshal dùng được trong production). Và cache Value ngoài vòng lặp giảm từ 2,24 xuống 1,68 ns — mẹo tối ưu đầu tiên.
Đo thật: gọi hàm chậm ~85x, có cấp phát
- Gọi trực tiếp (
cong): 1,318 ns, 0 cấp phát. reflect.Call: 112,5 ns (~85x), 39 B / 2 cấp phát.
reflect.Call chậm hơn ~85 lần và cấp phát 2 lần — vì phải đóng hộp tham số vào []reflect.Value (cấp phát slice + box mỗi giá trị) và bóc kết quả. Đây là lý do các hệ thống dựa reflect.Call (RPC động, plugin, dependency injection gọi hàm động) chậm ở đường nóng, và vì sao chúng thường được thay bằng codegen.
Ứng dụng thực tế
Đọc field bằng reflect chấp nhận được cho serialize. ~2x và 0 cấp phát nghĩa là json.Marshal/Unmarshal dùng được trong hầu hết ứng dụng — chi phí reflect field bị lấn át bởi I/O mạng/đĩa. Chỉ ở serialize cực nóng (hàng triệu lần/giây) mới cần codegen (easyjson, ffjson) để bỏ reflect.
Tránh reflect.Call ở đường nóng bằng mọi giá. ~85x + cấp phát khiến reflect.Call không phù hợp cho mã gọi thường xuyên. Nếu cần gọi động, cân nhắc: type switch trên các kiểu đã biết, sinh code, hoặc thiết kế lại để không cần gọi động. reflect.Call chỉ ổn cho việc hiếm (khởi tạo, setup, admin).
Cache mọi thứ reflect ngoài đường nóng. reflect.TypeOf, StructField, index field, method — tính một lần lúc khởi tạo và lưu, đừng tính lại mỗi lần. Nhiều thư viện reflect nhanh (như một số ORM) làm chính điều này: phân tích struct một lần thành "kế hoạch", rồi thực thi kế hoạch nhanh.
Đánh đổi cần cân nhắc
reflect đánh đổi hiệu năng lấy tính tổng quát — biết khi nào đáng. Với code chạy ít lần (khởi tạo, config, một request) reflect hoàn toàn ổn — vài trăm ns không đáng kể. Chỉ ở đường siêu nóng (vòng lặp hàng triệu lần) mới đáng lo. Đừng "sợ reflect" một cách máy móc; đo xem nó có thật sự ở đường nóng không.
Codegen là giải pháp cho reflect nóng, nhưng thêm phức tạp build. Công cụ như easyjson sinh mã Marshal tĩnh (không reflect) từ struct, nhanh gấp nhiều lần. Nhưng chúng thêm bước codegen vào build, mã sinh ra phải cập nhật khi struct đổi, và tăng kích thước binary. Chỉ đáng cho serialize thật sự là nút thắt — đo trước.
Generics thay được nhiều reflect (Go 1.18+), an toàn và nhanh hơn. Nếu bạn dùng reflect chỉ để viết "code chung cho nhiều kiểu" mà các kiểu biết lúc biên dịch, generics làm điều đó với an toàn kiểu và không có chi phí reflect (compiler sinh mã riêng). Reflect chỉ thật cần khi kiểu chỉ biết lúc chạy (JSON động, struct tag). Chọn generics khi được.
Ba ý mang về
- Chi phí reflect rất khác nhau tùy thao tác: đo thật, đọc field qua reflect chỉ ~2x trực tiếp và 0 cấp phát (chấp nhận được, là lý do json.Marshal dùng được), nhưng
reflect.Callchậm ~85x và cấp phát 2 lần vì phải đóng hộp tham số vào[]reflect.Value. - Cache Value/Type ngoài đường nóng để giảm chi phí: đo thật, tạo Value một lần ngoài vòng lặp giảm từ 2,24 xuống 1,68 ns — phân tích struct một lần thành "kế hoạch" rồi thực thi là mẫu của các thư viện reflect nhanh.
- reflect ổn cho code ít lần/khởi tạo, đắt ở đường nóng: tránh
reflect.Callở vòng nóng (dùng type switch/codegen), dùng codegen (easyjson) cho serialize cực nóng, và ưu tiên generics khi kiểu biết lúc biên dịch (an toàn + không chi phí reflect) — nhưng đo trước, đừng "sợ reflect" máy móc.
Phần sau ta dùng chính reflect để xây một thứ thực tế: Phần sau hướng dẫn tự viết một decoder dùng struct tag và reflection — biến map[string]any thành struct theo tag, hiểu cách các thư viện như mapstructure hoạt động từ bên trong.