V
Vel·ToolKit
Simple · Fast · Ready to use
EN
Chapter 18 of 20

File & IO

os / bufio / the io interface family, permissions, temp files, atomic write

Reading/Writing a Whole File at Once

For small files up to a few MB, ReadFile / WriteFile is simplest — Go handles the whole Open / Read / Close flow for you.

package main

import (
    "log"
    "os"
)

func main() {
    if err := os.WriteFile("out.txt", []byte("hello\n"), 0o644); err != nil {
        log.Fatal(err)
    }
    data, err := os.ReadFile("out.txt")
    if err != nil {
        log.Fatal(err)
    }
    os.Stdout.Write(data)
}

os.OpenFile and Permission Bits

When you need append, control over read/write mode, or custom permissions, use os.OpenFile. The most common Linux permission bits: 0o644 (owner read/write, others read-only), 0o600 (owner read/write only), 0o755 (directories or executables). On Windows permission bits are ignored; 0o644 is fine for writing files.

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    f, err := os.OpenFile("app.log",
        os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    fmt.Fprintln(f, "boot ok")

    // common flag combinations:
    //   O_RDONLY | O_RDWR | O_WRONLY  access mode
    //   O_CREATE create if it does not exist
    //   O_TRUNC  truncate if it exists
    //   O_APPEND append on write
    //   O_EXCL   with O_CREATE, error if it exists (prevent overwrite)
}

Reading a Large File Line by Line

bufio.Scanner holds only one line at a time, ideal for scanning hundreds of MB of logs. The default line limit is 64 KB; longer lines need Buffer to expand it.

package main

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

func main() {
    f, err := os.Open("big.log")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    sc := bufio.NewScanner(f)
    sc.Buffer(make([]byte, 1024*1024), 16*1024*1024) // 16 MB per-line limit
    for sc.Scan() {
        line := sc.Text()
        fmt.Println(line)
    }
    if err := sc.Err(); err != nil {
        log.Fatal(err)
    }
}

The io.Reader / io.Writer Family

Go abstracts IO with interfaces: anything readable implements io.Reader, anything writable implements io.Writer. The standard library's core IO functions take these two interfaces, so the same code handles files, network, memory, and compressed streams.

package main

import (
    "fmt"
    "io"
    "log"
    "os"
    "strings"
)

func main() {
    // prepare a source file
    _ = os.WriteFile("a.bin", []byte("hello world"), 0o644)

    src, err := os.Open("a.bin")
    if err != nil {
        log.Fatal(err)
    }
    defer src.Close()
    dst, err := os.Create("b.bin")
    if err != nil {
        log.Fatal(err)
    }
    defer dst.Close()

    n, _ := io.Copy(dst, src)
    fmt.Println("copied", n)

    // make a Reader in memory
    r := strings.NewReader("hello")
    buf, _ := io.ReadAll(r)
    fmt.Println(string(buf))

    // write to file + terminal at once
    mw := io.MultiWriter(dst, os.Stdout)
    fmt.Fprintln(mw, "broadcast")

    // rate-limited reading
    lr := io.LimitReader(strings.NewReader(strings.Repeat("x", 100)), 10)
    out, _ := io.ReadAll(lr)
    fmt.Println(len(out)) // 10
}

Buffering and Flush

Frequent small writes should be wrapped in a bufio.Writer, collapsing hundreds of system calls into a few. Be sure to Flush when done — otherwise buffered data may not reach disk.

package main

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

func main() {
    f, err := os.Create("out.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    bw := bufio.NewWriter(f)
    defer bw.Flush() // important: without Flush, data may not reach disk

    for i := 0; i < 10000; i++ {
        fmt.Fprintln(bw, "line", i)
    }
}

Temp Files and Temp Directories

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    // temp file with a random suffix in the name
    f, err := os.CreateTemp("", "upload-*.bin")
    if err != nil {
        log.Fatal(err)
    }
    defer os.Remove(f.Name())
    defer f.Close()

    // temp directory
    dir, err := os.MkdirTemp("", "build-")
    if err != nil {
        log.Fatal(err)
    }
    defer os.RemoveAll(dir)

    fmt.Println(f.Name(), dir)
}

Atomic Write: Write a Temp File, Then rename

Overwriting the target file directly can leave a half-written file if the process crashes midway. The standard approach is to write to a temp file in the same directory, flush, then rename — a same-partition rename is atomic on most filesystems.

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func atomicWrite(path string, data []byte) error {
    dir := filepath.Dir(path)
    f, err := os.CreateTemp(dir, ".tmp-*")
    if err != nil {
        return err
    }
    tmp := f.Name()
    defer os.Remove(tmp) // clean up on failure

    if _, err := f.Write(data); err != nil {
        f.Close()
        return err
    }
    if err := f.Sync(); err != nil { // flush to disk
        f.Close()
        return err
    }
    if err := f.Close(); err != nil {
        return err
    }
    return os.Rename(tmp, path)
}

func main() {
    if err := atomicWrite("config.json", []byte(`{"v":1}`)); err != nil {
        fmt.Println("err:", err)
        return
    }
    fmt.Println("written safely")
}

path/filepath and Directory Traversal

filepath automatically adapts to Windows / POSIX path separators. WalkDir (Go 1.16+) is faster than the early Walk because it reuses directory-entry metadata instead of Stat-ing every file again.

package main

import (
    "fmt"
    "io/fs"
    "log"
    "path/filepath"
)

func main() {
    p := filepath.Join("data", "users", "list.json")
    fmt.Println(filepath.Dir(p), filepath.Base(p), filepath.Ext(p))

    abs, _ := filepath.Abs(p)
    fmt.Println(abs)
    fmt.Println(filepath.Clean("a/./b/../c")) // a/c

    matches, _ := filepath.Glob("*.go")
    fmt.Println(matches)

    err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if d.IsDir() && d.Name() == ".git" {
            return filepath.SkipDir
        }
        if !d.IsDir() {
            fmt.Println(path)
        }
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }
}

Directory Management and Existence Checks

package main

import (
    "errors"
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    if err := os.MkdirAll("data/cache", 0o755); err != nil {
        log.Fatal(err)
    }

    path := "data/cache"
    if info, err := os.Stat(path); err == nil {
        fmt.Println("size", info.Size(), "mode", info.Mode())
    } else if errors.Is(err, fs.ErrNotExist) {
        fmt.Println("missing")
    }

    // rename / move / delete
    _ = os.WriteFile("a.txt", []byte("x"), 0o644)
    _ = os.Rename("a.txt", "b.txt")
    _ = os.Remove("b.txt")
    _ = os.RemoveAll("data/cache") // recursively delete the whole directory
}

embed: Compile Files into the Binary

Go 1.16+ uses the //go:embed directive to bundle static assets into the executable. Very handy when deploying a single binary.

package main

import (
    "embed"
    "io/fs"
    "log"
    "net/http"
)

//go:embed assets/* templates/*.html
var staticFS embed.FS

func main() {
    sub, _ := fs.Sub(staticFS, "assets")
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
    log.Fatal(http.ListenAndServe(":8080", nil))
}