сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Веб и тесты на Go

io.Reader, time и стандартная библиотека Go

io и time: мелочи, на которых горят

Напечатал две строки: time.Duration(500) выдало 500ns, а 500 * time.Millisecond выдало 500ms. Разница в миллион раз, и рождается она из одного лишнего умножения. Спрашивают про такие вещи: что такое io.Reader, как задать длительность, чем Ticker отличается от Timer.

Стержень: io.Reader и io.Writer - универсальные интерфейсы потоков, а time.Duration измеряется в наносекундах.

// Формулировки: «что такое io.Reader?», «как задать 500 мс?», «Ticker или Timer?»

Потоки вместо конкретных типов

io.Reader и io.Writer - интерфейсы с одним методом каждый. Поэтому им удовлетворяет всё подряд: файл, сетевое соединение, буфер в памяти, архиватор, тело запроса. Функция, принимающая io.Reader, работает со всеми ними и тестируется через strings.NewReader без единого файла на диске.

io.Copy переносит данные кусками через небольшой буфер, пока источник не отдаст io.EOF (end of file, признак конца потока). Поэтому файл на гигабайт отдаётся в ответ, не занимая гигабайт памяти.

// Мелочь с приятным эффектом: если у источника есть WriteTo или у приёмника ReadFrom, io.Copy позовёт их, и в лучшем случае данные переложит само ядро, минуя пользовательский буфер.

io.Reader
интерфейс с методом Read - любой источник данных
io.Copy
потоковый перенос через буфер до конца чтения

Длительность это наносекунды

time.Duration - это int64 наносекунд, обычное число под псевдонимом. Поэтому длительность собирают умножением на константу единицы: 500 * time.Millisecond. Голое приведение time.Duration(500) даёт 500 наносекунд, и таймаут «срабатывает мгновенно», а человек потом ищет проблему в сети.

Строку разбирают функцией time.ParseDuration("500ms") - у меня она вернула честные 500ms. Приведением строку в длительность не превратить.

// Интервалы меряют через time.Since. Значение time.Now несёт монотонную часть, её видно прямо в печати - у меня было m=+0.000190931. Благодаря ей перевод системного времени замер не испортит. Но Round(0) и любая сериализация монотонную часть срезают, я проверял оба случая.

time.Duration
int64 наносекунд, а не абстрактный тип
монотонная часть
m=+... в печати, устойчива к переводу часов

Таймеры и тикеры

Timer срабатывает один раз, Ticker шлёт события с интервалом - на нём строят периодические задачи. Ticker обязательно останавливают через Stop, иначе он продолжает тикать и держать ресурсы после выхода из цикла.

Главное про Ticker я замерил. Поставил интервал 10 мс, а получателю велел спать по 50 мс между чтениями. За 310 миллисекунд тикер должен был дать около тридцати тиков - получатель прочитал шесть. Тики не копятся: канал у Ticker буферизован на одно значение, и всё, что не забрали, выбрасывается.

// Это защита от лавины, а не баг. Но если периодическая задача обязана отработать каждый интервал, Ticker для неё не подходит - нужна очередь заданий.

Ticker
регулярные события, обязательно требует Stop
пропуск тиков
медленный получатель не копит очередь, тики теряются

Как отвечать: «Как правильно задать длительность и чем Ticker отличается от Timer?»

time.Duration это int64 наносекунд, поэтому длительность собирают умножением числа на константу единицы: 500 умножить на time.Millisecond. Просто написать time.Duration(500) нельзя, получится пятьсот наносекунд - я специально печатал обе строки, разница выходит в миллион раз, и это классическая причина таймаута, который срабатывает мгновенно, а виноватой назначают сеть. Если длительность приходит строкой, разбираю через time.ParseDuration. Интервалы меряю через time.Since: значение time.Now несёт монотонную часть, её видно прямо в печати как m=+ с числом, и благодаря ей перевод системного времени замер не портит. Правда, при сериализации монотонная часть теряется, это надо помнить, если время путешествует между сервисами. Про таймеры: Timer срабатывает один раз, Ticker шлёт события с заданным интервалом, и на нём делают периодические задачи. Ticker обязательно останавливаю через Stop, иначе он тикает и после выхода из цикла. И главное - тики не копятся: я проверял, при тикере на десять миллисекунд и получателе, который спит по пятьдесят, за триста миллисекунд из тридцати тиков дошло шесть. Так что если задача обязана отработать каждый интервал, Ticker для неё не годится.

Сильный ответ: названа единица измерения и конкретная ошибка с её последствием, упомянуты монотонные часы вместе с потерей монотонности при сериализации, разведены Timer и Ticker, а поведение при медленном получателе подтверждено числом, а не описано словом «пропускает».

На чём валят

  • Пишут time.Duration(500) вместо 500 * time.Millisecond и получают таймаут в миллион раз короче.
  • Забывают Stop у Ticker, и он тикает после выхода из цикла.
  • Ждут накопления пропущенных тиков. У меня из тридцати дошло шесть.
  • Читают файл целиком в память вместо io.Copy при отдаче в ответ.
  • Создают time.After в горячем цикле: каждый вызов заводит таймер, живущий до срабатывания.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 7, остальные разбираются в тренажёре.

  1. #go_stdlib_io_time1 / 5
    Что такое io.Reader и io.Writer в Go?
    A)Интерфейсы с одним методом (Read/Write) — контракт байтовых потоков
    B)Конкретные структуры для чтения и записи файлов на диск
    C)Дженерик-типы io.Reader[T] для потоков значений произвольного типа
    D)Каналы стандартной библиотеки для передачи байт между горутинами
    показать ответ и разбор
    +A)Интерфейсы с одним методом (Read/Write) — контракт байтовых потоков

    // разбор: io.Reader и io.Writer — крошечные интерфейсы с единственным методом (Read(p []byte) и Write(p []byte)). Их реализуют файлы, сеть, буферы, HTTP-тела, сжатие — что угодно. Благодаря общему контракту код пишут против интерфейса, а не конкретики: io.Copy(dst Writer, src Reader) свяжет любой источник с любым приёмником. Витрина «мелких интерфейсов» Go.

  2. #go_stdlib_io_time2 / 5
    Как правильно задать таймаут «5 секунд» типом time.Duration?
    A)5 — потому что Duration это просто число секунд по смыслу
    B)time.Duration(5) — это специальный конструктор длительности из числа секунд
    C)5 * time.Second — Duration в наносекундах, единицы задают константами
    D)"5s" — Duration это строка формата длительности
    показать ответ и разбор
    +C)5 * time.Second — Duration в наносекундах, единицы задают константами

    // разбор: time.Duration — это int64 наносекунд. Литерал 5 означал бы 5 наносекунд, и time.Duration(5) — то же самое. Правильно домножать на константу-единицу: 5 * time.Second (есть Millisecond, Minute и др.). Отсюда типовая ошибка — передать «голое» число туда, где ждут Duration, и получить наносекунды вместо секунд.

  3. #go_stdlib_io_time3 / 5
    Нужно переслать большой файл в HTTP-ответ, не загружая его целиком в память. Что использовать?
    A)io.ReadAll всего файла в один большой []byte, а затем один общий вызов w.Write
    B)Прочитать файл в строку через fmt.Sprintf и записать её
    C)io.Copy(w, file) — потоковая передача кусками через буфер
    D)Загрузить файл в срез и отдать json.Marshal
    показать ответ и разбор
    +C)io.Copy(w, file) — потоковая передача кусками через буфер

    // разбор: io.Copy(dst, src) перекачивает данные потоком фиксированными кусками (внутренний буфер ~32 КБ), не держа весь объём в памяти — идеально для больших файлов и тел. io.ReadAll, напротив, засасывает всё в один []byte (пик памяти = размер файла, риск OOM на больших). Правило: между Reader и Writer — io.Copy, а не «прочитать всё в память».

  4. #go_stdlib_io_time4 / 5
    Что делает io.Copy(dst, src)?
    A)Читает src целиком в память, а затем пишет содержимое в dst
    B)Переносит данные потоком через внутренний буфер до конца чтения
    C)Создаёт копию источника и возвращает её вызывающему коду
    D)Копирует ровно столько байт, сколько помещается в буфер приёмника
    показать ответ и разбор
    +B)Переносит данные потоком через внутренний буфер до конца чтения

    // разбор: Копирование идёт кусками через небольшой буфер, пока источник не отдаст io.EOF, — поэтому переслать файл в ответ можно, не загружая его в память целиком. Если у источника или приёмника есть методы ReadFrom и WriteTo, io.Copy использует их и в лучшем случае перекладывает данные средствами ядра, минуя пользовательское пространство.

  5. #go_stdlib_io_time5 / 5
    Почему для измерения длительности берут time.Since, а не разность двух time.Now?
    A)time.Since точнее, потому что использует другой источник времени
    B)Разность двух Now не работает при переходе через полночь
    C)Обе формы эквивалентны и опираются на монотонные часы
    D)Разность двух Now даёт результат в наносекундах и требует ручного перевода
    показать ответ и разбор
    +C)Обе формы эквивалентны и опираются на монотонные часы

    // разбор: time.Since(t) — это ровно time.Now().Sub(t), удобное сокращение. Важнее другое: результат Now несёт монотонные часы, и вычитание через Sub опирается именно на них, поэтому перевод системного времени интервал не испортит. А вот если время сериализовать или отформатировать, монотонная часть теряется — и вычитание восстановленного значения снова становится ненадёжным.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.