JSON Formatter Go — Hướng dẫn MarshalIndent()

·Systems Engineer·Đánh giá bởiTobias Müller·Đã xuất bản

Sử dụng Định dạng và Làm đẹp JSON miễn phí trực tiếp trên trình duyệt — không cần cài đặt.

Dùng thử Định dạng và Làm đẹp JSON trực tuyến →

Khi làm việc với Go microservice và cần kiểm tra phản hồi API hoặc file cấu hình, JSON dạng compact luôn là rào cản đầu tiên — một dòng với hàng trăm trường lồng nhau gần như không cho biết gì khi nhìn qua. Để định dạng JSON trong Go, thư viện chuẩn cung cấp tất cả những gì bạn cần: json.MarshalIndent đã có sẵn trong encoding/json, đi kèm với mọi bản cài đặt Go, và không cần bất kỳ dependency bên thứ ba nào. Hướng dẫn này bao gồm toàn bộ: struct tags, custom MarshalJSON() implementations, json.Indent để reformat raw bytes, streaming file lớn với json.Decoder, khi nào nên dùng go-json cho high-throughput paths, cộng với CLI one-liners để format nhanh trên terminal. Tất cả ví dụ dùng Go 1.21+.

  • json.MarshalIndent(v, "", "\t") là thư viện chuẩn — không phụ thuộc, đi kèm mọi bản cài Go.
  • Struct tags json:"field_name,omitempty" kiểm soát khóa serialization và bỏ qua các trường có giá trị zero.
  • Implement MarshalJSON() trên bất kỳ kiểu nào để kiểm soát hoàn toàn biểu diễn JSON của nó.
  • json.Indent() reformat []byte đã được marshal mà không cần parse lại struct — nhanh hơn cho raw bytes.
  • Với file lớn (>100 MB): dùng json.Decoder với Token() để stream mà không tải tất cả vào bộ nhớ.
  • go-json là drop-in replacement nhanh hơn 3–5× so với encoding/json cho API throughput cao.

Định dạng JSON là gì?

Định dạng JSON — còn gọi là pretty-printing — chuyển đổi một chuỗi JSON compact, minified thành layout dễ đọc cho người với thụt lề nhất quán và ngắt dòng. Dữ liệu bên dưới giống hệt nhau; chỉ có khoảng trắng thay đổi. JSON compact tối ưu cho truyền mạng khi mỗi byte đều quan trọng; JSON được định dạng tối ưu cho debug, review code, kiểm tra log và viết file cấu hình. Package encoding/json của Go xử lý cả hai chế độ với một lần gọi hàm — chuyển đổi giữa compact và indented output bằng cách chọn giữa json.Marshal json.MarshalIndent.

Before · json
After · json
{"service":"payments","port":8443,"workers":4}
{
	"service": "payments",
	"port": 8443,
	"workers": 4
}

json.MarshalIndent() — Cách tiếp cận thư viện chuẩn

json.MarshalIndent nằm trong package encoding/json là một phần của thư viện chuẩn Go — không cần go get. Signature của nó là MarshalIndent(v any, prefix, indent string) ([]byte, error): chuỗi prefix được thêm vào đầu mỗi dòng output (hầu như luôn để trống), và indent được lặp lại một lần mỗi cấp nesting. Dùng "\t" cho tab hoặc " " cho hai khoảng trắng.

Go — ví dụ hoạt động tối giản
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type ServiceConfig struct {
	Host       string   `json:"host"`
	Port       int      `json:"port"`
	Workers    int      `json:"workers"`
	TLSEnabled bool     `json:"tls_enabled"`
	AllowedIPs []string `json:"allowed_ips"`
}

func main() {
	cfg := ServiceConfig{
		Host:       "payments-api.internal",
		Port:       8443,
		Workers:    8,
		TLSEnabled: true,
		AllowedIPs: []string{"10.0.0.0/8", "172.16.0.0/12"},
	}

	data, err := json.MarshalIndent(cfg, "", "	")
	if err != nil {
		log.Fatalf("marshal config: %v", err)
	}
	fmt.Println(string(data))
}
// {
// 	"host": "payments-api.internal",
// 	"port": 8443,
// 	"workers": 8,
// 	"tls_enabled": true,
// 	"allowed_ips": [
// 		"10.0.0.0/8",
// 		"172.16.0.0/12"
// 	]
// }

Lựa chọn giữa tab và khoảng trắng phần lớn là quy ước của team. Nhiều dự án Go ưu tiên tab vì gofmt (định dạng source code Go) dùng tab. Thụt lề hai hoặc bốn khoảng trắng phổ biến khi JSON dành cho consumer JavaScript hoặc Python. Đây là cùng một struct với cả hai kiểu thụt lề đặt cạnh nhau:

Go — tab so với khoảng trắng
// Thụt lề tab — ưu tiên trong Go-native tooling
data, _ := json.MarshalIndent(cfg, "", "	")
// {
// 	"host": "payments-api.internal",
// 	"port": 8443
// }

// Thụt lề hai khoảng trắng — phổ biến cho API với consumer JS/Python
data, _ = json.MarshalIndent(cfg, "", "  ")
// {
//   "host": "payments-api.internal",
//   "port": 8443
// }

// Thụt lề bốn khoảng trắng
data, _ = json.MarshalIndent(cfg, "", "    ")
// {
//     "host": "payments-api.internal",
//     "port": 8443
// }
Lưu ý:Dùng json.Marshal(v) khi bạn cần output compact — network payload, giá trị cache, hoặc bất kỳ đường dẫn nào mà kích thước nhị phân quan trọng. Nó nhận cùng tham số giá trị và có cùng ngữ nghĩa lỗi, nhưng tạo ra JSON một dòng không có khoảng trắng.

Struct Tags — Kiểm soát tên trường và Omitempty

Struct tags trong Go là các chuỗi ký tự đặt sau khai báo trường, cho encoding/jsonbiết cách serialize mỗi trường. Có ba directive chính: json:"name" đổi tên trường trong output, omitempty bỏ qua trường khi nó giữ giá trị zero cho kiểu của nó, và json:"-" loại trừ hoàn toàn trường — hữu ích cho mật khẩu, identifier nội bộ, hoặc các trường không được rời khỏi ranh giới service.

Go — struct tags cho API response
type UserProfile struct {
	ID          string  `json:"id"`
	Email       string  `json:"email"`
	DisplayName string  `json:"display_name,omitempty"`  // bỏ qua nếu chuỗi rỗng
	AvatarURL   *string `json:"avatar_url,omitempty"`    // bỏ qua nếu nil pointer
	IsAdmin     bool    `json:"is_admin,omitempty"`      // bỏ qua nếu false
	passwordHash string                                   // unexported — tự động loại trừ
}

// Người dùng với tất cả trường tùy chọn đã điền
full := UserProfile{
	ID: "usr_7b3c", Email: "nguyen.van.an@example.vn",
	DisplayName: "Nguyễn Văn An", IsAdmin: true,
}
// {
//   "id": "usr_7b3c",
//   "email": "nguyen.van.an@example.vn",
//   "display_name": "Nguyễn Văn An",
//   "is_admin": true
// }

// Người dùng không có trường tùy chọn — hoàn toàn bị bỏ qua
minimal := UserProfile{ID: "usr_2a91", Email: "tran.thi.mai@example.vn"}
// {
//   "id": "usr_2a91",
//   "email": "tran.thi.mai@example.vn"
// }

Tag json:"-" là lựa chọn đúng cho các trường phải bị loại trừ vô điều kiện bất kể giá trị — thường là secret, trường theo dõi nội bộ, hoặc dữ liệu đúng trong bộ nhớ nhưng không được serialize ra hệ thống bên ngoài nào.

Go — loại trừ trường nhạy cảm
type AuthToken struct {
	TokenID      string `json:"token_id"`
	Subject      string `json:"sub"`
	IssuedAt     int64  `json:"iat"`
	ExpiresAt    int64  `json:"exp"`
	SigningKey    []byte `json:"-"`   // không bao giờ serialize
	RefreshToken string `json:"-"`   // không bao giờ serialize
}

tok := AuthToken{
	TokenID: "tok_8f2a", Subject: "usr_7b3c",
	IssuedAt: 1741614120, ExpiresAt: 1741617720,
	SigningKey: []byte("bí-mật"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", "  ")
// {
//   "token_id": "tok_8f2a",
//   "sub": "usr_7b3c",
//   "iat": 1741614120,
//   "exp": 1741617720
// }
// SigningKey và RefreshToken không bao giờ xuất hiện
Lưu ý:Trường unexported (chữ thường) luôn bị loại trừ bởi encoding/json bất kể tag nào. Bạn không cần thêm json:"-" vào trường unexported — việc loại trừ là tự động và không thể ghi đè.

MarshalJSON() tùy chỉnh — Xử lý kiểu không chuẩn

Bất kỳ kiểu Go nào đều có thể implement interface json.Marshaler bằng cách định nghĩa phương thức MarshalJSON() ([]byte, error). Khi encoding/json gặp kiểu như vậy, nó gọi phương thức thay vì marshaling dựa trên reflection mặc định. Đây là pattern Go chuẩn cho các kiểu domain cần biểu diễn wire cụ thể — giá trị tiền tệ, enum trạng thái, định dạng thời gian tùy chỉnh, hoặc bất kỳ kiểu nào lưu trữ dữ liệu khác với cách nó nên được serialize.

Kiểu tùy chỉnh — Tiền tệ với chuyển đổi từ Cents sang Decimal

Go — MarshalJSON tùy chỉnh cho Money
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Money struct {
	Amount   int64  // lưu bằng đồng để tránh drift dấu phẩy động
	Currency string
}

func (m Money) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		Amount   float64 `json:"amount"`
		Currency string  `json:"currency"`
		Display  string  `json:"display"`
	}{
		Amount:   float64(m.Amount) / 100,
		Currency: m.Currency,
		Display:  fmt.Sprintf("%s %.2f", m.Currency, float64(m.Amount)/100),
	})
}

type Invoice struct {
	ID       string `json:"id"`
	Subtotal Money  `json:"subtotal"`
	Tax      Money  `json:"tax"`
	Total    Money  `json:"total"`
}

func main() {
	inv := Invoice{
		ID:       "inv_9a2f91bc",
		Subtotal: Money{Amount: 4990000, Currency: "VND"},
		Tax:      Money{Amount: 499000, Currency: "VND"},
		Total:    Money{Amount: 5489000, Currency: "VND"},
	}
	data, err := json.MarshalIndent(inv, "", "  ")
	if err != nil {
		log.Fatalf("marshal invoice: %v", err)
	}
	fmt.Println(string(data))
}
// {
//   "id": "inv_9a2f91bc",
//   "subtotal": { "amount": 49900, "currency": "VND", "display": "VND 49900.00" },
//   "tax":      { "amount": 4990, "currency": "VND", "display": "VND 4990.00" },
//   "total":    { "amount": 54890, "currency": "VND", "display": "VND 54890.00" }
// }

Enum trạng thái — Biểu diễn chuỗi

Go — MarshalJSON tùy chỉnh cho enum trạng thái
type OrderStatus int

const (
	StatusPending OrderStatus = iota
	StatusPaid
	StatusShipped
	StatusCancelled
)

var orderStatusNames = map[OrderStatus]string{
	StatusPending:   "pending",
	StatusPaid:      "paid",
	StatusShipped:   "shipped",
	StatusCancelled: "cancelled",
}

func (s OrderStatus) MarshalJSON() ([]byte, error) {
	name, ok := orderStatusNames[s]
	if !ok {
		return nil, fmt.Errorf("trạng thái đơn hàng không xác định: %d", s)
	}
	return json.Marshal(name)
}

type Order struct {
	ID     string      `json:"id"`
	Status OrderStatus `json:"status"`
}

o := Order{ID: "ord_3c7f", Status: StatusShipped}
data, _ := json.MarshalIndent(o, "", "  ")
// {
//   "id": "ord_3c7f",
//   "status": "shipped"
// }
Lưu ý:Luôn implement cả MarshalJSON() UnmarshalJSON() cùng nhau. Nếu bạn chỉ implement marshaling, round-tripping kiểu qua JSON (serialize → lưu → deserialize) sẽ âm thầm mất cấu trúc hoặc trả về sai kiểu. Cặp này tạo ra một hợp đồng rằng kiểu có thể tồn tại qua một round-trip JSON.

UUID — Serialize dạng chuỗi

Thư viện chuẩn Go không có kiểu UUID. Lựa chọn phổ biến nhất là github.com/google/uuid, đã implement MarshalJSON() và serialize dạng chuỗi RFC 4122 có dấu nháy. Nếu bạn dùng raw [16]byte hoặc kiểu ID tùy chỉnh, hãy tự implement interface để tránh các blob nhị phân được encode base64 trong JSON output của bạn.

Go 1.21+ — serialize UUID
import (
    "encoding/json"
    "fmt"

    "github.com/google/uuid"
)

type AuditEvent struct {
    EventID   uuid.UUID `json:"event_id"`   // serialize thành "550e8400-e29b-41d4-a716-446655440000"
    SessionID uuid.UUID `json:"session_id"`
    Action    string    `json:"action"`
    ActorID   string    `json:"actor_id"`
    OccuredAt string    `json:"occurred_at"`
}

event := AuditEvent{
    EventID:   uuid.New(),
    SessionID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000"),
    Action:    "user.password_changed",
    ActorID:   "usr_7f3a91bc",
    OccuredAt: "2026-03-10T14:22:00Z",
}

data, _ := json.MarshalIndent(event, "", "  ")
fmt.Println(string(data))
// {
//   "event_id": "a4b2c1d0-...",
//   "session_id": "550e8400-e29b-41d4-a716-446655440000",
//   "action": "user.password_changed",
//   "actor_id": "usr_7f3a91bc",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }
Lưu ý:Nếu bạn lưu UUID dưới dạng [16]byte không có kiểu tùy chỉnh, encoding/json sẽ encode chúng dạng chuỗi base64 — ví dụ "VQ6EAOKbQdSnFkRmVUQAAA==". Luôn dùng kiểu UUID phù hợp hoặc implement MarshalJSON() để phát ra định dạng chuỗi có dấu gạch ngang chuẩn.

Tham khảo tham số json.MarshalIndent()

Signature hàm là func MarshalIndent(v any, prefix, indent string) ([]byte, error). Cả prefix indent đều là chuỗi ký tự — không có ký hiệu tắt số như indent=4 của Python.

Tham số
Type
Mô tả
v
any
Giá trị cần marshal — struct, map, slice hoặc kiểu nguyên thủy
prefix
string
Chuỗi thêm vào đầu mỗi dòng output (thường là "")
indent
string
Chuỗi dùng cho mỗi cấp thụt lề ("\t" hoặc " ")

Các tùy chọn struct tag phổ biến:

Tag
Tác dụng
json:"name"
Đổi tên trường thành name trong JSON output
json:"name,omitempty"
Đổi tên + bỏ qua nếu là giá trị zero (nil, "", 0, false)
json:"-"
Luôn loại trừ trường này khỏi JSON output
json:",string"
Mã hóa số hoặc bool thành giá trị chuỗi JSON

json.Indent() — Reformat các JSON Bytes đã có

Khi bạn đã có []byte của JSON — chẳng hạn từ body phản hồi HTTP, cột jsonb Postgres, hoặc file đọc bằng os.ReadFile — bạn không cần định nghĩa struct và unmarshal trước khi pretty-print. json.Indent trực tiếp reformat raw bytes bằng cách ghi output có thụt lề vào bytes.Buffer.

Go — json.Indent trên raw bytes
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	// Mô phỏng payload JSON thô từ service upstream
	raw := []byte(`{"trace_id":"tr_9a2f","service":"checkout","latency_ms":342,"error":null}`)

	var buf bytes.Buffer
	if err := json.Indent(&buf, raw, "", "	"); err != nil {
		log.Fatalf("indent: %v", err)
	}
	fmt.Println(buf.String())
}
// {
// 	"trace_id": "tr_9a2f",
// 	"service": "checkout",
// 	"latency_ms": 342,
// 	"error": null
// }

Một pattern tôi hay dùng trong microservices là gọi json.Indent trước khi ghi vào structured log — thêm overhead không đáng kể và làm cho các entry log dễ đọc hơn nhiều trong khi xử lý sự cố. Hàm này đặc biệt hữu ích để log phản hồi HTTP, pretty-print các chuỗi JSON đã lưu, và pipeline format-on-read khi định nghĩa struct không có sẵn.

Go — json.Indent cho debug logging
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
	var pretty bytes.Buffer
	if err := json.Indent(&pretty, body, "", "  "); err != nil {
		// Body không phải JSON hợp lệ — log dạng thô
		logger.Debug("upstream response", "status", statusCode, "body", string(body))
		return
	}
	logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}
Cảnh báo:json.Indent KHÔNG xác thực đầy đủ JSON ngoài những gì cần thiết về mặt cấu trúc để chèn khoảng trắng. Để xác thực cú pháp đầy đủ, gọi json.Valid(data) trước và xử lý trường hợp false trước khi thử indent.

Định dạng JSON từ File và Phản hồi HTTP

Hai tình huống thực tế phổ biến nhất trong Go services là định dạng JSON đọc từ file trên đĩa (file cấu hình, fixture data, migration seed) và pretty-printing body phản hồi HTTP để debug log hoặc kiểm tra trong test. Cả hai đều theo cùng pattern: đọc bytes, gọi json.Indent hoặc unmarshal rồi json.MarshalIndent, ghi lại hoặc log.

Đọc File → Định dạng → Ghi lại

Go — định dạng file JSON tại chỗ
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"os"
)

func formatJSONFile(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("đọc %s: %w", path, err)
	}

	if !json.Valid(data) {
		return fmt.Errorf("JSON không hợp lệ trong %s", path)
	}

	var buf bytes.Buffer
	if err := json.Indent(&buf, data, "", "	"); err != nil {
		return fmt.Errorf("indent %s: %w", path, err)
	}

	if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
		return fmt.Errorf("ghi %s: %w", path, err)
	}
	return nil
}

func main() {
	if err := formatJSONFile("config/database.json"); err != nil {
		log.Fatalf("format config: %v", err)
	}
	fmt.Println("config/database.json đã được định dạng thành công")
}

Phản hồi HTTP → Decode → Pretty-Print cho Debug Log

Go — định dạng phản hồi HTTP cho debug log
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

type HealthResponse struct {
	Status    string            `json:"status"`
	Version   string            `json:"version"`
	Checks    map[string]string `json:"checks"`
	UptimeSec int64             `json:"uptime_seconds"`
}

func main() {
	resp, err := http.Get("https://api.payments.internal/v2/health")
	if err != nil {
		log.Fatalf("health check: %v", err)
	}
	defer resp.Body.Close()

	var result HealthResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		log.Fatalf("decode phản hồi health: %v", err)
	}

	pretty, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		log.Fatalf("marshal phản hồi health: %v", err)
	}
	fmt.Printf("Health check (%d):
%s
", resp.StatusCode, string(pretty))
}
// Health check (200):
// {
//   "status": "ok",
//   "version": "1.4.2",
//   "checks": {
//     "database": "ok",
//     "cache": "ok",
//     "queue": "degraded"
//   },
//   "uptime_seconds": 172800
// }

Body Phản hồi Thô → json.Indent (Không cần Struct)

Go — định dạng body HTTP thô với io.ReadAll
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	resp, err := http.Get("https://api.payments.internal/v2/health")
	if err != nil {
		log.Fatalf("request: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("đọc body: %v", err)
	}

	var buf bytes.Buffer
	if err := json.Indent(&buf, body, "", "  "); err != nil {
		log.Fatalf("indent: %v", err)
	}
	fmt.Println(buf.String())
}

Pretty Print JSON từ phản hồi HTTP trong Go

Hai cách tiếp cận trên bao gồm các trường hợp phổ biến nhất: decode vào struct có kiểu rồi gọi json.MarshalIndent (tốt nhất khi cần xác thực hoặc kiểm tra các trường cụ thể), hoặc đọc raw body bytes bằng io.ReadAll và gọi json.Indent trực tiếp (tốt nhất cho debug log nhanh khi không có định nghĩa struct). Cách raw-bytes đơn giản hơn nhưng không cung cấp type safety Go hay truy cập trường — đây thuần túy là phép biến đổi hiển thị. Cả hai cách đều xử lý đúng body phản hồi lớn miễn là toàn bộ body vừa trong bộ nhớ.

Go — hai pattern đặt cạnh nhau
// Pattern A: decode có kiểu → MarshalIndent
// Dùng khi cần kiểm tra hoặc xác thực trường cụ thể
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
pretty, _ := json.MarshalIndent(result, "", "  ")
fmt.Println(string(pretty))

// Pattern B: raw bytes → json.Indent
// Dùng cho debug log nhanh — không cần định nghĩa struct
body, _ := io.ReadAll(resp.Body)
var buf bytes.Buffer
json.Indent(&buf, body, "", "  ")
fmt.Println(buf.String())

Định dạng JSON qua Command-Line trong dự án Go

Đôi khi bạn cần định dạng payload JSON ngay trên terminal mà không cần viết chương trình Go. Đây là những one-liner tôi ghi nhớ trong phát triển và xử lý sự cố.

bash — pipe JSON vào formatter tích hợp của Python
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash — pipe vào jq để định dạng và lọc đầy đủ
# Chỉ định dạng
cat api-response.json | jq .

# Trích xuất trường lồng nhau
cat api-response.json | jq '.checks.database'

# Lọc mảng
cat audit-log.json | jq '.[] | select(.severity == "error")'
bash — pipe vào Go main.go tối giản qua stdin
# main.go: đọc stdin, định dạng, ghi stdout
cat <<'EOF' > /tmp/fmt.go
package main
import ("bytes";"encoding/json";"io";"os")
func main() {
    b,_:=io.ReadAll(os.Stdin)
    var buf bytes.Buffer
    json.Indent(&buf,b,"","  ")
    os.Stdout.Write(buf.Bytes())
}
EOF
echo '{"port":8080,"debug":true}' | go run /tmp/fmt.go
Lưu ý:gofmt định dạng source code Go, không phải JSON. Đừng pipe file JSON qua gofmt — nó sẽ báo lỗi hoặc tạo output không nhận ra được. Dùng jq . hoặc python3 -m json.tool cho file JSON.

Lựa chọn hiệu suất cao — go-json

Đối với đại đa số Go services, encoding/json đã đủ nhanh. Nhưng nếu JSON marshaling xuất hiện trong profiler của bạn — phổ biến trong REST API throughput cao hoặc service phát ra các dòng log lớn theo cấu trúc mỗi request — thư viện go-json là drop-in replacement nhanh hơn 3–5× với API surface hoàn toàn giống.

bash — cài đặt go-json
go get github.com/goccy/go-json
Go — thay encoding/json bằng go-json với một thay đổi import
package main

import (
	// Thay thế cái này:
	// "encoding/json"

	// Bằng cái này — API giống hệt, không cần thay đổi code nào khác:
	json "github.com/goccy/go-json"

	"fmt"
	"log"
)

type AuditEvent struct {
	RequestID string `json:"request_id"`
	UserID    string `json:"user_id"`
	Action    string `json:"action"`
	ResourceID string `json:"resource_id"`
	IPAddress  string `json:"ip_address"`
	DurationMs int    `json:"duration_ms"`
}

func main() {
	event := AuditEvent{
		RequestID: "req_7d2e91", UserID: "usr_4421",
		Action: "invoice.download", ResourceID: "inv_9a2f",
		IPAddress: "203.0.113.45", DurationMs: 23,
	}
	data, err := json.MarshalIndent(event, "", "  ")
	if err != nil {
		log.Fatalf("marshal: %v", err)
	}
	fmt.Println(string(data))
	// Output giống hệt encoding/json — chỉ có tốc độ khác
}

github.com/bytedance/sonic là thư viện JSON Go nhanh nhất hiện có, nhưng chỉ chạy trên amd64 arm64 (dùng JIT compilation). Dùng go-json khi cần drop-in portable; chuyển sang sonic khi ở kiến trúc đã biết và cần từng microsecond trong hot path.

Làm việc với File JSON lớn

json.MarshalIndent json.Indent đều yêu cầu toàn bộ payload phải ở trong bộ nhớ. Với file trên 100 MB — xuất dữ liệu, audit log, payload Kafka consumer — dùng json.Decoder để stream input và xử lý từng bản ghi một lần.

Streaming mảng JSON lớn với json.Decoder

Go — stream mảng JSON lớn mà không tải vào bộ nhớ
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type AuditEvent struct {
	RequestID  string `json:"request_id"`
	UserID     string `json:"user_id"`
	Action     string `json:"action"`
	Severity   string `json:"severity"`
	DurationMs int    `json:"duration_ms"`
}

func processAuditLog(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("mở %s: %w", path, err)
	}
	defer file.Close()

	dec := json.NewDecoder(file)

	// Đọc token mở '['
	if _, err := dec.Token(); err != nil {
		return fmt.Errorf("đọc token mở: %w", err)
	}

	var processed int
	for dec.More() {
		var event AuditEvent
		if err := dec.Decode(&event); err != nil {
			return fmt.Errorf("decode event %d: %w", processed, err)
		}
		// Xử lý một event mỗi lần — sử dụng bộ nhớ cố định
		if event.Severity == "error" {
			fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
		}
		processed++
	}
	fmt.Printf("Đã xử lý %d audit event
", processed)
	return nil
}

func main() {
	if err := processAuditLog("audit-2026-03.json"); err != nil {
		log.Fatalf("xử lý audit log: %v", err)
	}
}

NDJSON — Một đối tượng JSON mỗi dòng

Go — xử lý NDJSON log stream từng dòng
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type LogLine struct {
	Timestamp  string `json:"ts"`
	Level      string `json:"level"`
	Service    string `json:"service"`
	Message    string `json:"msg"`
	TraceID    string `json:"trace_id"`
	DurationMs int    `json:"duration_ms,omitempty"`
}

func main() {
	file, err := os.Open("service-2026-03.ndjson")
	if err != nil {
		log.Fatalf("mở log: %v", err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB mỗi dòng

	for scanner.Scan() {
		var line LogLine
		if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
			continue // bỏ qua dòng bị lỗi định dạng
		}
		if line.Level == "error" {
			fmt.Printf("%s [%s] %s trace=%s
",
				line.Timestamp, line.Service, line.Message, line.TraceID)
		}
	}
	if err := scanner.Err(); err != nil {
		log.Fatalf("scan: %v", err)
	}
}
Lưu ý:Chuyển sang streaming khi file JSON vượt quá 100 MB hoặc khi xử lý stream không giới hạn (Kafka consumer, log pipeline, đọc S3 object). Tải file JSON 500 MB bằng os.ReadFile sẽ cấp phát toàn bộ buffer trên heap, kích hoạt GC pressure, và có thể gây OOM trên container bị giới hạn bộ nhớ.

Lỗi phổ biến

Bỏ qua giá trị lỗi trả về từ MarshalIndent

Vấn đề: Bỏ lỗi bằng _ có nghĩa là một giá trị không thể serialize (channel, hàm, số phức) âm thầm tạo ra output nil hoặc panic downstream khi bạn gọi string(nil).

Giải pháp: Luôn kiểm tra lỗi. Nếu bạn đang marshal một kiểu luôn thành công, log.Fatalf gây panic tốt hơn là mất dữ liệu âm thầm.

Before · Go
After · Go
data, _ := json.MarshalIndent(payload, "", "  ")
fmt.Println(string(data)) // chuỗi rỗng nếu marshal thất bại
data, err := json.MarshalIndent(payload, "", "  ")
if err != nil {
    log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))
Dùng fmt.Println thay vì os.Stdout.Write cho binary output

Vấn đề: fmt.Println(string(data)) thêm ký tự newline sau JSON, làm hỏng các pipeline coi output là raw bytes — ví dụ khi pipe vào jq hoặc ghi vào binary protocol.

Giải pháp: Dùng os.Stdout.Write(data) cho output sạch nhị phân. Nếu cần newline cuối để hiển thị cho người dùng, thêm nó một cách rõ ràng.

Before · Go
After · Go
data, _ := json.MarshalIndent(cfg, "", "  ")
fmt.Println(string(data)) // thêm newline thừa ở cuối
data, _ := json.MarshalIndent(cfg, "", "  ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // newline rõ ràng chỉ khi cần
Quên omitempty trên trường pointer

Vấn đề: Không có omitempty, nil *string hoặc *int pointer serialize thành "field": null. Điều này lộ tên trường nội bộ và có thể phá vỡ JSON schema validator nghiêm ngặt phía consumer.

Giải pháp: Thêm omitempty vào trường pointer mà bạn muốn vắng mặt (không phải null) trong output. nil *T với omitempty không tạo key nào trong JSON.

Before · Go
After · Go
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg"`  // xuất hiện null khi nil
}
// {"event_id":"evt_3c7f","error_msg":null}
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg,omitempty"`  // bỏ qua khi nil
}
// {"event_id":"evt_3c7f"}
Dùng map[string]interface{} thay vì struct

Vấn đề: Unmarshal vào map[string]any mất thông tin kiểu, đòi hỏi type assertion thủ công, và tạo thứ tự key không xác định — làm JSON diff và so sánh log khó hơn.

Giải pháp: Định nghĩa struct với json tag phù hợp. Struct type-safe, marshal nhanh hơn, tạo thứ tự trường xác định khớp với định nghĩa struct, và làm code tự tài liệu.

Before · Go
After · Go
var result map[string]any
json.Unmarshal(body, &result)
port := result["port"].(float64) // cần type assertion, panic nếu sai kiểu
type ServiceStatus struct {
    Service string `json:"service"`
    Port    int    `json:"port"`
    Healthy bool   `json:"healthy"`
}
var result ServiceStatus
json.Unmarshal(body, &result)
port := result.Port // có kiểu, an toàn, nhanh

encoding/json và các giải pháp thay thế — So sánh nhanh

Phương thức
Output đẹp
JSON hợp lệ
Kiểu tùy chỉnh
Streaming
Cần cài đặt
json.MarshalIndent
✓ qua MarshalJSON
Không (stdlib)
json.Indent
K/A (chỉ bytes)
Không (stdlib)
json.Encoder
✗ (compact)
✓ qua MarshalJSON
Không (stdlib)
go-json
go get
sonic
go get (amd64/arm64)
jq (CLI)
K/A
Cài đặt hệ thống

Dùng json.MarshalIndent cho bất kỳ trường hợp nào bạn kiểm soát định nghĩa struct và cần formatted output — file cấu hình, debug logging, test fixture, và logging phản hồi API. Dùng json.Indent khi bạn đã có raw bytes và chỉ cần thêm khoảng trắng mà không cần round-trip qua các kiểu Go. Chuyển sang go-json hay sonic chỉ sau khi profiling xác nhận JSON marshaling là bottleneck có thể đo lường được — với hầu hết service, thư viện chuẩn là quá đủ.

Câu hỏi thường gặp

Làm thế nào để in đẹp JSON trong Go?

Gọi json.MarshalIndent(v, "", "\t") từ package encoding/json — tham số thứ hai là tiền tố mỗi dòng (thường để trống) và thứ ba là ký tự thụt lề mỗi cấp. Dùng "\t" cho tab hoặc " " cho hai khoảng trắng. Không cần thư viện ngoài; encoding/json đi kèm với thư viện chuẩn Go.

Go
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	config := map[string]any{
		"service": "payments-api",
		"port":    8443,
		"region":  "ap-southeast-1",
	}
	data, err := json.MarshalIndent(config, "", "	")
	if err != nil {
		log.Fatalf("marshal: %v", err)
	}
	fmt.Println(string(data))
	// {
	// 	"port": 8443,
	// 	"region": "ap-southeast-1",
	// 	"service": "payments-api"
	// }
}

Sự khác nhau giữa json.Marshal và json.MarshalIndent là gì?

json.Marshal tạo ra JSON một dòng dạng compact không có khoảng trắng — lý tưởng để truyền qua mạng khi mỗi byte đều quan trọng. json.MarshalIndent nhận thêm hai tham số chuỗi (prefix và indent) và tạo ra output có thụt lề, dễ đọc cho người. Cả hai hàm đều chấp nhận cùng kiểu giá trị và trả về ([]byte, error). Chi phí duy nhất của MarshalIndent là nhiều byte output hơn một chút và CPU không đáng kể để chèn khoảng trắng.

Go
import "encoding/json"

type HealthCheck struct {
	Status  string `json:"status"`
	Version string `json:"version"`
	Uptime  int    `json:"uptime_seconds"`
}

h := HealthCheck{Status: "ok", Version: "1.4.2", Uptime: 86400}

compact, _ := json.Marshal(h)
// {"status":"ok","version":"1.4.2","uptime_seconds":86400}

pretty, _ := json.MarshalIndent(h, "", "  ")
// {
//   "status": "ok",
//   "version": "1.4.2",
//   "uptime_seconds": 86400
// }

Làm thế nào để định dạng []byte JSON mà không cần unmarshal struct?

Dùng json.Indent(&buf, src, "", "\t"). Hàm này nhận một []byte JSON hiện có và ghi phiên bản có thụt lề vào bytes.Buffer — không cần định nghĩa struct, không cần type assertion, không cần round-trip qua các kiểu Go. Đây là lựa chọn nhanh nhất khi bạn đã có raw JSON bytes, chẳng hạn từ body phản hồi HTTP hoặc cột database.

Go
import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
)

raw := []byte(`{"endpoint":"/api/v2/hoa-don","page":1,"per_page":50,"total":312}`)

var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", "	"); err != nil {
	log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
// {
// 	"endpoint": "/api/v2/hoa-don",
// 	"page": 1,
// 	"per_page": 50,
// 	"total": 312
// }

Tại sao json.MarshalIndent trả về lỗi?

encoding/json trả về lỗi khi giá trị không thể được biểu diễn dưới dạng JSON. Nguyên nhân phổ biến nhất: marshal một channel, một hàm hoặc số phức (chúng không có đối tác JSON); một struct implement MarshalJSON() và trả về lỗi; hoặc map với khóa không phải chuỗi. Quan trọng là: marshal một struct có trường unexported hoặc nil pointer KHÔNG gây ra lỗi — chúng chỉ đơn giản bị bỏ qua.

Go
import (
	"encoding/json"
	"fmt"
)

// Điều này sẽ trả về lỗi — channel không thể serialize thành JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", "	")
fmt.Println(err)
// json: unsupported type: chan int

// Điều này ổn — trường nil pointer có omitempty bị bỏ qua âm thầm
type Profile struct {
	ID     string  `json:"id"`
	Avatar *string `json:"avatar,omitempty"`
}
p := Profile{ID: "usr_7b3c"}
data, _ := json.MarshalIndent(p, "", "  ")
// {"id": "usr_7b3c"}  — avatar bị bỏ qua, không có lỗi

Làm thế nào để loại trừ một trường khỏi JSON output trong Go?

Có ba cách. Thứ nhất, dùng struct tag json:"-" — trường luôn bị loại trừ bất kể giá trị. Thứ hai, dùng omitempty — trường bị loại trừ chỉ khi nó giữ giá trị zero cho kiểu của nó (nil pointer, chuỗi rỗng, 0, false). Thứ ba, các trường unexported (chữ thường) tự động bị loại trừ bởi encoding/json mà không cần tag nào.

Go
type PaymentMethod struct {
	ID           string  `json:"id"`
	Last4        string  `json:"last4"`
	ExpiryMonth  int     `json:"expiry_month"`
	ExpiryYear   int     `json:"expiry_year"`
	CVV          string  `json:"-"`                    // luôn loại trừ
	BillingName  string  `json:"billing_name,omitempty"` // loại trừ nếu trống
	internalRef  string                                 // unexported — tự động loại trừ
}

pm := PaymentMethod{
	ID: "pm_9f3a", Last4: "4242",
	ExpiryMonth: 12, ExpiryYear: 2028,
	CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", "  ")
// CVV, BillingName (trống), và internalRef không xuất hiện trong output

Làm thế nào để xử lý time.Time trong JSON marshaling?

encoding/json marshal time.Time sang định dạng RFC3339Nano theo mặc định (ví dụ "2026-03-10T14:22:00Z"), tương thích với ISO 8601. Nếu bạn cần định dạng khác — chẳng hạn integer Unix epoch cho API cũ, hoặc chuỗi ngày tháng tùy chỉnh — implement MarshalJSON() trên kiểu wrapper nhúng time.Time và trả về định dạng bạn cần.

Go
import (
	"encoding/json"
	"fmt"
	"time"
)

// Hành vi mặc định — RFC3339Nano, không cần code tùy chỉnh
type AuditEvent struct {
	Action    string    `json:"action"`
	OccurredAt time.Time `json:"occurred_at"`
}

e := AuditEvent{
	Action:    "invoice.paid",
	OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", "  ")
// {
//   "action": "invoice.paid",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }

// Tùy chỉnh: Unix timestamp dạng integer
type UnixTime struct{ time.Time }

func (u UnixTime) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("%d", u.Unix())), nil
}

Công cụ liên quan

Cũng có sẵn trong:PythonJavaScriptBash
JO
James OkaforSystems Engineer

James is a systems engineer and Go enthusiast who focuses on high-performance microservices, command-line tooling, and infrastructure automation. He enjoys the simplicity and explicitness of Go and writes about building fast, reliable backend systems. When not coding he explores distributed systems concepts and contributes to open-source Go libraries.

TM
Tobias MüllerNgười đánh giá kỹ thuật

Tobias is a platform engineer who builds developer tooling and internal infrastructure in Go. He has authored several open-source CLI tools and contributes to the Go toolchain ecosystem. He writes about the cobra and urfave/cli frameworks, cross-platform binary distribution, configuration management, and the patterns that make Go an ideal language for building reliable, self-contained command-line utilities.