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

Ảnh chụp đoạn mã Go nền tối minh hoạ reflect kiểm và thao tác kiểu giá trị lúc chạy, package reflect cho phép xem kiểu và sửa giá trị mà không biết chúng lúc biên dịch nền tảng của json validate ORM, một ba khái niệm Type Value Kind t bằng reflect.TypeOf u mô tả kiểu User struct các field v bằng 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 Kind là loại nền nhiều kiểu khác nhau type A int type B int có cùng Kind int Type là danh tính riêng Kind là phân loại, hai duyệt field cộng đọc tag nền tảng json validate for i bằng 0 i nhỏ hơn t.NumField i f bằng t.Field i StructField tên kiểu tag ten bằng f.Name gtri bằng v.Field i Interface lấy giá trị field ra any js bằng f.Tag.Get json đọc struct tag, ba sửa giá trị cần con trỏ cộng Elem cộng CanSet pv bằng reflect.ValueOf và 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 reflect.ValueOf u tạo bản sao không sửa được CanSet false phải ValueOf và u Elem để có Value trỏ tới bản gốc field phải export chữ hoa, bốn vì sao dùng reflect json.Marshal Unmarshal duyệt field cộng tag json để mã hóa thư viện validate đọc tag validate required ORM map sang struct gán field theo tên tag lúc chạy điểm chung xử lý kiểu không biết lúc biên dịch

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}

Ảnh chụp bảng kết quả đo thật nền tối reflect đọc kiểu duyệt field cộng tag sửa giá trị go run reflect Go 1.23 arm64 struct User 3 field, đọc kiểu và duyệt field cộng tag Type User Kind struct 3 field duyệt field Ten string bằng Minh tag json ten validate required Tuoi int bằng 30 tag json tuoi validate rỗng Email string bằng m@x.vn tag json email validate rỗng reflect đọc được tên field kiểu giá trị và struct tag lúc chạy dù không biết kiểu User lúc viết code thư viện đây chính là cách json.Marshal và validator hoạt động, sửa giá trị qua reflection pv bằng reflect.ValueOf và u Elem pv.FieldByName Tuoi SetInt 31 pv.FieldByName Ten SetString Minh Sua sau khi sửa Ten Minh Sua Tuoi 31 Email m@x.vn struct gốc đổi, CanSet chỉ Value từ con trỏ mới sửa được v.Field 0 CanSet bằng false ValueOf u là bản sao pv.Field 0 CanSet bằng true ValueOf và u Elem từ con trỏ quy tắc reflect.ValueOf x tạo bản sao không địa chỉ hóa được CanSet false muốn sửa ValueOf và x Elem và field phải export chữ hoa sửa field không export panic, cốt lõi Type mô tả kiểu Name Kind NumField Field Tag Value mô tả giá trị Interface Field SetInt SetString Kind loại nền struct int slice phân loại khác Type duyệt đọc field cộng tag lúc chạy nền tảng json validate ORM sửa cần ValueOf và x Elem cộng field export CanSet true

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ề

  1. 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/ValueOf là cổng vào; Kind phân loại chung (nhiều kiểu khác nhau cùng Kind) khác với Type là danh tính riêng — thường switch theo Kind để xử lý.
  2. 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ách json.Marshal và validator hoạt động.
  3. 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ải ValueOf(&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.