Bạn đã dùng mapstructure (biến map[string]any từ config/JSON thành struct), viper, hay các thư viện binding form/env. Tất cả dùng chung một cơ chế: đọc struct tag qua reflect, khớp key nguồn với field, ép kiểu, và gán. Nghe phức tạp, nhưng lõi chỉ ~30 dòng. Bài này ta tự viết một decoder như vậy — chạy được thật — để hiểu các thư viện đó từ bên trong, và để tự viết binding cho định dạng riêng khi cần.
Struct với tag tùy biến
Định nghĩa struct với tag cfg của riêng ta, hỗ trợ cả option (default=) và bỏ qua (-):
type Config struct {
Host string `cfg:"host"`
Port int `cfg:"port"`
Debug bool `cfg:"debug"`
Timeout string `cfg:"timeout,default=30s"` // có default khi thiếu key
Bo string `cfg:"-"` // bỏ qua field này
}
Cú pháp tag này (key + option phân tách bằng dấu phẩy) giống hệt json:"ten,omitempty" — đó là quy ước chung mà mọi thư viện tag-based theo.

Hình 1: Decoder tự viết. Struct với tag cfg, duyệt field + phân tích tag, khớp key + ép kiểu + xử lý default.
Bốn bước của mọi decoder tag-based
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
tag := f.Tag.Get("cfg")
if tag == "" || tag == "-" { continue } // bỏ field không tag / "-"
parts := strings.Split(tag, ",") // "timeout,default=30s"
key := parts[0]
// đọc option default= từ parts[1:]
fv := rv.Field(i)
if !fv.CanSet() { continue } // field không export → bỏ
val, ok := m[key]
if !ok { // THIẾU key → dùng default
if def != "" { setFromString(fv, def) }
continue
}
setValue(fv, val) // CÓ key → gán + ép kiểu
}
Bốn bước cốt lõi: (1) duyệt field qua reflect, (2) đọc + phân tích tag (key + option), (3) khớp key nguồn với field và kiểm CanSet, (4) gán + ép kiểu — đây là phần khó nhất.
Ép kiểu: phần khó nhất
setValue xử lý việc gán, gồm ép kiểu khi kiểu nguồn khác kiểu field:
func setValue(fv reflect.Value, val any) {
vv := reflect.ValueOf(val)
if vv.Type().AssignableTo(fv.Type()) { fv.Set(vv); return } // cùng kiểu → gán
switch fv.Kind() {
case reflect.Int, reflect.Int64:
if vv.Kind() == reflect.Float64 { fv.SetInt(int64(vv.Float())) } // json number!
case reflect.String: fv.SetString(fmt.Sprintf("%v", val))
case reflect.Bool: if b, ok := val.(bool); ok { fv.SetBool(b) }
}
}
Ca kinh điển: JSON number là float64, nhưng field struct là int — phải ép. Đây là nguồn bug phổ biến khi decode JSON vào struct, và là lý do các thư viện binding phức tạp: chúng phải xử lý mọi cặp kiểu (float→int, string→time.Duration, string→enum...).
Đo thật: decoder chạy đúng

Hình 2: Input map → output struct. Mỗi field xử lý đúng: gán string, ép float64→int (Port=8080), gán bool, dùng default (Timeout=30s), bỏ qua tag "-" (Bo rỗng).
Chạy thật với input {host:"coffeecode.vn", port:float64(8080), debug:true} (thiếu timeout):
output = {Host:coffeecode.vn Port:8080 Debug:true Timeout:30s Bo:}
Mỗi field xử lý đúng: Host gán string trực tiếp; Port ép float64 (JSON number) sang int = 8080; Debug gán bool; Timeout thiếu key nên dùng default "30s" từ tag; Bo bị bỏ qua vì tag "-". Đây chính xác là cách mapstructure/viper hoạt động — chỉ trong ~30 dòng reflect.
Ứng dụng thực tế
Viết binding cho định dạng riêng. Khi cần biến CSV row, form values, biến môi trường, hay một định dạng tùy biến thành struct theo tag, mẫu này là khung. Bạn có tag riêng (csv:"...", form:"...") và logic ép kiểu phù hợp — không phụ thuộc thư viện ngoài.
Hiểu và gỡ lỗi các thư viện tag-based. Khi mapstructure không decode đúng (field bỏ trống, ép kiểu sai), hiểu cơ chế bên trong giúp bạn biết ngay lý do: field không export (CanSet=false), tag sai, hay kiểu nguồn không ép được. Bạn đọc lỗi của chúng chính xác hơn.
Gộp nhiều hành vi trong một lượt duyệt. Vì bạn kiểm soát vòng lặp reflect, có thể gộp validate + default + rename + ép kiểu trong một lượt duyệt field — hiệu quả hơn gọi nhiều thư viện riêng cho từng việc.
Đánh đổi cần cân nhắc
Ép kiểu là hố sâu — thư viện có sẵn xử lý nhiều ca hơn. Decoder ~30 dòng của ta chỉ xử lý vài kiểu cơ bản. mapstructure xử lý map lồng nhau, slice, con trỏ, time.Duration, custom decode hook, weakly-typed input... Tự viết cho mọi trường hợp là công việc lớn và dễ sai. Chỉ tự viết khi nhu cầu đơn giản và cố định; dùng thư viện khi cần đầy đủ.
Reflection ở decoder có chi phí (bài trước). Duyệt field + ép kiểu qua reflect chậm hơn code tĩnh. Với config load (một lần lúc khởi động) không đáng lo. Nhưng nếu decode mỗi request (parse body vào struct hàng nghìn lần/giây), cân nhắc cache "kế hoạch" decode theo kiểu, hoặc codegen. Đo trước.
Struct tag không được compiler kiểm. Tag là chuỗi thô — gõ sai (cfg:"hsot" thay "host") không có lỗi biên dịch, chỉ field không được gán lúc chạy. Đây là điểm yếu chung của mọi cách tiếp cận tag-based. Test kỹ, và cân nhắc validate tag lúc khởi động (fail-fast nếu tag sai).
Ba ý mang về
- Mọi decoder tag-based có bốn bước qua reflect: duyệt field (
NumField/Field), đọc + phân tích tag (key + option nhưdefault=), khớp key nguồn với field và kiểmCanSet, rồi gán + ép kiểu — đo thật một decoder ~30 dòng gán đúng 4 field, dùng default khi thiếu key, và bỏ qua field tag "-". - Ép kiểu là phần khó nhất: đo thật, JSON number là
float64nên phải ép sangint(ca kinh điển gây bug) — thư viện đầy đủ (mapstructure) xử lý nhiều cặp kiểu, map lồng nhau,time.Duration, decode hook mà decoder tự viết đơn giản không có. - Tự viết decoder để hiểu và cho nhu cầu đơn giản, dùng thư viện cho đầy đủ: mẫu này là khung cho binding định dạng riêng (CSV/form/env) và giúp gỡ lỗi mapstructure/viper — nhưng ép kiểu đầy đủ là hố sâu, reflect có chi phí (cache/codegen nếu decode nóng), và tag không được compiler kiểm (test kỹ).
Phần sau ta chuyển sang tính năng thay thế reflect ở nhiều chỗ, an toàn kiểu hơn: Phần sau mổ xẻ generics nâng cao — type set và constraint, cách định nghĩa ràng buộc kiểu phức tạp, và khi nào generics thay được reflect.