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

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.