Урок 5 «Быстрое изучение языка го» — гибкая нарезка

интервью задняя часть Python Go
Урок 5 «Быстрое изучение языка го» — гибкая нарезка

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

Те, кто изучил язык Java, разберутся со слайсами легче, потому что его внутренняя структура очень похожа на ArrayList, а внутренняя реализация ArrayList также представляет собой массив. Когда емкость массива недостаточна и его необходимо расширить, новый массив будет заменен, а содержимое старого массива необходимо скопировать в новый массив. Внутри ArrayList есть два очень важных свойства — емкость и длина. емкость представляет собой общую длину внутреннего массива, а длина представляет длину используемого в настоящее время массива. длина никогда не может превышать мощность.

Переменная среза на приведенном выше рисунке содержит три поля: указатель базового массива, длину среза и емкость среза. Срезы поддерживают операцию добавления для добавления нового содержимого к базовому массиву, который должен заполнить серую сетку выше. Если сетка заполнена, срез необходимо расширить, а базовый массив будет заменен.

Чтобы представить это в перспективе, переменная slice — это представление базового массива, базовый массив — это спальня, а переменная slice — это окно спальни. Через окно мы можем видеть часть или весь лежащий в основе массив. В спальне может быть несколько окон, причем разные окна выходят в разные части спальни.

Создание срезов

Существует множество способов создания слайсов.Давайте сначала рассмотрим наиболее распространенный метод создания слайсов, которым является встроенная функция make.

package main

import "fmt"

func main() {
 var s1 []int = make([]int, 5, 8)
 var s2 []int = make([]int, 8) // 满容切片
 fmt.Println(s1)
 fmt.Println(s2)
}

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

Функция make создает срез и должна предоставить три параметра, а именно тип среза, длину и емкость среза. Третий параметр является необязательным, если третий параметр не указан, длина и емкость равны, то есть слайс заполнен. Как и обычные переменные, срезы также могут быть выведены автоматически, без определения типа и ключевого слова var. Например, код выше эквивалентен коду ниже.

package main

import "fmt"

func main() {
 var s1 = make([]int, 5, 8)
 s2 := make([]int, 8)
 fmt.Println(s1)
 fmt.Println(s2)
}

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

Инициализация слайсов

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

package main

import "fmt"

func main() {
 var s []int = []int{1,2,3,4,5}  // 满容的
 fmt.Println(s, len(s), cap(s))
}

---------
[1 2 3 4 5] 5 5

Язык Go предоставляет встроенные функции len() и cap() для прямого получения свойств длины и емкости срезов.

пустой фрагмент

При создании слайсов следует учитывать два особых случая, то есть слайсы с нулевой емкостью и длиной, называемые «пустыми слайсами», которые отличаются от «срезов с нулевым значением», упомянутых ранее.

package main

import "fmt"

func main() {
 var s1 []int
 var s2 []int = []int{}
 var s3 []int = make([]int, 0)
 fmt.Println(s1, s2, s3)
 fmt.Println(len(s1), len(s2), len(s3))
 fmt.Println(cap(s1), cap(s2), cap(s3))
}

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

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

назначение среза

Присвоение среза — это операция неглубокого копирования, которая копирует три поля переменной среза.Вы можете думать о переменной среза как о массиве целых чисел длины 3, а присвоение массива — это неглубокая копия. Две переменные до и после копирования совместно используют базовый массив. Изменения в одном фрагменте повлияют на содержимое другого фрагмента, что требует особого внимания.

package main

import "fmt"

func main() {
 var s1 = make([]int, 5, 8)
 // 切片的访问和数组差不多
 for i := 0; i < len(s1); i++ {
  s1[i] = i + 1
 }
 var s2 = s1
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))
 
 // 尝试修改切片内容
 s2[0] = 255
 fmt.Println(s1)
 fmt.Println(s2)
}

--------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5]
[255 2 3 4 5]

Из приведенного выше вывода видно, что два назначенных среза совместно используют базовый массив.

обход срезов

Синтаксис обхода среза такой же, как у массива, за исключением того, что он поддерживает обход индекса, то есть с использованием ключевого слова диапазона.

package main


import "fmt"


func main() {
	var s = []int{1,2,3,4,5}
	for index := range s {
		fmt.Println(index, s[index])
	}
	for index, value := range s {
		fmt.Println(index, value)
	}
}

--------
0 1
1 2
2 3
3 4
4 5
0 1
1 2
2 3
3 4
4 5

добавление фрагментов

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

package main

import "fmt"

func main() {
 var s1 = []int{1,2,3,4,5}
 fmt.Println(s1, len(s1), cap(s1))

 // 对满容的切片进行追加会分离底层数组
 var s2 = append(s1, 6)
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))

 // 对非满容的切片进行追加会共享底层数组
 var s3 = append(s2, 7)
 fmt.Println(s2, len(s2), cap(s2))
 fmt.Println(s3, len(s3), cap(s3))
}

--------------------------
[1 2 3 4 5] 5 5
[1 2 3 4 5] 5 5
[1 2 3 4 5 6] 6 10
[1 2 3 4 5 6] 6 10
[1 2 3 4 5 6 7] 7 10

Именно потому, что к срезу добавляется новая переменная слайса, компилятор Go запрещает не использовать новую переменную слайса после добавления слайса, чтобы пользователи не думали, что возвращаемое значение операции добавления является той же переменной, что и исходная переменная среза.

package main

import "fmt"

func main() {
 var s1 = []int{1,2,3,4,5}
 append(s1, 6)
 fmt.Println(s1)
}

--------------
./main.go:7:8: append(s1, 6) evaluated but not used

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

package main

import "fmt"

func main() {
 var s1 = []int{1,2,3,4,5}
 _ = append(s1, 6)
 fmt.Println(s1)
}

----------
[1 2 3 4 5]

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

Поля фрагмента доступны только для чтения

Мы только что сказали, что длину среза можно изменить, почему мы говорим, что слайс доступен только для чтения? Разве это не противоречие? Это должно напомнить читателям, что новая переменная среза формируется после добавления среза, и три поля старой переменной среза не изменяются, изменяется только лежащий в основе массив. Это говорит о том, что «поле» среза доступно только для чтения, а не о том, что срез доступен только для чтения. «Домен» среза — это три части, составляющие переменную среза: указатель на базовый массив, длина среза и емкость среза. Читателям нужно тщательно жевать здесь.

резать резать

Происхождение названия слайса до сих пор не выяснено, раз он называется слайсом, то его надо уметь резать. Cut cut, у некоторых людей мурашки по коже, когда они слышат это слово. Обрезка среза может быть аналогична подстроке строки.При этом срез не обрезается, а копируется подсрез из родительского среза.Подсрез и родительский срез совместно используют лежащий в основе массив. Давайте посмотрим, как нарезаются ломтики.

package main

import "fmt"

func main() {
 var s1 = []int{1,2,3,4,5,6,7}
 // start_index 和 end_index,不包含 end_index
 // [start_index, end_index)
 var s2 = s1[2:5] 
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))
}

------------
[1 2 3 4 5 6 7] 7 7
[3 4 5] 3 5

Приведенный выше вывод требует особого внимания.Поскольку базовый массив является общим до и после резки, почему емкость отличается? Чтобы объяснить это, я должен нарисовать рисунок, читатель должен внимательно рассмотреть рисунок ниже.

Мы заметили, что внутренний указатель данных подслоя указывает на середину массива, а не на его начало. Размер емкости подфрагмента — это длина от средней позиции до конца фрагмента, а родительский фрагмент и вложенный фрагмент по-прежнему совместно используют базовый массив.

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

package main

import "fmt"

func main() {
 var s1 = []int{1, 2, 3, 4, 5, 6, 7}
 var s2 = s1[:5]
 var s3 = s1[3:]
 var s4 = s1[:]
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))
 fmt.Println(s3, len(s3), cap(s3))
 fmt.Println(s4, len(s4), cap(s4))
}

-----------
[1 2 3 4 5 6 7] 7 7
[1 2 3 4 5] 5 7
[4 5 6 7] 4 4
[1 2 3 4 5 6 7] 7 7

Внимательные студенты могут заметить, что приведенное выше s1[:] очень особенное, отличается ли оно от обычного назначения слайсов? Ответ — без разницы, что очень удивительно, тот же общий базовый массив — тоже неглубокая копия. Давайте проверим

package main

import "fmt"

func main() {
 var s = make([]int, 5, 8)
 for i:=0;i<len(s);i++ {
  s[i] = i+1
 }
 fmt.Println(s, len(s), cap(s))

 var s2 = s
 var s3 = s[:]
 fmt.Println(s2, len(s2), cap(s2))
 fmt.Println(s3, len(s3), cap(s3))

 // 修改母切片
 s[0] = 255
 fmt.Println(s, len(s), cap(s))
 fmt.Println(s2, len(s2), cap(s2))
 fmt.Println(s3, len(s3), cap(s3))
}

-------------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5] 5 8
[255 2 3 4 5] 5 8
[255 2 3 4 5] 5 8

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

Массив для среза

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

package main

import "fmt"

func main() {
	var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	var b = a[2:6]
	fmt.Println(b)
	a[4] = 100
	fmt.Println(b)
}

-------
[3 4 5 6]
[3 4 100 6]

функция копирования

Язык Go также имеет встроенную функцию копирования для глубокого копирования слайсов. Но это не так глубоко, это просто массив глубоко до дна. Если массив содержит указатели, такие как тип []*int, то содержимое, на которое указывает указатель, по-прежнему является общим.

func copy(dst, src []T) int

Функция копирования не будет дополнительно выделять память базового массива из-за длины исходного слайса и целевого слайса, она отвечает только за копирование содержимого массива из исходного слайса в целевой слайс. меньше длины исходного и целевого слайсов Значение - min(len(src), len(dst)), функция возвращает фактическую длину копии. Давайте посмотрим на пример

package main

import "fmt"

func main() {
 var s = make([]int, 5, 8)
 for i:=0;i<len(s);i++ {
  s[i] = i+1
 }
 fmt.Println(s)
 var d = make([]int, 2, 6)
 var n = copy(d, s)
 fmt.Println(n, d)
}
-----------
[1 2 3 4 5]
2 [1 2]

Точка расширения среза

При расширении относительно короткого слайса система выделит на 100 % больше места, то есть выделенная емкость массива в два раза больше длины слайса. Однако, когда длина фрагмента превышает 1024, стратегия расширения корректируется, чтобы выделить на 25 % больше места, чтобы избежать чрезмерной траты пространства. Попробуйте интерпретировать бегущие результаты ниже.

s1 := make([]int, 6)
s2 := make([]int, 1024)
s1 = append(s1, 1)
s2 = append(s2, 2)
fmt.Println(len(s1), cap(s1))
fmt.Println(len(s2), cap(s2))
-------------------------------------------
7 12
1025 1344

Приведенный выше результат запускается в goplayground, при запуске в локальной среде результат будет другим.

$ go run main.go
7 12
1025 1280

Расширение - относительно сложная операция. Внутренние детали должны быть известны путем анализа исходного кода. Если вы не понимаете деталей расширения, это не повлияет на обычное использование. Поэтому мы тщательно проанализируем исходный код фрагментов в расширенный контент позже.

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