Урок 7: Стандартная библиотека — io, os, filepath, bufio

Урок 7. Стандартная библиотека — io, os, filepath, bufio

🔄 Node.js → Go (ключевые аналоги):

📋 Что изучаем

📦 Инициализация проекта

mkdir go-stdlib && cd go-stdlib
go mod init go-stdlib

# Создадим тестовые файлы для демонстрации
echo "Первая строка" > test.txt
echo "Вторая строка" >> test.txt
echo "Третья строка" >> test.txt

mkdir -p subdir
echo "Вложенный файл" > subdir/nested.txt

mkdir -p data
echo '{"name": "Gopher", "age": 13}' > data/config.json

💻 Код программы

package main

import (
    "bufio"      // Буферизированное чтение/запись
    "embed"      // Встраивание файлов в бинарник (Go 1.16+)
    "encoding/json"
    "fmt"
    "io"         // Базовые интерфейсы: Reader, Writer, Closer
    "io/fs"      // Интерфейс файловой системы
    "os"         // Работа с ОС: файлы, окружение, аргументы
    "path/filepath" // Манипуляции с путями (кроссплатформенно)
    "strings"
    "time"
)

// ╔══════════════════════════════════════════════════════════╗
// ║  1. EMBED — встроенные файлы                            ║
// ╚══════════════════════════════════════════════════════════╝

//go:embed data/config.json
var configFile embed.FS // Встраивает файл в бинарник при компиляции

//go:embed data/*
var dataDir embed.FS // Встраивает всю директорию

// ╔══════════════════════════════════════════════════════════╗
// ║  2. ПОЛЬЗОВАТЕЛЬСКИЙ ТИП, РЕАЛИЗУЮЩИЙ io.Writer       ║
// ╚══════════════════════════════════════════════════════════╝

// UpperCaseWriter — пишет данные в верхнем регистре.
// Демонстрация композиции через embedding io.Writer.
type UpperCaseWriter struct {
    writer io.Writer // Оборачиваем любой Writer
}

// Write — реализация интерфейса io.Writer
func (w *UpperCaseWriter) Write(p []byte) (n int, err error) {
    upper := strings.ToUpper(string(p))
    return w.writer.Write([]byte(upper))
}

// ╔══════════════════════════════════════════════════════════╗
// ║  3. ДЕМОНСТРАЦИОННЫЕ ФУНКЦИИ                           ║
// ╚══════════════════════════════════════════════════════════╝

// demoIOReadersWriters — базовые операции io.Reader/Writer
func demoIOReadersWriters() {
    fmt.Println("── 1. IO.READER / IO.WRITER ──")

    // io.Reader — интерфейс с одним методом:
    //   Read(p []byte) (n int, err error)
    // Всё, что можно читать: файлы, сеть, строки, буферы.

    // io.Writer — интерфейс с одним методом:
    //   Write(p []byte) (n int, err error)
    // Всё, куда можно писать: файлы, сеть, буферы, stdout.

    // strings.NewReader создаёт Reader из строки
    reader := strings.NewReader("Hello, io.Reader!\nThis is a test.\n")

    // io.Copy — копирует из Reader в Writer (аналог pipe)
    written, err := io.Copy(os.Stdout, reader)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Copy error: %v\n", err)
    }
    fmt.Printf("(скопировано %d байт)\n\n", written)

    // io.TeeReader — читает и попутно пишет (как tee в Unix)
    fmt.Println("TeeReader (читаем + пишем в буфер):")
    r := strings.NewReader("Hello, TeeReader!")
    var buf strings.Builder // Реализует io.Writer
    tee := io.TeeReader(r, &buf)
    data, _ := io.ReadAll(tee)
    fmt.Printf("  Прочитано: %s\n", data)
    fmt.Printf("  В буфере: %s\n", buf.String())

    // io.MultiWriter — пишет в несколько Writer одновременно
    fmt.Println("\nMultiWriter:")
    var buf1, buf2 strings.Builder
    mw := io.MultiWriter(&buf1, &buf2, os.Stdout)
    fmt.Fprintln(mw, "Пишем в три места сразу!")
    fmt.Printf("  buf1: %q\n", buf1.String())
    fmt.Printf("  buf2: %q\n", buf2.String())

    // io.LimitReader — ограничивает количество читаемых байт
    fmt.Println("\nLimitReader (читаем только 10 байт):")
    lr := io.LimitReader(strings.NewReader("Очень длинная строка для чтения"), 10)
    limited, _ := io.ReadAll(lr)
    fmt.Printf("  %q\n", limited)
}

// demoOSFiles — работа с файловой системой
func demoOSFiles() {
    fmt.Println("── 2. OS: ФАЙЛЫ ──")

    // === os.ReadFile — прочитать файл целиком (аналог fs.readFileSync) ===
    data, err := os.ReadFile("test.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка чтения: %v\n", err)
    } else {
        fmt.Printf("  Содержимое test.txt:\n%s\n", string(data))
    }

    // === os.WriteFile — записать файл целиком ===
    content := []byte(fmt.Sprintf("Записано в %s\n", time.Now().Format(time.RFC3339)))
    err = os.WriteFile("output.txt", content, 0644) // 0644 — права доступа
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка записи: %v\n", err)
    } else {
        fmt.Println("  ✓ output.txt записан")
    }

    // === os.Open + bufio.Scanner — построчное чтение (потоково) ===
    fmt.Println("\n  Построчное чтение (bufio.Scanner):")
    file, err := os.Open("test.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка открытия: %v\n", err)
        return
    }
    // defer гарантирует закрытие файла при выходе из функции
    // даже если произойдёт паника или return
    defer file.Close()

    scanner := bufio.NewScanner(file)
    lineNum := 0
    for scanner.Scan() {
        lineNum++
        fmt.Printf("    %2d: %s\n", lineNum, scanner.Text())
    }
    // Всегда проверяем ошибки сканера
    if err := scanner.Err(); err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка сканирования: %v\n", err)
    }

    // === os.Create — создание файла (обрезает существующий) ===
    f, err := os.Create("created.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка создания: %v\n", err)
    } else {
        defer f.Close()
        fmt.Fprintf(f, "Файл создан через os.Create\n")
        fmt.Println("  ✓ created.txt создан")
    }

    // === os.OpenFile — открытие с флагами (append, create, etc.) ===
    af, err := os.OpenFile("appended.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка открытия: %v\n", err)
    } else {
        defer af.Close()
        fmt.Fprintf(af, "[%s] Новая запись в лог\n", time.Now().Format(time.RFC3339))
        fmt.Println("  ✓ appended.log дополнен")
    }
}

// demoFileInfo — информация о файлах
func demoFileInfo() {
    fmt.Println("── 3. ИНФОРМАЦИЯ О ФАЙЛАХ ──")

    // os.Stat — получить информацию о файле
    info, err := os.Stat("test.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка stat: %v\n", err)
        return
    }

    fmt.Printf("  Имя: %s\n", info.Name())
    fmt.Printf("  Размер: %d байт\n", info.Size())
    fmt.Printf("  Права: %s\n", info.Mode())
    fmt.Printf("  Изменён: %s\n", info.ModTime().Format(time.RFC3339))
    fmt.Printf("  Директория? %v\n", info.IsDir())

    // Информация о директории
    dirInfo, _ := os.Stat("subdir")
    fmt.Printf("\n  subdir/ — директория: %v\n", dirInfo.IsDir())
}

// demoFilepath — работа с путями
func demoFilepath() {
    fmt.Println("── 4. FILEPATH — ПУТИ ──")

    // filepath.Join — кроссплатформенное объединение путей
    // На Windows использует \, на Linux/Mac — /
    path := filepath.Join("home", "user", "documents", "file.txt")
    fmt.Printf("  Join: %s\n", path)

    // filepath.Base — имя файла из пути
    fmt.Printf("  Base: %s\n", filepath.Base(path))

    // filepath.Dir — директория из пути
    fmt.Printf("  Dir: %s\n", filepath.Dir(path))

    // filepath.Ext — расширение файла
    fmt.Printf("  Ext: %s\n", filepath.Ext(path))

    // filepath.Abs — абсолютный путь
    absPath, _ := filepath.Abs("test.txt")
    fmt.Printf("  Abs: %s\n", absPath)

    // filepath.Glob — поиск по паттерну
    matches, _ := filepath.Glob("*.txt")
    fmt.Printf("  Glob *.txt: %v\n", matches)

    // filepath.Walk — рекурсивный обход директории
    fmt.Println("\n  Walk по текущей директории:")
    filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err // Ошибка доступа к файлу
        }

        // Пропускаем скрытые файлы и .git
        if strings.HasPrefix(info.Name(), ".") && info.Name() != "." {
            if info.IsDir() {
                return filepath.SkipDir // Пропускаем всю директорию
            }
            return nil
        }

        indent := strings.Repeat("  ", strings.Count(path, string(os.PathSeparator)))
        if info.IsDir() {
            fmt.Printf("  %s📁 %s/\n", indent, info.Name())
        } else {
            fmt.Printf("  %s📄 %s (%d байт)\n", indent, info.Name(), info.Size())
        }
        return nil
    })
}

// demoOSEnv — работа с окружением
func demoOSEnv() {
    fmt.Println("── 5. OS: ОКРУЖЕНИЕ ──")

    // os.Getenv — получить переменную окружения
    home := os.Getenv("HOME")
    if home == "" {
        home = os.Getenv("USERPROFILE") // Windows
    }
    fmt.Printf("  HOME: %s\n", home)

    // os.LookupEnv — проверить наличие переменной
    if val, ok := os.LookupEnv("EDITOR"); ok {
        fmt.Printf("  EDITOR: %s\n", val)
    } else {
        fmt.Println("  EDITOR: не установлен")
    }

    // os.Environ — все переменные окружения (слайс "KEY=VALUE")
    fmt.Printf("  Количество переменных окружения: %d\n", len(os.Environ()))

    // os.Setenv / os.Unsetenv — установка/удаление
    os.Setenv("MY_TEMP", "hello")
    fmt.Printf("  MY_TEMP: %s\n", os.Getenv("MY_TEMP"))
    os.Unsetenv("MY_TEMP")

    // os.Getpid — ID текущего процесса
    fmt.Printf("  PID: %d\n", os.Getpid())

    // os.Getwd — текущая рабочая директория
    wd, _ := os.Getwd()
    fmt.Printf("  Working Dir: %s\n", wd)

    // os.UserCacheDir / os.UserConfigDir — стандартные директории
    cacheDir, _ := os.UserCacheDir()
    fmt.Printf("  Cache Dir: %s\n", cacheDir)
    configDir, _ := os.UserConfigDir()
    fmt.Printf("  Config Dir: %s\n", configDir)
}

// demoTempFiles — временные файлы и директории
func demoTempFiles() {
    fmt.Println("── 6. ВРЕМЕННЫЕ ФАЙЛЫ ──")

    // os.CreateTemp — создать временный файл
    // Параметры: директория ("" = os.TempDir), префикс имени
    tmpFile, err := os.CreateTemp("", "example-*.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка создания temp: %v\n", err)
        return
    }
    // defer — удалит файл при выходе из функции
    defer os.Remove(tmpFile.Name())

    fmt.Fprintf(tmpFile, "Временные данные: %s\n", time.Now())
    fmt.Printf("  Временный файл: %s\n", tmpFile.Name())

    // Читаем обратно
    tmpFile.Seek(0, 0) // Возвращаемся в начало файла
    data, _ := io.ReadAll(tmpFile)
    fmt.Printf("  Содержимое: %s", string(data))
    tmpFile.Close()

    // os.MkdirTemp — временная директория
    tmpDir, err := os.MkdirTemp("", "example-dir-*")
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка создания temp dir: %v\n", err)
        return
    }
    defer os.RemoveAll(tmpDir) // Удаляем рекурсивно
    fmt.Printf("  Временная директория: %s\n", tmpDir)
}

// demoEmbed — встроенные файлы (Go 1.16+)
func demoEmbed() {
    fmt.Println("── 7. EMBED (встроенные файлы) ──")

    // Чтение встроенного файла
    data, err := fs.ReadFile(configFile, "data/config.json")
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка embed: %v\n", err)
        return
    }

    fmt.Printf("  Содержимое data/config.json: %s\n", string(data))

    // Парсим JSON из встроенного файла
    var config struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }
    if err := json.Unmarshal(data, &config); err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка JSON: %v\n", err)
    } else {
        fmt.Printf("  Распарсено: name=%s, age=%d\n", config.Name, config.Age)
    }

    // Чтение директории из embed
    entries, err := fs.ReadDir(dataDir, "data")
    if err != nil {
        fmt.Fprintf(os.Stderr, "  Ошибка чтения директории: %v\n", err)
    } else {
        fmt.Println("  Файлы в data/:")
        for _, entry := range entries {
            fmt.Printf("    - %s\n", entry.Name())
        }
    }
}

// demoCustomWriter — кастомный Writer
func demoCustomWriter() {
    fmt.Println("── 8. КАСТОМНЫЙ WRITER ──")

    // UpperCaseWriter преобразует вывод в верхний регистр
    uw := &UpperCaseWriter{writer: os.Stdout}
    fmt.Fprintln(uw, "этот текст будет в верхнем регистре")

    // Композиция: MultiWriter + кастомный Writer
    var buf strings.Builder
    mw := io.MultiWriter(os.Stdout, uw, &buf)
    fmt.Fprintln(mw, "multi-writer test")
    fmt.Printf("  В буфере: %q\n", buf.String())
}

// ╔══════════════════════════════════════════════════════════╗
// ║  MAIN                                                    ║
// ╚══════════════════════════════════════════════════════════╝

func main() {
    fmt.Println("╔══════════════════════════════════════════╗")
    fmt.Println("║   СТАНДАРТНАЯ БИБЛИОТЕКА: IO, OS, FS  ║")
    fmt.Println("╚══════════════════════════════════════════╝")

    demoIOReadersWriters()
    fmt.Println()

    demoOSFiles()
    fmt.Println()

    demoFileInfo()
    fmt.Println()

    demoFilepath()
    fmt.Println()

    demoOSEnv()
    fmt.Println()

    demoTempFiles()
    fmt.Println()

    demoEmbed()
    fmt.Println()

    demoCustomWriter()

    fmt.Println("\n✅ Демонстрация завершена!")
}

🚀 Запуск программы

# Запуск
go run main.go

# Сборка (embed-файлы будут встроены в бинарник!)
go build -o stdlib-demo main.go
./stdlib-demo

# Просмотр размера бинарника со встроенными файлами
ls -lh stdlib-demo

# Очистка созданных файлов
rm -f output.txt created.txt appended.log

📊 Интерфейсы io.Reader и io.Writer

ИнтерфейсСигнатураТипичные реализации
io.ReaderRead([]byte) (int, error)os.File, strings.Reader, bytes.Buffer, net.Conn, http.Request.Body
io.WriterWrite([]byte) (int, error)os.File, strings.Builder, bytes.Buffer, net.Conn, http.ResponseWriter
io.CloserClose() erroros.File, net.Conn
io.ReadWriterReader + Writeros.File, net.Conn
io.ReadCloserReader + Closeros.File, http.Request.Body

📊 Полезные функции из пакетов

ПакетФункцияНазначение
ioio.Copy(dst, src)Копирование из Reader в Writer
ioio.ReadAll(r)Чтение всего из Reader в []byte
ioio.NopCloser(r)Оборачивает Reader в ReadCloser с пустым Close
osos.ReadFile(name)Чтение файла целиком
osos.WriteFile(name, data, perm)Запись файла целиком
osos.MkdirAll(path, perm)Создание директорий рекурсивно
osos.Remove(name)Удаление файла
osos.RemoveAll(path)Рекурсивное удаление
filepathfilepath.Walk(root, fn)Рекурсивный обход
bufiobufio.NewScanner(r)Построчное сканирование

🔑 Ключевые концепции

Что нужно запомнить из этого урока:

💡 Практический совет:

💡 Best practices от сеньоров:

💡 Для Node.js разработчика:

← Предыдущий урок Следующий урок →