суббота, 15 февраля 2020 г.

Массивы, срезы и строки: механика работы append в Golang

Одной из наиболее распространенных особенностей процедурных языков программирования является концепция массива. Массивы кажутся простыми вещами, но есть много вопросов, на которые нужно ответить при добавлении их в язык, таких как:

  • фиксированный размер или переменный размер?
  • размер является частью типа?
  • как выглядят многомерные массивы?
  • имеет ли пустой массив значение?

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

На ранней стадии разработки Go потребовалось около года, чтобы найти ответы на эти вопросы, прежде чем дизайн показался правильным. Ключевым этапом было введение срезов (slice), которые основаны на массивах фиксированного размера, чтобы обеспечить гибкую, расширяемую структуру данных. Тем не менее, по сей день программисты, плохо знакомые с Go, часто спотыкаются о то, как работают срезы, возможно, потому, что опыт других языков окрашивает их мышление.

В этом посте мы попытаемся прояснить путаницу. Мы сделаем это, собрав срезы, чтобы объяснить, как работает встроенная функция append и почему она работает так, как работает.

Массивы

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

Массивы не часто встречаются в программах Go, потому что размер массива является частью его типа, что ограничивает его выразительность.

Декларация

var buffer [256]byte

объявляет переменную buffer, которая содержит 256 байтов. Тип buffer включает его размер, [256]byte. Массив с 512 байтами будет иметь другой тип - [512]byte.

Данные, связанные с массивом, - это просто массив элементов. Схематически наш буфер выглядит так в памяти,

buffer: byte byte byte ... 256 раз ... byte byte byte

То есть переменная содержит 256 байтов данных и ничего больше. Мы можем получить доступ к его элементам с помощью знакомого синтаксиса индексации, buffer[0], buffer[1] и т. д. до buffer[255]. (Диапазон индекса от 0 до 255 охватывает 256 элементов.) Попытка индексировать буфер со значением вне этого диапазона приведет к сбою программы.

Существует встроенная функция len, которая возвращает количество элементов массива или среза, а также несколько других типов данных. Для массивов очевидно, что возвращает len. В нашем примере len(buffer) возвращает фиксированное значение 256.

Массивы имеют свое место - они, например, являются хорошим представлением матрицы преобразования - но их наиболее распространенная цель в Go - хранить память для среза.

Срезы: заголовок среза (slice header)

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

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

Учитывая нашу переменную массива buffer из предыдущего раздела, мы могли бы создать срез, который описывает элементы со 100 по 150 (точнее, со 100 по 149 включительно), разрезая массив:

var slice []byte = buffer[100:150]

В этом примере мы использовали полное объявление переменной, чтобы быть явными. Переменная slice имеет тип []byte, произносится как "срез байтов" и инициализируется из массива, называемого buffer, путем разбиения элементов от 100 (включительно) до 150 (исключая). Более идиоматический синтаксис отбрасывает тип, который устанавливается инициализирующим выражением:

var slice = buffer[100:150]

Внутри функции мы можем использовать краткую форму объявления,

slice := buffer[100:150]

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

type sliceHeader struct {
    Length        int
    ZerothElement *byte
}

slice := sliceHeader{
    Length:        50,
    ZerothElement: &buffer[100],
}

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

До сих пор мы использовали операцию среза в массиве, но мы также можем нарезать срез, например так:

slice2 := slice[5:10]

Как и прежде, эта операция создает новый срез, в данном случае с элементами с 5 по 9 (включительно) исходного среза, что означает элементы с 105 по 109 исходного массива. Базовая структура sliceHeader для переменной slice2 выглядит следующим образом:

slice2 := sliceHeader{
    Length:        5,
    ZerothElement: &buffer[105],
}

Обратите внимание, что этот header по-прежнему указывает на тот же базовый массив, хранящийся в переменной buffer.

Мы также можем создать срез заново, то есть срезать срез и сохранить результат обратно в исходную структуру slice. После

slice = slice[5:10]

структура sliceHeader для переменной slice выглядит так же, как и для переменной slice2. Вы увидите, что часто используется повторение, например, для усечения среза. Это утверждение удаляет первый и последний элементы нашего среза:

slice = slice[1:len(slice)-1]

Вы часто будете слышать, как опытные программисты на Go говорят о "slice header" (заголовок среза), потому что это именно то, что хранится в переменной среза. Например, когда вы вызываете функцию, которая принимает срез в качестве аргумента, например bytes.IndexRune, этот заголовок - это то, что передается в функцию. В этом вызове

slashPos := bytes.IndexRune(slice, '/')

аргумент slice, который передается в функцию IndexRune, фактически является "slice header".

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

Передача срезов в функции

Важно понимать, что хотя срез содержит указатель, он сам по себе является значением. Под обложками это значение структуры, содержащее указатель и длину. Это не указатель на структуру.

Это имеет значение.

Когда мы вызывали IndexRune в предыдущем примере, ему была передана копия заголовка среза. Такое поведение имеет важные последствия.

Рассмотрим эту простую функцию:

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}

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

Выполним:

func main() {
    slice := buffer[10:20]
    for i := 0; i < len(slice); i++ {
        slice[i] = byte(i)
    }
    fmt.Println("before", slice)
    AddOneToEachElement(slice)
    fmt.Println("after", slice)
}

Вывод:

before [0 1 2 3 4 5 6 7 8 9]
after [1 2 3 4 5 6 7 8 9 10]

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

Аргумент функции действительно является копией, как показано в следующем примере:

func SubtractOneFromLength(slice []byte) []byte {
    slice = slice[0 : len(slice)-1]
    return slice
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    newSlice := SubtractOneFromLength(slice)
    fmt.Println("After:  len(slice) =", len(slice))
    fmt.Println("After:  len(newSlice) =", len(newSlice))
}

Вывод:

Before: len(slice) = 50
After:  len(slice) = 50
After:  len(newSlice) = 49

Здесь мы видим, что содержимое аргумента slice может быть изменено функцией, но его заголовок не может. Длина, хранящаяся в переменной slice, не изменяется при вызове функции, поскольку функции передается копия заголовка slice, а не оригинал. Таким образом, если мы хотим написать функцию, которая изменяет заголовок, мы должны вернуть его в качестве параметра результата, как мы это сделали здесь. Переменная slice не изменяется, но возвращаемое значение имеет новую длину, которая затем сохраняется в newSlice,

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

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

func PtrSubtractOneFromLength(slicePtr *[]byte) {
    slice := *slicePtr
    *slicePtr = slice[0 : len(slice)-1]
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    PtrSubtractOneFromLength(&slice)
    fmt.Println("After:  len(slice) =", len(slice))
}

Вывод:

Before: len(slice) = 50
After:  len(slice) = 49

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

Допустим, мы хотели иметь метод на срезе, который усекает его на последнем слэше. Мы могли бы написать это так:

type path []byte

func (p *path) TruncateAtFinalSlash() {
    i := bytes.LastIndex(*p, []byte("/"))
    if i >= 0 {
        *p = (*p)[0:i]
    }
}

func main() {
    // Преобразование из строки в путь.
    pathName := path("/usr/bin/tso") 
    pathName.TruncateAtFinalSlash()
    fmt.Printf("%s\n", pathName)
}

Если вы запустите этот пример, вы увидите, что он работает правильно, обновляя срез в вызывающей стороне.

Вывод:

/usr/bin

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

type path []byte

func (p path) ToUpper() {
    for i, b := range p {
        if 'a' <= b && b <= 'z' {
            p[i] = b + 'A' - 'a'
        }
    }
}

func main() {
    pathName := path("/usr/bin/tso")
    pathName.ToUpper()
    fmt.Printf("%s\n", pathName)
}

Вывод:

/USR/BIN/TSO

Здесь метод ToUpper использует две переменные в конструкции for range для захвата элемента index и slice. Эта форма цикла позволяет избежать записи p[i] несколько раз в теле.

Емкость (capacity)

Посмотрите на следующую функцию, которая расширяет свой аргумент slice срез целых чисел на один элемент:

func Extend(slice []int, element int) []int {
    n := len(slice)
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

Теперь выполним

func main() {
    var iBuffer [10]int
    slice := iBuffer[0:0]
    for i := 0; i < 20; i++ {
        slice = Extend(slice, i)
        fmt.Println(slice)
    }
}

Вывод:

[0]
[0 1]
[0 1 2]
[0 1 2 3]
[0 1 2 3 4]
[0 1 2 3 4 5]
[0 1 2 3 4 5 6]
[0 1 2 3 4 5 6 7]
[0 1 2 3 4 5 6 7 8]
[0 1 2 3 4 5 6 7 8 9]
panic: runtime error: slice bounds out of range [:11] with capacity 10

Посмотрите, как срез растет, пока ... это не так.

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

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

Поле Capacity записывает, сколько места на самом деле имеет базовый массив; это максимальное значение, которое может достигать длина. Попытка вырастить срез за пределы его емкости выйдет за пределы массива и вызовет panic.

После того, как наш пример slice создан

slice := iBuffer[0:0]

его заголовок выглядит так:

slice := sliceHeader{
    Length:        0,
    Capacity:      10,
    ZerothElement: &iBuffer[0],
}

Поле Capacity равно длине базового массива минус индекс в массиве первого элемента среза (в данном случае ноль). Если вы хотите узнать, какая емкость для среза, используйте встроенную функцию cap:

if cap(slice) == len(slice) {
    fmt.Println("slice is full!")
}

Встроенная функция make

Что если мы хотим вырастить срез сверх его возможностей? Это невозможно! По определению, емкость - это предел роста. Но вы можете получить эквивалентный результат, выделив новый массив, скопировав данные и изменив срез, чтобы описать новый массив.

Начнем с выделения. Мы могли бы использовать встроенную функцию new, чтобы выделить больший массив и затем нарезать результат, но вместо этого проще использовать встроенную функцию make. Она выделяет новый массив и создает заголовок среза, чтобы описать его, все сразу. Функция make принимает три аргумента: тип среза, его начальную длину и его емкость, то есть длину массива, который make выделяет для хранения данных среза. Этот вызов создает фрагмент длиной 10 с местом для еще 5 (15-10), как вы можете увидеть, запустив его:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

Вывод:

len: 10, cap: 15

Этот пример удваивает емкость нашего int среза, но сохраняет его длину такой же:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
newSlice := make([]int, len(slice), 2*cap(slice))
for i := range slice {
    newSlice[i] = slice[i]
}
slice = newSlice
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

Вывод:

len: 10, cap: 15
len: 10, cap: 30

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

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

gophers := make([]Gopher, 10)

срез gophers имеет длину и емкость 10.

Встроенная функция copy

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

newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)

Функция copy умная. Она копирует только то, что может, обращая внимание на длину обоих аргументов. Другими словами, количество копируемых элементов является минимальной из длин двух срезов. Это может сэкономить немного расчетов. Кроме того, copy возвращает целочисленное значение, количество скопированных элементов, хотя это не всегда стоит проверять.

Функция copy также делает все правильно, когда источник и пункт назначения перекрываются, что означает, что ее можно использовать для перемещения элементов в одном срезе. Вот как использовать copy для вставки значения в середину среза.

// Insert вставляет значение в срез по указанному индексу,
// который должен быть в диапазоне.
// Срез должен иметь место для нового элемента.
func Insert(slice []int, index, value int) []int {
    // Вырастить срез на один элемент.
    slice = slice[0 : len(slice)+1]
    // Используем copy, чтобы переместить 
    // верхнюю часть среза в сторону и открыть отверстие.
    copy(slice[index+1:], slice[index:])
    // Сохраняем новое значение.
    slice[index] = value
    // Возвращаем результат.
    return slice
}

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

slice[i:]

означает тоже самое что и

slice[i:len(slice)]

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

slice[:]

просто означает сам срез, который полезен при разрезании массива. Это выражение является кратчайшим способом сказать "срез, описывающий все элементы массива":

array[:]

Теперь это не так, давайте запустим нашу функцию Insert.

// Отметьте емкость > длины: 
// пространство для добавления элемента.
slice := make([]int, 10, 20) 
for i := range slice {
    slice[i] = i
}
fmt.Println(slice)
slice = Insert(slice, 5, 99)
fmt.Println(slice)

Вывод:

[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 99 5 6 7 8 9]

Append: пример

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

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // Срез полон; должен расти.
        // Мы удваиваем его размер и добавляем 1, 
        // поэтому, если размер равен нулю, 
        // мы все равно наращиваем.
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

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

slice := make([]int, 0, 5)
for i := 0; i < 10; i++ {
    slice = Extend(slice, i)
    fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
    fmt.Println("address of 0th element:", &slice[0])
}

Вывод:

len=1 cap=5 slice=[0]
address of 0th element: 0x456000
len=2 cap=5 slice=[0 1]
address of 0th element: 0x456000
len=3 cap=5 slice=[0 1 2]
address of 0th element: 0x456000
len=4 cap=5 slice=[0 1 2 3]
address of 0th element: 0x456000
len=5 cap=5 slice=[0 1 2 3 4]
address of 0th element: 0x456000
len=6 cap=11 slice=[0 1 2 3 4 5]
address of 0th element: 0x454030
len=7 cap=11 slice=[0 1 2 3 4 5 6]
address of 0th element: 0x454030
len=8 cap=11 slice=[0 1 2 3 4 5 6 7]
address of 0th element: 0x454030
len=9 cap=11 slice=[0 1 2 3 4 5 6 7 8]
address of 0th element: 0x454030
len=10 cap=11 slice=[0 1 2 3 4 5 6 7 8 9]
address of 0th element: 0x454030

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

С надежной функцией Extend в качестве руководства мы можем написать еще более удобную функцию, которая позволяет нам расширять срез несколькими элементами. Для этого мы используем способность Go превращать список аргументов функции в срез при вызове функции. То есть мы используем вариационную (variadic) возможность функции Go.

Давайте назовем функцию Append. Для первой версии мы можем просто вызывать Extend несколько раз, чтобы механизм функции variadic был понятен. Сигнатура Append является следующей:

func Append(slice []int, items ...int) []int

Это говорит о том, что Append принимает один аргумент, slice, за которым следует ноль или более аргументов int. Эти аргументы представляют собой срез int, если говорить о реализации Append, как вы можете видеть:

// Append добавляет элементы к срезу.
// Первая версия: просто вызов Extend в цикле.
func Append(slice []int, items ...int) []int {
    for _, item := range items {
        slice = Extend(slice, item)
    }
    return slice
}

Обратите внимание на цикл for, проходящий по элементам аргумента items, который подразумевает тип []int. Также обратите внимание на использование пустого идентификатора _ для отбрасывания индекса в цикле, который нам не нужен в этом случае.

Запустим:

slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = Append(slice, 5, 6, 7, 8)
fmt.Println(slice)

Вывод:

[0 1 2 3 4]
[0 1 2 3 4 5 6 7 8]

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

slice := []int{0, 1, 2, 3, 4}

Функция Append интересна по другой причине. Мы можем не только добавлять элементы, мы можем добавить целый второй срез, "разорвав" его на аргументы, используя обозначение ... на месте вызова:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // '...' важно!
fmt.Println(slice1)

Вывод:

[0 1 2 3 4]
[0 1 2 3 4 55 66 77]

Конечно, мы можем сделать Append более эффективной, производя выделение не более одного раза, основываясь на внутренностях Extend:

// Append добавляет элементы к срезу.
// Эффективная версия.
func Append(slice []int, elements ...int) []int {
    n := len(slice)
    total := len(slice) + len(elements)
    if total > cap(slice) {
        // Перераспределяем. 
        // Выращиваем в 1,5 раза новый размер, 
        // чтобы мы могли расти.
        newSize := total*3/2 + 1
        newSlice := make([]int, total, newSize)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[:total]
    copy(slice[n:], elements)
    return slice
}

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

Выполним; поведение такое же, как и раньше:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // '...' важно!
fmt.Println(slice1)

Вывод:

[0 1 2 3 4]
[0 1 2 3 4 55 66 77]

Append: встроенная функция

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

Слабым местом Go является то, что любые операции универсального типа должны предоставляться средой выполнения. Когда-нибудь это может измениться, но сейчас, чтобы упростить работу со срезами, Go предоставляет встроенную универсальную функцию append. Она работает так же, как наша версия int среза, но для любого типа среза.

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

Вот примеры использования append:

// Создаем пару новых срезов.
slice := []int{1, 2, 3}
slice2 := []int{55, 66, 77}
fmt.Println("Start slice: ", slice)
fmt.Println("Start slice2:", slice2)

// Добавляем элемент в срез.
slice = append(slice, 4)
fmt.Println("Add one item:", slice)

// Добавляем один срез в другой.
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)

// Делаем копию среза (int).
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)

// Копируем срез в свой конец.
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)

Вывод:

Start slice:  [1 2 3]
Start slice2: [55 66 77]
Add one item: [1 2 3 4]
Add one slice: [1 2 3 4 55 66 77]
Copy a slice: [1 2 3 4 55 66 77]
Before append to self: [1 2 3 4 55 66 77]
After append to self: [1 2 3 4 55 66 77 1 2 3 4 55 66 77]

Nil

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

sliceHeader{
    Length:        0,
    Capacity:      0,
    ZerothElement: nil,
}

или просто

sliceHeader{}

Ключевым моментом является то, что указатель элемента также равен nil. Срез, созданный

array[0:0]

имеет нулевую длину (и, возможно, даже нулевую емкость), но его указатель не равен nil, поэтому это не нулевой срез.

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

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

Строки

Теперь краткий раздел о строках в Go в контексте срезов.

Строки на самом деле очень просты: это просто куски байтов только для чтения с небольшой дополнительной синтаксической поддержкой языка.

Поскольку они доступны только для чтения, нет необходимости в емкости (вы не можете увеличить их), но в большинстве случаев вы можете обращаться с ними так же, как с байтами только для чтения.

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

slash := "/usr/ken"[0] // возвращает значение байта '/'.

Мы можем нарезать строку, чтобы получить подстроку:

usr := "/usr/ken"[0:4] // возвращает строку "/usr"

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

Мы также можем взять обычный срез байтов и создать из него строку с простым преобразованием:

str := string(slice)

или обратное преобразование:

slice := []byte(usr)

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

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

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

Заключение

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

Как только вы оцените, как они работают, срезы становятся не только простыми в использовании, но и мощными и выразительными, особенно с помощью встроенных функций copy и append.


Читайте также:


Комментариев нет:

Отправить комментарий