golang

Большой разбор Слайсов, Типы и структуры данных Go

  • воскресенье, 29 июня 2025 г. в 00:00:08
https://habr.com/ru/articles/922736/

Привет, меня зовут Рома! Какое-то время назад я захотел изучить всю внутрянку Go, заглянуть в исходники языка и понять, почему все устроено так, как устроено. В этот самый момент я обнаружил, что на просторах интернета практически отсутствуют материалы, которые подробно разбирают типы данных, их вспомогательные функции, детали реализации runtime и так далее. Мной было принято решение сделать это самостоятельно. Изначально я занимался этим для себя, но позже решил, что стоит поделиться моими наблюдениями и выводами с миром.

Представляю вам первую статью из цикла "Типы и структуры данных Go"! Здесь мы познакомимся со Слайсами, разберем внутреннюю реализацию этого типа и его вспомогательных функций. Приятного аппетита!


Знакомство

Слайсы представляют собой последовательности переменной длины, элементы которых имеют одинаковый тип. Тип слайса записывается как []T, где элементы имеют тип T; он выглядит как тип массива без размера.

Источник: Alan A. A. Donovan, Brian W. Kernighan - «The Go Programming Language»


Внутренняя реализация

SliceType из go/src/internal/abi/type.go описывает тип среза в рантайме. Встраивает Type — общую информацию о типе (размер, выравнивание, флаги и прочее), и добавляет поле Elem, которое указывает на тип элементов среза. Например, тип []int будет представлен как SliceType, где Elem указывает на тип int.

  • SliceType

    type SliceType struct {
    	Type       // Встроенная общая информация о типе
    	Elem *Type // Указатель на тип элементов среза
    }

Структура выше не является описанием того, как срезы хранятся в памяти. Для этой цели существует структура slice из go/src/runtime/slice.go. Здесь важно понимать, что slice просто ОПИСАНИЕ для разработчиков, вспомогательный низкоуровневый мост, если хотите. Она не используется в обычном Go-коде и рантайме при компиляции.

  • slice

    type slice struct {
    	array unsafe.Pointer
    	len   int
    	cap   int
    }

Основные операции

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

Через []тип{} и make([]тип, длина, вместимость)

Выражение, создающее слайс s, отличается от создания массива a. Литерал слайса выглядит как литерал массива — последовательность значений, разделённых запятыми и заключённых в фигурные скобки — но без указания размера. Это неявно создаёт массив нужного размера и возвращает слайс, указывающий на него. Как и в случае с массивами, литералы слайсов могут:

  • задавать значения по порядку,

  • явно указывать индексы,

  • или сочетать оба подхода.

Источник: Alan A. A. Donovan, Brian W. Kernighan - «The Go Programming Language»

Другой способ создания среза представляет функция make() - она позволяет создать срез из нескольких элементов, которые будут иметь значения по умолчанию. Она имеет следующую форму:

  • Код

    имя_среза := make([] тип_элементов_среза, длина_среза, емкость_среза)

Пример использования:

  • Код

    var users []string = make([]string, 3) // длина среза - 3
    
    users[0] = "Tom"
    users[1] = "Alice"
    users[2] = "Bob"

Источник: < metanit.com >

Самой функции не существует, так как компилятор отвечает за генерацию вызова. Однако мы можем найти отдельные компоненты это процесса в go/src/runtime/slice.go.

makeslice выделяет память под срез длиной len и вместимостью cap. et — это тип элементов среза (используется для определения размера). makeslice64 — версия makeslice с параметрами int64. Приводит int64 к int и проверяет переполнение.

  • makeslice и makeslice64

    func makeslice(et *_type, len, cap int) unsafe.Pointer {
    	mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
    	if overflow || mem > maxAlloc || len < 0 || len > cap {
    	
    		/*
    		
      		  ПРИМЕЧАНИЕ: Генерируем ошибку 'len out of range', а не
    		  'cap out of range', если кто-то делает make([]T, очень_большое_число).
    		  Формально и 'cap out of range' тоже верно, но поскольку вместимость
    		  задаётся неявно, сообщение про длину более понятно.
    		  
    		  См. golang.org/issue/4085.
    		  
    		*/
    		
    		mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
    		if overflow || mem > maxAlloc || len < 0 {
    			panicmakeslicelen() // паника: длина выходит за пределы
    		}
    		panicmakeslicecap() // паника: вместимость выходит за пределы
    	}
    
    	return mallocgc(mem, et, true) // выделяем память через сборщик мусора
    }
    
    func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer {
    	len := int(len64)
    	if int64(len) != len64 {
    		panicmakeslicelen()
    	}
    
    	cap := int(cap64)
    	if int64(cap) != cap64 {
    		panicmakeslicecap()
    	}
    
    	return makeslice(et, len, cap)
    }

Из среза

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

  • рассчитывает ptr := &a[i]

  • выставляет len := j - i, cap := cap(a) - i

  • вставляет проверки границ (i <= j, j <= cap(a) и т. д.)

  • вызывает панику при ошибке

nil-слайс

Значение по умолчанию (zero value) для типа слайса — это nil. nil-слайс не имеет базового массива. Такой слайс имеет длину и ёмкость равные нулю. Однако существуют не-nil слайсы с нулевой длиной и ёмкостью, например:

  • Код

    []int{}
    
    make([]int, 3)[3:]

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

  • Код

    []int(nil)
  • Код

    var s []int // len(s) == 0, s == nil
    
    s = nil // len(s) == 0, s == nil
    s = []int(nil) // len(s) == 0, s == nil
    s = []int{} // len(s) == 0, s != nil

Поэтому, если нужно проверить, пуст ли слайс, используйте:

  • Код

    len(s) == 0

а не:

  • Код

    s == nil

За исключением сравнения с nil, nil-слайс ведёт себя как обычный слайс длины 0. Если не указано явно иное, функции Go должны одинаково обрабатывать все слайсы длины 0, будь то nil или нет.

Источник: Alan A. A. Donovan, Brian W. Kernighan - «The Go Programming Language»

Компилятор просто объявляет переменную, у которой s.ptr = nil, s.len = 0, s.cap = 0. Никаких вызовов рантайма нет, потому что ничего не аллоцируется.

Доступ к элементам

В Go доступ к элементу среза по индексу (s[i]) не вызывает отдельной функции в рантайме. Это поведение встраивается непосредственно компилятором (gc) как часть генерации машинного кода.

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

Добавление элементов

Встроенная функция append добавляет элементы в слайсы:

  • Код

    var runes []rune
    
    for _, r := range "Hello, BF" {
        runes = append(runes, r)
    }
    
    fmt.Printf("%q\\n", runes) // "['H' 'e' 'l' 'l' 'o' ' ' 'B' 'F']"

Цикл использует append, чтобы построить слайс из девяти рун, закодированных в строковом литерале, хотя конкретно эту задачу удобнее решить через встроенное преобразование: []rune("Hello, BF").

Нельзя также предполагать, что изменения в старом слайсе будут отражаться в новом, или наоборот. Поэтому стандартная практика — всегда присваивать результат append обратно в переменную:

  • Код

    runes = append(runes, r)

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

Наша appendInt добавляет один элемент. Встроенная append позволяет добавлять несколько:

  • Код

    var x []int
    
    x = append(x, 1)
    x = append(x, 2, 3)
    x = append(x, 4, 5, 6)
    x = append(x, x...) // добавить сам себя
    
    fmt.Println(x) // [1 2 3 4 5 6 1 2 3 4 5 6]

Небольшое изменение делает appendInt вариативной функцией:

  • Код

    func appendInt(x []int, y ...int) []int {
        var z []int
        
        zlen := len(x) + len(y)
        
        // ... увеличить z до как минимум zlen ...
        copy(z[len(x):], y)
        
        return z
    }

Источник: Alan A. A. Donovan, Brian W. Kernighan - «The Go Programming Language»

append() в чистом виде не существует, она генерируется компилятором Go, поэтому искать ее реализацию бессмысленно. Однако в go/src/runtime/slice.go мы можем увидеть функции, которые append() задействует.

Функция growslice, ответственна за перераспределение памяти, когда при добавлении append() длина превышает емкость. Аргументы: oldPtr — указатель на исходный массив среза, newLen — новая длина (= oldLen + num), oldCap — исходная вместимость среза, num — количество добавляемых элементов, et — тип элементов. Возвращаемые значения: newPtr — указатель на новый буфер, newLen — то же значение, что и в аргументе, newCap — вместимость нового буфера. Требуется, чтобы uint(newLen) > uint(oldCap). Предполагается, что исходная длина среза равна newLen - num. Выделяется новый буфер минимум под newLen элементов. Существующие элементы [0:oldLen] копируются в новый буфер. Добавленные элементы [oldLen, newLen] не инициализируются growslice (хотя для типов с указателями они зануляются). Инициализация добавленных элементов должна выполняться вызывающей стороной. Элементы после newLen [newLen, newCap] зануляются. Особенность growslice — нестандартное соглашение о вызове, что упрощает генерируемый код, который её вызывает. В частности, она принимает и возвращает новую длину, чтобы старая длина не была "живой" (не нужно сохранять/восстанавливать), а новая длина возвращается без дополнительных затрат.

  • growslice

    func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
    	oldLen := newLen - num
    	if raceenabled {
    		callerpc := sys.GetCallerPC()
    		racereadrangepc(oldPtr, uintptr(oldLen*int(et.Size_)), callerpc, abi.FuncPCABIInternal(growslice))
    	}
    	if msanenabled {
    		msanread(oldPtr, uintptr(oldLen*int(et.Size_)))
    	}
    	if asanenabled {
    		asanread(oldPtr, uintptr(oldLen*int(et.Size_)))
    	}
    
    	if newLen < 0 {
    		panic(errorString("growslice: len out of range"))
    	}
    
    	if et.Size_ == 0 {
    	
    		/*
    
    			append не должен создавать срез с nil-указателем, но ненулевой длиной.
    			Предполагается, что append не требует сохранения oldPtr в этом случае.
    
    		*/
    		
    		return slice{unsafe.Pointer(&zerobase), newLen, newLen}
    	}
    
    	newcap := nextslicecap(newLen, oldCap)
    
    	var overflow bool
    	var lenmem, newlenmem, capmem uintptr
    	
    	/*
    
    		Специализация для популярных размеров et.Size_:
    		- Для 1 не нужна деление/умножение.
    		- Для размера указателя (goarch.PtrSize) компилятор оптимизирует деление в сдвиг.
    		- Для степеней двойки используется сдвиг по переменной.
    
    		noscan — флаг отсутствия указателей в типе.
    
    	*/
    	
    	noscan := !et.Pointers()
    	switch {
    	case et.Size_ == 1:
    		lenmem = uintptr(oldLen)
    		newlenmem = uintptr(newLen)
    		capmem = roundupsize(uintptr(newcap), noscan)
    		overflow = uintptr(newcap) > maxAlloc
    		newcap = int(capmem)
    	case et.Size_ == goarch.PtrSize:
    		lenmem = uintptr(oldLen) * goarch.PtrSize
    		newlenmem = uintptr(newLen) * goarch.PtrSize
    		capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
    		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
    		newcap = int(capmem / goarch.PtrSize)
    	case isPowerOfTwo(et.Size_):
    		var shift uintptr
    		if goarch.PtrSize == 8 {
    			// Маска сдвига для улучшения генерации кода.
    			shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
    		} else {
    			shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
    		}
    		lenmem = uintptr(oldLen) << shift
    		newlenmem = uintptr(newLen) << shift
    		capmem = roundupsize(uintptr(newcap)<<shift, noscan)
    		overflow = uintptr(newcap) > (maxAlloc >> shift)
    		newcap = int(capmem >> shift)
    		capmem = uintptr(newcap) << shift
    	default:
    		lenmem = uintptr(oldLen) * et.Size_
    		newlenmem = uintptr(newLen) * et.Size_
    		capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
    		capmem = roundupsize(capmem, noscan)
    		newcap = int(capmem / et.Size_)
    		capmem = uintptr(newcap) * et.Size_
    	}
    
    	/*
    
    		Проверка overflow и capmem > maxAlloc необходима,
    		чтобы избежать переполнения, которое может вызвать segfault
    		на 32-битных архитектурах. Например, при таком коде:
    
    			type T [1<<27 + 1]int64
    			var d T
    			var s []T
    
    			func main() {
    				s = append(s, d, d, d, d)
    				print(len(s), "\\n")
    			}
    
    	*/
    	
    	if overflow || capmem > maxAlloc {
    		panic(errorString("growslice: len out of range"))
    	}
    
    	var p unsafe.Pointer
    	if !et.Pointers() {
    	
    		/*
    
    			Если в типе нет указателей, выделяем память без GC.
    			Обнуляем только область после newLen, поскольку
    			append будет перезаписывать новые элементы.
    
    		*/
    		
    		p = mallocgc(capmem, nil, false)
    		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    	} else {
    	
    		/*
    
    			Для типов с указателями нужно выделять память с учётом GC,
    			чтобы GC не сканировал неинициализированную память.
    
    			Если включён writeBarrier и есть старые элементы,
    			то вызывается bulkBarrierPreWriteSrcOnly для корректного
    			отслеживания указателей.
    
    		*/
    		
    		p = mallocgc(capmem, et, true)
    		if lenmem > 0 && writeBarrier.enabled {
    			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
    		}
    	}
    	memmove(p, oldPtr, lenmem)
    
    	return slice{p, newLen, newcap}
    }

nextslicecap вычисляет подходящую новую вместимость (capacity) для среза. Аргументы: newLen — желаемая длина нового среза, oldCap — текущая вместимость среза. Возвращаемое значение: рассчитанная вместимость (newcap), достаточная для размещения newLen элементов. Проверка на переполнение реализована через сравнение uint(newcap) >= uint(newLen), так как при переполнении newcap становится отрицательным, и это выражение всё ещё будет верным. Если расчёт newcap привёл к нулю или отрицательному значению (переполнение) — возвращается newLen как безопасное значение.

Алгоритм:

  • Если newLen превышает удвоенную текущую вместимость — сразу возвращается newLen.

  • Иначе, если текущая вместимость меньше порога (256), newcap удваивается.

  • Если больше — применяется увеличение примерно на 1.25x с плавным переходом между стратегиями.

  • nextslicecap

    func nextslicecap(newLen, oldCap int) int {
    	newcap := oldCap
    	doublecap := newcap + newcap
    	if newLen > doublecap {
    		return newLen
    	}
    
    	const threshold = 256
    	if oldCap < threshold {
    		return doublecap
    	}
    	for {
    	
    		/*
    		
    			Переход от роста x2 для маленьких срезов
    		  к росту x1.25 для больших.
    		  Формула даёт относительно плавный переход.
    		  newcap += (newcap + 3*threshold) >> 2
    
    		  Проверка на достижение нужной длины и на переполнение.
    		  Если newcap переполнится, uint(newcap) > uint(newLen) всё равно будет true.
    		  
    		*/
    		
    		if uint(newcap) >= uint(newLen) {
    			break
    		}
    	}
    
    	// В случае переполнения возвращаем минимально допустимое значение
    	if newcap <= 0 {
    		return newLen
    	}
    	return newcap
    }
    
    /*
    
    	reflect_growslice должен быть внутренней функцией,
    	но он используется внешними пакетами через linkname.
    
    	Замеченные нарушители:
    		- github.com/cloudwego/dynamicgo
    
    	Не удаляйте и не изменяйте сигнатуру функции.
    	См. <https://go.dev/issue/67401>.
    
    */
    
    // go:linkname reflect_growslice reflect.growslice
    
    func reflect_growslice(et *_type, old slice, num int) slice {
    
    	/*
    
    		Функция семантически аналогична slices.Grow,
    		однако вызывающая сторона обязана гарантировать,
    		что old.len + num > old.cap.
    
    		Сначала num уменьшается на количество уже доступных элементов
    		в [old.len:old.cap], чтобы сохранить память.
    
    	*/
    	
    	num -= old.cap - old.len
    	new := growslice(old.array, old.cap+num, old.cap, num, et)
    
    	/*
    
    		growslice не очищает память в диапазоне [old.cap:new.len],
    		поскольку предполагается, что он будет перезаписан через append().
    
    		Но reflect_growslice вызывается не из append(),
    		поэтому нужно явно занулить эту часть перед возвратом среза.
    
    	*/
    	
    	if !et.Pointers() {
    		oldcapmem := uintptr(old.cap) * et.Size_
    		newlenmem := uintptr(new.len) * et.Size_
    		memclrNoHeapPointers(add(new.array, oldcapmem), newlenmem-oldcapmem)
    	}
    
    	// Сохраняем исходную длину
    	new.len = old.len
    	return new
    }

Удаление элементов

В Go нет встроенной функции удаления элементов из среза, как delete() в map или remove() в других языках. Вместо этого используется срезание (slicing) и копирование.

  • Код

    s := []int{10, 20, 30, 40, 50}
    
    i := 2
    
    s = append(s[:i], s[i+1:]...) // s = [10 20 40 50]

Без сохранения порядка (быстрее):

  • Код

    s[i] = s[len(s)-1]
    s = s[:len(s)-1]

С сохранением порядка:

  • Код

    copy(s[i:], s[i+1:])
    s = s[:len(s)-1]

На уровне runtime нет отдельной функции удаления из среза — вся логика реализуется пользователем на уровне языка.

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

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

Стандартная библиотека предоставляет высокоэффективную функцию bytes.Equal для сравнения двух слайсов байтов ([]byte), но для слайсов других типов мы должны реализовать сравнение вручную:

  • Код

    func equal(x, y []string) bool {
        if len(x) != len(y) {
            return false
        }
        for i := range x {
            if x[i] != y[i] {
                return false
            }
        }
        return true
    }

С учётом того, насколько естественным кажется такое “глубокое” сравнение и что оно не дороже по времени выполнения, чем == для массивов строк, может показаться странным, что сравнение слайсов не работает аналогичным образом.

Есть две причины, почему “глубокое” равенство проблематично:

  1. В отличие от элементов массива, элементы слайса косвенные (indirect), поэтому возможно, что слайс может содержать сам себя. Хотя существуют способы справиться с такими случаями, ни один из них не является простым, эффективным и, что особенно важно, очевидным.

  2. Из-за того, что элементы слайса косвенные, фиксированное значение слайса может содержать разные элементы в разное время, поскольку содержимое базового массива может изменяться. Так как хеш-таблицы, например тип map в Go, создают только поверхностные копии ключей, требуется, чтобы результат сравнения ключей оставался постоянным в течение всей жизни хеш-таблицы. Поэтому “глубокое” равенство сделало бы слайсы непригодными для использования в качестве ключей.

Для ссылочных типов, таких как указатели и каналы, оператор == проверяет идентичность ссылок — то есть, указывают ли два значения на одно и то же. Аналогичная “поверхностная” проверка для слайсов могла бы быть полезной и решила бы проблему с map, но несогласованность в трактовке == для массивов и слайсов была бы слишком запутанной. Самый безопасный выбор — просто запретить сравнение слайсов.

Единственное допустимое сравнение слайса — это сравнение с nil, например:

  • Код

    if summer == nil {
        // ...
    }

Источник: Alan A. A. Donovan, Brian W. Kernighan - «The Go Programming Language»

На уровне райнтайма нет отдельной функции для сравнения срезов, потому что cравнение — задача уровня пользователя, cрез — это структура из трех полей (data, len, cap), и сравнение по значению data (указателя) — это просто a.data == b.data, но не поэлементное сравнение.

Копирование элементов

Встроенная функция copy копирует элементы из исходного среза src в целевой срез dst.

  • Код

    func copy(dst, src []Type) int

Она возвращает количество скопированных элементов, которое равно минимуму из len(dst) и len(src).

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

Источник: < yourbasic.org >

copy() — это встроенная функция Go. Она обрабатывается компилятором на этапе компиляции, а не как обычная функция. Компилятор сам генерирует вызов memmove. Код memmove написан на ассемблере и будет разниться в зависимости от архитектуры.

  • memmove для arm64

    // Register map
    //
    // dstin  R0          - указатель назначения (исходно)
    // src    R1          - указатель источника
    // count  R2          - количество байт для копирования
    // dst    R3          - указатель назначения, может изменяться при невыравненных копированиях
    // srcend R4          - указатель сразу после конца источника (src + count)
    // dstend R5          - указатель сразу после конца назначения (dst + count)
    // data   R6-R17      - временные регистры для загрузки и сохранения данных
    // tmp1   R14         - временный регистр
    
    // Основная идея: копирование разбито на 3 основных случая:
    // - маленькие копии до 32 байт
    // - средние копии до 128 байт
    // - большие копии свыше 128 байт
    //
    // Для больших копий используется программно-конвейерный цикл,
    // который обрабатывает по 64 байта за итерацию.
    // Указатель назначения выравнивается по 16 байтам, чтобы уменьшить
    // количество невыравненных обращений к памяти.
    // Оставшиеся байты (хвост) копируются всегда начиная с конца.
    
    // func memmove(to, from unsafe.Pointer, n uintptr)
    // Начало функции memmove, атрибуты NOSPLIT|NOFRAME — не используется стэк-фрейм и нет предостановки планировщика
    
    // CBZ R2, copy0
    // Если count (R2) равен нулю, переход к copy0 (ничего не копируем)
    
    // Маленькие копии: 1..16 байт
    // CMP $16, R2
    // BLE copy16
    // Если count <= 16, переход к copy16
    
    // Большие копии:
    // CMP $128, R2
    // BHI copy_long
    // Если count > 128, переход к copy_long (большие копии)
    
    // CMP $32, R2
    // BHI copy32_128
    // Если count > 32, переход к copy32_128 (средние копии)
    
    // Маленькие копии: 17..32 байт.
    // LDP (R1), (R6, R7)     - загрузка первых 16 байт из источника (парные регистры)
    // ADD R1, R2, R4         - вычисление srcend (адрес после конца исходных данных)
    // LDP -16(R4), (R12, R13) - загрузка последних 16 байт из источника
    // STP (R6, R7), (R0)     - сохранение первых 16 байт в назначение
    // ADD R0, R2, R5         - вычисление dstend (адрес после конца назначения)
    // STP (R12, R13), -16(R5) - сохранение последних 16 байт в назначение (с конца)
    // RET                    - выход из функции
    
    // Далее идет обработка маленьких копий длиной 1..16 байт, разбитая на случаи копирования
    // 8, 4, 2, 1 байт с использованием соответствующих команд загрузки и сохранения.
    
    // Средние копии 33..128 байт:
    // Загрузка и сохранение блоками по 16 байт с обеих сторон с проверкой длинны,
    // с последующим выходом из функции.
    
    // Большие копии более 128 байт:
    // - Проверка длины для выбора режима копирования
    // - Выравнивание указателей для оптимальной работы с памятью
    // - Обработка возможного перекрытия областей (копирование назад, если перекрытие есть)
    // - Программно-конвейерное копирование блоками по 64 байта в цикле
    // - Копирование хвоста (оставшихся байт)
    
    // Копирование назад (copy_long_backward) для перекрывающихся областей:
    // - Выравнивание концов источника и назначения
    // - Циклическое копирование блоками по 64 байта с конца к началу
    
    // Конец функции с возвратом после завершения копирования.

Получение длины среза

В Go функция len() для срезов является компиляторно-встроенной. То есть она вытаскивает поля из структуры среза SliceType напрямую, без вызова внешней функции.

Получение вместимости среза

В Go функция cap() для срезов является компиляторно-встроенной. То есть она вытаскивает поля из структуры среза SliceType напрямую, без вызова внешней функции.

Источники

  1. Alan A. A. Donovan, Brian W. Kernighan - «The Go Programming Language»

  2. Go Source Code < github.com >

  3. YourBasic WebSite < yourbasic.org >