Semua contoh sejauh ini memuat data seluruhnya ke memori sebelum memprosesnya — mustahil untuk berkas 10 GB atau aliran unduhan yang datang bertahap. Kunci pemecahannya adalah dua interface kecil: io.Reader dengan method Read(p []byte) dan io.Writer dengan method Write(p []byte), keduanya bekerja per potongan. Pada chapter ini kita merangkainya: io.Copy untuk menyalin, io.LimitReader dan io.TeeReader untuk membentuk aliran, io.Pipe untuk mengalirkan antar goroutine, dan compress/gzip sebagai rantai transformasi.
Menyalin Aliran dengan io.Copy #
io.Copy(dst, src) membaca dari src dan menulis ke dst berulang kali sampai src mengembalikan io.EOF, lalu melaporkan total byte tersalin. Fungsi ini mengurus buffer sendiri — 32 KB per putaran — sehingga pemanggil tidak perlu mengalokasikan apa pun:
n, err := io.Copy(dst, src)
if err != nil {
fmt.Println("gagal menyalin:", err)
return
}Bila src mengimplementasikan io.WriterTo atau dst mengimplementasikan io.ReaderFrom, io.Copy memakai method tersebut sebagai jalur pintas. Kendali atas ukuran potongan diperoleh lewat io.CopyBuffer(dst, src, buf) dengan buffer buatan sendiri — program utuh di bawah membuktikan bedanya: aliran 100 byte menghasilkan satu panggilan Write lewat io.Copy, tetapi tiga belas panggilan lewat CopyBuffer ber-buffer 8 byte.
Membentuk Aliran dengan Reader Pembungkus #
Kekuatan sebenarnya interface io.Reader ada pada pembungkus-pembungkusnya. io.LimitReader(r, n) membuat reader yang hanya melewatkan n byte pertama — pembacaan berikutnya langsung berakhir:
r := io.LimitReader(sumber, 1024)Pola ini adalah pagar anti-serangan: berkas raksasa atau respons jaringan tidak akan memuat lebih dari batas. io.TeeReader(r, w) bekerja lain arah: setiap potongan yang dibaca juga tersalin ke w:
r := io.TeeReader(sumber, logWriter)Pembacaan utama berjalan seperti biasa, tetapi salinan data mengalir ke log secara gratis — idiom nyaman untuk merekam lalu lintas sambil memprosesnya. Pembungkus bisa dirangkai: io.TeeReader(io.LimitReader(sumber, 4096), log) membatasi 4096 byte sekaligus mencatatnya.
Mengalirkan Antar Goroutine dengan io.Pipe #
io.Pipe() mengembalikan sepasang *io.PipeReader dan *io.PipeWriter yang tersambung di memori. Setiap Write pada sisi penulis memblokir sampai seluruh isinya dikonsumsi oleh Read di sisi pembaca — pipa ini benar-benar tanpa buffer, menjadi mekanisme sinkronisasi sekaligus jalur data. Pola baku penggunaannya:
pembaca, penulis := io.Pipe()
go func() {
defer penulis.Close()
penulis.Write(potongan)
}()
io.Copy(tujuan, pembaca)Goroutine penulis menghasilkan data sementara goroutine utama mengonsumsinya — tidak ada pihak yang menampung seluruh hasil. penulis.Close() menandai akhir aliran sehingga pembaca selesai normal; CloseWithError(err) menyebarkan kegagalan.
Rantai Transformasi: gzip #
Pembungkus yang mengubah isi aliran pun bentuknya sama. Paket compress/gzip menerima io.Writer dan menghasilkan kompresor; proses baliknya menerima io.Reader dan menghasilkan dekompresor:
zw := gzip.NewWriter(&terkompres)
zw.Write([]byte(teks))
zw.Close()
zr, _ := gzip.NewReader(&terkompres)
data, _ := io.ReadAll(zr)Kedua arah bekerja per potongan, sehingga data sebesar apa pun bisa dikompresi tanpa dimuat seluruhnya. Pola paling umum di aplikasi nyata: gzip.NewReader(berkas) mendekompresi langsung dari handle berkas, baris demi baris.
Program Utuh #
Berkas contoh.go mendemonstrasikan keenam gagasan. Tipe hitungWriter menghitung byte dan panggilan Write; tipe pembacaPolos memblokir optimasi WriteTo agar CopyBuffer terlihat jelas; io.Pipe mengalirkan tiga pesan dari goroutine penulis:
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
// hitungWriter menghitung total byte dan jumlah panggilan Write yang diterima.
type hitungWriter struct {
total int64
panggilan int
}
func (h *hitungWriter) Write(p []byte) (int, error) {
h.total += int64(len(p))
h.panggilan++
return len(p), nil
}
// pembacaPolos membungkus io.Reader tanpa method WriteTo
// sehingga io.CopyBuffer benar-benar memakai buffer yang kita sediakan.
type pembacaPolos struct {
r io.Reader
}
func (p *pembacaPolos) Read(b []byte) (int, error) {
return p.r.Read(b)
}
func main() {
// Bagian 1: io.Copy menyalin seluruh aliran dengan buffer internalnya.
sumber := strings.NewReader(strings.Repeat("ab", 50)) // 100 byte
var w1 hitungWriter
n, err := io.Copy(&w1, sumber)
if err != nil {
fmt.Println("gagal menyalin:", err)
return
}
fmt.Printf("io.Copy : %d byte, %d panggilan Write\n", n, w1.panggilan)
// Bagian 2: io.CopyBuffer memakai buffer yang kita sediakan sendiri.
var w2 hitungWriter
buf := make([]byte, 8)
if _, err := io.CopyBuffer(&w2, &pembacaPolos{strings.NewReader(strings.Repeat("ab", 50))}, buf); err != nil {
fmt.Println("gagal menyalin:", err)
return
}
fmt.Printf("CopyBuffer 8 : %d byte, %d panggilan Write\n", w2.total, w2.panggilan)
// Bagian 3: io.LimitReader hanya mengizinkan N byte pertama lolos.
terbatas := io.LimitReader(strings.NewReader("abcdefghij"), 4)
data, err := io.ReadAll(terbatas)
if err != nil {
fmt.Println("gagal membaca:", err)
return
}
fmt.Printf("LimitReader : %q (%d byte)\n", data, len(data))
// Bagian 4: io.TeeReader menyalin ke log sambil aliran tetap dibaca.
var log bytes.Buffer
asli := io.TeeReader(strings.NewReader("transaksi-001 OK"), &log)
data, err = io.ReadAll(asli)
if err != nil {
fmt.Println("gagal membaca:", err)
return
}
fmt.Printf("TeeReader : dibaca %d byte, log %d byte\n", len(data), log.Len())
// Bagian 5: io.Pipe menghubungkan goroutine penulis dan pembaca.
pembaca, penulis := io.Pipe()
go func() {
for _, pesan := range []string{"alpha\n", "beta\n", "gamma\n"} {
if _, err := penulis.Write([]byte(pesan)); err != nil {
penulis.CloseWithError(err)
return
}
}
penulis.Close()
}()
data, err = io.ReadAll(pembaca)
if err != nil {
fmt.Println("gagal membaca pipa:", err)
return
}
fmt.Printf("io.Pipe : %d byte (%q)\n", len(data), data)
// Bagian 6: gzip round-trip, kompres lalu dekompres kembali.
teks := "data yang dikompresi dan kembali utuh"
var terkompres bytes.Buffer
zw := gzip.NewWriter(&terkompres)
if _, err := zw.Write([]byte(teks)); err != nil {
fmt.Println("gagal mengompresi:", err)
return
}
if err := zw.Close(); err != nil {
fmt.Println("gagal menutup kompresor:", err)
return
}
ukuran := terkompres.Len()
zr, err := gzip.NewReader(&terkompres)
if err != nil {
fmt.Println("gagal membuka dekompresor:", err)
return
}
data, err = io.ReadAll(zr)
if err != nil {
fmt.Println("gagal mendekompresi:", err)
return
}
zr.Close()
fmt.Printf("gzip : %d byte -> %d byte -> %q\n", len(teks), ukuran, data)
}Jalankan program dari direktori chapter dengan perintah berikut:
go run contoh.goOutput yang Dihasilkan:
io.Copy : 100 byte, 1 panggilan Write
CopyBuffer 8 : 100 byte, 13 panggilan Write
LimitReader : "abcd" (4 byte)
TeeReader : dibaca 16 byte, log 16 byte
io.Pipe : 17 byte ("alpha\nbeta\ngamma\n")
gzip : 37 byte -> 62 byte -> "data yang dikompresi dan kembali utuh"Mari kita telaah baris demi baris program di atas:
- Baris 11-21 (
hitungWriter): tipe penghitung mengimplementasikanio.Writerdengan menambahlen(p)dan mencacah panggilan — alat ukur untuk membandingkanio.Copydanio.CopyBuffer. - Baris 34-42 (
io.Copy): aliran 100 byte tersalin dengan satu panggilanWritekarenastrings.Readermengimplementasikanio.WriterTo, yang menulis seluruh isinya sekali jalan. - Baris 23-31 dan 44-51 (
pembacaPolos+io.CopyBuffer): pembungkus polos menutup jalurWriteTosehinggaCopyBufferterpaksa memakai buffer 8 byte kita; hasilnya tiga belas panggilanWriteuntuk data yang sama. - Baris 53-60 (
io.LimitReader): dari sumber sepuluh byte, hanya"abcd"yang lolos — empat byte pertama sesuai batas, sisanya tidak pernah terbaca. - Baris 62-70 (
io.TeeReader): enam belas byte terbaca olehio.ReadAlldan bufferlogmenerima enam belas byte yang sama — salinan terjadi otomatis di dalam pembungkus. - Baris 72-88 (
io.Pipe): goroutine menulis tiga pesan; tiapWritemenungguReaddi sisi pembaca, lalupenulis.Close()menutup aliran sehinggaio.ReadAllmengembalikan 17 byte utuh. - Baris 90-114 (
gzipround-trip): teks 37 byte terkompresi menjadi 62 byte lalu kembali utuh saat dekompresi — bukti round-trip.
Aliran yang tidak pernah dimuat seluruhnya adalah fondasi pemrosesan data skala besar. Chapter berikutnya, Menangani File Besar
, menggabungkan streaming dan bufio.Scanner untuk memproses berkas ribuan baris dengan konsumsi memori yang tetap kecil.
Referensi #
- Dokumentasi io: Copy
— kontrak penyalinan aliran dan optimasi
WriterTo/ReaderFrom. - Dokumentasi io: Pipe — pipa memori tanpa buffer antar goroutine.
- Dokumentasi io: TeeReader — salinan otomatis ke writer saat aliran dibaca.
- Dokumentasi io: LimitReader — pembatasan jumlah byte yang terbaca.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.go
Unduh berkaspackage main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
// hitungWriter menghitung total byte dan jumlah panggilan Write yang diterima.
type hitungWriter struct {
total int64
panggilan int
}
func (h *hitungWriter) Write(p []byte) (int, error) {
h.total += int64(len(p))
h.panggilan++
return len(p), nil
}
// pembacaPolos membungkus io.Reader tanpa method WriteTo
// sehingga io.CopyBuffer benar-benar memakai buffer yang kita sediakan.
type pembacaPolos struct {
r io.Reader
}
func (p *pembacaPolos) Read(b []byte) (int, error) {
return p.r.Read(b)
}
func main() {
// Bagian 1: io.Copy menyalin seluruh aliran dengan buffer internalnya.
sumber := strings.NewReader(strings.Repeat("ab", 50)) // 100 byte
var w1 hitungWriter
n, err := io.Copy(&w1, sumber)
if err != nil {
fmt.Println("gagal menyalin:", err)
return
}
fmt.Printf("io.Copy : %d byte, %d panggilan Write\n", n, w1.panggilan)
// Bagian 2: io.CopyBuffer memakai buffer yang kita sediakan sendiri.
var w2 hitungWriter
buf := make([]byte, 8)
if _, err := io.CopyBuffer(&w2, &pembacaPolos{strings.NewReader(strings.Repeat("ab", 50))}, buf); err != nil {
fmt.Println("gagal menyalin:", err)
return
}
fmt.Printf("CopyBuffer 8 : %d byte, %d panggilan Write\n", w2.total, w2.panggilan)
// Bagian 3: io.LimitReader hanya mengizinkan N byte pertama lolos.
terbatas := io.LimitReader(strings.NewReader("abcdefghij"), 4)
data, err := io.ReadAll(terbatas)
if err != nil {
fmt.Println("gagal membaca:", err)
return
}
fmt.Printf("LimitReader : %q (%d byte)\n", data, len(data))
// Bagian 4: io.TeeReader menyalin ke log sambil aliran tetap dibaca.
var log bytes.Buffer
asli := io.TeeReader(strings.NewReader("transaksi-001 OK"), &log)
data, err = io.ReadAll(asli)
if err != nil {
fmt.Println("gagal membaca:", err)
return
}
fmt.Printf("TeeReader : dibaca %d byte, log %d byte\n", len(data), log.Len())
// Bagian 5: io.Pipe menghubungkan goroutine penulis dan pembaca.
pembaca, penulis := io.Pipe()
go func() {
for _, pesan := range []string{"alpha\n", "beta\n", "gamma\n"} {
if _, err := penulis.Write([]byte(pesan)); err != nil {
penulis.CloseWithError(err)
return
}
}
penulis.Close()
}()
data, err = io.ReadAll(pembaca)
if err != nil {
fmt.Println("gagal membaca pipa:", err)
return
}
fmt.Printf("io.Pipe : %d byte (%q)\n", len(data), data)
// Bagian 6: gzip round-trip, kompres lalu dekompres kembali.
teks := "data yang dikompresi dan kembali utuh"
var terkompres bytes.Buffer
zw := gzip.NewWriter(&terkompres)
if _, err := zw.Write([]byte(teks)); err != nil {
fmt.Println("gagal mengompresi:", err)
return
}
if err := zw.Close(); err != nil {
fmt.Println("gagal menutup kompresor:", err)
return
}
ukuran := terkompres.Len()
zr, err := gzip.NewReader(&terkompres)
if err != nil {
fmt.Println("gagal membuka dekompresor:", err)
return
}
data, err = io.ReadAll(zr)
if err != nil {
fmt.Println("gagal mendekompresi:", err)
return
}
zr.Close()
fmt.Printf("gzip : %d byte -> %d byte -> %q\n", len(teks), ukuran, data)
}