Skip to main content
โšก Calmops

File System Operations and Manipulation in Go

File System Operations and Manipulation in Go

Introduction

File system operations are fundamental to CLI applications. Go provides powerful packages for reading, writing, and manipulating files and directories. This guide covers comprehensive file system operations.

Proper file handling ensures your applications are robust, efficient, and handle edge cases gracefully.

File Reading and Writing

Basic File Operations

package main

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

// ReadFile reads entire file into memory
func ReadFile(filename string) ([]byte, error) {
	data, err := os.ReadFile(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}
	return data, nil
}

// WriteFile writes data to file
func WriteFile(filename string, data []byte) error {
	if err := os.WriteFile(filename, data, 0644); err != nil {
		return fmt.Errorf("failed to write file: %w", err)
	}
	return nil
}

// AppendToFile appends data to file
func AppendToFile(filename string, data []byte) error {
	file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	if _, err := file.Write(data); err != nil {
		return fmt.Errorf("failed to write to file: %w", err)
	}

	return nil
}

// CopyFile copies a file
func CopyFile(src, dst string) error {
	source, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("failed to open source: %w", err)
	}
	defer source.Close()

	destination, err := os.Create(dst)
	if err != nil {
		return fmt.Errorf("failed to create destination: %w", err)
	}
	defer destination.Close()

	if _, err := io.Copy(destination, source); err != nil {
		return fmt.Errorf("failed to copy: %w", err)
	}

	return nil
}

Good: Proper File System Implementation

package main

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

// FileManager handles file operations
type FileManager struct {
	basePath string
}

// NewFileManager creates a new file manager
func NewFileManager(basePath string) *FileManager {
	return &FileManager{
		basePath: basePath,
	}
}

// ReadLines reads file line by line
func (fm *FileManager) ReadLines(filename string) ([]string, error) {
	path := filepath.Join(fm.basePath, filename)

	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	var lines []string
	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}

	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("scanner error: %w", err)
	}

	return lines, nil
}

// WriteLines writes lines to file
func (fm *FileManager) WriteLines(filename string, lines []string) error {
	path := filepath.Join(fm.basePath, filename)

	file, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("failed to create file: %w", err)
	}
	defer file.Close()

	writer := bufio.NewWriter(file)
	for _, line := range lines {
		if _, err := writer.WriteString(line + "\n"); err != nil {
			return fmt.Errorf("failed to write line: %w", err)
		}
	}

	if err := writer.Flush(); err != nil {
		return fmt.Errorf("failed to flush: %w", err)
	}

	return nil
}

// ListFiles lists files in directory
func (fm *FileManager) ListFiles(dir string) ([]string, error) {
	path := filepath.Join(fm.basePath, dir)

	entries, err := os.ReadDir(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read directory: %w", err)
	}

	var files []string
	for _, entry := range entries {
		if !entry.IsDir() {
			files = append(files, entry.Name())
		}
	}

	return files, nil
}

// ListDirectories lists directories
func (fm *FileManager) ListDirectories(dir string) ([]string, error) {
	path := filepath.Join(fm.basePath, dir)

	entries, err := os.ReadDir(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read directory: %w", err)
	}

	var dirs []string
	for _, entry := range entries {
		if entry.IsDir() {
			dirs = append(dirs, entry.Name())
		}
	}

	return dirs, nil
}

// WalkDirectory walks directory tree
func (fm *FileManager) WalkDirectory(dir string, callback func(path string, info fs.FileInfo) error) error {
	path := filepath.Join(fm.basePath, dir)

	return filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return err
		}

		return callback(path, info)
	})
}

// CreateDirectory creates a directory
func (fm *FileManager) CreateDirectory(dir string) error {
	path := filepath.Join(fm.basePath, dir)

	if err := os.MkdirAll(path, 0755); err != nil {
		return fmt.Errorf("failed to create directory: %w", err)
	}

	return nil
}

// DeleteFile deletes a file
func (fm *FileManager) DeleteFile(filename string) error {
	path := filepath.Join(fm.basePath, filename)

	if err := os.Remove(path); err != nil {
		return fmt.Errorf("failed to delete file: %w", err)
	}

	return nil
}

// DeleteDirectory deletes a directory
func (fm *FileManager) DeleteDirectory(dir string) error {
	path := filepath.Join(fm.basePath, dir)

	if err := os.RemoveAll(path); err != nil {
		return fmt.Errorf("failed to delete directory: %w", err)
	}

	return nil
}

// FileExists checks if file exists
func (fm *FileManager) FileExists(filename string) bool {
	path := filepath.Join(fm.basePath, filename)
	_, err := os.Stat(path)
	return err == nil
}

// GetFileInfo gets file information
func (fm *FileManager) GetFileInfo(filename string) (os.FileInfo, error) {
	path := filepath.Join(fm.basePath, filename)
	return os.Stat(path)
}

// GetFileSize gets file size
func (fm *FileManager) GetFileSize(filename string) (int64, error) {
	info, err := fm.GetFileInfo(filename)
	if err != nil {
		return 0, err
	}
	return info.Size(), nil
}

// ChangePermissions changes file permissions
func (fm *FileManager) ChangePermissions(filename string, perm os.FileMode) error {
	path := filepath.Join(fm.basePath, filename)

	if err := os.Chmod(path, perm); err != nil {
		return fmt.Errorf("failed to change permissions: %w", err)
	}

	return nil
}

Bad: Improper File Operations

package main

import (
	"os"
)

// BAD: No error handling
func BadReadFile(filename string) []byte {
	data, _ := os.ReadFile(filename)
	return data
}

// BAD: No resource cleanup
func BadWriteFile(filename string, data []byte) {
	file, _ := os.Create(filename)
	// Missing defer file.Close()
	file.Write(data)
}

// BAD: No path validation
func BadDeleteFile(filename string) {
	os.Remove(filename)
}

Problems:

  • No error handling
  • No resource cleanup
  • No path validation
  • No permission checks

Advanced File Operations

Temporary Files

package main

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

// CreateTempFile creates a temporary file
func CreateTempFile(pattern string) (*os.File, error) {
	file, err := os.CreateTemp("", pattern)
	if err != nil {
		return nil, fmt.Errorf("failed to create temp file: %w", err)
	}
	return file, nil
}

// CreateTempDirectory creates a temporary directory
func CreateTempDirectory(pattern string) (string, error) {
	dir, err := os.MkdirTemp("", pattern)
	if err != nil {
		return "", fmt.Errorf("failed to create temp directory: %w", err)
	}
	return dir, nil
}

// WithTempFile executes function with temporary file
func WithTempFile(pattern string, fn func(*os.File) error) error {
	file, err := CreateTempFile(pattern)
	if err != nil {
		return err
	}
	defer os.Remove(file.Name())

	return fn(file)
}

File Watching

package main

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

// FileWatcher watches for file changes
type FileWatcher struct {
	filename string
	lastMod  time.Time
	interval time.Duration
}

// NewFileWatcher creates a new file watcher
func NewFileWatcher(filename string, interval time.Duration) *FileWatcher {
	return &FileWatcher{
		filename: filename,
		interval: interval,
	}
}

// Watch watches for file changes
func (fw *FileWatcher) Watch(callback func()) error {
	info, err := os.Stat(fw.filename)
	if err != nil {
		return err
	}

	fw.lastMod = info.ModTime()

	ticker := time.NewTicker(fw.interval)
	defer ticker.Stop()

	for range ticker.C {
		info, err := os.Stat(fw.filename)
		if err != nil {
			return err
		}

		if info.ModTime().After(fw.lastMod) {
			fw.lastMod = info.ModTime()
			callback()
		}
	}

	return nil
}

Atomic File Operations

package main

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

// AtomicWrite performs atomic file write
func AtomicWrite(filename string, data []byte) error {
	// Write to temporary file
	tmpFile, err := os.CreateTemp(filepath.Dir(filename), ".tmp")
	if err != nil {
		return fmt.Errorf("failed to create temp file: %w", err)
	}
	defer os.Remove(tmpFile.Name())

	if _, err := tmpFile.Write(data); err != nil {
		tmpFile.Close()
		return fmt.Errorf("failed to write: %w", err)
	}

	if err := tmpFile.Close(); err != nil {
		return fmt.Errorf("failed to close temp file: %w", err)
	}

	// Atomic rename
	if err := os.Rename(tmpFile.Name(), filename); err != nil {
		return fmt.Errorf("failed to rename: %w", err)
	}

	return nil
}

Best Practices

1. Always Close Files

file, err := os.Open(filename)
if err != nil {
	return err
}
defer file.Close()

2. Use Buffered I/O

// Use bufio for better performance
scanner := bufio.NewScanner(file)

3. Validate Paths

// Validate paths to prevent directory traversal
path := filepath.Join(basePath, userPath)

4. Handle Permissions

// Set appropriate permissions
os.WriteFile(filename, data, 0644)

Common Pitfalls

1. Not Closing Files

Always defer file.Close() to prevent resource leaks.

2. Ignoring Errors

Always check and handle errors.

3. No Path Validation

Validate paths to prevent security issues.

4. Inefficient I/O

Use buffered I/O for better performance.

Resources

Summary

Proper file system operations are essential for CLI tools. Key takeaways:

  • Always close files with defer
  • Use buffered I/O for efficiency
  • Validate paths for security
  • Handle errors properly
  • Use atomic writes for safety
  • Set appropriate permissions
  • Clean up temporary files

By mastering file operations, you can build robust CLI applications.

Comments