50 ям, на которые Голангу нужно не наступить (1)

Go

Недавно я собираюсь написать несколько технических постов в блоге о golang. Эта статья представляет собой технический перевод golang, который я уже видел на GitHub. Он кажется очень полезным. Позвольте мне сначала поделиться им с читателями.

предисловие

Go — простой и интересный язык программирования, как и другие языки, он неизбежно столкнется со многими подводными камнями при использовании, но большинство из них не являются недостатками дизайна самого Go. Если вы только что перешли на Go с другого языка, то на большинство ям в этой статье наступят.

Если вы потратите время на изучение официального документа, вики,список рассылки обсуждений,Rob PikeБольшое количество статей и исходный код Go, вы обнаружите, что ямы в этой статье очень распространены, и новичок, пропуская эти ямы, может сэкономить много времени на отладке кода.

Начинающие: 1-35

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

туман:обратитесь к исходному тексту

Эта статья воспроизведена изGitHub.com/no-yin/blog/…