├── 02. Коментарі.md ├── LICENSE ├── 04. Крапки з комою.md ├── 01. Форматування.md ├── 09. Методи.md ├── 15. Вебсервер.md ├── 08. Ініціалізація.md ├── 03. Найменування.md ├── 12. Вкладення.md ├── 06. Функції.md ├── 11. Порожній ідентифікатор.md ├── README.md ├── 14. Помилки.md ├── 05. Керуючі структури.md ├── 10. Інтерфейси та інші типи.md ├── 13. Конкурентність.md └── 07. Дані.md /02. Коментарі.md: -------------------------------------------------------------------------------- 1 | ## Коментарі 2 | У Go передбачено блокові коментарі у стилі C `/* */` та рядкові коментарі у стилі C++ `//`. Рядкові коментарі є нормою; блокові коментарі з'являються здебільшого як коментарі до пакетів, але можуть бути корисними всередині виразів або для вимкнення великих ділянок коду. 3 | 4 | Коментарі, які з'являються перед оголошеннями верхнього рівня, без проміжних нових рядків, вважаються такими, що документують саме оголошення. Ці «doc-коментарі» є основною документацією для певного пакета або команди Go. Докладнішу інформацію про них наведено у документації [«Doc-коментарі в Go»](https://go.dev/doc/comment) (англ.). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Vladyslav Pavlenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /04. Крапки з комою.md: -------------------------------------------------------------------------------- 1 | ## Крапки з комою 2 | Як і у C, формальна граматика Go використовує крапку з комою для поділу операцій-виразів, але на відміну від C, ці крапки не відображаються у вихідному коді. Натомість лексер використовує просте правило, щоб автоматично вставляти крапки з комою під час сканування, тому вхідний код здебільшого вільний від них. 3 | 4 | Правило полягає у наступному. Якщо останньою лексемою перед новим рядком є ідентифікатор (зокрема такі слова, як `int` і `float64`), базовий літерал, наприклад, числова або рядкова константа, або одна з лексем: `break`, `continue`, `fallthrough`, `return`, `++`, `--`, `)`, `}`, лексер завжди вставляє крапку з комою після лексеми. Це можна підсумувати так: «якщо новий рядок стоїть після лексеми, яка може завершити вираз, вставити крапку з комою». 5 | 6 | Крапку з комою також можна опустити безпосередньо перед дужкою, що закриває, тому вираз типу 7 | ```go 8 | go func() { for { dst <- <-src } }() 9 | ``` 10 | не потребує крапки з комою. У програмах ідіоматичною мовою Go крапка з комою ставиться лише у таких місцях, як вираз циклу, для відокремлення ініціалізатора, умови та елементів продовження. Вони також необхідні для розділення кількох операторів у рядку, якщо ви пишете код таким чином. 11 | 12 | Одним із наслідків правил вставки крапки з комою є те, що ви не можете переносити дужку, що відкриває вираз, керуючої структури (`if`, `for`, `switch` або `select`) на наступний рядок. Якщо ви це зробите, перед дужкою буде вставлено крапку з комою, що може призвести до небажаних наслідків. Пишіть так, 13 | ```go 14 | if i < f() { 15 | g() 16 | } 17 | ``` 18 | а не так: 19 | ```go 20 | if i < f() // помилка! 21 | { // помилка! 22 | g() 23 | } 24 | ``` -------------------------------------------------------------------------------- /01. Форматування.md: -------------------------------------------------------------------------------- 1 | ## Форматування 2 | Питання форматування є найбільш суперечливими, але водночас найменш значущими. Люди можуть адаптуватися до різних стилів форматування, але краще, якщо їм взагалі не доведеться цього робити, і менше часу приділяється темі, якщо всі дотримуються одного стилю. Проблема полягає в тому, як підійти до цієї утопії без необхідності в довгому керівництві по стилю. 3 | 4 | У Go ми застосовуємо незвичний підхід і дозволяємо машині подбати про більшість проблем форматування. Програма `gofmt` (також доступна як `go fmt`, яка працює на рівні пакетів, а не на рівні вихідного файлу) читає програму на Go і видає вихідний текст у стандартному стилі з відступами й вертикальним вирівнюванням, зберігаючи й, за необхідності, переформатовуючи коментарі. Якщо ви хочете дізнатися, як впоратися з якоюсь новою стилістичною ситуацією, запустіть `gofmt`; якщо відповідь не здається вам правильною, переробіть вашу програму (або повідомте про ваду в `gofmt`), а не обходьте її стороною. 5 | 6 | Наприклад, не потрібно витрачати час на вирівнювання коментарів до полів структури, адже `gofmt` зробить це за вас. У наступному оголошенні 7 | ```go 8 | type T struct { 9 | name string // ім'я об'єкта 10 | value int // його значення 11 | } 12 | ``` 13 | `gofmt` самостійно вирівняє колонки: 14 | ```go 15 | type T struct { 16 | name string // ім'я об'єкта 17 | value int // його значення 18 | } 19 | ``` 20 | Весь код Go у стандартних пакетах було відформатовано за допомогою `gofmt`. 21 | 22 | Дуже коротко про деякі деталі форматування: 23 | 24 | **Відступи** 25 | 26 | Ми використовуємо табуляцію для відступів і `gofmt` робить це за замовчуванням. Використовуйте пробіли лише за крайньої потреби. 27 | 28 | **Довжина рядка** 29 | 30 | Go не має обмежень на довжину рядка. Не хвилюйтеся про його переповнення. Якщо рядок здається вам занадто довгим, загорніть його і зробіть відступ за допомогою додаткової табуляції. 31 | 32 | **Дужки** 33 | 34 | Go потребує менше круглих дужок, ніж C та Java: керуючі структури (`if`, `for`, `switch`) не мають круглих дужок у своєму синтаксисі. Крім того, ієрархія пріоритетів операторів коротша і зрозуміліша, тому 35 | ```go 36 | x<<8 + y<<16 37 | ``` 38 | не потребує додавання пробілів, на відміну від інших мов. -------------------------------------------------------------------------------- /09. Методи.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Методи](#Методи) 3 | - [Вказівники чи значення](#Вказівники-чи-значення) 4 | 5 | ## Методи 6 | 7 | ### Вказівники чи значення 8 | Як ми бачили з `ByteSize` в [одному з минулих розділів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/08.%20Ініціалізація.md#константи), методи можуть бути визначені для будь-якого іменованого типу (крім вказівника або інтерфейсу); а так званий «отримувач» не обов'язково повинен бути структурою. 9 | 10 | В обговоренні [зрізів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Зрізи) ми власноруч написали функцію `Append`. Замість цього ми можемо визначити її як метод на зрізах. Для цього спочатку оголошуємо іменований тип, до якого ми можемо прив'язати метод, а потім робимо його приймачем для методу значення цього типу. 11 | ```go 12 | type ByteSlice []byte 13 | 14 | func (slice ByteSlice) Append(data []byte) []byte { 15 | // Тіло функції точно таке саме, як у функції Append, визначеній вище. 16 | } 17 | ``` 18 | Це все одно вимагає того, щоб метод повертав оновлений зріз. Ми можемо усунути цю незручність, перевизначивши метод так, щоб він приймав _вказівник_ на `ByteSlice` як приймач, щоб метод міг напряму перезаписати зріз користувача. 19 | ```go 20 | func (p *ByteSlice) Append(data []byte) { 21 | slice := *p 22 | // Тіло, як вище, без повернення. 23 | *p = slice 24 | } 25 | ``` 26 | Натомість ми можемо зробити ще краще. Якщо ми модифікуємо нашу функцію так, щоб вона виглядала як стандартний метод `Write`, ось так, 27 | ```go 28 | func (p *ByteSlice) Write(data []byte) (n int, err error) { 29 | slice := *p 30 | // Знову ж таки, як вище. 31 | *p = slice 32 | return len(data), nil 33 | } 34 | ``` 35 | то тип `*ByteSlice` задовольняти стандартний інтерфейс `io.Writer`, що дуже зручно. Наприклад, ми зможемо оформлювати виведення в один рядок. 36 | ```go 37 | var b ByteSlice 38 | fmt.Fprintf(&b, "This hour has %d days\n", 7) 39 | ``` 40 | Ми передаємо _адресу_ `ByteSlice`, тому що тільки `*ByteSlice` задовольняє `io.Writer`. Правило щодо вказівників та значень для приймачів полягає у тому, що методи значень можуть бути викликані як для вказівників, так і для значень, але методи вказівників можуть бути викликані _лише_ для вказівників. 41 | 42 | Це правило виникає через те, що методи вказівника можуть модифікувати приймач; їх виклик для значення призведе до того, що метод отримає копію значення, тому будь-які зміни будуть відкинуті. Мова нівелює ймовірність цієї помилки. Однак існує зручний виняток. Коли значення є адресованим, мова піклується про загальний випадок виклику методу за вказівником на значення, автоматично вставляючи оператор адресування. У нашому прикладі змінна `b` є адресованою, тому ми можемо викликати її метод `Write` просто з `b.Write`. Компілятор перепише це в `(&b).Write` за нас. 43 | 44 | До речі, ідея використання `Write` на зрізі байт є центральною в реалізації `bytes.Buffer`. -------------------------------------------------------------------------------- /15. Вебсервер.md: -------------------------------------------------------------------------------- 1 | ## Вебсервер 2 | 3 | Завершімо з повною повноцінною на Go, вебсервером. Google надає сервіс за адресою `chart.apis.google.com`, який автоматично форматує дані в діаграми та графіки. Однак його важко використовувати в інтерактивному режимі, тому що вам потрібно ввести дані в URL-адресу як запит. Наведена тут програма надає приємніший інтерфейс для однієї з форм даних: отримавши короткий фрагмент тексту, вона звертається до сервера діаграм, щоб створити QR-код — матрицю клітинок, які кодують текст. Це зображення можна захопити камерою мобільного телефону й інтерпретувати як, наприклад, URL-адресу, не вводячи її на крихітній клавіатурі телефону. 4 | 5 | Ось повна версія програми. Далі йде пояснення. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "html/template" 13 | "log" 14 | "net/http" 15 | ) 16 | 17 | var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18 18 | 19 | var templ = template.Must(template.New("qr").Parse(templateStr)) 20 | 21 | func main() { 22 | flag.Parse() 23 | http.Handle("/", http.HandlerFunc(QR)) 24 | err := http.ListenAndServe(*addr, nil) 25 | if err != nil { 26 | log.Fatal("ListenAndServe:", err) 27 | } 28 | } 29 | 30 | func QR(w http.ResponseWriter, req *http.Request) { 31 | templ.Execute(w, req.FormValue("s")) 32 | } 33 | 34 | const templateStr = ` 35 | 36 | 37 | QR Link Generator 38 | 39 | 40 | {{if .}} 41 | 42 |
43 | {{.}} 44 |
45 |
46 | {{end}} 47 |
48 | 49 | 50 |
51 | 52 | 53 | ` 54 | ``` 55 | 56 | Код до `main` має бути зрозумілим. Прапорець встановлює HTTP-порт за замовчуванням для нашого сервера. Змінна шаблону `templ` — це те місце, де відбувається найцікавіше. Вона створює HTML-шаблон, який буде виконуватися сервером для відображення сторінки; про це трохи пізніше. 57 | 58 | Функція `main` аналізує прапори й, використовуючи механізм, про який ми говорили вище, прив'язує функцію-обробник `QR` до шляху для сервера. Потім викликається `http.ListenAndServe` для запуску сервера; вона блокується під час роботи сервера. 59 | 60 | `QR` просто отримує запит, який містить дані форми, і виконує шаблон на даних у значенні форми з ім'ям `s`. 61 | 62 | Пакет шаблонів `html/template` є дуже потужним; ця програма залучає лише поверхневі його можливості. По суті, вона переписує фрагмент HTML-тексту на льоту, підставляючи елементи, отримані з даних, переданих до `templ.Execute`, в такому випадку значення форми. У тексті шаблону (`templateStr`) фрагменти, розділені подвійними фігурними дужками, позначають дії шаблону. Фрагмент від `{{if .}}` до `{{end}}` виконується тільки в тому випадку, якщо значення поточного елемента даних, який називається `.` (крапка), не є порожнім. Тобто, коли рядок порожній, цей фрагмент шаблону опускається. 63 | 64 | Два фрагменти `{{.}}` вказують на те, що на вебсторінці відображаються дані, представлені в шаблоні — рядок запиту. Пакет шаблонів HTML автоматично забезпечує відповідне екранування, щоб текст був безпечним для відображення. 65 | 66 | Решта рядка шаблону — це просто HTML-код, який відображається при завантаженні сторінки. Якщо це занадто коротке пояснення, зверніться до [документації](https://pkg.go.dev/html/template) до пакета `template` для більш детального обговорення. 67 | 68 | Ось і все: корисний вебсервер у кількох рядках коду плюс деякий HTML-текст, керований даними. Go — достатньо потужна мова, щоб зробити багато чого за допомогою всього декількох рядків. -------------------------------------------------------------------------------- /08. Ініціалізація.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Ініціалізація](#Ініціалізація) 3 | - [Константи](#Константи) 4 | - [Змінні](#Змінні) 5 | - [Функція init](#Функція-init) 6 | 7 | ## Ініціалізація 8 | Ініціалізація в мові Go є більш потужною, ніж у C або C++. Під час ініціалізації можна будувати складні структури, а питання порядку серед ініціалізованих об'єктів, навіть серед різних пакетів, вирішуються коректно. 9 | 10 | ### Константи 11 | Константи у Go — звичайні константи. Вони створюються під час компіляції, навіть якщо визначені як локальні у функціях, і можуть бути лише числами, символами (рунами), рядками або булевими виразами. Через обмеження часу компіляції, вирази, що їх визначають, повинні бути константними виразами, які обчислюються компілятором. Наприклад, `1<<3` є константним виразом, а `math.Sin(math.Pi/4)` — ні, оскільки виклик функції `math.Sin` має відбуватися під час виконання програми. 12 | 13 | У Go перечислювані константи створюються за допомогою перечислювача `iota`. Оскільки `iota` може бути частиною виразу, а вирази можуть неявно повторюватися, легко створювати складні набори значень. 14 | ```go 15 | type ByteSize float64 16 | 17 | const ( 18 | _ = iota // ігноруємо перше значення, присвоюючи його порожньому ідентифікатору 19 | KB ByteSize = 1 << (10 * iota) 20 | MB 21 | GB 22 | TB 23 | PB 24 | EB 25 | ZB 26 | YB 27 | ) 28 | ``` 29 | 30 | Можливість приєднати метод типу `String` до будь-якого типу, визначеного користувачем, дає змогу автоматично форматувати довільні значення для виведення. Хоча ви найчастіше бачите, як це застосовується до структур, ця техніка також корисна для скалярних типів, таких як типи з рухомою комою, як от `ByteSize`. 31 | ```go 32 | func (b ByteSize) String() string { 33 | switch { 34 | case b >= YB: 35 | return fmt.Sprintf("%.2fYB", b/YB) 36 | case b >= ZB: 37 | return fmt.Sprintf("%.2fZB", b/ZB) 38 | case b >= EB: 39 | return fmt.Sprintf("%.2fEB", b/EB) 40 | case b >= PB: 41 | return fmt.Sprintf("%.2fPB", b/PB) 42 | case b >= TB: 43 | return fmt.Sprintf("%.2fTB", b/TB) 44 | case b >= GB: 45 | return fmt.Sprintf("%.2fGB", b/GB) 46 | case b >= MB: 47 | return fmt.Sprintf("%.2fMB", b/MB) 48 | case b >= KB: 49 | return fmt.Sprintf("%.2fKB", b/KB) 50 | } 51 | return fmt.Sprintf("%.2fB", b) 52 | } 53 | ``` 54 | 55 | Вираз `YB` виводиться як `1.00YB`, тоді як `ByteSize(1e13)` виводить `9.09TB`. 56 | 57 | Використання `Sprintf` для реалізації методу `String` функції `ByteSize` є безпечним (дозволяє уникнути нескінченного повторення) не через перетворення, а через виклик `Sprintf` з `%f`, який не є рядковим форматом: `Sprintf` викликає метод `String` лише тоді, коли йому потрібен рядок, а `%f` — значення з рухомою комою. 58 | 59 | ### Змінні 60 | Змінні можна ініціалізувати так само як і константи, але ініціалізатором може бути загальний вираз, обчислений під час виконання програми. 61 | ```go 62 | var ( 63 | home = os.Getenv("HOME") 64 | user = os.Getenv("USER") 65 | gopath = os.Getenv("GOPATH") 66 | ) 67 | ``` 68 | 69 | ### Функція init 70 | Наостанок, кожен файл може визначити власну ніладичну функцію `init` для налаштування необхідного стану. (Фактично, кожен файл може мати кілька функцій `init`.) І «наостанок» дійсно означає «наостанок»: функція `init` викликається після того, як всі змінних у пакеті було ініціалізовано, а ті, своєю чергою, ініціалізуються тільки після того, як всі імпортовані пакети були ініціалізовані. 71 | 72 | Функція `init` часто використовується для перевірки або виправлення коректності стану програми перед початком реального виконання. 73 | ```go 74 | func init() { 75 | if user == "" { 76 | log.Fatal("$USER not set") 77 | } 78 | if home == "" { 79 | home = "/home/" + user 80 | } 81 | if gopath == "" { 82 | gopath = home + "/go" 83 | } 84 | // gopath може бути перевизначено за допомогою прапорця --gopath у командному рядку. 85 | flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH") 86 | } 87 | ``` -------------------------------------------------------------------------------- /03. Найменування.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Найменування](#Найменування) 3 | - [Назви пакетів](#Назви-пакетів) 4 | - [Геттери](#Геттери) 5 | - [Назви інтерфейсів](#Назви-інтерфейсів) 6 | - [MixedCaps](#MixedCaps) 7 | 8 | ## Найменування 9 | Найменування є так само важливими у Go, як і в будь-якій іншій мові. Вони навіть мають семантичний ефект: видимість імені поза пакетом визначається тим, чи є його перший символ верхнім регістром. Тому варто витратити трохи часу на розмову про угоди щодо іменування у програмах на Go. 10 | 11 | ### Назви пакетів 12 | Коли пакет імпортовано, його назва використовується для отримання доступу до його вмісту. Після того, як пакет імпортовано, 13 | ```go 14 | import "bytes" 15 | ``` 16 | пакет, що своєю чергою імпортує, може використовувати `bytes.Buffer`. Добре, якщо всі користувачі пакета можуть використовувати те саме ім'я для посилання на його вміст, а це означає, що ім'я пакета повинно бути гарним: коротким, лаконічним, таким, що викликає асоціації. За домовленістю, пакети мають назву з одного слова, написаного малими літерами; не слід використовувати підкреслення або mixedCaps. Зробіть ставку на стислість, оскільки всі, хто користуватиметься вашим пакетом, будуть багаторазово набирати цю назву. Але не турбуйтеся про унікальність імені. Ім'я пакета тільки за замовчуванням використовується під час імпорту; воно не повинно бути унікальним, і в рідкісних випадках, під час імпорту може бути вказано інше ім'я. У будь-якому разі, плутанина трапляється рідко, оскільки ім'я файлу в імпорті визначає, який саме пакет використовується. 17 | 18 | Іншою домовленістю є те, що ім'я пакета є базовим ім'ям його вихідного каталогу; пакет у `src/encoding/base64` імпортується як `"encoding/base64"`, але має ім'я `base64`, а не, скажімо, `encoding_base64` чи `encodingBase64`. 19 | 20 | Імпортер пакета використовуватиме назву для посилання на його вміст, тому експортовані назви у пакеті можуть використовувати цей факт, щоб уникнути повторень. (Не використовуйте нотацію `import .`, хоч це й, звісно, може спростити тести, що мають виконуватися поза пакетом, який вони тестують, але в інших випадках її слід уникати). Наприклад, тип буферизованого читача у пакеті `bufio` називається `Reader`, а не `BufReader`, оскільки користувачі сприймають його як `bufio.Reader`, що є зрозумілим і лаконічним іменем. Крім того, оскільки імпортовані об'єкти завжди адресуються за назвою пакета, `bufio.Reader` не конфліктує зі, скажімо, `io.Reader`. Аналогічно, функція для створення нових екземплярів `ring.Ring`, яка є визначеною як конструктор у Go, зазвичай називається `NewRing`, але оскільки `Ring` є єдиним типом, що експортується пакетом, і оскільки пакет називається `ring`, вона називається просто `New`, і користувачі пакета бачать її як `ring.New`. Використовуйте структуру пакетів для вибору вдалих назв. 21 | 22 | Інший короткий приклад — функція `once.Do`; `once.Do(setup)` читається добре, і водночас краще не стане, якщо її перейменувати в щось накшталт `once.DoOrWaitUntilDone(setup)`. Довгі імена не роблять назви більш читабельними. У той час як коментарі можуть бути ціннішими, ніж довгі імена. 23 | 24 | ### Геттери 25 | Go не надає автоматичної підтримки геттерів та сеттерів. Немає нічого поганого у тому, що ви самі визначаєте гетери та сетери, і це часто буває доречно, але додавати `Get` до імені геттера не є ні ідіоматичним, ні обов'язковим. Якщо у вас є поле, наприклад, `owner` (у нижньому регістрі, неекспортоване), метод геттера має називатися `Owner` (у верхньому регістрі, експортований), а не `GetOwner`. Використання імен у верхньому регістрі для експорту дозволяє відрізнити поле від методу. Метод сеттера, якщо у ньому є потреба, найімовірніше, називатиметься `SetOwner`. Обидві назви добре читаються на практиці: 26 | ```go 27 | owner := obj.Owner() 28 | if owner != user { 29 | obj.SetOwner(user) 30 | } 31 | ``` 32 | 33 | ### Назви інтерфейсів 34 | За домовленістю, однометодові інтерфейси називаються за назвою методу плюс суфікс `-er` або подібна модифікація для створення іменника-агента: `Reader`, `Writer`, `Formatter`, `CloseNotifier` тощо. 35 | 36 | Існує низка таких назв, і продуктивно дотримуватися їх та назв функцій, які вони охоплюють. `Read`, `Write`, `Close`, `Flush`, `String` і так далі мають канонічні підписи й значення. Щоб уникнути плутанини, не давайте своєму методу одну з цих назв, якщо вона не має такої ж сигнатури й значення. І навпаки, якщо ваш тип реалізує метод з тим самим значенням, що і метод на якомусь відомому типі, дайте йому те саме ім'я і сигнатуру; називайте ваш метод перетворення рядка `String`, а не `ToString`. 37 | 38 | ### MixedCaps 39 | Насамкінець, у Go прийнято використовувати `MixedCaps` або `mixedCaps`, а не підкреслення для написання назв, що складаються з кількох слів. -------------------------------------------------------------------------------- /12. Вкладення.md: -------------------------------------------------------------------------------- 1 | ## Вкладення 2 | Мова Go не надає типового, керованого типами поняття підкласів, але вона має можливість «запозичувати» частини реалізації, вбудовуючи типи в структуру або інтерфейс. 3 | 4 | Вбудовування інтерфейсів дуже просте. Ми вже згадували інтерфейси `io.Reader` та `io.Writer` раніше; ось їх визначення. 5 | ```go 6 | type Reader interface { 7 | Read(p []byte) (n int, err error) 8 | } 9 | 10 | type Writer interface { 11 | Write(p []byte) (n int, err error) 12 | } 13 | ``` 14 | Це говорить саме про те, на що це схоже: `ReadWriter` може робити й те, що робить `Reader`, і те, що робить `Writer`; це об'єднання вбудованих інтерфейсів. Зауважте, що тільки інтерфейси можуть бути вбудовані в інші інтерфейси. 15 | 16 | Та ж сама основна ідея застосовується до структур, але з більш далекосяжними наслідками. Пакет `bufio` має два типи структур, `bufio.Reader` і `bufio.Writer`, кожен з яких, звичайно, реалізує аналогічні інтерфейси з пакета `io`. Також `bufio` реалізує буферизований читач/записувач, що досягається шляхом об'єднання читача і записувача в одну структуру за допомогою вбудовування: він перераховує типи всередині структури, але не дає їм імен полів. 17 | 18 | ```go 19 | // ReadWriter зберігає вказівники на Reader та Writer. 20 | // Він реалізує io.ReadWriter. 21 | type ReadWriter struct { 22 | *Reader // *bufio.Reader 23 | *Writer // *bufio.Writer 24 | } 25 | ``` 26 | 27 | Вбудовані елементи є вказівниками на структури й, звичайно, повинні бути ініціалізовані перш ніж їх можна буде використовувати. Структуру `ReadWriter` можна записати так: 28 | ```go 29 | type ReadWriter struct { 30 | reader *Reader 31 | writer *Writer 32 | } 33 | ``` 34 | 35 | але тоді, щоб просувати методи полів і задовольнити інтерфейси `io`, нам також потрібно буде надати методи перенаправлення, як це зроблено тут: 36 | ```go 37 | func (rw *ReadWriter) Read(p []byte) (n int, err error) { 38 | return rw.reader.Read(p) 39 | } 40 | ``` 41 | 42 | Вбудовуючи структури безпосередньо, ми уникаємо цього. Методи вбудованих типів надаються заразом, а це означає, що `bufio.ReadWriter` не тільки має методи `bufio.Reader` і `bufio.Writer`, але й задовольняє всі три інтерфейси: `io.Reader`, `io.Writer` і `io.ReadWriter`. 43 | 44 | Існує важлива відмінність між вбудовуванням та підкласами. Коли ми вбудовуємо тип, методи цього типу стають методами зовнішнього типу, але коли вони викликаються, отримувачем методу є внутрішній тип, а не зовнішній. У нашому прикладі, коли викликається метод `Read` типу `bufio.ReadWriter`, він має такий самий ефект, як і метод пересилання, описаний вище; отримувачем є поле для читання `ReadWriter`, а не сам `ReadWriter`. 45 | 46 | Вбудовування також може бути просто зручнішим. У цьому прикладі показано вбудоване поле поряд зі звичайним іменованим полем. 47 | ```go 48 | type Job struct { 49 | Command string 50 | *log.Logger 51 | } 52 | ``` 53 | 54 | Тип `Job` тепер має `Print`, `Printf`, `Println` та інші методи `*log.Logger`. Звісно, ми могли б назвати поле `Logger`, але це не обов'язково. І тепер, після ініціалізації, ми можемо використовувати `Println` на `Job`: 55 | ```go 56 | job.Println("starting now...") 57 | ``` 58 | 59 | `Logger` є просто полем структури `Job`, тому ми можемо ініціалізувати його звичайним чином всередині конструктора `Job`, ось так: 60 | ```go 61 | func NewJob(command string, logger *log.Logger) *Job { 62 | return &Job{command, logger} 63 | } 64 | ``` 65 | або з використанням складеного літерала: 66 | ```go 67 | job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)} 68 | ``` 69 | 70 | Якщо нам потрібно звернутися до вбудованого поля безпосередньо, ім'я типу поля, ігноруючи кваліфікатор пакета, слугує як ім'я самого поля, як це було у методі `Read` нашої структури `ReadWriter`. Тут, якби нам потрібно було отримати доступ до `*log.Logger` змінної `job`, ми б написали `job.Logger`, що було б корисно, якби ми хотіли вдосконалити методи `Logger`. 71 | ```go 72 | func (job *Job) Printf(format string, args ...interface{}) { 73 | job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...)) 74 | } 75 | ``` 76 | 77 | Вкладення типів створює проблему конфлікту імен, але правила для її вирішення прості. По-перше, поле або метод `X` приховує будь-який інший елемент `X` у більш глибоко вкладеній частині типу. Якби `log.Logger` містив поле або метод з назвою `Command`, поле `Command` з `Job` домінувало б. 78 | 79 | По-друге, якщо те саме ім'я з'являється на тому самому рівні вкладеності, це зазвичай є помилкою; було б помилково вбудовувати `log.Logger`, якщо структура `Job` містила б інше поле або метод з назвою `Logger`. Однак, якщо повторюване ім'я ніколи не згадується у програмі за межами визначення типу, все гаразд. Ця кваліфікація забезпечує певний захист від змін, внесених до типів, вбудованих ззовні; немає жодних проблем, якщо додано поле, яке конфліктує з іншим полем в іншому підтипі, якщо жодне з полів ніколи не використовується. -------------------------------------------------------------------------------- /06. Функції.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Функції](#Функції) 3 | - [Множинне повернення результатів](#Множинне-повернення-результатів) 4 | - [Найменування параметрів результату](#Найменування-параметрів-результату) 5 | - [Defer](#Defer) 6 | 7 | ## Функції 8 | ### Множинне повернення результатів 9 | Однією з незвичних особливостей Go є те, що функції та методи можуть повертати декілька значень водночас. Ця форма може бути використана для покращення кількох незграбних ідіом у програмах на C: повернення помилок накшталт -1 для EOF та модифікація аргументу, переданого за адресою. 10 | 11 | У мові C помилка запису сигналізується за допомогою негативного лічильника, а код помилки зберігається в нестабільній локації. У Go метод `Write` може одночасно повернути й лічильник, і помилку: «Так, ви записали деякі байти, але не всі, оскільки заповнили пристрій». Сигнатура методу `Write` для файлів з пакета `os` має вигляд: 12 | ```go 13 | func (file *File) Write(b []byte) (n int, err error) 14 | ``` 15 | і, як зазначено у документації, даний метод повертає кількість записаних байт і ненульову помилку, якщо `n != len(b)`. Це поширений стиль; див. [розділ про обробку помилок](#Помилки) для отримання додаткових прикладів. 16 | 17 | Подібний підхід позбавляє від необхідності передавати вказівник на значення, що повертається, для імітації параметра-посилання. Ось проста функція для отримання числа з позиції у зрізі байт, яка повертає це число і наступну позицію. 18 | ```go 19 | func nextInt(b []byte, i int) (int, int) { 20 | for ; i < len(b) && !isDigit(b[i]); i++ { 21 | } 22 | x := 0 23 | for ; i < len(b) && isDigit(b[i]); i++ { } 24 | x = x*10 + int(b[i]) - '0' 25 | } 26 | return x, i 27 | } 28 | ``` 29 | 30 | Ви можете використовувати його для сканування чисел у вхідному зрізі `b` таким чином: 31 | ```go 32 | for i := 0; i < len(b); { 33 | x, i = nextInt(b, i) 34 | fmt.Println(x) 35 | } 36 | ``` 37 | 38 | ### Найменування параметрів результату 39 | У Go параметрам повернення або «параметрам результату» функції можна присвоювати імена й використовувати їх як звичайні змінні, так само як і вхідні параметри. Якщо параметрам присвоєно імена, вони ініціалізуються нульовими значеннями для своїх типів на початку роботи функції; якщо функція виконує оператор `return` без аргументів, то як значення, що повертаються, використовуються поточні значення параметрів результату. 40 | 41 | Імена не є обов'язковими, але вони можуть зробити код коротшим і зрозумілішим: це документація. Якщо ми дамо імена результатам функції `nextInt`, то стане зрозуміло, який саме `int` повертається. 42 | ```go 43 | func nextInt(b []byte, pos int) (value, nextPos int) { 44 | ``` 45 | 46 | Оскільки іменовані результати ініціалізуються і прив'язуються до повернення, вони можуть спростити та прояснити обробку та розуміння значень, що повертаються з функції. Ось версія `io.ReadFull`, яка добре їх використовує: 47 | ```go 48 | func ReadFull(r Reader, buf []byte) (n int, err error) { 49 | for len(buf) > 0 && err == nil { 50 | var nr int 51 | nr, err = r.Read(buf) 52 | n += nr 53 | buf = buf[nr:] 54 | } 55 | return 56 | } 57 | ``` 58 | 59 | ### Defer 60 | Оператор `defer` у Go планує виклик функції (відкладеної функції) на виконання безпосередньо перед тим, як функція, що виконує відкладення, повернеться. Це незвичний, але ефективний спосіб впоратися з такими ситуаціями, як ресурси, які необхідно звільнити незалежно від того, яким шляхом повернеться функція. Канонічними прикладами є розблокування м'ютексу або закриття файлу. 61 | ```go 62 | // Contents повертає вміст файлу у вигляді рядка. 63 | func Contents(filename string) (string, error) { 64 | f, err := os.Open(filename) 65 | if err != nil { 66 | return "", err 67 | } 68 | defer f.Close() // f.Close буде виконано, коли ми завершимо. 69 | 70 | var result []byte 71 | buf := make([]byte, 100) 72 | for { 73 | n, err := f.Read(buf[0:]) 74 | result = append(result, buf[0:n]...) // append буде обговорено пізніше. 75 | if err != nil { 76 | if err == io.EOF { 77 | break 78 | } 79 | return "", err // f.Close буде виконано, якщо ми повернемося тут. 80 | } 81 | } 82 | return string(result), nil // f.Close буде виконано, якщо ми повернемося тут. 83 | } 84 | ``` 85 | 86 | Відкладання виклику таких функцій, як `Close`, має дві переваги. По-перше, це гарантує, що ви ніколи не забудете закрити файл - помилка, якої легко припуститися, якщо пізніше відредагувати функцію і додати новий шлях повернення. По-друге, це означає, що `close` знаходиться поруч з `open`, що набагато зрозуміліше, ніж розміщувати його в кінці функції. 87 | 88 | Аргументи відкладеної функції (які включають приймач, якщо функція є методом) обчислюються під час виконання `defer`, а не під час виконання функції безпосередньо. Крім того, що ви можете не турбуватися про зміну значень змінних під час виконання функції, це означає, що одне місце виклику `defer` може відкласти виконання декількох функцій. Ось дурненький приклад: 89 | ```go 90 | for i := 0; i < 5; i++ { 91 | defer fmt.Printf("%d ", i) 92 | } 93 | ``` 94 | Відкладені функції виконуються у порядку LIFO (англ. last in, first out, «останнім прийшов — першим пішов»), тому цей код призведе до виведення `4 3 2 1 0`, коли функція повернеться. Більш правдоподібним прикладом є простий спосіб відстежити виконання функції у програмі. Ми можемо написати декілька простих підпрограм трасування на зразок наступної: 95 | ```go 96 | func trace(s string) { fmt.Println("entering:", s) } 97 | func untrace(s string) { fmt.Println("leaving:", s) } 98 | 99 | // Використовуйте їх так: 100 | func a() { 101 | trace("a") 102 | defer untrace("a") 103 | // якась робота... 104 | } 105 | ``` 106 | 107 | Ми можемо зробити краще, використовуючи той факт, що аргументи відкладених функцій обчислюються під час виконання відкладання `defer`. Процедура трасування може встановити аргумент для процедури зняття трасування. Маємо код, 108 | ```go 109 | func trace(s string) string { 110 | fmt.Println("entering:", s) 111 | return s 112 | } 113 | 114 | func un(s string) { 115 | fmt.Println("leaving:", s) 116 | } 117 | 118 | func a() { 119 | defer un(trace("a")) 120 | fmt.Println("in a") 121 | } 122 | 123 | func b() { 124 | defer un(trace("b")) 125 | fmt.Println("in b") 126 | a() 127 | } 128 | 129 | func main() { 130 | b() 131 | } 132 | ``` 133 | результатом виконання якого буде 134 | ``` 135 | entering: b 136 | in b 137 | entering: a 138 | in a 139 | leaving: a 140 | leaving: b 141 | ``` 142 | Для програмістів, які звикли до управління ресурсами на рівні блоків з інших мов, `defer` може здатися незвичним, але його найцікавіші та найпотужніші застосування пов'язані саме з тим, що він базується не на блоках, а на функціях. У [розділі про помилки](#Помилки) ми побачимо ще один приклад можливостей цього оператора. 143 | -------------------------------------------------------------------------------- /11. Порожній ідентифікатор.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Порожній ідентифікатор](#Порожній-ідентифікатор) 3 | - [Порожній ідентифікатор у множинному присвоюванні](#Порожній-ідентифікатор-у-множинному-присвоюванні) 4 | - [Невикористовувані імпортування й значення](#Невикористовувані-імпортування-й-значення) 5 | - [Імпортування для сторонніх ефектів](#Імпортування-для-сторонніх-ефектів) 6 | - [Перевірки інтерфейсів](#Перевірки-інтерфейсів) 7 | 8 | ## Порожній ідентифікатор 9 | Ми вже кілька разів згадували про порожній ідентифікатор у контексті [циклів `for` із `range`](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/05.%20Керуючі%20структури.md#For) та [мап](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Мапи). Порожній ідентифікатор можна призначити або оголосити з будь-яким значенням будь-якого типу, при цьому значення буде безболісно відкинуто. Це трохи схоже на запис до файлу Unix `/dev/null`: він являє собою значення, доступне лише для запису, яке можна використовувати як заповнювач там, де потрібна змінна, але фактичне її значення є нерелевантним. Порожній ідентифікатор має багато інших застосувань, окрім тих, що ми вже розглянули. 10 | 11 | ### Порожній ідентифікатор у множинному присвоюванні 12 | Використання порожнього ідентифікатора в циклі `for` із `range` є окремим випадком загальної ситуації: множинного присвоювання. 13 | 14 | Якщо присвоювання вимагає декількох значень у лівій частині, але одне зі значень не буде використовуватися програмою, порожній ідентифікатор у лівій частині присвоювання дозволяє уникнути необхідності створювати фіктивну змінну і дає зрозуміти, що це значення буде відкинуто. Наприклад, при виклику функції, яка повертає значення і помилку, але важлива лише помилка, використовуйте порожній ідентифікатор, щоб відкинути несуттєве значення. 15 | ```go 16 | if _, err := os.Stat(path); os.IsNotExist(err) { 17 | fmt.Printf("%s does not exist\n", path) 18 | } 19 | ``` 20 | Іноді ви можете побачити код, який відкидає значення помилки, щоб ігнорувати її; це жахлива практика. Завжди перевіряйте повідомлення про помилки; вони надаються не просто так. 21 | ```go 22 | // Погано! Цей код спровокує креш, якщо шляху не існує. 23 | fi, _ := os.Stat(path) 24 | if fi.IsDir() { 25 | fmt.Printf("%s is a directory\n", path) 26 | } 27 | ``` 28 | 29 | ### Невикористовувані імпортування й значення 30 | Помилкою є імпорт пакета або оголошення змінної без її використання. Невикористаний імпорт роздуває програму і сповільнює її компіляцію, тоді як ініціалізована, але невикористана змінна — це щонайменше даремні обчислення і, можливо, свідчення серйознішої помилки. Однак, коли програма перебуває на стадії активної розробки, часто виникають ситуації невикористання імпортованих даних та змінних, і видаляти їх, щоб продовжити компіляцію, буває набридливо, оскільки вони можуть знадобитися згодом. Порожній ідентифікатор забезпечує обхідний шлях. 31 | 32 | Ця напівнаписана програма має два невикористані імпорти (_fmt_ і _io_) і невикористану змінну (_fd_), тому вона не скомпілюється, але було б непогано перевірити, чи правильним є код на цей час. 33 | ```go 34 | package main 35 | 36 | import ( 37 | "fmt" 38 | "io" 39 | "log" 40 | "os" 41 | ) 42 | 43 | func main() { 44 | fd, err := os.Open("test.go") 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | // TODO: використати fd. 49 | } 50 | ``` 51 | 52 | Щоб заглушити скарги на невикористаний імпорт, використовуйте порожній ідентифікатор як посилання на імпортований пакет. Аналогічно, присвоєння невикористаної змінної `fd` порожньому ідентифікатору приглушить відповідну помилку. Цю версію програми буде скомпільовано. 53 | ```go 54 | package main 55 | 56 | import ( 57 | "fmt" 58 | "io" 59 | "log" 60 | "os" 61 | ) 62 | 63 | var _ = fmt.Printf // Для дебагу; видалити після завершення. 64 | var _ io.Reader // Для дебагу; видалити після завершення. 65 | 66 | func main() { 67 | fd, err := os.Open("test.go") 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | // TODO: використати fd. 72 | _ = fd 73 | } 74 | ``` 75 | 76 | За домовленістю, глобальні декларації про ігнорування помилок при імпорті повинні з'являтися одразу після імпорту й коментуватися, щоб їх було легко знайти, а також як нагадування про необхідність виправлення помилок пізніше. 77 | 78 | ### Імпортування для сторонніх ефектів 79 | Невикористаний імпорт, такий як `fmt` або `io` у попередньому прикладі, з часом слід використати або вилучити: порожні призначення ідентифікують код як незавершений. Але іноді корисно імпортувати пакет лише для його побічних ефектів, без будь-якого явного використання. Наприклад, під час функції `init` пакет `net/http/prof` реєструє HTTP-обробники, які надають налагоджувальну інформацію. Він має експортований API, але більшості клієнтів потрібна лише реєстрація обробників і доступ до даних через вебсторінку. Щоб імпортувати пакет лише для його побічних ефектів, перейменуйте посилання на пакет на порожній ідентифікатор: 80 | ```go 81 | import _ "net/http/pprof" 82 | ``` 83 | Ця форма імпорту дає зрозуміти, що пакет імпортується заради його побічних ефектів, оскільки немає іншого можливого використання пакета: у цьому файлі він не має назви. (Якби він мав, а ми не використали її, компілятор відхилив би програму). 84 | 85 | ### Перевірки інтерфейсів 86 | Як ми вже звернули увагу в обговоренні [інтерфейсів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/10.%20Інтерфейси%20та%20інші%20типи.md#Інтерфейси-та-інші-типи), тип не повинен явно оголошувати, що він реалізує інтерфейс. Натомість тип реалізує інтерфейс, просто реалізуючи методи інтерфейсу. На практиці більшість перетворень інтерфейсів є статичними й тому перевіряються під час компіляції. Наприклад, передача `*os.File` у функцію, яка очікує `io.Reader`, не скомпілюється, якщо `*os.File` не реалізує інтерфейс `io.Reader`. 87 | 88 | Деякі перевірки інтерфейсів все ж таки відбуваються під час виконання. Одна з них знаходиться у пакеті `encoding/json`, який визначає інтерфейс `Marshaler`. Коли кодувальник JSON отримує значення, яке реалізує цей інтерфейс, він викликає метод маршалування значення, щоб перетворити його в JSON, замість того, щоб виконувати стандартне перетворення. Кодувальник перевіряє цю властивість під час виконання за допомогою твердження типу: 89 | ```go 90 | m, ok := val.(json.Marshaler) 91 | ``` 92 | 93 | Якщо потрібно лише запитати, чи тип реалізує інтерфейс, без використання самого інтерфейсу, можливо, як частина перевірки помилок, використовуйте порожній ідентифікатор, щоб ігнорувати значення, стверджене типом: 94 | ```go 95 | if _, ok := val.(json.Marshaler); ok { 96 | fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val) 97 | } 98 | ``` 99 | 100 | Така ситуація виникає, коли необхідно гарантувати в пакеті, що реалізує тип, що він дійсно задовольняє інтерфейсу. Якщо тип — наприклад, `json.RawMessage`, потребує спеціального представлення JSON, він повинен реалізувати `json.Marshaler`. Проте, не існує статичних перетворень, які б змусили компілятор перевіряти це автоматично. Якщо тип ненавмисно не задовольняє інтерфейсу, кодувальник JSON все одно працюватиме, але не використовуватиме користувацьку реалізацію. Щоб гарантувати коректність реалізації, у пакеті можна використати глобальне оголошення з порожнім ідентифікатором: 101 | ```go 102 | var _ json.Marshaler = (*RawMessage)(nil) 103 | ``` 104 | 105 | У цьому оголошенні присвоєння, що включає перетворення `*RawMessage` у `Marshaler`, вимагає, щоб `*RawMessage` реалізовував `Marshaler`, і ця властивість буде перевірена під час компіляції. Якщо інтерфейс `json.Marshaler` зміниться, цей пакунок більше не компілюватиметься, і ми отримаємо повідомлення про необхідність його оновлення. 106 | 107 | Поява порожнього ідентифікатора у цій конструкції вказує на те, що оголошення існує лише для перевірки типу, а не для створення змінної. Однак не варто робити це для кожного типу, який задовольняє інтерфейсу. За домовленістю, такі оголошення використовуються лише тоді, коли в коді немає статичних перетворень, що є рідкісною подією. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ефективна Go (Effective Go) 2 | Оригінал: [«Effective Go»](https://go.dev/doc/effective_go) 3 | 4 | ## Зміст 5 | - [Вступ](#Вступ) 6 | - [Приклади](#Приклади) 7 | - [Форматування](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/01.%20Форматування.md#Форматування) 8 | - [Коментарі](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/02.%20Коментарі.md#Коментарі) 9 | - [Найменування](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/03.%20Найменування.md#Найменування) 10 | - [Назви пакетів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/03.%20Найменування.md#Назви-пакетів) 11 | - [Геттери](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/03.%20Найменування.md#Геттери) 12 | - [Назви інтерфейсів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/03.%20Найменування.md#Назви-інтерфейсів) 13 | - [MixedCaps](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/03.%20Найменування.md#MixedCaps) 14 | - [Крапки з комою](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/04.%20Крапки%20з%20комою.md#Крапки-з-комою) 15 | - [Керуючі структури](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/05.%20Керуючі%20структури.md#Керуючі-структури) 16 | - [If](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/05.%20Керуючі%20структури.md#If) 17 | - [Перевизначення й перепризначення](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/05.%20Керуючі%20структури.md#Перевизначення-й-перепризначення) 18 | - [For](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/05.%20Керуючі%20структури.md#For) 19 | - [Switch](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/05.%20Керуючі%20структури.md#Switch) 20 | - [Типізований switch](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/05.%20Керуючі%20структури.md#Типізований-switch) 21 | - [Функції](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/06.%20Функції.md#Функції) 22 | - [Множинне повернення результатів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/06.%20Функції.md#Множинне-повернення-результатів) 23 | - [Найменування параметрів результату](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/06.%20Функції.md#Найменування-параметрів-результату) 24 | - [Defer](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/06.%20Функції.md#Defer) 25 | - [Дані](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Дані) 26 | - [Створення за допомогою new](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Створення-за-допомогою-new) 27 | - [Конструктори та складені літерали](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Конструктори-та-складені-літерали) 28 | - [Створення за допомогою make](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Створення-за-допомогою-make) 29 | - [Масиви](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Масиви) 30 | - [Зрізи](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Зрізи) 31 | - [Двовимірні зрізи](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Двовимірні-зрізи) 32 | - [Мапи](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Мапи) 33 | - [Друк](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Друк) 34 | - [Приєднання](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/07.%20Дані.md#Приєднання) 35 | - [Ініціалізація](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/08.%20Ініціалізація.md#Ініціалізація) 36 | - [Константи](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/08.%20Ініціалізація.md#Константи) 37 | - [Змінні](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/08.%20Ініціалізація.md#Змінні) 38 | - [Функція init](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/08.%20Ініціалізація.md#Функція-init) 39 | - [Методи](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/09.%20Методи.md#Методи) 40 | - [Вказівники чи значення](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/09.%20Методи.md#Вказівники-чи-значення) 41 | - [Інтерфейси та інші типи](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/10.%20Інтерфейси%20та%20інші%20типи.md#Інтерфейси-та-інші-типи) 42 | - [Інтерфейси](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/10.%20Інтерфейси%20та%20інші%20типи.md#Інтерфейси) 43 | - [Перетворення](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/10.%20Інтерфейси%20та%20інші%20типи.md#Перетворення) 44 | - [Конвертація інтерфейсів та твердження типів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/10.%20Інтерфейси%20та%20інші%20типи.md#Конвертація-інтерфейсів-і-твердження-типів) 45 | - [Узагальнені типи](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/10.%20Інтерфейси%20та%20інші%20типи.md#Узагальнені-типи) 46 | - [Інтерфейси й методи](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/10.%20Інтерфейси%20та%20інші%20типи.md#Інтерфейси-й-методи) 47 | - [Порожній ідентифікатор](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/11.%20Порожній%20ідентифікатор.md#Порожній-ідентифікатор) 48 | - [Порожній ідентифікатор у множинному присвоюванні](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/11.%20Порожній%20ідентифікатор.md#Порожній-ідентифікатор-у-множинному-присвоюванні) 49 | - [Невикористовувані імпортування й значення](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/11.%20Порожній%20ідентифікатор.md#Невикористовувані-імпортування-й-значення) 50 | - [Імпортування для сторонніх ефектів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/11.%20Порожній%20ідентифікатор.md#Імпортування-для-сторонніх-ефектів) 51 | - [Перевірки інтерфейсів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/11.%20Порожній%20ідентифікатор.md#Перевірки-інтерфейсів) 52 | - [Вкладення](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/12.%20Вкладення.md#Вкладення) 53 | - [Конкурентність](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/13.%20Конкурентність.md#Конкурентність) 54 | - [Розподіл за повідомленнями](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/13.%20Конкурентність.md#Розподіл-за-повідомленнями) 55 | - [Горутини](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/13.%20Конкурентність.md#Горутини) 56 | - [Канали](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/13.%20Конкурентність.md#Канали) 57 | - [Канали каналів](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/13.%20Конкурентність.md#Канали-каналів) 58 | - [Паралелізм](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/13.%20Конкурентність.md#Паралелізм) 59 | - [Поточний буфер](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/13.%20Конкурентність.md#Поточний-буфер) 60 | - [Помилки](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/14.%20Помилки.md#Помилки) 61 | - [Паніка](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/14.%20Помилки.md#Паніка) 62 | - [Відновлення](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/14.%20Помилки.md#Відновлення) 63 | - [Вебсервер](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/15.%20Вебсервер.md#Вебсервер) 64 | 65 | ## Вступ 66 | Go — це нова мова. Запозичуючи ідеї з наявних мов, вона має й свої незвичайні властивості, які роблять ефективні програми на Go відмінними за характером від програм, написаних на подібних їй. Прямий переклад програм, написаних на C++ або Java, на Go навряд чи дасть задовільний результат — програми на Java написані на Java, а не на Go. З іншого боку, обдумування проблеми з перспективи Go може призвести до створення успішної, але зовсім іншої програми. Іншими словами, щоб добре писати на Go, важливо розуміти її властивості та ідіоми. Також важливо знати встановлені конвенції для програмування на Go, такі як найменування, форматування, побудова програм і так далі, щоб програми, які ви пишете, були зрозумілими для інших програмістів на Go. 67 | 68 | Цей документ містить поради щодо написання зрозумілого, ідіоматичного коду мовою Go. Він доповнює [специфікацію мови](https://go.dev/ref/spec) (англ.), [«Тур із Go»](https://go-tour-ua-translation.lm.r.appspot.com/welcome/1) (укр.) та [«Як писати код на Go»](https://go.dev/doc/code) (англ.), які вам слід прочитати першими. 69 | 70 | **Примітка, додана у січні 2022 року:** Цей документ було написано до випуску Go у 2009 році, і відтоді він суттєво не оновлювався. Хоча це хороший посібник для розуміння того, як користуватися самою мовою, завдяки стабільності мови, в ньому мало сказано про бібліотеки й нічого про значні зміни в екосистемі Go з моменту написання, такі як система збірки, тестування, модулі та поліморфізм. Ми не плануємо її оновлювати, оскільки змін було багато, а велика кількість документів, блогів та книг, що постійно зростає, чудово описують сучасне використання Go. Цей документ продовжує бути корисним, але ви повинні розуміти, що тут викладено далеко не всю інформацію. Додатковий контекст дивіться у [питанні 28782](https://github.com/golang/go/issues/28782). 71 | 72 | ### Приклади 73 | Вихідні коди пакетів Go призначені не лише для використання у якості основної бібліотеки, але і як приклади використання мови. Щобільше, багато пакетів містять робочі, самодостатні виконувані приклади, які ви можете запустити безпосередньо з вебсайту [go.dev](https://go.dev), наприклад, [цей](https://go.dev/pkg/strings/#example-Map) (якщо потрібно, натисніть на слово «Приклад», щоб відкрити його). Якщо у вас є питання про те, як підійти до проблеми або як щось можна реалізувати, документація, код і приклади в бібліотеці можуть надати відповіді, ідеї та підказки. -------------------------------------------------------------------------------- /14. Помилки.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Помилки](#Помилки) 3 | - [Паніка](#Паніка) 4 | - [Відновлення](#Відновлення) 5 | 6 | ## Помилки 7 | Бібліотечні процедури часто повинні повертати користувачеві деяке повідомлення про помилку. Як згадувалося раніше, множинне повернення у Go дозволяє легко повертати детальний опис помилки разом зі звичайним значенням, що повертається. Хорошим стилем є використання цієї можливості для надання детальної інформації про помилку. Наприклад, як ми побачимо, `os.Open` не просто повертає вказівник `nil` у разі невдачі, але й значення помилки, яке описує, що саме пішло не так. 8 | 9 | За домовленістю, помилки мають тип `error`, простий вбудований інтерфейс. 10 | ```go 11 | type error interface { 12 | Error() string 13 | } 14 | ``` 15 | 16 | Автор бібліотеки може реалізувати цей інтерфейс з багатшою моделлю під кришкою, що дозволить не лише побачити помилку, але й надати певний контекст. Як уже згадувалося, окрім звичайного значення `*os.File`, що повертається, `os.Open` також повертає значення помилки. Якщо файл відкрито успішно, помилка буде `nil`, але якщо виникла проблема, він буде містити `os.PathError`: 17 | 18 | ```go 19 | // PathError фіксує помилку та операцію 20 | // і шлях до файлу, який її спричинив. 21 | type PathError struct { 22 | Op string // "open", "unlink" тощо. 23 | Path string // Асоційований файл. 24 | Err error // Повернуто системним викликом. 25 | } 26 | 27 | func (e *PathError) Error() string { 28 | return e.Op + " " + e.Path + ": " + e.Err.Error() 29 | } 30 | ``` 31 | 32 | Функція `Error` у `PathError` генерує такий рядок: 33 | ``` 34 | open /etc/passwx: no such file or directory 35 | ``` 36 | 37 | Така помилка, яка містить ім'я проблемного файлу, операцію та помилку операційної системи, яку ця операція спричинила, є корисною, навіть якщо її виведено далеко від виклику, який її спричинив; вона є набагато інформативнішою, ніж просте «такого файлу або каталогу не існує». 38 | 39 | Якщо це можливо, у рядках помилок слід вказувати їхнє походження, наприклад, за допомогою префікса, що позначає операцію або пакунок, який згенерував помилку. Наприклад, у пакеті `image` рядок помилки декодування через невідомий формат має вигляд `"image: unknown format"`. 40 | 41 | Користувачі, яких цікавлять точні деталі помилки, можуть скористатися типізованим `switch` або твердженням типу для пошуку конкретних помилок і вилучення деталей. У випадку з `PathErrors` це може включати перевірку внутрішнього поля `Err` на наявність помилок, які можна виправити. 42 | ```go 43 | for try := 0; try < 2; try++ { 44 | file, err = os.Create(filename) 45 | if err == nil { 46 | return 47 | } 48 | if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC { 49 | deleteTempFiles() // Звільнити трохи місця. 50 | continue 51 | } 52 | return 53 | } 54 | ``` 55 | 56 | Другий оператор `if` тут є ще одним [твердженням типу](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/10.%20Інтерфейси%20та%20інші%20типи.md#Конвертація-інтерфейсів-і-твердження-типів). Якщо він не спрацює, то `ok` буде `false`, а `e` буде `nil`. Якщо він спрацює, то `ok` буде `true`, що означає, що помилка була типу `*os.PathError`, а також `e`, який ми можемо дослідити для отримання додаткової інформації про помилку. 57 | 58 | ### Паніка 59 | Звичайний спосіб повідомити користувача про помилку — повернути помилку як додаткове значення. Канонічний метод `Read` є добре відомим прикладом; він повертає лічильник байт та повідомлення про помилку. Але що робити, якщо помилку неможливо виправити? Іноді програма просто не може продовжити роботу. 60 | 61 | Для цього існує вбудована функція `panic`, яка фактично створює помилку під час виконання, яка зупиняє програму (але про це в наступному розділі). Функція приймає єдиний аргумент довільного типу — найчастіше рядок, який буде виведено під час завершення роботи програми. Це також спосіб вказати, що сталося щось неможливе, наприклад, вихід з нескінченного циклу. 62 | ```go 63 | // Реалізація обчислення кубічного кореня за методом Ньютона. 64 | func CubeRoot(x float64) float64 { 65 | z := x / 3 // Довільне початкове значення 66 | for i := 0; i < 1e6; i++ { 67 | prevz := z 68 | z -= (z*z*z - x) / (3*z*z) 69 | if veryClose(z, prevz) { 70 | return z 71 | } 72 | } 73 | // Мільйон ітерацій не зійшовся; щось не так. 74 | panic(fmt.Sprintf("CubeRoot(%g) не зійшовся", x)) 75 | } 76 | ``` 77 | Це лише приклад, але у реальних бібліотечних функціях слід уникати паніки. Якщо проблему можна замаскувати або обійти, завжди краще не зупиняти роботу, аніж виводити з ладу всю програму. Один з можливих контрприкладів — під час ініціалізації: якщо бібліотека дійсно не може налаштувати себе, може бути розумним, так би мовити, панікувати. 78 | ```go 79 | var user = os.Getenv("USER") 80 | 81 | func init() { 82 | if user == "" { 83 | panic("no value for $USER") 84 | } 85 | } 86 | ``` 87 | 88 | ### Відновлення 89 | Коли викликається `panic`, у тому числі неявно при помилках під час виконання, таких як індексація фрагмента за межами допустимих значень або помилка перевірки типу, вона негайно зупиняє виконання поточної функції й починає розгортати стек горутини, виконуючи всі відкладені функції по дорозі. Якщо це розгортання досягає вершини стека горутини, програма завершується. Однак, можна скористатися вбудованою функцією `recover`, щоб повернути контроль над горутиною й відновити нормальне виконання. 90 | 91 | Виклик `recover` зупиняє розгортання і повертає аргумент, переданий у `panic`. Оскільки єдиний код, який виконується під час розгортання, знаходиться всередині відкладених функцій, `recover` корисна лише всередині відкладених функцій. 92 | 93 | Одне із застосувань `recover` — це зупинка горутини, що вийшла з ладу на сервері, не вбиваючи при цьому інші горутини, що виконуються. 94 | 95 | ```go 96 | func server(workChan <-chan *Work) { 97 | for work := range workChan { 98 | go safelyDo(work) 99 | } 100 | } 101 | 102 | func safelyDo(work *Work) { } } 103 | defer func() { 104 | if err := recover(); err != nil { 105 | log.Println("work failed:", err) 106 | } 107 | }() 108 | do(work) 109 | } 110 | ``` 111 | 112 | У цьому прикладі, якщо `do(work)` запанікує, результат буде залоговано, і горутина завершить роботу, не заважаючи іншим. У відкладеному закритті не потрібно робити нічого іншого; виклик `recover` повністю обробляє умову. 113 | 114 | Оскільки `recover` завжди повертає `nil`, якщо не викликається безпосередньо з відкладеної функції, відкладений код може без проблем викликати бібліотечні процедури, які самі використовують `panic` і `recover`. Наприклад, відкладена функція у `safelyDo` може викликати функцію логування перед викликом `recover`, і код логування працюватиме без впливу стану паніки. 115 | 116 | З нашим шаблоном відновлення функція `do` (і все, що вона викликає) може вийти з будь-якої поганої ситуації, просто викликавши `panic`. Ми можемо використовувати цю ідею для спрощення обробки помилок у складному програмному забезпеченні. Розглянемо ідеалізовану версію пакета `regexp`, який повідомляє про помилки синтаксичного аналізу шляхом виклику `panic` з локальним типом помилки. Ось визначення `Error`, методу `error` та функції `Compile`. 117 | 118 | ```go 119 | // Error - це тип помилки розбору; він задовольняє інтерфейс error. 120 | type Error string 121 | func (e Error) Error() string { 122 | return string(e) 123 | } 124 | 125 | // error - метод *Regexp, що повідомляє про помилки розбору, 126 | // викликаючи паніку за допомогою Error. 127 | func (regexp *Regexp) error(err string) { 128 | panic(Error(err)) 129 | } 130 | 131 | // Compile повертає розібране представлення регулярного виразу. 132 | func Compile(str string) (regexp *Regexp, err error) { 133 | regexp = new(Regexp) 134 | // doParse викличе паніку, якщо станеться помилка розбору. 135 | defer func() { 136 | if e := recover(); e != nil { 137 | regexp = nil // Очистити значення, що повертається. 138 | err = e.(Error) // Знову викличе паніку, якщо це не помилка розбору. 139 | } 140 | }() 141 | return regexp.doParse(str), nil 142 | } 143 | ``` 144 | 145 | Якщо `doParse` панікує, блок відновлення встановить `nil` значенням повернення — відкладені функції можуть змінювати іменовані значення повернення. Потім він перевірить у присвоєнні `err`, що проблема була помилкою синтаксичного аналізу, стверджуючи, що вона має локальний тип `Error`. Якщо це не так, твердження типу не спрацює, що призведе до помилки під час виконання, яка продовжить розгортання стека так, ніби його ніщо не переривало. Ця перевірка означає, що якщо трапиться щось непередбачуване, наприклад, індекс вийде за межі, код завершиться невдачею, навіть якщо ми використовуємо паніку та відновлення для обробки помилок синтаксичного аналізу. 146 | 147 | Завдяки обробці помилок, метод `error` (оскільки це метод, прив'язаний до типу, цілком нормально, навіть природно, що він має те саме ім'я, що і вбудований тип `error`) дозволяє легко повідомляти про помилки розбору, не турбуючись про розмотування стека розбору власноруч: 148 | ```go 149 | if pos == 0 { 150 | re.error("'*' illegal at start of expression") 151 | } 152 | ``` 153 | 154 | Хоч цей патерн і корисний, його слід використовувати лише всередині пакета. `Parse` перетворює свої внутрішні виклики `panic` у значення `error`; він не показує `panic` своєму клієнту. Це хороше правило, якого слід дотримуватися. 155 | 156 | До речі, ця ідіома повторної паніки змінює значення паніки, якщо виникає справжня помилка. Однак, як початкова, так і нова помилки будуть представлені у звіті про збої, тому першопричину проблеми все одно буде видно. Таким чином, цього простого підходу до повторної паніки зазвичай достатньо — це все ж таки збій — але якщо ви хочете відображати лише початкове значення, ви можете написати трохи більше коду для фільтрації неочікуваних проблем і повторної паніки з початковою помилкою. Залишаємо це як вправу для читача. -------------------------------------------------------------------------------- /05. Керуючі структури.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Керуючі структури](#Керуючі-структури) 3 | - [If](#If) 4 | - [Перевизначення й перепризначення](#Перевизначення-й-перепризначення) 5 | - [For](#For) 6 | - [Switch](#Switch) 7 | - [Типізований switch](#Типізований-switch) 8 | 9 | ## Керуючі структури 10 | Керуючі структури у Go подібні до таких у C, але відрізняються важливими особливостями. У Go немає циклу `do` або `while`, є лише дещо узагальнений `for`; `switch` є більш гнучким; `if` і `switch` приймають необов'язковий оператор ініціалізації, як і `for`; оператори `break` і `continue` приймають необов'язкову мітку для визначення того, що потрібно перервати або продовжити; і є нові керуючі структури, включаючи типізований `switch` і багатоканальний мультиплексор комунікацій, `select`. Синтаксис також дещо відрізняється: немає круглих дужок, а тіло завжди повинно бути розділене фігурними дужками. 11 | 12 | ### If 13 | У Go простий if-вираз виглядає так: 14 | ```go 15 | if x > 0 { 16 | return y 17 | } 18 | ``` 19 | Обов'язкові дужки спрощують написання простих інструкцій `if` у декількох рядках. Так чи інакше, це гарний стиль, особливо коли тіло містить оператор керування, такий як `return` або `break`. 20 | 21 | Оскільки `if` і `switch` допускають оператор ініціалізації, їх часто використовують для встановлення локальної змінної. 22 | ```go 23 | if err := file.Chmod(0664); err != nil { 24 | log.Print(err) 25 | return err 26 | } 27 | ``` 28 | У бібліотеках Go ви побачите, що коли інструкція `if` не переходить у наступну інструкцію, тобто тіло закінчується `break`, `continue`, `goto` або `return`, непотрібна умова `else` опускається. 29 | ```go 30 | f, err := os.Open(name) 31 | if err != nil { 32 | return err 33 | } 34 | codeUsing(f) 35 | ``` 36 | Нижче наведено приклад поширеної ситуації, коли код є захищеним від послідовності помилок. Код читається добре, якщо виконується без помилок, оминаючи випадки їх виникнення. Оскільки обробки помилок, як правило, закінчуються операторами `return`, то код не потребує використання операторів `else`. 37 | ```go 38 | f, err := os.Open(name) 39 | if err != nil { 40 | return err 41 | } 42 | d, err := f.Stat() 43 | if err != nil { 44 | f.Close() 45 | return err 46 | } 47 | codeUsing(f, d) 48 | ``` 49 | 50 | ### Перевизначення й перепризначення 51 | **Зауваження:** Останній приклад у попередньому розділі демонструє, як працює форма короткого оголошення `:=`. Оголошення, яке викликає `os.Open`, виглядає так, 52 | ```go 53 | f, err := os.Open(name) 54 | ``` 55 | у цьому виразі оголошуються дві змінні, `f` та `err`. Кількома рядками пізніше йде виклик `f.Stat`, 56 | ```go 57 | d, err := f.Stat() 58 | ``` 59 | який виглядає так, ніби він оголошує `d` та `err`. Зверніть увагу, що `err` з'являється в обох операторах. Це дублювання є законним: `err` оголошується в першому виразі, але лише _перепризначається_ в другому. Це означає, що виклик `f.Stat` використовує змінну `err`, що вже існує, оголошену вище, і просто присвоює їй нове значення. 60 | 61 | В оголошенні `:=` змінна `v` може бути присутньою, навіть якщо вона вже була оголошена, за наступних умов: 62 | - оголошення відбувається в тій же самій області видимості, що й наявна змінна `v` (якщо `v` уже оголошено за межами видимості, то оголошення створить нову змінну§) 63 | - відповідне значення, під час ініціалізації, може бути присвоєно `v` 64 | - існує хоча б одна нова змінна в оголошенні, яка буде створена заново 65 | 66 | Ця незвичайна властивість — чиста практичність, яка слугує для використання однієї змінної `err`, наприклад, у довгому ланцюжку `if-else`. Ви побачите, що вона часто використовується. 67 | 68 | §Варто зазначити, що у Go область видимості параметрів функції та значень, що повертаються, збігається з областю видимості тіла функції, хоча лексично вони знаходяться за межами фігурних дужок, що охоплюють тіло функції. 69 | 70 | ### For 71 | Цикл `for` у Go схожий на відповідний цикл C, але він не є таким самим. Цей уніфікує `for` і `while`, але не містить `do-while`. Існує три форми, лише одна з яких має крапку з комою. 72 | ```go 73 | // C-подібний for 74 | for init; condition; post { } 75 | 76 | // С-подібний while 77 | for condition { } 78 | 79 | // C-подібний for(;;) 80 | for { } 81 | ``` 82 | Короткі оголошення дозволяють легко оголосити початкові умови прямо в циклі. 83 | ```go 84 | sum := 0 85 | for i := 0; i < 10; i++ { 86 | sum += i 87 | } 88 | ``` 89 | Якщо ви виконуєте цикл над масивом, зрізом, рядком або мапою, або читаєте з каналу, то для керування циклом можна використати оператор `range`. 90 | ```go 91 | for key, value := range oldMap { 92 | newMap[key] = value 93 | } 94 | ``` 95 | Якщо вам потрібен лише перший елемент у _діапазоні_ (ключ або індекс), відкиньте другий: 96 | ```go 97 | for key := range m { 98 | if key.expired() { 99 | delete(m, key) 100 | } 101 | } 102 | ``` 103 | Якщо вам потрібен лише другий елемент у _діапазоні_ (значення), використовуйте _порожній ідентифікатор_ (_), щоб відкинути перший: 104 | ```go 105 | sum := 0 106 | for _, value := range array { 107 | sum += value 108 | } 109 | ``` 110 | Порожній ідентифікатор має багато сценаріїв використання та буде описаний у [подальшому розділі](#Порожній-ідентифікатор). 111 | 112 | Для рядків `range` робить більше роботи за вас, зокрема виділяє окремі символи Unicode, виконуючи парсинг UTF-8. Помилкові кодування займають один байт і створюють руну (rune) заміни U+FFFD. (Назва (з відповідним вбудованим типом) руни є термінологією Go для одного символу Unicode. Детальніше дивіться у [специфікації мови](https://go.dev/ref/spec#Rune_literals) (англ.)). Цикл 113 | ```go 114 | for pos, char := range "日本\x80語" { // \x80 не є коректним кодуванням UTF-8 115 | fmt.Printf("character %#U starts at byte position %d\n", char, pos) 116 | } 117 | ``` 118 | виводить 119 | ``` 120 | character U+65E5 '日' starts at byte position 0 121 | character U+672C '本' starts at byte position 3 122 | character U+FFFD '�' starts at byte position 6 123 | character U+8A9E '語' starts at byte position 7 124 | ``` 125 | 126 | Наостанок, Go не має оператора `кома`, а `++` і `--` є інструкціями, а не виразами. Таким чином, якщо ви хочете використати декілька змінних в операторі `for`, вам слід скористатися паралельним присвоюванням (це виключає можливість використання `++` і `--`). 127 | ```go 128 | // Обертання масиву a 129 | for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { 130 | a[i], a[j] = a[j], a[i] 131 | } 132 | ``` 133 | 134 | ### Switch 135 | У мові Go `switch` є більш узагальненим, ніж у C. Вирази не обов'язково мають бути константами або навіть цілими числами, умови перевіряються зверху-вниз до знаходження відповідності, і якщо `switch` не має виразів, то переходить у `true`. Отже, ідіоматично можливо записувати `if-else-if-else` ланцюжок як `switch`. 136 | ```go 137 | func unhex(c byte) byte { 138 | switch { 139 | case '0' <= c && c <= '9': 140 | return c - '0' 141 | case 'a' <= c && c <= 'f': 142 | return c - 'a' + 10 143 | case 'A' <= c && c <= 'F': 144 | return c - 'A' + 10 145 | } 146 | return 0 147 | } 148 | ``` 149 | 150 | Автоматичний пропуск умов відсутній, але, при цьому, вони можуть бути записані через кому: 151 | ```go 152 | func shouldEscape(c byte) bool { 153 | switch c { 154 | case ' ', '?', '&', '=', '#', '+', '%': 155 | return true 156 | } 157 | return false 158 | } 159 | ``` 160 | 161 | Попри те, що вони не настільки поширені в Go, як у деяких інших C-подібних мовах, `break` може бути використаний для дострокового переривання `switch`. Хоча іноді треба перервати зовнішній (стосовно `switch`) цикл, а не сам `switch`, і в Go цього можна досягти шляхом додавання мітки перед циклом, і переходом до цієї мітки в разі виклику `break`. У наступному прикладі представлені обидва випадки: 162 | ```go 163 | Loop: 164 | for n := 0; n < len(src); n += size { 165 | switch { 166 | case src[n] < sizeOne: 167 | if validateOnly { 168 | break 169 | } 170 | size = 1 171 | update(src[n]) 172 | 173 | case src[n] < sizeTwo: 174 | if n+1 >= len(src) { 175 | err = errShortInput 176 | break Loop 177 | } 178 | if validateOnly { 179 | break 180 | } 181 | size = 2 182 | update(src[n] + src[n+1]< b 194 | func Compare(a, b []byte) int { 195 | for i := 0; i < len(a) && i < len(b); i++ { 196 | switch { 197 | case a[i] > b[i]: 198 | return 1 199 | case a[i] < b[i]: 200 | return -1 201 | } 202 | } 203 | switch { 204 | case len(a) > len(b): 205 | return 1 206 | case len(a) < len(b): 207 | return -1 208 | } 209 | return 0 210 | } 211 | ``` 212 | 213 | ### Типізований switch 214 | Оператор `switch` також можна використовувати для визначення динамічного типу інтерфейсних змінних. Таким чином, типізований `switch` використовує синтаксис перевірки типу з ключовим словом `type` у дужках. Якщо `switch` оголошує змінну у виразі, змінна матиме відповідний тип у кожному його пункті. Також ідіоматично повторно використовувати імена змінних у таких випадках, фактично оголошуючи нову змінну з тим самим іменем, але з іншим типом у кожному випадку. 215 | ```go 216 | var t interface{} 217 | t = functionOfSomeType() 218 | switch t := t.(type) { 219 | default: 220 | fmt.Printf("unexpected type %T\n", t) // %T виводить тип змінної t 221 | case bool: 222 | fmt.Printf("boolean %t\n", t) // t має тип bool 223 | case int: 224 | fmt.Printf("integer %d\n", t) // t має тип int 225 | case *bool: 226 | fmt.Printf("pointer to boolean %t\n", *t) // t має тип *bool 227 | case *int: 228 | fmt.Printf("pointer to integer %d\n", *t) // t має тип *int 229 | } 230 | ``` -------------------------------------------------------------------------------- /10. Інтерфейси та інші типи.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Інтерфейси та інші типи](#Інтерфейси-та-інші-типи) 3 | - [Інтерфейси](#Інтерфейси) 4 | - [Перетворення](#Перетворення) 5 | - [Конвертація інтерфейсів та твердження типів](#Конвертація-інтерфейсів-та-твердження-типів) 6 | - [Узагальнені типи](#Узагальнені-типи) 7 | - [Інтерфейси й методи](#Інтерфейси-й-методи) 8 | 9 | ## Інтерфейси та інші типи 10 | 11 | ### Інтерфейси 12 | Інтерфейси у Go надають можливість визначити поведінку об'єкта: якщо _щось_ може робити _це_, то _його_ можна використовувати _тут_. Ми вже бачили кілька простих прикладів: користувацькі принтери можна реалізувати за допомогою методу `String`, а `Fprintf` може генерувати вивід на будь-що за допомогою методу `Write`. Інтерфейси з одним або двома методами є поширеним явищем у коді Go, і їм зазвичай дають назву, похідну від методу, наприклад, `io.Writer` для того, що реалізує метод `Write`. 13 | 14 | Тип може реалізовувати декілька інтерфейсів. Наприклад, колекція може бути відсортована процедурами у пакеті `sort`, якщо він реалізує `sort.Interface`, який містить `Len()`, `Less(i, j int) bool` і `Swap(i, j int)`, а також може мати власний форматер. У цьому надуманому прикладі `Sequence` задовольняє обом вимогам. 15 | ```go 16 | type Sequence []int 17 | 18 | // Методи, необхідні для інтерфейсу sort.Interface. 19 | func (s Sequence) Len() int { 20 | return len(s) 21 | } 22 | func (s Sequence) Less(i, j int) bool { 23 | return s[i] < s[j] 24 | } 25 | func (s Sequence) Swap(i, j int) { 26 | s[i], s[j] = s[j], s[i] 27 | } 28 | 29 | // Copy повертає копію Sequence. 30 | func (s Sequence) Copy() Sequence { 31 | copy := make(Sequence, 0, len(s)) 32 | return append(copy, s...) 33 | } 34 | 35 | // Метод для друку - сортує елементи перед друком. 36 | func (s Sequence) String() string { 37 | s = s.Copy() // Робимо копію; не перезаписуємо аргумент. 38 | sort.Sort(s) 39 | str := "[" 40 | for i, elem := range s { // Цикл має складність O(N²); вирішимо це у наступному прикладі. 41 | if i > 0 { 42 | str += " " 43 | } 44 | str += fmt.Sprint(elem) 45 | } 46 | return str + "]" 47 | } 48 | ``` 49 | 50 | ### Перетворення 51 | Метод `String` у `Sequence` відтворює роботу, яку `Sprint` вже виконує для зрізів. (Він також має складність O(N²), що є поганим показником). Ми можемо розділити зусилля (а також пришвидшити роботу), якщо перетворимо `Sequence` у звичайний `[]int` перед викликом `Sprint`. 52 | ```go 53 | func (s Sequence) String() string { 54 | s = s.Copy() 55 | sort.Sort(s) 56 | return fmt.Sprint([]int(s)) 57 | } 58 | ``` 59 | 60 | Цей метод є ще одним прикладом техніки перетворення для безпечного виклику `Sprintf` з методу `String`. Оскільки ці два типи (`Sequence` та `[]int`) є однаковими, якщо ігнорувати назву, перетворення між ними не є забороненим. Перетворення не створює нового значення, воно лише тимчасово діє так, ніби значення, що вже існує, має новий тип. (Існують інші легальні перетворення, наприклад, з цілого у число з рухомою комою, які створюють нове значення). 61 | 62 | У програмах на Go існує ідіома перетворення типу виразу для доступу до іншого набору методів. Як приклад, ми можемо використати наявний тип `sort.IntSlice`, щоб звести весь приклад до цього: 63 | ```go 64 | type Sequence []int 65 | 66 | // Метод для друку - сортує елементи перед друком. 67 | func (s Sequence) String() string { 68 | s = s.Copy() 69 | sort.IntSlice(s).Sort() 70 | return fmt.Sprint([]int(s)) 71 | } 72 | ``` 73 | 74 | Тепер, замість того, щоб використовувати `Sequence` для реалізації декількох інтерфейсів (сортування та друку), ми використовуємо можливість перетворення елемента даних у декілька типів (`Sequence`, `sort.IntSlice` та `[]int`), кожен з яких виконує певну частину роботи. Це більш незвично на практиці, але може бути ефективно. 75 | 76 | ### Конвертація інтерфейсів та твердження типів 77 | [Типізований switch](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/05.%20Керуючі%20структури.md#Типізований-switch) є формою перетворення: він бере інтерфейс і для кожного випадку у `switch`, в певному сенсі, перетворює його на тип цього випадку. Ось спрощена версія того, як код у `fmt.Printf` перетворює значення у рядок за допомогою `типізованого switch`. Якщо значення вже є рядком, ми хочемо отримати власне значення рядка, яке зберігається в інтерфейсі, а якщо він має метод `String`, ми хочемо отримати результат виклику цього методу. 78 | ```go 79 | type Stringer interface { 80 | String() string 81 | } 82 | 83 | var value interface{} // Значення, надане викликачем. 84 | switch str := value.(type) { 85 | case string: 86 | return str 87 | case Stringer: 88 | return str.String() 89 | } 90 | ``` 91 | У першому випадку знаходиться конкретне значення, у другому — інтерфейс перетворюється на інший інтерфейс. Змішувати типи таким чином цілком нормально. 92 | 93 | Але що, якщо нас цікавить лише один тип? Якщо ми знаємо, що значення містить рядок, і ми просто хочемо його витягти? Для цього підійде _одноваріантний_ типізований `switch`, але також підійде і _твердження типу_. Твердження типу приймає значення інтерфейсу і витягує з нього значення вказаного явного типу. Синтаксис запозичено з умови, що відкриває типізований `switch`, але з явним типом, а не з ключовим словом `type`: 94 | ```go 95 | value.(typeName) 96 | ``` 97 | і результатом буде нове значення зі статичним типом `typeName`. Цей тип має бути або конкретним типом, що зберігається в інтерфейсі, або другим типом інтерфейсу, до якого можна перетворити значення. Щоб витягти рядок, який, як ми знаємо, міститься у значенні, можна написати 98 | ```go 99 | str := value.(string) 100 | ``` 101 | Але якщо виявиться, що значення не містить рядка, програма завершиться з помилкою під час виконання. Щоб уникнути цього, використовуйте ідіому «кома ok» для безпечної перевірки того, чи є значення рядком: 102 | ```go 103 | str, ok := value.(string) 104 | if ok { 105 | fmt.Printf("string value is: %q\n", str) 106 | } else { 107 | fmt.Printf("value is not a string\n") 108 | } 109 | ``` 110 | Якщо перевірка типу завершиться невдачею, `str` все одно існуватиме і матиме тип `string`, але матиме нульове значення, тобто буде порожнім рядком. 111 | 112 | Для ілюстрації цієї можливості наведено інструкцію `if`-`else`, яка еквівалентна типізованому `switch`, що відкриває цей розділ. 113 | ```go 114 | if str, ok := value.(string); ok { 115 | return str 116 | } else if str, ok := value.(Stringer); ok { 117 | return str.String() 118 | } 119 | ``` 120 | 121 | ### Узагальнені типи 122 | Якщо тип існує лише для реалізації інтерфейсу і ніколи не матиме експортованих методів за межами цього інтерфейсу, нема потреби експортувати сам тип. Експорт тільки інтерфейсу дає зрозуміти, що значення не має ніякої цікавої поведінки за межами того, що описано в інтерфейсі. Це також дозволяє уникнути необхідності повторювати документацію для кожного екземпляру спільного методу. 123 | 124 | У таких випадках конструктор повинен повертати значення інтерфейсу, а не тип, що його реалізує. Наприклад, у бібліотеках хешування і `crc32.NewIEEE`, і `adler32.New` повертають інтерфейсний тип `hash.Hash32`. Заміна алгоритму `CRC-32` на `Adler-32` у програмі на Go вимагає лише зміни виклику конструктора; решта коду не зазнає впливу від зміни алгоритму. 125 | 126 | Подібний підхід дозволяє відокремити алгоритми потокового шифрування в різних криптопакетах від блокових шифрів, які вони об'єднують. Інтерфейс `Block` у пакеті `crypto/cipher` визначає поведінку блокового шифру, який забезпечує шифрування одного блоку даних. Тоді, за аналогією з пакетом `bufio`, пакети шифрів, що реалізують цей інтерфейс, можна використовувати для побудови потокових шифрів, представлених інтерфейсом `Stream`, не знаючи деталей блокового шифрування. 127 | 128 | Інтерфейси `crypto/cypher` виглядають наступним чином: 129 | ```go 130 | type Block interface { 131 | BlockSize() int 132 | Encrypt(dst, src []byte) 133 | Decrypt(dst, src []byte) 134 | } 135 | 136 | type Stream interface { 137 | XORKeyStream(dst, src []byte) 138 | } 139 | ``` 140 | 141 | Ось визначення потоку в режимі лічильника (CTR), який перетворює блоковий шифр на потоковий; зверніть увагу, що деталі блокового шифру абстраговані: 142 | ```go 143 | // NewCTR повертає Stream, який шифрує/дешифрує за допомогою заданого Block у 144 | // режимі лічильника. Довжина iv має бути такою ж, як розмір блоку Block. 145 | func NewCTR(block Block, iv []byte) Stream 146 | ``` 147 | 148 | `NewCTR` застосовується не лише до одного конкретного алгоритму шифрування і джерела даних, але й до будь-якої реалізації інтерфейсу `Block` і будь-якого потоку. Оскільки вони повертають інтерфейсні значення, заміна шифрування CTR на інші режими шифрування є локальною зміною. Виклики конструктора необхідно відредагувати, але оскільки навколишній код повинен розглядати результат лише як потік, він не помітить різниці. 149 | 150 | ### Інтерфейси й методи 151 | Оскільки майже будь-що може мати методи, майже будь-що може відповідати інтерфейсу. Один з ілюстративних прикладів знаходиться у пакеті `http`, який визначає інтерфейс `Handler`. Будь-який об'єкт, що реалізує `Handler`, може обслуговувати HTTP-запити. 152 | ```go 153 | type Handler interface { 154 | ServeHTTP(ResponseWriter, *Request) 155 | } 156 | ``` 157 | `ResponseWriter` сам по собі є інтерфейсом, який надає доступ до методів, необхідних для повернення відповіді клієнту. Ці методи включають стандартний метод `Write`, тому `http.ResponseWriter` можна використовувати всюди, де можна використовувати `io.Writer`. `Request` — це структура, що містить синтаксично розібране представлення запиту від клієнта. 158 | 159 | Для стислості ігноруватимемо POST і вважатимемо, що HTTP-запити завжди є GET-запитами; це спрощення не впливає на те, як налаштовані обробники. Ось тривіальна реалізація обробника для підрахунку кількості відвідувань сторінки. 160 | ```go 161 | // Простий сервер підрахунку. 162 | type Counter struct { 163 | n int 164 | } 165 | 166 | func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { 167 | ctr.n++ 168 | fmt.Fprintf(w, "counter = %d\n", ctr.n) 169 | } 170 | ``` 171 | 172 | (Продовжуючи нашу тему, зверніть увагу на те, як `Fprintf` може друкувати в `http.ResponseWriter`). На реальному сервері доступ до `ctr.n` потребуватиме захисту від одночасного доступу. Поради щодо цього див. у документації до пакетів `sync` і `atomic`. 173 | 174 | Для довідки, ось як приєднати такий сервер до вузла у дереві URL-адрес. 175 | ```go 176 | import "net/http" 177 | // ... 178 | ctr := new(Counter) 179 | http.Handle("/counter", ctr) 180 | ``` 181 | 182 | Але навіщо робити `Counter` структурою? Ціле число — це все, що потрібно. (Одержувач повинен бути вказівником, щоб приріст був видимим для того, хто викликає). 183 | ```go 184 | // Ще простіший сервер підрахунку. 185 | type Counter int 186 | 187 | func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { 188 | *ctr++ 189 | fmt.Fprintf(w, "counter = %d\n", *ctr) 190 | } 191 | ``` 192 | 193 | Що робити, якщо у вашій програмі є внутрішній стан, який має бути сповіщений про те, що сторінку було відвідано? Прив'яжіть канал до вебсторінки. 194 | ```go 195 | // Канал, який надсилає сповіщення при кожному відвідуванні. 196 | // (Ймовірно, канал має бути буферизованим). 197 | type Chan chan *http.Request 198 | 199 | func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) { 200 | ch <- req 201 | fmt.Fprint(w, "notification sent") 202 | } 203 | ``` 204 | 205 | Нарешті, припустимо, що ми хочемо вивести в `/args` аргументи, які використовуються для запуску бінарного файлу сервера. Написати функцію для виведення аргументів дуже просто. 206 | ```go 207 | func ArgServer() { 208 | fmt.Println(os.Args) 209 | } 210 | ``` 211 | 212 | Як нам перетворити це на HTTP-сервер? Ми можемо зробити `ArgServer` методом якогось типу, значення якого ми ігноруємо, але є чистіший спосіб. Оскільки ми можемо визначити метод для будь-якого типу, окрім вказівників та інтерфейсів, ми можемо написати метод для функції. Пакет `http` містить такий код: 213 | ```go 214 | // Тип HandlerFunc є адаптером, що дозволяє використовувати 215 | // звичайні функції як обробники HTTP-запитів. Якщо f — це функція 216 | // з відповідним підписом, HandlerFunc(f) є об'єктом Handler, 217 | // який викликає f. 218 | type HandlerFunc func(ResponseWriter, *Request) 219 | 220 | // ServeHTTP викликає f(w, req). 221 | func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) { 222 | f(w, req) 223 | } 224 | ``` 225 | 226 | `HandlerFunc` — це тип з методом `ServeHTTP`, тому значення цього типу можуть обслуговувати HTTP-запити. Подивіться на реалізацію методу: приймачем є функція `f`, а метод викликає `f`. Це може здатися дивним, але це не набагато відрізняється від того, якби приймачем був канал, а метод — надсилав по каналу. 227 | 228 | Щоб перетворити `ArgServer` на HTTP-сервер, ми спочатку модифікуємо його так, щоб він мав правильну сигнатуру. 229 | ```go 230 | // Сервер аргументів. 231 | func ArgServer(w http.ResponseWriter, req *http.Request) { 232 | fmt.Fprintln(w, os.Args) 233 | } 234 | ``` 235 | 236 | `ArgServer` тепер має ту саму сигнатуру, що й `HandlerFunc`, тому його можна перетворити в цей тип, щоб отримати доступ до його методів, так само як ми перетворили `Sequence` на `IntSlice`, щоб отримати доступ до `IntSlice.Sort`. Код для його налаштування лаконічний: 237 | ```go 238 | http.Handle("/args", http.HandlerFunc(ArgServer)) 239 | ``` 240 | 241 | Коли хтось відвідує сторінку `/args`, обробник, встановлений на цій сторінці, має значення `ArgServer` і тип `HandlerFunc`. HTTP-сервер викличе метод `ServeHTTP` цього типу, з `ArgServer` як отримувачем, який своєю чергою викличе `ArgServer` (через виклик `f(w, req)` всередині `HandlerFunc.ServeHTTP`). Після цього будуть відображені аргументи. 242 | 243 | У цьому розділі ми створили HTTP-сервер зі структури, цілого числа, каналу та функції, а все тому, що інтерфейси — це просто набір методів, які можна визначити для (майже) будь-якого типу. 244 | -------------------------------------------------------------------------------- /13. Конкурентність.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Конкурентність](#Конкурентність) 3 | - [Розподіл за повідомленнями](#Розподіл-за-повідомленнями) 4 | - [Горутини](#Горутини) 5 | - [Канали](#Канали) 6 | - [Канали каналів](#Канали-каналів) 7 | - [Паралелізм](#Паралелізм) 8 | - [Поточний буфер](#Поточний-буфер) 9 | 10 | ## Конкурентність 11 | 12 | ### Розподіл за повідомленнями 13 | Паралельне програмування є великою темою й тут є місце лише для деяких специфічних для Go моментів. 14 | 15 | Паралельне програмування у багатьох середовищах ускладнюється тонкощами, необхідними для реалізації коректного доступу до спільних змінних. Go заохочує інший підхід, при якому спільні значення передаються по каналах і, фактично, ніколи активно не використовуються окремими потоками виконання. Тільки одна підпрограма має доступ до значення в будь-який момент часу. Перегони даних не можуть відбуватися за замовчуванням. Щоб заохотити такий спосіб мислення, ми перетворили його на слоган: 16 | 17 | > Не спілкуйтеся, ділячись пам'яттю; натомість, діліться пам'яттю, спілкуючись. 18 | 19 | Такий підхід може зайти надто далеко. Наприклад, підрахунок посилань найкраще здійснювати за допомогою м'ютексу навколо цілочисельної змінної. Але як високорівневий підхід, використання каналів для керування доступом полегшує написання зрозумілих, коректних програм. 20 | 21 | Один зі способів зрозуміти цю модель — розглянути типову однопотокову програму, що виконується на одному процесорі. Вона не потребує примітивів синхронізації. Тепер запустіть інший такий екземпляр; він також не потребує синхронізації. Тепер дозвольте цим двом взаємодіяти; якщо взаємодія відбувається за допомогою синхронізатора, іншої синхронізації все одно не потрібно. Конвеєри (pipelines) Unix, наприклад, ідеально підходять для цієї моделі. Хоча підхід Go до конкурентності бере свій початок у комунікаційних послідовних процесах (англ. Communicating Sequential Processes — CSP) Хоара, його також можна розглядати як безпечне для типів узагальнення конвеєрів Unix. 22 | 23 | ### Горутини 24 | Вони називаються _горутинами_, тому що інші терміни — потоки, підпрограми, процеси й так далі — мають неточні конотації. Горутина має просту модель: це функція, що виконується конкурентно з іншими горутинами в тому ж адресному просторі. Вона легка, коштує трохи більше, ніж виділення місця в стеку. Стеки ростуть коштом виділення (і звільнення) пам'яті купи за потреби, тому вони дешеві. 25 | 26 | Горутини мультиплексуються на декілька потоків ОС, тому якщо одна з них блокується, наприклад, під час очікування вводу/виводу, інші продовжують працювати. Їхній дизайн приховує багато складнощів у створенні та управлінні потоками. 27 | 28 | Додайте до виклику функції або методу ключове слово `go`, щоб запустити виклик у новій горутині. Коли виклик завершиться, вона безшумно завершить роботу. (Ефект подібний до нотації `&` в командній оболонці Unix для запуску команди у фоновому режимі). 29 | ```go 30 | go list.Sort() // запустити list.Sort конкурентно, не очікуючи на завершення виконання.. 31 | ``` 32 | 33 | Функціональний літерал може бути зручним для виклику підпрограми. 34 | ```go 35 | func Announce(message string, delay time.Duration) { 36 | go func() { 37 | time.Sleep(delay) 38 | fmt.Println(message) 39 | }() // Зверніть увагу на дужки - функцію буде викликано. 40 | } 41 | ``` 42 | 43 | У Go літерали функцій є закриттями: реалізація гарантує, що змінні, на які посилається функція, існують доти, доки вони активні. 44 | 45 | Ці приклади не надто практичні, оскільки функції не мають способу сигналізувати про завершення виконання. Для цього нам потрібні канали. 46 | 47 | ### Канали 48 | Як і мапи, канали виділяються за допомогою `make`, а отримане значення діє як посилання на базову структуру даних. Якщо надається необов'язковий цілочисельний параметр, він задає розмір буфера для каналу. За замовчуванням він дорівнює нулю для небуферизованого або синхронного каналу. 49 | ```go 50 | ci := make(chan int) // небуферизований канал цілих чисел 51 | cj := make(chan int, 0) // небуферизований канал цілих чисел 52 | cs := make(chan *os.File, 100) // буферизований канал покажчиків на файли 53 | ``` 54 | 55 | Небуферизовані канали поєднують комунікацію — обмін значеннями — з синхронізацією, яка гарантує, що два обчислення (горутини) перебувають у відомому стані. 56 | 57 | Існує багато гарних ідіом з використанням каналів. У попередньому розділі ми запустили сортування у фоновому режимі. Канал може дозволити програмі, яка запускає горутину, дочекатися завершення її виконання. 58 | ```go 59 | c := make(chan int) // Виділення каналу. 60 | // Розпочати сортування в горутині; коли воно завершиться, подати сигнал на канал. 61 | go func() { 62 | list.Sort() 63 | c <- 1 // Надіслати сигнал про завершення; значення не грає ролі. 64 | }() 65 | doSomethingForAWhile() 66 | <-c // Чекати на завершення сортування; відкинути надіслане значення. 67 | ``` 68 | 69 | Одержувачі завжди блокуються, поки не отримають дані. Якщо канал без буфера, відправник блокується, поки одержувач не отримає значення. Якщо канал має буфер, відправник блокується тільки до тих пір, поки значення не буде скопійовано в буфер; якщо буфер переповнений, це означає очікування, поки якийсь одержувач не отримає значення. 70 | 71 | Буферизований канал можна використовувати як семафор, наприклад, для обмеження пропускної здатності. У цьому прикладі вхідні запити передаються функції `handle`, яка надсилає значення в канал, обробляє запит, а потім отримує значення з каналу, щоб підготувати «семафор» для наступного споживача. Ємність буфера каналу обмежує кількість одночасних викликів функції `process`. 72 | ```go 73 | var sem = make(chan int, MaxOutstanding) 74 | 75 | func handle(r *Request) { 76 | sem <- 1 // Чекати, поки активна черга не звільниться. 77 | process(r) // Може зайняти багато часу. 78 | <-sem // Завершено; дозволити наступному запиту виконатись. 79 | } 80 | 81 | func Serve(queue chan *Request) { 82 | for { 83 | req := <-queue 84 | go handle(req) // Не чекати на завершення виконання handle. 85 | } 86 | } 87 | ``` 88 | 89 | Коли `MaxOutstanding` обробників виконують `process`, будь-який інший процес буде блокувати спроби надсилання у заповнений буфер каналу, доки один з наявних обробників не завершить роботу і не отримає дані з буфера. 90 | 91 | Однак, ця конструкція має проблему: `Serve` створює нову горутину для кожного вхідного запиту, хоча в будь-який момент може працювати лише `MaxOutstanding` з них. В результаті, програма може споживати необмежені ресурси, якщо запити надходять занадто швидко. Ми можемо усунути цей недолік, змінивши `Serve` на `gate` при створенні підпрограм. Це очевидне рішення, але будьте обережні, у ньому є баг, який ми згодом виправимо: 92 | ```go 93 | func Serve(queue chan *Request) { 94 | for req := range queue { 95 | sem <- 1 96 | go func() { 97 | process(req) // Містить помилку; див. пояснення нижче. 98 | <-sem 99 | }() 100 | } 101 | } 102 | ``` 103 | 104 | Помилка полягає в тому, що в циклі `for` змінна циклу повторно використовується для кожної ітерації, тому змінна `req` є спільною для всіх горутин. Це не те, чого ми хочемо. Нам потрібно переконатися, що `req` є унікальною для кожної горутини. Ось один зі способів зробити це — передати значення `req` як аргумент для закриття в горутині: 105 | ```go 106 | func Serve(queue chan *Request) { 107 | for req := range queue { 108 | sem <- 1 109 | go func(req *Request) { 110 | process(req) 111 | <-sem 112 | }(req) 113 | } 114 | } 115 | ``` 116 | 117 | Порівняйте цю версію з попередньою, щоб побачити різницю в тому, як оголошується і виконується закриття. Іншим рішенням є створення нової змінної з тим самим іменем, як у цьому прикладі: 118 | ```go 119 | func Serve(queue chan *Request) { 120 | for req := range queue { 121 | req := req // Створити новий екземпляр req для горутини. 122 | sem <- 1 123 | go func() { 124 | process(req) 125 | <-sem 126 | }() 127 | } 128 | } 129 | ``` 130 | Може здатися дивним писати 131 | ```go 132 | req := req 133 | ``` 134 | Проте, це цілком доречно та ідіоматично в Go. Ви отримаєте свіжу версію змінної з тим самим ім'ям, яка навмисно затінює змінну циклу локально, але є унікальною для кожної підпрограми. 135 | 136 | Повертаючись до загальної проблеми написання сервера, ще один підхід, який добре управляє ресурсами, полягає в тому, щоб запустити фіксовану кількість горутин для `handle`, які читають всі дані з каналу запитів. Кількість процедур обмежує кількість одночасних викликів `process`. Ця функція `Serve` також приймає канал, на якому їй буде вказано вийти; після запуску процедур вона блокує отримання з цього каналу. 137 | ```go 138 | func handle(queue chan *Request) { 139 | for r := range queue { 140 | process(r) 141 | } 142 | } 143 | 144 | func Serve(clientRequests chan *Request, quit chan bool) { 145 | // Запустити обробники 146 | for i := 0; i < MaxOutstanding; i++ { 147 | go handle(clientRequests) 148 | } 149 | <-quit // Чекати, поки не буде подано сигнал. 150 | } 151 | ``` 152 | 153 | ### Канали каналів 154 | Однією з найважливіших властивостей Go є те, що канали — це звичайні змінні, які, як і будь-які інші, можна виділяти та передавати. Ця властивість часто використовується для реалізації безпечного паралельного демультиплексування. 155 | 156 | У прикладі з попереднього розділу метод `handle` був ідеалізованим обробником запиту, але ми не визначили тип, який він обробляв. Якщо цей тип містить канал для відповіді, то кожен клієнт може надати свій власний шлях для неї. Ось схематичне визначення типу `Request`. 157 | ```go 158 | type Request struct { 159 | args []int 160 | f func([]int) int 161 | resultChan chan int 162 | } 163 | ``` 164 | 165 | Клієнт надає функцію та її аргументи, а також канал всередині об'єкта запиту, по якому потрібно отримати відповідь. 166 | 167 | ```go 168 | func sum(a []int) (s int) { 169 | for _, v := range a { 170 | s += v 171 | } 172 | return 173 | } 174 | 175 | request := &Request{[]int{3, 4, 5}, sum, make(chan int)} 176 | // Надіслати запит. 177 | clientRequests <- request 178 | // Очікувати на відповідь. 179 | fmt.Printf("answer: %d\n", <-request.resultChan) 180 | ``` 181 | На стороні сервера змінюється лише функція обробника. 182 | ```go 183 | func handle(queue chan *Request) { 184 | for req := range queue { 185 | req.resultChan <- req.f(req.args) 186 | } 187 | } 188 | ``` 189 | 190 | Очевидно, що ще багато чого можна зробити, аби це було реалістичнішим, але цей код є фреймворком для паралельної системи RPC з обмеженою швидкістю, що не блокує, і тут немає жодного м'ютексу. 191 | 192 | ### Паралелілзм 193 | Іншим застосуванням цих ідей є розпаралелювання обчислень на декількох ядрах процесора. Якщо обчислення можна розбити на окремі частини, які можуть виконуватися незалежно, його можна розпаралелити, створивши канал для сигналізації про завершення кожної частини. 194 | 195 | Припустимо, що нам потрібно виконати дорогу операцію над вектором елементів, і що вартість операції над кожним елементом є незалежною, як у цьому ідеалізованому прикладі. 196 | ```go 197 | type Vector []float64 198 | 199 | // Застосувати операцію до v[i], v[i+1] ... аж до v[n-1]. 200 | func (v Vector) DoSome(i, n int, u Vector, c chan int) { 201 | for ; i < n; i++ { 202 | v[i] += u.Op(v[i]) 203 | } 204 | c <- 1 // сигнал, що ця частина завершена 205 | } 206 | ``` 207 | 208 | Ми запускаємо фрагменти незалежно в циклі, по одному на кожне ядро процесора. Вони можуть завершуватися в будь-якому порядку, але це не має значення; ми просто рахуємо сигнали завершення, звільняючи канал після запуску всіх горутин. 209 | ```go 210 | const numCPU = 4 // Кількість ядер процесора. 211 | 212 | func (v Vector) DoAll(u Vector) { 213 | c := make(chan int, numCPU) // Буферизація необов'язкова, але є розумним кроком. 214 | for i := 0; i < numCPU; i++ { 215 | go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c) 216 | } 217 | // Очищення каналу. 218 | for i := 0; i < numCPU; i++ { 219 | <-c // Чекати на завершення одного завдання. 220 | } 221 | // Все зроблено. 222 | } 223 | ``` 224 | 225 | Замість того, щоб створювати константне значення для `numCPU`, ми можемо запитати у часу виконання, яке значення є відповідним. Функція [`runtime.NumCPU`](https://pkg.go.dev/runtime#NumCPU) повертає кількість апаратних ядер процесора у машині, тому ми можемо написати 226 | ```go 227 | var numCPU = runtime.NumCPU() 228 | ``` 229 | 230 | Існує також функція [`runtime.GOMAXPROCS`](https://go.dev/pkg/runtime#GOMAXPROCS), яка повідомляє (або встановлює) вказану користувачем кількість ядер, на яких може одночасно виконуватися програма. За замовчуванням вона має значення `runtime.NumCPU`, але його можна перевизначити, встановивши однойменну змінну або викликавши функцію з додатним числом. Виклик функції з нульовим значенням просто запитує значення. Тому, якщо ми хочемо задовольнити запит користувача на отримання ресурсу, ми повинні написати 231 | ```go 232 | var numCPU = runtime.GOMAXPROCS(0) 233 | ``` 234 | 235 | Не плутайте ідеї конкурентності — структурування програми як незалежно виконуваних компонентів — і паралелізму — виконання обчислень паралельно для ефективності на декількох ядрах процесора. Хоча можливості конкурентності в Go дозволяють легко структурувати деякі проблеми як паралельні обчислення, Go є конкурентною мовою, а не паралельною, і не всі проблеми розпаралелювання підходять під модель Go. Для обговорення цієї різниці дивіться доповідь, на яку є посилання у [цьому пості з блогу](https://go.dev/blog/waza-talk) (англ.). 236 | 237 | ### Поточний буфер 238 | Інструменти конкурентного програмування можуть навіть полегшити вираження неконкурентних концептів. Ось приклад, абстрагований від пакета RPC. Клієнтська горутина циклічно отримує дані з деякого джерела, можливо, з мережі. Щоб уникнути виділення та звільнення буферів, вона зберігає вільний список і використовує буферизований канал для його представлення. Якщо канал порожній, виділяється новий буфер. Як тільки буфер повідомлення буде готовий, його буде надіслано на сервер за допомогою `serverChan`. 239 | ```go 240 | var freeList = make(chan *Buffer, 100) 241 | var serverChan = make(chan *Buffer) 242 | 243 | func client() { 244 | for { 245 | var b *Buffer 246 | // Взяти буфер, якщо він доступний; виділити новий, якщо ні. 247 | select { 248 | case b = <-freeList: 249 | // Отримано; більше нічого не треба робити. 250 | default: 251 | // Вільних буферів немає, тому виділити новий. 252 | b = new(Buffer) 253 | } 254 | load(b) // Прочитати наступне повідомлення з мережі. 255 | serverChan <- b // Відправити серверу. 256 | } 257 | } 258 | ``` 259 | 260 | Серверний цикл отримує кожне повідомлення від клієнта, обробляє його і повертає буфер у вільний список. 261 | ```go 262 | func server() { 263 | for { 264 | b := <-serverChan // Чекати на роботу. 265 | process(b) 266 | // Повторно використати буфер, якщо є місце. 267 | select { 268 | case freeList <- b: 269 | // Буфер у списку вільних; більше нічого не треба робити. 270 | default: 271 | // Список вільних буферів заповнений, продовжуємо. 272 | } 273 | } 274 | } 275 | ``` 276 | Клієнт намагається отримати буфер з `freeList`; якщо його немає, він виділяє новий. Відправлення сервером запиту до `freeList` повертає `b` назад до вільного списку, якщо тільки список не заповнено, у цьому випадку буфер скидається, щоб його забрав збирач сміття. (Умова `default` в операторах `select` виконується, коли жоден інший випадок не задовільнений, що означає, що `select` ніколи не блокується). Ця реалізація будує список без дірявих відер всього за кілька рядків, покладаючись на буферизований канал і збирач сміття для ведення обліку. 277 | -------------------------------------------------------------------------------- /07. Дані.md: -------------------------------------------------------------------------------- 1 | ## Зміст 2 | - [Дані](#Дані) 3 | - [Створення за допомогою new](#Створення-за-допомогою-new) 4 | - [Конструктори та складені літерали](#Конструктори-та-складені-літерали) 5 | - [Створення за допомогою make](#Створення-за-допомогою-make) 6 | - [Масиви](#Масиви) 7 | - [Зрізи](#Зрізи) 8 | - [Двовимірні зрізи](#Двовимірні-зрізи) 9 | - [Мапи](#Мапи) 10 | - [Друк](#Друк) 11 | - [Приєднання](#Приєднання) 12 | 13 | ## Дані 14 | ### Створення за допомогою new 15 | У Go є два примітиви виділення, вбудовані функції `new` і `make`. Вони роблять різні речі й застосовуються до різних типів, що може заплутати, але правила їх використання досить прості. Спочатку поговоримо про `new`. Це вбудована функція, яка виділяє пам'ять, але на відміну від своїх тезок у деяких інших мовах вона _не ініціалізує_ пам'ять, а лише _зануляє_ її. Тобто `new(T)` виділяє обнулену пам'ять для нового елемента типу `T` і повертає його адресу — значення типу `*T`. У термінології Go, вона повертає вказівник на щойно виділене нульове значення типу `T`. 16 | 17 | Оскільки пам'ять, яку повертає `new`, зануляється, при проєктуванні структур даних корисно передбачити, що нульове значення кожного типу можна використовувати без додаткової ініціалізації. Це означає, що користувач структури даних може створити її за допомогою `new` і одразу взятися до роботи. Наприклад, у документації до `bytes.Buffer` зазначено, що «нульове значення для Buffer — це порожній буфер, готовий до використання». Аналогічно, `sync.Mutex` не має явного конструктора або методу `Init`. Натомість нульове значення для `sync.Mutex` визначається як розблокований м'ютекс. 18 | 19 | Властивість «нульове-значення-є-корисним» працює транзитивно. Розглянемо таке оголошення типу. 20 | ```go 21 | type SyncedBuffer struct { 22 | lock sync.Mutex 23 | buffer bytes.Buffer 24 | } 25 | ``` 26 | Значення типу `SyncedBuffer` також готові до використання одразу після виділення або просто оголошення. У наступному фрагменті і `p`, і `v` будуть коректно працювати без додаткових змін. 27 | ```go 28 | p := new(SyncedBuffer) // тип *SyncedBuffer 29 | var v SyncedBuffer // тип SyncedBuffer 30 | ``` 31 | 32 | ### Конструктори та складені літерали 33 | Іноді нульового значення недостатньо і потрібен ініціалізуючий конструктор, як у прикладі з пакета `os`, наведеному нижче. 34 | ```go 35 | func NewFile(fd int, name string) *File { 36 | if fd < 0 { 37 | return nil 38 | } 39 | f := new(File) 40 | f.fd = fd 41 | f.name = name 42 | f.dirinfo = nil 43 | f.nepipe = 0 44 | return f 45 | } 46 | ``` 47 | Існує багато шаблонів. Ми можемо спростити його за допомогою використання _складеного літерала_, тобто виразу, який створює новий екземпляр щоразу, коли він обчислюється. 48 | ```go 49 | func NewFile(fd int, name string) *File { 50 | if fd < 0 { 51 | return nil 52 | } 53 | f := File{fd, name, nil, 0} 54 | return &f 55 | } 56 | ``` 57 | Зверніть увагу, що, на відміну від мови C, повертати адресу локальної змінної цілком нормально; адреса, пов'язана зі змінною, зберігається після повернення функції. Насправді отримання адреси складеного літерала виділяє новий екземпляр кожного разу, коли він обчислюється, тому ми можемо об'єднати ці два останні рядки. 58 | ```go 59 | return &File{fd, name, nil, 0} 60 | ``` 61 | Поля складеного літерала розташовані у певному порядку і всі з них повинні бути присутніми. Однак, якщо позначити елементи явно як пари `поле: значення`, ініціалізатори можуть з'являтися у довільному порядку, а відсутні поля будуть проініціалізовані як відповідні нульові значення. Таким чином, можна сказати 62 | ```go 63 | return &File{fd: fd, name: name} 64 | ``` 65 | У граничному випадку, якщо складений літерал взагалі не містить полів, він створює нульове значення для типу. Таким чином, вирази `new(File)` і `&File{}` еквівалентні. 66 | 67 | Складені літерали також можна створювати для масивів, зрізів і мап, при цьому мітками полів можуть бути індекси або ключі мап відповідно. У цих прикладах ініціалізації працюють незалежно від значень `Enone`, `Eio` та `Einval`, якщо вони є різними. 68 | ```go 69 | a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} 70 | s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} 71 | m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"} 72 | ``` 73 | 74 | ### Створення за допомогою make 75 | Повернемося до виділення пам'яті. Вбудована функція `make(T, args)` має інше призначення, відмінне від `new(T)`. Вона створює лише зрізи, мапи та канали й повертає _ініціалізоване_ (не обнулене) значення типу `T` (а не `*T`). Причиною такої відмінності є те, що ці три типи являють собою посилання на структури даних, які необхідно ініціалізувати перед використанням. Наприклад, зріз — це дескриптор з трьох елементів, що містить вказівник на дані (всередині масиву), довжину та ємність, і поки ці елементи не ініціалізовано, зріз має _нульове_ (nil) значення. Для зрізів, мап і каналів `make` ініціалізує внутрішню структуру даних і готує значення до використання. Наприклад, 76 | ```go 77 | make([]int, 10, 100) 78 | ``` 79 | виділяє пам'ять для масиву розміром 100 значень типу `int`, а потім створює структуру зрізу довжиною 10 і ємністю 100 з посиланням, яке вказує лише на перші 10 елементів масиву. (При створенні зрізу ємність можна не вказувати; див. [розділ про зрізи](#Зрізи) для отримання додаткової інформації). На противагу цьому, `new([]int)` повертає вказівник на щойно виділену, обнулену структуру зрізу, тобто вказівник на `nil` значення зрізу. 80 | 81 | Подальші приклади ілюструють різницю між `new` та `make`: 82 | ```go 83 | var p *[]int = new([]int) // виділяє структуру зрізу; *p == nil; рідко коли корисно 84 | var v []int = make([]int, 100) // тепер зріз v відсилає до нового масиву зі 100 цілих чисел 85 | 86 | // Зайве ускладнення: 87 | var p *[]int = new([]int) 88 | *p = make([]int, 100, 100) 89 | 90 | // Ідіоматично: 91 | v := make([]int, 100) 92 | ``` 93 | 94 | Пам'ятайте, що `make` застосовується лише до мап, зрізів та каналів і не повертає вказівник. Щоб отримати явний вказівник, виділіть його за допомогою `new` або візьміть адресу змінної явно. 95 | 96 | ### Масиви 97 | Масиви корисні, коли точно відома необхідна кількість необхідної пам'яті, щоб не робити зайвих перестворень, але насамперед вони є складовою частиною для зрізів, які буде описано в [наступному розділі](#Зрізи). 98 | 99 | Існують суттєві відмінності між тим, як працюють масиви у Go та C. У Go, 100 | - Масиви — це значення. Присвоєння одного масиву іншому копіює всі елементи. 101 | - Зокрема, якщо ви передаєте масив у функцію, вона отримає копію масиву, а не вказівник на нього. 102 | - Розмір масиву є частиною його типу. Типи `[10]int` та `[20]int` є різними. 103 | 104 | Властивість `value` може бути корисною, але також і дорогою; якщо ви хочете C-подібну поведінку та ефективність, можете передати вказівник на масив. 105 | ```go 106 | func Sum(a *[3]float64) (sum float64) { 107 | for _, v := range *a { 108 | sum += v 109 | } 110 | return 111 | } 112 | 113 | array := [...]float64{7.0, 8.5, 9.1} 114 | x := Sum(&array) // Зверніть увагу на явний оператор адресації 115 | ``` 116 | Але такий стиль не є ідіоматичним для Go. Натомість використовуйте зрізи замість масивів. 117 | 118 | ### Зрізи 119 | Зрізи обгортають масиви, надаючи більш загальний, потужний і зручний інтерфейс для послідовностей даних та керування ними. За винятком елементів з явною розмірністю, таких як матриці перетворень, більшість програм для роботи з масивами у Go використовують зрізи, а не з прості масиви. 120 | 121 | Зрізи містять посилання на базовий масив, і якщо ви призначаєте один з них іншому, обидва посилаються на той самий масив. Якщо функція отримує аргумент зрізу, зміни, які вона вносить до елементів зрізу, будуть видимими для користувача, аналогічно до передачі вказівника на базовий масив. Таким чином, функція `Read` може приймати аргумент зрізу, а не вказівник і лічильник; довжина в межах зрізу встановлює верхню межу кількості даних, які можна прочитати. Ось сигнатура методу `Read` типу `File` у пакеті `os`: 122 | ```go 123 | func (f *File) Read(buf []byte) (n int, err error) 124 | ``` 125 | Метод повертає кількість прочитаних байтів і значення помилки, якщо вона виникла. Щоб прочитати перші 32 байти більшого буфера `buf`, розріжте буфер. 126 | ```go 127 | n, err := f.Read(buf[0:32]) 128 | ``` 129 | Таке розбиття на фрагменти є поширеним і ефективним. Насправді якщо залишити ефективність осторонь, наступний фрагмент також прочитає перші 32 байти буфера. 130 | ```go 131 | var n int 132 | var err error 133 | for i := 0; i < 32; i++ { 134 | nbytes, e := f.Read(buf[i:i+1]) // Прочитає один байт. 135 | n += nbytes 136 | if nbytes == 0 || e != nil { 137 | err = e 138 | break 139 | } 140 | } 141 | ``` 142 | Довжину зрізу можна змінювати доти, доки він не виходить за межі базового масиву; просто призначте його зрізу самого себе. Максимальну довжину, яку може мати зріз, можна дізнатися скориставшись вбудованою функцією `cap`. Ось функція для додавання даних до зрізу. Якщо дані перевищують ємність, зріз перерозподіляється. Отриманий фрагмент повертається. Функція використовує той факт, що `len` і `cap` можна застосувати до `nil` зрізу, отримавши 0. 143 | ```go 144 | func Append(slice, data []byte) []byte { 145 | l := len(slice) 146 | if l + len(data) > cap(slice) { // реалокація 147 | // Виділяємо подвійну кількість потрібного, для майбутнього зростання. 148 | newSlice := make([]byte, (l+len(data))*2) 149 | // Функція copy є попередньо оголошеною і працює для будь-якого типу зрізу. 150 | copy(newSlice, slice) 151 | slice = newSlice 152 | } 153 | slice = slice[0:l+len(data)] 154 | copy(slice[l:], data) 155 | return slice 156 | } 157 | ``` 158 | Після цього ми повинні повернути зріз, тому що, попри те, що `Append` може змінювати елементи зрізу, сам зріз (структура даних часу виконання, що містить вказівник, довжину та ємність) передається _за значенням_. 159 | 160 | Ідея додавання до зрізу є настільки корисною, що її реалізовано у вбудованій функції `append`. Однак, щоб зрозуміти принцип роботи цієї функції, нам потрібно трохи більше інформації, тому ми повернемося до неї пізніше. 161 | 162 | ### Двовимірні зрізи 163 | Масиви та зрізи в Go є одновимірними. Щоб створити еквівалент двовимірного масиву або зрізу, необхідно визначити масив з масивів або зріз зі зрізів, як показано нижче: 164 | ```go 165 | type Transform [3][3]float64 // Масив розміру 3x3, по суті — масив масивів. 166 | type LinesOfText [][]byte // Зріз зрізу байтів. 167 | ``` 168 | Оскільки зрізи мають змінну довжину, можна зробити так, щоб кожен внутрішній зріз мав різну довжину. Це може бути поширеною ситуацією, як у нашому прикладі `LinesOfText`: кожен рядок має незалежну довжину. 169 | ```go 170 | text := LinesOfText{ 171 | []byte("Ви знаєте, як липа шелестить..."), 172 | []byte("у місячні весняні ночі?"), 173 | []byte("А солов'ї!..."), 174 | } 175 | ``` 176 | Іноді необхідно виділити 2D-зріз — ситуація, яка може виникнути, наприклад, при обробці ліній розгортки пікселів. Існує два способи зробити це. Перший — виділити кожен зріз незалежно; другий — виділити єдиний масив і вказати на нього окремі зрізи. Який з них використовувати, залежить від вашої програми. Якщо зрізи можуть збільшуватися або зменшуватися, їх слід виділяти незалежно, щоб уникнути перезапису наступного рядка; якщо ні, то може бути ефективніше побудувати об'єкт за допомогою одного виділення. Для довідки, ось ескізи двох методів. Спершу, по одному рядку за раз: 177 | ```go 178 | // Виділити зріз верхнього рівня. 179 | picture := make([][]uint8, YSize) // Один рядок на кожну одиницю y. 180 | // Перебрати рядки, виділяючи зріз для кожного рядка. 181 | for i := range picture { 182 | picture[i] = make([]uint8, XSize) 183 | } 184 | ``` 185 | А тепер як одне виділення, розрізане на рядки: 186 | ```go 187 | // Виділити зріз верхнього рівня, так само як раніше. 188 | picture := make([][]uint8, YSize) // Один рядок на кожну одиницю y. 189 | // Виділити один великий зріз для зберігання всіх пікселів. 190 | pixels := make([]uint8, XSize*YSize) // Має тип []uint8, хоча picture має тип [][]uint8. 191 | // Перебрати рядки, виділяючи кожний рядок із передньої частини залишку зрізу пікселів. 192 | for i := range picture { 193 | picture[i], pixels = pixels[:XSize], pixels[XSize:] 194 | } 195 | ``` 196 | 197 | ### Мапи 198 | Мапи — це зручна і потужна вбудована структура даних, яка пов'язує значення одного типу (_ключ_) зі значеннями іншого типу (_елемент_ або _значення_). Ключ може бути будь-якого типу, для якого визначено оператор рівності, наприклад, цілі числа, числа з рухомою комою, та комплексні числа, рядки, вказівники, інтерфейси (якщо динамічний тип підтримує рівність), структури та масиви. Зрізи не можна використовувати як ключі мапи, оскільки для них не визначена рівність. Як і зрізи, мапи містять посилання на базову структуру даних. Якщо ви передаєте мапу у функцію, яка змінює її вміст, ці зміни будуть діяти лише в для того, хто її викликає. 199 | 200 | Мапи можна створювати за допомогою звичайного синтаксису складених літералів з парами ключ-значення, розділеними двокрапкою, тому їх легко створювати під час ініціалізації. 201 | ```go 202 | var timeZone = map[string]int{ 203 | "UTC": 0*60*60, 204 | "EST": -5*60*60, 205 | "CST": -6*60*60, 206 | "MST": -7*60*60, 207 | "PST": -8*60*60, 208 | } 209 | ``` 210 | 211 | Присвоєння та отримання значень мап синтаксично виглядає так само як і для масивів та зрізів, за винятком того, що індекс не обов'язково має бути цілим числом. 212 | ```go 213 | offset := timeZone["EST"] 214 | ``` 215 | 216 | Спроба отримати значення мапи за ключем, якого у ній немає, поверне нульове значення для типу записів мапи. Наприклад, якщо мапа містить цілі числа, пошук за ключем, якого не існує, поверне 0. Множину (`set`) можна реалізувати у вигляді мапи зі значенням типу `bool`. Встановіть для елемента мапи значення `true`, щоб помістити значення у множину, а потім перевірте його простим індексуванням. 217 | ```go 218 | attended := map[string]bool{ 219 | "Ann": true, 220 | "Joe": true, 221 | // ... 222 | } 223 | 224 | if attended[person] { // буде false, якщо особа відсутня у мапі 225 | fmt.Println(person, "was at the meeting") 226 | } 227 | ``` 228 | 229 | Іноді вам потрібно відрізнити відсутній запис від нульового значення. Чи є запис для `"UTC"`, чи це 0, тому що його взагалі немає у мапі? Ви можете розрізняти це за допомогою форми множинного присвоєння. 230 | ```go 231 | var seconds int 232 | var ok bool 233 | seconds, ok = timeZone[tz] 234 | ``` 235 | Зі зрозумілих причин це називається ідіомою «кома ok». У цьому прикладі, якщо `tz` присутня, секунди будуть встановлені належним чином і значення `ok` буде істинним; якщо ж ні, секунди будуть встановлені на нуль і значення `ok` буде хибним. Ось функція, яка поєднує це з гарним звітом про помилки: 236 | ```go 237 | func offset(tz string) int { 238 | if seconds, ok := timeZone[tz]; ok { 239 | return seconds 240 | } 241 | log.Println("unknown time zone:", tz) 242 | return 0 243 | } 244 | ``` 245 | 246 | Щоб перевірити наявність у мапі, не турбуючись про фактичне значення, ви можете використовувати порожній ідентифікатор (_) замість звичайної змінної для значення. 247 | ```go 248 | _, present := timeZone[tz] 249 | ``` 250 | 251 | Щоб видалити запис мапи, скористайтеся вбудованою функцією `delete`, аргументами якої є мапа і ключ, який потрібно видалити. Це безпечно робити, навіть якщо ключ вже відсутній на мапі. 252 | ```go 253 | delete(timeZone, "PDT") // Тепер за стандартним часом 254 | ``` 255 | 256 | ### Друк 257 | Форматований друк у Go використовує стиль, подібний до сімейства `printf` у C, але багатший і загальніший. Відповідні функції знаходяться у пакеті `fmt` і мають назви з великої літери: `fmt.Printf`, `fmt.Fprintf`, `fmt.Sprintf` і так далі. Рядкові функції (`Sprintf` тощо) повертають рядок, а не заповнюють наданий буфер. 258 | 259 | Вам не потрібно вказувати рядок форматування. Для кожної з функцій `Printf`, `Fprintf` і `Sprintf` існує інша пара функцій, наприклад `Print` і `Println`. Ці функції не приймають рядок форматування, а генерують формат за замовчуванням для кожного аргументу. Версії `Println` також вставляють пропуск між аргументами й додають новий рядок до друку, тоді як версії `Print` додають пропуски лише у тому випадку, якщо операнд з обох боків не є рядком. У цьому прикладі кожен рядок виводить однакові дані. 260 | ```go 261 | fmt.Printf("Hello %d\n", 23) 262 | fmt.Fprint(os.Stdout, "Hello ", 23, "\n") 263 | fmt.Println("Hello", 23) 264 | fmt.Println(fmt.Sprint("Hello ", 23)) 265 | ``` 266 | 267 | Функції форматованого друку `fmt.Fprint` та інші приймають як перший аргумент будь-який об'єкт, що реалізує інтерфейс `io.Writer`; змінні `os.Stdout` та `os.Stderr` є знайомими екземплярами. 268 | 269 | Тут все починає відрізнятися від C. По-перше, числові формати, такі як `%d`, не приймають прапорів знаковості або розміру; натомість, підпрограми друку використовують тип аргументу для визначення цих властивостей. 270 | ```go 271 | var x uint64 = 1<<64 - 1 272 | fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x)) 273 | ``` 274 | виведе 275 | ``` 276 | 18446744073709551615 ffffffffffffffff; -1 -1 277 | ``` 278 | 279 | Якщо вам потрібне перетворення за замовчуванням, наприклад, десяткове для цілих чисел, ви можете скористатися загальним форматом `%v` (для «значення»); результат буде таким самим, як і за допомогою команд `Print` і `Println`. Крім того, цей формат може виводити будь-які значення, навіть масиви, зрізи, структури та мапи. Нижче наведено інструкцію друку для мапи часових поясів, визначеної у попередньому розділі. 280 | ```go 281 | fmt.Printf("%v\n", timeZone) // або ж просто fmt.Println(timeZone) 282 | ``` 283 | що виведе: 284 | ``` 285 | map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0] 286 | ``` 287 | 288 | Для мап `Printf` і друзі сортують висновок лексикографічно за ключем. 289 | 290 | Під час друку структури модифікований формат `%+v` анотує поля структури їхніми назвами, а для будь-якого значення альтернативний формат `%#v` друкує значення у повному синтаксисі Go. 291 | ```go 292 | type T struct { 293 | a int 294 | b float64 295 | c string 296 | } 297 | t := &T{ 7, -2.35, "abc\tdef" } 298 | fmt.Printf("%v\n", t) 299 | fmt.Printf("%+v\n", t) 300 | fmt.Printf("%#v\n", t) 301 | fmt.Printf("%#v\n", timeZone) 302 | ``` 303 | Виводить: 304 | ``` 305 | &{7 -2.35 abc def} 306 | &{a:7 b:-2.35 c:abc def} 307 | &main.T{a:7, b:-2.35, c:"abc\tdef"} 308 | map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0} 309 | ``` 310 | 311 | Зверніть увагу на амперсанди. Цей формат рядка у лапках також доступний за допомогою `%q`, якщо його застосовано до значення типу рядок або `[]byte`. Альтернативний формат `%#q` за можливості використовує зворотні лапки. (Формат `%q` також застосовується до цілих чисел і рун, створюючи рунічну константу в одинарних лапках). Крім того, `%x` працює з рядками, масивами байт і зрізами байт так само як і з цілими числами, створюючи довгий шістнадцятковий рядок, а з пробілом у форматі (`% x`) він ставить пробіли між байтами. 312 | 313 | Ще одним зручним форматом є `%T`, який виводить тип значення. 314 | ```go 315 | fmt.Printf("%T\n", timeZone) 316 | ``` 317 | Виводить: 318 | ``` 319 | map[string]int 320 | ``` 321 | 322 | Якщо ви хочете керувати форматом за замовчуванням для користувацького типу, все, що вам потрібно, це визначити метод з сигнатурою `String()` рядка для цього типу. Для нашого простого типу `T` це може виглядати так. 323 | ```go 324 | func (t *T) String() string { 325 | return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c) 326 | } 327 | fmt.Printf("%v\n", t) 328 | ``` 329 | Для виведення у форматі: 330 | ``` 331 | 7/-2.35/"abc\tdef" 332 | ``` 333 | 334 | (Якщо вам потрібно виводити значення типу `T`, а також вказівники на `T`, приймач для `String` має бути типу значення; у цьому прикладі використано вказівник, оскільки це ефективніше та ідіоматичніше для структурних типів. Для отримання додаткової інформації зверніться до [розділу про вказівники та приймачі значень](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/09.%20Методи.md#Методи)). 335 | 336 | Наш метод `String` може викликати `Sprintf`, тому що процедури друку повністю реентерабельні й можуть бути обернуті таким чином. Однак у цьому підході є одна важлива деталь: не створюйте метод `String` за допомогою виклику `Sprintf` таким чином, щоб він повторювався у вашому методі `String` нескінченно. Це може статися, якщо виклик `Sprintf` спробує надрукувати приймач безпосередньо як рядок, що, у свою чергу, призведе до повторного виклику методу. Як показує цей приклад, це поширена і легка помилка. 337 | ```go 338 | type MyString string 339 | 340 | func (m MyString) String() string { 341 | return fmt.Sprintf("MyString=%s", m) // Помилка: нескінченне повторення. 342 | } 343 | ``` 344 | Це також легко виправити: перетворіть аргумент до базового рядкового типу, який не має методу. 345 | ```go 346 | type MyString string 347 | func (m MyString) String() string { 348 | return fmt.Sprintf("MyString=%s", string(m)) // ОК: зверніть увагу на зведення типу. 349 | } 350 | ``` 351 | 352 | У [розділі про ініціалізацію](https://github.com/vladyslavpavlenko/effective-go-ua/blob/main/08.%20Ініціалізація.md#Ініціалізація) ми розглянемо інший метод, який дозволяє уникнути цієї рекурсії. 353 | 354 | Інша техніка друку полягає у передачі аргументів процедури друку безпосередньо іншій такій процедурі. Сигнатура `Printf` використовує тип `...interface{}` для останнього аргументу, щоб вказати, що після формату може з'явитися довільна кількість параметрів (довільного типу). 355 | ```go 356 | func Printf(format string, v ...interface{}) (n int, err error) { 357 | ``` 358 | 359 | Усередині функції `Printf` `v` діє як змінна типу `[]interface{}`, але якщо її передати в іншу варіадну функцію, то вона діє як звичайний список аргументів. Ось реалізація функції `log.Println`, яку ми використовували вище. Вона передає свої аргументи безпосередньо до `fmt.Sprintln` для фактичного форматування. 360 | ```go 361 | // Println друкує в стандартний лог у стилі fmt.Println. 362 | func Println(v ...interface{}) { 363 | std.Output(2, fmt.Sprintln(v...)) // Output приймає параметри (int, string) 364 | } 365 | ``` 366 | 367 | Ми пишемо `...` після `v` у вкладеному виклику `Sprintln`, щоб вказати компілятору розглядати `v` як список аргументів; інакше він просто передасть `v` як єдиний аргумент типу зріз. 368 | 369 | Існує набагато більше можливостей для друку, ніж ми розглянули тут. Докладнішу інформацію наведено у документації до пакета `fmt` у розділі `godoc`. 370 | 371 | До речі, параметр `...` може бути певного типу, наприклад, `...int` для функції `min`, яка вибирає найменше зі списку цілих чисел: 372 | ```go 373 | func Min(a ...int) int { 374 | min := int(^uint(0) >> 1) // найбільший int 375 | for _, i := range a { 376 | if i < min { 377 | min = i 378 | } 379 | } 380 | return min 381 | } 382 | ``` 383 | 384 | ### Приєднання 385 | Тепер у нас є відсутній шматочок, який нам був потрібен для пояснення дизайну вбудованої функції `append`. Сигнатура `append` відрізняється від нашої користувацької функції `Append` вище. Схематично це виглядає так: 386 | ```go 387 | func append(slice []T, elements ...T) []T 388 | ``` 389 | 390 | де `T` — це заглушка для будь-якого заданого типу. Насправді ви не можете написати функцію на Go, де тип `T` визначається тим, хто її викликає. Саме тому `append` є вбудованою функцією: вона потребує підтримки компілятора. 391 | 392 | Що робить `append`, так це додає елементи в кінець фрагмента і повертає результат. Результат потрібно повертати, тому що, як і у випадку з нашим власним `Append`, базовий масив може змінитися. Цей простий приклад 393 | ```go 394 | x := []int{1,2,3} 395 | x = append(x, 4, 5, 6) 396 | fmt.Println(x) 397 | ``` 398 | виводить `[1 2 3 4 5 6]`. Отже, `append` працює трохи подібно до `Printf`, збираючи довільну кількість аргументів. 399 | 400 | Але що, якщо ми хочемо зробити те, що робить наш `Append`, і додати зріз до зрізу? Легко: використовуйте `...` у місці виклику, так само як ми це зробили у виклику `Output` вище. Цей код створить такий самий вивід, як і наведений вище. 401 | ```go 402 | x := []int{1,2,3} 403 | y := []int{4,5,6} 404 | x = append(x, y...) 405 | fmt.Println(x) 406 | ``` 407 | Без `...` код не скомпілюється, оскільки типи будуть неправильними: `y` не є `int`. --------------------------------------------------------------------------------