Avatar ·

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

Привет всем! Я начинающий Go-разработчик, и у меня возникла проблема с чтением текстового файла построчно. Мне нужно обработать большой лог-файл (около 500 МБ), где каждая строка — это отдельная запись. Проблема в том, что я не хочу загружать весь файл в память сразу, так как он очень большой. Я пробовал использовать `ioutil.ReadFile()`, но это выгружает всё содержимое в память, и программа начинает тормозить. К тому же, если файл окажется ещё больше, это может привести к исчерпанию памяти. Вот что я пробовал: ```go package main import ( "fmt" "io/ioutil" "strings" ) func main() { data, err := ioutil.ReadFile("large_log.txt") if err != nil { fmt.Println("Ошибка:", err) return } lines := strings.Split(string(data), "\n") for i, line := range lines { fmt.Printf("Строка %d: %s\n", i+1, line) } } ``` Этот код работает, но он загружает весь файл в память, что неэффективно. Плюс, если в файле есть символы возврата каретки (`\r\n`), то разбиение по `\n` может работать некорректно. Подскажите, как правильно организовать построчное чтение в Go? Нужно ли использовать `bufio.Scanner`, `bufio.Reader` или есть другие способы? Буду благодарен за примеры кода! Теги: go, файлы, чтение, построчно, bufio <<>>

Лучший способ: bufio.Scanner

Для построчного чтения файлов в Go рекомендуется использовать пакет bufio и его тип Scanner. Это эффективный способ, который читает файл небольшими кусками (буферами) и не загружает весь файл в память. Scanner также корректно обрабатывает разные окончания строк (\n, \r\n).

Вот правильное решение вашей задачи:

package main

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

func main() {
    // Открываем файл
    file, err := os.Open("large_log.txt")
    if err != nil {
        log.Fatal(err)
    }
    // Важно: закрываем файл после завершения работы
    defer file.Close()

    // Создаём новый Scanner для файла
    scanner := bufio.NewScanner(file)

    // Опционально: увеличиваем размер буфера, если строки очень длинные (по умолчанию 64КБ)
    // scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)

    // Счётчик строк
    lineNum := 0

    // Читаем строку за строкой
    for scanner.Scan() {
        lineNum++
        line := scanner.Text()
        fmt.Printf("Строка %d: %s\n", lineNum, line)
    }

    // Проверяем, не возникла ли ошибка при сканировании
    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

Ключевые моменты:

  • os.Open() — открывает файл для чтения. В отличие от ioutil.ReadFile(), он не загружает содержимое в память.
  • defer file.Close() — гарантирует закрытие файла даже при возникновении ошибки.
  • bufio.NewScanner() — создаёт сканер, который читает файл буферизованно.
  • scanner.Scan() — возвращает true, пока есть строки для чтения.
  • scanner.Text() — возвращает текущую строку без символа перевода строки.
  • scanner.Err() — проверяет, не возникла ли ошибка во время чтения (например, если строка превышает размер буфера).

Обработка ошибок

Важно обрабатывать ошибки на каждом этапе. log.Fatal() выводит сообщение и завершает программу. В production-коде можно использовать log.Printf() или возвращать ошибку из функции.

Что делать с очень длинными строками?

По умолчанию Scanner имеет буфер 64 КБ. Если ваши строки длиннее, вы получите ошибку bufio.Scanner: token too long. В этом случае увеличьте буфер:

scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024) // 1 МБ

Альтернатива: bufio.Reader

Если вам нужен более низкоуровневый контроль, можно использовать bufio.Reader:

reader := bufio.NewReader(file)
for {
    line, err := reader.ReadString('\n')
    if err != nil {
        if err == io.EOF {
            break // Конец файла
        }
        log.Fatal(err)
    }
    fmt.Print(line) // line включает символ '\n'
}

Но Scanner проще и чище для большинства задач.

Заключение

Используйте bufio.Scanner для построчного чтения файлов в Go. Это эффективно, безопасно по памяти и легко в использовании. Не забудьте обрабатывать ошибки и закрывать файлы!

<<>>

Дополнительный подход: чтение с помощью ioutil.ReadFile и strings.Split (для маленьких файлов)

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

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "strings"
)

func main() {
    data, err := ioutil.ReadFile("small_config.txt")
    if err != nil {
        log.Fatal(err)
    }

    // Нормализуем окончания строк: заменяем \r\n на \n
    content := strings.ReplaceAll(string(data), "\r\n", "\n")
    // Удаляем возможный завершающий \r
    content = strings.ReplaceAll(content, "\r", "")

    lines := strings.Split(content, "\n")
    for i, line := range lines {
        fmt.Printf("Строка %d: %s\n", i+1, line)
    }
}

Когда использовать этот метод?

  • Файл гарантированно маленький (конфиги, списки, настройки).
  • Вам нужно быстро прочитать файл целиком для дальнейшей обработки.
  • Вы не работаете с потоками данных (например, сетевыми).

Но помните: для больших файлов или production-кода всегда используйте bufio.Scanner — это стандартный идиоматичный способ в Go.