[Перевод] 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)
В большинстве других языков, использующих фигурные скобки, вам нужно выбирать, где их размещать. 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 {
result int
}
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-name data.result on left side of :=
Хотя разработчикам Go уже предлагали это исправить, не стоит надеяться на перемены: Робу Пайку нравится всё «как есть». Вам помогут временные переменные. Или предварительно объявляйте все свои переменные и используйте стандартный оператор присваивания.
Правильно:
package main
import (
"fmt"
)
type info struct {
result int
}
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 string and 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","c"}
for v := range x {
fmt.Println(v) // выводит 0, 1, 2
}
}
Правильно:
package main
import "fmt"
func main() {
x := []string{"a","b","c"}
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]
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]]
}
Предлагается разработать спецификацию на многомерные массивы и слайсы, но сейчас, судя по всему, у этой задачи низкий приоритет.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"]; !ok {
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
}
Стоит заметить, что это неправильный способ обновления символов в текстовой строке, потому что символ может состоять из нескольких байтов. В этом случае лучше конвертировать строку в слайс из «рун» (rune). Но даже внутри слайсов из «рун» одиночный символ может быть разбит на несколько рун, например если есть символ апострофа (grave accent). Такая непростая и запутанная природа «символов» является причиной того, что в Go строковые значения представляют собой последовательностей байтов. 17. Преобразование строк в байт-слайсы (Byte Slices), и наоборот
Когда вы преобразуете строку в байт-слайс (и наоборот), вы получаете полную копию исходных данных. Это не операция приведения (cast operation), как в других языках, и не перенарезка (reslicing), когда переменная нового слайса указывает на один и тот же массив, занятый исходным байт-слайсом.
В Go есть несколько оптимизаций для преобразований из []byte
в string
и из string
в []byte
, позволяющих избегать дополнительных выделений памяти (ещё больше оптимизаций в списке todo).
Первая оптимизация позволяет избежать дополнительного выделения памяти, когда ключи []byte
используются для поиска записей в коллекциях map[string]: m[string(key)]
.
Вторая оптимизация позволяет избегать дополнительного выделения в выражениях for range
, когда строки преобразуются в []byte: for i,v := range []byte(str) {...}
.
Оператор индекса, применяемый к строке, возвращает байтовое значение (byte value), а не символ (как в других языках).
package main
import "fmt"
func main() {
x := "text"
fmt.Println(x[0]) // выводит 116
fmt.Printf("%T",x[0]) // выводит uint8
}
Если нужно обратиться к конкретным «символам» (кодовым точкам/рунам Unicode), то используйте выражение
for range
. Также вам будут полезны официальный пакет 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 in composite 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).Выражения for range
со строковыми переменными пытаются интерпретировать данные как текст в кодировке 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(idx int) {
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(idx int) {
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(idx int) {
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"
type data struct {
num int
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-соединени