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ỗ).
Bài tập làm thử
Bài 1 (đọc hiểu). Cho hai kiểu tự định nghĩa:
type A int
type B int
var a A = 5
var b B = 5
reflect.TypeOf(a) và reflect.TypeOf(b) có bằng nhau không? reflect.TypeOf(a).Kind() và reflect.TypeOf(b).Kind() thì sao? Giải thích khác biệt giữa Type và Kind qua ví dụ này.
Đáp án
reflect.TypeOf(a) khác reflect.TypeOf(b) — Type là danh tính riêng của từng kiểu (A và B là hai kiểu khác nhau dù cùng underlying int). Nhưng Kind() của cả hai đều là reflect.Int — Kind chỉ là "loại nền", phân loại chung không phân biệt các kiểu tự định nghĩa có cùng cấu trúc cơ sở. Đây là lý do khi viết code reflect tổng quát, người ta thường switch theo Kind để xử lý theo nhóm (mọi kiểu int xử lý như nhau), còn khi cần phân biệt đúng kiểu cụ thể thì phải so Type.
Bài 2 (sửa lỗi). Đoạn mã sau muốn sửa giá trị field Tuoi của một struct nhưng bị panic lúc chạy. Tìm lỗi và sửa.
type User struct {
Ten string
Tuoi int
}
func capNhatTuoi(u User, tuoiMoi int) {
v := reflect.ValueOf(u)
v.FieldByName("Tuoi").SetInt(int64(tuoiMoi))
}
Đáp án
Lỗi: reflect.ValueOf(u) tạo một bản sao không địa chỉ hóa được (CanSet() = false), nên gọi SetInt sẽ panic. Sửa lại bằng cách truyền con trỏ và dùng Elem():
func capNhatTuoi(u *User, tuoiMoi int) {
v := reflect.ValueOf(u).Elem()
v.FieldByName("Tuoi").SetInt(int64(tuoiMoi))
}
Phải nhận u là con trỏ (*User) rồi gọi .Elem() để có một Value trỏ tới bản gốc, khi đó CanSet() mới là true và việc sửa mới phản ánh lên struct gốc.
Bài 3 (vận dụng thực tế). Bạn đang viết một hàm dùng reflect để in ra tên và giá trị mọi field của một struct bất kỳ, nhưng nhận thấy một số field không được in ra dù có mặt trong struct. Dựa vào bài học, nguyên nhân phổ biến nhất là gì?
Đáp án
Nguyên nhân phổ biến nhất là các field đó không export (tên bắt đầu bằng chữ thường). reflect chỉ đọc/ghi được các field export (chữ hoa) một cách đầy đủ; với field không export, Value.Interface() sẽ panic và việc set giá trị cũng panic — nhiều đoạn code phòng vệ (như ví dụ trong bài) bỏ qua các field này bằng cách kiểm CanSet() hoặc kiểm export trước khi thao tác, nên chúng không hiện ra trong output.
Bài 4 (đọc hiểu code Go). Cho đoạn duyệt field và đọc tag trong bài:
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
ten := f.Name
gtri := v.Field(i).Interface()
js := f.Tag.Get("json")
}
Với struct type User struct { Ten string \json:"ten" validate:"required"` }, nếu đổi f.Tag.Get("json")thànhf.Tag.Get("validate"), giá trị js(giờ nên đổi tên biến ý nghĩa) nhận được là gì? Nếu field không có tagxmlmà ta gọif.Tag.Get("xml")` thì kết quả ra sao — panic hay trả về gì?
Đáp án
f.Tag.Get("validate") trả về chuỗi "required". Nếu gọi f.Tag.Get("xml") trên một field không có tag đó, hàm không panic mà trả về chuỗi rỗng ("") — StructTag.Get tra cứu key trong chuỗi tag và trả rỗng nếu không tìm thấy, đây là hành vi an toàn giúp code đọc tag không cần kiểm tồn tại trước.
Bài 5 (bẫy/đánh đổi). Một lập trình viên định viết một hàm tổng quát dùng reflect để xử lý một danh sách cố định gồm 3 kiểu struct đã biết trước lúc biên dịch (không có kiểu nào khác sẽ được thêm vào). Theo bài học "cốt lõi" và "đánh đổi", đây có phải lựa chọn tốt nhất không? Đề xuất thay thế.
Đáp án
Không phải lựa chọn tốt nhất. reflect đánh đổi an toàn kiểu tĩnh lấy tính linh hoạt lúc chạy, và code reflect dài dòng, khó đọc, panic lúc chạy (không lỗi biên dịch) nếu sai. Vì tập kiểu đã biết trước và cố định lúc biên dịch, generics (từ Go 1.18) là lựa chọn tốt hơn: an toàn kiểu, lỗi bắt được lúc biên dịch, và không có chi phí reflect. reflect chỉ nên dùng khi kiểu thật sự không biết trước lúc biên dịch (như JSON động, cấu hình đọc từ file).
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.