Menu

Đọc và ghi tệp trong Golang: os, bufio và thư mục

Cách đọc và ghi tệp trong Go: os.ReadFile và os.WriteFile, đọc từng dòng với bufio.Scanner, ghi nối thêm với os.OpenFile, kiểm tra tệp có tồn tại không, và làm việc với thư mục.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Đọ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. Scan trả về false cả khi hết tệp lẫn khi gặp lỗi, và chỉ Err mớ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.ScanRunes trả 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_RDWRmở để đọc, để ghi, hoặc cả hai (chọn một)
os.O_CREATEtạo tệp nếu chưa tồn tại
os.O_TRUNCxóa sạch nội dung tệp khi mở
os.O_APPENDmọi lần ghi đều đi vào cuối tệp
os.O_EXCLdù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àmHàm
tạo một thư mụcos.Mkdir(path, 0o755)
tạo đường dẫn kèm các thư mục chaos.MkdirAll(path, 0o755)
liệt kê một thư mụcos.ReadDir(path)
duyệt cả cây thư mụcfilepath.WalkDir(root, fn)
xóa một tệp hoặc thư mục rỗngos.Remove(path)
xóa cả cây thư mụcos.RemoveAll(path)
đổi tên hoặc di chuyểnos.Rename(old, new)
tệp hoặc thư mục tạmos.CreateTemp("", "prefix-*"), os.MkdirTemp("", "prefix")
ghép các phần của đường dẫnfilepath.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ệnh Read, Write hay Close sau đó 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 Flush trên bufio.Writer. Phần cuối của tệp bị mất.
  • Bỏ qua lỗi từ Close sau 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. 644 không phải 0o644. Go hiểu 644 là số thập phân, tức 0o1204, 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".

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu