Работа с файлами и вводом-выводом
Привет! Седьмой урок — про работу с внешним миром. До этого мы писали код, который жил в памяти. Теперь научимся читать и писать файлы, работать с директориями, обрабатывать ввод от пользователя и выводить данные красиво.
В 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 байта).
- Обрабатывать их по частям, не загружая весь объём в память сразу.
- Экономить память и работать даже с огромными файлами или потоками.
Когда особенно важно контролировать размер:
- Большие файлы (логи, видео, дампы БД) — нельзя грузить целиком.
- Сетевые соединения (HTTP, сокеты) — данные приходят потоком, размер заранее неизвестен.
- Ограниченная память (серверы, встраиваемые устройства).
- Чтение из 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— права: владелец может читать/писать, остальные — только читать
Идеально для логгера!
Другие частые комбинации
-
Перезаписать файл с нуля
os.OpenFile("data.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) -
Только читать существующий файл
os.OpenFile("config.txt", os.O_RDONLY, 0) -
Создать новый файл и упасть, если он уже есть
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"— имя поля в JSONjson:"-"— пропустить поле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)— создаёт объект для записи CSVcsv.NewReader(r io.Reader)— создаёт объект для чтения CSVcsv.Writer.Write(record []string)— записывает строку в CSVcsv.Writer.WriteAll(records [][]string)— записывает все строки в CSV и сбрасывает буфер в файлcsv.Reader.Read()— читает строку из CSVcsv.Reader.ReadAll()— читает все строки из CSVcsv.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.NewWriter→Write/WriteAll→Flush() - Для чтения:
csv.NewReader→Read(пошагово) илиReadAll()(если файл не гигантский) - Всегда закрывай файл и проверяй ошибки в реальном коде!
- CSV — простой и надёжный способ сохранять и обмениваться табличными данными.