Глубокая расшифровка Slice на языке Go

Go

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

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

О чем мы говорим, когда говорим о срезах?

sliceв переводе на китайский есть切片, это и数组(array)Очень похоже, вы можете использовать индекс для доступа, если за пределы, он будет паниковать. Но он более гибкий, чем массивы, и может расширяться автоматически.

Самый простой способ понять природу слайса — взглянуть на его исходный код:

// runtime/slice.go
type slice struct {
	array unsafe.Pointer // 元素指针
	len   int // 长度 
	cap   int // 容量
}

видеть это,sliceЕсть три свойства:指针, указывая на базовый массив;长度, указывающее количество доступных элементов слайса, то есть при использовании нижнего индекса для доступа к элементам слайса нижний индекс не может превышать длину слайса;容量, количество элементов базового массива, емкость >= длина. В случае, если базовый массив не расширяется, емкость также является максимальной, которую может расширить срез.

切片数据结构

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

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

Существует несколько способов создания срезов:

серийный номер Способ пример кода
1 прямое заявление var slice []int
2 new slice := *new([]int)
3 буквальный slice := []int{1,2,3,4,5}
4 make slice := make([]int, 5, 10)
5 «Перехват» из среза или массива slice := array[1:5]илиslice := sourceSlice[1:5]

прямое заявление

Первый созданный фрагмент на самом деле являетсяnil slice. Его длина и емкость равны 0. иnilРезультат сравненияtrue.

Путаница здесь в том, чтоempty slice, его длина и емкость также равны 0, но указатели данных всех пустых слайсов указывают на один и тот же адрес0xc42003bda0. пустой кусок иnilРезультат сравненияfalse.

Их внутренняя структура выглядит следующим образом:

nil slice 与 empty slice

как создать нулевой срез пустой фрагмент
метод первый var s1 []int var s2 = []int{}
Способ 2 var s4 = *new([]int) var s3 = make([]int, 0)
длина 0 0
емкость 0 0
иnilсравнивать true false

nilСлайсы очень похожи на пустые слайсы, длина и емкость равны 0. Официальная рекомендация — использовать их как можно больше.nilкусочек.

оnil sliceиempty sliceДля исследования, пожалуйста, обратитесь к статье, написанной Лао Цянем, автором публичного аккаунта «Пещера кода», «Глубокий анализ трех особых состояний «кусочков» в языке го». Адрес прикреплен к разделу «Ссылки».

буквальный

Относительно просто, используйте напрямую初始化表达式Создайте.

package main

import "fmt"

func main() {
	s1 := []int{0, 1, 2, 3, 8: 100}
	fmt.Println(s1, len(s1), cap(s1))
}

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

[0 1 2 3 0 0 0 0 100] 9 9

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

make

makeФункция должна передавать три параметра: тип среза, длину и емкость. Конечно, емкость можно не указывать, а значение по умолчанию и длина равны.

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

Давайте начнем с небольшого кусочка игрушечного кода, используйтеmakeСоздание ключевого словаslice:

package main

import "fmt"

func main() {
	slice := make([]int, 5, 10) // 长度为5,容量为10
	slice[2] = 2 // 索引为2的元素赋值为2
	fmt.Println(slice)
}

Выполните следующую команду, чтобы получить код сборки Go:

go tool compile -S main.go

Мы сосредоточимся только на основной функции:

0x0000 00000 (main.go:5)TEXT    "".main(SB), $96-0
0x0000 00000 (main.go:5)MOVQ    (TLS), CX
0x0009 00009 (main.go:5)CMPQ    SP, 16(CX)
0x000d 00013 (main.go:5)JLS     228
0x0013 00019 (main.go:5)SUBQ    $96, SP
0x0017 00023 (main.go:5)MOVQ    BP, 88(SP)
0x001c 00028 (main.go:5)LEAQ    88(SP), BP
0x0021 00033 (main.go:5)FUNCDATA    $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (main.go:5)FUNCDATA    $1, gclocals·57cc5e9a024203768cbab1c731570886(SB)
0x0021 00033 (main.go:5)LEAQ    type.int(SB), AX
0x0028 00040 (main.go:6)MOVQ    AX, (SP)
0x002c 00044 (main.go:6)MOVQ    $5, 8(SP)
0x0035 00053 (main.go:6)MOVQ    $10, 16(SP)
0x003e 00062 (main.go:6)PCDATA  $0, $0
0x003e 00062 (main.go:6)CALL    runtime.makeslice(SB)
0x0043 00067 (main.go:6)MOVQ    24(SP), AX
0x0048 00072 (main.go:6)MOVQ    32(SP), CX
0x004d 00077 (main.go:6)MOVQ    40(SP), DX
0x0052 00082 (main.go:7)CMPQ    CX, $2
0x0056 00086 (main.go:7)JLS     221
0x005c 00092 (main.go:7)MOVQ    $2, 16(AX)
0x0064 00100 (main.go:8)MOVQ    AX, ""..autotmp_2+64(SP)
0x0069 00105 (main.go:8)MOVQ    CX, ""..autotmp_2+72(SP)
0x006e 00110 (main.go:8)MOVQ    DX, ""..autotmp_2+80(SP)
0x0073 00115 (main.go:8)MOVQ    $0, ""..autotmp_1+48(SP)
0x007c 00124 (main.go:8)MOVQ    $0, ""..autotmp_1+56(SP)
0x0085 00133 (main.go:8)LEAQ    type.[]int(SB), AX
0x008c 00140 (main.go:8)MOVQ    AX, (SP)
0x0090 00144 (main.go:8)LEAQ    ""..autotmp_2+64(SP), AX
0x0095 00149 (main.go:8)MOVQ    AX, 8(SP)
0x009a 00154 (main.go:8)PCDATA  $0, $1
0x009a 00154 (main.go:8)CALL    runtime.convT2Eslice(SB)
0x009f 00159 (main.go:8)MOVQ    16(SP), AX
0x00a4 00164 (main.go:8)MOVQ    24(SP), CX
0x00a9 00169 (main.go:8)MOVQ    AX, ""..autotmp_1+48(SP)
0x00ae 00174 (main.go:8)MOVQ    CX, ""..autotmp_1+56(SP)
0x00b3 00179 (main.go:8)LEAQ    ""..autotmp_1+48(SP), AX
0x00b8 00184 (main.go:8)MOVQ    AX, (SP)
0x00bc 00188 (main.go:8)MOVQ    $1, 8(SP)
0x00c5 00197 (main.go:8)MOVQ    $1, 16(SP)
0x00ce 00206 (main.go:8)PCDATA  $0, $1
0x00ce 00206 (main.go:8)CALL    fmt.Println(SB)
0x00d3 00211 (main.go:9)MOVQ    88(SP), BP
0x00d8 00216 (main.go:9)ADDQ    $96, SP
0x00dc 00220 (main.go:9)RET
0x00dd 00221 (main.go:7)PCDATA  $0, $0
0x00dd 00221 (main.go:7)CALL    runtime.panicindex(SB)
0x00e2 00226 (main.go:7)UNDEF
0x00e4 00228 (main.go:7)NOP
0x00e4 00228 (main.go:5)PCDATA  $0, $-1
0x00e4 00228 (main.go:5)CALL    runtime.morestack_noctxt(SB)
0x00e9 00233 (main.go:5)JMP     0

Позвольте мне сначала объяснить, сборка языка GoFUNCDATAиPCDATAОн генерируется компилятором и используется для сохранения некоторой информации, связанной со сборкой мусора, нам это не нужно.

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

Давайте посмотрим сверху вниз и рассмотрим несколько ключевых функций:

CALL    runtime.makeslice(SB)
CALL    runtime.convT2Eslice(SB)
CALL    fmt.Println(SB)
CALL    runtime.morestack_noctxt(SB)
серийный номер Функции
1 создать фрагмент
2 преобразование типов
3 функция печати
4 расширение пространства стека

1относится к созданию срезов;2это преобразование типа; вызовfmt.PrintlnНужно сделать преобразование слайса;3оператор печати;4Это функция расширения стека.В начале функции она проверяет, достаточно ли текущего пространства стека.Если этого недостаточно, ее необходимо вызвать для расширения. Можно пока игнорировать.

Вызов функции будет включать передачу параметров, а передача параметров Go осуществляется через пространство стека. Далее подробно разбираем весь этот процесс.

Ряды эффект
1 mainОпределение функции, размер кадра стека96B
2-4 Определите, нужно ли расширять стек, и при необходимости перейдите к228, который вызоветruntime.morestack_noctxt(SB)Выполните операцию расширения стека. Более подробная информация будет в статье
5-9 будетcaller BPНажимаем стек, подробности обсудим позже
10-15 перечислитьruntime.makeslice(SB)функции и препараты. *_тип означаетint, это,sliceТип элемента. Соответствующий исходный код здесь — строка 6, которая вызываетmakeСоздайтеsliceэта линия.5и10Представляйте длину и емкость соответственно, параметры функции будут подготовлены в верхней части стека, а затем будет выполнена команда вызова функции.CALL, войдите в кадр стека вызываемой функции, он будет последовательно изcallerВершина стека принимает параметры функции
16-18 перениматьmakesliceвозвращаемое значение черезmoveперейти в реестр
19-21 дать индексу массива значение2присвоить значение элементам2,потому чтоintТипslice, размер элемента равен 8 байтам, поэтомуMOVQ $2, 16(AX)Эта команда должна2перейти к индексу как2позиция. Здесь также проверяется размер значения индекса, и если он выходит за пределы, он переходит к221,воплощать в жизньpanicфункция
22-26 соответственно через реестрAX,CX,DXбудетmakesliceВозвращаемое значениеmoveв другие места в памяти, также известные как локальные переменные, создавая таким образомslice

makeslice 栈帧

Слева данные в стеке, справа данные в куче.arrayнаправлениеsliceБазовые данные размещаются в куче. Обратите внимание, что адреса в стеке растут от высокого к младшему, а куча растет от младшего к старшему. Число в левой части стека представляет количество строк соответствующего ассемблерного кода, а стрелка в правой части стека представляет адрес стека. (48) SP, (56) Содержание, обозначенное SP, будет прочитано ниже.

Обратите внимание, что на рисунке адрес стека увеличивается снизу вверх, поэтому SP представляет рисунок*_typeместоположение и так далее для других.

Ряды эффект
27-32 готов позвонитьruntime.convT2Eslice(SB)параметры функции
33-36 Получить возвращаемое значение через регистры AX, CXmoveк (48) СП, (56) СП

convT2EsliceОбъявление функции выглядит следующим образом:

func convT2Eslice(t *_type, elem unsafe.Pointer) (e eface) 

Первый параметр - указатель*_type,_typeпредставляет собой структуру, представляющую тип, а входящий сюдаsliceтип[]int; Второй параметр это указатель на элемент, который передается сюдаsliceПервый адрес базового массива.

возвращаемое значениеefaceСтруктура определяется следующим образом:

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

Так как мы будем называтьfmt.Println(slice), посмотрите на прототип функции:

func Println(a ...interface{}) (n int, err error)

Printlnпринимает тип интерфейса, поэтому нам нужно поставитьsliceПреобразование в тип интерфейса. так какsliceЭто невозможно, это "空 interfaceТак и будет звонитьconvT2EsliceЗавершите процесс преобразования.

convT2EsliceФункция возвращает указатель типа и адрес данных. Исходный код не будет опубликован, общий процесс: вызовmallocgcВыделите блок памяти, поместите данныеcopyВведите новую память, а затем верните адрес этой памяти,*_typeВозвращает переданные параметры напрямую.

convT2Eslice 栈帧

32(SP)и40(SP)ФактическиmakesliceВозвращаемое значение функции здесь можно игнорировать.

левыйfmt.Println(slice)Выполнен последний вызов функции, продолжаем.

Ряды эффект
37-40 ПодготовитьPrintlnпараметры функции. Всего 3 параметра, первый это адрес типа, а есть два1, Я не знаю, почему эта часть пока публикуется.Студенты, которые знают об этом, могут оставить сообщение в конце статьи.

так позвониfmt.Println(slice)фактически передается вslice类型的eface地址. так,PrintlnВы можете получить доступ к данным в типе и, наконец, «распечатать» их.

fmt.Println 栈帧

Наконец, давайте посмотрим наmainНачало и конец кадра стека функций.

0x0013 00019 (main.go:5)SUBQ    $96, SP
0x0017 00023 (main.go:5)MOVQ    BP, 88(SP)
0x001c 00028 (main.go:5)LEAQ    88(SP), BP
…………………………
0x00d3 00211 (main.go:9)MOVQ    88(SP), BP
0x00d8 00216 (main.go:9)ADDQ    $96, SP
RET

BPМожно понять, что сохраняется адрес в нижней части кадра стека текущей функции,SPЗатем сохраните адрес вершины стека.

исходный,BPиSPУ каждого есть начальное состояние.

mainПри выполнении функции сначалаmainРазмер кадра стека функций определяетсяSPновое направлениеmainдостигнут размер кадра стека функций96B. после старогоBPСохранить вmainнижней части рамки стека функций и сделайтеBPРегистр повторно указывает на новую нижнюю часть стека, т.е.mainНижняя часть рамки стека функций.

Наконец, когдаmainПосле выполнения функции поместите ее в конец стекаBPвернуть в нормуBPЗарегистрируйтесь, восстановите исходное состояние до звонка. Все как будто и не было, идеальная сцена.

栈帧变化

В этой части подробно анализируется процесс вызова функции. С одной стороны, позвольте вам просмотреть содержание предыдущей статьи, а с другой стороны, показать вам, как узнать, какие функции на самом деле вызываются за функцией в Go. Как и в примере, мы видимmakeЗа функцией она на самом деле называетсяmakesliceфункция; еще один момент, чтобы все меньше «боялись» сборки, вы можете легко что-то анализировать.

перехватывать

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

Создание нового объекта среза на основе существующего среза называетсяreslice. Новый слайс и старый слайс совместно используют базовый массив, и изменения в базовом массиве новым и старым слайсами будут влиять друг на друга. Новые объекты-срезы, созданные из массивов, имеют тот же эффект: изменения, внесенные в элементы массива или среза, влияют друг на друга.

Стоит отметить, что предположение о том, что новые и старые слайсы или старые и новые слайсы и старые массивы влияют друг на друга, заключается в том, что они совместно используют базовый массив.appendОперация заставляет базовый массив нового среза расширяться и перемещаться в новое место, и они не будут влиять друг на друга. так,问题的关键在于两者是否会共用底层数组.

Операция по перехвату выглядит следующим образом:

 data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
 slice := data[2:4:6] // data[low, high, max]

правильноdataИспользуйте 3 значения индекса, чтобы обрезать новыйslice. здесьdataМожет быть массивом илиslice.lowэто наименьшее значение индекса, это закрытый интервал, то есть первый элементdataродыlowэлемент по индексу; в то время какhighиmaxЭто открытый интервал, указывающий, что последний элемент может быть только индексомhigh-1элемент при , а максимальная емкость может быть только индексомmax-1элемент в .

max >= high >= low

когдаhigh == lowвремя, новыйsliceПусто.

Еще один момент,highиmaxДолжен быть в старом массиве или старомsliceемкость (cap) в пределах диапазона.

Давайте посмотрим на пример, из четвертого издания «Go Study Notes» Рейна Марка, страница P43, в справочных материалах есть адреса книг с открытым исходным кодом. Здесь я расширю и объясню подробно:

package main

import "fmt"

func main() {
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]
	s2 := s1[2:6:7]

	s2 = append(s2, 100)
	s2 = append(s2, 200)

	s1[2] = 20

	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(slice)
}

Сначала посмотрите на результаты запуска кода:

[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]

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

slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]

s1отsliceОт индекса 2 (закрытый интервал) до индекса 5 (открытый интервал, элемент фактически достигает индекса 4), длина равна 3, а емкость по умолчанию равна концу массива, что равно 8.s2отs1Индекс 2 (закрытый интервал) к индексу 6 (открытый интервал, элемент фактически получает индекс 5) и пропускная способность к индексу 7 (открытый интервал, действительно к индексу 6) равны 5.

slice origin

Затем, чтобыs2Добавьте элемент 100 в конец:

s2 = append(s2, 100)

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

append 100

снова кs2Добавить элемент 200:

s2 = append(s2, 100)

В настоящее время,s2Мощностей не хватает, пора расширяться. тогда,s2Начните с нуля, дублируйте исходные элементы в новых местах и ​​расширяйте свои возможности. и чтобы справиться с возможным будущимappendпринес еще одно расширение,s2Оставлю еще немного во время этого расширенияbuffer, новая емкость будет увеличена в 2 раза по сравнению с исходной емкостью, то есть 10.

append 200

Последний отзывs1Элемент с индексом 2:

s1[2] = 20

На этот раз будет затронут только элемент в соответствующей позиции исходного массива. это не влияетs2Ну, люди ушли далеко.

s1[2]=20

Еще один момент, распечататьs1, он будет распечатывать толькоs1элементы в пределах длины. Таким образом, будут напечатаны только 3 элемента, даже если его базовый массив содержит более 3 элементов.

Что касается, мы хотим увидеть, как они разделяют базовый массив на уровне сборки.Из-за нехватки места мы не будем расширять его здесь. Заинтересованные студенты могут ответить на фоне официального аккаунта:切片截取.

Я дам вам подробный анализ взаимосвязи вызовов функций, и поведение общего базового массива станет ясным с первого взгляда. См. QR-код внизу статьи.

В чем разница между срезом и массивом

Базовые данные среза — это массив, а срез — это инкапсуляция массива, которая описывает срез массива. Оба могут получить доступ к отдельным элементам путем подписки.

Массив имеет фиксированную длину и не может быть изменен после определения длины. В Go массивы встречаются редко, потому что их длина является частью типа, что ограничивает его выразительность, например.[3]intи[4]intПросто другой тип.

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

что именно делает append

Первый взглядappendПрототип функции:

func append(slice []Type, elems ...Type) []Type

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

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

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

append(slice, elem1, elem2)
append(slice, anotherSlice...)

Таким образом, приведенное выше использование неверно и не может быть скомпилировано.

Используйте append для добавления элементов в срез, эффективно добавляя элементы в базовый массив. Но длина базового массива фиксирована, если индексlen-1Указанный элемент уже является последним элементом базового массива и не может быть добавлен.

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

Новый фрагмент зарезервированbufferЕсть определенные правила по размерам. Большинство статей в Интернете описывают это так:

Когда исходная емкость слайса меньше1024, новая емкость слайса становится исходной2раз; исходная емкость слайса превышает1024, новая емкость слайса становится исходной1.25раз.

Я начну с вывода здесь: приведенное выше описание неверно.

Чтобы показать, что приведенное выше правило неверно, я написал небольшой кусок кода:

package main

import "fmt"

func main() {
	s := make([]int, 0)

	oldCap := cap(s)

	for i := 0; i < 2048; i++ {
		s = append(s, i)

		newCap := cap(s)

		if newCap != oldCap {
			fmt.Printf("[%d -> %4d] cap = %-4d  |  after append %-4d  cap = %-4d\n", 0, i-1, oldCap, i, newCap)
			oldCap = newCap
		}
	}
}

Сначала я создал пустойslice, а затем продолжайте идти внутрь в циклеappendновый элемент. Затем запишите изменение емкости, и всякий раз, когда емкость изменяется, записывайте старую емкость и емкость после добавления элементов и одновременно записывайте время.sliceэлементы в. Таким образом, я могу наблюдать, старые и новыеsliceизменения емкости, чтобы узнать закон.

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

[0 ->   -1] cap = 0     |  after append 0     cap = 1   
[0 ->    0] cap = 1     |  after append 1     cap = 2   
[0 ->    1] cap = 2     |  after append 2     cap = 4   
[0 ->    3] cap = 4     |  after append 4     cap = 8   
[0 ->    7] cap = 8     |  after append 8     cap = 16  
[0 ->   15] cap = 16    |  after append 16    cap = 32  
[0 ->   31] cap = 32    |  after append 32    cap = 64  
[0 ->   63] cap = 64    |  after append 64    cap = 128 
[0 ->  127] cap = 128   |  after append 128   cap = 256 
[0 ->  255] cap = 256   |  after append 256   cap = 512 
[0 ->  511] cap = 512   |  after append 512   cap = 1024
[0 -> 1023] cap = 1024  |  after append 1024  cap = 1280
[0 -> 1279] cap = 1280  |  after append 1280  cap = 1696
[0 -> 1695] cap = 1696  |  after append 1696  cap = 2304

Когда емкость старого слайса меньше 1024, емкость нового слайса действительно в два раза больше, чем у старого слайса. Пока это правильно.

Однако, когда старая емкость слайса больше или равна1024Когда ситуация изменилась. при добавлении элементов в срез1280, емкость старого слайса1280, который затем становится1696, оба не1.25умножить на отношение (1696/1280=1,325). добавлен1696После новой мощности2304Конечно, нет1696из1.25раз.

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

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

// go 1.9.5 src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
    // ……
    newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			for newcap < cap {
				newcap += newcap / 4
			}
		}
	}
	// ……
	
	capmem = roundupsize(uintptr(newcap) * ptrSize)
	newcap = int(capmem / ptrSize)
}

видеть это? Если посмотреть только на первую половину, о чем сейчас говорится в разных статьях в интернетеnewcapПравила правильные. Реальность такова, что вторая половина все еще праваnewcapсделал内存对齐, что связано со стратегией выделения памяти. После выравнивания памяти емкость нового слайса大于等于старая емкость среза2倍или1.25倍.

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

Наконец, чтобыgrowsliceВызывающая функция возвращает новый срез, длина которого не изменилась, но емкость которого увеличилась.

оappend, давайте, наконец, рассмотрим пример из [Правила расширения Golang Slice] в разделе «Ссылки».

package main

import "fmt"

func main() {
	s := []int{1,2}
	s = append(s,4,5,6)
	fmt.Printf("len=%d, cap=%d",len(s),cap(s))
}

Результат бега таков:

len=5, cap=6

Если резюмировать в разных статьях в интернете: когда длина исходного слайса меньше 1024, емкость каждый раз удваивается. При добавлении элемента 4 емкость становится равной 4, при добавлении элемента 5 емкость остается неизменной, при добавлении элемента 6 емкость удваивается до 8.

Результат выполнения приведенного выше кода:

len=5, cap=8

Это не правильно! Давайте подробнее разберемся, почему так происходит, и снова вынесем код:

// go 1.9.5 src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
    // ……
    newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		// ……
	}
	// ……
	
	capmem = roundupsize(uintptr(newcap) * ptrSize)
	newcap = int(capmem / ptrSize)
}

Параметры этой функции, в свою очередь,元素的类型,老的 slice,新 slice 最小求的容量.

примерsПолучается, что элементов всего 2,lenиcapоба 2,appendПосле трех элементов длина становится равной 3, а минимальная емкость становится равной 5, то есть вызовgrowsliceфункции, третий переданный параметр должен быть 5. которыйcap=5. С одной стороны,doublecapэто оригиналslice2 раза вместимость равна 4. встретить первогоifсостояние, такnewcapстановится 5.

Тогда позвониroundupsizeфункция, пройти 40. (PtrSize в коде относится к размеру указателя, который равен 8 на 64-битной машине)

Давайте еще раз посмотрим на выравнивание памяти, отойдемroundupsizeКод функции:

// src/runtime/msize.go:13
func roundupsize(size uintptr) uintptr {
	if size < _MaxSmallSize {
		if size <= smallSizeMax-8 {
			return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
		} else {
			//……
		}
	}
    //……
}

const _MaxSmallSize = 32768
const smallSizeMax = 1024
const smallSizeDiv = 8

Очевидно, мы в конечном итоге вернем результат этой формулы:

class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]

ЭтоGoДва связанных распределения памяти в исходном кодеslice.class_to_sizeпройти черезspanClassПолучатьspanразделенныйobjectразмер. иsize_to_class8выразить черезsizeвозьмиspanClass.

var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31}

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

мы прошли вsizeравняется 40. так(size+smallSizeDiv-1)/smallSizeDiv = 5;Получатьsize_to_class8Индекс в массиве5Элементы4;Получатьclass_to_sizeиндекс4Элементы48.

Наконец, емкость нового слайса равна6:

newcap = int(capmem / ptrSize) // 6

Что касается двух вышеперечисленных魔法数组Происхождение , пока не будет расширено.

Почему нулевой срез может добавляться напрямую

фактическиnil sliceилиempty sliceМожно получить расширение базового массива, вызвав функцию добавления. в конце концов позвонитьmallocgcЧтобы запросить часть памяти из диспетчера памяти Go, а затем назначить ее исходномуnil sliceилиempty slice, а потом превратился в "настоящего"slice.

В чем разница между передачей среза и указателем среза

Как мы упоминали ранее, slice на самом деле является структурой, состоящей из трех членов: len, cap, array. Указывает длину среза, емкость и адрес базовых данных соответственно.

Когда срез используется в качестве параметра функции, это обычная структура. На самом деле это легко понять: если вы передаете слайс напрямую, в представлении вызывающей стороны фактический параметр slice не будет изменен операцией в функции; если вы передаете указатель на слайс, в представлении вызывающей стороны исходный фрагмент будет изменен.

Значение значения заключается в том, что независимо от того, передается ли слайс или указатель слайса, если данные базового массива слайса изменены, будут отражены базовые данные фактического слайса параметра. Почему данные базового массива могут быть изменены? Это хорошо понятно: базовые данные являются указателем в структуре слайса, хотя сама структура слайса не будет изменена, то есть адрес базовых данных не будет изменен. Но с помощью указателя на базовые данные базовые данные среза можно изменить без проблем.

Адрес массива можно получить через поле массива слайса. В коде это напрямую через что-то вродеs[i]=10Эта операция изменяет значение базовых элементов массива среза.

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

Давайте взглянем на молодой и невежественный фрагмент кода:

package main

func main() {
	s := []int{1, 1, 1}
	f(s)
	fmt.Println(s)
}

func f(s []int) {
	// i只是一个副本,不能改变s中元素的值
	/*for _, i := range s {
		i++
	}
	*/

	for i := range s {
		s[i] += 1
	}
}

Запускаем, программа выдает:

[2 2 2]

Действительно изменены базовые данные исходного среза. Здесь передается копия среза вfв функции,sТолькоmainв функцииsкопия . существуетfвнутри функции, даsне изменяет внешний слойmainфункциональныйs.

Чтобы действительно изменить внешний слойslice, только назначьте возвращаемый новый слайс исходному слайсу или передайте указатель на слайс в функцию. Давайте посмотрим на другой пример:

package main

import "fmt"

func myAppend(s []int) []int {
	// 这里 s 虽然改变了,但并不会影响外层函数的 s
	s = append(s, 100)
	return s
}

func myAppendPtr(s *[]int) {
	// 会改变外层 s 本身
	*s = append(*s, 100)
	return
}

func main() {
	s := []int{1, 1, 1}
	newS := myAppend(s)

	fmt.Println(s)
	fmt.Println(newS)

	s = newS

	myAppendPtr(&s)
	fmt.Println(s)
}

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

[1 1 1]
[1 1 1 100]
[1 1 1 100 100]

myAppendфункция, хотя и измененнаяs, но это просто передача по значению и не влияет на внешнийs, поэтому результат, напечатанный в первой строке, по-прежнему[1 1 1].

иnewSэто новыйslice, который основан наsпринадлежит. Итак, он печатает добавление100Результат после:[1 1 1 100].

Наконец, будетnewSназначен наs,sТолько тогда он действительно становится новым срезом. После этого дайтеmyAppendPtrфункция проходит вs 指针, на этот раз он действительно изменился:[1 1 1 100 100].

Суммировать

До сих пор оsliceЧасть закончена, я не знаю, видели ли вы ее. Подведем наконец итоги:

  • Срез — это абстракция базового массива, описывающая его срез.
  • Слайс на самом деле представляет собой структуру с тремя полями: длина, емкость и адрес базовых данных.
  • Несколько срезов могут совместно использовать один и тот же базовый массив, и в этом случае изменения в одном из слайсов или в базовом массиве повлияют на другие слайсы.
  • appendФункция будет вызываться, когда емкости слайса будет недостаточноgrowsliceФункция получает требуемую память, которая называется расширением, и расширение изменит исходное положение элемента.
  • Стратегия расширения заключается не просто в расширении исходной емкости слайсов.2раз или1.25раз и операции с выравниванием по памяти. Расширенная емкость >= исходная емкость2раз или1.25раз.
  • Когда слайс используется непосредственно как параметр функции, элементы слайса могут быть изменены, но сам слайс изменить нельзя; если вы хотите изменить сам слайс, вы можете вернуть измененный слайс, а вызывающая функция может получить измененный срез или используйте указатель среза в качестве параметра функции.

Наконец, если вы считаете, что эта статья полезна для вас, пожалуйста, нажмите «Рекомендовать» в правом нижнем углу для меня, спасибо!

QR

использованная литература

[Code Cave "Глубокий анализ трех особых состояний "Slice" в языке Go"]nuggets.capable/post/684490…【Массив старых денег】nuggets.capable/post/684490…【Нарезанные старые деньги】nuggets.capable/post/684490…[исходный код интерфейса golang]i6448038.GitHub.IO/2018/10/01/…[исходный код интерфейса golang]легенда T read.com/2017/07/01/…【интерфейс】www.jishuwen.com/d/2C9z#tuit[Примечания к исследованию Go с открытым исходным кодом Rain Mark]github.com/qyuhen/book[Изображение среза очень красивое]halfrost.com/go_slice/[Правила расширения Golang Slice]Ответственность Джо.GitHub.IO/2017/05/Иди сюда…[срез как параметр]Блог Woohoo.cn на.com/services и другие полные серии/afraid/93…[Исходный код]IC tar Следующая остановка/2018/10/25/…【Перевод механизма добавления】В противном случае head.GitHub.IO/2017/05/24/…【сборка фрагментов】xargin.com/go-slice/【нарезка трюков】коло not.com/2017/03/22/…【Есть картинки】i6448038.GitHub.IO/2018/08/11/…【Суть ломтика】woohoo.fly snow.org/2018/12/21/…【Навыки использования слайсов】blog.thinker idea.com/201901/go/… да…[срез/массив, увеличение памяти]blog.thinker idea.com/201901/go/… да…