суббота, 26 января 2019 г.

Эффективный Go: печать

Форматированная печать в Go использует стиль, похожий на стиль в C printf семье, но более богатая и более общая. Функции живут в fmt пакете и имеют заглавные имена: fmt.Printf, fmt.Fprintf, fmt.Sprintf и так далее. Строковые функции (Sprintf и т. д.) возвращают строку, а не заполняют предоставленный буфер.

Вам не нужно предоставлять строку формата. Для каждого из Printf, Fprintf и Sprintf есть другая пара функций, например Print и Println. Эти функции не принимают строку формата, а генерируют формат по умолчанию для каждого аргумента. Версии Println также содержат пробел между аргументами и добавляют новую строку к выводу, а версии Print добавляют пробелы только в том случае, если операнд с обеих сторон является строкой. В следующем примере каждая строка выдает один и тот же результат.

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

Функции форматированной печати fmt.Fprint и похожие на нее принимают в качестве первого аргумента любой объект, который реализует интерфейс io.Writer; переменные os.Stdout и os.Stderr являются известными примерами таких объектов.

Здесь вещи начинают расходиться с C. Во-первых, числовые форматы, такие как %d не принимают флаги для подписи или размера; вместо этого процедуры печати используют тип аргумента для определения этих свойств.

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

печатает

18446744073709551615 ffffffffffffffff; -1 -1

Если вы просто хотите преобразование по умолчанию, например десятичное для целых чисел, вы можете использовать формат, принимающий любые форматы, %v ("value"); результат точно такой же что будут производить Print и Println. Кроме того, этот формат может печатать любое значение, даже массивы, срезы, структуры и карты. Вот оператор печати для карты часовых поясов, определенной в предыдущем посте.

fmt.Printf("%v\n", timeZone)  
// или просто fmt.Println(timeZone)

который дает вывод

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

Конечно, для карт ключи могут быть выведены в любом порядке. При печати структуры измененный формат %+v аннотирует поля структуры с их именами, а для любого значения альтернативный format %#v печатает значение в полном синтаксисе Go.

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

печатает

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, 
                "EST":-18000, "UTC":0, "MST":-25200}

(Обратите внимание на амперсанды.) Этот формат строки в кавычках также доступен через %q, когда применяется к значению типа string или []byte. Альтернативный формат %#q будет использовать обратные кавычки, если это возможно. (Формат %q также применяется к целым числам и рунам, создавая руну константа в кавычках.) Кроме того, %x работает со строками, байтовыми массивами и байтовыми срезами, а также с целыми числами, генерируя длинную шестнадцатеричную строку, а с пробелом в формате (% x) - ставит пробелы между байтами.

Другой удобный формат - это %T, который печатает тип значения.

fmt.Printf("%T\n", timeZone)

печатает

map[string] int

Если вы хотите контролировать формат по умолчанию для пользовательского типа, все, что требуется, это определить метод с сигнатурой String() string для типа. Для нашего простого типа T это может выглядеть следующим образом.

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

для печати в формате

7/-2.35/"abc\tdef"

Если вам нужно вывести значения типа T, а также указатели на T, получатель для String должен иметь тип значения; этот пример использовал указатель, потому что это более эффективно и идиоматично для структурных типов.

Наш метод String может вызывать Sprintf, потому что процедуры печати полностью реентерабельны и могут быть завернуты таким образом. Есть одна важная деталь, для понимания об этом подходе: не создавайте метод String, вызывая Sprintf таким образом, что он будет повторяться в вашем String методе до бесконечности. Это может произойти, если Sprintf вызов пытается распечатать получателя напрямую в виде строки, которая в свою очередь снова вызовет метод. Это распространенная ошибка, которую легко допустить, как показывает следующий пример.

type MyString string
func (m MyString) String() string {
    // Ошибка: будет возвращаться бесконечно.
    return fmt.Sprintf("MyString=%s", m) 
}

Это также легко исправить: преобразовать аргумент в базовый тип строки, который не имеет метода.

type MyString string
func (m MyString) String() string {
    // OK: обратите внимание на преобразование.
    return fmt.Sprintf("MyString=%s", string(m)) 
}

Другой метод печати заключается в передаче аргументов процедуры печати непосредственно другой такой программе. Сигнатура Printf использует тип ... interface {} в качестве последнего аргумента для указания, что означает - произвольное количество параметров (произвольного типа) может появиться после формата.

func Printf(format string, v ...interface{}) (n int, err error) {

В функции Printf v действует как переменная типа []interface{}, но если он передается другой функции с переменным числом, он действует как регулярный список аргументов. Вот реализация функции log.Println, которую мы использовали выше. Она передает свои аргументы непосредственно fmt.Sprintln для фактического форматирования.

// Println печатает стандартный логгер в манере fmt.Println
func Println(v ...interface{}) {
    // Output принимает параметры (int, string)
    std.Output(2, fmt.Sprintln(v...))  
}

Мы пишем ... после v во вложенном вызове Sprintln, чтобы сообщить компилятору рассматривать v как список аргументов; в противном случае он просто передал бы v как один аргумент среза.

В печати есть даже больше, чем мы рассмотрели здесь. Смотрите документацию по godoc для пакета fmt для деталей.

Кстати, параметр ... может быть определенного типа, например ... int для функции min, которая выбирает наименьшее из списка целых чисел:

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // наибольший int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}


Читайте также:


Комментариев нет:

Отправить комментарий