Что такое потоки ввода-вывода в Linux
Что проверяют
Понимаете ли вы, что за стандартным вводом и за открытым файлом лежит один и тот же дескриптор ядра, и как рантайм Go совмещает внешне блокирующий код с неблокирующими системными вызовами. Для Go разработчика это вопрос про то, почему горутины дёшевы.
Как отвечать
В Linux почти всё представлено файлом. У процесса есть таблица файловых дескрипторов, первые три заняты стандартным вводом, выводом и потоком ошибок. За дескриптором может стоять файл, сокет, канал или устройство, а читают и пишут во все одинаково, через read и write.
В Go это завёрнуто в интерфейсы io.Reader и io.Writer, поэтому источник подменяется в тестах буфером в памяти без изменения кода. os.Stdin и os.Stdout это те же дескрипторы, обёрнутые в os.File.
Буферизации у стандартного вывода в Go нет: каждая запись это системный вызов. Вывод в цикле заворачивают в bufio.Writer и не забывают про Flush.
w := bufio.NewWriter(os.Stdout)
defer w.Flush()Про блокировку стоит сказать отдельно. Сетевые дескрипторы рантайм переводит в неблокирующий режим и обслуживает через epoll, поэтому горутина, ждущая сокет, не занимает поток операционной системы. С обычными файлами так не выходит: чтение с диска уходит в блокирующий вызов, и планировщик отдаёт освободившееся место другому потоку.
Где спотыкаются
Игнорируют ошибку чтения, записывая её в пустой идентификатор. Именно там приходит конец файла и короткое чтение, и без проверки цикл либо крутится вечно, либо молча теряет хвост данных.
Не закрывают дескрипторы. Открытый файл или тело ответа без отложенного закрытия утекает, и на нагрузке процесс упирается в лимит открытых файлов, а выглядит это как отказ соединений.
Смешивают данные и диагностику в стандартном выводе. Логи пишут в поток ошибок именно поэтому: иначе конвейер из команд получает вперемешку полезный вывод и сообщения программы.