0

Go Zero to Hero - Bài 73: Ghi file và thiết lập quyền truy cập file

1. Ghi "Một Cục" và Bài toán Quyền truy cập (Permissions)

Giống như việc đọc, thao tác lười biếng và nhanh gọn nhất để ghi một lượng dữ liệu nhỏ xuống đĩa là dùng hàm os.WriteFile(). Hàm này sẽ tự động tạo file mới (hếu chưa có), hoặc XÓA SẠCH (Overwrite) nội dung cũ và ghi đè nội dung mới lên.

package main

import (
    "fmt"
    "os"
)

func main() {
    data := []byte("AFC_CONFIG_VER=2.0.1\nMAX_RETRIES=3\n")

    // Ghi toàn bộ mảng byte xuống file "config.env"
    // Tham số thứ 3 (0644) chính là Quyền truy cập (File Permissions)
    err := os.WriteFile("config.env", data, 0644)
    if err != nil {
        fmt.Println("Lỗi ghi file:", err)
        return
    }
    
    fmt.Println("✅ Đã ghi file thành công!")
}

🔐 Giải mã con số bí ẩn 0644 (Unix Permissions)

Khi bạn code trên Windows, bạn ít khi quan tâm đến tham số thứ 3 này. Nhưng khi Deploy ứng dụng Go lên các máy chủ Linux (Ubuntu, CentOS), đây là hàng rào bảo mật sống còn.

Con số này là hệ Bát phân (Octal), đại diện cho quyền R (Read - 4), W (Write - 2), X (Execute - 1) của 3 nhóm: Chủ sở hữu (Owner) - Nhóm (Group) - Người khác (Others).

Là một kỹ sư Backend, bạn phải nằm lòng 2 con số sau:

  • 0644 (Tiêu chuẩn): Chủ sở hữu được Đọc/Ghi (6 = 4+2). Tất cả những người khác trong server chỉ được Đọc (4). Dùng cho các file log thông thường, file báo cáo.
  • 0600 (Tuyệt mật): CHỈ CÓ Chủ sở hữu mới được phép Đọc/Ghi. Dùng cho các file chứa khóa bí mật (Private Key, RSA), file .env chứa mật khẩu Database. Nếu bạn set quyền 0644 cho file chứa mật khẩu, hacker chiếm được một tài khoản vớ vẩn trên server cũng có thể đọc được DB của bạn!

2. Mở nắp cống: Ghi nối tiếp (Append) với os.OpenFile

Trong thực tế vận hành hệ thống AFC, log giao dịch phải được ghi liên tục ngày qua ngày. Việc dùng os.WriteFile sẽ xóa sạch log cũ, một thảm họa không thể chấp nhận.

Để ghi nối tiếp vào cuối file (Append), chúng ta phải dùng hàm thao tác file nguyên thủy nhất: os.OpenFile kèm theo các Cờ hiệu (Flags).

package main

import (
    "fmt"
    "os"
)

func main() {
    // 1. Mở file với các Cờ hiệu (Flags) ghép bằng toán tử OR (|)
    // os.O_APPEND: Ghi nối tiếp vào cuối file
    // os.O_CREATE: Nếu file chưa tồn tại thì tự động tạo mới
    // os.O_WRONLY: Chỉ mở để ghi (Không đọc)
    file, err := os.OpenFile("transaction_logs.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        panic(err)
    }
    defer file.Close() // LUÔN LUÔN đóng file để nhả File Descriptor cho Hệ điều hành

    // 2. Ghi một dòng log mới
    newLog := "2026-08-15 11:50:00 | TRẠM_A | SUCCESS | MÃ_VÉ: 998877\n"
    
    // Hàm WriteString tiện lợi hơn Write() vì không cần ép kiểu từ string sang []byte
    _, err = file.WriteString(newLog)
    if err != nil {
        fmt.Println("Lỗi khi ghi log:", err)
    }
}

3. Nghệ thuật tối ưu: Ghi đệm với bufio.Writer

Bây giờ chúng ta tiến vào lãnh địa của hiệu năng cao (High Performance).

Hãy tưởng tượng bạn phải xuất báo cáo gồm 100,000 dòng giao dịch từ CSDL. Nếu bạn dùng vòng lặp for và gọi file.WriteString(line) 100,000 lần, hệ điều hành sẽ phải thực hiện 100,000 cú chạm vật lý (System calls) xuống ổ cứng SSD.

Đây là thao tác cực kỳ tốn kém, làm chậm chương trình gấp hàng chục lần và giảm tuổi thọ ổ cứng.

Giải pháp: Sử dụng bufio.Writer.

Bộ đệm này đóng vai trò như một chiếc xe tải. Thay vì chuyển từng viên gạch (từng dòng log) từ kho (RAM) ra công trường (Ổ cứng), bufio.Writer sẽ xếp gạch lên xe. Khi nào xe đầy (thường là 4KB), nó mới chở đúng 1 chuyến ra ổ cứng. 100,000 dòng có thể chỉ mất vài chục chuyến xe!

package main

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

func main() {
    file, _ := os.OpenFile("bulk_export.csv", os.O_CREATE|os.O_WRONLY, 0644)
    defer file.Close()

    // Bọc cái file thô bằng bufio.Writer
    writer := bufio.NewWriter(file)

    // Mô phỏng vòng lặp xuất 10,000 dòng
    for i := 1; i <= 10000; i++ {
        line := fmt.Sprintf("ID_%d,SUCCESS,15000\n", i)
        
        // Ghi vào "Xe tải" (RAM), CHƯA thực sự ghi xuống ổ cứng!
        writer.WriteString(line) 
    }

    // 🚨 QUY LUẬT SINH TỬ: Gọi Flush()
    // Nếu vòng lặp kết thúc mà "Xe tải" mới chứa được một nửa (chưa đầy),
    // nó sẽ cứ nằm im trong RAM. Nếu ứng dụng tắt lúc này, toàn bộ dữ liệu trên xe sẽ BỐC HƠI!
    // Lệnh Flush() ép xe tải phải đổ toàn bộ dữ liệu đang giữ xuống ổ cứng ngay lập tức.
    writer.Flush() 

    fmt.Println("✅ Đã xuất báo cáo siêu tốc bằng bufio!")
}

Bí kíp thực chiến: Luôn đặt defer writer.Flush() ngay sau khi khởi tạo writer := bufio.NewWriter(file) để đảm bảo dù hàm bị lỗi (panic) hay return giữa chừng, dữ liệu cuối cùng vẫn được cứu an toàn xuống đĩa cứng.


💡 Tổng kết

Bạn đã khép lại vòng tuần hoàn I/O của Backend bằng 3 mũi nhọn:

  • os.WriteFile: Dùng cho file cấu hình nhỏ gọn. Chú ý set quyền 0600 cho thông tin mật.
  • os.OpenFile: Cờ hiệu os.O_APPEND là bắt buộc khi ghi log để không phá nát dữ liệu cũ.
  • bufio.Writer: Vũ khí bắt buộc phải có khi xuất file CSV, Báo cáo lớn để bảo vệ hiệu năng CPU và ổ cứng. Tuyệt đối không được quên lệnh Flush().

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí