предисловие
Go — простой и интересный язык программирования, как и другие языки, он неизбежно столкнется со многими подводными камнями при использовании, но большинство из них не являются недостатками дизайна самого Go. Если вы только что перешли на Go с другого языка, то большинство ям в этой статье будут пройдены.
Если вы потратите время на изучение официального документа, вики,список рассылки обсуждений,Rob PikeБольшое количество статей и исходный код Go, вы обнаружите, что ямы в этой статье очень распространены, и новичок, пропуская эти ямы, может сэкономить много времени на отладке кода.
Начинающие: 1-34
1. Левая скоба{
Обычно не в одну строку
В большинстве других языков{
Место зависит от вас. Go является особенным и следует правилу автоматической инъекции точки с запятой: компилятор добавит определенный разделитель в конце каждой строки кода.;
для разделения нескольких операторов, например, в)
Добавьте точку с запятой после:
// 错误示例
func main()
{
println("hello world")
}
// 等效于
func main(); // 无函数体
{
println("hello world")
}
./main.go: missing function body ./main.go: syntax error: unexpected semicolon or newline before {
// 正确示例
func main() {
println("hello world")
}
Обратите внимание на особые случаи, такие как блоки кода:
// { 并不遵守分号注入规则,不会在其后边自动加分,此时可换行
func main() {
{
println("hello world")
}
}
Ссылаться на:Специальный разделитель для автоматической точки с запятой в Golang
2. Неиспользуемые переменные
Если в коде тела функции есть неиспользуемые переменные, она не скомпилируется, но глобальные переменные, объявленные, но не используемые, в порядке.
Даже если переменная объявлена и ей присвоено значение, она все равно не скомпилируется, ее нужно где-то использовать:
// 错误示例
var gvar int // 全局变量,声明不使用也可以
func main() {
var one int // error: one declared and not used
two := 2 // error: two declared and not used
var three int // error: three declared and not used
three = 3
}
// 正确示例
// 可以直接注释或移除未使用的变量
func main() {
var one int
_ = one
two := 2
println(two)
var three int
one = three
var four int
four = four
}
3. Неиспользованный импорт
Если вы импортируете пакет, но ни одна из переменных, функций, интерфейсов и структур пакета не используется, компиляция завершится ошибкой.
можно использовать_
Символ подчеркивания используется как псевдоним для игнорирования импортированных пакетов, что позволяет избежать ошибок компиляции.init()
// 错误示例
import (
"fmt" // imported and not used: "fmt"
"log" // imported and not used: "log"
"time" // imported and not used: "time"
)
func main() {
}
// 正确示例
// 可以使用 goimports 工具来注释或移除未使用到的包
import (
_ "fmt"
"log"
"time"
)
func main() {
_ = log.Println
_ = time.Now
}
4. Коротко объявленные переменные можно использовать только внутри функций
// 错误示例
myvar := 1 // syntax error: non-declaration statement outside function body
func main() {
}
// 正确示例
var myvar = 1
func main() {
}
5. Используйте короткие объявления, чтобы повторять объявления переменных
Невозможно повторить объявление переменной только с коротким объявлением,:=
Слева есть по крайней мере одна новая переменная, позволяющая повторять объявления нескольких переменных:
// 错误示例
func main() {
one := 0
one := 1 // error: no new variables on left side of :=
}
// 正确示例
func main() {
one := 0
one, two := 1, 2 // two 是新变量,允许 one 的重复声明。比如 error 处理经常用同名变量 err
one, two = two, one // 交换两个变量值的简写
}
6. Вы не можете использовать короткое объявление для установки значения поля
Невозможно использовать переменные поля структуры:=
чтобы назначить использование предопределенной переменной, чтобы избежать разрешения:
// 错误示例
type info struct {
result int
}
func work() (int, error) {
return 3, nil
}
func main() {
var data info
data.result, err := work() // error: non-name data.result on left side of :=
fmt.Printf("info: %+v\n", data)
}
// 正确示例
func main() {
var data info
var err error // err 需要预声明
data.result, err = work()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("info: %+v\n", data)
}
7. Случайное переопределение переменной
Для разработчиков, переключающихся с динамических языков, хорошо работают короткие объявления, которые могут ввести в заблуждение.:=
является оператором присваивания.
Если вы неправильно используете его в новом блоке кода, как показано ниже:=
, компилируется без ошибок, но переменные работают не так, как вы ожидаете:
func main() {
x := 1
println(x) // 1
{
println(x) // 1
x := 2
println(x) // 2 // 新的 x 变量的作用域只在代码块内部
}
println(x) // 1
}
Это распространенная ошибка разработчиков Go, и ее нелегко обнаружить.
быть пригодным для использованияvetИнструменты для диагностики такого покрытия переменных, Go не выполняет проверку покрытия по умолчанию, добавьте-shadow
возможность включить:
> go tool vet -shadow main.go
main.go:9: declaration of "x" shadows declaration at main.go:5
Обратите внимание, что vet не будет сообщать обо всех переопределенных переменных, вы можете использоватьgo-nyetДля дальнейшего тестирования:
> $GOPATH/bin/go-nyet main.go
main.go:10:3:Shadowing variable `x`
8. Переменные явного типа нельзя инициализировать nil
nil
является инициализатором по умолчанию для переменных типа интерфейса, функции, указателя, карты, среза и канала. Однако без указания типа при объявлении компилятор не может вывести конкретный тип переменной.
// 错误示例
func main() {
var x = nil // error: use of untyped nil
_ = x
}
// 正确示例
func main() {
var x interface{} = nil
_ = x
}
9. Используйте срез и карту напрямую с нулевым значением
Позволяет добавлять элементы в срез с нулевым значением, но добавление элемента в карту с нулевым значением вызовет панику во время выполнения.
// map 错误示例
func main() {
var m map[string]int
m["one"] = 1 // error: panic: assignment to entry in nil map
// m := make(map[string]int)// map 的正确声明,分配了实际的内存
}
// slice 正确示例
func main() {
var s []int
s = append(s, 1)
}
10. емкость карты
Емкость можно указать при создании переменной типа map, но ее нельзя использовать как срезcap()
Чтобы определить размер выделенного пространства:
// 错误示例
func main() {
m := make(map[string]int, 99)
println(cap(m)) // error: invalid argument m1 (type map[string]int) for cap
}
11. Значение переменной типа string не может быть нулевым
для тех, кто любит использоватьnil
Для тех, кто инициализирует строки, это яма:
// 错误示例
func main() {
var s string = nil // cannot use nil as type string in assignment
if s == nil { // invalid operation: s == nil (mismatched types string and nil)
s = "default"
}
}
// 正确示例
func main() {
var s string // 字符串类型的零值是空串 ""
if s == "" {
s = "default"
}
}
12. Значение типа массива как параметр функции
В C/C++ массивы (имена) являются указателями. При передаче массива в качестве параметра в функцию это эквивалентно передаче ссылки на адрес памяти массива, а значение массива будет изменено внутри функции.
В Go массивы — это значения. При передаче в функцию в качестве параметра передается копия исходного значения массива, и в это время массив не может быть обновлен внутри функции:
// 数组使用值拷贝传参
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]
}
Если вы хотите изменить массив параметров:
- Передайте тип указателя непосредственно в этот массив:
// 传址会修改原数据
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]
}
- Использовать срез напрямую: даже если функция получает копию значения среза, она все равно обновляет исходные данные среза (базовый массив).
// 会修改 slice 的底层 array,从而修改 slice
func main() {
x := []int{1, 2, 3}
func(arr []int) {
arr[0] = 7
fmt.Println(x) // [7 2 3]
}(x)
fmt.Println(x) // [7 2 3]
}
13. Диапазон пересекает срез и массив, запутывая возвращаемое значение
с другими языками программированияfor-in
,foreach
Операторы обхода в Go разные.range
При обходе генерируются 2 значения, первое — это индекс элемента, а второе — значение элемента:
// 错误示例
func main() {
x := []string{"a", "b", "c"}
for v := range x {
fmt.Println(v) // 1 2 3
}
}
// 正确示例
func main() {
x := []string{"a", "b", "c"}
for _, v := range x { // 使用 _ 丢弃索引
fmt.Println(v)
}
}
14. Срезы и массивы на самом деле являются одномерными данными
Похоже, что Go поддерживает многомерные массивы и слайсы, вы можете создавать массивы из массивов, слайсы из слайсов, но это не так.
Для приложений, которые полагаются на динамические вычисления значений многомерных массивов, реализация в Go не идеальна с точки зрения производительности и сложности.
Динамические многомерные массивы могут быть созданы с использованием примитивных одномерных массивов, «независимых» срезов и срезов «общего базового массива».
-
Используйте исходный 1D-массив: выполняйте проверки индексов, обнаружение переполнения и перераспределение при добавлении значений, когда массив заполнен.
-
Использование «независимых» срезов выполняется в два этапа:
-
Создать внешний срез
-
выделение памяти для каждого внутреннего слайса
Обратите внимание, что внутренние срезы не зависят друг от друга, так что любой внутренний срез не повлияет на другие срезы.
-
// 使用各自独立的 6 个 slice 来创建 [2][3] 的动态多维数组
func main() {
x := 2
y := 4
table := make([][]int, x)
for i := range table {
table[i] = make([]int, y)
}
}
- Ломтики «Общий базовый массив»
-
Создайте фрагмент контейнера, содержащий необработанные данные
-
создать другие фрагменты
-
Вырежьте исходный фрагмент, чтобы инициализировать другие фрагменты
func main() {
h, w := 2, 4
raw := make([]int, h*w)
for i := range raw {
raw[i] = i
}
// 初始化原始 slice
fmt.Println(raw, &raw[4]) // [0 1 2 3 4 5 6 7] 0xc420012120
table := make([][]int, h)
for i := range table {
// 等间距切割原始 slice,创建动态多维数组 table
// 0: raw[0*4: 0*4 + 4]
// 1: raw[1*4: 1*4 + 4]
table[i] = raw[i*w : i*w + w]
}
fmt.Println(table, &table[1][0]) // [[0 1 2 3] [4 5 6 7]] 0xc420012120
}
Дополнительные ссылки на многомерные массивы
go-how-is-two-dimensional-arrays-memory-representation
what-is-a-concise-way-to-create-a-2d-slice-in-go
15. Доступ к ключу, которого нет на карте
Как и в других языках программирования, ожидается, что он вернет nil, если будет осуществлен доступ к ключу, которого нет в карте, например, в PHP:
> php -r '$v = ["x"=>1, "y"=>2]; @var_dump($v["z"]);'
NULL
Go вернет нулевое значение соответствующего типа данных элемента, напримерnil
,''
,false
И 0, операция значения всегда возвращает значение, поэтому невозможно судить о том, находится ли ключ в карте по полученному значению.
Проверьте, существует ли ключ и доступен ли он напрямую с помощью карты, просто проверьте второй возвращаемый параметр:
// 错误的 key 检测方式
func main() {
x := map[string]string{"one": "2", "two": "", "three": "3"}
if v := x["two"]; v == "" {
fmt.Println("key two is no entry") // 键 two 存不存在都会返回的空字符串
}
}
// 正确示例
func main() {
x := map[string]string{"one": "2", "two": "", "three": "3"}
if _, ok := x["two"]; !ok {
fmt.Println("key two is no entry")
}
}
16. Значение типа string является постоянным и не может быть изменено
Попытка пройти по строке с использованием индекса для обновления отдельных символов в строке не допускается.
Значение типа string представляет собой двоичный фрагмент байта, доступный только для чтения.Если вы действительно хотите изменить символы в строке, преобразуйте строку в []byte и измените ее, а затем преобразуйте в строку:
// 修改字符串的错误示例
func main() {
x := "text"
x[0] = "T" // error: cannot assign to x[0]
fmt.Println(x)
}
// 修改示例
func main() {
x := "text"
xBytes := []byte(x)
xBytes[0] = 'T' // 注意此时的 T 是 rune 类型
x = string(xBytes)
fmt.Println(x) // Text
}
Уведомление:Вышеприведенный пример не является правильной позицией для обновления строки, потому что символ в кодировке UTF8 может занимать несколько байтов, например, китайские символы, для хранения которых требуется 3–4 байта, и неправильно обновлять один из байтов в этот момент. время. .
Правильная поза для обновления строки: преобразовать строку в фрагмент руны (одна руна в это время может занимать несколько байтов) и напрямую обновить символы в руне.
func main() {
x := "text"
xRunes := []rune(x)
xRunes[0] = '我'
x = string(xRunes)
fmt.Println(x) // 我ext
}
17. Преобразование между строкой и байтовым срезом
При преобразовании между строками и фрагментами байтов в преобразовании участвует исходное значение копии. Этот процесс преобразования отличается от операций приведения типов в других языках программирования, а также отличается тем, что новый слайс разделяет базовый массив со старым слайсом.
Go оптимизирует два момента в преобразовании между строкой и байтовым срезом, избегая дополнительного выделения памяти:
- существует
map[string]
При взгляде на ключ в соответствующем[]byte
, избегать делатьm[string(key)]
выделение памяти - использовать
for range
Перебрать итерируемую строку, преобразованную в []byte:for i,v := range []byte(str) {...}
туман:обратитесь к исходному тексту
18. Строковые и индексные операторы
Индексированный доступ к строке возвращает не символ, а значение байта.
Это обрабатывается так же, как и в других языках, таких как PHP:
> php -r '$name="中文"; var_dump($name);' # "中文" 占用 6 个字节
string(6) "中文"
> php -r '$name="中文"; var_dump($name[0]);' # 把第一个字节当做 Unicode 字符读取,显示 U+FFFD
string(1) "�"
> php -r '$name="中文"; var_dump($name[0].$name[1].$name[2]);'
string(3) "中"
func main() {
x := "ascii"
fmt.Println(x[0]) // 97
fmt.Printf("%T\n", x[0])// uint8
}
При необходимости используйтеfor range
Итеративный доступ к символам в строке (кодовая точка Unicode/руна), которая доступна в стандартной библиотеке."unicode/utf8"
пакет для декодирования и кодирования, связанного с UTF8. Кроме тогоutf8stringтакже есть лайкfunc (s *String) At(i int) rune
и другие очень удобные библиотечные функции.
19. Строки — это не весь текст UTF8
Значение строки не обязательно должно быть текстом UTF8 и может содержать произвольные значения. Строки являются текстом UTF8, только если они являются буквальными литералами, и строки могут быть экранированы, чтобы содержать другие данные.
Чтобы определить, является ли строка текстом UTF8, используйте пакет «unicode/utf8» вValidString()
функция:
func main() {
str1 := "ABC"
fmt.Println(utf8.ValidString(str1)) // true
str2 := "A\xfeC"
fmt.Println(utf8.ValidString(str2)) // false
str3 := "A\\xfeC"
fmt.Println(utf8.ValidString(str3)) // true // 把转义字符转义成字面值
}
20. Длина строки
В Питоне:
data = u'♥'
print(len(data)) # 1
Однако в Go:
func main() {
char := "♥"
fmt.Println(len(char)) // 3
}
Встроенные функции Golen()
Возвращает количество байтов в строке, а не количество символов Unicode, как в Python.
Чтобы получить количество символов в строке, используйте пакет «unicode/utf8».RuneCountInString(str string) (n int)
func main() {
char := "♥"
fmt.Println(utf8.RuneCountInString(char)) // 1
}
Уведомление: RuneCountInString
не всегда возвращает количество символов, которые мы видим, потому что некоторые символы занимают 2 руны:
func main() {
char := "é"
fmt.Println(len(char)) // 3
fmt.Println(utf8.RuneCountInString(char)) // 2
fmt.Println("cafe\u0301") // café // 法文的 cafe,实际上是两个 rune 的组合
}
Ссылаться на:normalization
21. Отсутствует в многострочном массиве, срезе, операторе карты,
Нет
func main() {
x := []int {
1,
2 // syntax error: unexpected newline, expecting comma or }
}
y := []int{1,2,}
z := []int{1,2}
// ...
}
в заявлении}
После сворачивания в одну строку завершающий,
не обязательно.
22. log.Fatal
иlog.Panic
не просто войти
Стандартная библиотека LOG предоставляет различные уровни ведения журналов, различные библиотеки журналов на других языках, а также называется пакет LOG для Go.Fatal*()
,Panic*()
может делать больше вещей вне лога, например прерывать выполнение программы и т.д.:
func main() {
log.Fatal("Fatal level log: log entry") // 输出信息后,程序终止执行
log.Println("Nomal level log: log entry")
}
23. Операции над встроенными структурами данных не синхронны
Хотя сам Go имеет большое количество функций для поддержки параллелизма, он не гарантирует безопасность параллельных данных.Пользователи должны убедиться, что такие данные, как переменные, обновляются с помощью атомарных операций.
горутины и каналы — отличные способы выполнять атомарные операции или использовать блокировки из пакета «sync».
24. Диапазон повторяет значение строки
Индекс, полученный по диапазону, представляет собой позицию первого байта значения символа (точка/руна Юникода), в отличие от других языков программирования, этот индекс не является непосредственно позицией символа в строке.
Обратите внимание, что персонаж может занимать более одной руны, например, é во французском слове cafe. Можно использовать специальные символы операцийnormСумка.
Итерация диапазона for попытается преобразовать строку в текст UTF8, напрямую используя суррогатный символ UNicode 0XFFFD rune(�) для любых недопустимых кодовых точек. Если в строке есть какие-либо данные, отличные от UTF8, вы должны сохранить строку как байтовый срез, а затем работать.
func main() {
data := "A\xfe\x02\xff\x04"
for _, v := range data {
fmt.Printf("%#x ", v) // 0x41 0xfffd 0x2 0xfffd 0x4 // 错误
}
for _, v := range []byte(data) {
fmt.Printf("%#x ", v) // 0x41 0xfe 0x2 0xff 0x4 // 正确
}
}
25. Карта итерации диапазона
Если вы хотите перебирать карту в определенном порядке (например, по ключу), имейте в виду, что каждая итерация может давать разные результаты.
Среда выполнения Go намеренно перемешивает порядок итераций, поэтому вы можете получить противоречивые результаты итераций. Но это не всегда нарушается, также возможно получить одинаковые результаты 5 итераций подряд, например:
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Запустите приведенный выше код несколько раз, вывод не изменится, он будет перекомпилирован только в том случае, если вы обновите код. Порядок итераций нарушается после перекомпиляции:
26. Оператор fallthrough в switch
switch
в предложенииcase
Блоки кода будут иметь разрыв по умолчанию, но вы можете использоватьfallthrough
для принудительного выполнения следующего блока case.
func main() {
isSpace := func(char byte) bool {
switch char {
case ' ': // 空格符会直接 break,返回 false // 和其他语言不一样
// fallthrough // 返回 true
case '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) // true
fmt.Println(isSpace(' ')) // false
}
Но вы можете использовать в конце блока casefallthrough
, чтобы принудительно выполнить следующий блок кода case.
Вы также можете переписать прецедент как решение с несколькими условиями:
func main() {
isSpace := func(char byte) bool {
switch char {
case ' ', '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) // true
fmt.Println(isSpace(' ')) // true
}
27. Операции увеличения и уменьшения
Многие языки программирования поставляются с pre и post.++
,--
операция. Но Go уникален, убирая предварительную операцию, и в то же время++
,—
Только как оператор, а не как выражение.
// 错误示例
func main() {
data := []int{1, 2, 3}
i := 0
++i // syntax error: unexpected ++, expecting }
fmt.Println(data[i++]) // syntax error: unexpected ++, expecting :
}
// 正确示例
func main() {
data := []int{1, 2, 3}
i := 0
i++
fmt.Println(data[i]) // 2
}
28. Побитовое отрицание
Многие языки программирования используют~
В качестве унарного оператора побитового отрицания (NOT) Go повторно использует^
Оператор XOR для побитового инвертирования:
// 错误的取反操作
func main() {
fmt.Println(~2) // bitwise complement operator is ^
}
// 正确示例
func main() {
var d uint8 = 2
fmt.Printf("%08b\n", d) // 00000010
fmt.Printf("%08b\n", ^d) // 11111101
}
в то же время^
Также оператор побитового исключающего ИЛИ (XOR).
Оператор можно использовать дважды из-за унарной операции НЕ.NOT 0x02
, с бинарной операцией XOR0x22 XOR 0xff
согласуется.
В Go также есть специальный оператор AND NOT&^
оператора, возьмите только 1 для разных битов.
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))
}
10000010 [A]
00000010 [B]
11111101 (NOT B)
00000010 ^ 11111111 = 11111101 [B XOR 0xff]
10000010 ^ 00000010 = 10000000 [A XOR B]
10000010 & 00000010 = 00000010 [A AND B]
10000010 &^00000010 = 10000000 [A 'AND NOT' B]
10000010&(^00000010)= 10000000 [A AND (NOT B)]
29. Приоритет оператора
В дополнение к оператору очистки битов в Go есть много таких же битовых операторов, как и в других языках, но с другим приоритетом.
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
}
Список приоритетов:
Precedence Operator
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||
30. Неэкспортированные поля структуры не могут быть закодированы
Доступ к элементам поля, начинающимся со строчной буквы, невозможен извне, поэтомуstruct
При выполнении операций кодирования в json, xml, gob и других форматах эти приватные поля будут игнорироваться, а при экспорте будут получены нулевые значения:
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} // 私有字段 two 被忽略了
var out MyData
json.Unmarshal(encoded, &out)
fmt.Printf("%#v\n", out) // main.MyData{One:1, two:""}
}
31. При выходе из программы выполняются горутины.
По умолчанию программа не ждет выполнения всех горутин перед выходом, что требует особого внимания:
// 主程序会直接退出
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) // 模拟 goroutine 正在执行
fmt.Printf("[%v] is done\n", workerID)
}
следующее,main()
Основная программа завершает работу, не дожидаясь завершения выполнения двух горутин:
Общее решение: Используйте переменную «WaitGrup», она будет ждать, пока все основные программы Goroutine заканчиваются до выхода.
Если вашим горутинам необходимо выполнять трудоемкие операции, такие как зацикливание сообщений, вы можете отправить имkill
сообщение, чтобы закрыть их. Или просто закройте канал, где они оба ожидают получения данных:
// 等待所有 goroutine 执行完毕
// 进入死锁
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)
}
Результаты:
Похоже, все горутины выполнены, но сообщается об ошибке:
fatal error: all goroutines are asleep - deadlock!
Почему возникает взаимоблокировка? Горутина вызывается перед выходомwg.Done()
, программа должна завершиться нормально.
Причина в том, что переменная «WaitGroup», которую получает горутина,var wg WaitGroup
Копия значения , т.е.doIt()
Передавать параметры только по значению. Поэтому, даже если он вызывается в каждой горутинеwg.Done()
, в основной программеwg
Переменные не затрагиваются.
// 等待所有 goroutine 执行完毕
// 使用传址方式为 WaitGroup 变量传参
// 使用 channel 关闭 goroutine
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
ch := make(chan interface{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doIt(i, ch, done, &wg) // wg 传指针,doIt() 内部会改变 wg 的值
}
for i := 0; i < workerCount; i++ { // 向 ch 中发送数据,关闭 goroutine
ch <- i
}
close(done)
wg.Wait()
close(ch)
fmt.Println("all done!")
}
func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
fmt.Printf("[%v] is running\n", workerID)
defer wg.Done()
for {
select {
case m := <-ch:
fmt.Printf("[%v] m => %v\n", workerID, m)
case <-done:
fmt.Printf("[%v] is done\n", workerID)
return
}
}
}
текущий результат:
32. Отправить данные на небуферизованный канал и вернуться, как только приемник будет готов
Отправитель блокирует только тогда, когда данные обрабатываются получателем. В зависимости от операционной среды после того, как отправитель отправил данные, горутине получателя может не хватить времени для обработки следующих данных. как:
func main() {
ch := make(chan string)
go func() {
for m := range ch {
fmt.Println("Processed:", m)
time.Sleep(1 * time.Second) // 模拟需要长时间运行的操作
}
}()
ch <- "cmd.1"
ch <- "cmd.2" // 不会被接收处理
}
текущий результат:
33. Отправка данных в закрытый канал вызовет панику
Безопасно получать данные из закрытого канала:
получить значение статусаok
даfalse
Когда это указывает на то, что в канале больше нет данных для приема. Точно так же, когда данные получены из буферизованного канала, когда буферизованные данные получены, а доступных данных больше нет, значение состояния такжеfalse
Отправка данных в закрытый канал вызывает панику:
func main() {
ch := make(chan int)
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- idx
}(i)
}
fmt.Println(<-ch) // 输出第一个发送的值
close(ch) // 不能关闭,还有其他的 sender
time.Sleep(2 * time.Second) // 模拟做其他的操作
}
результат операции:
Для ошибки в приведенном выше примере используйте устаревший каналdone
сообщить оставшимся горутинам, чтобы они прекратили отправку данных в ch. В настоящее время<- done
Результат{}
:
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, "Send result")
case <-done:
fmt.Println(idx, "Exiting")
}
}(i)
}
fmt.Println("Result: ", <-ch)
close(done)
time.Sleep(3 * time.Second)
}
текущий результат:
34. Используйте значение какnil
канал
Отправка и получение данных по нулевому каналу будут заблокированы навсегда:
func main() {
var ch chan int // 未初始化,值为 nil
for i := 0; i < 3; i++ {
go func(i int) {
ch <- i
}(i)
}
fmt.Println("Result: ", <-ch)
time.Sleep(2 * time.Second)
}
Ошибка взаимоблокировки во время выполнения:
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive (nil chan)]
Воспользовавшись этой функцией взаимоблокировки, можно динамически открывать и закрывать блоки case при выборе:
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:
println("--------")
out = nil
in = inCh
case val = <-in:
println("++++++++++")
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)
}
текущий результат:
34. Если функция-приемник передается по значению, исходное значение параметра не может быть изменено
Параметры приемника метода аналогичны параметрам обычных функций: если они объявлены как значение, тело метода получает копию значения параметра, и любое изменение параметра в это время не повлияет на исходное значение.
Если параметр получателя не является переменной типа карты или среза, а поле в карте и элемент в срезе обновляются указателем, исходное значение будет обновлено:
type data struct {
num int
key *string
items map[string]bool
}
func (this *data) pointerFunc() {
this.num = 7
}
func (this data) valueFunc() {
this.num = 8
*this.key = "valueFunc.key"
this.items["valueFunc"] = true
}
func main() {
key := "key1"
d := data{1, &key, make(map[string]bool)}
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)
d.pointerFunc() // 修改 num 的值为 7
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)
d.valueFunc() // 修改 key 和 items 的值
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)
}
результат операции:
Средний: 35-50
35. Закрыть тело ответа HTTP
При использовании стандартной библиотеки HTTP для инициирования запроса и получения ответа, даже если вы не читаете никаких данных из ответа или ответ пуст, вам необходимо закрыть тело ответа вручную. Новички могут легко забыть закрыть вручную или написать не в том месте:
// 请求失败造成 panic
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close() // resp 可能为 nil,不能读取 Body
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
func checkError(err error) {
if err != nil{
log.Fatalln(err)
}
}
Приведенный выше код может правильно инициировать запрос, но как только запрос завершается ошибкой, переменнаяresp
значениеnil
, вызывая панику:
panic: runtime error: invalid memory address or nil pointer dereference
Следует сначала проверить ошибку ответа HTTP какnil
, затем позвонитеresp.Body.Close()
чтобы закрыть тело ответа:
// 大多数情况正确的示例
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
checkError(err)
defer resp.Body.Close() // 绝大多数情况下的正确关闭方式
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
вывод:
Get api.ipify.org?format=json: x509: certificate signed by unknown authority
Подавляющее большинство запросов терпит неудачу,resp
значениеnil
иerr
заnon-nil
. но если вы получите ошибку перенаправления, значение обоихnon-nil
, и в конечном итоге может произойти утечка памяти. 2 решения:
- Тело ответа, отличное от nil, можно закрыть непосредственно в блоке кода, обрабатывающем ошибки ответа HTTP.
- вызов вручную
defer
чтобы закрыть тело ответа:
// 正确示例
func main() {
resp, err := http.Get("http://www.baidu.com")
// 关闭 resp.Body 的正确姿势
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
resp.Body.Close()
Предыдущая реализация заключалась в том, чтобы считывать данные в теле ответа и отбрасывать их, гарантируя, что поддерживающие активность HTTP-соединения могут быть повторно использованы для обработки более чем одного запроса. Но последняя версия Go оставляет задачу чтения и удаления данных пользователю. Если вы не справитесь с этим, HTTP-соединение может быть закрыто напрямую вместо повторного использования. См. документацию версии Go 1.5.
Если ваша программа часто повторно использует постоянные соединения HTTP, вы можете включить следующее в код логики ответа:
_, err = io.Copy(ioutil.Discard, resp.Body) // 手动丢弃读取完毕的数据
Если вам нужно прочитать ответ полностью, необходимо написать приведенный выше код. Например, при декодировании данных ответа JSON API:
json.NewDecoder(resp.Body).Decode(&data)
36. Закройте HTTP-соединение
Некоторые поддерживают конфигурацию HTTP1.1 или HTTP1.0.connection: keep-alive
Сервер опции будет поддерживать длительное соединение в течение определенного периода времени. Но соединение стандартной библиотеки «net/http» по умолчанию отключается только тогда, когда сервер активно запрашивает закрытие, поэтому ваша программа может исчерпать дескрипторы сокета. Есть 2 решения после завершения запроса:
- напрямую установить переменную запроса
Close
значение поляtrue
, соединение будет активно закрываться после завершения каждого запроса. - Установить параметры заголовка запроса заголовка
Connection: close
, и тогда заголовок ответа, возвращаемый сервером, также будет иметь эту опцию, и стандартная библиотека HTTP будет активно отключена.
// 主动关闭连接
func main() {
req, err := http.NewRequest("GET", "http://golang.org", nil)
checkError(err)
req.Close = true
//req.Header.Add("Connection", "close") // 等效的关闭方式
resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
Вы можете создать настроенный пользователем транспортный клиент HTTP для отмены глобального мультиплексирования HTTP:
func main() {
tr := http.Transport{DisableKeepAlives: true}
client := http.Client{Transport: &tr}
resp, err := client.Get("https://golang.google.cn/")
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
fmt.Println(resp.StatusCode) // 200
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(len(string(body)))
}
Выберите сценарии использования в соответствии с вашими потребностями:
-
Если ваша программа будет делать много запросов к одному и тому же серверу, используйте keep-alive по умолчанию.
-
Если вашей программе необходимо подключиться к большому количеству серверов, и каждый сервер запрашивает только один или два раза, то закройте соединение сразу после получения запроса. или увеличить максимальное количество открытых файлов
fs.file-max
значение .
37. Декодировать числа в JSON для типов интерфейса
При кодировании/декодировании данных JSON Go по умолчанию обрабатывает значение как float64.Например, следующий код вызовет панику:
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}
fmt.Printf("%T\n", result["status"]) // float64
var status = result["status"].(int) // 类型断言错误
fmt.Println("Status value: ", status)
}
panic: interface conversion: interface {} is float64, not int
Если поле JSON, которое вы пытаетесь декодировать, является целым числом, вы можете:
-
Преобразование значений int в float для унифицированного использования
-
Преобразуйте значение float, необходимое после декодирования, в int для использования
// 将 decode 的值转为 int 使用
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}
var status = uint64(result["status"].(float64))
fmt.Println("Status value: ", status)
}
- использовать
Decoder
тип для декодирования данных JSON, явно указывающий тип значения поля
// 指定字段类型
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 {
log.Fatalln(err)
}
var status, _ = result["status"].(json.Number).Int64()
fmt.Println("Status value: ", status)
}
// 你可以使用 string 来存储数值数据,在 decode 时再决定按 int 还是 float 使用
// 将数据转为 decode 为 string
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 {
log.Fatalln(err)
}
var status uint64
err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status);
checkError(err)
fmt.Println("Status value: ", status)
}
- использоватьstruct
Тип сопоставляет необходимые данные с числовым типом
// struct 中指定字段类型
func main() {
var data = []byte(`{"status": 200}`)
var result struct {
Status uint64 `json:"status"`
}
err := json.NewDecoder(bytes.NewReader(data)).Decode(&result)
checkError(err)
fmt.Printf("Result: %+v", result)
}
-
можно использовать
struct
Сопоставьте числовые типы сjson.RawMessage
собственный тип данныхОн подходит для таких ситуаций, как если данные JSON не спешат декодировать или тип значения поля в JSON не фиксирован:
// 状态名称可能是 int 也可能是 string,指定为 json.RawMessage 类型
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"`
}
err := json.NewDecoder(bytes.NewReader(record)).Decode(&result)
checkError(err)
var name string
err = json.Unmarshal(result.Status, &name)
if err == nil {
result.StatusName = name
}
var code uint64
err = json.Unmarshal(result.Status, &code)
if err == nil {
result.StatusCode = code
}
fmt.Printf("[%v] result => %+v\n", idx, result)
}
}
38. Сравнение значений структуры, массива, среза и карты
Можно использовать оператор равенства==
для сравнения переменных структур при условии, что члены обеих структур относятся к сопоставимым типам:
type data struct {
num int
fp float32
complex complex64
str string
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) // true
}
Если какой-либо член двух структур несовместим, это вызовет ошибку компиляции. Обратите внимание, что элементы массива сравнимы только в том случае, если элементы массива сравнимы.
type data struct {
num int
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)
}
invalid operation: v1 == v2 (struct containing [10]func() bool cannot be compared)
Go предоставляет некоторые библиотечные функции для сравнения тех, которые нельзя использовать.==
переменные для сравнения, например, использующие пакет "reflect"DeepEqual()
:
// 比较相等运算符无法比较的元素
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2)) // true
m1 := map[string]string{"one": "a", "two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2)) // true
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
// 注意两个 slice 相等,值和顺序必须一致
fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2)) // true
}
Этот метод сравнения может быть медленным, в зависимости от потребностей вашей программы.DeepEqual()
Есть и другие варианты использования:
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2: ", reflect.DeepEqual(b1, b2)) // false
}
Уведомление:
-
DeepEqual()
Не всегда подходит для сравнения срезов
func main() {
var str = "one"
var in interface{} = "one"
fmt.Println("str == in: ", reflect.DeepEqual(str, in)) // true
v1 := []string{"one", "two"}
v2 := []string{"two", "one"}
fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2)) // false
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)) // false
}
Если вы хотите сравнивать английский текст в байтах или строках без учета регистра, вы можете использовать пакеты «bytes» или «strings».ToUpper()
иToLower()
функция. Чтобы сравнить байты или строки на других языках, вы должны использоватьbytes.EqualFold()
иstrings.EqualFold()
Если байтовый срез содержит данные для аутентификации пользователя (хэш зашифрованного текста, токен и т. д.), его больше не следует использовать.reflect.DeepEqual()
,bytes.Equal()
,bytes.Compare()
. Эти три функции легко заставить программуtiming attacks, и в этом случае следует использовать пакет «crypto/subtle».subtle.ConstantTimeCompare()
Равная функция
-
reflect.DeepEqual()
Пустой слайс не считается равным нулевому слайсу, но обратите внимание, чтоbyte.Equal()
считал бы их равными:
func main() {
var b1 []byte = nil
b2 := []byte{}
// b1 与 b2 长度相等、有相同的字节序
// nil 与 slice 在字节上是相同的
fmt.Println("b1 == b2: ", bytes.Equal(b1, b2)) // true
}
39. Оправиться от паники
Вызывается в функции отсрочки отсрочкиrecover()
, он ловит / прерывает панику
// 错误的 recover 调用示例
func main() {
recover() // 什么都不会捕捉
panic("not good") // 发生 panic,主程序退出
recover() // 不会被执行
println("ok")
}
// 正确的 recover 调用示例
func main() {
defer func() {
fmt.Println("recovered: ", recover())
}()
panic("not good")
}
Как видно сверху,recover()
Вступят в силу только вызовы из функции, выполняемой defer.
// 错误的调用示例
func main() {
defer func() {
doRecover()
}()
panic("not good")
}
func doRecover() {
fmt.Println("recobered: ", recover())
}
recobered: panic: not good
40. Обновите элемент, обновив ссылку при переборе среза, массива, карты.
В итерации диапазона полученное значение фактически является копией значения элемента Обновление копии не изменяет исходный элемент, то есть адрес копии не является адресом исходного элемента:
func main() {
data := []int{1, 2, 3}
for _, v := range data {
v *= 10 // data 中原有元素是不会被修改的
}
fmt.Println("data: ", data) // data: [1 2 3]
}
Если вы хотите изменить значение исходного элемента, вы должны использовать индекс для прямого доступа:
func main() {
data := []int{1, 2, 3}
for i, v := range data {
data[i] = v * 10
}
fmt.Println("data: ", data) // data: [10 20 30]
}
Если ваша коллекция содержит указатели на значения, ее нужно немного изменить. Вам по-прежнему нужно использовать индекс для доступа к элементам, но вы можете использовать элементы из диапазона для непосредственного обновления исходного значения:
func main() {
data := []*struct{ num int }{{1}, {2}, {3},}
for _, v := range data {
v.num *= 10 // 直接使用指针更新
}
fmt.Println(data[0], data[1], data[2]) // &{10} &{20} &{30}
}
41. Скрытые данные в срезах
Когда новый слайс повторно нарезается из слайса, новый слайс ссылается на базовый массив исходного слайса. Если эту яму перепрыгнуть, программа может выделить большое количество временных срезов, чтобы указать на некоторые данные исходного базового массива, что приведет к непредсказуемому использованию памяти.
func get() []byte {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0]) // 10000 10000 0xc420080000
return raw[:3] // 重新分配容量为 10000 的 slice
}
func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0]) // 3 10000 0xc420080000
}
Это можно решить, скопировав данные временного среза, а не повторно нарезая:
func get() (res []byte) {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0]) // 10000 10000 0xc420080000
res = make([]byte, 3)
copy(res, raw[:3])
return
}
func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0]) // 3 3 0xc4200160b8
}
42. Неправильное использование данных в срезах
В качестве простого примера перепишите путь к файлу (хранится в срезе)
Разделите путь, чтобы он указывал на каждый из разных уровней каталога, измените имя первого каталога и реорганизуйте имена подкаталогов, чтобы создать новый путь:
// 错误使用 slice 的拼接示例
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path, '/') // 4
println(sepIndex)
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
println("dir1: ", string(dir1)) // AAAA
println("dir2: ", string(dir2)) // BBBBBBBBB
dir1 = append(dir1, "suffix"...)
println("current path: ", string(path)) // AAAAsuffixBBBB
path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
println("dir1: ", string(dir1)) // AAAAsuffix
println("dir2: ", string(dir2)) // uffixBBBB
println("new path: ", string(path)) // AAAAsuffix/uffixBBBB // 错误结果
}
Результат склеивания неверныйAAAAsuffix/BBBBBBBBB
, потому что данные, на которые ссылаются два среза dir1 и dir2, обаpath
Базовый массив , измененный в строке 13dir1
Также модифицированоpath
, также привело кdir2
Модификации
Решение:
- Перераспределите новый срез и скопируйте нужные данные
- Используйте полное выражение среза:
input[low:high:max]
, емкость регулируется от максимальной до низкой
// 使用 full slice expression
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path, '/') // 4
dir1 := path[:sepIndex:sepIndex] // 此时 cap(dir1) 指定为4, 而不是先前的 16
dir2 := path[sepIndex+1:]
dir1 = append(dir1, "suffix"...)
path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
println("dir1: ", string(dir1)) // AAAAsuffix
println("dir2: ", string(dir2)) // BBBBBBBBB
println("new path: ", string(path)) // AAAAsuffix/BBBBBBBBB
}
Третий параметр в строке 6 используется для управления новой емкостью каталога 1. При добавлении лишних элементов к каталогу 1 для сохранения будет выделен новый буфер. вместо перезаписи исходного пути, лежащего в основе массива
43. Старый кусочек
Когда вы создаете новый срез из существующего среза, оба данных указывают на один и тот же базовый массив. Если ваша программа использует эту функцию, имейте в виду проблемы с «устаревшими» слайсами.
В некоторых случаях при добавлении элементов к срезу и базовому массиву, на который он указывает, заканчивается емкость, для хранения данных будет перераспределен новый массив. Другие срезы также указывают на исходный старый базовый массив.
// 超过容量将重新分配数组来拷贝值、重新存储
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
}
// 此时的 s1 与 s2 是指向同一个底层数组的
fmt.Println(s1) // [1 22 23]
fmt.Println(s2) // [22 23]
s2 = append(s2, 4) // 向容量为 2 的 s2 中再追加元素,此时将分配新数组来存
for i := range s2 {
s2[i] += 10
}
fmt.Println(s1) // [1 22 23] // 此时的 s1 不再更新,为旧数据
fmt.Println(s2) // [32 33 14]
}
44. Объявления типов и методы
Создание нового типа из существующего неинтерфейсного типа не наследует исходные методы:
// 定义 Mutex 的自定义类型
type myMutex sync.Mutex
func main() {
var mtx myMutex
mtx.Lock()
mtx.UnLock()
}
mtx.Lock undefined (type myMutex has no field or method Lock)...
Если вам нужно использовать методы исходного типа, вы можете встроить исходный тип как анонимное поле в новую определяемую вами структуру:
// 类型以字段形式直接嵌入
type myLocker struct {
sync.Mutex
}
func main() {
var locker myLocker
locker.Lock()
locker.Unlock()
}
Объявление типа интерфейса также сохраняет свой набор методов:
type myLocker sync.Locker
func main() {
var locker myLocker
locker.Lock()
locker.Unlock()
}
45. Вырваться из блоков for-switch и for-select
Разрыв без указанной метки будет выпрыгивать только из оператора switch/select.Если вы не можете использовать оператор return для выхода, вы можете выйти из блока кода, указанного меткой для разрыва:
// break 配合 label 跳出指定代码块
func main() {
loop:
for {
switch {
case true:
fmt.Println("breaking out...")
//break // 死循环,一直打印 breaking out...
break loop
}
}
fmt.Println("out...")
}
goto
Хотя он также может перейти в указанную позицию, он все равно снова войдет в for-switch, бесконечный цикл.
46. Итерационные переменные и замыкающие функции в операторах for
Переменная итерации в операторе for повторно используется на каждой итерации, то есть параметр, полученный функцией закрытия, созданной в for, всегда является одной и той же переменной, и одно и то же значение итерации получается, когда горутина начинает выполняться:
func main() {
data := []string{"one", "two", "three"}
for _, v := range data {
go func() {
fmt.Println(v)
}()
}
time.Sleep(3 * time.Second)
// 输出 three three three
}
Самое простое решение: не нужно модифицировать функцию goroutine, используйте локальную переменную внутри for для сохранения значения итерации, а затем передайте параметр:
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
}
Другой обходной путь: напрямую передать текущее значение итерации в качестве параметра анонимной функции:
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
}
Обратите внимание на разницу между тремя чуть более сложными примерами ниже:
type field struct {
name string
}
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
}
// 正确示例
func main() {
data := []field{{"one"}, {"two"}, {"three"}}
for _, v := range data {
v := v
go v.print()
}
time.Sleep(3 * time.Second)
// 输出 one two three
}
// 正确示例
func main() {
data := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data { // 此时迭代值 v 是三个元素值的地址,每次 v 指向的值不同
go v.print()
}
time.Sleep(3 * time.Second)
// 输出 one two three
}
47. Значение аргумента функции отсрочки
Для функции, которая откладывает выполнение, ее параметры будут оцениваться во время объявления, а не во время выполнения:
// 在 defer 函数中参数会提前求值
func main() {
var i = 1
defer fmt.Println("result: ", func() int { return i * 2 }())
i++
}
result: 2
48. Время выполнения функции отсрочки
Функция, которая откладывает выполнение, будет выполняться в конце вызывающей ее функции, а не в конце вызывающего ее блока операторов, обратите внимание на различие.
Например, в долго выполняющейся функции, использующей defer во внутреннем цикле for для очистки вызовов ресурсов, сгенерированных каждой итерацией, возникнут проблемы:
// 命令行参数指定目录名
// 遍历读取目录下的文件
func main() {
if len(os.Args) != 2 {
os.Exit(1)
}
dir := os.Args[1]
start, err := os.Stat(dir)
if err != nil || !start.IsDir() {
os.Exit(2)
}
var targets []string
filepath.Walk(dir, func(fPath string, fInfo os.FileInfo, err error) error {
if err != nil {
return err
}
if !fInfo.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) //error:too many open files
break
}
defer f.Close() // 在每次 for 语句块结束时,不会关闭文件资源
// 使用 f 资源
}
}
Сначала создайте 10000 файлов:
#!/bin/bash
for n in {1..10000}; do
echo content > "file${n}.txt"
done
текущий результат:
Решение: отложите функцию отложенного выполнения в анонимную функцию:
// 目录遍历正常
func main() {
// ...
for _, target := range targets {
func() {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:", target, "error:", err)
return // 在匿名函数内使用 return 代替 break 即可
}
defer f.Close() // 匿名函数执行结束,调用关闭文件资源
// 使用 f 资源
}()
}
}
Конечно, вы также можете удалить отсрочку и вызвать ее сразу после того, как файловый ресурс будет израсходован.f.Close()
закрывать.
49. Неудачное утверждение типа
В операторе утверждения типа, если утверждение терпит неудачу, будет возвращено «нулевое значение» целевого типа, и может возникнуть исключение, когда переменная утверждения смешивается с исходной переменной:
// 错误示例
func main() {
var data interface{} = "great"
// data 混用
if data, ok := data.(int); ok {
fmt.Println("[is an int], data: ", data)
} else {
fmt.Println("[not an int], data: ", data) // [isn't a int], data: 0
}
}
// 正确示例
func main() {
var data interface{} = "great"
if res, ok := data.(int); ok {
fmt.Println("[is an int], data: ", res)
} else {
fmt.Println("[not an int], data: ", data) // [not an int], data: great
}
}
50. Блокировка горутины и утечек ресурсов
На Google I/O 2012 Роб ПайкGo Concurrency PatternsВ докладе обсуждаются несколько основных шаблонов параллелизма в Go, таких какполный кодФункция для получения первого фрагмента данных из набора данных в:
func First(query string, replicas []Search) Result {
c := make(chan Result)
replicaSearch := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go replicaSearch(i)
}
return <-c
}
Когда поиск повторяется, горутина по-прежнему каждый раз запускается для обработки, и каждая горутина отправляет свои результаты поиска в канал результатов, и первый фрагмент данных, полученный в канале, будет возвращен напрямую.
После возвращения первого фрагмента данных, что делать с результатами поиска других горутин? А как насчет их собственных сопрограмм?
существуетFirst()
Результат в канале небуферизованный, а это значит, что вернуть может только первая горутина, а поскольку получателя нет, другие горутины будут заблокированы при отправке. Если вы делаете много вызовов, это может привести к утечке ресурсов.
Чтобы избежать утечек, вы должны убедиться, что все горутины завершаются правильно, есть 2 обходных пути:
- Используйте буферизованный канал, чтобы убедиться, что все горутины возвращают результаты:
func First(query string, replicas ...Search) Result {
c := make(chan Result,len(replicas))
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
-
использовать
select
оператор в паре с каналом, который может содержать буферизованное значениеdefault
Заявление:default
Буферизованный канал гарантирует, что даже если канал результатов не получит данные, он не заблокирует горутину.
func First(query string, replicas ...Search) Result {
c := make(chan Result,1)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
default:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
- Используйте специальный канал отмены, чтобы прервать выполнение оставшихся горутин:
func First(query string, replicas ...Search) Result {
c := make(chan Result)
done := make(chan struct{})
defer close(done)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
case <- done:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
Чтобы упростить презентацию, Роб Пайк не упомянул эти проблемы в коде речи. Однако новички могут использовать его напрямую, не задумываясь.
Продвинутый: 51-57
51. Использование указателя в качестве приемника метода
Методы указателя можно вызывать непосредственно для значения, если это значение является адресуемым. То есть для метода достаточно, чтобы его получателем был указатель.
Но не все значения являются адресуемыми, например, элементы типа map, переменные, на которые ссылаются через интерфейсы:
type data struct {
name string
}
type printer interface {
print()
}
func (p *data) print() {
fmt.Println("name: ", p.name)
}
func main() {
d1 := data{"one"}
d1.print() // d1 变量可寻址,可直接调用指针 receiver 的方法
var in printer = data{"two"}
in.print() // 类型不匹配
m := map[string]data{
"x": data{"three"},
}
m["x"].print() // m["x"] 是不可寻址的 // 变动频繁
}
cannot use data literal (type data) as type printer in assignment:
data does not implement printer (print method has pointer receiver)
cannot call pointer method on m["x"] cannot take the address of m["x"]
52. Обновите значение поля карты
Если значение поля карты имеет тип struct, невозможно напрямую обновить отдельное поле этой структуры:
// 无法直接更新 struct 的字段值
type data struct {
name string
}
func main() {
m := map[string]data{
"x": {"Tom"},
}
m["x"].name = "Jerry"
}
cannot assign to struct field m["x"].name in map
Потому что элементы на карте не адресуются. Разница в том, что элементы среза являются адресуемыми:
type data struct {
name string
}
func main() {
s := []data{{"Tom"}}
s[0].name = "Jerry"
fmt.Println(s) // [{Jerry}]
}
Примечание: не так давно компилятор gccgo мог обновлять значение поля элемента структуры карты, но вскоре это было исправлено, официально считается потенциальной фичей Go1.3, которую не нужно реализовывать вовремя, и все еще находится в списке дел.
Есть 2 способа обновить значение поля элемента структуры на карте:
- использовать локальные переменные
// 提取整个 struct 到局部变量中,修改字段值后再整个赋值
type data struct {
name string
}
func main() {
m := map[string]data{
"x": {"Tom"},
}
r := m["x"]
r.name = "Jerry"
m["x"] = r
fmt.Println(m) // map[x:{Jerry}]
}
- использовать указатели карты на элементы
func main() {
m := map[string]*data{
"x": {"Tom"},
}
m["x"].name = "Jerry" // 直接修改 m["x"] 中的字段
fmt.Println(m["x"]) // &{Jerry}
}
Но остерегайтесь следующего неправильного использования:
func main() {
m := map[string]*data{
"x": {"Tom"},
}
m["z"].name = "what???"
fmt.Println(m["x"])
}
panic: runtime error: invalid memory address or nil pointer dereference
53. нулевой интерфейс и нулевые интерфейсные значения
Хотя интерфейс выглядит как тип указателя, это не так. Переменная типа interface равна нулю, только если и тип, и значение равны нулю
Если значение вашей интерфейсной переменной следует за другими переменными (туман), будьте осторожны при сравнении с nil для равенства:
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
}
Если тип возвращаемого значения вашей функции — интерфейс, будьте осторожнее с этой ямой:
// 错误示例
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>
fmt.Printf("%T\n", res) // *struct {} // res 不是 nil,它的值为 nil
fmt.Printf("%v\n", res) // <nil>
}
}
// 正确示例
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) // Bad result: <nil>
}
}
54. Переменные стека
Вы не всегда знаете, размещены ли ваши переменные в куче или в стеке.
Использование в С++new
Созданные переменные всегда выделяются на кучу, но в идут даже с использованиемnew()
,make()
Чтобы создать переменную, расположение выделения памяти для переменной по-прежнему управляется компилятором Go.
Компилятор Go определит место хранения переменной в соответствии с размером переменной и результатом «анализа экранирования», поэтому он может точно вернуть адрес локальной переменной, что невозможно в C/C++.
Когда идет сборка или запуск, добавление параметра -m может точно проанализировать расположение размещения переменных в программе:
55. GOMAXPROCS, параллелизм и параллелизм
Для Go 1.4 и ниже программа будет использовать только 1 контекст выполнения/поток ОС, то есть в любой момент времени выполняется не более 1 горутины.
Версия Go 1.5 устанавливает количество исполняемых контекстов равнымruntime.NumCPU()
Количество возвращенных логических ядер ЦП. Соответствует ли это число фактическому общему количеству логических ядер ЦП в системе, зависит от количества ядер, которые ваш ЦП назначает программе. Вы можете использоватьGOMAXPROCS
Переменные среды или динамическое использованиеruntime.GOMAXPROCS()
настроить.
Непонимание:GOMAXPROCS
Указывает количество ядер ЦП, выполняющих горутины, см.Документация
GOMAXPROCS
Значение может превышать фактическое количество процессоров, до 256 в версии 1.5.
func main() {
fmt.Println(runtime.GOMAXPROCS(-1)) // 4
fmt.Println(runtime.NumCPU()) // 4
runtime.GOMAXPROCS(20)
fmt.Println(runtime.GOMAXPROCS(-1)) // 20
runtime.GOMAXPROCS(300)
fmt.Println(runtime.GOMAXPROCS(-1)) // Go 1.9.2 // 300
}
56. Изменение порядка операций чтения и записи
Go может изменить порядок выполнения некоторых операций, что может гарантировать последовательное выполнение операций в одной горутине, но не гарантирует порядок выполнения нескольких горутин:
var _ = runtime.GOMAXPROCS(3)
var a, b int
func u1() {
a = 1
b = 2
}
func u2() {
a = 3
b = 4
}
func p() {
println(a)
println(b)
}
func main() {
go u1() // 多个 goroutine 的执行顺序不定
go u2()
go p()
time.Sleep(1 * time.Second)
}
текущий результат:
Если вы хотите, чтобы несколько горутин выполнялись последовательно, как в коде, вы можете использовать механизм блокировки в канале или пакете синхронизации и т. д.
57. Приоритетное планирование
В вашей программе может быть горутина, которая блокирует запуск других горутин, например, есть программа, которая не позволяет запускать планировщик.for
цикл:
func main() {
done := false
go func() {
done = true
}()
for !done {
}
println("done !")
}
for
Тело петли не должно быть пустым, но будут проблемы, если код не запускает планировщик для выполнения.
Планировщик также будет выполняться в ненастроенных функциональных вызовах в GC, GO объявлений, блокировкой канала, блокирующих системных вызовов и операций блокировки после реализации:
func main() {
done := false
go func() {
done = true
}()
for !done {
println("not done !") // 并不内联执行
}
println("done !")
}
можно добавить-m
параметры для анализаfor
Встроенная функция, вызываемая в блоке кода:
Вы также можете использовать пакет времени выполненияGosched()
Чтобы запустить планировщик вручную:
func main() {
done := false
go func() {
done = true
}()
for !done {
runtime.Gosched()
}
println("done !")
}
текущий результат:
вперед отgithub.com/wuYin/blog
Эта статья также опубликована в общедоступной учетной записи WeChat [Информация Xiaodao]. Пожалуйста, отсканируйте код, чтобы следовать!