· 12 мин 👁 1.1k Начинающий

Язык Go — пакет fmt (часть 2)

Сканирование в Go — функции Scan, Scanf, Sscan, обработка пробелов и переносов строк, ширина поля, интерфейс Scanner и примеры форматирования.

fmtформатированиеscanscanfsscanвводсканирование
Содержание

Пакет fmt предоставляет симметричный набор функций для сканирования (чтения) форматированного текста — по аналогии с scanf из C.

Семейства функций сканирования

Функции сканирования делятся на три группы в зависимости от источника данных:

  • Scan, Scanf, Scanln — читают из os.Stdin
  • Fscan, Fscanf, Fscanln — читают из указанного io.Reader
  • Sscan, Sscanf, Sscanln — читают из строки-аргумента

Поведение внутри каждой группы определяется суффиксом:

  • Scan / Fscan / Sscan — переносы строк в вводе считаются пробелами
  • Scanln / Fscanln / Sscanln — останавливаются на переносе строки; в конце ввода ожидается перенос строки или EOF
  • Scanf / Fscanf / Sscanf — разбирают аргументы по строке формата (аналогично Printf)

Строка формата в Scanf

В строке формата глагол, введённый символом %, потребляет и разбирает входные данные. Любой символ кроме %, пробела или переноса строки потребляет ровно тот же символ из ввода — он должен присутствовать.

Пробелы в строке формата обрабатываются так:

  • перенос строки с нулём или более пробелами перед ним — потребляет нуль или более пробелов во вводе, а затем один перенос строки или конец ввода
  • пробел после переноса строки в строке формата — потребляет нуль или более пробелов во вводе
  • любая последовательность из одного и более пробелов — потребляет максимально возможное количество пробелов во вводе; если последовательность не примыкает к переносу строки, она должна потребить хотя бы один пробел или встретить конец ввода

Обработка пробелов и переносов строк отличается от поведения scanf в C: в C переносы строк — это просто пробелы, и отсутствие пробелов во вводе никогда не является ошибкой.

Глаголы форматирования

Глаголы работают аналогично Printf. Например, %x читает целое число в шестнадцатеричной нотации, а %v читает в формате по умолчанию.

Глаголы %p и %T, а также флаги # и + не поддерживаются.

Для чисел с плавающей точкой и комплексных чисел все допустимые глаголы форматирования (%b, %e, %E, %f, %F, %g, %G, %x, %X и %v) эквивалентны — все они принимают как десятичную, так и шестнадцатеричную нотацию (например "2.3e+7", "0x4.5p-8"), а также разделители разрядов подчёркиванием (например "3.14159_26535_89793").

Каждый глагол, кроме %c, по умолчанию пропускает ведущие пробелы во вводе. Глагол %s%v при чтении в строку) останавливается на первом пробеле или переносе строки.

При сканировании целых чисел без строки формата или с глаголом %v принимаются привычные префиксы систем счисления: 0b (двоичная), 0o и 0 (восьмеричная), 0x (шестнадцатеричная), а также разделители разрядов подчёркиванием.

Ширина поля

Ширина интерпретируется в тексте ввода, но синтаксиса для точности при сканировании нет (то есть %5.2f недопустим — только %5f).

Если ширина задана, она применяется после пропуска ведущих пробелов и ограничивает максимальное количество рун, считываемых для данного глагола:

var s string
var i int

fmt.Sscanf(" 1234567 ", "%5s%d", &s, &i)
// s = "12345", i = 67

fmt.Sscanf(" 12 34 567 ", "%5s%d", &s, &i)
// s = "12", i = 34

Интерфейс Scanner

Во всех функциях сканирования, если операнд реализует метод Scan (то есть реализует интерфейс Scanner), для чтения ввода будет использован именно этот метод.

Все аргументы для сканирования должны быть либо указателями на базовые типы, либо реализациями интерфейса Scanner.

Если количество успешно просканированных аргументов меньше числа переданных — возвращается ошибка.

Технические детали

В отличие от Scanf, функции Sscanf и Fscanf не обязаны потреблять весь ввод. Узнать, сколько входной строки обработал Sscanf, невозможно.

Во всех функциях сканирования комбинация \r\n в вводе обрабатывается как одиночный перенос строки \n.

Fscan может прочитать один символ (руну) дальше возвращённого результата — это значит, что в цикле можно потерять часть ввода. Проблема актуальна только при отсутствии пробелов между значениями. Если io.Reader, переданный в Fscan, реализует ReadRune, этот метод будет использован для чтения символов. Если ридер также реализует UnreadRune — символ будет возвращён назад, и данные не потеряются. Чтобы добавить эти методы к ридеру без такой возможности — используйте bufio.NewReader.

Пример: глаголы форматирования

Следующий пример демонстрирует основы форматирования с помощью строки формата. Функции Printf, Sprintf и Fprintf принимают строку формата, задающую способ вывода последующих аргументов. Глагол %d означает вывод целого числа в десятичной системе. Глагол %v выводит значение в формате по умолчанию — так же, как Print или Println. Специальный глагол %T выводит тип аргумента, а не его значение.

package main

import (
	"fmt"
	"math"
	"time"
)

func main() {
	// Базовые примеры: %v — формат по умолчанию, для целых это десятичная
	// запись, которую можно запросить явно через %d.
	// Вывод совпадает с тем, что генерирует Println.
	integer := 23
	// Каждая из строк ниже выведет "23" (без кавычек).
	fmt.Println(integer)
	fmt.Printf("%v\n", integer)
	fmt.Printf("%d\n", integer)

	// Специальный глагол %T показывает тип, а не значение.
	fmt.Printf("%T %T\n", integer, &integer)
	// Выведет: int *int

	// Println(x) эквивалентен Printf("%v\n", x), поэтому далее используем
	// только Printf. Каждый пример демонстрирует форматирование
	// значений конкретного типа. Формат начинается с %v (вывод по умолчанию),
	// затем — один или несколько пользовательских форматов.

	// Булевы значения: %v и %t выводят true или false.
	truth := true
	fmt.Printf("%v %t\n", truth, truth)
	// Выведет: true true

	// Целые числа: %v и %d — десятичная, %x — шестнадцатеричная,
	// %o — восьмеричная, %b — двоичная.
	answer := 42
	fmt.Printf("%v %d %x %o %b\n", answer, answer, answer, answer, answer)
	// Выведет: 42 42 2a 52 101010

	// Числа с плавающей точкой: %v и %g — компактный вывод,
	// %f — с десятичной точкой, %e — в экспоненциальной форме.
	// %6.2f задаёт ширину и точность: общая ширина поля — 6 символов
	// (обратите внимание на ведущие пробелы), знаков после запятой — 2.
	pi := math.Pi
	fmt.Printf("%v %g %.2f (%6.2f) %e\n", pi, pi, pi, pi, pi)
	// Выведет: 3.141592653589793 3.141592653589793 3.14 (  3.14) 3.141593e+00

	// Комплексные числа выводятся как пара вещественной и мнимой частей
	// в скобках; за мнимой частью следует 'i'.
	point := 110.7 + 22.5i
	fmt.Printf("%v %g %.2f %.2e\n", point, point, point, point)
	// Выведет: (110.7+22.5i) (110.7+22.5i) (110.70+22.50i) (1.11e+02+2.25e+01i)

	// Руны — целые числа, но %c выводит символ Unicode с указанным кодом.
	// %q — символ в одинарных кавычках, %U — код в формате Unicode,
	// %#U — и код, и сам символ, если он печатаемый.
	smile := '😀'
	fmt.Printf("%v %d %c %q %U %#U\n", smile, smile, smile, smile, smile, smile)
	// Выведет: 128512 128512 😀 '😀' U+1F600 U+1F600 '😀'

	// Строки: %v и %s — как есть, %q — в двойных кавычках,
	// %#q — в обратных кавычках.
	placeholders := `foo "bar"`
	fmt.Printf("%v %s %q %#q\n", placeholders, placeholders, placeholders, placeholders)
	// Выведет: foo "bar" foo "bar" "foo \"bar\"" `foo "bar"`

	// Мепы с %v выводят ключи и значения в формате по умолчанию.
	// %#v (# называется флагом) показывает мепу в синтаксисе Go.
	// Ключи выводятся в отсортированном порядке.
	isLegume := map[string]bool{
		"peanut":    true,
		"dachshund": false,
	}
	fmt.Printf("%v %#v\n", isLegume, isLegume)
	// Выведет: map[dachshund:false peanut:true] map[string]bool{"dachshund":false, "peanut":true}

	// Структуры с %v выводят значения полей в формате по умолчанию.
	// %+v добавляет имена полей, %#v — синтаксис Go.
	person := struct {
		Name string
		Age  int
	}{"Kim", 22}
	fmt.Printf("%v %+v %#v\n", person, person, person)
	// Выведет: {Kim 22} {Name:Kim Age:22} struct { Name string; Age int }{Name:"Kim", Age:22}

	// Для указателей формат по умолчанию показывает значение с амперсандом.
	// %p выводит адрес в шестнадцатеричном виде. Здесь передаём типизированный
	// nil для %p, поскольку адрес реального указателя меняется от запуска к запуску.
	pointer := &person
	fmt.Printf("%v %p\n", pointer, (*int)(nil))
	// Выведет: &{Kim 22} 0x0
	// fmt.Printf("%v %p\n", pointer, pointer)
	// Выведет: &{Kim 22} 0x010203 // см. комментарий выше

	// Массивы и срезы форматируются поэлементно.
	fruits := [5]string{"apple", "banana", "cherry", "mango", "peach"}
	fmt.Printf("%v %q\n", fruits, fruits)
	// Выведет: [apple banana cherry mango peach] ["apple" "banana" "cherry" "mango" "peach"]

	kFruits := fruits[:3]
	fmt.Printf("%v %q %#v\n", kFruits, kFruits, kFruits)
	// Выведет: [apple banana cherry] ["apple" "banana" "cherry"] []string{"apple", "banana", "cherry"}

	// Срезы байт — особый случай. Целочисленные глаголы (%d) выводят элементы
	// в соответствующем формате. %s и %q обрабатывают срез как строку.
	// У %x есть особая форма с флагом пробела: ставит пробел между байтами.
	cmd := []byte("a⌘")
	fmt.Printf("%v %d %s %q %x % x\n", cmd, cmd, cmd, cmd, cmd, cmd)
	// Выведет: [97 226 140 152] [97 226 140 152] a⌘ "a⌘" 61e28c98 61 e2 8c 98

	// Типы, реализующие Stringer, выводятся как строки. Поскольку Stringer
	// возвращает строку, можно использовать строковые глаголы, например %q.
	now := time.Unix(123456789, 0).UTC() // time.Time реализует fmt.Stringer
	fmt.Printf("%v %q\n", now, now)
	// Выведет: 1973-11-29 21:33:09 +0000 UTC "1973-11-29 21:33:09 +0000 UTC"
}

Пример: Print, Println и Printf

Print, Println и Printf по-разному расставляют пробелы между аргументами. Println всегда добавляет пробелы между элементами, Print добавляет пробелы только между аргументами, ни один из которых не является строкой, а Printf выводит ровно то, что задано строкой формата. Sprint, Sprintln, Sprintf, Fprint, Fprintln и Fprintf ведут себя так же, как соответствующие Print, Println и Printf.

package main

import (
	"fmt"
	"math"
)

func main() {
	a, b := 3.0, 4.0
	h := math.Hypot(a, b)

	// Print добавляет пробел между аргументами, только если ни один из них
	// не является строкой. Перенос строки не добавляется — задаём его явно.
	fmt.Print("The vector (", a, b, ") has length ", h, ".\n")
	// Выведет: The vector (3 4) has length 5.

	// Println всегда вставляет пробелы между аргументами, поэтому вывод
	// будет отличаться от Print — появятся лишние пробелы.
	// Println также всегда добавляет перенос строки в конце.
	fmt.Println("The vector (", a, b, ") has length", h, ".")
	// Выведет: The vector ( 3 4 ) has length 5 .

	// Printf даёт полный контроль над выводом, но требует строки формата.
	// Перенос строки не добавляется — задаём его явно в конце формата.
	fmt.Printf("The vector (%g %g) has length %g.\n", a, b, h)
	// Выведет: The vector (3 4) has length 5.
}

Итоги

  • Три семейства функций сканирования: Scan* → stdin, Fscan*io.Reader, Sscan* → строка
  • Суффикс определяет поведение: без суффикса — переносы строк как пробелы, ln — остановка на переносе строки, f — разбор по строке формата
  • Глаголы аналогичны Printf, но %p, %T, флаги # и + не поддерживаются
  • Для float все глаголы эквивалентны и принимают как десятичную, так и шестнадцатеричную нотацию
  • Ширина ограничивает количество считываемых рун; точность не поддерживается
  • Все аргументы должны быть указателями или реализовывать интерфейс Scanner
  • Print, Println и Printf по-разному расставляют пробелы: Println — всегда, Print — только между нестроковыми аргументами, Printf — строго по формату

Предыдущий шаг: Пакет fmt (часть 1) — функции вывода, глаголы форматирования, флаги, ширина и точность. Предыдущий шаг: Пакет fmt (часть 3)