Углубленный анализ трех особых состояний «среза» в языке Go

интервью задняя часть Go
Углубленный анализ трех особых состояний «среза» в языке Go

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

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

type slice struct {
  array unsafe.Pointer
  length int
  capcity int
}

Одно из особых состояний, о которых мы поговорим сегодня, «нулевой срез» на самом деле не является специальным срезом, это просто означает, что двоичное содержимое базового массива равно нулю. Например, переменная s в следующем коде является «нулевым срезом».

var s = make([]int, 10)
fmt.Println(s)
------------
[0 0 0 0 0 0 0 0 0 0]

Если это срез типа указателя, то содержимое базового массива равно нулю.

var s = make([]*int, 10)
fmt.Println(s)
------------
[<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>]

Нулевой срез относительно прост для понимания, и я не буду продолжать мучить себя в этой части.

Далее мы познакомимся с «пустыми слайсами» и «нулевыми слайсами». Прежде чем понять разницу между ними, давайте взглянем на формы, которые можно создать для слайса нулевой длины.

var s1 []int
var s2 = []int{}
var s3 = make([]int, 0)
// new 函数返回是指针类型,所以需要使用 * 号来解引用
var s4 = *new([]int)

fmt.Println(len(s1), len(s2), len(s3), len(s4))
fmt.Println(cap(s1), cap(s2), cap(s3), cap(s4))
fmt.Println(s1, s2, s3, s4)

----------------
0 0 0 0
0 0 0 0
[] [] [] []

Судя по результатам вывода, четыре вышеупомянутые формы кажутся абсолютно одинаковыми, и между ними нет никакой разницы. Но на самом деле разница есть.Два специальных типа, о которых мы собираемся поговорить, это «пустые слайсы» и «нулевые слайсы», которые скрыты в вышеупомянутых четырех формах.

Как мы можем проанализировать разницу между тремя сторонами и четырьмя формами внутренней структуры? Далее мы будем использовать расширенный контент языка Go для преобразования любого типа переменных языка Go через unsafe.Pointer.

Поскольку внутренняя структура слайса является структурой, она содержит три целочисленные переменные размера машинного слова, первая переменная является переменной-указателем, а переменная-указатель хранит целочисленное значение, но это значение является другой переменной адреса памяти. Мы можем думать об этой структуре как о массиве целых чисел [3]int длины 3. Затем преобразуйте переменную slice в [3]int.

var s1 []int
var s2 = []int{}
var s3 = make([]int, 0)
var s4 = *new([]int)

var a1 = *(*[3]int)(unsafe.Pointer(&s1))
var a2 = *(*[3]int)(unsafe.Pointer(&s2))
var a3 = *(*[3]int)(unsafe.Pointer(&s3))
var a4 = *(*[3]int)(unsafe.Pointer(&s4))
fmt.Println(a1)
fmt.Println(a2)
fmt.Println(a3)
fmt.Println(a4)

---------------------
[0 0 0]
[824634199592 0 0]
[824634199592 0 0]
[0 0 0]

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

Переменные s1 и s4, вывод которых равен [0 0 0], являются «нулевыми слайсами», а переменные s2 и s3 — «пустыми слайсами». 824634199592 Это значение является специальным адресом памяти, общим для всех типов «пустых слайсов».

var s2 = []int{}
var s3 = make([]int, 0)

var a2 = *(*[3]int)(unsafe.Pointer(&s2))
var a3 = *(*[3]int)(unsafe.Pointer(&s3))
fmt.Println(a2)
fmt.Println(a3)

var s5 = make([]struct{ x, y, z int }, 0)
var a5 = *(*[3]int)(unsafe.Pointer(&s5))
fmt.Println(a5)

--------
[824634158720 0 0]
[824634158720 0 0]
[824634158720 0 0]

Графически представить «пустые слайсы» и «нулевые слайсы» следующим образом

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

//// runtime/malloc.go

// base address for all 0-byte allocations
var zerobase uintptr

// 分配对象内存
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
    ...
}

//// runtime/slice.go
// 创建切片
func makeslice(et *_type, len, cap int) slice {
  ...
     p := mallocgc(et.size*uintptr(cap), et, true)
	 return slice{p, len, cap}
}

Последний вопрос: есть ли разница в использовании между «нулевым фрагментом» и «пустым фрагментом»?

Ответ: никакой разницы! Нет! Нет, есть одно маленькое отличие! Пожалуйста, смотрите код ниже

package main

import "fmt"

func main() {
	var s1 []int
	var s2 = []int{}

	fmt.Println(s1 == nil)
	fmt.Println(s2 == nil)

	fmt.Printf("%#v\n", s1)
	fmt.Printf("%#v\n", s2)
}

-------
true
false
[]int(nil)
[]int{}

Таким образом, лучший способ избежать головокружения при написании кода — не создавать «пустые слайсы», единообразно использовать «нулевые слайсы» и избегать сравнения срезов с нулевыми значениями для выполнения некоторой логики. Это официальная стандартная рекомендация.

Первый объявляет нулевое значение среза, а второй не равен нулю, но имеет нулевую длину.Они функционально эквивалентны — их len и cap равны нулю, но предпочтительным стилем является нулевой срез.

"Пустые срезы" и "нулевые срезы" иногда скрыты в структурах. В настоящее время их различия игнорируются слишком многими людьми. Давайте рассмотрим пример.

type Something struct {
	values []int
}

var s1 = Something{}
var s2 = Something{[]int{}}
fmt.Println(s1.values == nil)
fmt.Println(s2.values == nil)

--------
true
false

Можно обнаружить, что результаты этих двух структур различны!

Еще одно совершенно другое место между «пустым фрагментом» и «нулевым фрагментом» — это сериализация JSON.

type Something struct {
	Values []int
}

var s1 = Something{}
var s2 = Something{[]int{}}
bs1, _ := json.Marshal(s1)
bs2, _ := json.Marshal(s2)
fmt.Println(string(bs1))
fmt.Println(string(bs2))

---------
{"Values":null}
{"Values":[]}

Бан!Бан!Бан!У них разные результаты сериализации json!

Отсканируйте QR-код, чтобы прочитать дополнительные главы «Быстрое изучение языка Go».