diff --git a/effective_go_ru.md b/effective_go_ru.md index 2adffdd..552f6ed 100644 --- a/effective_go_ru.md +++ b/effective_go_ru.md @@ -34,6 +34,9 @@ - [Выделение (аллокация) памяти с помощью `make`](#выделение-аллокация-памяти-с-помощью-make) - [Массивы](#массивы) - [Слайсы (Срезы, Slices)](#слайсы-срезы-slices) + - [Двумерные срезы](#двумерные-срезы) + - [Карты](#карты) + - [Печать](#печать) ## Форматирование @@ -912,3 +915,270 @@ func Append(slice, data []byte) []byte { **Мы должны вернуть срез после изменения, потому что, хотя функция Append может изменять элементы среза, сам срез (структура, содержащая указатель, длину и вместимость) передается по значению.** Идея добавления в срез настолько полезна, что она реализована во встроенной функции `append`. Чтобы понять дизайн этой функции, нам нужно немного больше информации, к которой мы вернёмся позже. + + +### Двумерные срезы + +Массивы и срезы в Go **одномерны**. Чтобы создать эквивалент двумерного массива или среза, необходимо определить **массив массивов или срез срезов**, например: + +```go +type Transform [3][3]float64 // 3x3 массив, на самом деле массив массивов. +type LinesOfText [][]byte // Срез срезов байтов. +``` + +Поскольку срезы имеют переменную длину, **каждый внутренний срез может иметь разную длину**. Это может быть распространенной ситуацией, как в нашем примере `LinesOfText`: каждая строка имеет независимую длину. + +```go +text := LinesOfText{ + []byte("Now is the time"), + []byte("for all good gophers"), + []byte("to bring some fun to the party."), +} +``` + +Иногда необходимо выделить двумерный срез, например, при обработке сканирующих строк пикселей. **Существует два способа достичь этого**. Один из способов — выделять каждый срез независимо; другой — выделить один массив и направить в него отдельные срезы. Какой способ использовать, зависит от вашего приложения. Если срезы могут увеличиваться или уменьшаться, их следует выделять независимо, чтобы избежать перезаписи следующей строки; если нет, то может быть эффективнее создать объект с помощью одного выделения памяти. Для справки, вот схемы двух методов. Сначала — строка за строкой: + +```go +// Для примера они содержат такие значения. +YSize := 20 +XSize := 10 +// Выделием срез верхнего уровня. +picture := make([][]uint8, YSize) // Одна строка на единицу y. +// Переберите строки, выделяя срез для каждой строки. +for i := range picture { + picture[i] = make([]uint8, XSize) +} +``` + +А теперь — одно выделение, нарезанное на строки: + +```go +YSize := 20 +XSize := 10 +// Выделите срез верхнего уровня, так же как и прежде. +picture := make([][]uint8, YSize) // Одна строка на единицу y. +// Выделите один большой срез для всех пикселей. +pixels := make([]uint8, XSize*YSize) // Имеет тип []uint8, хотя picture — это [][]uint8. +// Переберите строки, нарезая каждую строку из начала оставшегося среза пикселей. +for i := range picture { + picture[i], pixels = pixels[:XSize], pixels[XSize:] +} +``` + +### Карты + +**Карты — это удобная и мощная встроенная структура данных, которая связывает значения одного типа (ключ) со значениями другого типа (элемент или значение)**. Ключ может быть любого типа, для которого определён оператор равенства, например, целые числа, числа с плавающей точкой и комплексные числа, строки, указатели, интерфейсы (если динамический тип поддерживает равенство), структуры и массивы. Срезы не могут использоваться в качестве ключей карт, потому что на них не определено равенство. Подобно срезам, карты хранят ссылки на основную структуру данных. **Если вы передадите карту функции, которая изменяет содержимое карты, изменения будут видны вызывающему коду**. + +Карты можно создавать с помощью обычного синтаксиса составного литерала с парами ключ-значение, разделёнными двоеточием, поэтому их легко строить при инициализации. + +```go +var timeZone = map[string]int{ + "UTC": 0*60*60, + "EST": -5*60*60, + "CST": -6*60*60, + "MST": -7*60*60, + "PST": -8*60*60, +} +``` + +Присваивание и извлечение значений из карты синтаксически выглядит так же, как и для массивов и срезов, за исключением того, что индекс не обязательно должен быть целым числом. + +```go +offset := timeZone["EST"] +``` + +Попытка получить значение карты по ключу, которого нет в карте, **вернёт нулевое значение для типа элементов карты**. Например, если карта содержит целые числа, поиск несуществующего ключа вернёт 0. Множество можно реализовать как карту с типом значения bool. Установите значение карты в true, чтобы добавить значение в множество, а затем проверьте его с помощью простого индексирования. + +```go +attended := map[string]bool{ + "Ann": true, + "Joe": true, + ... +} + +if attended[person] { // будет false, если person нет в карте + fmt.Println(person, "was at the meeting") +} +``` + +**Иногда нужно отличить отсутствующую запись от нулевого значения**. Есть ли запись для `"UTC"`, или это 0, потому что её вообще нет в карте? Вы можете узнать это с помощью формы множественного присваивания. + +```go +var seconds int +var ok bool +seconds, ok = timeZone[tz] +``` + +По очевидным причинам это называется идиомой **“comma-ok”**. В этом примере, если `tz` присутствует, `seconds` будет установлен соответственно, а `ok` будет `true`; если нет, `seconds` будет установлен в `0`, а `ok` будет `false`. Вот функция, которая объединяет это с хорошим отчётом об ошибке: + +```go +func offset(tz string) int { + if seconds, ok := timeZone[tz]; ok { + return seconds + } + log.Println("unknown time zone:", tz) + return 0 +} +``` + +Чтобы проверить присутствие в карте, не беспокоясь о фактическом значении, вы можете использовать пустой идентификатор (_) вместо обычной переменной для значения. + +```go +_, present := timeZone[tz] +``` + +Чтобы удалить запись из карты, используйте встроенную функцию `delete`, аргументы которой — это карта и ключ, который нужно удалить. **Это безопасно, даже если ключ уже отсутствует в карте.** + +```go +delete(timeZone, "PDT") // Удаляем ключ "PDT" и его значение, соответственно +``` + +### Печать + +Форматированная печать в Go использует стиль, похожий на семействo функций `printf` в `C`, но более богатый и универсальный. Функции находятся в пакете `fmt` и имеют имена с заглавных букв: `fmt.Printf`, `fmt.Fprintf`, `fmt.Sprintf` и т.д. **Функции строк (например, `Sprintf` и др.) возвращают строку, а не заполняют предоставленный буфер.** + +Не обязательно указывать строку формата. Для каждой из функций `Printf`, `Fprintf` и `Sprintf` существуют парные функции, например `Print` и `Println`. Эти функции не принимают строку формата, а вместо этого генерируют формат по умолчанию для каждого аргумента. **Версии `Println` также вставляют пробел между аргументами и добавляют новую строку в вывод**, **тогда как версии `Print` добавляют пробелы только в случае, если операнд с обеих сторон не является строкой**. В этом примере каждая строка производит одинаковый вывод. + +```go +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`, не принимают флаги для знаковости или размера; вместо этого функции печати используют тип аргумента для определения этих свойств. + +```go +var x uint64 = 1<<64 - 1 +fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x)) +``` +печатает +```go +18446744073709551615 ffffffffffffffff; -1 -1 +``` +Если вы хотите получить формат по умолчанию, такой как десятичный для целых чисел, вы можете использовать универсальный формат `%v` (для "значения"); результат будет таким же, как `Print` и `Println`. **Более того, этот формат может печатать любое значение, включая массивы, срезы, структуры и карты**. Вот пример для карты временных зон, определённой в предыдущем разделе. + +```go +fmt.Printf("%v\n", timeZone) // или просто fmt.Println(timeZone) +``` +что выводит: + +```go +map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0] +``` + +**Для карт функции `Printf` и другие сортируют вывод лексикографически по ключу.** + +При печати структуры модифицированный формат `%+v` аннотирует поля структуры их именами, а для любого значения альтернативный формат `%#v` печатает значение в полном синтаксисе Go. + +```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) +``` + +печатает + +```go +&{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, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0} +``` + +**(Обратите внимание на амперсанды.)** Этот формат строки также доступен через `%q`, если применить его к значению типа `string` или `[]byte`. Альтернативный формат `%#q` будет использовать обратные кавычки, если это возможно. (Формат `%q` также применяется к целым числам и рунам, создавая строковую константу с одинарными кавычками.) Также `%x` работает со строками, массивами байтов и срезами байтов, а также с целыми числами, создавая длинную строку в шестнадцатеричном формате, **и при использовании пробела в формате (`% x`) добавляет пробелы между байтами**. + +Другой полезный формат — `%T`, который печатает тип значения. + +```go +fmt.Printf("%T\n", timeZone) +``` + +печатает + +```go +map[string]int +``` + +Если вы хотите контролировать формат по умолчанию для пользовательского типа, всё, что нужно сделать, это определить метод с сигнатурой `String() string` для типа. Для нашего простого типа `T` это может выглядеть так. + +```go +func (t *T) String() string { + return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c) +} +fmt.Printf("%v\n", t) +``` + +чтобы напечатать в формате + +```go +7/-2.35/"abc\tdef" +``` + +*(Если вам нужно печатать значения типа `T`, а также указатели на `T`, метод `String` должен быть методом типа значения; в этом примере использован указатель, потому что это более эффективно и идиоматично для структур. См. раздел ниже о получателях значений и указателей для получения дополнительной информации.)* + +Наш метод `String` может вызывать `Sprintf`, **поскольку функции печати полностью реентерабельны и могут быть обёрнуты таким образом**. Однако есть важная деталь, которую нужно понять об этом подходе: **не конструируйте метод `String`, вызывая `Sprintf` таким образом, чтобы он рекурсивно вызывал ваш метод `String` бесконечно**. **Это может произойти, если вызов `Sprintf` попытается напечатать получателя напрямую как строку, что, в свою очередь, снова вызовет метод**. Это обычная и легкая ошибка, как показано в этом примере. + +```go +type MyString string + +func (m MyString) String() string { + return fmt.Sprintf("MyString=%s", m) // Ошибка. + // Будет рекурсивно вызываться бесконечно. +} +``` + +Это также легко исправить: **преобразуйте аргумент к базовому строковому типу, у которого нет метода.** + +```go +type MyString string +func (m MyString) String() string { + return fmt.Sprintf("MyString=%s", string(m)) // OK + // Обратите внимание на преобразование. +} +``` + +В разделе инициализации мы увидим ещё одну технику, которая избегает этой рекурсии. + +Ещё одна техника печати — это передача аргументов функции печати напрямую другой такой функции. Сигнатура функции `Printf` использует тип `...interface{}` для своего последнего аргумента, чтобы указать, что может быть произвольное количество параметров (произвольного типа) после формата. + +```go +func Printf(format string, v ...interface{}) (n int, err error) { +``` + +Внутри функции `Printf` `v` действует как переменная типа `[]interface{}`, но если она передана другой вариадической функции, она действует как обычный список аргументов. Вот реализация функции `log.Println`, которую мы использовали выше. Она передаёт свои аргументы напрямую в `fmt.Sprintln` для фактического форматирования. + +```go +// Println печатает в стандартный логгер, подобно fmt.Println. +func Println(v ...interface{}) { + std.Output(2, fmt.Sprintln(v...)) // Output принимает параметры (int, string) +} +``` + +**Мы пишем `...` после `v` в вложенном вызове `Sprintln`, чтобы указать компилятору рассматривать `v` как список аргументов**; в противном случае он просто передаст `v` как один аргумент в виде среза. + +Есть ещё много аспектов печати, которые мы не рассмотрели здесь. См. документацию `godoc` для пакета `fmt` для получения подробной информации. + +Кстати, параметр `...` может быть конкретного типа, например, `...int` для функции `min`, которая выбирает минимальное значение из списка целых чисел: + +```go +func Min(a ...int) int { + min := int(^uint(0) >> 1) // наибольшее целое число + for _, i := range a { + if i < min { + min = i + } + } + return min +} +``` +