Làm sao json.Marshal(u) biết struct u của bạn có những field nào, tên tag JSON là gì, để chuyển thành {"ten":"Minh",...}? Nó không biết kiểu của bạn lúc biên dịch — nó dùng reflection để khám phá cấu trúc lúc chạy. Package reflect là công cụ cho việc này: kiểm kiểu, đọc field và tag, sửa giá trị mà không cần biết kiểu cụ thể khi viết code. Bài này mổ xẻ ba khái niệm cốt lõi — Type, Value, Kind — bằng ví dụ thật.
Type, Value, Kind: ba khái niệm cốt lõi
reflect có hai cổng vào chính: TypeOf (mô tả kiểu) và ValueOf (mô tả giá trị):
t := reflect.TypeOf(u) // mô tả KIỂU (User, struct, các field)
v := reflect.ValueOf(u) // mô tả GIÁ TRỊ (đọc/ghi được)
t.Name() // "User" — tên kiểu
t.Kind() // struct — LOẠI nền (struct/int/slice/ptr...)
t.NumField() // số field
Phân biệt quan trọng: Kind là "loại nền". Nhiều kiểu khác nhau (type A int, type B int) có cùng Kind=int nhưng khác Type (A vs B). Type là danh tính riêng của kiểu; Kind là phân loại chung. Khi viết code reflect, bạn thường switch theo Kind để xử lý (struct thì duyệt field, slice thì duyệt phần tử...).

Hình 1: reflect. Ba khái niệm Type/Value/Kind, duyệt field + đọc tag, sửa giá trị qua con trỏ, và vì sao dùng reflect.
Duyệt field và đọc tag: nền tảng json/validate
Đây là ứng dụng phổ biến nhất của reflect — duyệt field một struct và đọc struct tag:
for i := 0; i < t.NumField(); i++ {
f := t.Field(i) // StructField: tên, kiểu, tag
ten := f.Name
gtri := v.Field(i).Interface() // lấy giá trị field ra any
js := f.Tag.Get("json") // đọc struct tag
}
Đo thật trên type User struct { Ten string \json:"ten" validate:"required"` ... }`:
Type=User Kind=struct; 3 field
duyệt field:
Ten (string) = Minh | tag json="ten" validate="required"
Tuoi (int) = 30 | tag json="tuoi" validate=""
Email (string) = m@x.vn| tag json="email" validate=""
reflect đọc được tên field, kiểu, giá trị, và struct tag lúc chạy — dù người viết json.Marshal không hề biết kiểu User khi viết thư viện. Đây chính xác là cách json.Marshal, các thư viện validate (validate:"required"), và ORM hoạt động: duyệt field + đọc tag để quyết định mã hóa/kiểm tra/ánh xạ.
Sửa giá trị: quy tắc CanSet
reflect còn sửa được giá trị — nhưng có quy tắc chặt:
pv := reflect.ValueOf(&u).Elem() // Value SỬA ĐƯỢC (từ con trỏ)
pv.FieldByName("Tuoi").SetInt(31)
pv.FieldByName("Ten").SetString("Minh Sua")
// → struct GỐC bị đổi thật: {Ten:Minh Sua Tuoi:31 Email:m@x.vn}

Hình 2: Đọc kiểu + duyệt field với tag; sửa giá trị qua ValueOf(&u).Elem() (struct gốc đổi); và CanSet — false với bản sao, true với Value từ con trỏ.
Quy tắc: reflect.ValueOf(x) tạo một bản sao không địa chỉ hóa được → CanSet()=false (không sửa được). Muốn sửa, phải reflect.ValueOf(&x).Elem() để có Value trỏ tới bản gốc — đo thật pv.Field(0).CanSet()=true so với v.Field(0).CanSet()=false. Và field phải export (chữ hoa); sửa field không export gây panic. Đây là nguồn bug phổ biến khi mới dùng reflect.
Ứng dụng thực tế
Hiểu json/xml/yaml marshaling. Mọi thư viện serialize dùng reflect để duyệt field + đọc tag. Hiểu reflect giúp bạn dùng struct tag đúng (json:"ten,omitempty"), gỡ lỗi khi field không được mã hóa (thường vì không export), và biết vì sao field không export bị bỏ qua.
Thư viện validate và cấu hình. validate:"required,email", env:"PORT", mapstructure:"..." — tất cả đọc tag qua reflect. Khi viết một thư viện chung xử lý mọi struct người dùng đưa vào (validator, config loader, DI container), reflect là công cụ bắt buộc.
Ánh xạ động map ↔ struct. Khi cần chuyển map[string]any (từ JSON động, DB) sang struct theo tên/tag, hoặc ngược lại, reflect cho phép gán field theo tên lúc chạy — điều không làm được với kiểu tĩnh.
Đánh đổi cần cân nhắc
reflect mất an toàn kiểu lúc biên dịch. Code reflect thao tác any và panic lúc chạy nếu sai (sửa field không export, SetInt trên field string, gọi FieldByName tên sai). Compiler không bắt được. Đây là đánh đổi lớn: bạn đổi an toàn tĩnh lấy tính linh hoạt động. Chỉ dùng khi thật sự cần xử lý kiểu chưa biết; với kiểu đã biết, code tĩnh (hoặc generics) an toàn hơn nhiều.
reflect khó đọc và khó bảo trì. Code reflect dài dòng, gián tiếp, khó theo dõi. Một hàm 5 dòng xử lý một kiểu cụ thể rõ hơn nhiều hàm reflect 50 dòng xử lý mọi kiểu. Chỉ trả cái giá phức tạp này khi tính tổng quát thật sự cần (thư viện dùng cho nhiều kiểu), không phải cho code ứng dụng biết trước kiểu.
Generics thay reflect ở nhiều chỗ (từ Go 1.18). Trước generics, reflect là cách duy nhất viết code "chung cho mọi kiểu". Giờ nhiều trường hợp (container chung, hàm map/filter) dùng generics — an toàn kiểu, nhanh hơn (không có chi phí reflect — bài sau đo). Reflect vẫn cần khi kiểu thật sự chỉ biết lúc chạy (JSON động, struct tag), nhưng đừng dùng nó cho việc generics làm được.
Ba ý mang về
- reflect có ba khái niệm cốt lõi: Type (mô tả kiểu), Value (mô tả giá trị), Kind (loại nền):
TypeOf/ValueOflà cổng vào;Kindphân loại chung (nhiều kiểu khác nhau cùng Kind) khác vớiTypelà danh tính riêng — thườngswitchtheo Kind để xử lý. - Duyệt field + đọc struct tag lúc chạy là nền tảng của json/validate/ORM: đo thật, reflect đọc được tên field, kiểu, giá trị và tag (
json:"ten" validate:"required") dù không biết kiểu lúc viết thư viện — đây chính là cáchjson.Marshalvà validator hoạt động. - Sửa giá trị qua reflect cần Value địa chỉ hóa được và field export:
reflect.ValueOf(x)là bản sao (CanSet=false), phảiValueOf(&x).Elem()để sửa (CanSet=true) và field phải chữ hoa — reflect đánh đổi an toàn kiểu tĩnh lấy linh hoạt động, panic lúc chạy nếu sai, nên chỉ dùng khi kiểu thật sự chưa biết (generics thay được nhiều chỗ).
Phần sau ta đo chính cái giá của công cụ mạnh này: Phần sau mổ xẻ chi phí reflection — benchmark truy cập field và gọi hàm qua reflect so với code tĩnh, vì sao nó chậm hơn nhiều lần, và cách giảm chi phí khi buộc phải dùng.