Pustaka standar Go menata aliran data masukan dan keluaran di atas interface yang sangat kecil, yaitu io.Reader dan io.Writer. Topik ini membahas semantik kedua interface tersebut, alasan ukuran yang kecil dijadikan prinsip desain, serta pola terima interface kembalikan struct yang mengikuti jejak pustaka standar. Setelah membacanya, satu fungsi dapat digunakan untuk berkas, jaringan, maupun string tanpa mengubah kode fungsinya.
io.Reader dan io.Writer#
io.Reader mewakili sumber data yang dapat dibaca, sedangkan io.Writer mewakili tujuan data yang dapat ditulis. Keduanya menuntut satu metode saja, dan definisinya pada paket io adalah sebagai berikut.
// tidak-dijalankan
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}Metode Read mengisi buffer, yaitu irisan byte yang disiapkan pemanggil untuk menampung hasil pembacaan, lalu melaporkan banyaknya byte yang terisi beserta errornya. Akhir data tidak ditandai nilai khusus, melainkan error io.EOF (end of file, tanda habisnya data) yang dikembalikan ketika tidak ada byte lagi yang dapat dibaca. Metode Write menerima irisan byte dan melaporkan berapa banyak yang tertulis. Kesederhanaan ini membuat kedua kontrak dapat dipenuhi tipe yang sangat beragam, mulai dari strings.Reader, bytes.Buffer, sampai os.File.
Tipe buatan sendiri memenuhi io.Reader dengan cara yang sama. Program berikut membungkus strings.Reader dan mengubah huruf kecil menjadi huruf besar ketika data mengalir.
package main
import (
"fmt"
"io"
"strings"
)
// PembacaBesar adalah io.Reader yang mengubah huruf kecil menjadi huruf besar.
type PembacaBesar struct {
sumber *strings.Reader
}
// PembacaBesarBaru menerima teks biasa dan mengembalikan struct siap baca.
func PembacaBesarBaru(teks string) *PembacaBesar {
return &PembacaBesar{sumber: strings.NewReader(teks)}
}
// Read memenuhi io.Reader: isi buffer diubah lebih dahulu, lalu hasil
// pembacaan sumber diteruskan apa adanya.
func (p *PembacaBesar) Read(buffer []byte) (int, error) {
n, err := p.sumber.Read(buffer)
for i := 0; i < n; i++ {
if buffer[i] >= 'a' && buffer[i] <= 'z' {
buffer[i] -= 'a' - 'A'
}
}
return n, err
}
func main() {
var pembaca io.Reader = PembacaBesarBaru("log sensor: suhu naik")
isi, err := io.ReadAll(pembaca)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(string(isi))
}LOG SENSOR: SUHU NAIKMetode Read pada PembacaBesar mengubah isi buffer lebih dahulu, lalu meneruskan hasil pembacaan sumber apa adanya. Pembungkusan semacam ini menyambungkan transformasi ke aliran data tanpa mengubah tipe sumbernya, sehingga seluruh fungsi yang menerima io.Reader otomatis dapat membaca hasilnya. Program tersebut dapat dijalankan lewat terminal sebagaimana berikut.
# siapkan folder kerja untuk topik ini
mkdir -p /tmp/gointerface/interface-kecil
cp pembaca-besar.go /tmp/gointerface/interface-kecil/
cd /tmp/gointerface/interface-kecil
# periksa format sebelum menjalankan programnya
gofmt -l .
# jalankan programnya
go run pembaca-besar.goLOG SENSOR: SUHU NAIKKeluarannya sama dengan blok sebelumnya karena keduanya menjalankan program yang sama, sedangkan gofmt -l tidak mencetak apa pun ketika format kode sudah sesuai.
Mengapa interface dibuat kecil#
Interface dengan satu metode mudah dipenuhi, sehingga semakin banyak tipe yang dapat mengalir melaluinya. Fungsi yang ditulis terhadap io.Reader dan io.Writer tidak perlu mengetahui sumber dan tujuan sebenarnya. Fungsi io.Copy pada pustaka standar adalah contohnya, yaitu menyalin seluruh data dari sembarang pembaca ke sembarang penulis. Program berikut memakainya untuk menyalin teks ke strings.Builder, yaitu tipe yang mengumpulkan string secara efisien.
package main
import (
"fmt"
"io"
"strings"
)
func main() {
sumber := strings.NewReader("draf dokumen: laporan bulanan")
var tujuan strings.Builder
jumlah, err := io.Copy(&tujuan, sumber)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%d byte tersalin\n", jumlah)
fmt.Println(tujuan.String())
}29 byte tersalin
draf dokumen: laporan bulananBaris io.Copy(&tujuan, sumber) tidak menyebut tipe apa pun, karena yang tertulis hanya nama variabel dan yang dipersyaratkan hanya kesesuaian dengan kontrak. Apabila sumbernya kelak menjadi os.File atau respons HTTP, baris tersebut tidak berubah sama sekali. Di sinilah keuntungan interface kecil terasa, yaitu satu fungsi melayani semua tipe yang memenuhi kontraknya tanpa duplikasi kode.
Terima interface, kembalikan struct#
Pola yang lazim pada pustaka standar dapat diringkas menjadi idiom accept interfaces, return structs, yakni menerima interface pada parameter dan mengembalikan struct konkret pada nilai kembalian. Parameter bertipe interface memberi keleluasaan kepada pemanggil, sedangkan struct yang dikembalikan mempertahankan seluruh field dan metode konkretnya sehingga informasi tipenya tidak hilang. Program berikut menerapkan pola itu pada penghitung kata.
package main
import (
"fmt"
"io"
"strings"
)
// Rekap menyimpan hasil penghitungan satu dokumen teks.
type Rekap struct {
JumlahKata int
}
// HitungKata menerima io.Reader apa pun dan mengembalikan struct Rekap.
func HitungKata(pembaca io.Reader) (Rekap, error) {
isi, err := io.ReadAll(pembaca)
if err != nil {
return Rekap{}, err
}
return Rekap{JumlahKata: len(strings.Fields(string(isi)))}, nil
}
func main() {
dokumen := "alur dokumen teks: draf disunting, ditinjau, lalu disetujui"
rekap, err := HitungKata(strings.NewReader(dokumen))
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("jumlah kata:", rekap.JumlahKata)
}jumlah kata: 8Fungsi HitungKata menerima io.Reader, sehingga dokumennya dapat berasal dari string, berkas, maupun jaringan. Nilai kembaliannya bertipe Rekap, sehingga field JumlahKata dapat diakses langsung tanpa pemeriksaan tipe. Bentuk sebaliknya justru keliru, yaitu menerima struct konkret padahal yang diperlukan hanya Read, atau mengembalikan interface sehingga field konkret tersembunyi dari pemanggil.
// tidak-dijalankan
func PembacaBaru(teks string) io.Reader {
return &PembacaBesar{sumber: strings.NewReader(teks)}
}Konstruktor pada contoh di atas menyembunyikan tipe PembacaBesar di balik io.Reader, sehingga pemanggil yang membutuhkan metode konkretnya terpaksa melakukan pemeriksaan tipe terlebih dahulu. Dengan mengembalikan *PembacaBesar, pemanggil tetap bebas menyimpannya sebagai io.Reader kapan pun dibutuhkan, karena pointer tersebut memenuhi interface itu. Kedua arah pemakaian pun terbuka tanpa kehilangan apa pun.
Kesalahan umum#
Tiga kesalahan berikut kerap muncul ketika aliran data mulai dibungkus interface.
- Menganggap satu panggilan
Readmengisi seluruh buffer. Bila buffer berukuran 64 byte dan data berukuran 200 byte, panggilan pertama dapat mengembalikan sepuluh byte saja, sehingga data yang diproses terpotong tanpa pesan error. Perbaikannya, gunakanio.ReadAllatau perulangan yang berhenti saat bertemuio.EOF. - Menerima struct konkret pada fungsi yang hanya membutuhkan
ReadatauWrite. Fungsi seperti itu tidak dapat diuji denganbytes.Buffermaupunstrings.Reader, sehingga pengujian bergantung pada berkas nyata. Perbaikannya, perkecil parameter menjadi interface yang sesuai kebutuhannya. - Mengembalikan interface dari konstruktor. Field dan metode konkret hilang dari pandangan pemanggil, sehingga pemeriksaan tipe pun bermunculan. Perbaikannya, kembalikan struct konkret dan biarkan pemanggil menyimpannya sebagai interface bila diperlukan.
Ringkasan#
io.Readerdanio.Writermasing-masing menuntut satu metode, denganio.EOFsebagai penanda habisnya data pada pembacaan.- Interface kecil dipenuhi banyak tipe, sehingga fungsi yang ditulis terhadapnya dapat dipakai ulang untuk string, buffer, berkas, dan jaringan.
- Pola terima interface kembalikan struct memberi keleluasaan pada masukan dan mempertahankan informasi tipe pada keluaran.
io.Copydanio.ReadAllmenangani perulangan pembacaan, sehingga kode pemanggil tidak mengelola buffer secara manual.
Referensi#
- Package io — The Go Authors
— Dokumentasi resmi
io.Reader,io.Writer,io.Copy, danio.ReadAll. - Effective Go: Interfaces and types — Go Documentation — Prinsip desain interface pada Go.
- A Tour of Go: Methods and interfaces — Go Documentation
— Latihan resmi yang memuat implementasi
io.Readerdanio.Writer.