encoding/json nằm trong thư viện chuẩn và bạn sẽ dùng nó hằng ngày. Bài này về bốn chỗ nó im lặng làm điều bạn không mong đợi.
Tag và quy tắc cơ bản
type Nguoi struct {
Ten string `json:"ten"`
Tuoi int `json:"tuoi,omitempty"`
Bimat string `json:"-"`
rieng string
Luc time.Time `json:"luc"`
}
{"ten":"Minh","luc":"2026-08-13T10:00:00Z"}
Ba trường biến mất, mỗi cái vì một lý do khác nhau:
Tuoi vì omitempty và giá trị là zero. Bimat vì tag json:"-". rieng vì viết thường — bài 19 đã nói: phản chiếu không thấy trường không xuất khẩu.
Cái thứ ba là bẫy số một với người mới: struct trông đúng, JSON ra thiếu trường, không có lỗi nào.
time.Time được mã hoá thành RFC 3339 tự động, và đọc lại được — đây là một trong số ít kiểu thư viện chuẩn có xử lý JSON sẵn.
Bẫy một: trường lạ bị bỏ im lặng
{"ten":"Lan","khong_co":123} -> err=<nil>
Go bỏ qua trường không có trong struct, không báo gì. Với API bên ngoài thì đó là hành vi đúng — bên kia thêm trường mới không nên làm hỏng bạn.
Nhưng với cấu hình nội bộ thì nó nguy hiểm: gõ nhầm timeout thành timout và bạn im lặng dùng giá trị mặc định.
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
json: unknown field "khong_co"
Bật nó cho mọi tệp cấu hình. Đây là cùng bài học với FAIL_ON_UNKNOWN_PROPERTIES của Jackson ở bài 60 sê-ri Java.
Bẫy hai: số thành float64
m["n"] kiểu float64 giá trị 42
Unmarshal vào map[string]any hoặc any thì mọi số thành float64 — kể cả số nguyên. Ép về int phải qua int(m["n"].(float64)).
Và với số lớn:
{"id": 12345678901234567890} -> 1.2345678901234567e+19
Mất độ chính xác. float64 chỉ giữ chính xác được 53 bit, nên ID kiểu snowflake hay số tiền tính bằng đơn vị nhỏ nhất sẽ sai.
Hai cách chữa:
dec.UseNumber() // số thành json.Number, giữ nguyên chuỗi
hoặc — cách tôi khuyên — định nghĩa struct thay vì dùng any. Với Id int64 trong struct, Go đọc đúng.
Đây là lý do nên tránh map[string]any cho dữ liệu bạn biết cấu trúc.
Bẫy ba: không phân biệt "thiếu" với "bằng 0"
{} -> N=<nil>
{"n":0} -> N=0
Với trường int thường, cả hai trường hợp đều cho 0 và bạn không biết người gửi có ý gì. Với API PATCH, đây là khác biệt giữa "đừng đổi" và "đặt về 0".
Dùng con trỏ:
type Cau struct{ N *int `json:"n"` }
nil nghĩa là không có trong JSON; trỏ tới 0 nghĩa là có và bằng 0.
Cái giá là mã dùng phải kiểm nil khắp nơi, nên chỉ dùng ở những trường thật sự cần phân biệt.
Bẫy bốn: omitempty không làm điều bạn nghĩ
omitempty bỏ trường khi giá trị là zero value: 0, "", false, nil, slice/map rỗng.
Nghĩa là Tuoi: 0 biến mất — dù đó có thể là tuổi thật (trẻ sơ sinh) hoặc số dư thật.
Và nó không bỏ struct rỗng: Diem{} vẫn được ghi ra thành {"x":0,"y":0}. Muốn bỏ thì phải dùng con trỏ.
Với Go 1.24 có thêm omitzero phân biệt rõ hơn, nhưng trên 1.23 thì omitempty là thứ bạn có.
Streaming: Decoder và Encoder
// đọc từ HTTP request
var p Nguoi
if err := json.NewDecoder(r.Body).Decode(&p); err != nil { ... }
// ghi ra HTTP response
json.NewEncoder(w).Encode(p)
Dùng dạng này thay vì ReadAll rồi Unmarshal — nó không nạp cả thân request vào bộ nhớ.
Và luôn giới hạn kích thước với dữ liệu từ ngoài:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
Không có nó, một request 10GB làm hết bộ nhớ máy chủ.
Tuỳ biến bằng MarshalJSON
type Tien int64 // lưu bằng xu
func (t Tien) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%d.%02d"`, t/100, t%100)), nil
}
Cài json.Marshaler và json.Unmarshaler để kiểm soát hoàn toàn. Hữu ích cho tiền, ngày tháng định dạng riêng, enum thành chuỗi.
Cẩn thận đệ quy vô hạn: gọi json.Marshal(t) bên trong MarshalJSON của chính t. Cách chuẩn là định nghĩa kiểu phụ:
type alias Tien
json.Marshal(alias(t)) // alias KHÔNG có method MarshalJSON
Hiệu năng
encoding/json dùng phản chiếu và không phải nhanh nhất. Với dịch vụ throughput cao, các thư viện như json-iterator hay sonic nhanh hơn vài lần.
Nhưng đừng đổi trước khi đo — với đa số dịch vụ, JSON không phải chỗ nghẽn, và thư viện chuẩn ổn định hơn.
Thử ba mươi giây
type T struct {
Ten string
tuoi int
}
j, _ := json.Marshal(T{Ten: "a", tuoi: 30})
fmt.Println(string(j))
In ra {"Ten":"a"}. Trường tuoi biến mất và không có lỗi.
Ba mươi giây đó là lý do quy tắc chữ hoa ở bài 19 đáng nhớ.
Ngày mai: net/http — dựng máy chủ trong mười dòng.