Golang – 檔案與 I/O 操作
主題:CSV 與 XML 處理
簡介
在日常開發中,資料交換、報表產出、以及設定檔常以 CSV 或 XML 格式出現。
CSV(Comma‑Separated Values)簡潔、易於手動編輯,適合大批量資料的匯入匯出;
XML(eXtensible Markup Language)則提供層次結構與自描述性,常用於 Web Service、設定檔或跨平台資料傳遞。
Go 語言自帶 encoding/csv、encoding/xml 兩個標準套件,讓開發者可以 用最少的程式碼 完成讀寫、編碼、解碼等工作。掌握這兩種格式的處理技巧,能大幅提升系統與外部環境的整合效率。
本篇文章將從 核心概念、實作範例、常見陷阱、以及 最佳實踐 逐步說明,讓初學者也能快速上手,同時提供給中級開發者作為日常開發的參考手冊。
核心概念
1. CSV 基本概念與 Go 套件
CSV 檔案本質上是一列列文字,每列以換行分隔,欄位則以逗號(或自訂分隔符)分開。Go 的 encoding/csv 套件提供 Reader、Writer 兩個主要類型:
| 類型 | 主要方法 | 功能 |
|---|---|---|
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 套件提供 Marshal、Unmarshal 兩個核心函式,配合結構體標籤 (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 { … }。 |
最佳實踐
- 統一錯誤處理:將 CSV、XML 的錯誤包裝成自訂錯誤類型,方便上層呼叫者判斷是 I/O 錯誤、格式錯誤或資料驗證失敗。
- 使用結構體標籤:盡量讓資料模型與檔案格式直接對應,減少手動映射的程式碼。
- 設定緩衝大小:對於頻繁 I/O 操作,可使用
bufio.NewReader、bufio.NewWriter包裝底層檔案,以提升效能。 - 驗證資料完整性:在寫入前檢查欄位長度、必填欄位、資料型別(例如日期、數字),避免產出不合法的 CSV/XML。
- 支援自訂分隔符與編碼:CSV 可透過
Reader.Comma、Writer.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 專案中自信地處理各種檔案與資料交換情境,讓系統整合更快速、更可靠。祝開發順利! 🚀