Log server, ekspor basis data, dan hasil pengolahan sensor mudah mencapai ukuran ratusan megabyte — jauh melebihi memori yang layak dialokasikan untuk satu operasi baca. Membaca berkas seperti itu dengan os.ReadFile membuat program terhenti atau terbunuh sistem operasi karena kehabisan memori. Solusinya: memproses baris demi baris dengan bufio.Scanner, atau potongan demi potongan dengan loop Read. Pada chapter ini kita membuktikan kedua pola itu pada berkas uji berisi 1000 baris, lalu mengatasi jebakan ketika satu barisnya sendiri terlalu panjang.
Mengapa Tidak Sekadar ReadFile #
os.ReadFile memuat seluruh isi berkas ke satu slice di memori sebelum mengembalikannya. Untuk berkas 5 MB pola ini masih wajar; untuk berkas 5 GB program terpaksa menahan 5 GB data — dua kali lipatnya bila isi lalu diolah menjadi struktur lain. Konsumsi memori os.ReadFile tumbuh linear mengikuti ukuran berkas. Pola streaming menjaganya konstan: yang tertahan di memori hanya satu baris atau satu potongan buffer pada satu waktu.
Memproses Baris demi Baris #
bufio.Scanner mengalokasikan satu buffer internal dan mengisinya dari berkas; setiap Scan() memotong buffer pada tanda baris baru dan menyerahkan baris tersebut lewat Text() atau Bytes(). Setelah baris diproses, iterasi berikutnya menimpa buffer yang sama:
scanner := bufio.NewScanner(f)
for scanner.Scan() {
baris := scanner.Text()
_ = baris // proses satu baris di sini
}
if err := scanner.Err(); err != nil {
fmt.Println("gagal memindai:", err)
}Konsumsi memori pola ini konstan: berkas 1 MB dan 10 GB diproses dengan alokasi buffer yang sama. Program utuh di bawah menghitung 1000 baris berkas uji dengannya — pada berkas log nyata, cukup ganti ekspresi di dalam loop.
Membaca Potongan demi Potongan #
Berkas biner tidak punya konsep baris. Pola yang cocok adalah loop Read dengan buffer berukuran tetap — pola mentah yang sudah kita kenal pada chapter Membaca File
:
buf := make([]byte, 4096)
for {
n, err := f.Read(buf)
if n > 0 {
_ = buf[:n] // proses potongan di sini
}
if err == io.EOF {
break
}
if err != nil {
fmt.Println("gagal membaca potongan:", err)
break
}
}Buffer 4096 byte seukuran satu halaman memori dan selaras dengan buffer internal bufio — setiap Read menyalin satu blok dari kernel tanpa alokasi baru. Loop pada program utuh membaca 11000 byte dengan tiga panggilan berisi data plus satu panggilan akhir ber-io.EOF.
Baris Panjang dan Batas Scanner #
Scanner bawaan menyimpan satu kejutan: batas maksimum token adalah 64 KB. Satu baris JSON yang dihasilkan mesin, atau baris CSV dengan kolom banyak, mudah melampaui batas tersebut — dan pemindaian berhenti dengan error bufio.Scanner: token too long (pesan asli dari Go 1.27). Method Buffer menaikkan batas itu:
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024), 1<<20) // naikkan hingga 1 MBArgumen pertama adalah buffer awal yang akan ditumbuhkan bila perlu; argumen kedua adalah batas maksimum ukuran token. Penaikkan batas menambah potensi konsumsi memori sebesar batas baru tersebut — angka yang masih jauh lebih kecil daripada memuat seluruh berkas.
Program Utuh #
Berkas contoh.go membuat berkas uji 1000 baris (11000 byte) di direktori sementara, menghitung barisnya dengan Scanner, membaca isinya per potongan 4096 byte, lalu menguji berkas berisi satu baris 200000 byte: pertama dengan pemindai bawaan yang gagal, kemudian dengan Buffer yang berhasil:
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-besar-*")
if err != nil {
fmt.Println("gagal membuat direktori:", err)
return
}
defer os.RemoveAll(dir)
// Berkas uji: 1000 baris masing-masing 11 byte, total 11000 byte.
besar := filepath.Join(dir, "data-besar.txt")
f, err := os.Create(besar)
if err != nil {
fmt.Println("gagal membuat berkas:", err)
return
}
for i := 1; i <= 1000; i++ {
fmt.Fprintf(f, "baris-%04d\n", i)
}
if err := f.Close(); err != nil {
fmt.Println("gagal menutup berkas:", err)
return
}
info, err := os.Stat(besar)
if err != nil {
fmt.Println("gagal membaca metadata:", err)
return
}
fmt.Printf("berkas uji : %d byte\n", info.Size())
// Scanner menghitung baris dengan memori konstan.
barisan, err := hitungBaris(besar)
if err != nil {
fmt.Println("gagal menghitung baris:", err)
return
}
fmt.Printf("scanner : %d baris\n", barisan)
// Loop Read membaca per potongan 4096 byte.
total, panggilan, err := bacaPerPotongan(besar, 4096)
if err != nil {
fmt.Println("gagal membaca potongan:", err)
return
}
fmt.Printf("loop Read 4096 : %d byte, %d panggilan Read\n", total, panggilan)
// Satu baris raksasa mengalahkan batas bawaan Scanner.
isiPanjang := strings.Repeat("x", 200000) + "\n"
panjang := filepath.Join(dir, "baris-panjang.txt")
if err := os.WriteFile(panjang, []byte(isiPanjang), 0644); err != nil {
fmt.Println("gagal menulis berkas:", err)
return
}
fmt.Printf("baris panjang : %d byte\n", len(isiPanjang))
err = pemindaianBawaan(panjang)
fmt.Println("scanner bawaan:", err)
barisanPanjang, ukuran, err := pemindaianBerbuffer(panjang)
if err != nil {
fmt.Println("gagal memindai:", err)
return
}
fmt.Printf("scanner+Buffer : %d baris, %d byte\n", barisanPanjang, ukuran)
}
// hitungBaris menghitung jumlah baris berkas dengan bufio.Scanner.
func hitungBaris(berkas string) (int, error) {
f, err := os.Open(berkas)
if err != nil {
return 0, err
}
defer f.Close()
barisan := 0
scanner := bufio.NewScanner(f)
for scanner.Scan() {
barisan++
}
return barisan, scanner.Err()
}
// bacaPerPotongan membaca berkas dengan buffer berukuran tertentu.
func bacaPerPotongan(berkas string, ukuran int) (int, int, error) {
f, err := os.Open(berkas)
if err != nil {
return 0, 0, err
}
defer f.Close()
buf := make([]byte, ukuran)
total, panggilan := 0, 0
for {
n, err := f.Read(buf)
panggilan++
if n > 0 {
total += n
}
if err == io.EOF {
return total, panggilan, nil
}
if err != nil {
return total, panggilan, err
}
}
}
// pemindaianBawaan memindai dengan batas bawaan Scanner (64 KB per token).
func pemindaianBawaan(berkas string) error {
f, err := os.Open(berkas)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
}
return scanner.Err()
}
// pemindaianBerbuffer menaikkan batas token Scanner lewat method Buffer.
func pemindaianBerbuffer(berkas string) (int, int, error) {
f, err := os.Open(berkas)
if err != nil {
return 0, 0, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024), 1<<20)
barisan, ukuran := 0, 0
for scanner.Scan() {
barisan++
ukuran += len(scanner.Bytes())
}
if err := scanner.Err(); err != nil {
return 0, 0, err
}
return barisan, ukuran, nil
}Jalankan program dari direktori chapter dengan perintah berikut:
go run contoh.goOutput yang Dihasilkan:
berkas uji : 11000 byte
scanner : 1000 baris
loop Read 4096 : 11000 byte, 4 panggilan Read
baris panjang : 200001 byte
scanner bawaan: bufio.Scanner: token too long
scanner+Buffer : 1 baris, 200000 byteMari kita telaah baris demi baris program di atas:
- Baris 22-40 (pembuatan berkas uji): loop menulis
baris-0001sampaibaris-1000, masing-masing 11 byte, sehinggaos.Statmelaporkan ukuran tetap 11000 byte — angka yang sama pada setiap run. - Baris 78-92 (
hitungBaris): Scanner maju satu baris perScan(); buffer internalnya dipakai ulang sehingga memori yang tertahan tidak bertambah meski berkasnya jauh lebih besar. - Baris 94-117 (
bacaPerPotongan): buffer 4096 byte diisi berulang; 11000 byte terbagi menjadi tiga panggilan berisi data (4096 + 4096 + 2808) dan satu panggilan terakhir yang mengembalikanio.EOF— total empat panggilanRead. - Baris 58-68 (baris raksasa + pemindai bawaan): berkas 200001 byte berisi satu baris melebihi batas token 64 KB, sehingga
scanner.Err()mengembalikan error aslibufio.Scanner: token too long. - Baris 141-147 (
scanner.Buffer): buffer awal 1024 byte dengan batas maksimum 1 MB (1<<20) menumbuhkan kapasitas Scanner sampai satu baris 200000 byte berhasil dipindai utuh — satu baris, 200000 byte, tanpa error.
Kita kini memproses berkas lokal dengan dua pola hemat memori. Pola io.Reader pada chapter Streaming Data
berlaku juga untuk koneksi HTTP dan pipa antar proses. Sebagai penutup section File dan I/O, Anda memegang rantai lengkapnya: dari os.ReadFile untuk berkas kecil hingga streaming untuk data raksasa.
Referensi #
- Dokumentasi bufio: Scanner
— aturan batas token dan method
Buffer. - Dokumentasi bufio: Writer — buffer baca/tulis untuk pemrosesan efisien.
- Dokumentasi os: ReadFile — kontrak baca sekali jalan dan implikasi memorinya.
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-besar-*")
if err != nil {
fmt.Println("gagal membuat direktori:", err)
return
}
defer os.RemoveAll(dir)
// Berkas uji: 1000 baris masing-masing 11 byte, total 11000 byte.
besar := filepath.Join(dir, "data-besar.txt")
f, err := os.Create(besar)
if err != nil {
fmt.Println("gagal membuat berkas:", err)
return
}
for i := 1; i <= 1000; i++ {
fmt.Fprintf(f, "baris-%04d\n", i)
}
if err := f.Close(); err != nil {
fmt.Println("gagal menutup berkas:", err)
return
}
info, err := os.Stat(besar)
if err != nil {
fmt.Println("gagal membaca metadata:", err)
return
}
fmt.Printf("berkas uji : %d byte\n", info.Size())
// Scanner menghitung baris dengan memori konstan.
barisan, err := hitungBaris(besar)
if err != nil {
fmt.Println("gagal menghitung baris:", err)
return
}
fmt.Printf("scanner : %d baris\n", barisan)
// Loop Read membaca per potongan 4096 byte.
total, panggilan, err := bacaPerPotongan(besar, 4096)
if err != nil {
fmt.Println("gagal membaca potongan:", err)
return
}
fmt.Printf("loop Read 4096 : %d byte, %d panggilan Read\n", total, panggilan)
// Satu baris raksasa mengalahkan batas bawaan Scanner.
isiPanjang := strings.Repeat("x", 200000) + "\n"
panjang := filepath.Join(dir, "baris-panjang.txt")
if err := os.WriteFile(panjang, []byte(isiPanjang), 0644); err != nil {
fmt.Println("gagal menulis berkas:", err)
return
}
fmt.Printf("baris panjang : %d byte\n", len(isiPanjang))
err = pemindaianBawaan(panjang)
fmt.Println("scanner bawaan:", err)
barisanPanjang, ukuran, err := pemindaianBerbuffer(panjang)
if err != nil {
fmt.Println("gagal memindai:", err)
return
}
fmt.Printf("scanner+Buffer : %d baris, %d byte\n", barisanPanjang, ukuran)
}
// hitungBaris menghitung jumlah baris berkas dengan bufio.Scanner.
func hitungBaris(berkas string) (int, error) {
f, err := os.Open(berkas)
if err != nil {
return 0, err
}
defer f.Close()
barisan := 0
scanner := bufio.NewScanner(f)
for scanner.Scan() {
barisan++
}
return barisan, scanner.Err()
}
// bacaPerPotongan membaca berkas dengan buffer berukuran tertentu.
func bacaPerPotongan(berkas string, ukuran int) (int, int, error) {
f, err := os.Open(berkas)
if err != nil {
return 0, 0, err
}
defer f.Close()
buf := make([]byte, ukuran)
total, panggilan := 0, 0
for {
n, err := f.Read(buf)
panggilan++
if n > 0 {
total += n
}
if err == io.EOF {
return total, panggilan, nil
}
if err != nil {
return total, panggilan, err
}
}
}
// pemindaianBawaan memindai dengan batas bawaan Scanner (64 KB per token).
func pemindaianBawaan(berkas string) error {
f, err := os.Open(berkas)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
}
return scanner.Err()
}
// pemindaianBerbuffer menaikkan batas token Scanner lewat method Buffer.
func pemindaianBerbuffer(berkas string) (int, int, error) {
f, err := os.Open(berkas)
if err != nil {
return 0, 0, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024), 1<<20)
barisan, ukuran := 0, 0
for scanner.Scan() {
barisan++
ukuran += len(scanner.Bytes())
}
if err := scanner.Err(); err != nil {
return 0, 0, err
}
return barisan, ukuran, nil
}