JSON Formatter Go — คู่มือ MarshalIndent()
ใช้ จัดรูปแบบและตกแต่ง JSON ฟรีโดยตรงในเบราว์เซอร์ของคุณ — ไม่ต้องติดตั้ง
ลอง จัดรูปแบบและตกแต่ง JSON ออนไลน์ →เมื่อทำงานกับ Go microservice และต้องการตรวจสอบ API response หรือไฟล์ config JSON แบบ compact มักเป็นอุปสรรคแรกเสมอ — บรรทัดเดียวที่มี field ซ้อนกันหลายร้อย ตัวแทบไม่บอกอะไรได้เลยเมื่อมองผ่านๆ สำหรับการ จัดรูปแบบ JSON ใน Go standard library มีทุกอย่างที่คุณต้องการ: json.MarshalIndent มีอยู่ใน encoding/json แล้ว มาพร้อมกับการติดตั้ง Go ทุกครั้ง และไม่ต้องใช้ dependency ของบุคคลที่สาม คู่มือนี้ครอบคลุมทุกอย่าง: struct tags, custom MarshalJSON() implementations, json.Indent สำหรับ reformat raw bytes, streaming ไฟล์ขนาดใหญ่ด้วย json.Decoder, ว่าเมื่อไหรควรใช้ go-json สำหรับ high-throughput paths และ CLI one-liners สำหรับ format เร็วๆ ใน terminal ตัวอย่างทั้งหมดใช้ Go 1.21+
- ✓json.MarshalIndent(v, "", "\t") คือ standard library — ไม่มี dependency มาพร้อมการติดตั้ง Go ทุกครั้ง
- ✓Struct tags json:"field_name,omitempty" ควบคุม serialization key และละเว้น field ที่มี zero value จาก output
- ✓Implement MarshalJSON() บน type ใดก็ได้เพื่อควบคุม JSON representation อย่างสมบูรณ์
- ✓json.Indent() reformat []byte ที่ marshal แล้วโดยไม่ต้อง parse struct ใหม่ — เร็วกว่าสำหรับ raw bytes
- ✓สำหรับไฟล์ขนาดใหญ่ (>100 MB): ใช้ json.Decoder กับ Token() เพื่อ stream โดยไม่โหลดทั้งหมดเข้า memory
- ✓go-json คือ drop-in replacement ที่เร็วกว่า 3–5× จาก encoding/json สำหรับ API throughput สูง
JSON Formatting คืออะไร?
JSON formatting — หรือเรียกว่า pretty-printing — แปลง JSON string แบบ compact ที่ minified ให้เป็น layout ที่อ่านได้สำหรับมนุษย์พร้อม indentation และการขึ้นบรรทัดใหม่ ที่สอดคล้องกัน ข้อมูลพื้นฐานเหมือนกัน มีแค่ whitespace ที่เปลี่ยนแปลง JSON แบบ compact เหมาะสำหรับการส่งข้อมูลผ่านเครือข่ายที่ทุก byte สำคัญ ส่วน JSON ที่จัด รูปแบบแล้วเหมาะสำหรับ debugging, code review, การตรวจสอบ log และการเขียนไฟล์ config Package encoding/json ของ Go จัดการทั้งสองโหมดด้วยการเรียกฟังก์ชันเดียว — สลับระหว่าง compact และ indented output โดยเลือกระหว่าง json.Marshal และ json.MarshalIndent
{"service":"payments","port":8443,"workers":4}{
"service": "payments",
"port": 8443,
"workers": 4
}json.MarshalIndent() — วิธีการของ Standard Library
json.MarshalIndent อยู่ใน package encoding/json ซึ่งเป็นส่วนหนึ่งของ Go standard library — ไม่ต้องใช้ go getSignature คือ MarshalIndent(v any, prefix, indent string) ([]byte, error): string prefix จะถูกเพิ่มต้นทุกบรรทัดของ output (มักเว้นว่างเสมอ) และ indent จะถูกทำซ้ำหนึ่งครั้งต่อระดับ nesting ใส่ "\t" สำหรับ tab หรือ " " สำหรับสองช่องว่าง
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"
// ]
// }การเลือกระหว่าง tab และ space ส่วนใหญ่เป็นเรื่องของ convention ทีม หลายโปรเจกต์ Go ชอบ tab เพราะ gofmt (ซึ่ง format source code Go) ใช้ tab Indentation สองหรือสี่ช่องว่างพบได้ทั่วไป เมื่อ JSON ถูกส่งให้ consumer ที่เป็น JavaScript หรือ Python นี่คือ struct เดียวกัน กับทั้งสองรูปแบบ indent วางเคียงข้างกัน:
// Tab indent — นิยมใน Go-native tooling
data, _ := json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Two-space indent — พบบ่อยสำหรับ API ที่มี consumer JS/Python
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Four-space indent
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }json.Marshal(v) เมื่อต้องการ compact output — network payload, cache value หรือเส้นทางใดก็ตาม ที่ขนาด binary สำคัญ รับ argument ค่าเดิมและมี error semantics เดียวกัน แต่ สร้าง JSON บรรทัดเดียวโดยไม่มี whitespace ใดๆStruct Tags — ควบคุมชื่อ Field และ Omitempty
Struct tags ใน Go คือ string literal ที่วางหลัง field declaration เพื่อบอก encoding/jsonวิธี serialize แต่ละ field มี directive หลักสามอย่าง: json:"name" เปลี่ยนชื่อ field ใน output, omitempty ละเว้น field เมื่อมี zero value ของ type นั้น และ json:"-" ยกเว้น field อย่างสมบูรณ์ — มีประโยชน์สำหรับ password, internal identifier หรือ field ที่ไม่ควรออกจาก service boundary
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name,omitempty"` // ละเว้นถ้าเป็น string ว่าง
AvatarURL *string `json:"avatar_url,omitempty"` // ละเว้นถ้าเป็น nil pointer
IsAdmin bool `json:"is_admin,omitempty"` // ละเว้นถ้าเป็น false
passwordHash string // unexported — ยกเว้นอัตโนมัติ
}
// ผู้ใช้ที่มี optional field ทั้งหมดถูกกรอก
full := UserProfile{
ID: "usr_7b3c", Email: "somchai.j@example.th",
DisplayName: "สมชาย ใจดี", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "somchai.j@example.th",
// "display_name": "สมชาย ใจดี",
// "is_admin": true
// }
// ผู้ใช้ที่ไม่มี optional field — ถูกละเว้นทั้งหมด
minimal := UserProfile{ID: "usr_2a91", Email: "somying.r@example.th"}
// {
// "id": "usr_2a91",
// "email": "somying.r@example.th"
// }Tag json:"-" เป็นตัวเลือกที่ถูกต้องสำหรับ field ที่ต้องถูกยกเว้นอย่างไม่มีเงื่อนไขโดยไม่คำนึง ถึงค่า — มักเป็น secret, internal tracking field หรือข้อมูลที่ถูกต้องใน memory แต่ไม่ควร serialize ไปยังระบบภายนอกใดๆ
type AuthToken struct {
TokenID string `json:"token_id"`
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
SigningKey []byte `json:"-"` // ไม่เคย serialize
RefreshToken string `json:"-"` // ไม่เคย serialize
}
tok := AuthToken{
TokenID: "tok_8f2a", Subject: "usr_7b3c",
IssuedAt: 1741614120, ExpiresAt: 1741617720,
SigningKey: []byte("ลับ"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", " ")
// {
// "token_id": "tok_8f2a",
// "sub": "usr_7b3c",
// "iat": 1741614120,
// "exp": 1741617720
// }
// SigningKey และ RefreshToken ไม่ปรากฏเลยencoding/json โดยไม่คำนึงถึง tag ใดๆ คุณไม่ต้อง เพิ่ม json:"-" ใน field unexported — การยกเว้นเป็น อัตโนมัติและไม่สามารถ override ได้MarshalJSON() แบบกำหนดเอง — จัดการ Type ที่ไม่ใช่มาตรฐาน
Type ใดๆ ใน Go สามารถ implement interface json.Marshaler โดยนิยาม method MarshalJSON() ([]byte, error)เมื่อ encoding/json พบ type เช่นนั้น จะเรียก method แทนที่จะใช้ marshaling แบบ reflection เริ่มต้น นี่คือ pattern Go แบบ canonical สำหรับ domain type ที่ต้องการ wire representation เฉพาะ — ค่าเงิน, status enum, format เวลาแบบกำหนดเอง หรือ type ใดก็ตามที่เก็บ ข้อมูลต่างจากวิธีที่ควร serialize
Custom Type — เงินพร้อมการแปลง Cents เป็น Decimal
package main
import (
"encoding/json"
"fmt"
"log"
)
type Money struct {
Amount int64 // เก็บเป็นสตางค์เพื่อหลีกเลี่ยง floating-point drift
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: 199000, Currency: "THB"},
Tax: Money{Amount: 15920, Currency: "THB"},
Total: Money{Amount: 214920, Currency: "THB"},
}
data, err := json.MarshalIndent(inv, "", " ")
if err != nil {
log.Fatalf("marshal invoice: %v", err)
}
fmt.Println(string(data))
}
// {
// "id": "inv_9a2f91bc",
// "subtotal": { "amount": 1990, "currency": "THB", "display": "THB 1990.00" },
// "tax": { "amount": 159.2, "currency": "THB", "display": "THB 159.20" },
// "total": { "amount": 2149.2, "currency": "THB", "display": "THB 2149.20" }
// }Status Enum — String Representation
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("สถานะคำสั่งซื้อที่ไม่รู้จัก: %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"
// }MarshalJSON() และ UnmarshalJSON() เสมอ ถ้า implement แค่ marshaling การ round-trip ของ type ผ่าน JSON (serialize → เก็บ → deserialize) อาจสูญเสีย structure อย่างเงียบๆ หรือ return type ที่ผิด คู่นี้สร้าง contract ว่า type สามารถผ่าน JSON round-trip ได้UUID — Serialize เป็น String
Go standard library ไม่มี UUID type ตัวเลือกที่พบบ่อยที่สุดคือ github.com/google/uuidซึ่ง implement MarshalJSON() แล้วและ serialize เป็น quoted RFC 4122 string ถ้าใช้ raw [16]byte หรือ custom ID type ให้ implement interface เองเพื่อหลีกเลี่ยง base64-encoded binary blob ใน JSON output
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
type AuditEvent struct {
EventID uuid.UUID `json:"event_id"` // serialize เป็น "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"
// }[16]byte โดยไม่มี custom type, encoding/json จะ encode เป็น base64 string — เช่น "VQ6EAOKbQdSnFkRmVUQAAA==" ใช้ UUID type ที่ เหมาะสมเสมอหรือ implement MarshalJSON() เพื่อส่งออก format string แบบ canonical ที่มีขีดกลางเอกสารอ้างอิง Parameter ของ json.MarshalIndent()
Signature ฟังก์ชันคือ func MarshalIndent(v any, prefix, indent string) ([]byte, error)ทั้ง prefix และ indent คือ string literal — ไม่มี shorthand ตัวเลขแบบ indent=4 ของ Python
ตัวเลือก struct tag ที่พบบ่อย:
json.Indent() — Reformat JSON Bytes ที่มีอยู่
เมื่อคุณมี []byte ของ JSON อยู่แล้ว — เช่น จาก HTTP response body, คอลัมน์ jsonb ของ Postgres หรือไฟล์ที่อ่านด้วย os.ReadFile — คุณ ไม่ต้องนิยาม struct และ unmarshal ก่อน pretty-print json.Indent reformat raw bytes โดยตรงโดยเขียน indented output ลงใน bytes.Buffer
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
// จำลอง raw JSON payload จาก upstream service
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
// }Pattern ที่ฉันใช้บ่อยใน microservice คือการเรียก json.Indent ก่อนเขียนลง structured log — เพิ่ม overhead เล็กน้อยมากและทำให้ log entry อ่านง่าย ขึ้นมากในช่วงที่เกิดปัญหา ฟังก์ชันนี้มีประโยชน์เป็นพิเศษสำหรับ log HTTP response, pretty-print JSON string ที่เก็บไว้ และ pipeline แบบ format-on-read ที่ไม่มี struct definition
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
// Body ไม่ใช่ JSON ที่ valid — log แบบ raw
logger.Debug("upstream response", "status", statusCode, "body", string(body))
return
}
logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}json.Indent ไม่ validate JSON อย่างสมบูรณ์นอกเหนือ จากที่จำเป็นเชิงโครงสร้างสำหรับการแทรก whitespace สำหรับการ validate syntax อย่างสมบูรณ์ ให้เรียก json.Valid(data) ก่อนและจัดการกรณี false ก่อน พยายาม indentFormat JSON จาก File และ HTTP Response
สองสถานการณ์จริงที่พบบ่อยที่สุดใน Go service คือการ format JSON ที่อ่านจากไฟล์ บนดิสก์ (config file, fixture data, migration seed) และ pretty-print HTTP response body เพื่อ debug log หรือ test assertion ทั้งสองตามรูปแบบเดียวกัน: อ่าน bytes, เรียก json.Indent หรือ unmarshal แล้ว json.MarshalIndent, เขียนกลับหรือ log
อ่านไฟล์ → Format → เขียนกลับ
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("อ่าน %s: %w", path, err)
}
if !json.Valid(data) {
return fmt.Errorf("JSON ไม่ valid ใน %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("เขียน %s: %w", path, err)
}
return nil
}
func main() {
if err := formatJSONFile("config/database.json"); err != nil {
log.Fatalf("format config: %v", err)
}
fmt.Println("format config/database.json สำเร็จ")
}HTTP Response → Decode → Pretty-Print สำหรับ 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 health response: %v", err)
}
pretty, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("marshal health response: %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
// }Raw Response Body → json.Indent (ไม่ต้องการ Struct)
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("อ่าน 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 จาก HTTP Response ใน Go
สองแนวทางข้างต้นครอบคลุมกรณีที่พบบ่อยที่สุด: decode เป็น typed struct แล้วเรียก json.MarshalIndent (ดีที่สุดเมื่อต้องการ validate หรือตรวจสอบ field เฉพาะ) หรืออ่าน raw body bytes ด้วย io.ReadAll และเรียก json.Indent โดยตรง (ดีที่สุดสำหรับ debug log แบบเร็วเมื่อไม่มี struct definition) แนวทาง raw-bytes ง่ายกว่าแต่ไม่ให้ Go type safety หรือการเข้าถึง field — เป็นแค่การ แปลงการแสดงผลล้วนๆ ทั้งสองแนวทางจัดการ response body ขนาดใหญ่ได้อย่างถูกต้อง ตราบใดที่ body ทั้งหมดใส่ในหน่วยความจำได้
// Pattern A: decode มี type → MarshalIndent // ใช้เมื่อต้องการตรวจสอบหรือ validate field เฉพาะ 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 // ใช้สำหรับ debug log แบบเร็ว — ไม่ต้องนิยาม struct body, _ := io.ReadAll(resp.Body) var buf bytes.Buffer json.Indent(&buf, body, "", " ") fmt.Println(buf.String())
การ Format JSON ผ่าน Command-Line ในโปรเจกต์ Go
บางครั้งคุณต้อง format JSON payload ใน terminal โดยตรงโดยไม่ต้องเขียนโปรแกรม Go นี่คือ one-liner ที่ฉันจำไว้ใช้ตลอดระหว่างการพัฒนาและการรับมือกับปัญหา
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# Format เท่านั้น cat api-response.json | jq . # ดึง nested field cat api-response.json | jq '.checks.database' # Filter array cat audit-log.json | jq '.[] | select(.severity == "error")'
# main.go: อ่าน stdin, format, เขียน 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.gogofmt format source code Go ไม่ใช่ JSON อย่า pipe ไฟล์ JSON ผ่าน gofmt — จะ error หรือสร้าง output ที่จำไม่ได้ ใช้ jq . หรือ python3 -m json.tool สำหรับไฟล์ JSONทางเลือกประสิทธิภาพสูง — go-json
สำหรับ Go service ส่วนใหญ่ encoding/json เร็วพอ แต่ถ้า JSON marshaling ปรากฏใน profiler ของคุณ — พบได้บ่อยใน REST API throughput สูงหรือ service ที่ส่ง structured log line ขนาดใหญ่ทุก request — library go-json คือ drop-in replacement ที่เร็วกว่า 3–5× ด้วย API surface ที่เหมือนกัน
go get github.com/goccy/go-json
package main
import (
// แทนที่สิ่งนี้:
// "encoding/json"
// ด้วยสิ่งนี้ — API เหมือนกัน ไม่ต้องเปลี่ยน code อื่น:
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 เหมือนกับ encoding/json — แค่ความเร็วต่างกัน
}github.com/bytedance/sonic คือ Go JSON library ที่เร็วที่สุดที่มีอยู่ แต่รันได้บน amd64 และ arm64 เท่านั้น (ใช้ JIT compilation) ใช้ go-json เมื่อต้องการ portable drop-in; ใช้ sonic เมื่ออยู่บน architecture ที่รู้จักและต้องการทุก microsecond ใน hot path
การทำงานกับไฟล์ JSON ขนาดใหญ่
json.MarshalIndent และ json.Indent ทั้งคู่ต้องการ payload ทั้งหมดอยู่ในหน่วยความจำ สำหรับไฟล์ขนาดเกิน 100 MB — data export, audit log, Kafka consumer payload — ใช้ json.Decoder เพื่อ stream input และประมวลผล record ทีละตัว
Streaming JSON Array ขนาดใหญ่ด้วย json.Decoder
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("เปิด %s: %w", path, err)
}
defer file.Close()
dec := json.NewDecoder(file)
// อ่าน opening token '['
if _, err := dec.Token(); err != nil {
return fmt.Errorf("อ่าน opening token: %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)
}
// ประมวลผล event ทีละตัว — การใช้ memory คงที่
if event.Severity == "error" {
fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
}
processed++
}
fmt.Printf("ประมวลผล audit event %d รายการ
", processed)
return nil
}
func main() {
if err := processAuditLog("audit-2026-03.json"); err != nil {
log.Fatalf("ประมวลผล audit log: %v", err)
}
}NDJSON — หนึ่ง JSON Object ต่อบรรทัด
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("เปิด log: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB ต่อบรรทัด
for scanner.Scan() {
var line LogLine
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
continue // ข้ามบรรทัดที่ผิดรูปแบบ
}
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)
}
}os.ReadFile จะ allocate buffer ทั้งหมดบน heap, trigger GC pressure และอาจทำให้เกิด OOM บน container ที่จำกัด memoryข้อผิดพลาดทั่วไป
ปัญหา: การทิ้ง error ด้วย _ หมายความว่า value ที่ไม่สามารถ serialize ได้ (channel, function, complex number) สร้าง nil output อย่างเงียบๆ หรือ panic downstream เมื่อเรียก string(nil)
วิธีแก้: ตรวจสอบ error เสมอ ถ้า marshal type ที่ควรสำเร็จเสมอ log.Fatalf ที่ทำให้ panic ดีกว่าการสูญเสียข้อมูลอย่างเงียบๆ
data, _ := json.MarshalIndent(payload, "", " ") fmt.Println(string(data)) // string ว่างถ้า marshal ล้มเหลว
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))ปัญหา: fmt.Println(string(data)) เพิ่ม newline character หลัง JSON ซึ่งทำให้ pipeline ที่ถือว่า output เป็น raw bytes เสียหาย — เช่น เมื่อ pipe ไปยัง jq หรือเขียนไปยัง binary protocol
วิธีแก้: ใช้ os.Stdout.Write(data) สำหรับ binary-clean output ถ้าต้องการ trailing newline สำหรับการแสดงผลสำหรับมนุษย์ ให้เพิ่มอย่างชัดเจน
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // เพิ่ม newline พิเศษต่อท้าย
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // newline ที่ชัดเจนเฉพาะเมื่อต้องการปัญหา: ไม่มี omitempty, nil *string หรือ *int pointer จะ serialize เป็น "field": null ซึ่ง expose ชื่อ field ภายในและอาจทำให้ JSON schema validator ที่เข้มงวดด้าน consumer เสียหาย
วิธีแก้: เพิ่ม omitempty ใน pointer field ที่ต้องการให้ไม่มี (ไม่ใช่ null) ใน output nil *T ที่มี omitempty จะไม่สร้าง key ใดๆ ใน JSON เลย
type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg"` // ปรากฏเป็น null เมื่อ nil
}
// {"event_id":"evt_3c7f","error_msg":null}type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg,omitempty"` // ละเว้นเมื่อ nil
}
// {"event_id":"evt_3c7f"}ปัญหา: Unmarshal เป็น map[string]any สูญเสีย type information ต้องการ type assertion แบบ manual และสร้างลำดับ key ที่ไม่แน่นอน — ทำให้ JSON diff และการเปรียบเทียบ log ยากขึ้น
วิธีแก้: นิยาม struct ด้วย json tag ที่เหมาะสม Struct มี type-safe, marshal เร็วกว่า สร้างลำดับ field ที่แน่นอนตรงกับ struct definition และทำให้ code อธิบายตัวเอง
var result map[string]any json.Unmarshal(body, &result) port := result["port"].(float64) // ต้องการ type assertion, panic ถ้า type ผิด
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 // มี type, ปลอดภัย, เร็วencoding/json กับทางเลือก — เปรียบเทียบด่วน
ใช้ json.MarshalIndent สำหรับกรณีที่คุณควบคุม struct definition และต้องการ formatted output — config file, debug logging, test fixture และ API response logging ใช้ json.Indent เมื่อมี raw bytes อยู่แล้วและต้องการแค่เพิ่ม whitespace โดยไม่ต้อง round-trip ผ่าน Go type เปลี่ยนไปใช้ go-json หรือ sonic หลังจาก profiling ยืนยันว่า JSON marshaling เป็น bottleneck ที่วัดได้เท่านั้น — สำหรับ service ส่วนใหญ่ standard library เพียงพอมากกว่า
คำถามที่พบบ่อย
จะ pretty print JSON ใน Go ได้อย่างไร?
เรียก json.MarshalIndent(v, "", "\t") จาก package encoding/json — argument ที่สองคือ prefix ต่อบรรทัด (มักว่างเปล่า) และที่สามคือตัวอักษรย่อหน้าต่อระดับ ใช้ "\t" สำหรับ tab หรือ " " สำหรับสองช่องว่าง ไม่จำเป็นต้องใช้ library ภายนอก encoding/json มาพร้อมกับ Go standard library
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"
// }
}ความแตกต่างระหว่าง json.Marshal และ json.MarshalIndent คืออะไร?
json.Marshal สร้าง JSON แบบ compact ในบรรทัดเดียวโดยไม่มี whitespace — เหมาะสำหรับการส่งผ่านเครือข่ายที่ทุก byte สำคัญ json.MarshalIndent รับ string parameter พิเศษ 2 ตัว (prefix และ indent) และสร้าง output แบบ indented ที่อ่านได้สำหรับมนุษย์ ทั้งสองฟังก์ชันรับ value type เดียวกันและ return ([]byte, error) ต้นทุนเดียวของ MarshalIndent คือ bytes output มากขึ้นเล็กน้อยและ CPU ที่ไม่มีนัยสำคัญสำหรับการแทรก whitespace
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
// }จะ format []byte JSON โดยไม่ต้อง unmarshal struct ได้อย่างไร?
ใช้ json.Indent(&buf, src, "", "\t") ฟังก์ชันนี้รับ []byte ของ JSON ที่มีอยู่แล้วและเขียน version ที่ indent แล้วลงใน bytes.Buffer — ไม่ต้องนิยาม struct ไม่ต้อง type assertion ไม่ต้อง round-trip ผ่าน Go types เป็นตัวเลือกที่เร็วที่สุดเมื่อคุณมี raw JSON bytes อยู่แล้ว เช่น จาก HTTP response body หรือ database column
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
raw := []byte(`{"endpoint":"/api/v2/invoice","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/invoice",
// "page": 1,
// "per_page": 50,
// "total": 312
// }ทำไม json.MarshalIndent ถึง return error?
encoding/json return error เมื่อ value ไม่สามารถแสดงเป็น JSON ได้ สาเหตุที่พบบ่อยที่สุด: การ marshal channel, function หรือตัวเลขซับซ้อน (ไม่มี JSON equivalent); struct ที่ implement MarshalJSON() และ return error; หรือ map ที่มี key ที่ไม่ใช่ string สิ่งสำคัญ: การ marshal struct ที่มี unexported หรือ nil pointer field ไม่ก่อให้เกิด error — สิ่งเหล่านี้จะถูกละเว้นเท่านั้น
import (
"encoding/json"
"fmt"
)
// นี่จะ return error — channel ไม่สามารถ serialize เป็น JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", " ")
fmt.Println(err)
// json: unsupported type: chan int
// นี่ไม่มีปัญหา — nil pointer field ที่มี omitempty จะถูกละเว้นอย่างเงียบ
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 ถูกละเว้น ไม่มี errorจะยกเว้น field จาก JSON output ใน Go ได้อย่างไร?
มีสามวิธี แรก ใช้ struct tag json:"-" — field จะถูกยกเว้นเสมอโดยไม่คำนึงถึงค่า สอง ใช้ omitempty — field จะถูกยกเว้นเฉพาะเมื่อมี zero value ของ type นั้น (nil pointer, string ว่าง, 0, false) สาม field unexported (ตัวพิมพ์เล็ก) จะถูกยกเว้นโดยอัตโนมัติโดย encoding/json โดยไม่ต้องมี tag ใดๆ
type PaymentMethod struct {
ID string `json:"id"`
Last4 string `json:"last4"`
ExpiryMonth int `json:"expiry_month"`
ExpiryYear int `json:"expiry_year"`
CVV string `json:"-"` // ยกเว้นเสมอ
BillingName string `json:"billing_name,omitempty"` // ยกเว้นถ้าว่าง
internalRef string // unexported — ยกเว้นอัตโนมัติ
}
pm := PaymentMethod{
ID: "pm_9f3a", Last4: "4242",
ExpiryMonth: 12, ExpiryYear: 2028,
CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", " ")
// CVV, BillingName (ว่าง) และ internalRef ไม่ปรากฏใน outputจะจัดการ time.Time ใน JSON marshaling ได้อย่างไร?
encoding/json marshal time.Time เป็น format RFC3339Nano โดยค่าเริ่มต้น (เช่น "2026-03-10T14:22:00Z") ซึ่งเข้ากันได้กับ ISO 8601 ถ้าต้องการ format อื่น — เช่น Unix epoch integers สำหรับ legacy API หรือ custom date-only string — ให้ implement MarshalJSON() บน wrapper type ที่ embed time.Time และ return format ที่ต้องการ
import (
"encoding/json"
"fmt"
"time"
)
// พฤติกรรมเริ่มต้น — RFC3339Nano ไม่ต้องใช้ custom code
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"
// }
// Custom: Unix timestamp เป็น integer
type UnixTime struct{ time.Time }
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", u.Unix())), nil
}เครื่องมือที่เกี่ยวข้อง
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.
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.