Ответ
Чтение последних строк из файла — частая задача, особенно при работе с логами. Существует два основных подхода: неэффективный, но простой, и эффективный, но более сложный.
1. Неэффективный подход: Чтение всего файла
Этот метод заключается в чтении всего файла построчно и хранении последних N строк в памяти. Он прост в реализации, но потребляет много памяти и времени для больших файлов.
// Подходит только для небольших файлов!
func readLastLinesNaive(filename string, n int) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
// Когда буфер превышает N, удаляем самый старый элемент
if len(lines) > n {
lines = lines[1:]
}
}
return lines, scanner.Err()
}
2. Эффективный подход: Чтение с конца файла (Seek)
Этот метод является предпочтительным для больших файлов. Идея состоит в том, чтобы переместить курсор в конец файла (os.SeekEnd) и читать файл небольшими блоками в обратном направлении, подсчитывая символы новой строки (n).
import (
"bufio"
"bytes"
"io"
"os"
)
// Эффективно читает последние N строк
func readLastLines(filename string, n int) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Получаем информацию о файле, чтобы узнать его размер
stat, err := file.Stat()
if err != nil {
return nil, err
}
lines := make([]string, 0, n)
var lineCount int
var buffer bytes.Buffer
// Читаем файл с конца блоками по 1024 байта
const chunkSize = 1024
for offset := stat.Size(); offset > 0 && lineCount < n; {
readSize := int64(chunkSize)
if offset < readSize {
readSize = offset
}
offset -= readSize
chunk := make([]byte, readSize)
_, err := file.ReadAt(chunk, offset)
if err != nil && err != io.EOF {
return nil, err
}
// Ищем переносы строк в прочитанном блоке
for i := len(chunk) - 1; i >= 0; i-- {
if chunk[i] == 'n' {
// Переворачиваем буфер, чтобы получить строку в правильном порядке
lineBytes := buffer.Bytes()
for j, k := 0, len(lineBytes)-1; j < k; j, k = j+1, k-1 {
lineBytes[j], lineBytes[k] = lineBytes[k], lineBytes[j]
}
lines = append(lines, string(lineBytes))
buffer.Reset()
lineCount++
if lineCount >= n {
break
}
} else {
buffer.WriteByte(chunk[i])
}
}
}
// Не забываем про самую первую строку файла, если она попала в буфер
if lineCount < n && buffer.Len() > 0 {
lineBytes := buffer.Bytes()
for j, k := 0, len(lineBytes)-1; j < k; j, k = j+1, k-1 {
lineBytes[j], lineBytes[k] = lineBytes[k], lineBytes[j]
}
lines = append(lines, string(lineBytes))
}
// Переворачиваем итоговый срез, чтобы строки были в хронологическом порядке
for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
lines[i], lines[j] = lines[j], lines[i]
}
return lines, nil
}
Вывод: Для производственного кода всегда используйте второй, эффективный подход, так как он не зависит от размера файла и имеет предсказуемое потребление памяти.
Ответ 18+ 🔞
А, слушай, вот тебе классическая задачка, как из файла последние строчки вытащить. Ну, все с ней сталкиваются, когда логи смотреть надо, а там овердохуища записей. Так вот, есть два пути, и один из них — полная пиздец-стратегия.
Первый способ — для ленивых и безбашенных. Берёшь и читаешь весь файл целиком, строку за строкой, а потом просто откусываешь хвост. Просто? Да, пиздец как просто. Но представь, у тебя лог на 50 гигов — ты его всю оперативку сожрёшь, как голодный студент дошик. Комп уйдёт в себя, а ты будешь ждать, пока он отойдёт. Код вон, смотри, выглядит невинно:
// Подходит только для небольших файлов!
func readLastLinesNaive(filename string, n int) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
// Когда буфер превышает N, удаляем самый старый элемент
if len(lines) > n {
lines = lines[1:]
}
}
return lines, scanner.Err()
}
Вроде норм, да? Но это только для файлов размером с записку от мамки. На практике — терпения ноль ебать, если файл большой.
Второй способ — для тех, кто думает головой, а не жопой. Тут уже надо шевелить извилинами. Суть в чём: мы не читаем весь этот ёбаный файл с начала, а подъезжаем с чёрного хода. Берём, перемещаемся в самый конец файла (это os.SeekEnd), и начинаем читать маленькими кусочками назад, пока не насчитаем нужное количество строк. Это как задом наперёд газету читать, но зато эффективно, блядь!
import (
"bufio"
"bytes"
"io"
"os"
)
// Эффективно читает последние N строк
func readLastLines(filename string, n int) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Получаем информацию о файле, чтобы узнать его размер
stat, err := file.Stat()
if err != nil {
return nil, err
}
lines := make([]string, 0, n)
var lineCount int
var buffer bytes.Buffer
// Читаем файл с конца блоками по 1024 байта
const chunkSize = 1024
for offset := stat.Size(); offset > 0 && lineCount < n; {
readSize := int64(chunkSize)
if offset < readSize {
readSize = offset
}
offset -= readSize
chunk := make([]byte, readSize)
_, err := file.ReadAt(chunk, offset)
if err != nil && err != io.EOF {
return nil, err
}
// Ищем переносы строк в прочитанном блоке
for i := len(chunk) - 1; i >= 0; i-- {
if chunk[i] == 'n' {
// Переворачиваем буфер, чтобы получить строку в правильном порядке
lineBytes := buffer.Bytes()
for j, k := 0, len(lineBytes)-1; j < k; j, k = j+1, k-1 {
lineBytes[j], lineBytes[k] = lineBytes[k], lineBytes[j]
}
lines = append(lines, string(lineBytes))
buffer.Reset()
lineCount++
if lineCount >= n {
break
}
} else {
buffer.WriteByte(chunk[i])
}
}
}
// Не забываем про самую первую строку файла, если она попала в буфер
if lineCount < n && buffer.Len() > 0 {
lineBytes := buffer.Bytes()
for j, k := 0, len(lineBytes)-1; j < k; j, k = j+1, k-1 {
lineBytes[j], lineBytes[k] = lineBytes[k], lineBytes[j]
}
lines = append(lines, string(lineBytes))
}
// Переворачиваем итоговый срез, чтобы строки были в хронологическом порядке
for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
lines[i], lines[j] = lines[j], lines[i]
}
return lines, nil
}
Вывод, ёпта, простой: если не хочешь, чтобы тебя послали нахуй на проде, всегда используй второй способ. Он не сожрёт всю память и отработает даже на файле размером с историю твоих ошибок в жизни.