50 ям, которые нужно избегать Голангу

Go

предисловие

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 не идеальна с точки зрения производительности и сложности.

Динамические многомерные массивы могут быть созданы с использованием примитивных одномерных массивов, «независимых» срезов и срезов «общего базового массива».

  1. Используйте исходный 1D-массив: выполняйте проверки индексов, обнаружение переполнения и перераспределение при добавлении значений, когда массив заполнен.

  2. Использование «независимых» срезов выполняется в два этапа:

  • Создать внешний срез

    • выделение памяти для каждого внутреннего слайса

      Обратите внимание, что внутренние срезы не зависят друг от друга, так что любой внутренний срез не повлияет на другие срезы.

// 使用各自独立的 6 个 slice 来创建 [2][3] 的动态多维数组
func main() {
	x := 2
	y := 4
	
	table := make([][]int, x)
	for i  := range table {
		table[i] = make([]int, y)
	}
}
  1. Ломтики «Общий базовый массив»
  • Создайте фрагмент контейнера, содержащий необработанные данные

  • создать другие фрагменты

  • Вырежьте исходный фрагмент, чтобы инициализировать другие фрагменты

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Запустите приведенный выше код несколько раз, вывод не изменится, он будет перекомпилирован только в том случае, если вы обновите код. Порядок итераций нарушается после перекомпиляции:

image

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()Основная программа завершает работу, не дожидаясь завершения выполнения двух горутин:

image

Общее решение: Используйте переменную «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)
}

Результаты:

image

Похоже, все горутины выполнены, но сообщается об ошибке:

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
		}
	}
}

текущий результат:

image

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" // 不会被接收处理
}

текущий результат:

image

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)	// 模拟做其他的操作
}

результат операции:

image

Для ошибки в приведенном выше примере используйте устаревший канал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)
}

текущий результат:

image

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)
}

текущий результат:

image

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)
}

результат операции:

image

Средний: 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

текущий результат:

image

Решение: отложите функцию отложенного выполнения в анонимную функцию:

// 目录遍历正常
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 может точно проанализировать расположение размещения переменных в программе:

image

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)
}

текущий результат:

image

Если вы хотите, чтобы несколько горутин выполнялись последовательно, как в коде, вы можете использовать механизм блокировки в канале или пакете синхронизации и т. д.

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Встроенная функция, вызываемая в блоке кода:

image

Вы также можете использовать пакет времени выполненияGosched()Чтобы запустить планировщик вручную:

func main() {
	done := false

	go func() {
		done = true
	}()

	for !done {
		runtime.Gosched()
	}

	println("done !")
}

текущий результат:

image

вперед отgithub.com/wuYin/blog

Эта статья также опубликована в общедоступной учетной записи WeChat [Информация Xiaodao]. Пожалуйста, отсканируйте код, чтобы следовать!

image