В этой статье разберём зачем нужен BufReader, как он работает и какие есть способы читать данные из файлов. Мы сравним разные подходы и посмотрим сколько они занимают времени.
🎙️ К статье прилагается подкаст-версия - лёгкий и образный рассказ с аналогиями. Включайте, если хотите услышать объяснение на слух.Подкаст Rust в деталях. Выпуск 1
Когда мы читаем файл или сокет через Read, каждый вызов .read() почти всегда приводит к системному вызову read(2) (на Linux/Unix) или его аналогу в других ОС.
⚙️ Разберём по шагам что же происходит когда в коде вызвается этот метод:
Чтобы уменьшить число системных вызовов существует BufReader.
Он один раз считывает крупный блок (по умолчанию 8 КБ) во внутренний буфер, а дальше выдаёт данные уже из памяти. Так вместо тысячи вызовов вы получаете десятки.
BufReader: чтение всего файла
Шаг 1/2
1// ?hidden:start
2usestd::fs::{File,OpenOptions}; 3usestd::io::{self,BufReader,Read,Write}; 4usestd::path::Path; 5 6fnensure_file()-> io::Result<()>{ 7if!Path::new("hello.txt").exists(){ 8letmutf=OpenOptions::new().create(true).write(true).open("hello.txt")?; 9writeln!(f,"Знакомство с BufReader!")?;10}11Ok(())12}13// ?hidden:end
14fnmain()-> io::Result<()>{15// Проверяем, что файл существует (создаём при необходимости)
16ensure_file()?;1718// Открываем файл
19letfile=File::open("hello.txt")?;2021// Оборачиваем его в BufReader
22letmutreader=BufReader::new(file);2324// Читаем содержимое в строку
25letmutcontents=String::new();26reader.read_to_string(&mutcontents)?;2728println!("Содержимое файла: {}",contents);2930Ok(())31}32
Разберем подробнее:
Функция ensure_file проверяет наличие файла hello.txt.
Если его нет — создаём и записываем туда строку “Знакомство с BufReader!”.
В main открываем файл и оборачиваем его в BufReader.
Буферизованное чтение делает работу эффективной.
С помощью read_to_string считываем всё содержимое в строку.
Результат печатаем в консоль.
Это самый простой пример: мы просто читаем файл целиком.
Но ключевая идея уже видна - BufReader используется как оболочка
вокруг файла, которая делает операции ввода-вывода более эффективными.
Результат выполнения:
1usestd::fs::{File,OpenOptions}; 2usestd::io::{self,BufReader,BufRead,Write}; 3usestd::path::Path; 4 5 6fnensure_file()-> io::Result<()>{ 7if!Path::new("app.log").exists(){ 8letmutf=OpenOptions::new().create(true).write(true).open("app.log")?; 9writeln!(f,"[2025-08-27 10:00:01] INFO — Приложение запущено")?;10writeln!(f,"[2025-08-27 10:00:03] WARN — Соединение нестабильно")?;11writeln!(f,"[2025-08-27 10:00:05] ERROR — Ошибка чтения конфигурации")?;12writeln!(f,"[2025-08-27 10:00:07] INFO — Перезапуск сервиса")?;13}14Ok(())15}161718fnmain()-> io::Result<()>{19ensure_file()?;2021letfile=File::open("app.log")?;22letreader=BufReader::new(file);2324letmutline_count=0;25letmuterror_count=0;2627forlineinreader.lines(){28letline=line?;29line_count+=1;30ifline.contains("ERROR"){31error_count+=1;32}33println!(">> {}",line);34}3536println!("\nВ логе {} строки. Из них {} с ошибками.",line_count,error_count);3738Ok(())39}40
Разберем подробнее что же тут происходит.Мы читаем лог-файл с помощью BufReader:
Функция ensure_file имитирует создание файла с логом приложения. Если файла app.log нет — создаём его и записываем несколько строк с метками времени, уровнями логов (INFO, WARN, ERROR) и сообщениями.
В main открываем файл и оборачиваем его в BufReader чтобы читать эффективно.
С помощью reader.lines() проходим построчно в цикле:
считаем общее количество строк
ищем вхождения слова "ERROR" и увеличиваем отдельный счётчик ошибок
печатаем строку с префиксом >>
После цикла выводим статистику: сколько строк всего и сколько среди них сообщений об ошибках.
Такой приём реально полезен: BufReader позволяет работать даже с огромными логами построчно,
без необходимости загружать их целиком в память.
BufReader читает файл большими блоками и хранит их в своём буфере.
Трейт BufRead добавляет методы, которые работают не с файловым дескриптором напрямую, а с этим буфером в памяти.
Метод .lines() ищет символ перевода строки \n в буфере и возвращает готовые строки.
Если бы у нас был только Read, то для поиска конца строки пришлось бы каждый раз вызывать .read() → это означало бы больше системных вызовов и ненужных копирований.
1// ?hidden:start
2usestd::fs::{self,OpenOptions}; 3usestd::io::{self,Write,Read}; 4usestd::path::Path; 5usestd::fs::File; 6 7 8/// Создаём файл big.txt, если его ещё нет.
9/// 100_0 строк с текстом и номерами.
10fnensure_file()-> io::Result<()>{11ifPath::new("big.txt").exists(){12returnOk(());13}14println!("Создается файл big.txt...");15letmutf=OpenOptions::new().create(true).write(true).open("big.txt")?;16foriin0..100_0{17writeln!(f,"Строка номер {i:06} — пример содержимого файла для BufReader")?;18}19println!("Файл создан!");20Ok(())21}22// ?hidden:end
23fnmain()-> io::Result<()>{24ensure_file()?;// создаём файл
2526letstart=std::time::Instant::now();27letmutfile=File::open("big.txt")?;28letmutbuffer=[0;1];29letmutline=String::new();3031whilefile.read(&mutbuffer)?>0{32letch=buffer[0]aschar;33ifch=='\n'{34line.clear();35}else{36line.push(ch);37}38}39letend=std::time::Instant::now();40println!("Время выполнения: {:?}",end-start);4142Ok(())43}44
Небуферизованное чтение по одному символу
❌ Очень медленно ~123 миллисекунды
Здесь каждая операция read идёт напрямую к ОС.
Удобно для понимания, но не для реальной работы.
Результат выполнения:
1// ?hidden:start
2usestd::fs::{self,OpenOptions}; 3usestd::io::{self,Write,BufRead,BufReader}; 4usestd::path::Path; 5usestd::fs::File; 6 7/// Создаём файл big.txt, если его ещё нет.
8/// 100_0 строк с текстом и номерами.
9fnensure_file()-> io::Result<()>{10ifPath::new("big.txt").exists(){11returnOk(());12}13println!("Создается файл big.txt...");14letmutf=OpenOptions::new().create(true).write(true).open("big.txt")?;15foriin0..100_0{16writeln!(f,"Строка номер {i:06}")?;17}18println!("Файл создан!");19Ok(())20}21// ?hidden:end
22fnmain()-> io::Result<()>{23ensure_file()?;// создаём файл
24letstart=std::time::Instant::now();25letmutlines_count=0;2627letfile=File::open("big.txt")?;28letreader=BufReader::new(file);2930forlineinreader.lines(){31lines_count+=1;32}3334letend=std::time::Instant::now();35println!("Время выполнения: {:?}",end-start);3637Ok(())38}39
BufReader + .lines()
⚡ Гораздо быстрее ~500µs
Это стандартный способ, удобный и быстрый.
Результат выполнения:
1// ?hidden:start
2usestd::fs::{self,OpenOptions}; 3usestd::io::{self,Write,BufRead,BufReader}; 4usestd::path::Path; 5usestd::fs::File; 6 7 8 9/// Создаём файл big.txt, если его ещё нет.
10/// 100_0 строк с текстом и номерами.
11fnensure_file()-> io::Result<()>{12ifPath::new("big.txt").exists(){13returnOk(());14}15println!("Создается файл big.txt...");16letmutf=OpenOptions::new().create(true).write(true).open("big.txt")?;17foriin0..100_0{18writeln!(f,"Строка номер {i:06}")?;19}20println!("Файл создан!");21Ok(())22}23// ?hidden:end
24fnmain()-> io::Result<()>{25ensure_file()?;// создаём файл
2627letstart=std::time::Instant::now();28letmutlines_count=0;29letfile=File::open("big.txt")?;30letmutreader=BufReader::new(file);31letmutline=String::new();3233whilereader.read_line(&mutline)?>0{34lines_count+=1;35line.clear();// строка очищается, но не выделяется заново
36}3738letend=std::time::Instant::now();39println!("Время выполнения: {:?}",end-start);4041Ok(())42}43
BufReader + переиспользование строки
⚡ Ещё быстрее ~360µs
read_line записывает прямо в существующую строку.
Нет лишних аллокаций памяти, работает эффективнее.
Результат выполнения:
1// ?hidden:start
2usestd::fs::{self,OpenOptions}; 3usestd::io::{self,Write,Read}; 4usestd::path::Path; 5usestd::fs::File; 6 7/// Создаём файл big.txt, если его ещё нет.
8/// 100_0 строк с текстом и номерами.
9fnensure_file()-> io::Result<()>{10ifPath::new("big.txt").exists(){11returnOk(());12}13println!("Создается файл big.txt...");14letmutf=OpenOptions::new().create(true).write(true).open("big.txt")?;15foriin0..100_0{16writeln!(f,"Строка номер {i:06}")?;17}18println!("Файл создан!");19Ok(())20}21// ?hidden:end
22fnmain()-> io::Result<()>{2324ensure_file()?;// создаём файл
25letstart=std::time::Instant::now();2627letmutlines_count=0;28letmutfile=File::open("big.txt")?;29letmutcontents=String::new();30file.read_to_string(&mutcontents)?;3132forlineincontents.lines(){33lines_count+=1;34}3536letend=std::time::Instant::now();37println!("Время выполнения: {:?}",end-start);3839Ok(())40}41
4. Еще один вариант
⚡ ~270µs
Всё содержимое читается за раз, а потом разбивается на строки.
Время выполнения у вас может отличаться от представленного здесь.
Минус: выделяется объём памяти равный размеру файла.
Если у вас есть Vec<u8>, String или срез &[u8], никакого дополнительного системного вызова не происходит. Данные уже находятся в оперативной памяти, а значит никакой выгоды от дополнительного буфера не будет.
Если у вас тысячи одновременных соединений (например, работаете с сервером), каждый BufReader создаёт внутренний буфер (по умолчанию 8 КБ). При тысячах соединений это может означать десятки мегабайт памяти, которые не всегда оправданы.
В асинхронном коде часто используют другие подходы: читают ровно столько - сколько пришло в сокете и сами управляют буферами чтобы экономить память.