I/O #
Salah satu desain paling elegan di Go adalah cara seluruh ekosistem I/O dibangun di atas dua interface yang sangat sederhana: io.Reader dan io.Writer. File, HTTP request body, network connection, string, byte buffer, gzip stream, database row — semuanya bisa diperlakukan sama karena semuanya mengimplementasikan interface yang sama. Ini membuat kode I/O di Go sangat composable: kamu bisa membungkus reader dengan reader lain untuk menambahkan buffering, hashing, atau decompression tanpa mengubah kode yang menggunakan reader tersebut.
io.Reader dan io.Writer — Fondasi Segalanya
#
// Dua interface yang menjadi dasar seluruh I/O di Go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Read membaca sampai len(p) byte ke p dan mengembalikan jumlah byte yang dibaca. Saat tidak ada lagi data, ia mengembalikan io.EOF. Write menulis len(p) byte dari p dan mengembalikan jumlah byte yang berhasil ditulis.
Apa saja yang mengimplementasikan io.Reader:
*os.File, net.Conn, *http.Request.Body, *bytes.Buffer,
*bytes.Reader, *strings.Reader, *bufio.Reader,
*gzip.Reader, *zip.Reader, io.LimitedReader, ...
Dan io.Writer:
*os.File, net.Conn, http.ResponseWriter, *bytes.Buffer,
*bufio.Writer, *gzip.Writer, io.MultiWriter, os.Stdout, ...
Package os — Operasi File
#
Membaca File #
import "os"
// Cara modern (Go 1.16+) — paling ringkas untuk file kecil
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("gagal membaca config: %w", err)
}
fmt.Println(string(data))
// Cara manual — lebih kontrol, untuk file besar atau streaming
f, err := os.Open("data.txt") // read-only
if err != nil {
return err
}
defer f.Close()
buf := make([]byte, 4096)
for {
n, err := f.Read(buf)
if n > 0 {
process(buf[:n])
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
Menulis File #
// Cara modern — tulis sekaligus, paling ringkas
err := os.WriteFile("output.txt", []byte("isi file\n"), 0644)
// Cara manual dengan os.OpenFile — lebih kontrol
f, err := os.OpenFile("output.txt",
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, // flag
0644) // permission
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString("Baris pertama\n")
_, err = fmt.Fprintf(f, "Baris %d\n", 2)
// Append ke file yang sudah ada
f2, err := os.OpenFile("log.txt",
os.O_WRONLY|os.O_CREATE|os.O_APPEND,
0644)
Flag os.OpenFile
#
os.O_RDONLY — baca saja (default os.Open)
os.O_WRONLY — tulis saja
os.O_RDWR — baca dan tulis
os.O_APPEND — tambah di akhir file
os.O_CREATE — buat file jika belum ada
os.O_TRUNC — kosongkan file jika sudah ada
os.O_EXCL — error jika file sudah ada (atomic create)
os.O_SYNC — tulis langsung ke disk (tanpa OS cache)
Informasi dan Manajemen File #
// Cek apakah file ada
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
fmt.Println("File tidak ditemukan")
}
// Info file
info, err := os.Stat("data.txt")
if err == nil {
fmt.Println("Nama :", info.Name())
fmt.Println("Ukuran:", info.Size(), "bytes")
fmt.Println("Diubah:", info.ModTime())
fmt.Println("IsDir :", info.IsDir())
}
// Operasi file/direktori
os.Remove("temp.txt")
os.Rename("old.txt", "new.txt")
os.MkdirAll("path/to/dir", 0755) // buat direktori rekursif
os.RemoveAll("direktori/") // hapus direktori dan isinya
// List direktori
entries, err := os.ReadDir(".")
for _, entry := range entries {
fmt.Printf("%-30s %v\n", entry.Name(), entry.IsDir())
}
Package io — Utility Functions
#
Package io menyediakan fungsi-fungsi yang bekerja dengan Reader dan Writer secara generik.
io.Copy — Salin Data Antar Stream
#
// Salin semua data dari src ke dst
n, err := io.Copy(dst, src)
fmt.Printf("Disalin %d byte\n", n)
// Contoh nyata: salin file
src, _ := os.Open("source.txt")
defer src.Close()
dst, _ := os.Create("dest.txt")
defer dst.Close()
io.Copy(dst, src)
// Salin HTTP response body ke file
resp, _ := http.Get("https://example.com/file.zip")
defer resp.Body.Close()
f, _ := os.Create("file.zip")
defer f.Close()
io.Copy(f, resp.Body)
io.ReadAll — Baca Semua Data
#
// Baca seluruh isi reader ke memory
data, err := io.ReadAll(resp.Body)
// Hati-hati untuk stream yang sangat besar!
// Gunakan io.LimitReader untuk membatasi
limited := io.LimitReader(resp.Body, 10*1024*1024) // max 10MB
data, err := io.ReadAll(limited)
io.TeeReader — Baca sambil Tulis
#
TeeReader membaca dari r dan setiap byte yang dibaca juga ditulis ke w secara bersamaan. Berguna untuk hashing sambil membaca:
import (
"crypto/sha256"
"encoding/hex"
)
// Hitung SHA-256 dari file sambil membacanya (satu pass)
f, _ := os.Open("data.bin")
defer f.Close()
hasher := sha256.New()
tee := io.TeeReader(f, hasher) // semua yang dibaca dari tee juga ditulis ke hasher
dst, _ := os.Create("copy.bin")
defer dst.Close()
io.Copy(dst, tee) // baca dari tee = baca dari f + tulis ke hasher
hash := hex.EncodeToString(hasher.Sum(nil))
fmt.Println("SHA-256:", hash)
io.MultiReader — Gabungkan Beberapa Reader
#
// Baca dari beberapa sumber seolah-olah satu stream
r1 := strings.NewReader("header\n")
r2 := strings.NewReader("body content\n")
r3 := strings.NewReader("footer\n")
combined := io.MultiReader(r1, r2, r3)
io.Copy(os.Stdout, combined)
// Output:
// header
// body content
// footer
io.MultiWriter — Tulis ke Beberapa Tujuan
#
// Tulis ke file dan stdout sekaligus
f, _ := os.Create("log.txt")
defer f.Close()
mw := io.MultiWriter(os.Stdout, f)
fmt.Fprintln(mw, "Pesan ini muncul di terminal DAN disimpan ke file")
io.LimitReader — Batasi Jumlah Byte yang Dibaca
#
// Cegah membaca lebih dari N byte (keamanan upload)
const maxBodySize = 1 << 20 // 1 MB
http.MaxBytesReader(w, r.Body, maxBodySize)
// Atau manual
limited := io.LimitReader(source, maxBodySize)
data, _ := io.ReadAll(limited)
Package bufio — Buffered I/O
#
Membaca atau menulis satu byte atau satu baris pada satu waktu langsung ke os.File sangat tidak efisien karena setiap Read/Write adalah system call. bufio menambahkan buffer di atas reader/writer untuk mengurangi jumlah system call.
bufio.Reader
#
f, _ := os.Open("data.txt")
defer f.Close()
reader := bufio.NewReader(f) // buffer default 4096 byte
// atau
reader = bufio.NewReaderSize(f, 65536) // buffer 64KB
// Baca per baris
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
fmt.Print(line)
}
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
}
// Peek — lihat byte tanpa mengkonsumsinya
peeked, _ := reader.Peek(5)
fmt.Println(string(peeked)) // 5 byte pertama tanpa memajukan posisi
bufio.Scanner — Cara Idiomatik Baca Per Baris
#
Scanner lebih idiomatik dari ReadString('\n') untuk baca per baris:
f, _ := os.Open("data.txt")
defer f.Close()
scanner := bufio.NewScanner(f)
// Default: split per baris
for scanner.Scan() {
line := scanner.Text() // baris tanpa \n
fmt.Println(line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
// Baca per kata
scanner2 := bufio.NewScanner(strings.NewReader("hello world foo"))
scanner2.Split(bufio.ScanWords)
for scanner2.Scan() {
fmt.Println(scanner2.Text()) // hello, world, foo
}
// Baca per byte
scanner3 := bufio.NewScanner(r)
scanner3.Split(bufio.ScanBytes)
// Buffer kustom untuk baris panjang
scanner4 := bufio.NewScanner(f)
buf := make([]byte, 1024*1024) // 1MB buffer
scanner4.Buffer(buf, len(buf)) // atur max size
bufio.Writer
#
f, _ := os.Create("output.txt")
defer f.Close()
writer := bufio.NewWriter(f)
writer.WriteString("Baris pertama\n")
fmt.Fprintf(writer, "Baris %d\n", 2)
writer.WriteByte('\n')
// PENTING: Flush buffer ke disk sebelum tutup file
if err := writer.Flush(); err != nil {
log.Fatal(err)
}
// Data di buffer akan hilang jika Flush tidak dipanggil!
Selalu panggilFlush()padabufio.Writersebelum file ditutup. Data yang belum di-flush masih ada di buffer memori dan belum ditulis ke disk. Gunakandefer writer.Flush()segera setelah membuat writer, sebelumdefer f.Close().
bytes.Buffer dan strings.Builder — In-Memory I/O
#
Ketika butuh reader/writer di memori (bukan file atau network):
import "bytes"
// bytes.Buffer — bisa dipakai sebagai Reader dan Writer
var buf bytes.Buffer
buf.WriteString("Hello")
buf.WriteString(", ")
fmt.Fprintf(&buf, "World %d!", 42)
fmt.Println(buf.String()) // "Hello, World 42!"
fmt.Println(buf.Len()) // panjang dalam byte
// Baca kembali dari buffer
data := make([]byte, 5)
buf.Read(data) // membaca 5 byte pertama
// Reset untuk dipakai ulang
buf.Reset()
// bytes.NewReader — reader dari byte slice (immutable)
reader := bytes.NewReader([]byte("Hello, Go!"))
io.Copy(os.Stdout, reader)
// strings.NewReader — reader dari string
reader2 := strings.NewReader("Hello from string!")
io.Copy(os.Stdout, reader2)
// strings.Builder — tulis string efisien
var sb strings.Builder
sb.WriteString("bagian 1")
sb.WriteString(" dan ")
sb.WriteString("bagian 2")
result := sb.String() // tidak ada alokasi string saat += setiap kali
io.Pipe — In-Process Pipe
#
io.Pipe membuat pasangan PipeReader dan PipeWriter yang terhubung — data yang ditulis ke writer langsung tersedia di reader, seperti Unix pipe:
pr, pw := io.Pipe()
// Writer — jalankan di goroutine
go func() {
defer pw.Close() // sinyal EOF ke reader
for i := 0; i < 5; i++ {
fmt.Fprintf(pw, "baris %d\n", i+1)
time.Sleep(100 * time.Millisecond)
}
}()
// Reader — di goroutine utama
scanner := bufio.NewScanner(pr)
for scanner.Scan() {
fmt.Println("Diterima:", scanner.Text())
}
// Berguna untuk: encode data sambil mengupload
// tanpa menyimpan semua data di memori
pr2, pw2 := io.Pipe()
go func() {
defer pw2.Close()
encoder := json.NewEncoder(pw2)
encoder.Encode(largeData) // encode langsung ke pipe
}()
http.Post(url, "application/json", pr2) // upload dari pipe
stdin, stdout, stderr #
// os.Stdin, os.Stdout, os.Stderr adalah *os.File
// keduanya mengimplementasikan io.Reader dan io.Writer
// Baca dari stdin
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fmt.Println("Kamu mengetik:", scanner.Text())
}
// Baca satu baris dari stdin
reader := bufio.NewReader(os.Stdin)
fmt.Print("Masukkan nama: ")
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name)
// Tulis ke stderr (untuk error dan log)
fmt.Fprintln(os.Stderr, "Error: sesuatu salah")
// Redirect stdout ke file
f, _ := os.Create("output.txt")
oldStdout := os.Stdout
os.Stdout = f
fmt.Println("Ini ke file!") // ke file, bukan terminal
os.Stdout = oldStdout // kembalikan ke terminal
f.Close()
Temporary File dan Directory #
// Buat file sementara — otomatis dapat nama unik
tmpFile, err := os.CreateTemp("", "prefix-*.txt")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpFile.Name()) // hapus setelah selesai
defer tmpFile.Close()
fmt.Println("File temp:", tmpFile.Name()) // /tmp/prefix-123456789.txt
tmpFile.WriteString("data sementara")
// Buat direktori sementara
tmpDir, err := os.MkdirTemp("", "myapp-*")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(tmpDir)
fmt.Println("Dir temp:", tmpDir) // /tmp/myapp-123456789
fs.FS — Abstraksi Filesystem (Go 1.16+)
#
fs.FS adalah interface untuk filesystem yang memungkinkan kode bekerja dengan filesystem nyata, embedded filesystem, atau filesystem di memori secara seragam:
import (
"embed"
"io/fs"
)
//go:embed static/*
var staticFiles embed.FS
// Fungsi yang menerima fs.FS — bekerja dengan filesystem apapun
func processFiles(fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
f, err := fsys.Open(path)
if err != nil {
return err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return err
}
fmt.Printf("%s: %d bytes\n", path, len(data))
return nil
})
}
func main() {
// Pakai dengan embedded files
processFiles(staticFiles)
// Pakai dengan filesystem nyata
processFiles(os.DirFS("."))
// Pakai dengan sub-direktori
subFS, _ := fs.Sub(staticFiles, "static")
processFiles(subFS)
}
Contoh Program Lengkap #
Program berikut membangun log processor pipeline yang membaca log dari file, memfilter, mentransformasi, dan menulis ke output:
package main
import (
"bufio"
"compress/gzip"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"os"
"strings"
"time"
)
type LogEntry struct {
Timestamp string
Level string
Message string
Raw string
}
func parseLog(line string) (LogEntry, bool) {
// Format: 2024-07-28 15:30:45 [INFO] message here
parts := strings.SplitN(line, " ", 4)
if len(parts) < 4 {
return LogEntry{}, false
}
level := strings.Trim(parts[2], "[]")
return LogEntry{
Timestamp: parts[0] + " " + parts[1],
Level: level,
Message: parts[3],
Raw: line,
}, true
}
// processLogs membaca log, filter, dan tulis ke output
func processLogs(
input io.Reader,
output io.Writer,
minLevel string,
stats *struct{ total, filtered, written int },
) error {
// MultiWriter: tulis ke output dan hitung MD5 sekaligus
hasher := md5.New()
mw := io.MultiWriter(output, hasher)
writer := bufio.NewWriter(mw)
defer writer.Flush()
levelPriority := map[string]int{
"DEBUG": 0, "INFO": 1, "WARN": 2, "ERROR": 3, "FATAL": 4,
}
minPriority := levelPriority[minLevel]
scanner := bufio.NewScanner(input)
// Set buffer besar untuk baris panjang
buf := make([]byte, 256*1024)
scanner.Buffer(buf, len(buf))
for scanner.Scan() {
line := scanner.Text()
stats.total++
entry, ok := parseLog(line)
if !ok {
continue
}
// Filter berdasarkan level
if levelPriority[entry.Level] < minPriority {
stats.filtered++
continue
}
// Transformasi: tambah prefix dan format ulang
formatted := fmt.Sprintf("[%s] %-5s | %s\n",
entry.Timestamp, entry.Level, entry.Message)
fmt.Fprint(writer, formatted)
stats.written++
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error membaca log: %w", err)
}
// Flush sebelum hitung hash final
writer.Flush()
fmt.Fprintf(os.Stderr, "Checksum output: %s\n",
hex.EncodeToString(hasher.Sum(nil)))
return nil
}
func main() {
// Buat log sample di memory
sampleLogs := `2024-07-28 08:00:01 [DEBUG] Memulai aplikasi
2024-07-28 08:00:02 [INFO] Server berjalan di port 8080
2024-07-28 08:01:15 [DEBUG] Request masuk: GET /health
2024-07-28 08:01:15 [INFO] Health check: OK
2024-07-28 08:05:30 [WARN] Memory usage 75%
2024-07-28 08:10:45 [ERROR] Database connection timeout
2024-07-28 08:10:46 [INFO] Mencoba reconnect ke database
2024-07-28 08:10:47 [INFO] Reconnect berhasil
2024-07-28 08:15:00 [DEBUG] Garbage collection selesai
2024-07-28 08:20:00 [FATAL] Disk penuh, tidak bisa menulis log`
stats := &struct{ total, filtered, written int }{}
fmt.Println("=== Log Processor Pipeline ===")
fmt.Println()
// Demo 1: Filter ke stdout (hanya WARN ke atas)
fmt.Println("--- Log Level WARN ke atas ---")
reader1 := strings.NewReader(sampleLogs)
if err := processLogs(reader1, os.Stdout, "WARN", stats); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
}
fmt.Printf("\nStatistik: total=%d, difilter=%d, ditulis=%d\n\n",
stats.total, stats.filtered, stats.written)
// Demo 2: Tulis ke file biasa
stats2 := &struct{ total, filtered, written int }{}
outFile, _ := os.CreateTemp("", "processed-*.log")
defer os.Remove(outFile.Name())
defer outFile.Close()
fmt.Printf("--- Menulis ke file: %s ---\n", outFile.Name())
reader2 := strings.NewReader(sampleLogs)
processLogs(reader2, outFile, "INFO", stats2)
fmt.Printf("Ditulis %d entri ke file\n\n", stats2.written)
// Demo 3: Tulis ke gzip file menggunakan io.Writer composition
fmt.Println("--- Menulis ke gzip file ---")
gzFile, _ := os.CreateTemp("", "compressed-*.log.gz")
defer os.Remove(gzFile.Name())
defer gzFile.Close()
stats3 := &struct{ total, filtered, written int }{}
gzWriter := gzip.NewWriter(gzFile)
defer gzWriter.Close()
reader3 := strings.NewReader(sampleLogs)
processLogs(reader3, gzWriter, "DEBUG", stats3) // kirim ke gzip writer!
gzWriter.Close()
gzInfo, _ := os.Stat(gzFile.Name())
fmt.Printf("Semua %d entri dikompresi ke: %s (%d bytes)\n",
stats3.written, gzFile.Name(), gzInfo.Size())
// Demo 4: TeeReader — baca sambil duplikasi
fmt.Println("\n--- TeeReader: baca sambil duplikasi ---")
var duplicate strings.Builder
original := strings.NewReader("data penting yang perlu diduplikasi\n")
tee := io.TeeReader(original, &duplicate)
// Baca dari tee (data juga masuk ke duplicate)
mainData, _ := io.ReadAll(tee)
fmt.Printf("Data utama : %q\n", string(mainData))
fmt.Printf("Duplikat : %q\n", duplicate.String())
// Demo 5: io.Pipe — stream tanpa buffer
fmt.Println("\n--- io.Pipe: in-process stream ---")
pr, pw := io.Pipe()
done := make(chan struct{})
go func() {
defer pw.Close()
defer close(done)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for i := 0; i < 3; i++ {
<-ticker.C
fmt.Fprintf(pw, "stream event %d\n", i+1)
}
}()
scanner := bufio.NewScanner(pr)
for scanner.Scan() {
fmt.Println("Diterima dari pipe:", scanner.Text())
}
<-done
}
Ringkasan #
io.Readerdanio.Writeradalah fondasi semua I/O di Go — apapun yang bisa dibaca atau ditulis mengimplementasikan keduanya.os.ReadFile/os.WriteFileuntuk file kecil yang dibaca/ditulis sekaligus;os.Open/os.Createuntuk kontrol lebih atau file besar.bufio.Scanneradalah cara idiomatik membaca per baris; lebih bersih dariReadString('\n').bufio.Writer— selalu panggilFlush()sebelum file ditutup; gunakandefer writer.Flush().io.Copyuntuk menyalin data antar stream tanpa muat seluruhnya ke memori.io.TeeReaderuntuk membaca sambil menduplikasi ke writer lain (misal: hashing sambil mengunduh).io.MultiWriteruntuk menulis ke beberapa tujuan sekaligus (misal: file + stdout).io.LimitReaderuntuk membatasi jumlah byte yang dibaca — penting untuk keamanan upload.io.Pipeuntuk menyambung writer dan reader di goroutine berbeda tanpa buffer memory.fs.FS(Go 1.16+) untuk abstraksi filesystem — kode bisa bekerja dengan file nyata, embedded, atau memory.