Setiap penulisan dan pembacaan berkas pada akhirnya melewati syscall — permintaan yang program ajukan ke kernel sistem operasi. Syscall bukan operasi gratis: kernel harus berganti konteks, memeriksa izin, dan menggerakkan data antara memori program dan buffer kernel. Package bufio menyisipkan satu lapisan memori perantara sehingga puluhan operasi kecil bisa digabung menjadi sedikit operasi besar. Pada chapter ini kita membuktikan mekanisme itu dengan mengamati buffer langsung, lalu memakai bufio.Reader dan bufio.Scanner pada sisi pembacaan.
Buffer Menahan Data Sampai Flush #
bufio.NewWriter(f) membungkus handle f dengan buffer memori berukuran 4096 byte secara default. Method WriteString pada writer tersebut menulis ke buffer, bukan ke berkas — dan status berkas di disk tetap kosong sampai method Flush dipanggil. Program di bawah membuktikannya dengan memeriksa sisa ruang buffer lewat w.Available() dan ukuran berkas lewat f.Stat():
w := bufio.NewWriter(f)
fmt.Println("buffer awal:", w.Available()) // 4096
w.WriteString("halo\n")
fmt.Println("setelah tulis:", w.Available()) // 4091
info, _ := f.Stat()
fmt.Println("disk:", info.Size()) // 0 — data masih di buffer
w.Flush()
info, _ = f.Stat()
fmt.Println("disk:", info.Size()) // 5 — data baru sampai ke diskSetelah WriteString("halo\n") menulis 5 byte, sisa ruang buffer turun dari 4096 menjadi 4091 byte, sementara ukuran berkas di disk masih 0 byte. Baru setelah Flush, kelima byte tersebut berpindah ke berkas. Ketika data melebihi sisa ruang, bufio.Writer otomatis mengirim isi buffer yang penuh lebih dulu, lalu melanjutkan — Flush tetap bertanggung jawab atas sisa data terakhir.
Angka di Balik Buffering #
Mari kita hitung konseptual untuk 5 penulisan masing-masing 20 byte. Tanpa buffer, f.WriteString lima kali menghasilkan lima syscall tulis terpisah. Dengan bufio.Writer, kelima tulisan masuk ke buffer (total 100 byte, masih di bawah 4096), dan satu Flush menghasilkan satu syscall tulis:
| Skenario | Jumlah syscall tulis |
|---|---|
5 × f.WriteString langsung | 5 |
5 × w.WriteString + Flush | 1 |
1000 × f.WriteString langsung | 1000 |
1000 × w.WriteString + Flush | sekitar 5 (100000 byte / 4096, dibulatkan ke atas) |
Pengurangan jumlah syscall adalah sumber utama kecepatan buffered I/O — pada operasi kecil yang berulang, selisihnya bisa mencapai lipat puluhan. Ukuran buffer bisa diatur sendiri lewat bufio.NewWriterSize(f, 8192) bila profil beban program menuntutnya, meski 4096 default sudah seimbang untuk hampir semua kasus.
Membaca dengan bufio.Reader #
Sisi pembacaan punya pasangannya: bufio.NewReader(f) mengisi buffer dari berkas sekali jalan, lalu pemanggilan ReadString mengambil baris dari buffer tanpa menyentuh disk lagi. Method tersebut membaca sampai delimiter ditemukan dan mengembalikan baris beserta delimiternya:
r := bufio.NewReader(f)
for {
baris, err := r.ReadString('\n')
if len(baris) > 0 {
fmt.Print(baris) // masih berakhiran \n
}
if err == io.EOF {
break
}
if err != nil {
fmt.Println("gagal membaca:", err)
break
}
}Ketika aliran habis, ReadString mengembalikan io.EOF — baris terakhir tanpa tanda baris baru tetap ikut terbaca sebelum EOF, sehingga pengecekan len(baris) > 0 harus mendahului pengecekan error. Method lain pada pembaca ini antara lain ReadByte untuk satu byte dan ReadSlice untuk potongan mentah.
Scanner Revisited #
Chapter Membaca File
memakai bufio.Scanner dengan pemisah baris bawaan. Pemisah tersebut sebenarnya bisa diganti lewat method Split:
scanner := bufio.NewScanner(teks)
scanner.Split(bufio.ScanWords)Pilihan siap pakai mencakup ScanLines (default), ScanWords, ScanRunes untuk karakter Unicode satu per satu, dan ScanByte. Dengan ScanWords, method Scan() maju per kata, sehingga menghitung jumlah kata sebuah teks cukup dilakukan dengan loop for scanner.Scan().
Program Utuh #
Berkas contoh.go menggabungkan ketiga bagian: pembuktian buffer dengan Available dan Stat, pembacaan baris dengan ReadString, lalu penghitungan kata dengan ScanWords. Seluruh berkas kerja dibuat di direktori sementara yang dibersihkan otomatis:
package main
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func main() {
// Direktori kerja sementara agar program aman dijalankan berulang.
dir, err := os.MkdirTemp("", "contoh-buffer-*")
if err != nil {
fmt.Println("gagal membuat direktori:", err)
return
}
defer os.RemoveAll(dir)
// Bagian 1: buffer bufio.Writer menahan data sampai Flush dipanggil.
f, err := os.Create(filepath.Join(dir, "log.txt"))
if err != nil {
fmt.Println("gagal membuat berkas:", err)
return
}
w := bufio.NewWriter(f)
fmt.Println("buffer awal :", w.Available(), "byte")
if _, err := w.WriteString("halo\n"); err != nil {
fmt.Println("gagal menulis buffer:", err)
f.Close()
return
}
fmt.Println("setelah tulis :", w.Available(), "byte")
info, err := f.Stat()
if err != nil {
fmt.Println("gagal membaca metadata:", err)
f.Close()
return
}
fmt.Println("disk sebelum Flush :", info.Size(), "byte")
if err := w.Flush(); err != nil {
fmt.Println("gagal mengirim buffer:", err)
f.Close()
return
}
info, err = f.Stat()
if err != nil {
fmt.Println("gagal membaca metadata:", err)
f.Close()
return
}
fmt.Println("disk setelah Flush :", info.Size(), "byte")
f.Close()
// Bagian 2: bufio.Reader membaca baris lewat ReadString.
berkas := filepath.Join(dir, "catatan.txt")
if err := os.WriteFile(berkas, []byte("senin\nselasa\nrabu\n"), 0644); err != nil {
fmt.Println("gagal menulis berkas:", err)
return
}
if err := bacaDenganReader(berkas); err != nil {
fmt.Println("gagal membaca berkas:", err)
}
// Bagian 3: Scanner dengan pemisah kata menggantikan pemisah baris.
teks := strings.NewReader("belajar bufio itu menyenangkan")
scanner := bufio.NewScanner(teks)
scanner.Split(bufio.ScanWords)
kata := 0
for scanner.Scan() {
kata++
}
if err := scanner.Err(); err != nil {
fmt.Println("gagal memindai:", err)
return
}
fmt.Println("jumlah kata :", kata)
}
// bacaDenganReader mencetak setiap baris dengan bufio.Reader.ReadString.
func bacaDenganReader(berkas string) error {
f, err := os.Open(berkas)
if err != nil {
return err
}
defer f.Close()
r := bufio.NewReader(f)
nomor := 0
for {
baris, err := r.ReadString('\n')
if len(baris) > 0 {
nomor++
fmt.Printf("baris %d: %s", nomor, strings.TrimRight(baris, "\n"))
fmt.Println()
}
if err == io.EOF {
return nil
}
if err != nil {
return err
}
}
}Jalankan program dari direktori chapter dengan perintah berikut:
go run contoh.goOutput yang Dihasilkan:
buffer awal : 4096 byte
setelah tulis : 4091 byte
disk sebelum Flush : 0 byte
disk setelah Flush : 5 byte
baris 1: senin
baris 2: selasa
baris 3: rabu
jumlah kata : 4Mari kita telaah baris demi baris program di atas:
- Baris 27-28 (
bufio.NewWriter+Available): writer dibungkus di sekeliling handlef;w.Available()melaporkan sisa ruang buffer 4096 byte — ukuran default yang tidak pernah kita atur. - Baris 30-35 (
WriteString+Available): 5 byte masuk buffer sehingga sisa ruang turun menjadi 4091 byte; tidak ada satu byte pun yang menyentuh disk pada tahap ini. - Baris 37-43 (
f.Statsebelum Flush): ukuran berkas di disk terbaca 0 byte — bukti langsung bahwabufio.Writermenahan data di memori. - Baris 45-56 (
Flush+f.Stat): satu panggilanFlushmemindahkan seluruh isi buffer ke berkas;Statkedua melaporkan 5 byte, tepat setelahhalo\ntiba di disk. - Baris 94-105 (
ReadStringloop):bufio.Readermengembalikan setiap baris beserta tanda\n; pengecekanlen(baris) > 0diletakkan sebelum pengecekanio.EOFagar baris terakhir tanpa baris baru tetap terproses. - Baris 70-76 (
ScanWords): pemisah bawaan diganti denganbufio.ScanWordssehinggascanner.Scan()maju per kata dan loop menghitung 4 kata dari teks uji.
Buffering mempercepat operasi kecil yang berulang, tetapi program tidak berhenti pada satu berkas: aplikasi nyata menjelajah folder, memeriksa metadata, dan membersihkan hasil kerjanya. Chapter Directory dan Filesystem
membahas os.ReadDir, os.Stat, dan penjelajahan pohon direktori.
Referensi #
- Dokumentasi package bufio — ringkasan seluruh tipe buffered I/O.
- Dokumentasi bufio: Writer
— kontrak
Available,Flush, danWriteString. - Dokumentasi bufio: Scanner
— daftar fungsi pemisah untuk
Split.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.go
Unduh berkaspackage main
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func main() {
// Direktori kerja sementara agar program aman dijalankan berulang.
dir, err := os.MkdirTemp("", "contoh-buffer-*")
if err != nil {
fmt.Println("gagal membuat direktori:", err)
return
}
defer os.RemoveAll(dir)
// Bagian 1: buffer bufio.Writer menahan data sampai Flush dipanggil.
f, err := os.Create(filepath.Join(dir, "log.txt"))
if err != nil {
fmt.Println("gagal membuat berkas:", err)
return
}
w := bufio.NewWriter(f)
fmt.Println("buffer awal :", w.Available(), "byte")
if _, err := w.WriteString("halo\n"); err != nil {
fmt.Println("gagal menulis buffer:", err)
f.Close()
return
}
fmt.Println("setelah tulis :", w.Available(), "byte")
info, err := f.Stat()
if err != nil {
fmt.Println("gagal membaca metadata:", err)
f.Close()
return
}
fmt.Println("disk sebelum Flush :", info.Size(), "byte")
if err := w.Flush(); err != nil {
fmt.Println("gagal mengirim buffer:", err)
f.Close()
return
}
info, err = f.Stat()
if err != nil {
fmt.Println("gagal membaca metadata:", err)
f.Close()
return
}
fmt.Println("disk setelah Flush :", info.Size(), "byte")
f.Close()
// Bagian 2: bufio.Reader membaca baris lewat ReadString.
berkas := filepath.Join(dir, "catatan.txt")
if err := os.WriteFile(berkas, []byte("senin\nselasa\nrabu\n"), 0644); err != nil {
fmt.Println("gagal menulis berkas:", err)
return
}
if err := bacaDenganReader(berkas); err != nil {
fmt.Println("gagal membaca berkas:", err)
}
// Bagian 3: Scanner dengan pemisah kata menggantikan pemisah baris.
teks := strings.NewReader("belajar bufio itu menyenangkan")
scanner := bufio.NewScanner(teks)
scanner.Split(bufio.ScanWords)
kata := 0
for scanner.Scan() {
kata++
}
if err := scanner.Err(); err != nil {
fmt.Println("gagal memindai:", err)
return
}
fmt.Println("jumlah kata :", kata)
}
// bacaDenganReader mencetak setiap baris dengan bufio.Reader.ReadString.
func bacaDenganReader(berkas string) error {
f, err := os.Open(berkas)
if err != nil {
return err
}
defer f.Close()
r := bufio.NewReader(f)
nomor := 0
for {
baris, err := r.ReadString('\n')
if len(baris) > 0 {
nomor++
fmt.Printf("baris %d: %s", nomor, strings.TrimRight(baris, "\n"))
fmt.Println()
}
if err == io.EOF {
return nil
}
if err != nil {
return err
}
}
}