Zero-Allocation I/O for Go Contests: Six Engineering Solutions
Go developers in competitive programming often hit limits with standard I/O tools. fmt.Fscan allocates memory on every call, bufio.Scanner is locked into scanning mode, and manual parsing with bufio.Reader means writing tons of boilerplate. The contestio library fixes this by working directly with bufio.Reader's internal buffer, delivering zero-allocation for all operations.
Six key solutions blend the speed of manual parsing, the ease of fmt, and the flexibility of bufio. It handles mixed input types—numbers, strings, even char-by-char parsing—without mode switches.
Direct Buffer Access: _nextToken Without Copying
The core trick is the _nextToken function, which returns a byte slice from bufio.Reader's internal buffer. No allocations or copying. You can still use bufio.Reader methods like ReadString, Peek, and ReadByte.
var age int
var name string
ScanInt(br, &age) // via contestio
name, _ = br.ReadString('\n') // standard method
name = strings.TrimSpace(name)
High-level ScanInt and ScanWord play nice with low-level calls. No rigid "scanner mode"—read data in any order you want.
Universal Integer Parser with Generics
One generic _parseInt[T Int] handles int8, uint16, int64, uint64. The compiler spits out specialized code for each type:
- Skips sign handling for unsigned types.
- Embeds range constants (
absMin = 1<<7forint8). - Custom digit loop up to 20 digits, with fallback to
ParseUint.
func _parseIntT Int (T, error) {
unsigned := ^T(0) >= 0
// sign handling, digit loop, range checks
}
Assembly shows tight, optimized code with no extra checks. Versatility without performance hits.
Zero-Allocation Output: Scratch Buffer for Dynamic Data
For writing, it uses bufio.Writer with an attached scratch [32]byte array. If the main buffer's too full, it fills the scratch buffer and flushes with a single Write.
func _writeAppendFunc(bw *Writer, appendVal func([]byte, T) []byte, v T) error {
var buf []byte
if bw.Available() < len(bw.scratch) {
buf = bw.scratch[:0]
} else {
buf = bw.AvailableBuffer()
}
buf = appendVal(buf, v)
_, err := bw.Write(buf)
return err
}
Result: zero-allocation output for any supported type.
Lightweight Reflection for Any Types
Typed functions like ScanInt[T], ScanFloat[T], ScanWord[T] form the main API. For flexibility, ScanAny(br, &product, &price) uses a parser table by reflect.Kind:
var _parseAnyTab = []_parseAnyFunc{
reflect.Int: _parseIntToAny[int],
reflect.Int8: _parseIntToAny[int8],
reflect.Float32: _parseFloatToAny[float32],
reflect.String: _parseWordToAny[string],
}
Reflection just dispatches: it checks the arg type and calls the right parser. Pass pointers to reusable vars outside loops—no allocations.
var x int
for i := 0; i < N; i++ {
x = data[i]
PrintAny(bw, op, &x)
}
Must Tags: Panic or Errors via Build Tags
Contest input is usually clean, but checks are essential. Skip if err != nil { panic(err) } after every call—use private _must functions.
func _scanSliceT any (int, error) {
return _must(_scanSliceCommon(br, parse, a))
}
In must.go (//go:build must), _must panics on errors except io.EOF. In nomust.go, it returns error. Compiler inlines the right version.
go run -tags=must main.go # panics
go run main.go # errors
Inline Utility for Self-Contained Solutions
Platforms like Codeforces ban external packages. contestio-inline analyzes AST and embeds only the used contestio functions into main.go.
contestio-inline main.go # embeds
contestio-inline -clear main.go # restores
Needs a pinpoint import import . "github.com/aaa2ppp/contestio". The tool checks compilation, backs up files, and supports -tags=must.
Inline benefits:
- No dependencies.
- Only needed code (dependency graph).
- Your solution code stays untouched.
Benchmarks: 1M Numbers, 4KB Buffer
| Method | Read Time, ms | Allocations | Write Time, ms | Allocations |
|--------|---------------|-------------|----------------|-------------|
| fmt.Fscan/Fprint | 473 | 1 005 000 | 109 | 965 000 |
| Scanner+Atoi | 106 | 0 | - | - |
| strconv.AppendInt | - | - | 37 | 2 600 |
| contestio | 62 | 0 | 39 | 0 |
Testing: Intel Core i3-7100, Go 1.24.2, Windows.
Key Takeaways
- Zero-allocation read/write via direct buffer access.
- Generics generate optimal code per integer type.
- Scratch buffer enables allocation-free output.
- Reflection only for dispatch, not serialization.
mustbuild tags cut error-checking boilerplate.
— Editorial Team
No comments yet.