Golang – 檔案與 I/O 操作
主題:JSON 編解碼(encoding/json)
簡介
在現代的 Web、微服務與資料交換中,JSON(JavaScript Object Notation) 已成為最常見的資料格式之一。Go 語言內建的 encoding/json 套件提供了簡潔且效能不錯的編碼(Encode)與解碼(Decode)功能,讓開發者可以毫不費力地在 Go 結構與 JSON 之間互相轉換。
掌握 JSON 編解碼不僅能讓你快速實作 API、讀寫設定檔、或是與前端、其他服務進行資料互通,更是日常開發中不可或缺的基礎技能。本文將從核心概念出發,搭配實用範例,說明如何在 Go 中正確且高效地使用 encoding/json,並提供常見陷阱與最佳實踐,幫助你在實務專案中得心應手。
核心概念
1. 基本的 Marshal 與 Unmarshal
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、""、nil、false)時,省略該欄位。-:完全忽略此欄位。
type Article struct {
Title string `json:"title"` // 直接映射
Content string `json:"content,omitempty"` // 空字串時不輸出
Author string `json:"author_name"` // 自訂鍵名
Secret string `json:"-"` // 永遠不會編碼
}
3. json.Decoder 與 json.Encoder:串流式 I/O
在處理大型檔案或網路連線時,直接使用 Marshal/Unmarshal 會一次把全部資料載入記憶體,可能造成記憶體壓力。json.NewDecoder 與 json.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.Marshaler 與 json.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/json 的 Number 類型。 |
| 錯誤訊息不夠明確 | json.Unmarshal 的錯誤往往只顯示位置,缺乏上下文。 |
包裝錯誤訊息(fmt.Errorf("decode user: %w", err)),或使用 json.Decoder.DisallowUnknownFields() 及早捕捉不合法欄位。 |
最佳實踐小結
- 結構化資料:盡可能使用
struct+ 標籤,避免過度依賴interface{}。 - 流式處理:對大檔案或網路串流使用
Decoder/Encoder。 - 嚴格校驗:開發階段啟用
DisallowUnknownFields(),提升 API 兼容性。 - 錯誤包裝:提供足夠上下文,方便除錯與日誌追蹤。
- 自訂類型:對特殊格式(時間、十六進位、加密字串)實作編解碼介面,保持程式碼可讀性。
實際應用場景
| 場景 | 需求 | 典型程式碼片段 |
|---|---|---|
| RESTful API | 收發 JSON 請求與回應 | json.NewEncoder(w).Encode(respStruct)、json.NewDecoder(r.Body).Decode(&reqStruct) |
| 設定檔 | 以 JSON 保存程式設定,支援動態重載 | ioutil.ReadFile("config.json") → json.Unmarshal → watcher 監控變更 |
| 日志系統 | 大量 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 程式碼! 🚀