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
    }
}
// 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