50 відтінків Go: пастки, підводні камені і поширені помилки новачків



Go — простий і забавний мову. Але в ньому, як і в будь-яких інших мовах, є свої підводні камені. І в багатьох з них сам Go не винен. Одні — це природний наслідок приходу програмістів з інших мов, інші виникають із-за помилкових уявлень і брак подробиць. Якщо ви знайдете час і почитаєте офіційні специфікації, вікі, поштові розсилки, публікації в блогах і вихідний код, то багато підводних каменів стануть для вас очевидні. Але далеко не кожен так починає, і це нормально. Якщо ви новачок в Go, стаття допоможе заощадити чимало годин, які ви б витратили на налагодження коду. Ми будемо розглядати версією Go 1.5 і нижче.

Зміст
Рівень: початківець

1. Відкриває фігурну дужку не можна розміщувати в окремому рядку
2. Невикористовувані змінні
3. Невикористовувані імпорти
4. Короткі оголошення змінних можна використовувати тільки всередині функцій
5. Переоб'явлення змінних з допомогою коротких оголошень
6. не Можна використовувати короткі оголошення змінних для визначення значень полів
7. Випадкове приховування змінних
8. не Можна використовувати nil для ініціалізації змінної без явної вказівки типу
9. Використання nil-слайсів (slice) і хеш-таблиць (map)
10. Ємність хеш-таблиць
11. Рядка не можуть бути nil
12. Передача масивів у функції
13. Несподівані значення у виразах range в слайсах і масивах
14. Одномірність слайсів і масивів
15. Звернення до неіснуючих ключам в map
16. Незмінюваність рядків
17. Перетворення рядків у байт-слайсы (Byte Slices), і навпаки
18. Рядка і оператор індексу
19. Рядка — не завжди текст у кодуванні UTF-8
20. Довжина рядків
21. Відсутня кома в багаторядкових литералах slice/array/map
22. log.Fatal та log.Panic не тільки журналируют
23. Несинхронізовані операції вбудованих структур даних
24. Ітераційні значення для рядків у виразах range
25. Итерирование хеш-таблиць (map) з допомогою виразу for range
26. Помилка поведінка у виразах switch
27. Инкременты і декременты
28. Побітове NOT-оператор
29. Відмінності пріоритетів операторів
30. Неэкспортированные поля структур не кодуються
31. Вихід з додатків за допомогою активних горутин
32. При відправці в небуферизованный канал дані повертаються у міру готовності отримувача
33. Відправка в закритий канал призводить до panic
34. Використання «nil»-каналів
35. Методи, що приймають параметри за значенням, не змінюють вихідних значень

Рівень: більш досвідчений новачок

36. Закриття тіла HTTP-відповіді
37. Закриття HTTP-з'єднань
38. Десериализация (unmarshalling) JSON-чисел в інтерфейсні значення
39. Порівняння struct, array, slice і map
40. Відновлення після panic
41. Оновлення і прив'язка значень полів в slice, array і map у виразах for range
42. «Приховані дані» слайсах
43. «Пошкодження» даних в слайсах
44. «Застарілі» слайсы
45. Методи та оголошення типів
46. Як вибратися з кодових блоків for switch і for select
47. Ітераційні змінні і замикання у виразах for
48. Обчислення аргументу блоку defer (Deferred Function Call Argument Evaluation)
49. Виклик блоку defer
50. Помилки при приведенні типів
51. Блоковані горутины і витоку ресурсів

Рівень: просунутий новачок

52. Застосування методів, що приймають значення з посиланням (pointer receiver), до екземплярів значень
53. Оновлення полів значень хеш-таблиці
54. nil-інтерфейси та nil-інтерфейсні значення
55. Змінні стека і купи
56. GOMAXPROCS, узгодженість (concurrency) і паралелізм
57. Зміна порядку операцій читання і запису
58. Диспетчеризація за пріоритетами (Preemptive Scheduling)

1. Відкриває фігурну дужку не можна розміщувати в окремому рядку
У більшості інших мов, які використовують фігурні дужки, вам потрібно вибирати, де їх розміщувати. Go вибивається з правила. За це ви можете дякувати автоматичну вставку крапки з комою (крапка з комою передбачається в кінці кожного рядка, без аналізу наступної). Так, в Go є крапка з комою!

Неправильно:

package main

import "fmt"

func main() 
{ // помилка, не можна виносити відкриває фігурну дужку в окремий рядок
fmt.Println("hello there!")
}

Помилка компілювання:

/tmp/sandbox826898458/main.go:6: syntax error: unexpected semicolon or newline before {

Правильно:

package main

import "fmt"

func main() { 
fmt.Println("works!")
}

2. Невикористані змінні
Якщо у вас є невикористані змінні, то код не відбудеться створення. Виняток: змінні, які оголошуються в межах функцій. Це правило не стосується глобальних змінних. Також можна мати невикористовувані аргументи функцій.

Якщо ви присвоїли вільної змінної нове значення, то ваш код все одно не буде компілюватися. Доведеться її використовувати, щоб догодити компілятору.

Неправильно:

package main

var gvar int // not an error

func main() { 
var one int // помилка, невживана змінна
two := 2 // помилка, невживана змінна
var three int // помилка, навіть незважаючи на присвоювання значення 3 у наступному рядку
three = 3

func(unused string) {
fmt.Println("Unused arg. No compile error")
}("what?")
}

Помилки компілювання:

/tmp/sandbox473116179/main.go:6: one declared and not used /tmp/sandbox473116179/main.go:7: two declared and not used /tmp/sandbox473116179/main.go:8: three declared and not used

Правильно:

package main

import "fmt"

func main() { 
var one int
_ = one

two := 2
fmt.Println(two)

var three int
three = 3
one = three

var four int
four = four
}

Інше рішення: коментувати або видаляти невикористовувані змінні.

3. Невикористані імпорти
Якщо ви імпортуєте пакет і потім не використовуєте які-небудь з його функцій, інтерфейсів, структур або змінних, то код не відбудеться створення. Якщо потрібно імпортувати пакет, ідентифікатор «_» в якості його імені допоможе уникнути помилок компілювання. Ідентифікатор «_» найчастіше застосовується для використання сайд-ефектів імпортованих бібліотек.

Неправильно:

package main

import ( 
"fmt"
"log"
"time"
)

func main() { 
}

Помилки компілювання:

/tmp/sandbox627475386/main.go:4: imported and not used: "fmt" /tmp/sandbox627475386/main.go:5: imported and not used: "log" /tmp/sandbox627475386/main.go:6: imported and not used: "time"

Правильно:

package main

import ( 
_ "fmt"
"log"
"time"
)

var _ = log.Println

func main() { 
_ = time.Now
}

Інше рішення: видалити або закоментувати невикористовувані імпорти. У цьому допоможе інструмент goimports.

4. Короткі оголошення змінних можна використовувати тільки всередині функцій
Неправильно:

package main

myvar := 1 // помилка

func main() { 
}

Помилка компілювання:

/tmp/sandbox265716165/main.go:3: non-declaration statement outside function body

Правильно:

package main

var myvar = 1

func main() { 
}

5. Переоб'явлення змінних з допомогою коротких оголошень
В одній області видимості вираження не можна переобъявлять змінні, але це можна робити в оголошенні декількох змінних (multi-variable declarations), серед яких хоча б одна нова. Переобъявляемые змінні повинні розташовуватися в тому ж блоці, інакше вийде прихована змінна (shadowed variable).
Неправильно:

package main

func main() { 
one := 0
one := 1 // помилка
}

Помилка компілювання:

/tmp/sandbox706333626/main.go:5: no new variables on left side of :=

Правильно:

package main

func main() { 
one := 0
one, two := 1,2

one,two = two,one
}

6. Не можна використовувати короткі оголошення змінних для визначення значень полів
Неправильно:

package main

import ( 
"fmt"
)

type info struct { 
int result
}

func work() (int,error) { 
return 13,nil 
}

func main() { 
var data info

data.result, err := work() // помилка
fmt.Printf("info: %+v\n",data)
}

Помилка компілювання:

prog.go:18: non-data name.result on left side of :=

Хоча розробників Go вже пропонували це виправити, не варто сподіватися на зміни: Робу Пайку подобається все «як є». Вам допоможуть тимчасові змінні. Або попередньо оголошуйте всі свої змінні і використовуйте стандартний оператор присвоювання.

Правильно:

package main

import ( 
"fmt"
)

type info struct { 
int result
}

func work() (int,error) { 
return 13,nil 
}

func main() { 
var data info

var err error
data.result, err = work() // ok
if err != nil {
fmt.Println(err)
return
}

fmt.Printf("info: %+v\n",data) // виводить: info: {result:13}
}

7. Випадкове приховування змінних
Синтаксис короткого оголошення змінних так зручний (особливо для тих, хто прийшов в Go динамічних мов), що його легко прийняти за регулярну операцію присвоювання. Якщо ви зробите цю помилку в новому блоці коду, компілятор не видасть помилку, але додаток буде працювати некоректно.

package main

import "fmt"

func main() { 
x := 1
fmt.Println(x) // виводить 1
{
fmt.Println(x) // виводить 1
x := 2
fmt.Println(x) // виводить 2
}
fmt.Println(x) // виводить 1 (погано, якщо потрібно було 2)
}

Це дуже поширена помилка навіть серед досвідчених Go-розробників. Її легко зробити і важко помітити. Для виявлення подібних ситуацій можна використовувати команду vet. За замовчуванням вона не виконує перевірку змінних на прихованість. Тому використовуйте прапор
-shadow: go tool vet -shadow your_file.go


8. Не можна використовувати nil для ініціалізації змінної без явної вказівки типу
Ідентифікатор
nil
можна використовувати як «нульове значення» (zero value) для інтерфейсів, функцій, покажчиків, хеш-таблиць (map), слайсів (slices) і каналів. Якщо не задати тип змінної, то компілятор не зможе завершити роботу, тому що не зможе вгадати тип.

Неправильно:

package main

func main() { 
var x = nil // помилка

_ = x
}

Помилка компілювання:

/tmp/sandbox188239583/main.go:4: use of untyped nil

Правильно:

package main

func main() { 
var x interface{} = nil

_ = x
}

9. Використання nil-слайсів (slice) і хеш-таблиць (map)
Можна додавати елементи в
nil
-слайс, але якщо те ж саме зробити з хеш-таблицею, то це призведе до runtime panic.

Правильно:

package main

func main() { 
var s []int
s = append(s,1)
}

Неправильно:

package main

func main() { 
var m map[string]int
m["one"] = 1 // помилка

}

10. Ємність хеш-таблиць
Можна встановлювати ємність при створенні хеш-таблиць, але не можна застосовувати до них функцію
cap()
.

Неправильно:

package main

func main() { 
m := make(map[string]int,99)
cap(m) // помилка
}

Помилка компілювання:

/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap

11. Рядки не можуть бути nil
Це підводний камінь для початківців, які присвоюють рядковим змінним
nil
-ідентифікатори.

Неправильно:

package main

func main() { 
var x string = nil // помилка

if x == nil { // помилка
x = "default"
}
}

Помилки компілювання:

/tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment /tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types and string nil)

Правильно:

package main

func main() { 
var x string // повертає значення за замовчуванням "" (нульове значення)

if x == "" {
x = "default"
}
}

12. Передача масивів у функцію
Якщо ви розробляєте на С/С++, то масиви для вас — покажчики. Коли ви передаєте масиви функцій, функції посилаються на ту ж область пам'яті і тому можуть оновлювати вихідні дані. В Go масиви є значеннями, так що, коли ми передаємо їх функцій, ті отримують копію вихідного масиву. Це може стати проблемою, якщо ви намагаєтеся оновлювати дані в масиві.

package main

import "fmt"

func main() { 
x := [3]int{1,2,3}

func(arr [3]int) {
arr[0] = 7
fmt.Println(arr) // виводить [7 2 3]
}(x)

fmt.Println(x) // виводить [1 2 3] (погано, якщо вам потрібно було [7 2 3])
}

Якщо потрібно оновити дані в масиві, використовуйте типи покажчиків масиву.

package main

import "fmt"

func main() { 
x := [3]int{1,2,3}

func(arr *[3]int) {
(*arr)[0] = 7
fmt.Println(arr) // виводить &[7 2 3]
}(&x)

fmt.Println(x) // виводить [7 2 3]
}

Інше рішення: слайсы. Хоча ваша функція отримує копію змінної слайсу, та все ще є посиланням на вихідні дані.

package main

import "fmt"

func main() { 
x : = [] (int) {1,2,3}

func(arr []int) {
arr[0] = 7
fmt.Println(arr) // виводить [7 2 3]
}(x)

fmt.Println(x) // виводить [7 2 3]
}

13. Несподівані значення у виразах range в слайсах і масивах
Це може статися, якщо ви звикли до виразів
for-in
або
foreach
в інших мовах. Але в Go вираз
range
відрізняється тим, що воно генерує два значення: перше — це індекс елементу (item index), а друге — дані елементу (item data).

Неправильно:

package main

import "fmt"

func main() { 
x := []string{"a","b","с"}

for v := range x {
fmt.Println(v) // виводить 0, 1, 2
}
}

Правильно:

package main

import "fmt"

func main() { 
x := []string{"a","b","с"}

for _, v := range x {
fmt.Println(v) // виводить a, b, c
}
}

14. Одномірність слайсів і масивів
Здається, що Go підтримує багатовимірні масиви і слайсы? Ні, це не так. Хоча можна створювати масиви з масивів і слайсы з слайсів. З точки зору продуктивності і складності — далеко не ідеальне рішення для додатків, які виконують числові обчислення і засновані на динамічних багатовимірних масивах.

Можна створювати динамічні багатовимірні масиви з допомогою звичайних одновимірних масивів, слайсів з «незалежних» слайсів, а також слайсів з слайсів «спільно використовуваними даними».

Якщо ви використовуєте звичайні одномірні масиви, то при їх зростанні ви відповідаєте за індексування, перевірку кордонів і перерозподіл пам'яті.

Процес створення динамічного багатовимірного масиву з допомогою слайсів з «незалежних» слайсів складається з двох кроків. Спочатку потрібно створити зовнішній слайс, а потім розмістити в пам'яті всі внутрішні слайсы. Внутрішні слайсы не залежать один від одного. Їх можна збільшувати та зменшувати, не зачіпаючи інші.

package main

func main() { 
x := 2
y := 4

table := make ([] [] (int x)
for i:= range table {
table[i] = make([]int y)
}
}

Створення динамічного багатовимірного масиву з допомогою слайсів з слайсів «спільно використовуваними даними» складається з трьох кроків. Спочатку потрібно створити слайс, що виконує роль «контейнера» даних, які він містить вихідні дані (raw data). Потім — зовнішній слайс. В кінці ми ініціалізуємо кожен з внутрішніх слайсів, перенарезая слайс з вихідними даними.

package main

import "fmt"

func main() { 
h, w := 2, 4

raw := make([]int,h*w)
for i := range raw {
raw[i] = i
}
fmt.Println(raw&raw[4])
// виводить: [0 1 2 3 4 5 6 7] <ptr_addr_x>

table := make([][]int h)
for i:= range table {
table[i] = raw[i*w:i*w + w]
}

fmt.Println(table,&table[1][0])
// виводить: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>
}

Пропонується розробити специфікацію на багатовимірні масиви і слайсы, але зараз, судячи з усього, у цій задачі низький пріоритет.

15. Звернення до неіснуючих ключам в map
Цю помилку роблять розробники, які при зверненні до неіснуючого ключу очікують отримати
nil
-значення (як це відбувається в деяких мовах). Повертає значення
nil
, якщо «нульове значення» для відповідного типу даних —
nil
. Але для інших типів обчислене значення виявиться іншим. Визначати, чи є запис у хеш-таблиці (map record), можна з допомогою перевірки на правильне «нульове значення». Але це не завжди надійно (наприклад, що ви будете робити, якщо у вас є таблиця бульових значень, де «нульове значення» — false). Найнадійніший спосіб дізнатися, чи є запис, — перевірити друге значення, що повертається операцією доступу до таблиці.

Погано:

package main

import "fmt"

func main() { 
x := map[string]string{"one":"a","two":"","three":"c"}

if v := x["two"]; v == "" { // некоректно
fmt.Println("no entry")
}
}

Добре:

package main

import "fmt"

func main() { 
x := map[string]string{"one":"a","two":"","three":"c"}

if _,ok := x["two"]; !ок {
fmt.Println("no entry")
}
}

16. Незмінюваність рядків
Якщо ви спробуєте оновити окремі символи в рядковій змінній з допомогу оператора індексу, то це не спрацює. Рядки — це байт-слайсы (byte slices), доступні тільки для читання. Якщо вам все-таки потрібно оновити рядок, то варто використовувати байт-слайс і перетворювати його в рядок за необхідності.

Неправильно:

package main

import "fmt"

func main() { 
x := "text"
x[0] = 'T'

fmt.Println(x)
}

Помилка компілювання:

/tmp/sandbox305565531/main.go:7: cannot assign to x[0]

Правильно:

package main

import "fmt"

func main() { 
x := "text"
xbytes := []byte(x)
xbytes[0] = 'T'

fmt.Println(string(xbytes)) // виводить Text
}

Варто зауважити, що це неправильний спосіб оновлення символів у текстовому рядку, тому що символ може складатися з декількох байт. В цьому випадку краще конвертувати рядок у слайс з «рун» (руна). Але навіть всередині слайсів з «рун» одиночний символ може бути розбитий на кілька рун, наприклад якщо є символ апострофа (grave accent). Така непроста і заплутана природа «символів» є причиною того, що в Go рядкові значення являють собою послідовностей байтів.

17. Перетворення рядків у байт-слайсы (Byte Slices), і навпаки
Коли ви перетворюєте рядок в байт-слайс (і навпаки), ви отримуєте повну копію вихідних даних. Це не операція приведення (cast operation), як в інших мовах, і не перенарезка (reslicing), коли змінна нового слайсу вказує на один і той же масив, зайнятий вихідним байт-слайсом.

В Go є декілька оптимізацій для перетворень з
[]byte
на
string
та
string
на
[]byte
, що дозволяють уникати додаткових виділень пам'яті (ще більше оптимізацій в списку todo).

Перша оптимізація дозволяє уникнути додаткового виділення пам'яті, коли ключі
[]byte
використовуються для пошуку записів в колекціях
map[string]: m[string(key)]
.

Друга оптимізація дозволяє уникати додаткового виділення у виразах
range for
, коли рядки перетворюються в
[]byte: for i,v := range []byte(str) {...}
.

18. Рядки і оператор індексу
Оператор індексу, що застосовується до рядку, повертає байтове значення (byte value), а не символ (як в інших мовах).

package main

import "fmt"

func main() { 
x := "text"
fmt.Println(x[0]) // виводить 116
fmt.Printf("%T",x[0]) // виводить uint8
}

Якщо потрібно звернутися до конкретних «символів» (кодовою точкам/рунами Unicode), то використовуйте вираз
range for
. Також вам будуть корисні офіційний пакет unicode/utf8 і експериментальний utf8string (golang.org/x/exp/utf8string). utf8string включає в себе зручний метод
At()
. Можна також перетворити рядок слайс рун (slice of runes).

19. Рядки — не завжди текст у кодуванні UTF-8
Рядкові значення необов'язково повинні бути представлені у вигляді тексту в кодуванні UTF-8. Тут можливий довільний набір байтів. Єдиний випадок, коли рядки повинні бути в кодуванні UTF-8, — коли вони використовуються як рядкові літерали. Але навіть вони можуть включати в себе дані з екранованими послідовностями.

Щоб дізнатися кодування рядка, використовуйте функцію
ValidString()
з пакету unicode/utf8.

package main

import ( 
"fmt"
"unicode/utf8"
)

func main() { 
data1 := "ABC"
fmt.Println(utf8.ValidString(data1)) // виводить: true

data2 := "A\xfeC"
fmt.Println(utf8.ValidString(data2)) // виводить: false
}

20. Довжина рядків
Припустимо, ви розробляєте на Python і у вас є такий код:

data = u" 
print(len(data)) #prints: 1 

Якщо перетворити його в аналогічний код на Go, то результат може вас здивувати.

package main

import "fmt"

func main() { 
data := ""
fmt.Println(len(data)) // виводить: 3
}

Вбудована функція
len()
повертає символ, а кількість байт, як це відбувається з Unicode-рядками в Python.

Щоб отримати такий же результат в Go, використовуйте функцію
RuneCountInString()
з пакету unicode/utf8.

package main

import ( 
"fmt"
"unicode/utf8"
)

func main() { 
data := ""
fmt.Println(utf8.RuneCountInString(data)) // виводить: 1

Технічно функція
RuneCountInString()
повертає кількість символів, тому що один символ може займати кілька рун.

package main

import ( 
"fmt"
"unicode/utf8"
)

func main() { 
data := "é"
fmt.Println(len(data)) // виводить: 3
fmt.Println(utf8.RuneCountInString(data)) // виводить: 2
}

21. Відсутня кома в багаторядкових литералах slice/array/map
Неправильно:

package main

func main() { 
x : = [] (int) {
1,
2 // error
}
_ = x
}

Помилки компілювання:

/tmp/sandbox367520156/main.go:6: syntax error: need trailing comma before newline composite in literal /tmp/sandbox367520156/main.go:8: non-declaration statement outside function body /tmp/sandbox367520156/main.go:9: syntax error: unexpected }

Правильно:

package main

func main() { 
x : = [] (int) {
1,
2,
}
x = x

y : = [] (int) {3,4,} // помилки немає
y = y
}

Ви не отримаєте помилку компілювання, якщо залишите замикаючу кому при оголошенні в один рядок.

22. log.Fatal та log.Panic не тільки журналируют
Бібліотеки для логування часто забезпечують різні рівні для повідомлень. На відміну від інших мов, пакет логування в Go робить більше. Якщо викликати його функції
Fatal*()
та
Panic*()
, то додаток буде закрито.

package main

import "log"

func main() { 
log.Fatalln("Fatal Level: log entry") // тут виконується вихід з програми
log.Println("Normal Level: log entry")
}

23. Несинхронізовані операції вбудованих структур даних
Деякі можливості Go нативно підтримують багатозадачність (concurrency), але в їх число не входять потокобезопасные колекції (concurrency safe). Ви самі відповідаєте за атомарність оновлення колекцій. Для реалізації атомарних операцій рекомендується використовувати горутины і канали, але можна задіяти і пакет sync, якщо це доцільно для вашої програми.

24. Ітераційні значення для рядків у виразах range
Значення індексу (перше значення, яке повертається операцією
range
) — це індекс першого байта поточного «символу» (кодова точка/руна Unicode), повернутий у другому значенні. Це не індекс поточного «символу», як в інших мовах. Зверніть увагу, що цей символ може бути представлений декількома рунами. Якщо вам потрібно працювати саме з символами, то варто використовувати пакет norm (golang.org/x/text/unicode/norm).

Вираження
range for
з рядковими змінними намагаються інтерпретувати дані як текст у кодуванні UTF-8. Якщо вони не розпізнають якусь послідовність байтів, то повертають руни 0xfffd (символи заміни Unicode), а не реальні дані. Якщо у вашій рядку зберігаються довільні дані (не UTF-8), то для збереження перетворіть їх в байт-слайсы.

package main

import "fmt"

func main() { 
data := "A\xfe\x02\xff\x04"
for _,v := range data {
fmt.Printf("%#x ",v)
}
// виводить: 0x41 0xfffd 0x2 0xfffd 0x4 (недобре)

fmt.Println()
for _,v := range []byte(data) {
fmt.Printf("%#x ",v)
}
// виводить: 0x41 0xfe 0x2 0xff 0x4 (добре)
}

25. Итерирование хеш-таблиць (map) з допомогою виразу for range
На цей підводний камінь наражаються ті, хто очікують, що елементи будуть розташовуватися в певному порядку (наприклад, відсортовані за значенням ключа). Кожна ітерація хеш-таблиці призводить до різних результатів. Середовище виконання (runtime) Go намагається зробити все можливе, рандомизируя порядок итерирования, але їй це не завжди вдається, тому ви можете отримати кілька однакових ітерацій (наприклад, п'ять).

package main

import "fmt"

func main() { 
m := map[string]int{"one":1,"two":2,"three":3,"four":4}
for k,v := range m {
fmt.Println(k,v)
}
}

А якщо ви використовуєте Go Playground (https://play.golang.org/), то завжди будете отримувати однакові результати, тому що код не перекомпилируется, поки ви його не зміните.

26. Помилка поведінка у виразах switch
Блоки
case
у виразах
switch
за замовчуванням перериваються (break). В інших мовах поведінку за промовчанням інше: перехід (fall through) до наступного блоку
case
.

package main

import "fmt"

func main() { 
isSpace := func(ch byte) bool {
switch(ch) {
case ' ': // помилка
case '\t':
return true
}
return false
}

fmt.Println(isSpace('\t')) // виводить true (добре)
fmt.Println(isSpace(' ')) // виводить false (погано)
}

Можна змусити блоки case переходити примусово з допомогою виразу
fallthrough
в кінці кожного блоку. Можна також переписати ваше вираз
switch
, щоб у блоках використовувалися списки виразів.

package main

import "fmt"

func main() { 
isSpace := func(ch byte) bool {
switch(ch) {
case ' ', '\t':
return true
}
return false
}

fmt.Println(isSpace('\t')) // виводить true (добре)
fmt.Println(isSpace(' ')) // виводить true (добре)
}

27. Инкременты і декременты
У багатьох мовах є оператори инкрементирования і декрементирования. Але в Go не підтримуються їх префіксні версії. Також не можна в одному вираженні використати обидва вирази.

Неправильно:

package main

import "fmt"

func main() { 
data : = [] (int) {1,2,3}
i := 0
++i // error
fmt.Println(data[i++]) // помилка
}

Помилки компілювання:

/tmp/sandbox101231828/main.go:8: syntax error: unexpected ++ /tmp/sandbox101231828/main.go:9: syntax error: unexpected ++, expecting :

Правильно:

package main

import "fmt"

func main() { 
data : = [] (int) {1,2,3}
i := 0
i++
fmt.Println(data[i])
}

28. Побітове NOT-оператор
У багатьох мовах символ ~ використовується в якості унарной NOT-операції (aka побітове доповнення, bitwise complement), проте в Go для цього застосовується XOR-оператор (^).

Неправильно:

package main

import "fmt"

func main() { 
fmt.Println(~2) // помилка
}

Помилка компілювання:

/tmp/sandbox965529189/main.go:6: the bitwise complement operator is ^

Правильно:

package main

import "fmt"

func main() { 
var d uint8 = 2
fmt.Printf("%08b\n",^d)
}

Когось може заплутати, що ^ в Go — це XOR-оператор. Якщо хочете, висловлюйте унарную NOT-операцію (наприклад,
NOT 0x02
) з допомогою бінарної XOR-операції (наприклад,
0x02 XOR 0xff
). Це пояснює, чому ^ використовується для вираження унарной NOT-операції.

Також в Go є спеціальний побітове оператор AND NOT (&^), який легко прийняти за оператор NOT. AND NOT виглядає як спеціальна функція/хак заради підтримки
A AND (NOT B)
без обов'язкового використання фігурних дужок.

package main

import "fmt"

func main() { 
var a uint8 = 0x82
var b uint8 = 0x02
fmt.Printf("%08b [A]\n",a)
fmt.Printf("%08b [B]\n",b)

fmt.Printf("%08b (NOT B)\n",^b)
fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n",b,0xff,b ^ 0xff)

fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n",a,b,a ^ b)
fmt.Printf("%08b & %08b = %08b [A AND B]\n",a,b,a & b)
fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n",a,b,a &^ b)
fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n",a,b,a & (^b))
}

29. Відмінності пріоритетів операторів
Крім «досить зрозумілих» (bit clear) операторів (&^), Go є набір стандартних операторів, використовуються багатьма іншими мовами. Але їх пріоритети в даному випадку не завжди такі ж.

package main

import "fmt"

func main() { 
fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n",0x2 & 0x2 + 0x4)
//prints: 0x2 & 0x2 + 0x4 -> 0x6
//Go: (0x2 & 0x2) + 0x4
//C++: 0x2 & (0x2 + 0x4) -> 0x2

fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n",0x2 + 0x2 << 0x1)
//prints: 0x2 + 0x2 << 0x1 -> 0x6
//Go: 0x2 + (0x2 << 0x1)
//C++: (0x2 + 0x2) << 0x1 -> 0x8

fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n",0xf | 0x2 ^ 0x2)
//prints: 0xf | 0x2 ^ 0x2 -> 0xd
//Go: (0xf | 0x2) ^ 0x2
//C++: 0xf | (0x2 ^ 0x2) -> 0xf
}

30. Неэкспортированные поля структур не кодуються
Поля структур (struct fields), що починаються з літер, не будуть кодуватися (JSON, XML, GON і т. д.), так що при декодуванні структури ви отримаєте в цих неэкспортированных полях нульові значення.

package main

import ( 
"fmt"
"encoding/json"
)

type MyData struct { 
One int
two string
}

func main() { 
in := MyData{1,"two"}
fmt.Printf("%#v\n",in) // виводить main.MyData{One:1, two:"two"}

encoded,_ := json.Marshal(in)
fmt.Println(string(encoded)) // виводить {"One":1}

var out MyData
json.Unmarshal(encoded,&out)

fmt.Printf("%#v\n",out) // виводить main.MyData{One:1, two:""}
}

31. Вихід з додатків за допомогою активних горутин
Додаток не буде чекати завершення ваших горутин. Новачки часто про це забувають. Всі колись починають — в таких помилках немає нічого соромно.

package main

import ( 
"fmt"
"time"
)

func main() { 
workerCount := 2

for i := 0; i < workerCount; i++ {
go doit(i)
}
time.Sleep(1 * time.Second)
fmt.Println("all done!")
}

func doit(workerId int) { 
fmt.Printf("[%v] is running\n",workerId)
time.Sleep(3 * time.Second)
fmt.Printf("[%v] is done\n",workerId)
}

Ви побачите:

[0] is running 
[1] is running 
all done!

Одне з найбільш популярних рішень — змінна
WaitGroup
. Це дозволить головною горутине очікувати завершення роботи всіх робочих горутин. Якщо ваш додаток використовує довго виконуються робочі горутины з циклами обробки повідомлень, то вам знадобиться як-то сигналізувати про те, що пора виходити. Можна відправляти кожної такої горутине повідомлення
kill
. Або закривати канали, з яких робочі горутины отримують дані: це простий спосіб сигналізувати оптом.

package main

import ( 
"fmt"
"sync"
)

func main() { 
var wg sync.WaitGroup
done := make(chan struct{})
workerCount := 2

for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i,done,wg)
}

close(done)
wg.Wait()
fmt.Println("all done!")
}

func doit(workerId int,done <-chan struct{},wg sync.WaitGroup) { 
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()
<- done
fmt.Printf("[%v] is done\n",workerId)
}

Якщо запустити цей додаток, ви побачите:

[0] is running 
[0] is done 
[1] is running 
[1] is done

Схоже, всі горутины закінчили працювати до виходу головною горутины. Чудово! Однак ви побачите і це:

fatal error: all goroutines are asleep - deadlock!

Недобре! Що відбувається? Звідки взялася глухий кут? Адже всі вийшли і виконали
wg.Done()
. Додаток повинен працювати.

Блокування виникає, тому що кожен робітник отримує копію вихідної змінної
WaitGroup
. І коли всі вони виконують
wg.Done()
, це ніяк не впливає на змінну
WaitGroup
в головній горутине.

package main

import ( 
"fmt"
"sync"
)

func main() { 
var wg sync.WaitGroup
done := make(chan struct{})
wq := make(chan interface{})
workerCount := 2

for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i,wq,done,&wg)
}

for i := 0; i < workerCount; i++ {
wq <- i
}

close(done)
wg.Wait()
fmt.Println("all done!")
}

func doit(workerId int, wq <-chan interface{},done <-chan struct{},wg *sync.WaitGroup) { 
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()
for {
select {
case m := <- wq:
fmt.Printf("[%v] m => %v\n",workerId,m)
case <- done:
fmt.Printf("[%v] is done\n",workerId)
return
}
}
}

Тепер все працює правильно.

32. При відправці в небуферизованный канал дані повертаються у міру готовності отримувача
Відправник не буде заблокований, поки одержувач обробляє ваше повідомлення. В залежності від машини, на якій виконується код, отримує горутина може і не мати достатньо часу на обробку повідомлення, перш ніж продовжиться виконання відправника.

package main

import "fmt"

func main() { 
ch := make(chan string)

go func() {
for m := range ch {
fmt.Println("processed:",m)
}
}()

ch <- "cmd.1"
ch <- "cmd.2" // не буде оброблено
}

33. Відправка в закритий канал призводить до panic
Отримання із закритого каналу безпечно. Повертає значення
ok
в вихідному виразі (receive statement) стане
false
, що говорить про те, що ніякі дані не були отримані. Якщо ви отримуєте з буферизованного каналу, то отримаєте спочатку буферизовані дані, а коли вони закінчаться, вираз
ok
стане
false
.

Надсилання даних закритий канал призводить до panic. Це задокументоване поведінку, але воно не завжди інтуїтивно очікувано розробниками, які можуть вважати, що поведінка при відправці буде аналогічно поведінці при прийомі.

package main

import ( 
"fmt"
"time"
)

func main() { 
ch := make(chan int)
for i := 0; i < 3; i++ {
go func(int idx) {
ch <- (idx + 1) * 2
}(i)
}

// get the first result
fmt.Println(<-ch)
close(ch) //недобре (у вас все ще є інші відправники)
// do other work
time.Sleep(2 * time.Second)
}

Рішення залежить від вашого застосування. Це може бути невелика зміна коду — чи архітектури, якщо буде потрібно. У будь-якому випадку переконайтеся, що програма не намагається надіслати дані закритий канал.

Приклад з багом можна виправити, сигналізуючи через спеціальний канал скасування (special cancellation channel) іншим робочим горутинам, що їх результати більше не потрібні.

package main

import ( 
"fmt"
"time"
)

func main() { 
ch := make(chan int)
done := make(chan struct{})
for i := 0; i < 3; i++ {
go func(int idx) {
select {
case ch <- (idx + 1) * 2: fmt.Println(idx,"sent result")
case <- done: fmt.Println(idx,"exiting")
}
}(i)
}

// get first result
fmt.Println("result:",<-ch)
close(done)
// do other work
time.Sleep(3 * time.Second)
}

34. Використання «nil»-каналів
В каналі
nil
операції відправки і прийому блокуються назавжди. Це добре задокументовану поведінку, але воно може стати сюрпризом для новачків.

package main

import ( 
"fmt"
"time"
)

func main() { 
var ch chan int
for i := 0; i < 3; i++ {
go func(int idx) {
ch <- (idx + 1) * 2
}(i)
}

// get first result
fmt.Println("result:",<-ch)
// do other work
time.Sleep(2 * time.Second)
}

При виконанні цього коду ви побачите помилку runtime зразок
fatal error: all goroutines are asleep - deadlock!


Це поведінка можна використовувати для динамічного включення і відключення блоків
case
у вираженні
select
.

package main

import "fmt" 
import "time"

func main() { 
inch := make(chan int)
outch := make(chan int)

go func() {
var in <- chan int = inch
var out chan <- int
var val int
for {
select {
case out <- val:
out = nil
in = inch
case val = <- in:
out = outch
in = nil
}
}
}()

go func() {
for r := range outch {
fmt.Println("result:",r)
}
}()

time.Sleep(0)
inch <- 1
inch <- 2
time.Sleep(3 * time.Second)
}

35. Методи, що приймають параметри за значенням, не змінюють вихідних значень
Параметри методів — це звичайні аргументи функцій. Якщо вони оголошуються значенням, функція/метод отримує копію вашого аргументу (receiver argument). Зміни в прийнятому значенні не вплинуть на початкове значення, якщо значення — мінлива хеш-таблиці (map) або слайсу і ви оновлюєте елементи колекції або якщо оновлювані поля в значенні — це покажчики.

package main

import "fmt"

data type struct { 
int num
key *string
items map[string]bool
}

func (this *data) pmethod() { 
this.num = 7
}

func (this data) vmethod() { 
this.num = 8
*this.key = "v.key"
this.items["vmethod"] = true
}

func main() { 
key := "key.1"
d := data{1,&key,make(map[string]bool)}

fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
// prints num=1 key=key.1 items=map[]

d.pmethod()
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
// prints num=7 key=key.1 items=map[]

d.vmethod()
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
// prints num=7 key=v.key items=map[vmethod:true]
}

36. Закриття тіла HTTP-відповіді
Роблячи запит з допомогою стандартної HTTP бібліотеки, ви отримуєте змінну HTTP-відповіді. Навіть якщо ви не читаєте тіло відповіді, все одно треба його закрити. Зверніть увагу: це відноситься і до порожніх відповідей. Про них дуже легко забути, особливо новачкам.

Деякі новачки намагаються закривати тіло відповіді, але в неправильному місці.

package main

import ( 
"fmt"
"net/http"
"io/ioutil"
)

func main() { 
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close()// неправильно
if err != nil {
fmt.Println(err)
return
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(string(body))
}

Цей код буде працювати з успішними HTTP-запитами, але в разі збою мінлива
resp
може бути
nil
, що призведе до runtime panic.

Найпоширеніший спосіб закрити тіло відповіді — з допомогою виклику
defer
після перевірки помилковості HTTP-відповіді.

package main

import ( 
"fmt"
"net/http"
"io/ioutil"
)

func main() { 
resp, err := http.Get("https://api.ipify.org?format=json")
if err != nil {
fmt.Println(err)
return
}

defer resp.Body.Close()// припустимо, в більшості випадків :-)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(string(body))
}

У більшості випадків, коли виникають збої HTTP-запитів, мінлива
resp
nil
, а змінна
err — non-nil
. Але при збої переадресації обидві змінні будуть
non-nil
. Це означає виникнення витоку.

Її можна запобігти, додавши виклик для закриття тел відповідей
non-nil
в блоці обробки помилок HTTP-запитів. Інше рішення: використовувати один виклик
defer
для закриття тел відповідей для всіх збійних і успішних запитів.

package main

import ( 
"fmt"
"net/http"
"io/ioutil"
)

func main() { 
resp, err := http.Get("https://api.ipify.org?format=json")
if resp != nil {
defer resp.Body.Close()
}

if err != nil {
fmt.Println(err)
return
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(string(body))
}

Вихідна реалізація
resp.Body.Close()
також зчитує і відхиляє дані залишилися тел відповідей. Завдяки цьому HTTP-з'єднання може бути повторно використано для іншого запиту, якщо включено поведінка
keep alive
. Поведінка самого останнього HTTP-клієнт відрізняється. Тепер ви відповідальні за читання та відхилення даних, що залишилися відповідей. Якщо цього не зробити, то HTTP-з'єднання замість повторного використання може бути закрито. Сподіваюся, цей маленький підводний камінь буде задокументовано у Go 1.5.

Якщо для вашого додатки важливо повторно використовувати HTTP-з'єднання, то в кінці логіки обробки відповіді може знадобитися додати щось на зразок цього:

_, err = io.Copy(ioutil.Discard, resp.Body)

Це буде необхідно, якщо ви не прочитаєте все тіло відповіді негайно, наприклад при обробці відповідей JSON API за допомогою такого коду:

json.NewDecoder(resp.Body).Decode(&data)

37. Закриття HTTP-з'єднань
Деякі HTTP-сервери якийсь час тримають мережеві з'єднання відкритими (згідно специфікації HTTP 1.1 і серверної конфігурації
keep alive
). За замовчуванням стандартна HTTP-бібліотека закриває з'єднання, лише коли про це просить цільової HTTP-сервер. Тоді при певних умовах у вашому додатку можуть закінчитися сокети / файлові дескриптори.

Можна попросити бібліотеку закривати з'єднання після завершення вашого запиту, задавши значення
true
в полі
Close
змінної запиту.

Інше рішення: додати заголовок
Connection
і задати йому значення
close
. Цільовий HTTP-сервер повинен відповісти заголовком
Connection: close
. Коли бібліотека його побачить, вона закриє з'єднання.

package main

import ( 
"fmt"
"net/http"
"io/ioutil"
)

func main() { 
req, err := http.NewRequest("GET","http://golang.org",nil)
if err != nil {
fmt.Println(err)
return
}

req.Close = true
//or do this:
//req.Header.Add("Connection", "close")

resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}

if err != nil {
fmt.Println(err)
return
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(len(string(body)))
}

Можна ще глобально вимкнути повторне використання HTTP-з'єднань. Для цього створіть кастомний конфігурацію HTTP транспорту.

package main

import ( 
"fmt"
"net/http"
"io/ioutil"
)

func main() { 
tr := &http.Transport{DisableKeepAlives: true}
client := &http.Client{Transport: tr}

resp, err := client.Get("http://golang.org")
if resp != nil {
defer resp.Body.Close()
}

if err != nil {
fmt.Println(err)
return
}

fmt.Println(resp.StatusCode)

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(len(string(body)))
}

Якщо ви відправляєте на один сервер багато запитів, то цього достатньо для збереження з'єднання відкритим. Але якщо додаток за короткий час шле один-два запиту на багато різних серверів, то краще закривати з'єднання відразу після одержання відповідей. Також можна збільшити ліміт на кількість відкритих файлів. Що краще — залежить від вашого застосування.

38. Десериализация (unmarshalling) JSON-чисел в інтерфейсні значення
Коли ви декодируете/десериализуете JSON-дані в інтерфейс, Go за замовчуванням звертається з числовими значеннями в JSON як з числами
float64
. Значить, ось такий код викличе panic:

package main

import ( 
"encoding/json"
"fmt"
)

func main() { 
var data = []byte(`{"status": 200}`)

var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
fmt.Println("error:", err)
return
}

var status = result["status"].(int) // помилка
fmt.Println("status value:",status)
}

Runtime Panic:

panic: interface conversion: interface is float64, int not


Якщо JSON-значення, яке ви намагаєтеся декодувати ціле, є кілька варіантів.

  • Використовувати значення з плаваючою комою є :-)
  • Перетворити значення з плаваючою комою в цілочисельний тип, який вам потрібен.

    package main
    
    import ( 
    "encoding/json"
    "fmt"
    )
    
    func main() { 
    var data = []byte(`{"status": 200}`)
    
    var result map[string]interface{}
    if err := json.Unmarshal(data, &result); err != nil {
    fmt.Println("error:", err)
    return
    }
    
    var status = uint64(result["status"].(float64)) // добре
    fmt.Println("status value:",status)
    }
    

  • тип
    Decoder
    для десеріалізації JSON та подання JSON-чисел з допомогою інтерфейсного типу
    Number
    .

    package main
    
    import ( 
    "encoding/json"
    "bytes"
    "fmt"
    )
    
    func main() { 
    var data = []byte(`{"status": 200}`)
    
    var result map[string]interface{}
    var decoder = json.NewDecoder(bytes.NewReader(data))
    decoder.UseNumber()
    
    if err := decoder.Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
    }
    
    var status,_ = result["status"].(json.Number).Int64() // добре
    fmt.Println("status value:",status)
    }
    

    Можна використовувати рядкове представлення вашого значення
    Number
    , щоб десериализовать його в інший числовий тип:

    package main
    
    import ( 
    "encoding/json"
    "bytes"
    "fmt"
    )
    
    func main() { 
    var data = []byte(`{"status": 200}`)
    
    var result map[string]interface{}
    var decoder = json.NewDecoder(bytes.NewReader(data))
    decoder.UseNumber()
    
    if err := decoder.Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
    }
    
    var status uint64
    if err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status); err != nil {
    fmt.Println("error:", err)
    return
    }
    
    fmt.Println("status value:",status)
    }
    

  • тип
    struct
    , який перетворює (maps) числове значення в потрібний вам числовий тип.

    package main
    
    import ( 
    "encoding/json"
    "bytes"
    "fmt"
    )
    
    func main() { 
    var data = []byte(`{"status": 200}`)
    
    var result struct {
    Status uint64 `json:"status"`
    }
    
    if err := json.NewDecoder(bytes.NewReader(data)).Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
    }
    
    fmt.Printf("result => %+v",result)
    //prints: result => {Status:200}
    }
    

  • struct
    для перетворення числового значення типу
    json.RawMessage
    , якщо потрібно відкласти декодування значення.

    Це корисно, якщо ви повинні виконати декодування умовного JSON-поля (conditional field) в умовах можливості зміни структури або типу поля.

    package main
    
    import ( 
    "encoding/json"
    "bytes"
    "fmt"
    )
    
    func main() { 
    records := [][]byte{
    []byte(`{"status": 200, "tag":"one"}`),
    []byte(`{"status":"ok", "tag":"two"}`),
    }
    
    for idx, record := range records {
    var result struct {
    StatusCode uint64
    StatusName string
    Status json.RawMessage `json:"status"`
    Tag string `json:"tag"`
    }
    
    if err := json.NewDecoder(bytes.NewReader(record)).Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
    }
    
    var sstatus string
    if err := json.Unmarshal(result.Status, &sstatus); err == nil {
    result.StatusName = sstatus
    }
    
    var nstatus uint64
    if err := json.Unmarshal(result.Status, &nstatus); err == nil {
    result.StatusCode = nstatus
    }
    
    fmt.Printf("[%v] result => %+v\n",idx,result)
    }
    }
    

39. Порівняння struct, array, slice і map
Можна використовувати оператор еквівалентності
==
для порівняння змінних структур, якщо кожне поле структури можна порівняти з допомогою цього оператора.

package main

import "fmt"

data type struct { 
int num
fp float32
complex complex64
string str
char rune
yes bool
events <-chan string
handler interface{}
ref *byte
raw [10]byte
}

func main() { 
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2) // виводить: v1 == v2: true
}

Якщо хоч одне з полів несравниваемо, то застосування оператора еквівалентності призведе до помилки компілювання. Зверніть увагу, що порівнювати масиви можна тільки тоді, коли порівнювані дані.

package main

import "fmt"

data type struct { 
int num // ok
checks [10]func() bool // несравниваемо
doit func() bool // несравниваемо
m map[string] string // несравниваемо
bytes []byte // несравниваемо
}

func main() { 
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2)
}

Go надає декілька допоміжних функцій для порівняння змінних, які не можна порівнювати з допомогою операторів порівняння.

Найпопулярніше рішення: використовувати функцію
DeepEqual()
з пакету reflect.

package main

import ( 
"fmt"
"reflect"
)

data type struct { 
int num // ok
checks [10]func() bool // несравниваемо
doit func() bool // несравниваемо
m map[string] string // несравниваемо
bytes []byte // несравниваемо
}

func main() { 
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2)) // prints: v1 == v2: true

m1 := map[string]string{"one": "a","two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) // prints: m1 == m2: true

s1 : = [] (int) {1, 2, 3}
s2 : = [] (int) {1, 2, 3}
fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2)) // prints: s1 == s2: true
}

Крім невисокій швидкості (що може бути критичним для вашого додатки),
DeepEqual()
має свої підводні камені.

package main

import ( 
"fmt"
"reflect"
)

func main() { 
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2)) // prints: b1 == b2: false
}

DeepEqual()
не вважає марною слайс еквівалентним
nil
-слайсу. Це поведінка відрізняється від того, що ви отримаєте при використанні функції
bytes.Equal()
: вона вважає еквівалентними
nil
і порожні слайсы.

package main

import ( 
"fmt"
"bytes"
)

func main() { 
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) // prints: b1 == b2: true
}

DeepEqual()
не завжди ідеальна при порівнянні слайсів.

package main

import ( 
"fmt"
"reflect"
"encoding/json"
)

func main() { 
var string str = "one"
var in interface{} = "one"
fmt.Println("str == in:",str == in,reflect.DeepEqual(str, in))
//prints: str == in: true true

v1 := []string{"one","two"}
v2 := []interface{}{"one","two"}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2))
//prints: v1 == v2: false, not ok)

data := map[string]interface{}{
"code": 200,
"value": []string{"one","two"},
}
encoded, _ := json.Marshal(data)
var decoded map[string]interface{}
json.Unmarshal(encoded, &decoded)
fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded))
//prints: data == decoded: false, not ok)
}

Якщо ваші байт-слайсы (або рядка) містять текстові дані, то, коли знадобиться порівняти значення без урахування регістру, ви можете використовувати
ToUpper()
або
ToLower()
з пакетів bytes і strings (перш ніж вдатися до
==
,
bytes.Equal()
або
bytes.Compare()
). Це спрацює для англомовних текстів, але не для багатьох інших мов. Так що краще вибрати
strings.EqualFold()
та
bytes.EqualFold()
.

Якщо ваші байт-слайсы містять секретні дані (криптографічні хеш, токени тощо), які потрібно порівнювати з наданої користувачами інформацією, обійдіться без
reflect.DeepEqual()
,
bytes.Equal()
або
bytes.Compare()
. Ці функції зроблять додаток вразливим до атак по часу. Щоб уникнути витоку інформації про час, використовуйте функції з пакету crypto/subtle (наприклад,
subtle.ConstantTimeCompare()
).

40. Відновлення після panic
Функцію
recover()
можна використовувати для упіймання/перехоплення panic. Це вийде, тільки якщо викликати її в блоці defer.

Некоректно:

package main

import "fmt"

func main() { 
recover() // нічого не робить
panic("not good")
recover() // виконано не буде :)
fmt.Println("ok")
}

Правильно:

package main

import "fmt"

func main() { 
defer func() {
fmt.Println("відновлені:",recover())
}()

panic("not good")
}

Виклик
recover()
спрацює, тільки якщо буде виконаний в блоці defer.

Неправильно:

package main

import "fmt"

func doRecover() { 
fmt.Println("відновлені =>",recover()) // prints: відновлені => <nil>
}

func main() { 
defer func() {
doRecover() // відновлення panic не сталося
}()

panic("not good")
}

41. Оновлення і прив'язка значень полів в slice, array і map у виразах for range
Згенеровані у виразах
range
значення даних — це копії реальних елементів колекцій, а не посилання на вихідні елементи. Стало бути, оновлення значень не змінить вихідні дані. Крім того, якщо взяти адресу значення, то ви не отримаєте вказівник на вихідні дані.

package main

import "fmt"

func main() { 
data : = [] (int) {1,2,3}
for _,v := range data {
v *= 10 // оригінал не змінився
}

fmt.Println("data:",data) // виводить: [1 2 3]
}

Якщо вам потрібно оновити початкове значення запису в колекції, то для доступу до даних скористайтесь індексним оператором.

package main

import "fmt"

func main() { 
data : = [] (int) {1,2,3}
for i,_ := range data {
data[i] *= 10
}

fmt.Println("data:",data) // виводить: [10 20 30]
}

Якщо колекція містить значення покажчиків, правил трохи змінюються. Вам все ще потрібно використовувати індексний оператор, щоб вихідна запис вказувала на інше значення. Але ви можете оновлювати дані, що зберігаються в цільовому місці, з допомогою другого значення у вираженні
range for
.

package main

import "fmt"

func main() { 
data := []*struct{int num} {{1},{2},{3}}

for _,v := range data {
v.num *= 10
}

fmt.Println(data[0],data[1],data[2]) // prints &{10} &{20} &{30}
}

42. «Приховані дані» слайсах
При перенарезке одержаний слайс буде посилатися на масив вихідного слайсу. Не забувайте про це. Інакше може виникнути непередбачене споживання пам'яті, коли програма розмістить в ній великі тимчасові слайсы і створює з них нові, щоб посилатися на невеликі шматки вихідних даних.

package main

import "fmt"

func get() []byte { 
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0]) // виводить: 10000 10000 <byte_addr_x>
return raw[:3]
}

func main() { 
data := get()
fmt.Println(len(data),cap(data),&data[0]) // виводить: 3 10000 <byte_addr_x>
}

Щоб уникнути цієї помилки, переконайтеся, що скопіювати потрібні дані з тимчасового слайсу (замість перенарезкі).

package main

import "fmt"

func get() []byte { 
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0]) // виводить: 10000 10000 <byte_addr_x>
res := make([]byte,3)
copy(res,raw[:3])
return res
}

func main() { 
data := get()
fmt.Println(len(data),cap(data),&data[0]) // виводить: 3 3 <byte_addr_y>
}

43. «Пошкодження» даних в слайсах
Припустимо, вам потрібно переписати шлях (зберігається в слайсе). Щоб посилатися на кожну папку, ви його перенарезаете, змінюючи ім'я першої папки, а потім комбінуєте імена в новий шлях.

package main

import ( 
"fmt"
"bytes"
)

func main() { 
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1)) // виводить: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) // виводить: dir2 => BBBBBBBBB

dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

fmt.Println("dir1 =>",string(dir1)) // виводить: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) // виводить: dir2 => uffixBBBB (not ok)

fmt.Println("new path =>",string(path))
}

Так не спрацює. Замість AAAAsuffix/BBBBBBBBB ви отримаєте AAAAsuffix/uffixBBBB. Причина в тому, що слайсы обох папок посилаються на один і той же масив даних з вихідного слайсу шляху. Тобто вихідний шлях теж змінився. Це може бути проблемою для вашої програми.

Її можна вирішити, розмістивши у пам'яті нові слайсы і скопіювавши туди потрібні дані. Інший вихід: використовувати повне вираження слайсу (full slice expression).

package main

import ( 
"fmt"
"bytes"
)

func main() { 
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex:sepIndex] // повне вираження слайсу
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1)) // виводить: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) // виводить: dir2 => BBBBBBBBB

dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

fmt.Println("dir1 =>",string(dir1)) // виводить: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) // виводить: dir2 => BBBBBBBBB (ok now)

fmt.Println("new path =>",string(path))
}

Додатковий параметр в повному вираженні управляє ємністю нового слайсу. Додавання цього параметра до нового слайсу запустить розміщення в пам'яті нового буфера замість перезапису даних у другій слайс.

44. «Застарілі» слайсы
На одні і ті ж дані можуть посилатися кілька слайсів. Наприклад, коли ви створюєте новий слайс на основі наявного. Якщо така поведінка важливо для вашого додатки, подбайте про «застарілих» слайсах.

У якийсь момент додавання даних в один з слайсів призведе до розміщення в пам'яті нового масиву, тому що в старому не вистачить місця для нових даних. Тепер на старий масив (зі старими даними) посилаються кілька слайсів.

import "fmt"

func main() { 
s1 : = [] (int) {1,2,3}
fmt.Println(len(s1),cap(s1),s1) // виводить 3 3 [1 2 3]

s2 := s1[1:]
fmt.Println(len(s2),cap(s2),s2) // виводить 2 2 [2 3]

for i := range s2 { s2[i] += 20 }

// все ще посилається на той же масив
fmt.Println(s1) // виводить [1 22 23]
fmt.Println(s2) // виводить [22 23]

s2 = append(s2,4)

for i := range s2 { s2[i] += 10 }

//s1 is now "stale"
fmt.Println(s1) // виводить [1 22 23]
fmt.Println(s2) // виводить [32 33 14]
}

45. Методи і оголошення типів
Коли ви визначаєте новий тип на основі існуючого (не інтерфейсного), тим самим ви створюєте оголошення типу і не наслідуєте методи, оголошені в існуючому типі.

Неправильно:

package main

import "sync"

type myMutex sync.Mutex

func main() { 
var mtx myMutex
mtx.Lock() // помилка
mtx.Unlock() // помилка
}

Помилки компілювання:

/tmp/sandbox106401185/main.go:9: mtx.Lock undefined (type myMutex field has no or method Lock) /tmp/sandbox106401185/main.go:10: mtx.Unlock undefined (type myMutex field has no or method Unlock)

Якщо вам потрібні методи з вихідного типу, ви можете задати новий тип структури, вмонтувавши вихідний в якості анонімного поля.

Правильно:

package main

import "sync"

type myLocker struct { 
sync.Mutex
}

func main() { 
var lock myLocker
lock.Lock() // ok
lock.Unlock() // ok
}

Оголошення інтерфейсних типів також зберігають свої набори методів.

Правильно:

package main

import "sync"

type myLocker sync.Locker

func main() { 
var lock myLocker = new(sync.Mutex)
lock.Lock() // ok
lock.Unlock() // ok
}

46. Як вибратися з кодових блоків for switch і for select
  • рівень: більш досвідчений
Вираз
break
без мітки (label) виводить вас тільки з внутрішнього блоку
switch/select
. Якщо використати вираз
return
— не варіант, тоді найкращий вихід — поставити мітку для зовнішнього циклу.

package main

import "fmt"

func main() { 
loop:
for {
switch {
case true:
fmt.Println("breaking out...")
break loop
}
}

fmt.Println("out!")
}

Те ж саме і з виразом
goto


47. Ітераційні змінні і замикання у виразах for
Найпоширеніша проблема в Go. Ітераційні змінні у вираженні
for
знову використовуються для кожної ітерації. Це означає, що кожне замикання (aka функціональний літерал), створене у вашому цикл
for
, буде посилатися на ту ж змінну (і вони отримають значення змінної в той момент, коли почнеться виконання їх горутин).

Некоректно:

package main

import ( 
"fmt"
"time"
)

func main() { 
data := []string{"one","two","three"}

for _,v := range data {
go func() {
fmt.Println(v)
}()
}

time.Sleep(3 * time.Second)
// горутины виводять: three, three, three
}

Найпростіше рішення (не вимагає змінювати горутины): зберегти поточне значення ітераційної змінної локальної змінної всередині блоку циклу
for
.

Правильно:

package main

import ( 
"fmt"
"time"
)

func main() { 
data := []string{"one","two","three"}

for _,v := range data {
vcopy := v //
go func() {
fmt.Println(vcopy)
}()
}

time.Sleep(3 * time.Second)
// горутины виводять: one, two, three
}

Інше рішення: передати поточну ітераційну змінну анонімної горутине у вигляді параметра.

Правильно:

package main

import ( 
"fmt"
"time"
)

func main() { 
data := []string{"one","two","three"}

for _,v := range data {
go func(in string) {
fmt.Println(in)
}(v)
}

time.Sleep(3 * time.Second)
// горутины виводять: one, two, three
}

Тут трохи більш складна версія пастки.

Некоректно:

package main

import ( 
"fmt"
"time"
)

type field struct { 
string name
}

func (p *field) print() { 
fmt.Println(p.name)
}

func main() { 
data := []field{{"one"},{"two"},{"three"}}

for _,v := range data {
go v.print()
}

time.Sleep(3 * time.Second)
// горутины виводять: three, three, three
}

Правильно:

package main

import ( 
"fmt"
"time"
)

type field struct { 
string name
}

func (p *field) print() { 
fmt.Println(p.name)
}

func main() { 
data := []field{{"one"},{"two"},{"three"}}

for _,v := range data {
v := v
go v.print()
}

time.Sleep(3 * time.Second)
// горутины виводять: one, two, three
}

Як ви думаєте, що ви побачите (і чому), запустивши цей код?

package main

import ( 
"fmt"
"time"
)

type field struct { 
string name
}

func (p *field) print() { 
fmt.Println(p.name)
}

func main() { 
data := []*field{{"one"},{"two"},{"three"}}

for _,v := range data {
go v.print()
}

time.Sleep(3 * time.Second)
}

48. Обчислення аргументу блоку defer (Deferred Function Call Argument Evaluation)
Аргументи для дзвінків відкладених функцій обчислюються тоді ж, коли і вираз
defer
(а не коли насправді виконується функція).

package main

import "fmt"

func main() { 
var i int = 1

defer fmt.Println("result =>",func() int { return i * 2 }())
i++
//виводить: result => 2 (not ok if you expected 4)
}

49. Виклик блоку defer
Відстрочені виклики виконуються в кінці містить їх функції, а не в кінці містить їх кодового блоку. Новачки часто помиляються, плутаючи правила виконання відкладеного коду з правилами визначення області видимості змінної. Це може стати проблемою, якщо ви довго виконуєте функцію цикл
for
, яка під час кожної ітерації намагається відкласти (
defer
) виклики очищення ресурсів.

package main

import ( 
"fmt"
"os"
"path/filepath"
)

func main() { 
if len(os.Args) != 2 {
os.Exit(-1)
}

start, err := os.Stat(os.Args[1])
if err != nil || !start.IsDir(){
os.Exit(-1)
}

var targets []string
filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}

if !fi.Mode().IsRegular() {
return nil
}

targets = append(targets,fpath)
return nil
})

for _,target := range targets {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err) //виводить помилку: too many open files
break
}
defer f.Close() // не буде закрито в кінці цього блоку
// зроби що-небудь з файлом...
}
}

Один із способів вирішення проблеми — обернути кодовий блок в функцію.

package main

import ( 
"fmt"
"os"
"path/filepath"
)

func main() { 
if len(os.Args) != 2 {
os.Exit(-1)
}

start, err := os.Stat(os.Args[1])
if err != nil || !start.IsDir(){
os.Exit(-1)
}

var targets []string
filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}

if !fi.Mode().IsRegular() {
return nil
}

targets = append(targets,fpath)
return nil
})

for _,target := range targets {
func() {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err)
return
}
defer f.Close() // ok
// зроби що-небудь з файлом...
}()
}
}

Інше рішення: позбутися від виразу
defer
:-)

50. Помилки при приведенні типів
Збійні затвердження типів повертають «нульове значення» для цільових типів, використаних в операторі затвердження. Накладені на приховування змінних, це може призвести до непередбачуваного поведінки.

Некоректно:

package main

import "fmt"

func main() { 
var data interface{} = "great"

if data, ok := data.(int); ok {
fmt.Println("[is an int] value =>",data)
} else {
fmt.Println("[not an int] value =>",data)
//виводить: [not an int] value => 0 (not "great")
}
}

Правильно:

package main

import "fmt"

func main() { 
var data interface{} = "great"

if res, ok := data.(int); ok {
fmt.Println("[is an int] value =>",res)
} else {
fmt.Println("[not an int] value =>",data)
// виводить: [not an int] value => great (as expected)
}
}

51. Блоковані горутины і витоку ресурсів
У виступі «Go Concurrency Patterns» на конференції Google I/O 2012-го Роб Пайк розповів про кількох фундаментальних concurrency-шаблонах. Один з них — отримання першого результату.

func First(query string, replicas ...Search) Result { 
c := make(chan Result)
searchReplica := func(int i) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}

Для кожної копії (replica) функція пошуку запускає окрему горутину. Кожна з горутин відправляє свої пошукові результати в канал результатів. Повертається перше значення з каналу.

А що з результатами від інших горутин? І що щодо них самих?

У функції
First()
канал результатів не буферізованние. Це означає, що повертається тільки перша горутина. Всі інші застряють в спробі відправити свої результати. Виходить, що якщо у вас більше однієї копії (replica), то при кожному виклику відбувається витік ресурсів.

Щоб цього уникнути, все горутины повинні завершитися (exit). Одне з можливих рішень: використовувати досить великий буферизований канал результатів, здатний вмістити всі результати.

func First(query string, replicas ...Search) Result { 
c := make(chan Result,len(replicas))
searchReplica := func(int i) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}

Інше рішення: використовувати вираз
select
зі сценарієм (case)
default
і буферизований канал на одне значення. Сценарій
default
дозволяє бути впевненим, що горутина не застрягла, навіть якщо канал результатів не може приймати повідомлення.

func First(query string, replicas ...Search) Result { 
c := make(chan Result,1)
searchReplica := func(int i) {
select {
case c <- replicas[i](query):
default:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}

Також можна використовувати спеціальний канал скасування (special cancellation channel) для переривання робочих горутин.

func First(query string, replicas ...Search) Result { 
c := make(chan Result)
done := make(chan struct{})
defer close(done)
searchReplica := func(int i) {
select {
case c <- replicas[i](query):
case <- done:
}
}
for i := range replicas {
go searchReplica(i)
}

return <-c
}

Чому в презентації є такі баги? Роб Пайк просто не хотів ускладнювати слайди (slides) своєї презентації. Таке пояснення має сенс, але це може бути проблемою для новачків, які використовують код, не думаючи про ймовірні проблеми.

52. Застосування методів, що приймають значення з посиланням (pointer receiver), до екземплярів значень
Поки значення адресуються (addressable), до нього можна застосовувати метод, що приймає значення по посиланню. Іншими словами, в деяких випадках вам не потрібно мати версію методу, що приймає параметр за значенням.

Але не кожна змінна адресуема. Елементи хеш-таблиці (map) неадресуемы. Змінні, на які посилаються через інтерфейси, теж неадресуемы.

package main

import "fmt"

data type struct { 
string name
}

func (p *data) print() { 
fmt.Println("name:",p.name)
}

type printer interface { 
print()
}

func main() { 
d1 := data{"one"}
d1.print() //ok

var in printer = data{"two"} // помилка
in.print()

m := map[string]data {"x":data{"three"}}
m["x"].print() //помилка
}

Помилки компілювання:

/tmp/sandbox017696142/main.go:21: cannot use data literal (data type) as type printer in assignment: data does not implement printer (print method has pointer receiver)
/tmp/sandbox017696142/main.go:25: cannot call pointer method on m["x"] /tmp/sandbox017696142/main.go:25: cannot take the address of m["x"]

53. Оновлення полів значень хеш-таблиці
Якщо у вас є таблиця, що складається з структур, то ви не можете оновлювати окремі структурні поля.

Неправильно:

package main

data type struct { 
string name
}

func main() { 
m := map[string]data {"x":{"one"}}
m["x"].name = "two" // помилка
}

Помилка компілювання:

/tmp/sandbox380452744/main.go:9: cannot assign to m["x"].name

Це не працює, тому що елементи таблиці не адресуемы.

Новачків може додатково плутати те, що елементи слайсів — адресуемы.

package main

import "fmt"

data type struct { 
string name
}

func main() { 
s := []data {{"one"}}
s[0].name = "two" // ok
fmt.Println(s) // prints: [{two}]
}

Зверніть увагу, що коли-то в одному з компіляторів (gccgo) можна було оновлювати поля елементів таблиці. Але це швидко пофиксили :-) Також вважалося, що така можливість з'явиться в Go 1.3. Але в той час це було не так важливо, так що фіча все ще висить у списку todo.

Перше обхідні рішення: використовувати тимчасову змінну
.package main


import "fmt"

data type struct { 
string name
}

func main() { 
m := map[string]data {"x":{"one"}}
r := m["x"]
r.name = "two"
m["x"] = r
fmt.Printf("%v",m) //виводить: map[x:{two}]
}

Другий спосіб рішення: використовувати хеш-таблицю з покажчиками.

package main

import "fmt"

data type struct { 
string name
}

func main() { 
m := map[string]*data {"x":{"one"}}
m["x"].name = "two" //ok
fmt.Println(m["x"]) //виводить: &{two}
}

До речі, що буде, якщо виконати цей код?

package main

data type struct { 
string name
}

func main() { 
m := map[string]*data {"x":{"one"}}
m["z"].name = "what?" //???
}

54. nil-інтерфейси та nil-інтерфейсні значення
Це друга за поширеністю пастка Go. Інтерфейси — не покажчики, навіть якщо вони так виглядають. Інтерфейсні змінні будуть
nil
тільки тоді, коли їх типи і поля значень будуть
nil
.

Інтерфейсний тип і значення поля заповнюються на основі типу та значення змінної, використаної для створення відповідної інтерфейсної змінної. Якщо ви спробуєте перевірити, еквівалентна мінлива
nil
, то це може призвести до непередбачуваного поведінки.

package main

import "fmt"

func main() { 
var data *byte
var in interface{}

fmt.Println(data,data == nil) // виводить: <nil> true
fmt.Println(in,in == nil) // виводить: <nil> true

in = data
fmt.Println(in,in == nil) // виводить: <nil> false
//'data' є 'nil', але 'in' — 'nil'
}

Остерігайтеся цієї пастки, коли у вас є функція, що повертає інтерфейси.

Некоректно:

package main

import "fmt"

func main() { 
doit := func(arg int) interface{} {
var result *struct{} = nil

if(arg > 0) {
result = &struct{}{}
}

return result
}

if res := doit(-1); res != nil {
fmt.Println("good result:",res) // виводить: good result: <nil>
// 'res' не є 'nil', але його значення — 'nil'
}
}

Правильно:

package main

import "fmt"

func main() { 
doit := func(arg int) interface{} {
var result *struct{} = nil

if(arg > 0) {
result = &struct{}{}
} else {
return nil // повертає явний 'nil'
}

return result
}

if res := doit(-1); res != nil {
fmt.Println("good result:",res)
} else {
fmt.Println("bad result (res is nil)") // тут — як і очікувалося
}
}

55. Змінні стека і купи
Не завжди відомо, чи є змінна в стеці або купі. Якщо у З++ створити змінну за допомогою оператора
new
, то вона завжди буде в купі. В Go місце розміщення змінної вибирає компілятор, навіть якщо використовуються функції
new()
або
make()
. Компілятор робить вибір на підставі розміру і результату «аналізу локальності» (escape analysis). Це також означає, що можна повертати посилання на локальні змінні, що неприпустимо в інших мовах, наприклад в С і С++.

Якщо ви хочете знати, де знаходяться змінні, то передайте gc flag
m
на
go build
або
go run
(наприклад,
go run -gcflags -m app.go
).

56. GOMAXPROCS, узгодженість (concurrency) і паралелізм
Go 1.4 і нижче використовують тільки один тред контексту виконання / ОС. Це означає, що в кожен момент часу може виконуватися лише одна горутина. Починаючи з Go 1.5 кількість контекстів виконання стало дорівнює кількості логічних процесорних ядер, повернутого
runtime.NumCPU()
. Воно може не збігатися із загальною кількістю логічних ядер в системі, в залежності від налаштувань прив'язки CPU для процесу. Кількість можна налаштувати, змінивши змінну середовища
GOMAXPROCS
або викликавши функцію
runtime.GOMAXPROCS()
.

Існує поширена помилка, що
GOMAXPROCS
представляє собою кількість процесорів, які Go буде використовувати для запуску горутин. Документація до функції
runtime.GOMAXPROCS()
тільки додає плутанини. Але в описі до змінної
GOMAXPROCS
(https://golang.org/pkg/runtime/) йдеться саме про тредах ОС.

Значення
GOMAXPROCS
може перевищувати кількість ваших процесорів, верхня межа — 256.

package main

import ( 
"fmt"
"runtime"
)

func main() { 
fmt.Println(runtime.GOMAXPROCS(-1)) // виводить: X (1 on play.golang.org)
fmt.Println(runtime.NumCPU()) // виводить: X (1 on play.golang.org)
runtime.GOMAXPROCS(20)
fmt.Println(runtime.GOMAXPROCS(-1)) // виводить: 20
runtime.GOMAXPROCS(300)
fmt.Println(runtime.GOMAXPROCS(-1)) // виводить: 256
}

57. Зміна порядку операцій читання і запису
Go може змінювати порядок деяких операцій, але загальна поведінка всередині горутины, де це відбувається, не змінюється. Проте сказане не відноситься до порядку виконання самих горутин.

package main

import ( 
"runtime"
"time"
)

var _ = runtime.GOMAXPROCS(3)

var a, int b

func u1() { 
a = 1
b = 2
}

func u2() { 
a = 3
b = 4
}

func p() { 
println(a)
println(b)
}

func main() { 
go u1()
go u2()
go p()
time.Sleep(1 * time.Second)
}

Якщо запустити цей код кілька разів, то можна побачити такі комбінації змінних
a
та
b
:

1 
2

3 
4

0 
2

0 
0

1 
4

Сама цікава комбінація — 02 — говорить про те, що
b
була оновлена раніше
a
.

Якщо потрібно зберегти порядок операцій читання і запису серед декількох горутин, то використовуйте канали або відповідні конструкції з пакету sync.

58. Диспетчеризація за пріоритетами (Preemptive Scheduling)
Можуть з'являтися розбійницькі (rogue) горутины, не дають іншим горутинам виконуватися. Таке трапляється, якщо у вас є цикл
for
, що не дозволяє запустити диспетчер.

package main

import "fmt"

func main() { 
done := false

go func(){
done = true
}()

for !done {
}
fmt.Println("done!")
}

Цикл
for
не повинен бути порожнім. Проблема не зникне, поки в циклі міститься код, не запускає виконання диспетчера.

Він запускається після збирання сміття, виразів
go
, операцій блокування каналів, які блокують системних викликів і операцій блокування. Також він може працювати, коли викликана невстроенная (non-inlined) функція.

package main

import "fmt"

func main() { 
done := false

go func(){
done = true
}()

for !done {
fmt.Println("not done!") // не вбудована
}
fmt.Println("done!")
}

Щоб дізнатися, вбудована чи викликається вами в циклі функція, передайте gc flag
m
на
go build
або
go run
(наприклад,
go build -gcflags -m
).

Інше рішення: явно викликати диспетчер. Це можна зробити за допомогою функції
Gosched()
з пакету runtime.

package main

import ( 
"fmt"
"runtime"
)

func main() { 
done := false

go func(){
done = true
}()

for !done {
runtime.Gosched()
}
fmt.Println("done!")
}

Якщо ви дочитали до кінця і у вас є коментарі або ідеї, ласкаво просимо в дискусію на Reddit (і коментарі тут, на Хабре. — Приміт. пер.).
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.