IO #

Hampir setiap program yang bermakna perlu berinteraksi dengan dunia luar — membaca file konfigurasi, menulis log, menerima data dari jaringan, atau memproses input pengguna. Go merancang sistem I/O-nya di atas dua interface yang sangat sederhana: io.Reader dan io.Writer. Dari dua interface inilah seluruh ekosistem I/O Go dibangun — file, koneksi jaringan, buffer di memori, kompressor, enkripsi, semuanya saling kompatibel karena semua mengimplementasikan interface yang sama. Artikel ini membahas cara kerja sistem I/O Go dari dasar, bagaimana memanfaatkan package io, bufio, dan os secara efektif, serta pola-pola yang umum digunakan di aplikasi produksi.

Dua Interface Inti: Reader dan Writer #

Sebelum masuk ke fungsi dan struct yang lebih spesifik, penting untuk memahami dua interface yang menjadi fondasi seluruh I/O di Go. Keduanya didefinisikan di package io dan sangat minimalis — masing-masing hanya punya satu method.

// io.Reader — sumber data yang bisa dibaca
type Reader interface {
    Read(p []byte) []byte (n int, err error)
}

// io.Writer — tujuan data yang bisa ditulis
type Writer interface {
    Write(p []byte) (n int, err error)
}

Read mengisi slice p dengan data, mengembalikan jumlah byte yang berhasil dibaca dan error jika ada. Ketika sumber data habis, Read mengembalikan io.EOF. Write menulis isi slice p ke tujuan, mengembalikan jumlah byte yang berhasil ditulis.

flowchart LR
    A[Sumber Data\nos.File / net.Conn\nstrings.Reader\nbytes.Buffer] -- "Read(p []byte)" --> B[io.Reader]
    B -- "data mengalir" --> C[Kode kamu]
    C -- "Write(p []byte)" --> D[io.Writer]
    D -- "Write(p []byte)" --> E[Tujuan Data\nos.File / net.Conn\nbytes.Buffer / os.Stdout]

Kekuatan desain ini adalah komposabilitas. Sebuah fungsi yang menerima io.Reader bisa menerima file, koneksi jaringan, string di memori, atau output dari proses lain — tanpa perlu tahu sumbernya. Ini adalah prinsip yang membuat kode Go sangat mudah di-test dan dikomposisikan.

// Fungsi ini bekerja dengan SEMUA sumber data
func hitungBaris(r io.Reader) (int, error) {
    buf := make([]byte, 32*1024)
    count := 0
    lineSep := []byte{'\n'}

    for {
        c, err := r.Read(buf)
        count += bytes.Count(buf[:c], lineSep)
        if err == io.EOF {
            break
        }
        if err != nil {
            return count, err
        }
    }
    return count, nil
}

// Bisa dipanggil dengan file...
f, _ := os.Open("data.txt")
defer f.Close()
n, _ := hitungBaris(f)

// ...atau string di memori (berguna untuk testing)
n, _ = hitungBaris(strings.NewReader("baris 1\nbaris 2\nbaris 3\n"))

Package io — Fungsi Utilitas Dasar #

Package io tidak hanya mendefinisikan interface — ia juga menyediakan fungsi-fungsi utilitas penting untuk bekerja dengan reader dan writer.

io.Copy — Mengalirkan Data Antar Stream #

io.Copy adalah fungsi yang paling sering digunakan: ia membaca dari src (Reader) dan menulis ke dst (Writer) sampai EOF, mengembalikan jumlah byte yang dipindahkan.

import (
    "io"
    "os"
)

// Salin file
src, err := os.Open("sumber.txt")
if err != nil {
    log.Fatal(err)
}
defer src.Close()

dst, err := os.Create("tujuan.txt")
if err != nil {
    log.Fatal(err)
}
defer dst.Close()

n, err := io.Copy(dst, src)
fmt.Printf("Berhasil menyalin %d byte\n", n)

io.Copy secara internal menggunakan buffer 32KB — efisien untuk file besar karena tidak membaca seluruh file ke memori sekaligus. Ini adalah perbedaan penting dibanding os.ReadFile yang membaca seluruh isi file.

sequenceDiagram
    participant Copy as io.Copy
    participant Src as src (Reader)
    participant Buf as Buffer (32KB)
    participant Dst as dst (Writer)

    loop sampai EOF
        Copy->>Src: Read(buf)
        Src-->>Buf: n byte data
        Copy->>Dst: Write(buf[:n])
        Dst-->>Copy: n byte ditulis
    end
    Copy-->>Copy: return total, nil

io.ReadAll — Baca Seluruh Konten ke Memori #

io.ReadAll membaca semua data dari reader hingga EOF dan mengembalikannya sebagai []byte. Gunakan ini hanya jika kamu memang perlu seluruh konten di memori sekaligus.

import (
    "io"
    "strings"
)

r := strings.NewReader("Halo dari Go!")
data, err := io.ReadAll(r)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data)) // "Halo dari Go!"
Jangan gunakan io.ReadAll untuk membaca file atau response HTTP yang ukurannya tidak terduga. Jika kontennya besar (ratusan MB), seluruhnya akan dimuat ke RAM. Gunakan io.Copy dengan tujuan yang tepat, atau proses data secara streaming menggunakan buffer.

io.ReadFull — Baca Tepat N Byte #

io.ReadFull membaca tepat len(buf) byte dari reader. Jika data tidak cukup, ia mengembalikan error io.ErrUnexpectedEOF. Berguna untuk membaca data dengan format biner yang sudah diketahui ukurannya.

// Baca header 8 byte dari protokol biner
header := make([]byte, 8)
n, err := io.ReadFull(r, header)
if err == io.ErrUnexpectedEOF {
    fmt.Printf("Data terpotong, hanya ada %d byte\n", n)
} else if err != nil {
    log.Fatal(err)
}
// header sekarang pasti berisi tepat 8 byte

io.MultiReader dan io.MultiWriter #

io.MultiReader menggabungkan beberapa reader menjadi satu — data dibaca dari reader pertama sampai EOF, lalu lanjut ke reader berikutnya. io.MultiWriter meneruskan setiap penulisan ke semua writer secara bersamaan (seperti tee di Unix).

// MultiReader — gabungkan beberapa sumber
r1 := strings.NewReader("Bagian pertama. ")
r2 := strings.NewReader("Bagian kedua. ")
r3 := strings.NewReader("Bagian ketiga.")

combined := io.MultiReader(r1, r2, r3)
data, _ := io.ReadAll(combined)
fmt.Println(string(data))
// "Bagian pertama. Bagian kedua. Bagian ketiga."

// MultiWriter — tulis ke banyak tujuan sekaligus
var buf bytes.Buffer
multi := io.MultiWriter(os.Stdout, &buf)

fmt.Fprintln(multi, "pesan ini ke stdout DAN buffer")
fmt.Println("Di buffer:", buf.String())

io.TeeReader — Baca Sambil Terus-Teruskan #

io.TeeReader membungkus reader sehingga setiap data yang dibaca juga otomatis ditulis ke writer lain. Berguna untuk logging, debugging, atau menghitung checksum sambil memproses data.

// Baca HTTP response body sambil log isinya
var logBuf bytes.Buffer
tee := io.TeeReader(resp.Body, &logBuf)

// Proses data dari tee (sekaligus tersalin ke logBuf)
var hasil HasilJSON
json.NewDecoder(tee).Decode(&hasil)

// logBuf sekarang berisi salinan response untuk debugging
log.Printf("Response body: %s", logBuf.String())

io.LimitReader — Batasi Jumlah Data yang Dibaca #

io.LimitReader membungkus reader dan memastikan maksimal N byte yang bisa dibaca. Sangat penting untuk keamanan — mencegah user mengunggah file yang terlalu besar dan menghabiskan memori server.

// ANTI-PATTERN: baca seluruh body tanpa batas
body, err := io.ReadAll(r.Body)  // bisa ratusan MB!

// BENAR: batasi ukuran yang boleh dibaca
const maxBodySize = 10 << 20 // 10 MB
limited := io.LimitReader(r.Body, maxBodySize)
body, err := io.ReadAll(limited)
if err != nil {
    http.Error(w, "body terlalu besar", http.StatusRequestEntityTooLarge)
    return
}

io.Pipe — Sambungkan Writer ke Reader #

io.Pipe membuat sepasang PipeWriter dan PipeReader yang tersambung — apapun yang ditulis ke writer bisa langsung dibaca dari reader. Data tidak tersimpan di buffer, sehingga write dan read harus terjadi secara concurrent.

pr, pw := io.Pipe()

// Goroutine penulis
go func() {
    defer pw.Close()
    for i := 0; i < 5; i++ {
        fmt.Fprintf(pw, "data ke-%d\n", i)
    }
}()

// Goroutine pembaca (atau fungsi yang menerima io.Reader)
scanner := bufio.NewScanner(pr)
for scanner.Scan() {
    fmt.Println("Diterima:", scanner.Text())
}

io.Pipe sangat berguna untuk menyambungkan output encoder (misalnya gzip.Writer) langsung ke input yang mengharapkan io.Reader — tanpa perlu buffer perantara di memori.


Package os — Bekerja dengan File #

Package os menyediakan fungsi untuk membuka, membuat, membaca, dan menulis file di sistem. os.File mengimplementasikan io.Reader, io.Writer, dan io.Seeker sekaligus.

Membuka dan Membaca File #

import (
    "fmt"
    "io"
    "os"
)

// os.Open — buka file untuk dibaca (read-only)
f, err := os.Open("data.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close() // selalu defer Close setelah Open berhasil

// Baca seluruh isi (cocok untuk file kecil)
data, err := io.ReadAll(f)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

Untuk file kecil yang seluruh isinya memang perlu dimuat, os.ReadFile adalah cara yang lebih ringkas — ia membuka file, membaca isinya, dan menutupnya dalam satu panggilan:

// os.ReadFile — buka, baca, tutup dalam satu langkah
data, err := os.ReadFile("config.json")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

Membuat dan Menulis File #

// os.Create — buat file baru (atau truncate jika sudah ada)
f, err := os.Create("output.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

f.WriteString("Baris pertama\n")
f.Write([]byte("Baris kedua\n"))
fmt.Fprintf(f, "Baris ke-%d\n", 3)

// os.WriteFile — tulis ke file dalam satu langkah
err = os.WriteFile("ringkas.txt", []byte("konten file"), 0644)

os.OpenFile — Kontrol Penuh atas Mode Pembukaan #

Ketika os.Open (read-only) dan os.Create (write, truncate) tidak cukup, gunakan os.OpenFile dengan flag yang sesuai.

FlagMakna
os.O_RDONLYBuka hanya untuk dibaca
os.O_WRONLYBuka hanya untuk ditulis
os.O_RDWRBuka untuk dibaca dan ditulis
os.O_CREATEBuat file jika belum ada
os.O_TRUNCKosongkan file saat dibuka
os.O_APPENDTambahkan data di akhir file
os.O_EXCLError jika file sudah ada (untuk atomic create)
// Buka file log untuk ditambahkan (append), buat jika belum ada
f, err := os.OpenFile("app.log",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY,
    0644,
)
if err != nil {
    log.Fatal(err)
}
defer f.Close()

fmt.Fprintln(f, "Log entry baru")

// Buka untuk read-write tanpa menghapus isi
f2, err := os.OpenFile("data.bin", os.O_RDWR, 0644)

Seek — Navigasi di Dalam File #

os.File mengimplementasikan io.Seeker, memungkinkan kamu berpindah posisi baca/tulis di dalam file.

f, _ := os.Open("data.txt")
defer f.Close()

// Baca 10 byte pertama
buf := make([]byte, 10)
f.Read(buf)
fmt.Println(string(buf))

// Kembali ke awal file
f.Seek(0, io.SeekStart)

// Lompat ke 100 byte dari akhir file
f.Seek(-100, io.SeekEnd)

// Maju 50 byte dari posisi sekarang
f.Seek(50, io.SeekCurrent)

Informasi File dengan os.Stat #

info, err := os.Stat("data.txt")
if err != nil {
    if os.IsNotExist(err) {
        fmt.Println("File tidak ditemukan")
    } else {
        log.Fatal(err)
    }
}

fmt.Println("Nama:", info.Name())
fmt.Println("Ukuran:", info.Size(), "byte")
fmt.Println("Mode:", info.Mode())
fmt.Println("Terakhir dimodifikasi:", info.ModTime())
fmt.Println("Direktori?", info.IsDir())

Package bufio — I/O dengan Buffer #

Setiap panggilan Read atau Write pada os.File adalah system call — mahal jika dilakukan untuk data kecil dalam jumlah banyak. bufio membungkus reader/writer dengan buffer di memori, mengelompokkan banyak operasi kecil menjadi sedikit system call besar.

flowchart TD
    A[Kode kamu\nbanyak Write kecil] --> B{Menggunakan\nbufio?}
    B -- Tidak --> C[Setiap Write\n= 1 system call]
    B -- Ya --> D[bufio.Writer\nbuffer 4096 byte]
    D -- "Hanya saat buffer penuh\natau Flush dipanggil" --> E[1 Write besar\n= 1 system call]
    C --> F[Banyak system call\nperforma lambat]
    E --> G[Sedikit system call\nperforma optimal]

bufio.NewReader — Membaca dengan Buffer #

import (
    "bufio"
    "os"
    "fmt"
)

f, _ := os.Open("data.txt")
defer f.Close()

reader := bufio.NewReader(f)

// ReadString — baca hingga delimiter
baris, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
    log.Fatal(err)
}
fmt.Print(baris)

// ReadLine — baca satu baris (tanpa alokasi ekstra)
line, isPrefix, err := reader.ReadLine()
// isPrefix = true jika baris terlalu panjang dan terpotong

// Peek — lihat N byte ke depan tanpa memajukan posisi
head, _ := reader.Peek(4)
fmt.Printf("4 byte pertama: %q\n", head)

// ReadByte dan UnreadByte — baca/kembalikan satu byte
b, _ := reader.ReadByte()
reader.UnreadByte() // kembalikan byte ke buffer

bufio.Scanner — Cara Paling Idiomatik Membaca Baris #

bufio.Scanner adalah cara yang direkomendasikan untuk membaca teks baris demi baris. Lebih bersih dan aman dibanding menggunakan ReadString secara manual.

// ANTI-PATTERN: membaca baris dengan ReadString secara manual
reader := bufio.NewReader(f)
for {
    baris, err := reader.ReadString('\n')
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Print(baris)
}

// BENAR: gunakan bufio.Scanner
scanner := bufio.NewScanner(f)
for scanner.Scan() {
    fmt.Println(scanner.Text()) // sudah tanpa '\n'
}
if err := scanner.Err(); err != nil {
    log.Fatal(err)
}

Scanner juga mendukung custom split function. Secara default ia memisahkan berdasarkan baris (ScanLines), tapi kamu bisa ganti dengan ScanWords, ScanBytes, atau fungsi kustom:

// Baca per kata
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
    fmt.Println("Kata:", scanner.Text())
}

// Baca per byte
scanner.Split(bufio.ScanBytes)

// Custom split: pisahkan berdasarkan koma
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
    for i, b := range data {
        if b == ',' {
            return i + 1, data[:i], nil
        }
    }
    if atEOF && len(data) > 0 {
        return len(data), data, nil
    }
    return 0, nil, nil
})
Buffer default bufio.Scanner adalah 64KB. Jika kamu memproses file dengan baris yang sangat panjang (misalnya file JSON satu baris berukuran besar), kamu perlu menaikkan batas buffer dengan scanner.Buffer(buf, maxSize), atau scanner akan mengembalikan bufio.ErrTooLong.

bufio.NewWriter — Menulis dengan Buffer #

f, _ := os.Create("output.txt")
defer f.Close()

writer := bufio.NewWriter(f)

// Tulis ke buffer (belum ke disk)
writer.WriteString("Baris pertama\n")
writer.WriteString("Baris kedua\n")
fmt.Fprintln(writer, "Baris ketiga")

// WAJIB: flush buffer ke disk sebelum Close
if err := writer.Flush(); err != nil {
    log.Fatal(err)
}

Jangan lupa memanggil writer.Flush() sebelum program selesai atau file ditutup. Jika kamu melewatkan ini, data yang masih ada di buffer tidak akan ditulis ke disk — kehilangan data secara diam-diam tanpa error apapun. Jadikan pola ini kebiasaan:

writer := bufio.NewWriter(f)
defer func() {
    if err := writer.Flush(); err != nil {
        log.Println("Gagal flush:", err)
    }
}()

bufio.ReadWriter — Buffer Dua Arah #

Untuk koneksi dua arah seperti TCP socket, bufio.ReadWriter menggabungkan bufio.Reader dan bufio.Writer dalam satu struct:

conn, _ := net.Dial("tcp", "localhost:8080")
defer conn.Close()

rw := bufio.NewReadWriter(
    bufio.NewReader(conn),
    bufio.NewWriter(conn),
)

// Kirim request
fmt.Fprintln(rw.Writer, "GET / HTTP/1.0")
rw.Writer.Flush()

// Baca response
resp, _ := rw.ReadString('\n')
fmt.Println(resp)

bytes.Buffer — Buffer di Memori #

bytes.Buffer dari package bytes adalah buffer serba guna di memori yang mengimplementasikan io.Reader, io.Writer, dan io.ByteScanner. Tidak perlu dibuka atau ditutup — sangat praktis untuk membangun konten di memori sebelum mengirimkannya.

import "bytes"

var buf bytes.Buffer

// Tulis ke buffer
buf.WriteString("Halo, ")
buf.WriteString("Golang!")
fmt.Fprintf(&buf, " Versi %d", 21)

// Baca dari buffer
fmt.Println(buf.String())  // "Halo, Golang! Versi 21"
fmt.Println(buf.Len())     // sisa byte yang belum dibaca

// Reset buffer untuk digunakan ulang
buf.Reset()

// bytes.NewBuffer — inisialisasi dengan isi awal
buf2 := bytes.NewBuffer([]byte("data awal"))

// bytes.NewBufferString — inisialisasi dari string
buf3 := bytes.NewBufferString("data awal dari string")
_ = buf2
_ = buf3

Pola: Membangun Response Body #

bytes.Buffer sangat umum digunakan untuk membangun konten yang akan dikirim sebagai HTTP response atau ditulis ke file:

func buatLaporan(data []Item) []byte {
    var buf bytes.Buffer

    fmt.Fprintf(&buf, "LAPORAN HARIAN\n")
    fmt.Fprintf(&buf, "=============\n\n")

    for i, item := range data {
        fmt.Fprintf(&buf, "%d. %s — Rp %d\n", i+1, item.Nama, item.Harga)
    }

    fmt.Fprintf(&buf, "\nTotal: %d item\n", len(data))
    return buf.Bytes()
}

Komposisi Reader dan Writer #

Kekuatan terbesar sistem I/O Go adalah kemampuan menumpuk (composing) reader dan writer. Setiap lapisan menambahkan kemampuan baru tanpa mengubah antarmuka.

flowchart LR
    A[os.File\ndata mentah] --> B[gzip.Reader\ndekompresi]
    B --> C[bufio.Reader\nbuffer]
    C --> D[bufio.Scanner\nbaca per baris]
    D --> E[Kode kamu]

    style A fill:#e8f4f8
    style B fill:#e8f8e8
    style C fill:#f8f4e8
    style D fill:#f8e8e8
    style E fill:#e8e8f8
import (
    "bufio"
    "compress/gzip"
    "os"
)

// Baca file gzip yang terkompresi, baris per baris
func bacaGzip(namaFile string) error {
    f, err := os.Open(namaFile) // layer 1: file
    if err != nil {
        return err
    }
    defer f.Close()

    gz, err := gzip.NewReader(f) // layer 2: dekompresi
    if err != nil {
        return err
    }
    defer gz.Close()

    scanner := bufio.NewScanner(gz) // layer 3: baca per baris
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    return scanner.Err()
}

// Tulis ke file gzip, dengan buffer
func tulisGzip(namaFile string, baris []string) error {
    f, err := os.Create(namaFile)
    if err != nil {
        return err
    }
    defer f.Close()

    gz := gzip.NewWriter(f)   // layer 2: kompresi
    defer gz.Close()

    bw := bufio.NewWriter(gz) // layer 3: buffer
    defer bw.Flush()

    for _, b := range baris {
        fmt.Fprintln(bw, b)
    }
    return nil
}

Pola Penggunaan Nyata #

Membaca File Konfigurasi Baris per Baris #

Pola yang sangat umum: membaca file konfigurasi sederhana format KEY=VALUE, mengabaikan baris kosong dan komentar.

func bacaKonfigurasi(path string) (map[string]string, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("buka konfigurasi: %w", err)
    }
    defer f.Close()

    config := make(map[string]string)
    scanner := bufio.NewScanner(f)

    for scanner.Scan() {
        baris := strings.TrimSpace(scanner.Text())

        // Lewati baris kosong dan komentar
        if baris == "" || strings.HasPrefix(baris, "#") {
            continue
        }

        // Pisahkan KEY=VALUE
        idx := strings.Index(baris, "=")
        if idx == -1 {
            continue
        }
        key := strings.TrimSpace(baris[:idx])
        val := strings.TrimSpace(baris[idx+1:])
        config[key] = val
    }

    return config, scanner.Err()
}

Menyalin File dengan Progress #

Dengan membungkus writer, kita bisa menghitung progress tanpa mengubah logika penyalinan:

type progressWriter struct {
    total   int64
    written int64
    onProgress func(persen int)
}

func (pw *progressWriter) Write(p []byte) (int, error) {
    n := len(p)
    pw.written += int64(n)
    if pw.total > 0 {
        persen := int(pw.written * 100 / pw.total)
        pw.onProgress(persen)
    }
    return n, nil
}

func salinDenganProgress(src, dst string) error {
    sumber, err := os.Open(src)
    if err != nil {
        return err
    }
    defer sumber.Close()

    info, _ := sumber.Stat()

    tujuan, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer tujuan.Close()

    pw := &progressWriter{
        total: info.Size(),
        onProgress: func(persen int) {
            fmt.Printf("\rProgress: %d%%", persen)
        },
    }

    // Tulis ke tujuan DAN progressWriter sekaligus
    multi := io.MultiWriter(tujuan, pw)
    _, err = io.Copy(multi, sumber)
    fmt.Println() // newline setelah progress
    return err
}

Memproses File Besar Secara Streaming #

Untuk file CSV atau log berukuran besar, proses baris per baris — jangan load ke memori:

func prosesCSVBesar(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)
    // Naikkan buffer untuk baris yang panjang
    scanner.Buffer(make([]byte, 1024*1024), 1024*1024)

    // Lewati header
    scanner.Scan()

    var totalBaris int
    for scanner.Scan() {
        baris := scanner.Text()
        kolom := strings.Split(baris, ",")
        if len(kolom) < 3 {
            continue
        }
        // proses setiap baris di sini...
        totalBaris++
    }

    fmt.Printf("Diproses: %d baris\n", totalBaris)
    return scanner.Err()
}

Membaca Input dari Stdin #

// Baca satu baris dari terminal
reader := bufio.NewReader(os.Stdin)
fmt.Print("Masukkan nama: ")
nama, _ := reader.ReadString('\n')
nama = strings.TrimSpace(nama)
fmt.Printf("Halo, %s!\n", nama)

// Baca stdin yang di-pipe (misalnya: echo "data" | ./program)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
    fmt.Println("Diterima:", scanner.Text())
}

Penanganan Error I/O #

I/O adalah area di mana error sangat umum terjadi — file tidak ada, disk penuh, permission ditolak, koneksi putus. Penanganan error yang baik adalah bagian tak terpisahkan dari kode I/O yang robust.

Membedakan Jenis Error #

f, err := os.Open("data.txt")
if err != nil {
    // Cek jenis error spesifik
    if os.IsNotExist(err) {
        fmt.Println("File tidak ditemukan")
    } else if os.IsPermission(err) {
        fmt.Println("Akses ditolak")
    } else {
        fmt.Printf("Error tidak diketahui: %v\n", err)
    }
    return
}
defer f.Close()

Pola Defer yang Aman #

defer f.Close() tidak menangkap error dari Close — padahal Close bisa gagal (misalnya saat flush akhir ke disk penuh). Untuk file yang ditulis, tangani error Close secara eksplisit:

// ANTI-PATTERN: abaikan error dari Close saat menulis
f, _ := os.Create("output.txt")
defer f.Close() // error dari Close diabaikan

// BENAR: tangani error Close untuk file yang ditulis
func tulisFile(path string, data []byte) (err error) {
    f, err := os.Create(path)
    if err != nil {
        return
    }
    defer func() {
        cerr := f.Close()
        if err == nil { // hanya overwrite jika belum ada error
            err = cerr
        }
    }()

    _, err = f.Write(data)
    return
}

Wrapping Error I/O #

Selalu beri konteks pada error I/O agar mudah di-debug:

// ANTI-PATTERN: propagate error tanpa konteks
func bacaConfig(path string) ([]byte, error) {
    return os.ReadFile(path)
    // error: "no such file or directory" — tapi file mana?
}

// BENAR: bungkus error dengan konteks
func bacaConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("bacaConfig %q: %w", path, err)
    }
    return data, nil
}
// error: `bacaConfig "config/app.yaml": no such file or directory`

Kapan Beralih ke Alternatif #

Tetap gunakan io / bufio / os jika:
  ✓ Membaca atau menulis file di sistem lokal
  ✓ Memproses stream data secara sequential (log, CSV, teks)
  ✓ Membangun pipeline I/O dengan komposisi reader/writer
  ✓ Membaca input dari terminal atau stdin
  ✓ Menyalin data antar stream (file, network, buffer)

Pertimbangkan package lain jika:
  ✗ Bekerja dengan file system secara abstrak → gunakan io/fs (Go 1.16+)
  ✗ Membaca/menulis format biner terstruktur → gunakan encoding/binary
  ✗ Membaca/menulis JSON, XML, CSV → gunakan encoding/json, encoding/xml, encoding/csv
  ✗ Kompresi/dekompresi → gunakan compress/gzip, compress/zlib
  ✗ Sinkronisasi I/O concurrent yang kompleks → pertimbangkan channel + goroutine
  ✗ Operasi file system (copy, rename, walk) yang lebih tinggi level → gunakan os, path/filepath

Ringkasan #

  • io.Reader dan io.Writer — dua interface inti seluruh I/O Go; fungsi yang menerima interface ini bisa bekerja dengan file, network, buffer, atau sumber apapun secara transparan.
  • io.Copy — cara paling efisien memindahkan data antar stream; menggunakan buffer internal 32KB sehingga tidak membebani memori meski data besar.
  • io.ReadAll — baca seluruh konten ke memori; gunakan hanya jika ukuran data sudah pasti kecil atau kamu memang butuh semua datanya sekaligus.
  • io.LimitReader — selalu batasi ukuran data yang dibaca dari sumber tidak terpercaya (user upload, HTTP body) untuk mencegah exhaustion memori.
  • bufio.Scanner — cara idiomatik membaca teks baris per baris; lebih bersih dan aman dari ReadString; naikkan buffer jika baris bisa sangat panjang.
  • bufio.Writer — wajib panggil Flush() sebelum selesai; data yang belum di-flush hilang tanpa error jika program berakhir.
  • bytes.Buffer — buffer serba guna di memori yang mengimplementasikan io.Reader dan io.Writer; gunakan untuk membangun konten di memori sebelum mengirimkannya.
  • Komposisi reader/writer — tumpuk layer (file → gzip → bufio → scanner) untuk membangun pipeline yang powerful; setiap layer menambahkan kemampuan tanpa mengubah antarmuka.
  • Error Close pada file tulis — tangani error dari Close secara eksplisit untuk file yang ditulis; kegagalan flush akhir ke disk tidak akan terdeteksi jika diabaikan.

← Sebelumnya: Strings   Berikutnya: Math →

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