Os #

Setiap aplikasi Go yang berjalan di dunia nyata pasti berinteraksi dengan sistem operasi — membaca file konfigurasi, menulis log, mengakses environment variable, menerima sinyal shutdown, atau memeriksa apakah sebuah path ada di filesystem. Package os adalah jembatan antara kode Go dan sistem operasi yang menjalankannya. Ia menyediakan antarmuka yang seragam untuk semua operasi ini, terlepas dari apakah program berjalan di Linux, macOS, atau Windows. Yang membuat os penting bukan hanya kemampuannya, tapi juga pendekatan error handling-nya yang konsisten — setiap operasi yang bisa gagal mengembalikan error yang bisa diperiksa dengan os.IsNotExist, os.IsPermission, dan fungsi sejenis. Artikel ini membahas seluruh package os: operasi file, direktori, environment variable, proses, dan sinyal.

Gambaran Besar Package os #

Package os mengorganisir fungsinya berdasarkan apa yang dioperasikan — file, direktori, environment, atau proses itu sendiri.

flowchart TD
    OS["package os"] --> File["Operasi File"]
    OS --> Dir["Operasi Direktori"]
    OS --> Env["Environment & Proses"]
    OS --> Signal["Sinyal OS"]
    OS --> Stdio["Standard I/O"]

    File --> F1["os.Open / os.Create / os.OpenFile"]
    File --> F2["os.ReadFile / os.WriteFile"]
    File --> F3["os.Remove / os.Rename / os.Chmod"]
    File --> F4["os.Stat / os.Lstat"]

    Dir --> D1["os.Mkdir / os.MkdirAll"]
    Dir --> D2["os.ReadDir / os.Getwd"]
    Dir --> D3["os.Remove / os.RemoveAll"]
    Dir --> D4["os.TempDir / os.MkdirTemp"]

    Env --> E1["os.Getenv / os.Setenv / os.Environ"]
    Env --> E2["os.Args — argumen program"]
    Env --> E3["os.Exit / os.Getpid"]
    Env --> E4["os.Hostname / os.Executable"]

    Signal --> S1["os/signal.Notify"]
    Signal --> S2["syscall.SIGINT / SIGTERM"]

    Stdio --> IO1["os.Stdin / os.Stdout / os.Stderr"]

    style OS fill:#4f86c6,color:#fff
    style File fill:#e8f5e9
    style Dir fill:#e3f2fd
    style Env fill:#fff3e0
    style Signal fill:#fce4ec
    style Stdio fill:#f3e5f5

Membaca dan Menulis File #

Operasi file adalah yang paling sering dilakukan dengan os. Go menyediakan dua level API: fungsi tingkat tinggi (ReadFile/WriteFile) untuk kasus sederhana, dan os.File untuk kontrol penuh.

ReadFile dan WriteFile — Cara Termudah #

Untuk file yang ukurannya masuk memori, os.ReadFile dan os.WriteFile adalah pilihan terbaik — satu baris, tidak perlu buka-tutup manual.

package main

import (
    "fmt"
    "os"
)

func main() {
    // Tulis file sekaligus
    konten := []byte("Halo dari Go!\nBaris kedua.\n")
    err := os.WriteFile("output.txt", konten, 0644)
    if err != nil {
        fmt.Fprintf(os.Stderr, "gagal tulis file: %v\n", err)
        os.Exit(1)
    }

    // Baca file sekaligus
    data, err := os.ReadFile("output.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "gagal baca file: %v\n", err)
        os.Exit(1)
    }
    fmt.Printf("Isi file (%d byte):\n%s", len(data), data)
}

Permission 0644 adalah konvensi umum untuk file teks: owner bisa baca-tulis, group dan others hanya bisa baca. Untuk file executable gunakan 0755.

os.Open, os.Create, dan os.OpenFile #

Untuk kontrol lebih lanjut — membaca sebagian file, append, atau mode akses tertentu — gunakan fungsi-fungsi ini:

flowchart LR
    subgraph Shortcut["Shortcut Functions"]
        Open["os.Open(path)\nread-only, O_RDONLY"]
        Create["os.Create(path)\nwrite, O_RDWR|O_CREATE|O_TRUNC"]
    end

    subgraph Full["Kontrol Penuh"]
        OpenFile["os.OpenFile(path, flag, perm)"]
    end

    subgraph Flags["Flag yang Umum"]
        direction TB
        F1["O_RDONLY — baca saja"]
        F2["O_WRONLY — tulis saja"]
        F3["O_RDWR — baca dan tulis"]
        F4["O_CREATE — buat jika belum ada"]
        F5["O_TRUNC — kosongkan saat dibuka"]
        F6["O_APPEND — tambahkan di akhir"]
        F7["O_EXCL — gagal jika sudah ada"]
    end

    OpenFile --> Flags

    style Shortcut fill:#e8f5e9
    style Full fill:#e3f2fd
    style Flags fill:#fff3e0
// os.Open — hanya untuk membaca
f, err := os.Open("config.txt")
if err != nil {
    // periksa jenis error
    if os.IsNotExist(err) {
        fmt.Println("file tidak ditemukan")
    } else {
        fmt.Fprintf(os.Stderr, "gagal buka file: %v\n", err)
    }
    return
}
defer f.Close() // selalu defer Close setelah Open berhasil

// os.Create — buat file baru atau kosongkan yang sudah ada
f2, err := os.Create("laporan.txt")
if err != nil {
    fmt.Fprintf(os.Stderr, "gagal buat file: %v\n", err)
    return
}
defer f2.Close()
fmt.Fprintln(f2, "Laporan harian")

// os.OpenFile — kontrol penuh dengan flag
// Append ke file yang sudah ada, buat jika belum ada
f3, err := os.OpenFile("app.log",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    fmt.Fprintf(os.Stderr, "gagal buka log: %v\n", err)
    return
}
defer f3.Close()
fmt.Fprintln(f3, "entry log baru")

// Buat file baru, gagal jika sudah ada (untuk menghindari overwrite)
f4, err := os.OpenFile("data.json",
    os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if os.IsExist(err) {
    fmt.Println("file sudah ada, lewati")
} else if err != nil {
    fmt.Fprintf(os.Stderr, "error: %v\n", err)
    return
} else {
    defer f4.Close()
    fmt.Fprintln(f4, "{}")
}

Membaca File Sebagian — io.Reader Interface #

os.File mengimplementasikan io.Reader, sehingga bisa digunakan dengan semua fungsi yang menerima reader — termasuk bufio.Scanner untuk membaca per baris:

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

func bacaPerBaris(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("bacaPerBaris: %w", err)
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)
    nomor := 1
    for scanner.Scan() {
        fmt.Printf("%3d: %s\n", nomor, scanner.Text())
        nomor++
    }

    // Periksa error scanner — bukan hanya EOF
    if err := scanner.Err(); err != nil {
        return fmt.Errorf("bacaPerBaris: error saat scan: %w", err)
    }
    return nil
}

Begitu juga os.File mengimplementasikan io.Writer, sehingga fmt.Fprintf dan bufio.Writer bisa langsung digunakan:

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

func tulisLaporan(path string, data []string) error {
    f, err := os.Create(path)
    if err != nil {
        return fmt.Errorf("tulisLaporan: %w", err)
    }
    defer f.Close()

    // Gunakan bufio.Writer untuk performa yang lebih baik
    // saat menulis banyak baris kecil
    w := bufio.NewWriter(f)

    fmt.Fprintf(w, "Laporan — %s\n", time.Now().Format("2006-01-02 15:04:05"))
    fmt.Fprintf(w, "%s\n", "===================")

    for i, item := range data {
        fmt.Fprintf(w, "%3d. %s\n", i+1, item)
    }

    // PENTING: Flush wajib dipanggil agar data benar-benar ditulis ke file
    // defer f.Close() tidak otomatis flush bufio.Writer
    if err := w.Flush(); err != nil {
        return fmt.Errorf("tulisLaporan: flush gagal: %w", err)
    }
    return nil
}
Jika menggunakan bufio.Writer, selalu panggil w.Flush() sebelum f.Close(). defer f.Close() tidak akan men-flush buffer secara otomatis — data yang belum di-flush akan hilang tanpa ada error. Ini adalah sumber bug yang sering diabaikan karena program tidak melaporkan error apapun tapi file yang ditulis tidak lengkap.

Informasi File — os.Stat dan FileInfo #

os.Stat mengembalikan fs.FileInfo yang berisi metadata file tanpa membukanya: ukuran, permission, waktu modifikasi, dan apakah itu direktori.

info, err := os.Stat("config.yaml")
if err != nil {
    if os.IsNotExist(err) {
        fmt.Println("file tidak ada")
        return
    }
    fmt.Fprintf(os.Stderr, "stat error: %v\n", err)
    return
}

fmt.Println("Nama:", info.Name())          // config.yaml
fmt.Println("Ukuran:", info.Size(), "byte") // 1024
fmt.Println("Permission:", info.Mode())     // -rw-r--r--
fmt.Println("Direktori?", info.IsDir())     // false
fmt.Println("Modifikasi:", info.ModTime().Format("2006-01-02 15:04:05"))

Perbedaan Stat dan Lstat #

flowchart TD
    A["Path target"] --> B{Apakah\nsymlink?}
    B -- Bukan symlink --> C["os.Stat dan os.Lstat\nmengembalikan info\nfile yang sama"]
    B -- Symlink --> D{Fungsi yang\ndigunakan?}
    D -- "os.Stat()" --> E["Ikuti symlink\n→ info file tujuan"]
    D -- "os.Lstat()" --> F["Tidak ikuti symlink\n→ info symlink itu sendiri"]

    E --> G["info.Mode().IsRegular() → true\njika tujuan adalah file biasa"]
    F --> H["info.Mode()&fs.ModeSymlink != 0\n→ true, ini adalah symlink"]
// Stat — ikuti symlink, periksa file tujuan
info, _ := os.Stat("link-ke-file.txt")

// Lstat — jangan ikuti symlink, periksa symlink itu sendiri
infoLink, _ := os.Lstat("link-ke-file.txt")
fmt.Println(infoLink.Mode()&os.ModeSymlink != 0) // true jika symlink

Memeriksa Keberadaan File #

// ANTI-PATTERN: periksa exist lalu open — ada race condition
if _, err := os.Stat(path); err == nil {
    f, err := os.Open(path) // file bisa dihapus di antara Stat dan Open!
    // ...
}

// BENAR: langsung open, periksa error
f, err := os.Open(path)
if err != nil {
    if os.IsNotExist(err) {
        // tangani file tidak ada
        return
    }
    // tangani error lain
    return
}
defer f.Close()

Operasi Direktori #

Membuat Direktori #

// Mkdir — buat satu direktori, gagal jika parent belum ada
err := os.Mkdir("output", 0755)
if err != nil && !os.IsExist(err) {
    fmt.Fprintf(os.Stderr, "gagal buat dir: %v\n", err)
}

// MkdirAll — buat semua direktori yang diperlukan sekaligus
// seperti "mkdir -p" di shell
err = os.MkdirAll("output/2024/03/laporan", 0755)
if err != nil {
    fmt.Fprintf(os.Stderr, "gagal buat dir tree: %v\n", err)
}

Membaca Isi Direktori #

// ReadDir — baca semua entry dalam direktori, sudah terurut berdasarkan nama
entries, err := os.ReadDir(".")
if err != nil {
    fmt.Fprintf(os.Stderr, "gagal baca dir: %v\n", err)
    return
}

for _, entry := range entries {
    tipe := "file"
    if entry.IsDir() {
        tipe = "dir "
    }

    // Info() mengembalikan FileInfo — bisa nil jika file berubah
    info, err := entry.Info()
    if err != nil {
        continue
    }

    fmt.Printf("[%s] %-30s %8d byte\n",
        tipe, entry.Name(), info.Size())
}

Traversal Rekursif dengan os.WalkDir #

os.WalkDir adalah cara paling efisien untuk menelusuri seluruh direktori secara rekursif. Ia lebih efisien dari filepath.Walk karena tidak memanggil Lstat untuk setiap entry.

import (
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
    "strings"
)

func hitungFile(root string) (jumlahFile, jumlahDir int, totalSize int64) {
    os.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            // Lanjutkan meski ada error di satu entry
            fmt.Fprintf(os.Stderr, "skip %s: %v\n", path, err)
            return nil
        }

        if d.IsDir() {
            // Skip direktori tersembunyi
            if strings.HasPrefix(d.Name(), ".") && path != root {
                return fs.SkipDir
            }
            jumlahDir++
            return nil
        }

        jumlahFile++
        info, err := d.Info()
        if err == nil {
            totalSize += info.Size()
        }
        return nil
    })
    return
}

// Kumpulkan semua file .go dalam project
func cariFileGo(root string) ([]string, error) {
    var files []string
    err := os.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if !d.IsDir() && filepath.Ext(path) == ".go" {
            files = append(files, path)
        }
        return nil
    })
    return files, err
}

File Temporary #

// MkdirTemp — buat direktori temporary (dihapus setelah selesai)
tmpDir, err := os.MkdirTemp("", "myapp-*")
if err != nil {
    fmt.Fprintf(os.Stderr, "gagal buat tmp dir: %v\n", err)
    return
}
defer os.RemoveAll(tmpDir) // bersihkan setelah selesai

// CreateTemp — buat file temporary
tmpFile, err := os.CreateTemp(tmpDir, "data-*.json")
if err != nil {
    fmt.Fprintf(os.Stderr, "gagal buat tmp file: %v\n", err)
    return
}
defer os.Remove(tmpFile.Name()) // hapus file setelah selesai
defer tmpFile.Close()

fmt.Fprintf(tmpFile, `{"status": "ok"}`)
fmt.Println("File temporary:", tmpFile.Name())
// /tmp/myapp-123456789/data-987654321.json

Environment Variable #

Environment variable adalah cara standar untuk mengkonfigurasi aplikasi tanpa mengubah kode atau file konfigurasi. Ini adalah praktik yang sangat umum di container dan cloud deployment.

flowchart LR
    subgraph Sources["Sumber Config"]
        EnvVar["Environment Variable\nDB_HOST=localhost"]
        DotEnv[".env file\n(dengan library godotenv)"]
        Args["os.Args\n--host=localhost"]
    end

    subgraph App["Aplikasi Go"]
        GetEnv["os.Getenv('DB_HOST')"]
        LookupEnv["os.LookupEnv('DB_HOST')"]
        Environ["os.Environ()\nsemua env"]
    end

    subgraph Config["Struct Config"]
        C["Config{\n  Host: 'localhost'\n  Port: 5432\n}"]
    end

    Sources --> App --> Config

    style Sources fill:#e3f2fd
    style App fill:#e8f5e9
    style Config fill:#fff3e0
// Getenv — kembalikan string kosong jika tidak ada
host := os.Getenv("DB_HOST")
if host == "" {
    host = "localhost" // nilai default
}

// ANTI-PATTERN: tidak bisa bedakan "tidak ada" vs "kosong sengaja"
port := os.Getenv("DB_PORT")
if port == "" {
    port = "5432" // bisa salah jika DB_PORT="" disengaja
}

// BENAR: LookupEnv — bedakan "tidak ada" dan "nilai kosong"
portStr, ada := os.LookupEnv("DB_PORT")
if !ada {
    portStr = "5432" // default hanya jika benar-benar tidak ada
}

// Setenv — set environment variable untuk proses ini (dan child process)
os.Setenv("APP_ENV", "production")

// Unsetenv — hapus environment variable
os.Unsetenv("DEBUG_MODE")

// Environ — ambil semua environment variable sebagai []string "KEY=VALUE"
for _, env := range os.Environ() {
    parts := strings.SplitN(env, "=", 2)
    if len(parts) == 2 {
        fmt.Printf("%-20s = %s\n", parts[0], parts[1])
    }
}

// Clearenv — hapus semua environment variable (hati-hati!)
// os.Clearenv() // jangan dipakai sembarangan

Pola: Config dari Environment #

import (
    "fmt"
    "os"
    "strconv"
    "time"
)

type Config struct {
    DBHost     string
    DBPort     int
    DBName     string
    DBUser     string
    DBPassword string
    MaxConn    int
    Timeout    time.Duration
    Debug      bool
}

func configDariEnv() (*Config, error) {
    cfg := &Config{
        DBHost:  getEnvDefault("DB_HOST", "localhost"),
        DBName:  getEnvDefault("DB_NAME", "myapp"),
        DBUser:  getEnvDefault("DB_USER", "postgres"),
        MaxConn: 10,
        Timeout: 30 * time.Second,
    }

    // Port — butuh konversi ke int
    portStr := getEnvDefault("DB_PORT", "5432")
    port, err := strconv.Atoi(portStr)
    if err != nil {
        return nil, fmt.Errorf("DB_PORT tidak valid: %w", err)
    }
    cfg.DBPort = port

    // Password — wajib ada
    password, ada := os.LookupEnv("DB_PASSWORD")
    if !ada || password == "" {
        return nil, fmt.Errorf("DB_PASSWORD wajib diset")
    }
    cfg.DBPassword = password

    // MaxConn — opsional dengan default
    if maxConnStr, ada := os.LookupEnv("DB_MAX_CONN"); ada {
        maxConn, err := strconv.Atoi(maxConnStr)
        if err != nil {
            return nil, fmt.Errorf("DB_MAX_CONN tidak valid: %w", err)
        }
        cfg.MaxConn = maxConn
    }

    // Debug flag
    cfg.Debug = os.Getenv("APP_DEBUG") == "true" ||
        os.Getenv("APP_DEBUG") == "1"

    return cfg, nil
}

func getEnvDefault(key, defaultVal string) string {
    if val, ada := os.LookupEnv(key); ada {
        return val
    }
    return defaultVal
}

Argumen Program — os.Args #

os.Args adalah slice string yang berisi argumen yang diberikan saat program dijalankan. os.Args[0] adalah nama program itu sendiri.

// Program: ./myapp --env production --port 8080 --debug

fmt.Println("Program:", os.Args[0])    // ./myapp
fmt.Println("Semua args:", os.Args[1:]) // [--env production --port 8080 --debug]
fmt.Println("Jumlah arg:", len(os.Args)-1) // 5

// Parsing manual — untuk program sederhana
func parseArgs() map[string]string {
    args := make(map[string]string)
    a := os.Args[1:]

    for i := 0; i < len(a); i++ {
        if strings.HasPrefix(a[i], "--") {
            key := strings.TrimPrefix(a[i], "--")
            if i+1 < len(a) && !strings.HasPrefix(a[i+1], "--") {
                args[key] = a[i+1]
                i++
            } else {
                args[key] = "true" // flag tanpa nilai
            }
        }
    }
    return args
}
Untuk parsing argumen yang lebih kompleks, gunakan package flag dari standard library (dibahas di artikel tersendiri) atau library pihak ketiga seperti cobra (untuk CLI tools yang lebih lengkap dengan subcommand). os.Args biasanya hanya diakses langsung untuk program yang sangat sederhana.

Informasi Proses #

// PID proses ini
fmt.Println("PID:", os.Getpid())

// PID parent process
fmt.Println("PPID:", os.Getppid())

// Hostname mesin
hostname, err := os.Hostname()
if err == nil {
    fmt.Println("Host:", hostname)
}

// Path executable program ini
execPath, err := os.Executable()
if err == nil {
    fmt.Println("Executable:", execPath)
    fmt.Println("Dir:", filepath.Dir(execPath))
}

// Working directory saat ini
wd, err := os.Getwd()
if err == nil {
    fmt.Println("CWD:", wd)
}

// Ganti working directory
err = os.Chdir("/tmp")
if err != nil {
    fmt.Fprintf(os.Stderr, "chdir gagal: %v\n", err)
}

Penanganan Sinyal OS #

Sinyal OS adalah mekanisme komunikasi antara sistem operasi dan proses. Sinyal yang paling penting untuk ditangani di aplikasi produksi adalah SIGINT (Ctrl+C) dan SIGTERM (shutdown dari orchestrator seperti Kubernetes). Tanpa penanganan yang tepat, aplikasi akan langsung mati saat menerima sinyal ini — koneksi database tidak ditutup, request yang sedang diproses terpotong, data bisa korup.

sequenceDiagram
    participant OS as Sistem Operasi
    participant Signal as os/signal
    participant App as Aplikasi Go
    participant DB as Database/Resources

    OS->>Signal: SIGTERM (dari K8s, Docker stop, dll)
    Signal->>App: channel sigChan <- syscall.SIGTERM
    App->>App: terima sinyal, mulai shutdown
    App->>DB: tutup koneksi database
    App->>App: tunggu request aktif selesai
    App->>App: os.Exit(0)

    Note over App,DB: Graceful shutdown — tidak ada data yang hilang
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // Setup channel untuk menangkap sinyal
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan,
        syscall.SIGINT,  // Ctrl+C
        syscall.SIGTERM, // kill / docker stop / kubernetes
    )

    // Jalankan aplikasi di goroutine terpisah
    done := make(chan struct{})
    go func() {
        defer close(done)
        jalankanServer()
    }()

    // Blokir sampai sinyal diterima
    sig := <-sigChan
    fmt.Printf("\nMenerima sinyal: %v\n", sig)
    fmt.Println("Memulai graceful shutdown...")

    // Beri waktu untuk cleanup
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    // Lakukan cleanup
    if err := bersihkanResource(ctx); err != nil {
        fmt.Fprintf(os.Stderr, "cleanup error: %v\n", err)
        os.Exit(1)
    }

    fmt.Println("Shutdown selesai")
    os.Exit(0)
}

func bersihkanResource(ctx context.Context) error {
    // Simulasi cleanup: tutup koneksi DB, flush buffer log, dll
    fmt.Println("Menutup koneksi database...")
    select {
    case <-time.After(2 * time.Second): // simulasi operasi cleanup
        fmt.Println("Koneksi database ditutup")
        return nil
    case <-ctx.Done():
        return fmt.Errorf("cleanup timeout: %w", ctx.Err())
    }
}

Error Handling di os — Pemeriksaan Jenis Error #

Package os menyediakan fungsi-fungsi helper untuk memeriksa jenis error yang dikembalikan operasi file dan direktori. Ini penting karena tindakan yang tepat berbeda tergantung jenis errornya.

flowchart TD
    E["error dari os.*"] --> Check{Periksa jenis\nerror}

    Check --> IsNotExist["os.IsNotExist(err)\natau errors.Is(err, fs.ErrNotExist)"]
    Check --> IsExist["os.IsExist(err)\natau errors.Is(err, fs.ErrExist)"]
    Check --> IsPerm["os.IsPermission(err)\natau errors.Is(err, fs.ErrPermission)"]
    Check --> IsTimeout["os.IsTimeout(err)"]
    Check --> Other["error lain\nlog dan return"]

    IsNotExist --> A1["Buat file/dir\natau kembalikan not found"]
    IsExist --> A2["Lewati atau\nganti nama"]
    IsPerm --> A3["Minta permission\natau jalankan sebagai root"]
    IsTimeout --> A4["Retry dengan\nbackoff"]

    style E fill:#fce4ec
    style Check fill:#4f86c6,color:#fff
    style IsNotExist fill:#e8f5e9
    style IsExist fill:#e3f2fd
    style IsPerm fill:#fff3e0
    style IsTimeout fill:#f3e5f5
func muatKonfigurasi(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        switch {
        case os.IsNotExist(err):
            // File tidak ada — gunakan config default
            fmt.Printf("config %s tidak ditemukan, pakai default\n", path)
            return konfigDefault(), nil

        case os.IsPermission(err):
            // Tidak punya akses — ini adalah error yang serius
            return nil, fmt.Errorf("tidak ada izin baca %s — coba jalankan dengan sudo: %w", path, err)

        default:
            // Error lain — wrap dan return
            return nil, fmt.Errorf("gagal baca config %s: %w", path, err)
        }
    }
    return data, nil
}

// Versi modern menggunakan errors.Is (Go 1.13+)
// errors.Is lebih robust karena bekerja dengan wrapped errors
import "io/fs"

func periksaFile(path string) {
    _, err := os.Stat(path)
    if errors.Is(err, fs.ErrNotExist) {
        fmt.Println("tidak ada")
    } else if errors.Is(err, fs.ErrPermission) {
        fmt.Println("tidak punya izin")
    } else if err != nil {
        fmt.Println("error lain:", err)
    } else {
        fmt.Println("ada")
    }
}

Operasi File Lainnya #

Rename, Copy, dan Remove #

// Rename — juga bisa dipakai untuk memindahkan file
err := os.Rename("lama.txt", "baru.txt")
// Atau pindahkan ke direktori lain
err = os.Rename("tmp/data.json", "output/data.json")

// Remove — hapus file atau direktori kosong
err = os.Remove("tidak-perlu.txt")
if err != nil && !os.IsNotExist(err) {
    fmt.Fprintf(os.Stderr, "gagal hapus: %v\n", err)
}

// RemoveAll — hapus direktori beserta isinya (seperti rm -rf)
err = os.RemoveAll("tmp/")
// HATI-HATI: tidak ada undo untuk RemoveAll!

// Chmod — ubah permission file
err = os.Chmod("script.sh", 0755) // jadikan executable

// Truncate — potong file ke ukuran tertentu
err = os.Truncate("file.txt", 0) // kosongkan file tanpa menghapus

Menyalin File #

Package os tidak menyediakan fungsi copy langsung — Go sengaja memisahkan operasi membaca dan menulis. Pola yang benar menggunakan io.Copy:

import (
    "io"
    "os"
)

func salinFile(src, dst string) error {
    // Buka file sumber
    sumber, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("salinFile: buka sumber: %w", err)
    }
    defer sumber.Close()

    // Buat file tujuan
    tujuan, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("salinFile: buat tujuan: %w", err)
    }
    defer tujuan.Close()

    // Salin konten
    bytesDisalin, err := io.Copy(tujuan, sumber)
    if err != nil {
        return fmt.Errorf("salinFile: copy: %w", err)
    }

    // Salin permission dari file sumber
    infoSumber, err := sumber.Stat()
    if err == nil {
        os.Chmod(dst, infoSumber.Mode())
    }

    fmt.Printf("Disalin %d byte dari %s ke %s\n", bytesDisalin, src, dst)
    return nil
}

Pola Penggunaan di Produksi #

Atomic Write — Tulis Tanpa Risiko Korupsi #

Menulis langsung ke file target berisiko: jika program crash di tengah penulisan, file menjadi korup. Pola atomic write menggunakan file temporary sebagai buffer:

flowchart LR
    A["Data baru"] --> B["Tulis ke\nfile.tmp"]
    B --> C{Tulis\nberhasil?}
    C -- Ya --> D["os.Rename\nfile.tmp → file.json"]
    C -- Tidak --> E["os.Remove\nfile.tmp"]
    D --> F["file.json\nselalu valid"]
    E --> G["file.json lama\ntetap utuh"]

    style D fill:#e8f5e9
    style E fill:#fce4ec
    style F fill:#e8f5e9
    style G fill:#e3f2fd
func tulisAtomic(path string, data []byte) error {
    // Tulis ke file temporary di direktori yang sama
    // (penting: harus di filesystem yang sama agar rename atomic)
    dir := filepath.Dir(path)
    tmpFile, err := os.CreateTemp(dir, ".tmp-*")
    if err != nil {
        return fmt.Errorf("tulisAtomic: buat tmp: %w", err)
    }
    tmpPath := tmpFile.Name()

    // Pastikan file tmp dibersihkan jika ada error
    defer func() {
        tmpFile.Close()
        os.Remove(tmpPath) // no-op jika rename berhasil
    }()

    // Tulis data ke file temporary
    if _, err := tmpFile.Write(data); err != nil {
        return fmt.Errorf("tulisAtomic: tulis: %w", err)
    }

    // Sync ke disk sebelum rename
    if err := tmpFile.Sync(); err != nil {
        return fmt.Errorf("tulisAtomic: sync: %w", err)
    }

    // Rename — operasi atomic di filesystem yang sama
    // File lama tidak pernah dalam keadaan parsial dari sudut pandang reader
    if err := os.Rename(tmpPath, path); err != nil {
        return fmt.Errorf("tulisAtomic: rename: %w", err)
    }

    return nil
}

Memastikan Direktori Ada Sebelum Menulis #

func pastikanDirAda(path string) error {
    dir := filepath.Dir(path)
    if err := os.MkdirAll(dir, 0755); err != nil {
        return fmt.Errorf("pastikanDirAda %s: %w", dir, err)
    }
    return nil
}

func tulisFile(path string, data []byte) error {
    if err := pastikanDirAda(path); err != nil {
        return err
    }
    return os.WriteFile(path, data, 0644)
}

Membaca File Konfigurasi dengan Fallback #

// Urutan prioritas konfigurasi yang umum di aplikasi produksi
func muatConfig() (*Config, error) {
    lokasi := []string{
        os.Getenv("CONFIG_PATH"),           // eksplisit dari env
        "./config.yaml",                     // direktori saat ini
        filepath.Join(os.Getenv("HOME"),
            ".config/myapp/config.yaml"),    // home directory user
        "/etc/myapp/config.yaml",            // system-wide config
    }

    for _, path := range lokasi {
        if path == "" {
            continue
        }
        data, err := os.ReadFile(path)
        if err != nil {
            if os.IsNotExist(err) {
                continue // coba lokasi berikutnya
            }
            return nil, fmt.Errorf("gagal baca config %s: %w", path, err)
        }
        fmt.Printf("Menggunakan config dari: %s\n", path)
        return parseConfig(data)
    }

    fmt.Println("Tidak ada config ditemukan, pakai nilai default")
    return configDefault(), nil
}

Kapan Beralih ke Alternatif #

Tetap gunakan os jika:
  ✓ Baca/tulis file dan direktori secara langsung
  ✓ Akses environment variable dan argumen program
  ✓ Menangani sinyal OS untuk graceful shutdown
  ✓ Informasi proses (PID, hostname, executable path)
  ✓ Operasi filesystem: rename, remove, chmod, stat

Pertimbangkan io/fs dan fs.FS jika:
  ✗ Kamu ingin abstraksi filesystem yang bisa di-mock di testing
  ✗ Bekerja dengan embedded files (go:embed)
  ✗ Membuat abstraksi yang bisa digunakan dengan filesystem virtual

Pertimbangkan path/filepath jika:
  ✗ Manipulasi dan join path yang lintas platform
  ✗ Mencari file dengan pola glob
  ✗ Konversi path relatif ke absolut

Pertimbangkan bufio jika:
  ✗ Membaca file besar baris per baris
  ✗ Perlu buffer untuk meningkatkan performa I/O
  ✗ Parsing format teks yang kompleks baris per baris

Pertimbangkan library eksternal jika:
  ✗ Watching perubahan file secara real-time → fsnotify
  ✗ Operasi filesystem yang sangat kompleks → afero (mock-friendly)

Ringkasan #

  • os.ReadFile / os.WriteFile adalah cara tercepat untuk baca/tulis file kecil sekaligus — tidak perlu open/close manual, cocok untuk config dan data sederhana.
  • os.OpenFile dengan flag memberi kontrol penuh: O_APPEND untuk log, O_EXCL untuk menghindari overwrite, O_RDWR untuk baca-tulis sekaligus.
  • Selalu defer f.Close() tepat setelah os.Open atau os.Create berhasil — jangan tunda deklarasi ini.
  • Jika menggunakan bufio.Writer, selalu Flush() sebelum file ditutup — defer f.Close() tidak men-flush buffer secara otomatis.
  • os.LookupEnv lebih baik dari os.Getenv ketika kamu perlu membedakan antara variabel yang tidak ada dengan variabel yang sengaja dikosongkan.
  • os.MkdirAll adalah padanan mkdir -p — gunakan ini daripada os.Mkdir untuk menghindari error jika parent directory belum ada.
  • Pola atomic write (tulis ke tmp lalu rename) memastikan file target tidak pernah dalam keadaan parsial — penting untuk file konfigurasi dan data penting.
  • Tangani sinyal SIGINT dan SIGTERM untuk graceful shutdown — ini standar wajib di aplikasi yang berjalan di container atau cloud.
  • os.IsNotExist / errors.Is(err, fs.ErrNotExist) untuk memeriksa jenis error filesystem — jangan hanya print error, periksa jenisnya dan tangani secara berbeda.
  • os.WalkDir lebih efisien dari filepath.Walk untuk traversal direktori rekursif karena menghindari Lstat tambahan untuk setiap entry.

← Sebelumnya: Fmt   Berikutnya: Time →

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