Перейти к основному содержимому

Работа с файлами и вводом-выводом

Привет! Седьмой урок — про работу с внешним миром. До этого мы писали код, который жил в памяти. Теперь научимся читать и писать файлы, работать с директориями, обрабатывать ввод от пользователя и выводить данные красиво.

В Go всё построено вокруг интерфейсов io.Reader и io.Writer — это делает код универсальным: один и тот же код может работать с файлом, сетью, строкой или консолью.

Основные пакеты

  • os — работа с файловой системой, аргументами командной строки
  • io — базовые интерфейсы чтения/записи
  • bufio — буферизованный ввод-вывод (очень полезно!)
  • fmt — форматированный вывод и ввод
  • encoding/csv, encoding/json — работа с популярными форматами

Чтение файлов

Весь файл сразу (os.ReadFile)

Идеально для небольших файлов (конфиги, шаблоны).

data, err := os.ReadFile("config.txt")
if err != nil {
fmt.Printf("Ошибка чтения: %v\n", err)
return
}
fmt.Println(string(data))

Построчное чтение (bufio.Scanner)

Лучший способ для больших файлов и логов.

file, err := os.Open("big.log")
if err != nil { /* Работа с ошибкой */ }
defer file.Close()

scanner := bufio.NewScanner(file)
lineNum := 1
for scanner.Scan() {
fmt.Printf("%d: %s\n", lineNum, scanner.Text())
lineNum++
}

if err := scanner.Err(); err != nil {
fmt.Printf("Ошибка сканирования: %v\n", err)
}

Чтение порциями (bufio.Reader)

Когда нужно контролировать размер буфера.

reader := bufio.NewReader(file)
buffer := make([]byte, 1024)

for {
n, err := reader.Read(buffer)
if n > 0 {
fmt.Print(string(buffer[:n]))
}

if err == io.EOF {
break
}

if err != nil {
fmt.Printf("Ошибка: %v\n", err)
break
}
}

Зачем контролировать размер буфера при чтении?

Когда ты читаешь данные (из файла, сети, stdin и т.д.), они могут быть очень большими — мегабайты, гигабайты или даже бесконечный поток. Если пытаться прочитать всё сразу в память, программа может съесть всю RAM и упасть.

Контроль размера буфера позволяет:

  • Читать данные порциями фиксированного размера (например, по 1024 байта).
  • Обрабатывать их по частям, не загружая весь объём в память сразу.
  • Экономить память и работать даже с огромными файлами или потоками.

Когда особенно важно контролировать размер:

  1. Большие файлы (логи, видео, дампы БД) — нельзя грузить целиком.
  2. Сетевые соединения (HTTP, сокеты) — данные приходят потоком, размер заранее неизвестен.
  3. Ограниченная память (серверы, встраиваемые устройства).
  4. Чтение из stdin — пользователь может ввести сколько угодно текста.


stdin (сокращение от standard input) — это стандартный поток ввода в программе. Это место, откуда программа по умолчанию читает данные (текст), когда запускается.

Запись в файлы

Перезапись файла (os.Create + WriteString)

file, err := os.Create("output.txt")
if err != nil { ... }
defer file.Close()

content := "Привет из Go!\nЕщё одна строка.\n"
if _, err := file.WriteString(content); err != nil {
fmt.Printf("Ошибка записи: %v\n", err)
}

Добавление в конец (os.OpenFile с флагом O_APPEND)

file, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { ... }
defer file.Close()

logEntry := fmt.Sprintf("%s - Успешно!\n", time.Now().Format("2006-01-02 15:04:05"))
file.WriteString(logEntry)

Что такое флаги в Go?

Флаги при работе с файлами — это специальные константы из пакета os, которые говорят функции os.OpenFile, как именно открыть файл: для чтения, записи, добавления в конец и т.д.

Самая важная функция — os.OpenFile(name, flags, perm), где:

  • name — имя файла
  • flags — комбинация флагов (что делать с файлом)
  • perm — права доступа (например, 0644)

Основные флаги (самые нужные)

ФлагЧто значитКогда использовать
os.O_RDONLYТолько чтениеОткрыть файл для чтения
os.O_WRONLYТолько записьОткрыть файл для записи
os.O_RDWRЧтение и записьНужно и читать, и писать
os.O_APPENDДобавлять в конец файлаЛоги, чтобы не перезаписывать старое
os.O_CREATEСоздать файл, если его нетВсегда с записью, чтобы не падать
os.O_TRUNCОбрезать файл до 0 (очистить) при открытииПерезаписать файл с нуля
os.O_EXCLОшибка, если файл уже существует (с O_CREATE)Защита от перезаписи

Флаги комбинируются через побитовое ИЛИ |.

Примеры

file, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)

Это значит:

  • os.O_APPEND — новые данные будут писаться в конец файла (не затирая старые логи)
  • os.O_CREATE — если файла нет — создать его
  • os.O_WRONLY — открыть только для записи
  • 0644 — права: владелец может читать/писать, остальные — только читать

Идеально для логгера!

Другие частые комбинации

  1. Перезаписать файл с нуля

    os.OpenFile("data.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
  2. Только читать существующий файл

    os.OpenFile("config.txt", os.O_RDONLY, 0)
  3. Создать новый файл и упасть, если он уже есть

    os.OpenFile("lock.file", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)

Зачем нужны флаги?

Без них os.Open и os.Create могут делать только простые вещи.
С флагами ты точно контролируешь поведение: добавлять в конец, не затирать случайно, создавать при необходимости и т.д.

Буферизованная запись (bufio.Writer)

Эффективно при множестве мелких записей.

writer := bufio.NewWriter(file)
for i := 1; i <= 100; i++ {
fmt.Fprintf(writer, "Строка номер %d\n", i)
}
writer.Flush() // Обязательно! Иначе данные останутся в буфере

Работа с директориями

Создание структуры

os.MkdirAll("project/cmd/api", 0755) // Создаст все вложенные папки

Список содержимого

entries, err := os.ReadDir(".")
if err != nil { /* Работа с ошибкой */ }

for _, e := range entries {
info := "файл"
if e.IsDir() {
info = "папка"
}
fmt.Printf("%s: %s\n", info, e.Name())
}

Рекурсивный обход (filepath.WalkDir — рекомендуется в новых версиях)

err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil { return err }
if d.IsDir() {
fmt.Printf("📁 %s\n", path)
} else {
fmt.Printf("📄 %s\n", path)
}
return nil
})

Форматированный ввод-вывод

Вывод (fmt.Printf, fmt.Fprintf)

name := "Алиса"
age := 28
salary := 123456.789

fmt.Printf("Имя: %s, возраст: %d, зарплата: %.2f\n", name, age, salary)
// Имя: Алиса, возраст: 28, зарплата: 123456.79

Ввод от пользователя

var name string
fmt.Print("Введите имя: ")
fmt.Scanln(&name)

var age int
fmt.Print("Возраст: ")
fmt.Scanf("%d", &age)

Для надёжного ввода лучше использовать bufio.Scanner.

Копирование файлов

Самый простой и эффективный способ:

src, _ := os.Open("source.txt")
dst, _ := os.Create("copy.txt")
defer src.Close()
defer dst.Close()

io.Copy(dst, src) // Копирует всё за один вызов!

Практический пример: простая утилита поиска текста

package main

import (
"bufio"
"fmt"
"os"
"strings"
)

func searchInFile(filename, query string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("не открыть файл: %w", err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
lineNum := 0
found := false

for scanner.Scan() {
lineNum++
line := scanner.Text()
if strings.Contains(strings.ToLower(line), strings.ToLower(query)) {
fmt.Printf("%s:%d: %s\n", filename, lineNum, line)
found = true
}
}

if !found {
fmt.Printf("В файле %s ничего не найдено\n", filename)
}
return scanner.Err()
}

func main() {
if len(os.Args) < 3 {
fmt.Println("Использование: grep <текст> <файл1> [файл2]...")
return
}

query := os.Args[1]
files := os.Args[2:]

for _, file := range files {
if err := searchInFile(file, query); err != nil {
fmt.Printf("Ошибка при обработке %s: %v\n", file, err)
}
}
}

Запустите: go run main.go Привет *.txt

Работа с JSON

JSON (JavaScript Object Notation) — это самый популярный формат для обмена данными: между сервером и клиентом, для хранения настроек, API и т.д. В Go работа с JSON встроена в стандартную библиотеку — пакет encoding/json.

Основные операции

  • Marshal — превращает Go-структуры/значения в JSON (сериализация, "запись")
  • MarshalIndent — это функция из пакета encoding/json, которая превращает Go-значение (структуру, слайс, map и т.д.) в JSON, но делает это красиво — с отступами и переносами строк, чтобы результат было легко читать человеку
  • Unmarshal — превращает JSON в Go-структуры/значения (десериализация, "чтение")
  • json.NewDecoder(r) — создаёт декодер, который читает из любого io.Reader (файл, сеть, строка и т.д.).
  • Decoder.More() — возвращает true, пока в текущем массиве или объекте есть ещё элементы. Идеально для чтения внутри [] или .
  • Decoder.Decode(&value) — читает следующий JSON-объект и кладёт его в переменную (по указателю!).
  • Decoder.Token() — используется для чтения скобок, запятых и других "токенов" (нужно, чтобы пропустить [ и ]).

Структура и теги json

Чтобы Go знал, как преобразовывать поля структуры в JSON, используют теги (backticks):

type User struct {
ID int `json:"id"` // поле ID → "id" в JSON
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"` // "-" — игнорировать поле полностью
Admin bool `json:"is_admin,omitempty"` // omitempty — не выводить, если false или пустое
}

Важные опции тегов:

  • json:"name" — имя поля в JSON
  • json:"-" — пропустить поле
  • json:",omitempty" — не включать поле, если оно пустое ("" / 0 / false / nil)

Запись в JSON (Marshal)

users := []User{
{ID: 1, Name: "Алексей", Email: "alex@example.com"},
{ID: 2, Name: "Мария", Email: "maria@example.com"},
}

// Красивый (отформатированный) JSON
data, err := json.MarshalIndent(users, "", " ") // префикс "", отступ " "
if err != nil {
panic(err)
}

// Сохраняем в файл
if err = os.WriteFile("users.json", data, 0644); err != nil {
panic(err)
}

Результат в файле:

[
{
"id": 1,
"name": "Алексей",
"email": "alex@example.com"
},
{
"id": 2,
"name": "Мария",
"email": "maria@example.com"
}
]

Если нужен компактный JSON (без отступов):

data, _ := json.Marshal(users)  // просто Marshal, без Indent

Чтение из JSON (Unmarshal)

// Читаем файл
data, err := os.ReadFile("users.json")
if err != nil {
panic(err)
}

// Подготовим переменную для результата
var users []User

// ОБЯЗАТЕЛЬНО передаём указатель!
if err = json.Unmarshal(data, &users); err != nil {
panic(err)
}

fmt.Println(users[0].Name) // Алексей

Важно: Unmarshal требует указатель (&users), чтобы могла изменить значение переменной.

Работа с неизвестной структурой JSON

Если структура JSON заранее неизвестна — используй map[string]interface{} или []interface{}:

var result map[string]interface{}
json.Unmarshal(data, &result)

fmt.Println(result["name"]) // доступ по ключу

Или для массива:

var result []interface{}
json.Unmarshal(data, &result)

Потоковая обработка (большой JSON)

Если JSON очень большой — не стоит грузить весь в память. Используй json.Decoder:

file, _ := os.Open("big.json")
decoder := json.NewDecoder(file)

var user User
for decoder.More() { // пока есть объекты в массиве
err := decoder.Decode(&user)
if err != nil {
break
}
fmt.Println(user.Name)
}

Идеально для обработки огромных списков.

Пример полной программы

package main

import (
"encoding/json"
"fmt"
"os"
)

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}

func main() {
// Запись
users := []User{
{ID: 1, Name: "Иван"},
{ID: 2, Name: "Ольга", Email: "olga@mail.ru"},
}

data, _ := json.MarshalIndent(users, "", " ")
os.WriteFile("users.json", data, 0644)

// Чтение
data, _ = os.ReadFile("users.json")
var loadedUsers []User
json.Unmarshal(data, &loadedUsers)

fmt.Println("Загружено пользователей:", len(loadedUsers))
for _, u := range loadedUsers {
fmt.Printf("%d: %s (%s)\n", u.ID, u.Name, u.Email)
}
}

Рекомендации

  • Используй структуры с тегами json:"..." — самый удобный и типобезопасный способ.
  • Marshal → в JSON, Unmarshal → из JSON.
  • Всегда проверяй ошибки в реальном коде!
  • Для больших данных — json.Decoder или буферизованное чтение.

Работа с CSV

CSV (Comma-Separated Values) — это простой текстовый формат для хранения табличных данных: строки — это записи, столбцы разделены запятыми (или другим разделителем). Очень популярен для экспорта/импорта данных (Excel, Google Sheets, базы данных и т.д.).

В Go работа с CSV встроена в стандартную библиотеку — пакет encoding/csv.

Основные инструменты

  • csv.NewWriter(w io.Writer) — создаёт объект для записи CSV
  • csv.NewReader(r io.Reader) — создаёт объект для чтения CSV
  • csv.Writer.Write(record []string) — записывает строку в CSV
  • csv.Writer.WriteAll(records [][]string) — записывает все строки в CSV и сбрасывает буфер в файл
  • csv.Reader.Read() — читает строку из CSV
  • csv.Reader.ReadAll() — читает все строки из CSV
  • csv.Reader.Flush() — Сбрасывает буфер в файл
import (
"encoding/csv"
"os"
)

func main() {
// Создаём (или перезаписываем) файл
file, err := os.Create("data.csv")
if err != nil {
panic(err)
}
defer file.Close()

// Создаём писателя CSV
writer := csv.NewWriter(file)

// Записываем строки по одной — каждая строка это слайс строк
writer.Write([]string{"Имя", "Возраст", "Город"}) // заголовок
writer.Write([]string{"Алиса", "30", "Москва"})
writer.Write([]string{"Борис", "25", "СПб"})
writer.Write([]string{"Вера", "28", "Казань"})

// ОБЯЗАТЕЛЬНО! Сбрасываем буфер на диск
writer.Flush()

// Проверяем ошибку после Flush
if err := writer.Error(); err != nil {
panic(err)
}
}

Результат в файле data.csv:

Имя,Возраст,Город
Алиса,30,Москва
Борис,25,СПб
Вера,28,Казань

Важные моменты при записи:

  • writer.Write записывает в буфер, а не сразу на диск.
  • Всегда вызывай writer.Flush() в конце.
  • Можно записывать сразу много строк: writer.WriteAll([][]string{...}) — удобно и автоматически делает Flush.

Пример с WriteAll:

data := [][]string{
{"Имя", "Возраст", "Город"},
{"Алиса", "30", "Москва"},
{"Борис", "25", "СПб"},
}

writer.WriteAll(data) // записывает всё сразу и делает Flush

Чтение из CSV

file, err := os.Open("data.csv")
if err != nil {
panic(err)
}
defer file.Close()

reader := csv.NewReader(file)

// Читаем все строки сразу
records, err := reader.ReadAll()
if err != nil {
panic(err)
}

for _, row := range records {
fmt.Printf("Имя: %s, Возраст: %s, Город: %s\n", row[0], row[1], row[2])
}

Вывод:

Имя: Имя, Возраст: Возраст, Город: Город
Имя: Алиса, Возраст: 30, Город: Москва
...

Пошаговое чтение (для очень больших файлов)

reader := csv.NewReader(file)

for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
fmt.Println(row)
}

Настройка разделителя и другие опции

По умолчанию разделитель — запятая, но можно изменить:

reader := csv.NewReader(file)
reader.Comma = ';' // теперь разделитель — точка с запятой
reader.LazyQuotes = true // разрешает нестрогие кавычки
reader.FieldsPerRecord = 3 // ожидать ровно 3 поля в строке (иначе ошибка)

Полный пример: структура → CSV → структура

type Person struct {
Name string
Age int
City string
}

people := []Person{
{"Алиса", 30, "Москва"},
{"Борис", 25, "СПб"},
}

// Запись
file, _ := os.Create("people.csv")
w := csv.NewWriter(file)
w.Write([]string{"Name", "Age", "City"}) // заголовок
for _, p := range people {
w.Write([]string{p.Name, strconv.Itoa(p.Age), p.City})
}
w.Flush()
file.Close()

// Чтение обратно
file, _ = os.Open("people.csv")
r := csv.NewReader(file)
records, _ := r.ReadAll()

var loaded []Person
for i, row := range records {
if i == 0 { continue } // пропустить заголовок
age, _ := strconv.Atoi(row[1])
loaded = append(loaded, Person{row[0], age, row[2]})
}

Рекомендации

  • Для записи: csv.NewWriterWrite/WriteAllFlush()
  • Для чтения: csv.NewReaderRead (пошагово) или ReadAll() (если файл не гигантский)
  • Всегда закрывай файл и проверяй ошибки в реальном коде!
  • CSV — простой и надёжный способ сохранять и обмениваться табличными данными.