Ba gói này là nền của mọi thứ đọc ghi trong Go. Bài này về cách chúng ghép với nhau.
io.Reader và io.Writer
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
Mỗi cái đúng một method, và đó là toàn bộ lý do chúng ở khắp nơi. Bài 13 đã nói: interface càng nhỏ, càng nhiều thứ thoả mãn nó.
Thoả mãn io.Reader: tệp, kết nối mạng, thân HTTP request, chuỗi, bộ đệm, tệp nén, luồng mã hoá. Viết một hàm nhận io.Reader là nó làm việc với tất cả.
b, _ := io.ReadAll(strings.NewReader("xin chào"))
io.Copy(&buf, r)
io.Copy là hàm tôi dùng nhiều nhất: chép từ Reader sang Writer với bộ đệm 32KB, không nạp hết vào bộ nhớ.
Vài kiểu tiện ích đáng nhớ:
io.Discard // Writer vứt mọi thứ
io.LimitReader(r, n) // chỉ đọc n byte đầu — chống tấn công gửi dữ liệu vô hạn
io.MultiWriter(a, b) // ghi ra nhiều đích cùng lúc
io.TeeReader(r, w) // đọc từ r, đồng thời ghi bản sao vào w
io.LimitReader đáng dùng ở mọi chỗ đọc dữ liệu từ ngoài vào.
bufio: gom lời gọi hệ thống
ghi thẳng 200.000 lần : 58 ms
qua bufio.Writer : 1 ms
Năm mươi tám lần.
Nguyên nhân: mỗi f.Write() là một lời gọi hệ thống, và lời gọi hệ thống đắt. bufio.Writer gom dữ liệu vào bộ đệm 4KB và chỉ gọi hệ thống khi đầy.
w := bufio.NewWriter(f)
defer w.Flush() // BẮT BUỘC
for ... { w.WriteString(...) }
Flush() là mất dữ liệu. Phần còn trong bộ đệm không bao giờ xuống đĩa, và f.Close() KHÔNG tự flush bộ đệm của bufio — nó chỉ đóng tệp bên dưới.
Thứ tự đúng khi dùng cả hai: w.Flush() trước f.Close(). Vì defer chạy ngược (bài 26), viết defer f.Close() rồi defer w.Flush() sẽ cho đúng thứ tự.
Và như bài 30 đã nói, với tệp ghi thì lỗi Flush cũng cần được kiểm.
Chiều đọc tương tự: bufio.NewReader(f) gom nhiều lần đọc nhỏ thành ít lời gọi hệ thống.
bufio.Scanner: đọc từng dòng
sc := bufio.NewScanner(f)
for sc.Scan() {
dong := sc.Text()
}
if err := sc.Err(); err != nil { ... }
quét được 3 dòng, err=<nil>
sc.Err() là dòng người ta hay quên. Scan() trả false cho cả hai trường hợp: hết dữ liệu, và có lỗi. Không kiểm Err() thì lỗi đọc giữa chừng trông y hệt đọc xong.
Và một giới hạn hay cắn: mặc định 64KB mỗi dòng. Dòng dài hơn thì Scan() trả false với lỗi bufio.Scanner: token too long.
sc.Buffer(make([]byte, 1024*1024), 1024*1024) // nâng lên 1MB
Với dữ liệu không kiểm soát được độ dài dòng, dùng bufio.Reader.ReadString('\n') thay vì Scanner.
Scanner tách được theo thứ khác:
sc.Split(bufio.ScanWords) // theo từ
sc.Split(bufio.ScanRunes) // theo rune
fmt: động từ định dạng
fmt.Printf("%v %+v %#v %T %q\n", x, x, x, x, s)
Năm động từ đáng thuộc:
%v — dạng mặc định. %+v — thêm tên trường của struct, thứ bạn muốn 90% thời gian khi gỡ lỗi. %#v — cú pháp Go, dán lại vào mã được. %T — in ra kiểu, công cụ tốt nhất khi làm việc với any (bài 14). %q — chuỗi có nháy và thoát ký tự, cực hữu ích với dữ liệu có khoảng trắng ẩn.
Hai hàm phân biệt:
fmt.Sprintf(...) // trả về chuỗi
fmt.Fprintf(w, ...) // ghi vào Writer bất kỳ
Fprintf với os.Stdout chính là Printf. Viết hàm nhận io.Writer thay vì in thẳng làm nó test được.
Và nhớ từ bài 12: định nghĩa String() string là fmt tự gọi nó — nhưng cẩn thận đệ quy vô hạn nếu bên trong lại Sprintf("%v", chính nó).
fmt chậm hơn bạn nghĩ
fmt dùng phản chiếu, nên nó chậm hơn nhiều so với nối chuỗi trực tiếp. Trong đường chạy nóng:
s := "id=" + strconv.Itoa(n) // nhanh
s := fmt.Sprintf("id=%d", n) // chậm hơn nhiều
Với mã thường thì không đáng bận tâm — độ đọc quan trọng hơn. Nhưng trong vòng lặp hàng triệu lần hoặc trong hàm log gọi liên tục, khác biệt là thật. Bài 52 sẽ đo.
Thử ba mươi giây
f, _ := os.Create("/tmp/t")
w := bufio.NewWriter(f)
w.WriteString("dữ liệu quan trọng")
f.Close() // KHÔNG có w.Flush()
Mở /tmp/t — tệp rỗng. Thêm w.Flush() trước f.Close() và dữ liệu xuất hiện.
Ba mươi giây đó là lỗi bufio phổ biến nhất, và nó im lặng hoàn toàn.
Ngày mai: os và làm việc với tệp.