Как читать из стандартного ввода в Go: от новичка до профи
Вы когда-нибудь задумывались, как заставить вашу Go-программу «слушать» пользователя? Как написать утилиту, которая принимает команды из консоли, или интерактивный CLI-инструмент? Если да, то вы попали по адресу. Сегодня мы разберем чтение из стандартного ввода (stdin) в Go — настолько подробно, насколько это вообще возможно.
Эта тема — настоящий хит на Stack Overflow. Вопрос «How can I read from standard input in the console?» собрал тысячи просмотров. И не зря: стандартный ввод — это база, без которой не обходится ни один серьезный проект на Go. Мы адаптируем этот вопрос под русскоязычную аудиторию и превратим его в полноценный туториал.
В этой статье вы узнаете:
- Что такое
os.Stdinи как с ним работать - Как использовать
bufio.Scannerдля построчного чтения - Почему
fmt.Scan— это палка о двух концах - Как читать целые файлы из stdin
- Секреты обработки ошибок и производительности
Поехали!
Что такое stdin и зачем он нужен в Go?
Стандартный ввод (stdin) — это поток данных, который ваша программа получает из консоли или из другого процесса. В Go он представлен переменной os.Stdin типа *os.File. Это шлюз, через который данные попадают в вашу программу.
Зачем это нужно? Представьте:
- Вы пишете CLI-утилиту, которая обрабатывает ввод пользователя
- Вы создаете парсер логов, который читает данные через пайп (
cat log.txt | myapp) - Вам нужно реализовать интерактивный ввод с подсказками
Go, будучи языком для системного программирования, предоставляет элегантные инструменты для работы со stdin. Давайте разберем их по порядку.
Метод 1: bufio.Scanner — король построчного чтения
Самый популярный и рекомендуемый способ чтения из stdin в Go — это bufio.Scanner. Он прост, эффективен и решает 90% задач.
Вот как выглядит классический пример:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("Введите текст (Ctrl+D для завершения):")
for scanner.Scan() {
line := scanner.Text()
fmt.Println("Вы ввели:", line)
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "Ошибка чтения:", err)
}
}
Как это работает?
bufio.NewScanner(os.Stdin)— создает сканер, который читает из стандартного вводаscanner.Scan()— возвращаетtrue, пока есть данные (по умолчанию до символа новой строки)scanner.Text()— возвращает текущую строку без символа\n- Цикл прерывается при
EOF(Ctrl+D в Linux/Mac, Ctrl+Z в Windows)
Этот код — база для любого консольного ввода Go. Он отлично работает с пайпами и интерактивным вводом.
Кастомизация Scanner
По умолчанию Scanner делит ввод по строкам. Но вы можете изменить это:
// Чтение по словам
scanner.Split(bufio.ScanWords)
// Чтение по байтам
scanner.Split(bufio.ScanBytes)
// Чтение по символам (рунам)
scanner.Split(bufio.ScanRunes)
А еще можно задать свой сплиттер — например, для CSV или кастомных разделителей. Но это уже тема для отдельной статьи.
Метод 2: fmt.Scan — быстро и грязно
Если вам нужно ввести одно значение — число, строку или несколько переменных — fmt.Scan придет на помощь. Это как scanf в C, только проще.
package main
import "fmt"
func main() {
var name string
var age int
fmt.Print("Введите имя и возраст: ")
n, err := fmt.Scan(&name, &age)
if err != nil {
fmt.Println("Ошибка ввода:", err)
return
}
fmt.Printf("Прочитано %d значений: %s, %d лет\n", n, name, age)
}
Плюсы: коротко, удобно для простых скриптов.
Минусы: проблемы с пробелами в строках, нет гибкости, сложно обрабатывать ошибки.
Для чтения строки с пробелами используйте fmt.Scanln или bufio.Reader:
reader := bufio.NewReader(os.Stdin)
fmt.Print("Введите строку: ")
text, _ := reader.ReadString('\n')
fmt.Println("Вы ввели:", text)
Но помните: ReadString возвращает строку с символом \n на конце. Удалить его можно через strings.TrimSpace(text).
Метод 3: os.Stdin.Read — низкоуровневый контроль
Иногда (очень редко) вам нужен полный контроль над процессом чтения из стандартного ввода. Например, при работе с бинарными протоколами. Тогда используйте os.Stdin.Read():
package main
import (
"fmt"
"os"
)
func main() {
buf := make([]byte, 1024)
n, err := os.Stdin.Read(buf)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Printf("Прочитано %d байт: %s", n, string(buf[:n]))
}
Этот метод блокирует выполнение, пока не получит данные. Его стоит использовать только когда bufio.Scanner не подходит.
Метод 4: Чтение всего stdin сразу
Если вы знаете, что ввод будет небольшим (например, данные из пайпа), можно прочитать все разом:
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
data, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("Прочитано", len(data), "байт")
fmt.Println(string(data))
}
Внимание: не используйте этот метод для интерактивного ввода — программа зависнет, ожидая EOF. Он хорош только для пайпов и перенаправлений.
Сравнение методов: что выбрать?
Вот краткая таблица, чтобы вы могли быстро выбрать правильный инструмент: