Contest IO - удобный ввод-вывод, который не тормозит. Специально для контестов на Go.
Статус: Бета-версия. Твои замечания и PR делают библиотеку лучше.
- Почему Contest IO?
- Быстрый старт
- Установка
- Читаем массив, обрабатываем, выводим
- Надоели
if err != nil? - Чтение всех чисел из строки (неизвестная длина)
- Чтение строк
ScanString/ScanBytes - Ввод/вывод слов (последовательность байт без пробелов)
- Вывод с разделителями
- Смешанный ввод/вывод (contestio / bufio / fmt)
- Чтение/вывод матрицы
- Единый интерфейс для разных типов
- Ввод/вывод значений разных типов
ScanAny/PrintAny - Как избежать лишних аллокаций в
PrintAny? - Больше примеров
- Обработка ошибок: тег сборки
must - Киллер-фича: contestio-inline
- Производительность
- Расширенное использование: API с интерфейсами (тег
sugar) - Настройка VS Code для работы с тегами сборки
Ты здесь, чтобы побеждать! А не писать ввод/вывод.
fmt? Комфортный седан. Довезет, но с аллокациями.
bufio.Scanner? Шустрее, без аллокаций. Но на смешанных данных - танцы с бубном.
bufio.Reader? Быстро. Но циклы и парсинг - знакомо, да?
Contest IO - это спорткар.
- Ноль аллокаций - только твои данные
- Токенизатор обгоняет
bufio.Scannerв 1.5 раза - Любые целые - на скорости
Atoi ScanInt/PrintIntдля целых,ScanFloat/PrintFloatдля float,ScanWord/PrintWordдля слов- Ввод/вывод массивов - одной строкой
- Режим контролируемой паники - альтернатива
if err != nil - Киллер-фича:
contestio-inlineвстроит код библиотеки в твое решение
Испытай Contest IO в деле!
go get github.com/aaa2ppp/contestio@latestpackage main
import (
"os"
. "github.com/aaa2ppp/contestio" // Точечный импорт делает код кратким
)
func main() {
br := NewReader(os.Stdin)
bw := NewWriter(os.Stdout)
defer bw.Flush()
var n int
if _, err := ScanIntLn(br, &n); err != nil {
panic(err) // В задачах ввод всегда корректен, поэтому можно падать
}
a := make([]int, n)
if _, err := ScanInts(br, a); err != nil {
panic(err)
}
for i := range a {
a[i] *= a[i]
}
PrintIntsLn(bw, a)
}Важно: Не игнорируй ошибки ввода! Используй контролируемую панику. В большинстве случаев она подскажет, если ты неверно понимаешь условие. Многие "почему не работает?" решаются такой проверкой.
Включи режим Must - Scan будет паниковать на любой ошибке, кроме EOF.
package main
import (
"os"
. "github.com/aaa2ppp/contestio"
)
func main() {
br := NewReader(os.Stdin)
bw := NewWriter(os.Stdout)
defer bw.Flush()
var n int
ScanIntLn(br, &n)
a := make([]int, n)
ScanInts(br, a)
for i := range a {
a[i] *= a[i]
}
PrintIntsLn(bw, a)
}// Вариант 1: предварительная аллокация
a := make([]int32, 0, 1<<20)
a, _ = ScanIntsLn(br, a) // функция сделает append
// Вариант 2: аллокация на лету (нужно явно указать тип слайса)
b, _ := ScanIntsLn[[]int32](br, nil)for {
s, err := ScanString(br, '\n') // прочтет строку и обрежет финальные пробелы
if err != nil {
break
}
// ...
}Нужны байты? Используй
ScanBytes. Обе функции читают до разделителя или конца файла, обрезают разделитель и финальные пробелы, возвращают копию данных. Размер строки ограничен только доступной оперативкой.
Аналогично числам.
var word1, word2 string
ScanWordLn(br, &word1, &word2)
words := make([]string, 0, 100)
words, _ = ScanWordsLn(br, words)
PrintWordLn(bw, word1, word2)
PrintWordsLn(bw, words)Ограничение: при сканировании длина токена не может превышать размер внутреннего буфера (по умолчанию 4КБ) минус один символ.
Для чисел это не критично, а при чтении длинных слов может возникнуть ошибка ErrTokenTooLong.
Если максимальная длина слова заранее неизвестна, используй ScanString/ScanBytes в цикле.
op := WO{Begin: "[", Sep: ", ", End: "]\n"}
PrintInts(bw, op, []int32{1, 2, 3, 4}) // [1, 2, 3, 4]var age int
var name string
ScanIntLn(br, &age)
name, _ = br.ReadString('\n') // стандартный метод bufio.Reader
name = strings.TrimSpace(name)
// Выводим через fmt
fmt.Fprintf(bw, "name: %q, age: %d\n", name, age)Тебе доступны все методы
bufio.Readerиbufio.Writer.
var rows, cols int
ScanIntLn(br, &rows, &cols) // можно читать несколько чисел за раз
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
ScanInts(br, matrix[i])
}
for _, row := range matrix {
PrintIntsLn(bw, row)
}var (
a []int8
b []int32
c []uint64
d []float32
e []float64
f []string // Слова (последовательности без пробелов)
)
a, _ = ScanIntsLn(br, a)
b, _ = ScanIntsLn(br, b)
c, _ = ScanIntsLn(br, c)
d, _ = ScanFloatsLn(br, d)
e, _ = ScanFloatsLn(br, e)
f, _ = ScanWordsLn(br, f)
PrintIntsLn(bw, a)
PrintIntsLn(bw, b)
PrintIntsLn(bw, c)
PrintFloatsLn(bw, d)
PrintFloatsLn(bw, e)
PrintWordsLn(bw, f)var product string // слово
var price float64
var amount uint
ScanAny(br, &product, &price, &amount)
PrintAnyLn(bw, "Товар:", &product, "\nЦена:", &price, "\nКол-во:", &amount)Экспериментальный функционал под тегом
-tags=any
- Всегда передавай указатели (не значения).
- В циклах объявляй переменные вне цикла и переиспользуй их.
Правильно:
var x int
for i := 0; i < N; i++ {
x = data[i]
PrintAny(bw, op, &x) // без аллокаций
}Неправильно (аллокации на каждой итерации):
for i := 0; i < N; i++ {
x := data[i] // новая переменная
PrintAny(bw, op, &x) // всё равно аллокация
}Также неправильно (передача значения):
PrintAny(bw, op, data[i]) // аллокация для каждого значенияБольше примеров можешь найти в github.com/aaa2ppp/ya-algo-training9-pub.
По умолчанию все функции сканирования и печати возвращают ошибки.
В задачах с гарантированно корректным вводом это приводит к шаблонным проверкам if err != nil { panic(err) }.
Чтобы избавиться от них, добавь тег сборки must:
go test -tags=must ./solve
cat input.txt | go run -tags=must ./solveПри активном теге must любая ошибка (кроме io.EOF) вызывает панику.
Это касается всех публичных функций ScanXXX/PrintXXX и их вариантов с Ln.
Важно: Функции библиотеки возвращают io.EOF только, если данных больше нет. Неполный ввод возвращает io.UnexpectedEOF. Функции никогда не возвращают ошибку, если прочли все запрошенные данные.
Пример:
// Без тега must
if _, err := ScanInt(br, &x); err != nil {
panic(err)
}
// С тегом - тоже поведение, но меньше кода
ScanInt(br, &x)Многие проверяющие платформы (Яндекс.Контест, Codeforces) запрещают внешние зависимости.
Ручное копирование парсеров в каждое решение - громоздко и неудобно.
contestio-inline решает эту проблему:
- Пишешь решение с точечным импортом
contestio- код чистый и краткий. - Одной командой утилита встраивает в твой файл только те функции библиотеки, которые реально используются.
- Полученный файл самодостаточен и готов к отправке.
# Установка
go install github.com/aaa2ppp/contestio/cmd/contestio-inline@latest
# Инлайн (требуется точечный импорт!)
contestio-inline main.go
# Откат (восстанавливает исходный код)
contestio-inline -clear main.goВажно:
- Утилита работает только с точечным импортом:
import . "github.com/aaa2ppp/contestio". Обычный или алиасный импорт не поддерживается. - Если используешь теги сборки (например,
-tags=must), укажи их:contestio-inline -tags=must main.go. - При ошибках (неверный импорт, синтаксическая ошибка) утилита ничего не меняет - твой код в безопасности. На крайний случай есть бэкап
main.go~.
Все бенчмарки - для 1M (2^20) чисел (int или float64), буфер 4 КБ.
Система: Intel Core i3-7100 @ 3.90GHz, Windows, Go 1.24.2.
В таблицах жирным выделены core функции contestio.
Время в миллисекундах - чем меньше, тем лучше.
| Метод | Время (ms) | Память (KB) | Аллокации |
|---|---|---|---|
fmt.Fscan |
473 | 12857 | 1 005 000 |
ReadString + Fields |
67 | 43670 | 3 400 |
Scanner + ScanWords |
106 | 0 | 0 |
scanSlice |
62 | 0 | 0 |
scanSliceLn |
62 | 0 | 0 |
scanVars (in loop) |
65 | 0 | 0 |
| Метод | Время (ms) | Память (KB) | Аллокации |
|---|---|---|---|
fmt.Fprintf |
109 | 7540 | 965 000 |
AppendInt |
37 | 50 | 2 600 |
printSlice |
39 | 0 | 0 |
printVals (in loop) |
52 | 0 | 0 |
Бенчмарки для чисел с плавающей точкой (нажми, чтобы развернуть)
| Метод | Время (ms) | Память (KB) | Аллокации |
|---|---|---|---|
fmt.Fscan |
759 | 24216 | 1 049 000 |
ReadString + Fields |
141 | 59646 | 5 400 |
Scanner + ScanWords |
194 | 0 | 0 |
scanSlice |
130 | 0 | 0 |
scanSliceLn |
131 | 0 | 0 |
scanVars (in loop) |
134 | 0 | 0 |
| Метод | Время (ms) | Память (KB) | Аллокации |
|---|---|---|---|
fmt.Fprintf |
201 | 8193 | 1 049 000 |
AppendFloat |
109 | 179 | 7 900 |
printSlice |
113 | 0 | 0 |
printVals (in loop) |
123 | 0 | 0 |
fmt.Fscan- универсален, для каждого значения создает интерфейс. 1 млн чисел - 1 млн аллокаций.ReadString + Fields- сначала должен все прочесть.Scanner + ScanWords- универсальность съедает время.AppendXXX- пишет напрямую в буфер, но иногда места уже нет (но это решаемо).- Функции
contestio- парсят и пишут числа напрямую в буфер.
Почему Contest IO не быстрее
AppendInt?
Потому что под капотом использует его. В бенчмаркахprintSliceна 2 ms медленнееAppendInt.
Это плата за возможность работать с любыми числовыми слайсами и разделители.
fmt- когда не торопишься и не жалко ресурсов.Scanner+AppendXXX- хорошая пара, но требует ручного кода.- Contest IO - дает скорость и ноль аллокаций с минимумом усилий.
Если хочешь абстрагироваться от конкретных типов и использовать интерфейсы - включи сахар.
Он активируется тегом сборки -tags=sugar и добавляет обертки над слайсами Ints[T Int] и Floats[T Float], которые реализуют интерфейсы для универсальных функций ScanSlice и PrintSlice.
Пример с «сахаром»:
package main
import (
"os"
. "github.com/aaa2ppp/contestio"
)
func main() {
br := NewReader(os.Stdin)
bw := NewWriter(os.Stdout)
defer bw.Flush()
var n int
var a Ints[int] // сахарный тип - работает, как обычный слайс
ScanIntLn(br, &n)
a = Resize(a, n)
ScanSlice(br, a)
a = solve(a)
PrintSliceLn(bw, a)
}
// Здесь нет ошибки! Да, Ints[int] можно присваивать переменной []int и наоборот
func solve(a []int) []int {
for i := range a {
a[i] *= a[i]
}
return a
}Особенности:
- Позволяет писать более абстрактный код, не привязываясь к конкретным типам срезов, и отказаться от специализированных функций вроде
ScanInts,PrintInts. - Типы
Ints[T],Floats[T]иWords[T]прозрачно работают как обычные слайсы: их можно передавать, изменять, индексировать и итерироватьrange. - Требует указания тега сборки:
go build -tags=sugar. - Для комфортной работы в VS Code необходимо добавить
"go.buildTags": "sugar"в настройки проекта (иначе автодополнение и тесты не увидят код с тегом).
Сахар не обязателен для эффективного использования библиотеки. Основной API (ScanInts, PrintInts и т.п.) справляется со всем, что нужно на контестах.
Сахар - это дополнительный слой для тех, кто хочет поэкспериментировать с интерфейсами или использовать библиотеку вне соревнований.
Если ты используешь теги must или sugar, добавь в .vscode/settings.json:
"go.buildTags": "must,sugar"После этого автодополнение, анализ кода и тесты будут корректно видеть код, защищённый тегами.