Aplikasi jarang bekerja dengan satu berkas saja. Program backup harus menelusuri pohon folder, alat analisis memeriksa ukuran dan jenis setiap entri, sedangkan cache perlu membuat struktur direktori bersarang dengan cepat. Package os menutup semua kebutuhan itu: os.ReadDir untuk isi direktori, os.Stat untuk metadata, os.MkdirAll dan os.Remove untuk membangun serta membongkar struktur, dan filepath.WalkDir untuk penjelajahan penuh. Kita juga berkenalan dengan fstest.MapFS, filesystem virtual dalam memori yang membuat demo deterministik.

Mendaftar Isi Direktori dengan os.ReadDir #

Fungsi os.ReadDir(dir) membaca isi satu direktori dan mengembalikan []os.DirEntry yang sudah terurut berdasarkan nama — urutan yang dijamin stabil sehingga output program tidak perlu diurutkan lagi. Setiap entri membawa method Name() untuk nama, IsDir() untuk membedakan direktori dari berkas, dan Info() yang mengembalikan fs.FileInfo lengkap:

go
entries, err := os.ReadDir("dokumen")
if err != nil {
	fmt.Println("gagal membaca direktori:", err)
	return
}
for _, e := range entries {
	fmt.Println(e.Name(), e.IsDir())
}

Info() pada os.DirEntry bersifat malas: metadata hanya dibaca bila diminta, sehingga menelusuri ribuan entri tidak memicu syscall stat per entri.

Membaca Metadata dengan os.Stat #

Fungsi os.Stat(path) mengambil metadata satu entri dalam bentuk fs.FileInfo. Interface tersebut menyediakan Name(), Size() dalam byte, Mode(), IsDir(), dan ModTime():

go
info, err := os.Stat("dokumen/catatan.txt")
if err != nil {
	fmt.Println("gagal membaca metadata:", err)
	return
}
fmt.Println(info.Size(), info.IsDir())

Saudara kembarnya, os.Lstat, berperilaku identik kecuali pada satu kasus: tautan simbolik. os.Stat mengikuti tautan dan melaporkan metadata berkas tujuan, sedangkan os.Lstat melaporkan metadata tautannya sendiri. Bit tautan dapat diuji dengan info.Mode()&fs.ModeSymlink != 0. Perbedaan ini penting bagi alat yang tidak boleh mengikuti tautan, misalnya program backup.

Membangun dan Membongkar Struktur #

os.Mkdir membuat satu direktori dan gagal bila induknya belum ada. os.MkdirAll membuat seluruh jalur sekaligus — MkdirAll("dokumen/laporan/2026") membuat tiga level tanpa komplain bila sebagian sudah ada. Untuk pembongkaran, os.Remove menghapus satu berkas atau direktori kosong, sedangkan os.RemoveAll memusnahkan pohon direktori beserta seluruh isinya tanpa bertanya.

Menelusuri Pohon dengan WalkDir #

filepath.WalkDir(akar, fn) mengunjungi akar, lalu setiap entri di bawahnya secara berurutan leksikal. Argumen kedua adalah fungsi callback yang menerima path, fs.DirEntry, dan error; mengembalikan fs.SkipDir membuat penjelajahan melompati satu direktori, sedangkan error menghentikan seluruh penjelajahan.

Untuk pengujian dan demonstrasi, testing/fstest menyediakan fstest.MapFS — filesystem virtual yang dibangun dari map path ke isi berkas. Direktori tersintesis otomatis dari jalur yang ada, dan nilai ini mengimplementasikan fs.FS sehingga bisa langsung ditelusuri dengan fs.WalkDir:

go
peta := fstest.MapFS{
	"proyek/README.md":    {Data: []byte("demo\n")},
	"proyek/util/util.go": {Data: []byte("package util\n")},
}
fs.WalkDir(peta, ".", func(path string, d fs.DirEntry, err error) error {
	fmt.Println(path, d.IsDir())
	return nil
})

Pola fstest.MapFS dipakai luas dalam pengujian karena struktur pohonnya pasti identik di setiap mesin. Chapter Test Fixture menunjukkan pemakaiannya pada berkas _test.go.

Program Utuh #

Berkas contoh.go merangkai seluruh materi di atas: membangun struktur dokumen/laporan, mendaftar entri, membandingkan Stat dengan Lstat pada sebuah tautan simbolik, menghapus satu berkas, menelusuri pohon di disk dengan filepath.WalkDir, lalu menelusuri fstest.MapFS dengan fs.WalkDir:

go
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"testing/fstest"
)

func main() {
	// Direktori kerja sementara agar program aman dijalankan berulang.
	dir, err := os.MkdirTemp("", "contoh-dir-*")
	if err != nil {
		fmt.Println("gagal membuat direktori:", err)
		return
	}
	defer os.RemoveAll(dir)

	// Struktur uji: MkdirAll membuat seluruh jalur bersarang sekaligus.
	if err := os.MkdirAll(filepath.Join(dir, "dokumen", "laporan"), 0755); err != nil {
		fmt.Println("gagal membuat direktori:", err)
		return
	}
	if err := buatBerkas(filepath.Join(dir, "baca-saya.txt"), "mulai dari sini\n"); err != nil {
		fmt.Println("gagal membuat berkas:", err)
		return
	}
	if err := buatBerkas(filepath.Join(dir, "dokumen", "catatan.txt"), "catatan harian\n"); err != nil {
		fmt.Println("gagal membuat berkas:", err)
		return
	}
	if err := buatBerkas(filepath.Join(dir, "dokumen", "laporan", "penjualan.txt"), "omzet: 1000000\n"); err != nil {
		fmt.Println("gagal membuat berkas:", err)
		return
	}

	// os.ReadDir mengembalikan daftar entri terurut berdasarkan nama.
	fmt.Println("isi direktori utama:")
	entries, err := os.ReadDir(dir)
	if err != nil {
		fmt.Println("gagal membaca direktori:", err)
		return
	}
	for _, e := range entries {
		jenis := "berkas"
		if e.IsDir() {
			jenis = "direktori"
		}
		fmt.Printf("  %-14s %s\n", e.Name(), jenis)
	}

	// os.Stat memberikan metadata satu entri.
	target := filepath.Join(dir, "dokumen", "catatan.txt")
	info, err := os.Stat(target)
	if err != nil {
		fmt.Println("gagal membaca metadata:", err)
		return
	}
	fmt.Printf("Stat catatan.txt : %d byte, isDir=%v\n", info.Size(), info.IsDir())

	// Stat mengikuti tautan simbolik; Lstat melaporkan tautannya sendiri.
	if err := os.Symlink(target, filepath.Join(dir, "pintasan.txt")); err != nil {
		fmt.Println("gagal membuat tautan:", err)
		return
	}
	infoStat, err := os.Stat(filepath.Join(dir, "pintasan.txt"))
	if err != nil {
		fmt.Println("gagal membaca metadata:", err)
		return
	}
	infoLstat, err := os.Lstat(filepath.Join(dir, "pintasan.txt"))
	if err != nil {
		fmt.Println("gagal membaca metadata:", err)
		return
	}
	fmt.Printf("Stat  pintasan  : %d byte (mengikuti tautan)\n", infoStat.Size())
	fmt.Printf("Lstat pintasan  : symlink=%v\n", infoLstat.Mode()&fs.ModeSymlink != 0)

	// os.Remove menghapus satu entri.
	if err := os.Remove(filepath.Join(dir, "baca-saya.txt")); err != nil {
		fmt.Println("gagal menghapus berkas:", err)
		return
	}
	_, err = os.Stat(filepath.Join(dir, "baca-saya.txt"))
	fmt.Println("setelah Remove  :", errors.Is(err, fs.ErrNotExist))

	// filepath.WalkDir menelusuri seluruh pohon secara leksikal.
	fmt.Println("pohon di disk:")
	err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(dir, path)
		if err != nil {
			return err
		}
		if rel == "." {
			return nil
		}
		jenis := "berkas"
		if d.IsDir() {
			jenis = "direktori"
		}
		fmt.Printf("  %-30s %s\n", rel, jenis)
		return nil
	})
	if err != nil {
		fmt.Println("gagal menelusuri pohon:", err)
		return
	}

	// fstest.MapFS adalah filesystem virtual dalam memori.
	peta := fstest.MapFS{
		"proyek/README.md":    {Data: []byte("demo fstest\n")},
		"proyek/main.go":      {Data: []byte("package main\n")},
		"proyek/util/util.go": {Data: []byte("package util\n")},
	}
	fmt.Println("pohon di fstest.MapFS:")
	err = fs.WalkDir(peta, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if path == "." {
			return nil
		}
		jenis := "berkas"
		if d.IsDir() {
			jenis = "direktori"
		}
		fmt.Printf("  %-30s %s\n", path, jenis)
		return nil
	})
	if err != nil {
		fmt.Println("gagal menelusuri pohon:", err)
		return
	}
}

// buatBerkas membuat berkas kecil untuk keperluan demo.
func buatBerkas(path, isi string) error {
	return os.WriteFile(path, []byte(isi), 0644)
}

Jalankan program dari direktori chapter dengan perintah berikut:

bash
go run contoh.go

Output yang Dihasilkan:

text
isi direktori utama:
  baca-saya.txt  berkas
  dokumen        direktori
Stat catatan.txt : 15 byte, isDir=false
Stat  pintasan  : 15 byte (mengikuti tautan)
Lstat pintasan  : symlink=true
setelah Remove  : true
pohon di disk:
  dokumen                        direktori
  dokumen/catatan.txt            berkas
  dokumen/laporan                direktori
  dokumen/laporan/penjualan.txt  berkas
  pintasan.txt                   berkas
pohon di fstest.MapFS:
  proyek                         direktori
  proyek/README.md               berkas
  proyek/main.go                 berkas
  proyek/util                    direktori
  proyek/util/util.go            berkas

Mari kita telaah baris demi baris program di atas:

  1. Baris 22-37 (MkdirAll + buatBerkas): satu panggilan MkdirAll membuat dokumen/laporan dua level sekaligus dengan permission 0755; tiga berkas uji lalu diisi lewat helper buatBerkas yang membungkus os.WriteFile.
  2. Baris 41-52 (os.ReadDir): hasil []os.DirEntry terurut nama (baca-saya.txt sebelum dokumen), dan e.IsDir() membedakan jenis tanpa membaca metadata tambahan.
  3. Baris 55-61 (os.Stat): metadata catatan.txt terbaca 15 byte dan bukan direktori; ModTime sengaja tidak dicetak agar output stabil antar run.
  4. Baris 64-79 (Symlink + Stat vs Lstat): Stat pada pintasan.txt melaporkan 15 byte milik berkas tujuan karena tautan diikuti; Lstat pada entri yang sama melaporkan bit fs.ModeSymlink karena tautannya sendiri yang diperiksa.
  5. Baris 82-87 (os.Remove): berkas dihapus, lalu os.Stat pada path yang sama gagal — errors.Is(err, fs.ErrNotExist) mengembalikan true.
  6. Baris 89-113 dan 116-133 (kedua WalkDir): penelusuran disk mencetak path relatif (lewat filepath.Rel) agar path sementara acak tidak muncul di output; penelusuran fstest.MapFS memakai fs.WalkDir generik pada antarmuka fs.FS.

Struktur direktori kini bisa dibangun, dijelajahi, dan dibongkar. Satu properti entri yang tadi hanya lewat di permukaan — info.Mode() dengan notasi -rw-r--r-- — menyimpan seluruh aturan siapa boleh melakukan apa terhadap sebuah berkas. Chapter File Permissions membedah notasi oktal tersebut sampai tuntas.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.go

Unduh berkas
go
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"testing/fstest"
)

func main() {
	// Direktori kerja sementara agar program aman dijalankan berulang.
	dir, err := os.MkdirTemp("", "contoh-dir-*")
	if err != nil {
		fmt.Println("gagal membuat direktori:", err)
		return
	}
	defer os.RemoveAll(dir)

	// Struktur uji: MkdirAll membuat seluruh jalur bersarang sekaligus.
	if err := os.MkdirAll(filepath.Join(dir, "dokumen", "laporan"), 0755); err != nil {
		fmt.Println("gagal membuat direktori:", err)
		return
	}
	if err := buatBerkas(filepath.Join(dir, "baca-saya.txt"), "mulai dari sini\n"); err != nil {
		fmt.Println("gagal membuat berkas:", err)
		return
	}
	if err := buatBerkas(filepath.Join(dir, "dokumen", "catatan.txt"), "catatan harian\n"); err != nil {
		fmt.Println("gagal membuat berkas:", err)
		return
	}
	if err := buatBerkas(filepath.Join(dir, "dokumen", "laporan", "penjualan.txt"), "omzet: 1000000\n"); err != nil {
		fmt.Println("gagal membuat berkas:", err)
		return
	}

	// os.ReadDir mengembalikan daftar entri terurut berdasarkan nama.
	fmt.Println("isi direktori utama:")
	entries, err := os.ReadDir(dir)
	if err != nil {
		fmt.Println("gagal membaca direktori:", err)
		return
	}
	for _, e := range entries {
		jenis := "berkas"
		if e.IsDir() {
			jenis = "direktori"
		}
		fmt.Printf("  %-14s %s\n", e.Name(), jenis)
	}

	// os.Stat memberikan metadata satu entri.
	target := filepath.Join(dir, "dokumen", "catatan.txt")
	info, err := os.Stat(target)
	if err != nil {
		fmt.Println("gagal membaca metadata:", err)
		return
	}
	fmt.Printf("Stat catatan.txt : %d byte, isDir=%v\n", info.Size(), info.IsDir())

	// Stat mengikuti tautan simbolik; Lstat melaporkan tautannya sendiri.
	if err := os.Symlink(target, filepath.Join(dir, "pintasan.txt")); err != nil {
		fmt.Println("gagal membuat tautan:", err)
		return
	}
	infoStat, err := os.Stat(filepath.Join(dir, "pintasan.txt"))
	if err != nil {
		fmt.Println("gagal membaca metadata:", err)
		return
	}
	infoLstat, err := os.Lstat(filepath.Join(dir, "pintasan.txt"))
	if err != nil {
		fmt.Println("gagal membaca metadata:", err)
		return
	}
	fmt.Printf("Stat  pintasan  : %d byte (mengikuti tautan)\n", infoStat.Size())
	fmt.Printf("Lstat pintasan  : symlink=%v\n", infoLstat.Mode()&fs.ModeSymlink != 0)

	// os.Remove menghapus satu entri.
	if err := os.Remove(filepath.Join(dir, "baca-saya.txt")); err != nil {
		fmt.Println("gagal menghapus berkas:", err)
		return
	}
	_, err = os.Stat(filepath.Join(dir, "baca-saya.txt"))
	fmt.Println("setelah Remove  :", errors.Is(err, fs.ErrNotExist))

	// filepath.WalkDir menelusuri seluruh pohon secara leksikal.
	fmt.Println("pohon di disk:")
	err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(dir, path)
		if err != nil {
			return err
		}
		if rel == "." {
			return nil
		}
		jenis := "berkas"
		if d.IsDir() {
			jenis = "direktori"
		}
		fmt.Printf("  %-30s %s\n", rel, jenis)
		return nil
	})
	if err != nil {
		fmt.Println("gagal menelusuri pohon:", err)
		return
	}

	// fstest.MapFS adalah filesystem virtual dalam memori.
	peta := fstest.MapFS{
		"proyek/README.md":    {Data: []byte("demo fstest\n")},
		"proyek/main.go":      {Data: []byte("package main\n")},
		"proyek/util/util.go": {Data: []byte("package util\n")},
	}
	fmt.Println("pohon di fstest.MapFS:")
	err = fs.WalkDir(peta, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if path == "." {
			return nil
		}
		jenis := "berkas"
		if d.IsDir() {
			jenis = "direktori"
		}
		fmt.Printf("  %-30s %s\n", path, jenis)
		return nil
	})
	if err != nil {
		fmt.Println("gagal menelusuri pohon:", err)
		return
	}
}

// buatBerkas membuat berkas kecil untuk keperluan demo.
func buatBerkas(path, isi string) error {
	return os.WriteFile(path, []byte(isi), 0644)
}