Đọc và ghi toàn bộ tệp
os.WriteFile và os.ReadFile đáp ứng hầu hết nhu cầu. Chúng mở, ghi hoặc đọc, rồi đóng tệp chỉ trong một lời gọi.
Các ví dụ trên trang này làm việc trong một thư mục tạm tạo bằng os.MkdirTemp và xóa nó bằng defer os.RemoveAll(dir), nên không để lại gì. Trong code của bạn, một đường dẫn tương đối như "config.json" được tính từ thư mục làm việc của tiến trình, không nhất thiết là thư mục chứa tệp mã nguồn hay tệp thực thi.
os.WriteFile tạo tệp nếu cần và xóa sạch nội dung (truncate) nếu tệp đã tồn tại. Đối số thứ ba là quyền Unix cho tệp mới được tạo: 0o644 nghĩa là chủ sở hữu được đọc và ghi, những người khác chỉ được đọc. Đối số này bị bỏ qua với tệp đã có sẵn, và umask của tiến trình có thể gỡ bớt một số bit.
os.ReadFile đọc toàn bộ vào bộ nhớ. Điều đó phù hợp với tệp cấu hình và đầu vào nhỏ, nhưng không phù hợp với một tệp log nhiều gigabyte.
Đọc từng dòng với bufio.Scanner
Với tệp lớn, hoặc khi bạn vốn cần xử lý theo dòng, hãy dùng bufio.Scanner. Nó đọc theo từng khối và trả cho bạn mỗi lần một dòng, không kèm ký tự xuống dòng.
Ba chi tiết cần lưu ý:
- Kiểm tra
sc.Err()sau vòng lặp.Scantrả vềfalsecả khi hết tệp lẫn khi gặp lỗi, và chỉErrmới phân biệt được hai trường hợp này. - Giới hạn dòng 64 KB. Theo mặc định, một dòng dài hơn 64 KB sẽ làm scanner dừng với
bufio.Scanner: token too long. Với tệp có dòng dài (JSON đã minify, một số tệp log), hãy nâng giới hạn trước vòng lặp:sc.Buffer(make([]byte, 1024*1024), 10*1024*1024). - Đơn vị khác.
sc.Split(bufio.ScanWords)trả về từng từ;bufio.ScanRunestrả về từng ký tự.
Để đọc một luồng dữ liệu theo các khối kích thước cố định thay vì theo dòng, hãy dùng f.Read(buf) trong một vòng lặp hoặc io.Copy sang một writer khác.
Ghi tệp: os.Create, os.OpenFile và ghi nối thêm
os.Create(name) mở một tệp để ghi, tạo mới hoặc xóa sạch nội dung cũ. os.OpenFile cho bạn toàn quyền kiểm soát qua các flag:
| Flag | Ý nghĩa |
|---|---|
os.O_RDONLY, os.O_WRONLY, os.O_RDWR | mở để đọc, để ghi, hoặc cả hai (chọn một) |
os.O_CREATE | tạo tệp nếu chưa tồn tại |
os.O_TRUNC | xóa sạch nội dung tệp khi mở |
os.O_APPEND | mọi lần ghi đều đi vào cuối tệp |
os.O_EXCL | dùng với O_CREATE: thất bại nếu tệp đã tồn tại |
os.Open(name) tương đương OpenFile(name, O_RDONLY, 0). os.Create(name) tương đương OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0o666).
Khi ghi tệp, lỗi từ Close rất quan trọng. Một số hệ thống tệp chỉ báo lỗi ghi vào lúc đóng tệp, nên chỉ dùng defer f.Close() có thể che giấu việc mất dữ liệu. Với tệp bạn ghi, hãy kiểm tra Close một cách tường minh như appendLine làm. Với tệp bạn chỉ đọc, defer f.Close() là đủ.
fmt.Fprintln và các hàm in của fmt khác nhận bất kỳ io.Writer nào, kể cả một tệp. bufio.Writer gom các lần ghi nhỏ lại trong bộ nhớ. Quên gọi w.Flush() là một bug kinh điển: chương trình thoát bình thường nhưng vài kilobyte cuối cùng không bao giờ được ghi vào tệp.
Tệp có tồn tại không?
Go không có os.Exists. Hãy gọi os.Stat và xem lỗi trả về:
errors.Is(err, fs.ErrNotExist) là cách viết chuẩn hiện nay. Nó thay cho os.IsNotExist(err) cũ, vốn không nhìn xuyên qua được các lỗi đã được wrap.
Kiểm tra trước khi mở thường không cần thiết và dễ gây race: tệp có thể xuất hiện hoặc biến mất giữa lúc kiểm tra và lúc mở. Thông thường bạn chỉ cần mở tệp và xử lý fs.ErrNotExist từ lệnh mở. Để chỉ tạo tệp khi nó chưa tồn tại, hãy dùng O_CREATE|O_EXCL, giúp việc kiểm tra và tạo tệp diễn ra trong một bước nguyên tử.
Thư mục
| Việc cần làm | Hàm |
|---|---|
| tạo một thư mục | os.Mkdir(path, 0o755) |
| tạo đường dẫn kèm các thư mục cha | os.MkdirAll(path, 0o755) |
| liệt kê một thư mục | os.ReadDir(path) |
| duyệt cả cây thư mục | filepath.WalkDir(root, fn) |
| xóa một tệp hoặc thư mục rỗng | os.Remove(path) |
| xóa cả cây thư mục | os.RemoveAll(path) |
| đổi tên hoặc di chuyển | os.Rename(old, new) |
| tệp hoặc thư mục tạm | os.CreateTemp("", "prefix-*"), os.MkdirTemp("", "prefix") |
| ghép các phần của đường dẫn | filepath.Join(a, b, c) |
Dùng path/filepath cho đường dẫn trong hệ thống tệp: package này dùng đúng ký tự phân cách của hệ điều hành (\ trên Windows). Package path dành cho các đường dẫn phân cách bằng dấu gạch chéo như URL.
Go 1.24 còn bổ sung os.Root (os.OpenRoot(dir)), chỉ mở tệp bên trong một thư mục và từ chối những đường dẫn thoát ra ngoài bằng .. hoặc symlink. Hãy dùng nó khi tên tệp đến từ người dùng.
Những lỗi thường gặp
- Không kiểm tra lỗi. Mọi lời gọi ở trên đều có thể thất bại. Một tệp mở không thành công sẽ là
nil, và mọi lệnhRead,WritehayClosesau đó trên nó đều trả vềinvalid argument, che mất nguyên nhân thật (tệp không tồn tại, hoặc bị từ chối quyền truy cập). - Quên
sc.Err()sau vòng lặp scan. Một lỗi đọc trông giống như hết tệp. - Quên
Flushtrênbufio.Writer. Phần cuối của tệp bị mất. - Bỏ qua lỗi từ
Closesau khi ghi. Lỗi ghi có thể chỉ xuất hiện ở đó. defer f.Close()trong vòng lặp qua nhiều tệp. Các tệp vẫn mở cho đến khi hàm trả về và bạn có thể dùng hết file descriptor. Hãy chuyển phần thân vòng lặp vào một hàm để mỗi tệp được đóng sau từng lần lặp.- Viết quyền dưới dạng thập phân.
644không phải0o644. Go hiểu644là số thập phân, tức0o1204, và đặt những bit kỳ lạ.
Câu hỏi thường gặp
Làm sao đọc toàn bộ tệp thành chuỗi trong Go?
data, err := os.ReadFile("notes.txt") trả về nội dung dưới dạng []byte; chuyển đổi bằng string(data). Hàm này tự mở, đọc và đóng tệp cho bạn. Dùng nó cho những tệp vừa vặn trong bộ nhớ; với tệp lớn, hãy đọc từng dòng bằng bufio.Scanner.
Làm sao đọc tệp từng dòng trong Go?
Mở tệp bằng os.Open, gọi defer f.Close(), bọc nó trong bufio.NewScanner(f), lặp với for sc.Scan() { line := sc.Text() }, và kiểm tra sc.Err() sau vòng lặp. Những dòng dài hơn 64 KB khiến scanner thất bại với token too long, trừ khi bạn mở rộng bộ đệm bằng sc.Buffer.
Làm sao ghi nối thêm vào tệp trong Go?
Mở tệp bằng os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644), ghi dữ liệu, rồi kiểm tra lỗi từ Close. O_CREATE tạo tệp nếu chưa có, còn O_APPEND khiến mọi lần ghi đều đi vào cuối tệp.
Làm sao kiểm tra tệp có tồn tại trong Go?
Gọi os.Stat(path) và kiểm tra lỗi bằng errors.Is(err, fs.ErrNotExist). Lỗi bằng nil nghĩa là tệp tồn tại. Bất kỳ lỗi nào khác (chẳng hạn bị từ chối quyền truy cập) nghĩa là bạn không thể biết chắc, nên hãy xử lý riêng thay vì coi đó là "không tồn tại".