0

Go Zero to Hero - Bài 72: Đọc file cơ bản (Toàn bộ, từng dòng, từng chunk)

1. Đọc "Một Cục" (Đọc toàn bộ file vào RAM)

Cách đơn giản và lười biếng nhất. Go cung cấp hàm os.ReadFile() (Từ bản Go 1.16 trở đi, hàm này thay thế cho ioutil.ReadFile). Nó mở file, nạp toàn bộ nội dung vào RAM, rồi tự động đóng file.

package main

import (
    "fmt"
    "os"
)

func main() {
    // Trả về một mảng []byte chứa toàn bộ nội dung file
    data, err := os.ReadFile("config.json")
    if err != nil {
        fmt.Println("Lỗi đọc file:", err)
        return
    }

    // Ép kiểu sang string để in ra màn hình
    fmt.Println(string(data))
}

Khi nào dùng?

Chỉ dùng cho các file rất nhỏ, vòng đời ngắn (như đọc file cấu hình .env, config.json, file template HTML, hoặc TLS Certificates).

🚨 Bẫy Sập Server (OOM - Out of Memory): Nếu bạn dùng hàm này để đọc một file Log giao dịch nặng 5GB, hệ điều hành sẽ phải cấp phát ngay lập tức 5GB RAM cho tiến trình Go của bạn. Máy chủ sẽ cạn kiệt bộ nhớ và bị hệ thống (OOM Killer) "bắn gục" ngay lập tức!


2. Đọc theo Chunk (Dùng Buffer thô)

Đây chính là kỹ thuật chúng ta đã phân tích ở Bài 71. Khi phải đối mặt với file dung lượng lớn (video, file zip, hoặc chuyển tiếp file thẳng ra mạng), chúng ta phải mở file bằng os.Open() và đọc theo từng "thìa" (Chunk).

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 1. Mở file (BẮT BUỘC phải defer Close để tránh rò rỉ File Descriptor)
    file, err := os.Open("big_data.bin")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 2. Tạo một cái "xô" dung lượng 4KB (4096 bytes)
    chunk := make([]byte, 4096)

    for {
        // 3. Múc dữ liệu đổ vào xô
        n, err := file.Read(chunk)
        if err == io.EOF {
            break // Đã đến cuối file
        }
        if err != nil {
            panic(err)
        }

        // 4. Xử lý phần dữ liệu vừa múc được (từ index 0 đến n)
        fmt.Printf("Đã đọc được %d bytes\n", n)
        // ... Bơm chunk[:n] này sang một Writer khác ...
    }
}
  • Ưu điểm: Tiết kiệm RAM tuyệt đối. Dù file có nặng 100GB, RAM của bạn cũng chỉ tốn đúng 4KB.
  • Nhược điểm: Phù hợp với dữ liệu nhị phân (Binary) hoặc truyền tải tệp. Rất tệ cho xử lý văn bản, vì một dòng chữ có thể bị "chém" đứt đôi nằm ở 2 Chunk khác nhau, làm sai lệch logic phân tích cú pháp.

3. Đọc từng dòng (Line-by-Line) với bufio.Scanner

Đây là Tiêu chuẩn vàng (Golden Standard) của mọi kỹ sư Backend khi làm việc với file văn bản (Log, CSV, TXT).

Package bufio (Buffered I/O) sẽ bọc quanh một io.Reader thô ráp. Nó đóng vai trò là một "Kế toán viên thông minh": Nó tự động đọc từng Chunk vào bộ đệm ngầm, tự động tìm kiếm các ký tự xuống dòng (\n), và cắt gọt gọn gàng trả về cho bạn từng dòng text hoàn chỉnh.

Hãy xem kịch bản chúng ta bóc tách một file lịch sử quẹt vé chứa hàng triệu dòng:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    file, err := os.Open("afc_error_logs.txt")
    if err != nil {
        fmt.Println("Lỗi mở file:", err)
        return
    }
    defer file.Close()

    // Bọc cái file (io.Reader) thô bằng Scanner
    scanner := bufio.NewScanner(file)

    lineCount := 0
    errorCount := 0

    // Vòng lặp Scan() sẽ tự động chạy cho đến khi hết file (gặp EOF)
    for scanner.Scan() {
        lineCount++
        
        // Rút nội dung dòng hiện tại ra dưới dạng string
        line := scanner.Text() 
        // Nếu muốn tiết kiệm RAM hơn, có thể dùng scanner.Bytes() để lấy []byte

        // Nghiệp vụ: Đếm số lượng giao dịch bị rớt mạng
        if strings.Contains(line, "TIMEOUT") {
            errorCount++
            fmt.Printf("[Dòng %d] Phát hiện lỗi rớt mạng: %s\n", lineCount, line)
        }
    }

    // Luật an toàn: Luôn kiểm tra lỗi của Scanner sau khi vòng lặp kết thúc
    if err := scanner.Err(); err != nil {
        fmt.Println("Bị lỗi trong quá trình quét file:", err)
    }

    fmt.Printf("--- TỔNG KẾT: Đã quét %d dòng, phát hiện %d lỗi ---\n", lineCount, errorCount)
}

🚨 Bẫy "Dòng chữ quá dài" (Scanner Buffer Too Small)

Khi đi phỏng vấn vị trí Senior, bạn rất dễ bị hỏi câu này: "Nhược điểm của bufio.Scanner là gì?"

Theo mặc định, bộ đệm nội bộ của Scanner chỉ chứa tối đa 64KB (Khoảng ~65,000 ký tự). Nếu nó đang đọc file mà gặp một dòng chữ khổng lồ (ví dụ một chuỗi Base64 dài dòng dằng dặc) vượt quá 64KB mà không hề có dấu xuống dòng \n nào, nó sẽ đứng hình và báo lỗi bufio.Scanner: token too long.

Cách gỡ bom: Nếu bạn biết chắc mình sắp đọc file có dòng siêu dài, hãy chủ động nới rộng xô chứa cho Scanner:

scanner := bufio.NewScanner(file)

// Nới rộng bộ đệm lên tối đa 1MB
const maxCapacity = 1024 * 1024 
buf := make([]byte, maxCapacity)

// Ép Scanner dùng cái xô lớn này
scanner.Buffer(buf, maxCapacity) 

📊 Tổng kết: Quy tắc chọn vũ khí khi đối mặt với File I/O

Vũ khí Trường hợp sử dụng tối ưu Điểm cần lưu ý
Đọc toàn bộ (os.ReadFile) Chỉ dành cho file hệ thống, cấu hình siêu nhỏ (< vài MB). Nguy cơ sập OOM nếu file lớn.
Đọc theo Chunk (os.Open + .Read) Dành cho stream dữ liệu nhị phân (Video, TCP Socket, Proxy). RAM luôn cố định. Khó xử lý văn bản dạng dòng.
Đọc từng dòng (bufio.Scanner) Vũ khí chủ lực để xử lý file Text, CSV, Log. Tối ưu, tiện lợi và tự động phân tách bằng ký tự \n. Nhớ chú ý giới hạn 64KB nếu dòng text quá dài.

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.