main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
package main

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

// FileManager handles common file operations with robust error handling
type FileManager struct {
	baseDir string
}

// NewFileManager creates a new FileManager with a base directory (creates it if missing)
func NewFileManager(baseDir string) (*FileManager, error) {
	// Create base directory if it doesn't exist
	if err := os.MkdirAll(baseDir, 0755); err != nil {
		return nil, fmt.Errorf("failed to create base directory: %w", err)
	}

	return &FileManager{baseDir: baseDir}, nil
}

// WriteFile writes content to a file with atomic write pattern (prevents partial writes)
func (fm *FileManager) WriteFile(filename string, content []byte) error {
	filePath := filepath.Join(fm.baseDir, filename)
	
	// Create temp file in the same directory to avoid cross-filesystem issues
	tempFile, err := os.CreateTemp(fm.baseDir, fmt.Sprintf("%s.tmp", filename))
	if err != nil {
		return fmt.Errorf("failed to create temp file: %w", err)
	}
	tempFileName := tempFile.Name()

	// Ensure temp file is cleaned up if something goes wrong
	defer func() {
		if err != nil {
			os.Remove(tempFileName) // Clean up temp file on error
		}
		tempFile.Close()
	}()

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

	// Sync to disk to ensure data is written
	if err := tempFile.Sync(); err != nil {
		return fmt.Errorf("failed to sync temp file: %w", err)
	}

	// Atomically rename temp file to target file (safe write)
	if err := os.Rename(tempFileName, filePath); err != nil {
		return fmt.Errorf("failed to rename temp file: %w", err)
	}

	// Set proper file permissions
	if err := os.Chmod(filePath, 0644); err != nil {
		return fmt.Errorf("failed to set file permissions: %w", err)
	}

	return nil
}

// ReadFile reads content from a file with existence check
func (fm *FileManager) ReadFile(filename string) ([]byte, error) {
	filePath := filepath.Join(fm.baseDir, filename)

	// Check if file exists and is a regular file
	fileInfo, err := os.Stat(filePath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil, fmt.Errorf("file does not exist: %s", filePath)
		}
		return nil, fmt.Errorf("failed to stat file: %w", err)
	}

	if fileInfo.IsDir() {
		return nil, fmt.Errorf("path is a directory, not a file: %s", filePath)
	}

	// Read entire file content
	content, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}

	return content, nil
}

// DeleteFile safely deletes a file with existence check
func (fm *FileManager) DeleteFile(filename string) error {
	filePath := filepath.Join(fm.baseDir, filename)

	err := os.Remove(filePath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return fmt.Errorf("cannot delete: file does not exist: %s", filePath)
		}
		return fmt.Errorf("failed to delete file: %w", err)
	}

	return nil
}

func main() {
	// Initialize file manager with a "data" directory
	fm, err := NewFileManager("data")
	if err != nil {
		fmt.Printf("Failed to create FileManager: %v\n", err)
		return
	}

	// Write to file
	testContent := []byte("Hello, Go File I/O!\nThis is a test file with multiple lines.")
	if err := fm.WriteFile("test.txt", testContent); err != nil {
		fmt.Printf("Write failed: %v\n", err)
		return
	}
	fmt.Println("Successfully wrote to test.txt")

	// Read from file
	content, err := fm.ReadFile("test.txt")
	if err != nil {
		fmt.Printf("Read failed: %v\n", err)
		return
	}
	fmt.Printf("File content:\n%s\n", content)

	// Delete file
	if err := fm.DeleteFile("test.txt"); err != nil {
		fmt.Printf("Delete failed: %v\n", err)
		return
	}
	fmt.Println("Successfully deleted test.txt")
}

How It Works

Wraps common file operations so writes are crash-safe, reads verify state, and temporary files are cleaned up automatically.

Constructor ensures the base directory exists; WriteFile creates a temp file in the same folder, writes bytes, syncs, renames atomically, and sets mode; ReadFile stats the path to reject missing or directory targets before reading; DeleteFile removes files and surfaces fs.ErrNotExist distinctly.

Key Concepts

  • 1MkdirAll guarantees the working directory exists before any I/O.
  • 2Atomic write path (temp file + Sync + Rename) prevents partial files.
  • 3Deferred cleanup removes temp artifacts on error and always closes descriptors.
  • 4Error wrapping adds context for each failed operation.

When to Use This Pattern

  • Persisting configs or small datasets safely on disk.
  • CLI tools that read/write user files with predictable error messages.
  • Atomic cache or state snapshots for long-running services.
  • Test helpers that need clean temporary storage semantics.

Best Practices

  • Keep temp files on the same filesystem as the target to preserve atomic renames.
  • Call Sync before rename when durability matters.
  • Check for fs.ErrNotExist separately to inform callers correctly.
  • Set permissions explicitly instead of relying on defaults.
Go Version1.16+
Difficultyintermediate
Production ReadyYes
Lines of Code139