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
    }
}
// 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 panggil Flush() pada bufio.Writer sebelum file ditutup. Data yang belum di-flush masih ada di buffer memori dan belum ditulis ke disk. Gunakan defer writer.Flush() segera setelah membuat writer, sebelum defer 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.Reader dan io.Writer adalah fondasi semua I/O di Go — apapun yang bisa dibaca atau ditulis mengimplementasikan keduanya.
  • os.ReadFile / os.WriteFile untuk file kecil yang dibaca/ditulis sekaligus; os.Open / os.Create untuk kontrol lebih atau file besar.
  • bufio.Scanner adalah cara idiomatik membaca per baris; lebih bersih dari ReadString('\n').
  • bufio.Writer — selalu panggil Flush() sebelum file ditutup; gunakan defer writer.Flush().
  • io.Copy untuk menyalin data antar stream tanpa muat seluruhnya ke memori.
  • io.TeeReader untuk membaca sambil menduplikasi ke writer lain (misal: hashing sambil mengunduh).
  • io.MultiWriter untuk menulis ke beberapa tujuan sekaligus (misal: file + stdout).
  • io.LimitReader untuk membatasi jumlah byte yang dibaca — penting untuk keamanan upload.
  • io.Pipe untuk 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.

← Sebelumnya: Goroutine   Berikutnya: Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact