本文 AI 產出,尚未審核

Golang – 檔案與 I/O 操作

主題:CSV 與 XML 處理


簡介

在日常開發中,資料交換報表產出、以及設定檔常以 CSV 或 XML 格式出現。
CSV(Comma‑Separated Values)簡潔、易於手動編輯,適合大批量資料的匯入匯出;
XML(eXtensible Markup Language)則提供層次結構與自描述性,常用於 Web Service、設定檔或跨平台資料傳遞。

Go 語言自帶 encoding/csvencoding/xml 兩個標準套件,讓開發者可以 用最少的程式碼 完成讀寫、編碼、解碼等工作。掌握這兩種格式的處理技巧,能大幅提升系統與外部環境的整合效率。

本篇文章將從 核心概念實作範例常見陷阱、以及 最佳實踐 逐步說明,讓初學者也能快速上手,同時提供給中級開發者作為日常開發的參考手冊。


核心概念

1. CSV 基本概念與 Go 套件

CSV 檔案本質上是一列列文字,每列以換行分隔,欄位則以逗號(或自訂分隔符)分開。Go 的 encoding/csv 套件提供 ReaderWriter 兩個主要類型:

類型 主要方法 功能
csv.NewReader(io.Reader) Read(), ReadAll() 逐行或一次性讀取 CSV
csv.NewWriter(io.Writer) Write([]string), Flush() 寫入單行或多行 CSV,Flush 立即將緩衝寫入檔案

範例 1:讀取 CSV 檔案並列印每筆資料

package main

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

func main() {
	// 開啟 CSV 檔案
	f, err := os.Open("data.csv")
	if err != nil {
		log.Fatalf("無法開啟檔案: %v", err)
	}
	defer f.Close()

	r := csv.NewReader(f)
	// 設定欄位分隔符號(預設為逗號)
	r.Comma = ','

	// 逐行讀取
	for {
		record, err := r.Read()
		if err != nil {
			// io.EOF 代表讀到檔案結尾
			if err.Error() == "EOF" {
				break
			}
			log.Fatalf("讀取錯誤: %v", err)
		}
		fmt.Printf("第 %d 筆資料: %v\n", r.FieldPos, record)
	}
}

說明Read() 會回傳 []string,每個元素對應一個欄位。若檔案很大,使用 Read() 逐行處理可避免一次讀入過多記憶體。


2. CSV 寫入與自訂分隔符

範例 2:將結構體資料寫入 CSV,並使用分號作為分隔符

package main

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

type User struct {
	ID    string
	Name  string
	Email string
}

func main() {
	users := []User{
		{"001", "Alice", "alice@example.com"},
		{"002", "Bob", "bob@example.com"},
		{"003", "Charlie", "charlie@example.com"},
	}

	f, err := os.Create("users.csv")
	if err != nil {
		log.Fatalf("建立檔案失敗: %v", err)
	}
	defer f.Close()

	w := csv.NewWriter(f)
	// 使用分號作為欄位分隔符
	w.Comma = ';'

	// 寫入表頭
	if err := w.Write([]string{"ID", "Name", "Email"}); err != nil {
		log.Fatalf("寫入表頭失敗: %v", err)
	}

	// 寫入每筆資料
	for _, u := range users {
		if err := w.Write([]string{u.ID, u.Name, u.Email}); err != nil {
			log.Fatalf("寫入資料失敗: %v", err)
		}
	}
	// 確保緩衝寫入磁碟
	w.Flush()
	if err := w.Error(); err != nil {
		log.Fatalf("Flush 時發生錯誤: %v", err)
	}
}

重點Writer.Flush() 必須在寫入完成後呼叫,否則緩衝區的資料不會真正寫入檔案。若有錯誤,Writer.Error() 會回傳最後一次的錯誤資訊。


3. XML 基本概念與 Go 套件

XML 使用標籤 (<tag>...</tag>) 來描述資料的層次結構。Go 的 encoding/xml 套件提供 MarshalUnmarshal 兩個核心函式,配合結構體標籤 (xml:"TagName") 完成序列化與反序列化。

範例 3:將結構體序列化成 XML

package main

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

type Book struct {
	XMLName   xml.Name `xml:"book"`          // 指定根標籤名稱
	ISBN      string   `xml:"isbn,attr"`     // 以屬性形式輸出
	Title     string   `xml:"title"`         // 子標籤
	Author    string   `xml:"author"`        // 子標籤
	Published int      `xml:"published"`     // 子標籤
}

func main() {
	b := Book{
		ISBN:      "978-986-123456-7",
		Title:     "深入淺出 Go 語言",
		Author:    "王小明",
		Published: 2024,
	}
	data, err := xml.MarshalIndent(b, "", "  ")
	if err != nil {
		log.Fatalf("XML 序列化失敗: %v", err)
	}
	fmt.Println(xml.Header + string(data))
}

輸出結果:

<?xml version="1.0" encoding="UTF-8"?>
<book isbn="978-986-123456-7">
  <title>深入淺出 Go 語言</title>
  <author>王小明</author>
  <published>2024</published>
</book>

說明xml.MarshalIndent 會自動加入縮排,xml.Header 為標準的 XML 宣告。XMLName 欄位用來指定根節點名稱,isbn,attr 表示將 ISBN 以屬性方式輸出。


4. XML 反序列化(Unmarshal)與自訂結構

範例 4:讀取 XML 檔案並解析成結構體

package main

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

type Catalog struct {
	XMLName xml.Name `xml:"catalog"`
	Books   []Book   `xml:"book"` // 內部的 <book> 會自動映射成 slice
}

type Book struct {
	ISBN      string `xml:"isbn,attr"`
	Title     string `xml:"title"`
	Author    string `xml:"author"`
	Published int    `xml:"published"`
}

func main() {
	f, err := os.Open("catalog.xml")
	if err != nil {
		log.Fatalf("開啟檔案失敗: %v", err)
	}
	defer f.Close()

	bytes, err := ioutil.ReadAll(f)
	if err != nil {
		log.Fatalf("讀取檔案失敗: %v", err)
	}

	var cat Catalog
	if err := xml.Unmarshal(bytes, &cat); err != nil {
		log.Fatalf("XML 解析失敗: %v", err)
	}

	for _, b := range cat.Books {
		fmt.Printf("ISBN: %s, Title: %s, Author: %s, Year: %d\n",
			b.ISBN, b.Title, b.Author, b.Published)
	}
}

重點xml.Unmarshal 會根據結構體的 xml 標籤自動匹配對應的 XML 元素或屬性。若 XML 中的欄位名稱與結構體欄位不一致,可透過 xml:"自訂名稱" 進行映射。


5. CSV 與 XML 混合應用

在實務上,常會需要 將 CSV 轉成 XML(或相反)以符合不同系統的介面規範。以下示範把 CSV 讀入後,直接產生符合特定 XML Schema 的檔案。

範例 5:CSV → XML 轉換

package main

import (
	"encoding/csv"
	"encoding/xml"
	"log"
	"os"
)

type Person struct {
	XMLName xml.Name `xml:"person"`
	ID      string   `xml:"id,attr"`
	Name    string   `xml:"name"`
	Email   string   `xml:"email"`
}

type People struct {
	XMLName xml.Name `xml:"people"`
	Items   []Person `xml:"person"`
}

func main() {
	// 1. 讀取 CSV
	csvFile, err := os.Open("people.csv")
	if err != nil {
		log.Fatalf("開啟 CSV 失敗: %v", err)
	}
	defer csvFile.Close()

	r := csv.NewReader(csvFile)
	records, err := r.ReadAll()
	if err != nil {
		log.Fatalf("CSV 讀取失敗: %v", err)
	}

	// 2. 轉換成結構體切片
	var people []Person
	for i, rec := range records {
		// 假設第一列是表頭,直接跳過
		if i == 0 {
			continue
		}
		if len(rec) < 3 {
			log.Printf("第 %d 列資料不完整,跳過", i+1)
			continue
		}
		people = append(people, Person{
			ID:    rec[0],
			Name:  rec[1],
			Email: rec[2],
		})
	}

	// 3. 序列化成 XML
	output := People{Items: people}
	xmlData, err := xml.MarshalIndent(output, "", "  ")
	if err != nil {
		log.Fatalf("XML 序列化失敗: %v", err)
	}

	// 4. 寫入檔案
	if err := os.WriteFile("people.xml", append([]byte(xml.Header), xmlData...), 0644); err != nil {
		log.Fatalf("寫入 XML 失敗: %v", err)
	}
}

實務提示:在大量資料轉換時,建議使用 流式(stream) 方式處理 CSV(Read())與 XML(xml.Encoder),避免一次性載入全部資料導致記憶體吃緊。


常見陷阱與最佳實踐

陷阱 說明 解決方案
CSV 欄位中含有逗號或換行 預設 encoding/csv 會自動以雙引號包住含特殊字元的欄位,但若手動組字串時忘記加引號會導致解析錯誤。 使用 csv.Writer 產生檔案,讓套件自行處理轉義;若自行組字串,務必使用 strconv.Quote
Unicode BOM(Byte Order Mark) 某些 Windows 產生的 CSV 會在檔案開頭帶有 BOM,導致第一筆欄位出現奇怪字元。 在讀取前使用 bytes.TrimPrefix(data, []byte{0xEF,0xBB,0xBF}) 去除 BOM。
XML 內部的命名空間(Namespace) encoding/xml 只會解析沒有命名空間的標籤,若 XML 含有 xmlns 會導致 Unmarshal 失敗。 在結構體標籤中加入命名空間前綴,例如 xml:"ns:book",或使用 xml.Decoder 手動忽略 Namespace。
大檔案一次性讀取 ReadAll() 會把整個檔案載入記憶體,對於 GB 級別的檔案會造成 OOM。 使用 流式 讀寫:csv.Reader.Read() 逐行處理;xml.Encoder 逐元素寫入。
錯誤處理忽略 Writer.Flush() 之後若不檢查 Writer.Error(),寫入失敗的情況不易被發現。 每次 Flush 後必須呼叫 if err := w.Error(); err != nil { … }

最佳實踐

  1. 統一錯誤處理:將 CSV、XML 的錯誤包裝成自訂錯誤類型,方便上層呼叫者判斷是 I/O 錯誤、格式錯誤或資料驗證失敗。
  2. 使用結構體標籤:盡量讓資料模型與檔案格式直接對應,減少手動映射的程式碼。
  3. 設定緩衝大小:對於頻繁 I/O 操作,可使用 bufio.NewReaderbufio.NewWriter 包裝底層檔案,以提升效能。
  4. 驗證資料完整性:在寫入前檢查欄位長度、必填欄位、資料型別(例如日期、數字),避免產出不合法的 CSV/XML。
  5. 支援自訂分隔符與編碼:CSV 可透過 Reader.CommaWriter.Comma 調整;XML 若需支援 UTF‑16,需自行轉碼或使用 golang.org/x/text/encoding

實際應用場景

場景 為何使用 CSV 為何使用 XML
資料匯入/匯出 大量平面表格資料(如報表、統計)易於 Excel 直接編輯。 需要保留層次結構或屬性(如產品目錄)時,XML 更適合。
設定檔 輕量級、只需要鍵值對的設定檔可用 CSV(如簡易的映射表)。 複雜的系統設定、包含多層次參數或需要驗證(XSD)時,使用 XML。
跨系統介面 多數金融、ERP 系統仍支援 CSV 匯入。 SOAP、RESTful API 常以 XML 作為訊息格式(特別是老舊系統)。
報表產生 產出給非技術使用者的 CSV,讓他們自行在 Excel 中分析。 產生符合業界標準的 XML(如 JATS、MARC),供出版或圖書館系統使用。

案例:一家電商平台每日從倉儲系統接收 CSV 庫存清單,再將每筆商品資訊轉成 XML 上傳至合作的第三方比價網站。透過上述的流式讀寫與結構體映射,可在 數秒內完成 10 萬筆資料的轉換,同時保持記憶體佔用低於 50 MB。


總結

  • CSV 以簡潔、易於手動編輯著稱,Go 的 encoding/csv 提供了 逐行讀寫自訂分隔符 以及 自動轉義 功能,適合大批量平面資料的處理。
  • XML 則以層次結構與自描述性為優勢,encoding/xml 讓我們能透過 結構體標籤 完成 序列化 / 反序列化,同時支援屬性、命名空間與縮排。
  • 在實務開發中,流式 I/O錯誤檢查、以及 資料驗證 是避免常見陷阱的關鍵。
  • 透過 範例程式,你已掌握從檔案讀取、資料映射、再到產出 CSV / XML 的完整流程,並了解如何在 CSV ↔ XML 之間做轉換,以滿足不同系統的介面需求。

只要熟悉這兩套標準套件的使用方式,你就能在 Go 專案中自信地處理各種檔案與資料交換情境,讓系統整合更快速、更可靠。祝開發順利! 🚀