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:

go
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:

go
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:

go
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:

go
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:

go
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:

go
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:

bash
go run contoh.go

Output yang Dihasilkan:

text
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:

  1. Baris 11-21 (hitungWriter): tipe penghitung mengimplementasikan io.Writer dengan menambah len(p) dan mencacah panggilan — alat ukur untuk membandingkan io.Copy dan io.CopyBuffer.
  2. Baris 34-42 (io.Copy): aliran 100 byte tersalin dengan satu panggilan Write karena strings.Reader mengimplementasikan io.WriterTo, yang menulis seluruh isinya sekali jalan.
  3. Baris 23-31 dan 44-51 (pembacaPolos + io.CopyBuffer): pembungkus polos menutup jalur WriteTo sehingga CopyBuffer terpaksa memakai buffer 8 byte kita; hasilnya tiga belas panggilan Write untuk data yang sama.
  4. Baris 53-60 (io.LimitReader): dari sumber sepuluh byte, hanya "abcd" yang lolos — empat byte pertama sesuai batas, sisanya tidak pernah terbaca.
  5. Baris 62-70 (io.TeeReader): enam belas byte terbaca oleh io.ReadAll dan buffer log menerima enam belas byte yang sama — salinan terjadi otomatis di dalam pembungkus.
  6. Baris 72-88 (io.Pipe): goroutine menulis tiga pesan; tiap Write menunggu Read di sisi pembaca, lalu penulis.Close() menutup aliran sehingga io.ReadAll mengembalikan 17 byte utuh.
  7. Baris 90-114 (gzip round-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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.go

Unduh berkas
go
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)
}