本文 AI 產出,尚未審核

Golang – 檔案與 I/O 操作

主題:JSON 編解碼(encoding/json


簡介

在現代的 Web、微服務與資料交換中,JSON(JavaScript Object Notation) 已成為最常見的資料格式之一。Go 語言內建的 encoding/json 套件提供了簡潔且效能不錯的編碼(Encode)與解碼(Decode)功能,讓開發者可以毫不費力地在 Go 結構與 JSON 之間互相轉換。

掌握 JSON 編解碼不僅能讓你快速實作 API、讀寫設定檔、或是與前端、其他服務進行資料互通,更是日常開發中不可或缺的基礎技能。本文將從核心概念出發,搭配實用範例,說明如何在 Go 中正確且高效地使用 encoding/json,並提供常見陷阱與最佳實踐,幫助你在實務專案中得心應手。


核心概念

1. 基本的 MarshalUnmarshal

  • json.Marshal(v interface{}) ([]byte, error):將 Go 變數(結構、切片、地圖等)序列化成 JSON 位元組。
  • json.Unmarshal(data []byte, v interface{}) error:將 JSON 位元組反序列化成 Go 變數。

注意Marshal 只會編碼可匯出(首字母大寫)的欄位;未匯出的欄位會被忽略。

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name string // 匯出欄位
	age  int    // 不會被編碼
}

func main() {
	p := Person{Name: "Alice", age: 30}
	// Marshal
	b, err := json.Marshal(p)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b)) // {"Name":"Alice"}

	// Unmarshal
	var p2 Person
	if err := json.Unmarshal(b, &p2); err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", p2) // {Name:Alice age:0}
}

2. 使用 struct 標籤(Tag)自訂 JSON 欄位名稱

json:"fieldName,omitempty" 標籤可控制:

  • 欄位名稱:映射到 JSON 中的鍵名。
  • omitempty:當欄位為零值(0""nilfalse)時,省略該欄位。
  • -:完全忽略此欄位。
type Article struct {
	Title   string `json:"title"`                // 直接映射
	Content string `json:"content,omitempty"`   // 空字串時不輸出
	Author  string `json:"author_name"`         // 自訂鍵名
	Secret  string `json:"-"`                    // 永遠不會編碼
}

3. json.Decoderjson.Encoder:串流式 I/O

在處理大型檔案或網路連線時,直接使用 Marshal/Unmarshal 會一次把全部資料載入記憶體,可能造成記憶體壓力。json.NewDecoderjson.NewEncoder 提供 流式 讀寫,配合 io.Reader / io.Writer 使用更為高效。

package main

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

type LogEntry struct {
	Time    string `json:"time"`
	Level   string `json:"level"`
	Message string `json:"msg"`
}

func main() {
	// 讀取大型 JSON 陣列檔案,逐筆解碼
	f, err := os.Open("logs.json")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	dec := json.NewDecoder(f)

	// 期待檔案內容是 [ {...}, {...}, ... ]
	// 先讀取開頭的 '['
	_, err = dec.Token()
	if err != nil {
		log.Fatal(err)
	}

	for dec.More() {
		var e LogEntry
		if err := dec.Decode(&e); err != nil {
			log.Fatal(err)
		}
		log.Printf("%s - %s: %s", e.Time, e.Level, e.Message)
	}
}

4. 自訂編碼/解碼行為:實作 json.Marshalerjson.Unmarshaler

有時候欄位的資料型別與 JSON 表示方式不一致(例如時間格式、十六進位字串),可以透過實作介面自行控制。

type MyTime time.Time

func (t MyTime) MarshalJSON() ([]byte, error) {
	// 以 RFC3339 格式輸出字串
	s := time.Time(t).Format(time.RFC3339)
	return json.Marshal(s)
}

func (t *MyTime) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	tt, err := time.Parse(time.RFC3339, s)
	if err != nil {
		return err
	}
	*t = MyTime(tt)
	return nil
}

5. 處理不確定結構:map[string]interface{}json.RawMessage

當 JSON 結構在執行時才決定,或需要保留原始 JSON 片段以供後續處理,可使用:

  • map[string]interface{}:最通用的動態結構。
  • json.RawMessage:延遲解碼,僅在需要時再解析。
type Wrapper struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"` // 延遲解碼
}

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

type Order struct {
	OrderID string  `json:"order_id"`
	Amount  float64 `json:"amount"`
}

func handle(msg []byte) {
	var w Wrapper
	if err := json.Unmarshal(msg, &w); err != nil {
		panic(err)
	}
	switch w.Type {
	case "user":
		var u User
		json.Unmarshal(w.Data, &u)
		fmt.Printf("User: %+v\n", u)
	case "order":
		var o Order
		json.Unmarshal(w.Data, &o)
		fmt.Printf("Order: %+v\n", o)
	}
}

常見陷阱與最佳實踐

陷阱 說明 最佳做法
未匯出欄位 欄位首字母小寫會被 json.Marshal 忽略,導致資料遺失。 確保需要編碼的欄位首字母大寫,或使用 json:"-" 明確排除。
時間格式不一致 time.Time 會以 RFC3339 編碼,若前端期待不同格式會產生錯誤。 實作 json.Marshaler/Unmarshaler 或在結構標籤使用 omitempty 搭配自訂格式。
omitempty 與零值衝突 整數 0、布林 false 會被省略,若必須保留請移除 omitempty 只在確定「零值等同於不存在」的情況下使用 omitempty
大檔案一次性 Unmarshal 會佔用大量記憶體,甚至導致 OOM。 使用 json.Decoder 串流解碼,或分批讀取。
interface{} 失去型別資訊 直接解碼成 map[string]interface{} 會得到 float64[]interface{} 等通用型別,後續操作較繁瑣。 若結構已知,盡量使用具體 struct;若必須使用 interface{},可在取得值後使用類型斷言或 encoding/jsonNumber 類型。
錯誤訊息不夠明確 json.Unmarshal 的錯誤往往只顯示位置,缺乏上下文。 包裝錯誤訊息(fmt.Errorf("decode user: %w", err)),或使用 json.Decoder.DisallowUnknownFields() 及早捕捉不合法欄位。

最佳實踐小結

  1. 結構化資料:盡可能使用 struct + 標籤,避免過度依賴 interface{}
  2. 流式處理:對大檔案或網路串流使用 Decoder/Encoder
  3. 嚴格校驗:開發階段啟用 DisallowUnknownFields(),提升 API 兼容性。
  4. 錯誤包裝:提供足夠上下文,方便除錯與日誌追蹤。
  5. 自訂類型:對特殊格式(時間、十六進位、加密字串)實作編解碼介面,保持程式碼可讀性。

實際應用場景

場景 需求 典型程式碼片段
RESTful API 收發 JSON 請求與回應 json.NewEncoder(w).Encode(respStruct)json.NewDecoder(r.Body).Decode(&reqStruct)
設定檔 以 JSON 保存程式設定,支援動態重載 ioutil.ReadFile("config.json")json.Unmarshalwatcher 監控變更
日志系統 大量 JSON 行日志寫入檔案或 Kafka enc := json.NewEncoder(file)enc.Encode(logEntry)
微服務間訊息 使用 gRPC/HTTP+JSON 交換資料,需保持版本相容 omitempty + DisallowUnknownFields 保障向前/向後相容
資料匯入/匯出 從外部系統匯入大量 JSON 陣列,逐筆寫入資料庫 dec := json.NewDecoder(reader); for dec.More() { var rec T; dec.Decode(&rec); db.Insert(rec) }

總結

encoding/json 是 Go 生態系中最常用的套件之一,掌握它的 基本編解碼、標籤運用、流式 I/O、以及自訂行為,可以讓你在開發 API、處理設定檔、或是建構資料管線時事半功倍。

本文從核心概念出發,提供了從最簡單的 Marshal/Unmarshal 到進階的 Decoder/Encoder、自訂類型與動態結構處理,並列舉了常見的陷阱與實務最佳做法。只要遵循「結構化資料、流式處理、嚴格校驗」的原則,你就能在任何 Golang 專案中安全、有效地使用 JSON,提升開發效率與程式品質。

祝你在 Go 的世界裡玩得開心,寫出乾淨、可維護的 JSON 程式碼! 🚀