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.
Aliran data dari sumber (source) ke tujuan akhir (sink) menggunakan interface io.Reader dan io.Writer melalui sebuah buffer perantara dapat divisualisasikan dalam diagram berikut:
flowchart LR
Source["Sumber Data\n(File / Network / String)"] -->|"Mengimplementasikan"| Reader["io.Reader"]
Reader -->|"Read(p []byte)"| Buffer["Buffer Sementara\n([]byte)"]
Buffer -->|"Write(p []byte)"| Writer["io.Writer"]
Writer -->|"Mengimplementasikan"| Sink["Tujuan Akhir\n(Stdout / File / Network)"]
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.