Skip to content

Commit

Permalink
feat: added 2-dimensional slices, maps and printing blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
0x0FACED committed Sep 15, 2024
1 parent 2247818 commit 01c149d
Showing 1 changed file with 270 additions and 0 deletions.
270 changes: 270 additions & 0 deletions effective_go_ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
- [Выделение (аллокация) памяти с помощью `make`](#выделение-аллокация-памяти-с-помощью-make)
- [Массивы](#массивы)
- [Слайсы (Срезы, Slices)](#слайсы-срезы-slices)
- [Двумерные срезы](#двумерные-срезы)
- [Карты](#карты)
- [Печать](#печать)

## Форматирование

Expand Down Expand Up @@ -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
}
```

0 comments on commit 01c149d

Please sign in to comment.