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