Avatar ·

Как читать файл построчно в Go: пошаговое руководство

Всем привет! Я только начал изучать Go и столкнулся с задачей, которая кажется простой, но почему-то вызывает у меня трудности. Мне нужно прочитать текстовый файл построчно. Файл может быть достаточно большим (несколько сотен мегабайт), поэтому я хочу избежать загрузки всего содержимого в память. Вот что я пробовал: ```go package main import ( "fmt" "os" ) func main() { data, err := os.ReadFile("file.txt") if err != nil { panic(err) } lines := strings.Split(string(data), "\n") for _, line := range lines { fmt.Println(line) } } ``` Но этот код загружает весь файл в память, что неэффективно для больших файлов. К тому же, я слышал, что в Go есть специальный сканер для построчного чтения, но не могу разобраться, как его правильно использовать. Подскажите, пожалуйста, как правильно читать файл построчно в Go, чтобы это было эффективно по памяти и времени? Мне нужно обрабатывать каждую строку отдельно, возможно, пропускать пустые строки или строки, начинающиеся с комментариев. Буду благодарен за примеры кода с пояснениями. Использую Go 1.21, ОС — Ubuntu 22.04. <<>>

Правильный способ: использование bufio.Scanner

В Go для эффективного построчного чтения файлов используется пакет bufio и его тип Scanner. Это самый идиоматичный и производительный способ, который не загружает весь файл в память, а читает его порциями по мере необходимости.

Базовый пример построчного чтения:

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        fmt.Println(line)
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

Как это работает

  1. Открытие файла — os.Open() возвращает дескриптор файла. Важно сразу закрыть его через defer file.Close().
  2. Создание сканера — bufio.NewScanner(file) создаёт сканер, который по умолчанию разбивает данные на строки (разделитель — символ новой строки \n).
  3. Цикл чтения — scanner.Scan() возвращает true, пока есть следующая строка. Когда строки заканчиваются или происходит ошибка, возвращается false.
  4. Получение строки — scanner.Text() возвращает текущую строку без символа новой строки.
  5. Проверка ошибок — после цикла обязательно проверьте scanner.Err(), чтобы убедиться, что чтение завершилось без ошибок (например, из-за повреждения файла).

Расширенные примеры

Фильтрация строк (пропуск пустых строк и комментариев):

package main

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

func main() {
    file, err := os.Open("config.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    lineNumber := 0

    for scanner.Scan() {
        lineNumber++
        line := strings.TrimSpace(scanner.Text())

        // Пропускаем пустые строки и комментарии (начинающиеся с # или //)
        if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
            continue
        }

        // Обрабатываем значимую строку
        fmt.Printf("Строка %d: %s\n", lineNumber, line)
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

Чтение больших файлов с кастомным буфером:

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    file, err := os.Open("large_file.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    // Увеличиваем размер буфера до 1 МБ (по умолчанию 64 КБ)
    scanner.Buffer(make([]byte, 1024*1024), 1024*1024)

    for scanner.Scan() {
        line := scanner.Text()
        // Обработка строки
        _ = line
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

Обработка ошибок и специальные случаи:

  • Если строка длиннее буфера, Scanner вернёт ошибку ErrTooLong. Увеличьте буфер через scanner.Buffer().
  • Для Windows-файлов с разделителями \r\n сканер автоматически удаляет \r.
  • Если вам нужно читать строки с кастомным разделителем, используйте scanner.Split(bufio.ScanLines) или напишите свою функцию разделения.

Альтернативы (когда Scanner не подходит)

  • bufio.Reader — если вам нужен более низкоуровневый контроль, используйте bufio.NewReader(file).ReadString('\n') или ReadBytes('\n').
  • ioutil.ReadFile — только для маленьких файлов, которые гарантированно помещаются в память.

В 99% случаев bufio.Scanner — это правильный выбор. Он прост, эффективен и обрабатывает все краевые случаи.

<<>>

Альтернативный подход: использование bufio.Reader

Если вам нужен более гибкий контроль над процессом чтения (например, чтение большими блоками или работа с произвольными разделителями), можно использовать bufio.Reader.

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            // Если достигнут конец файла и есть прочитанные данные
            if err.Error() == "EOF" && len(line) > 0 {
                fmt.Print(line)
                break
            }
            if err.Error() == "EOF" {
                break
            }
            log.Fatal(err)
        }
        // Удаляем символ новой строки
        line = line[:len(line)-1]
        fmt.Println(line)
    }
}

Когда использовать bufio.Reader вместо Scanner:

  • Когда нужно читать строки с произвольным разделителем (не только \n).
  • Когда требуется обрабатывать очень длинные строки (более 64 КБ по умолчанию).
  • Когда нужно комбинировать построчное чтение с другими операциями ввода-вывода (например, чтением бинарных данных).

Однако для большинства задач bufio.Scanner удобнее — он автоматически обрабатывает конец файла и ошибки, а код получается чище.