io และ bufio
Interface io.Reader และ io.Writer
หัวข้อที่มีชื่อว่า “Interface io.Reader และ io.Writer”Package io นิยาม interface ขนาดเล็กแต่เป็นรากฐานสองตัว:
type Reader interface { Read(p []byte) (n int, err error)}
type Writer interface { Write(p []byte) (n int, err error)}Interface ทั้งสองนี้คือภาษากลางของ I/O ใน Go มี type จำนวนมากที่ implement ทั้งสอง: ไฟล์, network connection, HTTP request และ response body, in-memory buffer และอื่นๆ function ที่รับ io.Reader หรือ io.Writer จะทำงานกับทุก type เหล่านั้นโดยไม่ต้องแก้ไขโค้ด
strings.NewReader — Reader ในหน่วยความจำ
หัวข้อที่มีชื่อว่า “strings.NewReader — Reader ในหน่วยความจำ”strings.NewReader(s) ห่อ string เป็น io.Reader มีประโยชน์มากใน test และตัวอย่าง — สามารถทดสอบ function ที่อ่านจาก stream ได้โดยไม่ต้องแตะ filesystem
r := strings.NewReader("hello, world")data, _ := io.ReadAll(r)fmt.Println(string(data)) // hello, worldbytes.Buffer — buffer สำหรับอ่านและเขียน
หัวข้อที่มีชื่อว่า “bytes.Buffer — buffer สำหรับอ่านและเขียน”bytes.Buffer implement ทั้ง io.Reader และ io.Writer เขียนลงไปแล้วอ่านกลับออกมา:
var buf bytes.Bufferfmt.Fprintln(&buf, "first line")fmt.Fprintln(&buf, "second line")content, _ := io.ReadAll(&buf)fmt.Print(string(content))fmt.Fprintln รับ io.Writer ใดก็ได้ ดังนั้นจึงเปลี่ยน output ที่ format แล้วไปยัง buffer, ไฟล์ หรือ network connection ได้โดยไม่ต้องเปลี่ยนโค้ด
bufio.Scanner — การอ่านทีละบรรทัด
หัวข้อที่มีชื่อว่า “bufio.Scanner — การอ่านทีละบรรทัด”bufio.Scanner ห่อ io.Reader ใดก็ได้และให้การอ่านทีละบรรทัด (หรือทีละ token) อย่างสะดวก function split แบบ default คือ bufio.ScanLines
scanner := bufio.NewScanner(r)for scanner.Scan() { fmt.Println(scanner.Text()) // one line at a time}if err := scanner.Err(); err != nil { log.Fatal(err)}scanner.Scan() คืน false เมื่อ input หมดหรือเกิด error ควรตรวจสอบ scanner.Err() หลังลูปเสมอ — การคืน false เพียงอย่างเดียวไม่ได้บอกว่าเป็น EOF หรือ error
function helper ของ io
หัวข้อที่มีชื่อว่า “function helper ของ io”Package io ยังมี helper ที่มีประโยชน์:
io.ReadAll(r) // read until EOF, return []byteio.Copy(dst, src) // copy from Reader to Writerio.MultiReader(r1, r2) // chain readers sequentiallyio.Discard // Writer that discards all bytes (like /dev/null)io.LimitReader(r, n) // read at most n bytespackage main
import ( "bufio" "bytes" "fmt" "io" "os" "strings")
// printLines accepts any io.Reader — works with files, buffers, or stringsfunc printLines(r io.Reader) { scanner := bufio.NewScanner(r) for scanner.Scan() { fmt.Println(">", scanner.Text()) } if err := scanner.Err(); err != nil { fmt.Println("scan error:", err) }}
func main() { // strings.NewReader implements io.Reader r := strings.NewReader("first line\nsecond line\nthird line") printLines(r)
// bytes.Buffer implements both io.Reader and io.Writer var buf bytes.Buffer fmt.Fprintln(&buf, "written to buffer") fmt.Fprintln(&buf, "another line") content, _ := io.ReadAll(&buf) fmt.Print(string(content))
// io.MultiReader chains two readers sequentially r1 := strings.NewReader("hello ") r2 := strings.NewReader("world\n") combined := io.MultiReader(r1, r2) fmt.Print("MultiReader output: ") io.Copy(os.Stdout, combined)
// bufio.Scanner over in-memory CSV csv := "alice,30\nbob,25\ncharlie,35" scanner := bufio.NewScanner(strings.NewReader(csv)) for scanner.Scan() { parts := strings.Split(scanner.Text(), ",") fmt.Printf("name=%s age=%s\n", parts[0], parts[1]) }} Contract ของ io.Reader แบบละเอียด
หัวข้อที่มีชื่อว่า “Contract ของ io.Reader แบบละเอียด”signature สองบรรทัด Read(p []byte) (n int, err error) ซ่อนกฎสามข้อที่ทำให้โค้ดที่ AI เขียนพลาดบ่อยมาก:
Readคืนn > 0พร้อมerrที่ไม่ใช่ nil ในการเรียกครั้งเดียวกันได้ caller ต้อง process byte จำนวนnก่อนที่จะเช็กerr— ถ้าทิ้งข้อมูลนั้นไปเพราะerr != nilจะทำให้ chunk สุดท้ายหายไปเงียบๆ- “short read” เป็นเรื่องถูกกฎหมาย
Readอาจเติมข้อมูลให้pน้อยกว่าlen(p)ได้แม้ว่าจะยังมีข้อมูลเหลือและerr == nilห้ามสมมติว่าเรียกReadครั้งเดียวจะเต็ม buffer เสมอ — ต้องวน loop หรือใช้ helper ที่วน loop ให้ io.EOFไม่ใช่ความล้มเหลว มันคือสัญญาณปกติว่าไม่มีข้อมูลเหลือแล้ว ให้ปฏิบัติกับerr == io.EOFว่า “หยุด loop ได้ ไม่มี error ต้องรายงาน” — ห้ามlog.Fatalมันหรือส่งมันขึ้นไปทั้งดุ้นเหมือนมีอะไรพัง
n, err := r.Read(buf)if n > 0 { process(buf[:n]) // always handle n bytes first}if err != nil { if err == io.EOF { return nil // clean end of stream, not an error } return err}เมื่อต้องการ byte ตามจำนวนที่แน่นอนหรือขั้นต่ำโดยไม่อยากเขียน loop เอง io.ReadFull(r, buf) อ่านจนกว่า buf จะเต็มหรือคืน io.ErrUnexpectedEOF ถ้า stream จบก่อน; io.ReadAtLeast(r, buf, min) คือแนวคิดเดียวกันแต่สำหรับความยาวขั้นต่ำแทนที่จะเต็ม buffer
ขีดจำกัด token 64 KB ของ bufio.Scanner
หัวข้อที่มีชื่อว่า “ขีดจำกัด token 64 KB ของ bufio.Scanner”bufio.Scanner จำกัดแต่ละ token (บรรทัด) ไว้ที่ bufio.MaxScanTokenSize — 64 KB — โดย default เมื่อบรรทัดยาวเกินนั้น Scan() จะคืน false และ Err() จะคืน bufio.ErrTooLong ซึ่งดูเหมือน EOF ที่สะอาดทุกประการถ้าไม่ได้เช็ก Err() หลัง loop — input ที่เหลือจะหายไปเงียบๆ
scanner := bufio.NewScanner(r)scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) // raise the max to 1 MBfor scanner.Scan() { process(scanner.Text())}if err := scanner.Err(); err != nil { log.Fatal(err) // catches ErrTooLong, not just I/O errors}scanner.Buffer(initial, max) กำหนดทั้ง buffer เริ่มต้นและเพดานที่มันขยายไปได้ เรียกก่อน Scan() ครั้งแรกถ้ามีโอกาสที่บรรทัดเดียวจะยาวเกิน 64 KB จริงๆ (บรรทัด JSON ยาวๆ, base64 blob)
bufio.Writer, Flush และ error จาก Close
หัวข้อที่มีชื่อว่า “bufio.Writer, Flush และ error จาก Close”bufio.Writer buffer การเขียนไว้ในหน่วยความจำและจะไปถึง io.Writer ข้างล่างก็ต่อเมื่อ buffer เต็ม หรือเมื่อเรียก Flush() เอง ไม่มีอะไรรับประกันว่าข้อมูลถึง disk หรือ network แล้วจนกว่า Flush() จะ return
error จาก Close() ก็สำคัญด้วยเหตุผลเดียวกัน: type ที่สร้างบน buffered writer (หรือไฟล์) อาจแสดง write failure ที่ล่าช้า — เช่น disk เต็ม — ได้เฉพาะตอนที่ flush ครั้งสุดท้ายเกิดขึ้นภายใน Close เท่านั้น การไม่สนใจ return value ของ defer f.Close() จะกลืน failure นั้นไปเงียบๆ
w := bufio.NewWriter(f)fmt.Fprintln(w, "important data")if err := w.Flush(); err != nil { return fmt.Errorf("flush: %w", err) // the write may not have failed until now}if err := f.Close(); err != nil { return fmt.Errorf("close: %w", err) // a writer's Close error is not decorative}io.Copy, TeeReader, Pipe และ NopCloser
หัวข้อที่มีชื่อว่า “io.Copy, TeeReader, Pipe และ NopCloser”io.Copy(dst, src) อ่านจาก src แล้วเขียนไปยัง dst จนกว่า src จะคืน io.EOF โดยอาศัย contract ของ Writer: Write ต้องคืน error ที่ไม่ใช่ nil ถ้าเขียนได้น้อยกว่า len(p) byte ดังนั้น io.Copy จึงเชื่อได้ว่า error ที่เป็น nil หมายความว่า chunk ทั้งหมดถูกเขียนสำเร็จ
helper เล็กๆ อีกสามตัวของ io ที่ครบชุดเครื่องมือ:
io.TeeReader(r, w)— คืน Reader ที่ mirror ทุก byte ที่มันอ่านไปยังwเป็น side effect (checksum หรือ log stream ระหว่างที่ถูกอ่าน โดยไม่ต้อง buffer ทั้งหมดไว้ในหน่วยความจำก่อน)io.Pipe()— คู่io.Reader/io.Writerในหน่วยความจำแบบ synchronous ไม่มี buffer; write จะ block จนกว่าจะมี read ที่ตรงกันมาดึงออกไป มีประโยชน์เมื่อจะ stream ข้อมูลจาก goroutine หนึ่งไปอีก goroutine โดยไม่ต้องมี buffer คั่นกลางio.NopCloser(r)— ห่อio.Readerด้วยCloseที่ไม่ทำอะไร (no-op) เพื่อให้ตอบสนองio.ReadCloserนี่คือสิ่งที่http.Request.Bodyต้องการพอดีใน test หรือสิ่งที่ส่งกลับเมื่อไม่อยากให้ caller ปิด reader ที่ใช้ร่วมกัน
tee := io.TeeReader(resp.Body, hasher) // reading tee also feeds hasherreq.Body = io.NopCloser(bytes.NewReader(replayed)) // satisfy io.ReadCloser in a testpackage main
import ( "bufio" "bytes" "fmt" "io" "strings")
// shortReader returns at most 3 bytes per Read call, and delivers n>0// together with io.EOF on the final call — a legal io.Reader.type shortReader struct { data []byte}
func (r *shortReader) Read(p []byte) (int, error) { if len(r.data) == 0 { return 0, io.EOF } n := copy(p, r.data[:min(3, len(r.data))]) r.data = r.data[n:] if len(r.data) == 0 { return n, io.EOF // n > 0 AND err != nil in the same call } return n, nil}
func main() { // 1. short reads: n>0 with err!=nil on the last call sr := &shortReader{data: []byte("hello!")} buf := make([]byte, 10) for { n, err := sr.Read(buf) fmt.Printf("Read: n=%d err=%v data=%q\n", n, err, buf[:n]) if err != nil { break } }
// 2. io.ReadFull loops for you sr2 := &shortReader{data: []byte("hello!")} exact := make([]byte, 6) n, err := io.ReadFull(sr2, exact) fmt.Printf("ReadFull: n=%d err=%v data=%q\n", n, err, exact[:n])
// io.ReadFull when the stream has fewer bytes than requested sr3 := &shortReader{data: []byte("hi")} short := make([]byte, 6) n, err = io.ReadFull(sr3, short) fmt.Printf("ReadFull short: n=%d err=%v\n", n, err)
// 3. bufio.Scanner ErrTooLong when a token exceeds the buffer max longLine := strings.Repeat("x", 20) scanner := bufio.NewScanner(strings.NewReader(longLine + "\ntail\n")) scanner.Buffer(make([]byte, 0, 16), 16) // small max on purpose for scanner.Scan() { fmt.Println("scanned:", scanner.Text()) } fmt.Println("scanner.Err():", scanner.Err())
// 4. bufio.Writer needs Flush var out bytes.Buffer w := bufio.NewWriter(&out) fmt.Fprint(w, "buffered, not flushed yet") fmt.Println("before Flush, out.Len():", out.Len()) w.Flush() fmt.Println("after Flush, out.Len():", out.Len())
// 5. io.TeeReader mirrors what it reads into a side buffer var tee bytes.Buffer tr := io.TeeReader(strings.NewReader("mirrored"), &tee) got, _ := io.ReadAll(tr) fmt.Printf("TeeReader read=%q sideBuffer=%q\n", got, tee.String())
// 6. io.NopCloser satisfies io.ReadCloser for a plain Reader var rc io.ReadCloser = io.NopCloser(strings.NewReader("wrapped")) data, _ := io.ReadAll(rc) fmt.Println("NopCloser Close err:", rc.Close(), "data:", string(data))} output จริงจากโปรแกรมนี้บน Go 1.26.5:
Read: n=3 err=<nil> data="hel"Read: n=3 err=EOF data="lo!"ReadFull: n=6 err=<nil> data="hello!"ReadFull short: n=2 err=unexpected EOFscanner.Err(): bufio.Scanner: token too longbefore Flush, out.Len(): 0after Flush, out.Len(): 25TeeReader read="mirrored" sideBuffer="mirrored"NopCloser Close err: <nil> data: wrappedสังเกต loop ของ scanner ด้านบน: scanned: ไม่ถูกพิมพ์เลย เพราะบรรทัดยาว 20 byte เกินเพดาน 16 byte ตั้งแต่ token แรก Scan() จึงคืน false ทันที และ Err() รายงาน bufio.Scanner: token too long — บรรทัด "tail" ไม่ถูกเห็นเลย
Pipeline ของ Reader/Writer
หัวข้อที่มีชื่อว่า “Pipeline ของ Reader/Writer”flowchart LR File["os.File / net.Conn"] --> BR["bufio.Reader"] BR --> Scanner["bufio.Scanner"] Scanner --> App["your code"] App2["your code"] --> BW["bufio.Writer"] BW -- "Flush()" --> Out["os.File / net.Conn"] Src["io.Reader"] -- "io.Copy" --> Dst["io.Writer"] Src -- "io.TeeReader" --> Side["side io.Writer"]
bytes.Buffer ห้าม copy
หัวข้อที่มีชื่อว่า “bytes.Buffer ห้าม copy”bytes.Buffer เก็บข้อมูลไว้ใน slice ภายใน; การ copy bytes.Buffer โดยค่า (assign, ส่งเป็น argument โดยค่า, หรือเก็บใน slice ที่จะ append ต่อ) จะ copy แค่ slice header โดยไม่มี mutex หรือ lock ให้ copy ตามไปด้วย — แต่ทั้งสอง copy จะ alias backing array เดียวกันในขณะที่แต่ละตัวติดตาม offset การอ่าน/เขียนของตัวเองแยกกัน ทำให้การเขียนและอ่านบน copy หนึ่งไปทำลายสิ่งที่อีก copy เห็น package doc พูดตรงๆ ไว้เลยว่า “The zero value for Buffer is an empty buffer ready to use… Do not copy a non-zero Buffer.”
ต่างจาก sync.Mutex ตรงที่ไม่มี check แบบ go vet copylocks สำหรับกรณีนี้ — bytes.Buffer ไม่ได้ embed marker noCopy ดังนั้น compiler และ go vet จะเงียบ ให้ส่ง *bytes.Buffer เสมอ ไม่ใช่ bytes.Buffer โดยค่า เมื่อมันมีข้อมูลอยู่แล้ว
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart LR
subgraph R["io.Reader interface"]
Src["Source"] --> Read["Read(p []byte)"] --> Buf["buffer p"] --> Consumer["consumer"]
end
subgraph C["Chaining readers"]
F["os.File (raw)"] --> BRd["bufio.NewReader (buffered)"] --> GZ["gzip.NewReader (decompressed)"] --> JD["json.NewDecoder (decoded)"]
end
subgraph W["bufio.Writer batching"]
Small["small writes (many syscalls)"] --> WB["buffer 4 KB"] --> Flush["one Flush (one syscall)"] --> Out["os.File"]
end ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| bufio.Reader | ลด syscall (batching reads) | เพิ่ม memory สำหรับ buffer |
| bufio.Writer | ลด syscall (batching writes) | ต้อง Flush() ก่อน close |
| io.Reader chain | composable, zero-copy pipeline | error handling ซับซ้อนเมื่อ chain ยาว |
| io.Copy | efficient transfer ระหว่าง Reader→Writer | ไม่ inspect data ระหว่างทาง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- unbuffered read เร็วพอ — ทุก Read() call โดยไม่มี buffer = 1 syscall — ช้ามากสำหรับ small reads จำนวนมาก
- bufio.Writer flush อัตโนมัติ — ไม่ — ต้อง Flush() เองก่อน close หรือส่ง response ไม่งั้น data อาจหาย
- io.Reader อ่าน exact n bytes เสมอ — Read() อาจ return น้อยกว่า n bytes — ต้องวน loop ถ้าต้องการ exact read
- strings.NewReader กับ bytes.NewReader เหมือนกัน — strings.NewReader สำหรับ string literal, bytes.NewReader สำหรับ []byte ที่มีอยู่แล้ว
💡 ตัวอย่างจากของจริง
Go HTTP server: net/http ห่อ TCP connection ด้วย bufio.Reader โดยอัตโนมัติ — request parsing ไม่ต้อง syscall ทุก byte
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI assistant เขียนโค้ด io/bufio ที่ compile ผ่านและดูสมเหตุสมผลบ่อยกว่าโค้ดที่ถูกต้องจริงๆ ในกรณีขอบเยอะมาก เช็กสิ่งเหล่านี้ก่อนเชื่อว่า review ผ่าน:
- loop ของ
bufio.Scannerเช็กscanner.Err()หลัง loop หรือถือว่าfalseหมายถึง EOF ที่สะอาดเสมอ? - ถ้า input มีโอกาสมีบรรทัดยาวเกิน 64 KB มีการเรียก
scanner.Buffer(...)เพื่อขยายเพดานหรือไม่ หรือจะถูกตัดทิ้งเงียบๆ? - สิ่งที่เขียนผ่าน
bufio.WriterถูกFlush()ก่อน function จะ return หรือก่อนส่ง response หรือไม่? - return value ของ
f.Close()ถูกเช็กหรือไม่เมื่อfถูกเขียนลงไป ไม่ใช่แค่ตอนอ่านอย่างเดียว? Readที่เขียนขึ้นเอง สมมติว่า caller จะได้ buffer เต็มเสมอหรือไม่ หรือ caller วน loop / ใช้io.ReadFullอย่างถูกต้อง?bytes.Bufferถูก copy โดยค่า (ส่งเป็น argument, เก็บใน slice, return จาก function) หลังจากมันมีข้อมูลแล้วหรือไม่?
🐛 หา bug ในโค้ดนี้
func loadConfig(path string) (map[string]string, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close()
cfg := map[string]string{} scanner := bufio.NewScanner(f) for scanner.Scan() { k, v, _ := strings.Cut(scanner.Text(), "=") cfg[k] = v } return cfg, nil}เฉลย
loop ไม่เคยเช็ก scanner.Err() เลย ถ้าบรรทัดหนึ่งใน path ยาวเกินเพดาน default 64 KB — ค่ายาวๆ, ไฟล์ binary หลุดเข้ามา — Scan() จะคืน false, loop จบ, และ loadConfig จะคืน config ที่ไม่ครบพร้อม error เป็น nil เหมือนกับว่าไฟล์ถูกอ่านครบแล้ว แก้ได้โดยเช็ก error หลัง loop:
if err := scanner.Err(); err != nil { return nil, fmt.Errorf("scan %s: %w", path, err)}return cfg, nilถ้าบรรทัดที่ยาวเกิน 64 KB เป็นเรื่องปกติที่คาดไว้ ให้เรียก scanner.Buffer(make([]byte, 0, 64*1024), maxLineSize) ก่อน loop แทนที่จะแค่ตรวจจับ failure