Go File & Data

Reading and writing files, streaming with io.Reader/Writer, JSON encoding, and working with paths.

os.ReadFile bufio.Scanner io.Reader io.Writer JSON encode JSON decode json tags filepath
๐Ÿ“–

Reading Files

  Choose the right read method:

  Small file (fits in memory)  โ†’  os.ReadFile โ€” one call, whole file as []byte
  Read line by line            โ†’  bufio.Scanner โ€” streams, low memory
  Random access / large file   โ†’  os.Open + manual Read / io.ReadAll
  Embed at compile time        โ†’  //go:embed directive
os.ReadFile โ€” whole file at once ReadFile
// Reads entire file into []byte in one call
data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("read config: %w", err)
}
fmt.Println(string(data))

// os.ReadFile is equivalent to:
// f, _ := os.Open(name)
// defer f.Close()
// io.ReadAll(f)
bufio.Scanner โ€” line by line Scanner
f, err := os.Open("data.txt")
if err != nil {
    return err
}
defer f.Close()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
    line := scanner.Text()  // one line, no trailing \n
    fmt.Println(line)
}
if err := scanner.Err(); err != nil {
    return err
}
os.Open vs os.OpenFile โ€” open modes OpenFile
// os.Open โ€” read-only shorthand
f, err := os.Open("file.txt")

// os.OpenFile โ€” full control over flags and permissions
f, err = os.OpenFile("file.txt",
    os.O_RDWR|os.O_CREATE|os.O_TRUNC,
    0644,   // permission bits: owner rw, group r, other r
)

// Common flag combinations:
// O_RDONLY                      โ€” read-only (default for os.Open)
// O_WRONLY | O_CREATE | O_TRUNC โ€” create/overwrite for writing
// O_WRONLY | O_CREATE | O_APPENDโ€” open or create, append only
// O_RDWR                        โ€” read and write
๐Ÿ’ก
Always defer f.Close() immediately after a successful open โ€” before any other error-return path โ€” so the file is closed even if subsequent code returns early.
โœ๏ธ

Writing Files

os.WriteFile โ€” write all at once WriteFile
// Creates or overwrites the file atomically
data := []byte("hello, file\n")
err := os.WriteFile("out.txt", data, 0644)
if err != nil {
    return err
}

// Write a string directly
err = os.WriteFile("out.txt",
    []byte("content"), 0644)
bufio.Writer โ€” buffered writes bufio.Writer
f, err := os.Create("out.txt")  // create or truncate
if err != nil {
    return err
}
defer f.Close()

w := bufio.NewWriter(f)
fmt.Fprintln(w, "line 1")
fmt.Fprintln(w, "line 2")

// Must flush โ€” buffered writes are not sent until Flush()
if err := w.Flush(); err != nil {
    return err
}
โš ๏ธ
When using bufio.Writer, always call Flush() before closing the file. Data sitting in the buffer is silently discarded if you only call Close() โ€” the file ends up truncated with no error returned.
Append to an existing file O_APPEND
f, err := os.OpenFile("log.txt",
    os.O_WRONLY|os.O_CREATE|os.O_APPEND,
    0644,
)
if err != nil {
    return err
}
defer f.Close()

fmt.Fprintf(f, "[%s] event happened\n", time.Now().Format(time.RFC3339))
๐Ÿ”Œ

io.Reader & io.Writer

โ„น๏ธ
io.Reader and io.Writer are the two most important interfaces in Go's standard library. Any type that implements Read([]byte) or Write([]byte) fits โ€” files, network connections, bytes.Buffer, strings.Reader all compose freely.
  io.Reader                          io.Writer
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€      โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  Read(p []byte) (n int, err error)  Write(p []byte) (n int, err error)

  Implementors: *os.File, *bytes.Buffer, *strings.Reader,
                *bufio.Reader, net.Conn, http.Request.Body, โ€ฆ

  io.Copy(dst Writer, src Reader)  โ€” streams src โ†’ dst without loading all in memory
io.Copy โ€” stream between any Reader/Writer io.Copy
// Copy a file efficiently (32KB chunks internally)
src, _ := os.Open("input.txt")
defer src.Close()

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

n, err := io.Copy(dst, src)
fmt.Printf("copied %d bytes\n", n)

// io.CopyN โ€” copy at most N bytes
io.CopyN(dst, src, 1024)
bytes.Buffer & strings.Reader In-memory
// bytes.Buffer โ€” in-memory Reader and Writer
var buf bytes.Buffer
fmt.Fprintf(&buf, "hello %s", "world")
fmt.Println(buf.String())  // hello world

// strings.NewReader โ€” turn a string into io.Reader
r := strings.NewReader("some data")
io.Copy(os.Stdout, r)

// io.ReadAll โ€” drain a Reader into []byte
data, err := io.ReadAll(r)
io.MultiWriter & io.TeeReader Fan-out
// MultiWriter โ€” write to multiple destinations simultaneously
logFile, _ := os.Create("app.log")
defer logFile.Close()

mw := io.MultiWriter(os.Stdout, logFile)
fmt.Fprintln(mw, "written to stdout and file")

// TeeReader โ€” read from src, simultaneously write to dst
// Useful for logging/inspecting data passing through a pipeline
var buf bytes.Buffer
tee := io.TeeReader(src, &buf)  // every read from tee also writes to buf
json.NewDecoder(tee).Decode(&v)
fmt.Println("raw payload:", buf.String())
๐Ÿ“ค

JSON Encoding

json tags โ€” control serialization struct tags
type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email,omitempty"`  // omit if empty string
    Password  string `json:"-"`               // always omit
    CreatedAt time.Time `json:"created_at"`
}

// omitempty omits: false, 0, nil pointer, nil slice/map, ""
// It does NOT omit a zero time.Time โ€” use *time.Time for that
json.Marshal โ€” to bytes Marshal
u := User{ID: 1, Name: "Alice"}

// Compact JSON
data, err := json.Marshal(u)
// {"id":1,"name":"Alice","created_at":"0001-..."}

// Indented JSON for human readability
data, err = json.MarshalIndent(u, "", "  ")
if err != nil {
    return err
}
fmt.Println(string(data))
json.Encoder โ€” stream to Writer Encoder
// Prefer Encoder when writing to http.ResponseWriter,
// os.File, or any io.Writer โ€” avoids allocating []byte
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "  ")

users := []User{{ID: 1, Name: "Alice"}}
if err := enc.Encode(users); err != nil {
    return err
}
// Encode appends a trailing newline automatically
๐Ÿ’ก
Use json.Encoder when writing to an http.ResponseWriter or file. Use json.Marshal when you need a []byte to store, sign, or pass to another function. Encoder is more memory-efficient for large payloads.
๐Ÿ“ฅ

JSON Decoding

json.Unmarshal โ€” from bytes Unmarshal
payload := []byte(`{"id":1,"name":"Alice"}`)

var u User
if err := json.Unmarshal(payload, &u); err != nil {
    return fmt.Errorf("decode user: %w", err)
}
fmt.Println(u.Name)  // Alice

// Unmarshal unknown structure into map
var m map[string]any
json.Unmarshal(payload, &m)
fmt.Println(m["name"])  // Alice
json.Decoder โ€” stream from Reader Decoder
// Decode from an HTTP response body or file
resp, _ := http.Get("https://api.example.com/user")
defer resp.Body.Close()

var u User
if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
    return err
}

// Reject unknown fields to catch typos in clients
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
dec.Decode(&u)
Custom MarshalJSON / UnmarshalJSON Custom
type Duration struct{ time.Duration }

// Encode as a human-readable string: "1h30m"
func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.Duration.String())
}

// Decode from a human-readable string
func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    dur, err := time.ParseDuration(s)
    d.Duration = dur
    return err
}
โš ๏ธ
JSON numbers decode to float64 by default when unmarshalling into map[string]any. A large integer like a database ID can lose precision. Use json.Decoder with d.UseNumber() to get a json.Number instead, then call .Int64() on it.
๐Ÿ“‚

File Paths & Directories

filepath โ€” portable path manipulation filepath
// Join โ€” OS-correct separator (/ on Unix, \ on Windows)
p := filepath.Join("data", "2024", "report.csv")
// "data/2024/report.csv"

filepath.Dir(p)   // "data/2024"
filepath.Base(p)  // "report.csv"
filepath.Ext(p)   // ".csv"

// Abs โ€” resolve relative path to absolute
abs, err := filepath.Abs("./config.yaml")

// Clean โ€” normalize ../ and //
filepath.Clean("a/../b//c")  // "b/c"
Creating & removing directories MkdirAll
// Create a single directory
os.Mkdir("output", 0755)

// Create all intermediate directories
os.MkdirAll("output/2024/reports", 0755)

// Remove a file
os.Remove("tmp.txt")

// Remove directory and all contents
os.RemoveAll("output")

// Rename (also works as move)
os.Rename("old.txt", "new.txt")
fs.WalkDir โ€” traverse a directory tree WalkDir
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    if err != nil {
        return err  // permission error accessing this entry
    }
    if d.IsDir() && d.Name() == ".git" {
        return filepath.SkipDir  // skip entire subtree
    }
    if filepath.Ext(path) == ".go" {
        fmt.Println(path)
    }
    return nil
})

// d.Type()     โ€” FileMode (file, dir, symlink)
// d.Info()     โ€” fs.FileInfo (size, modtime, etc.)
// d.IsDir()    โ€” shorthand for d.Type().IsDir()
๐Ÿ“ฆ

Embedding Files

โ„น๏ธ
The //go:embed directive (Go 1.16+) bakes files into the binary at compile time. No runtime file reads, no deployment packaging โ€” the binary is self-contained.
Embed as string, []byte, or embed.FS //go:embed
import _ "embed"  // blank import required when embedding into var

// Embed a single file as string
//go:embed version.txt
var version string

// Embed a single file as []byte
//go:embed default_config.json
var defaultConfig []byte

// Embed a whole directory (or multiple files) as embed.FS
//go:embed templates/*
var templates embed.FS

func main() {
    fmt.Println(version)

    data, _ := templates.ReadFile("templates/index.html")
    fmt.Println(string(data))
}
embed.FS โ€” serve HTTP from binary embed.FS
//go:embed static/*
var staticFiles embed.FS

// Serve embedded files over HTTP
sub, _ := fs.Sub(staticFiles, "static")
http.Handle("/static/",
    http.StripPrefix("/static/",
        http.FileServer(http.FS(sub)),
    ),
)
Glob patterns in //go:embed patterns
// Multiple files or patterns on separate lines
//go:embed migrations/*.sql
//go:embed config/defaults.yaml
var assets embed.FS

// List embedded files
entries, _ := assets.ReadDir("migrations")
for _, e := range entries {
    fmt.Println(e.Name())
}

// Note: files beginning with . or _ are excluded
// unless the pattern explicitly names them.
๐Ÿ“‹

Quick Reference

Concept Call Notes
Read whole fileos.ReadFile(path)Returns []byte; small files only
Read line by linebufio.NewScanner(f).Scan()Check scanner.Err() after loop
Open read-onlyos.Open(path)Shorthand for O_RDONLY
Open with flagsos.OpenFile(path, flags, perm)O_CREATE, O_TRUNC, O_APPENDโ€ฆ
Write whole fileos.WriteFile(path, data, 0644)Creates or overwrites
Create fileos.Create(path)Equivalent to OpenFile O_WRONLY|O_CREATE|O_TRUNC
Buffered writebufio.NewWriter(f)Call Flush() before Close()
Stream copyio.Copy(dst, src)Any Reader โ†’ any Writer; 32KB chunks
Drain readerio.ReadAll(r)Returns []byte
In-memory bufferbytes.BufferImplements both Reader and Writer
String as Readerstrings.NewReader(s)Lightweight, no allocation
Multi-destinationio.MultiWriter(w1, w2, โ€ฆ)Writes to all simultaneously
Tap a streamio.TeeReader(r, w)Reads from r and copies to w
Marshal to bytesjson.Marshal(v)Returns []byte
Marshal to writerjson.NewEncoder(w).Encode(v)More efficient for HTTP/file
Unmarshal bytesjson.Unmarshal(b, &v)v must be a pointer
Decode from readerjson.NewDecoder(r).Decode(&v)Use for HTTP bodies
Reject unknown fieldsdec.DisallowUnknownFields()Useful for strict APIs
Join pathfilepath.Join(partsโ€ฆ)OS-correct separator
Dir / Base / Extfilepath.Dir/Base/Ext(p)Path components
Walk directoryfilepath.WalkDir(root, fn)Return filepath.SkipDir to skip subtree
Create dirsos.MkdirAll(path, 0755)No error if already exists
Embed file (string)//go:embed file.txt + var s stringBaked in at compile time
Embed dir//go:embed dir/* + var f embed.FSUse f.ReadFile / f.ReadDir