Irbisa · Шпаргалка

Слайсы, мапы и строки

Заголовок слайса, алиасинг при append, внутренности map, руны и strings.Builder

Junior-разработчик · 24 вопроса

  1. 01

    Чем массив отличается от слайса в Go и что хранит заголовок слайса?

    Массив представляет собой значение фиксированного размера, и длина входит в его тип, поэтому присваивание или передача копирует все элементы; слайс устроен как маленький заголовок (указатель на массив, len и cap), который смотрит на часть массива, общего с другими слайсами.

    arr := [4]int{1, 2, 3, 4}cp := arr // array: full copycp[0] = 99fmt.Println(arr[0]) // 1 s := arr[1:3] // slice over arr: len 2, cap 3…
  2. 02

    Чем nil-слайс отличается от пустого слайса и что можно делать с nil-слайсом?

    Нулевое значение слайса равно nil: заголовок с nil-указателем, len 0 и cap 0.

  3. 03

    Чем make([]T, n) отличается от make([]T, 0, n) и что пойдёт не так, если делать append в первый?

    make([]T, n) создаёт слайс, в котором уже лежат n нулевых значений (len n); make([]T, 0, n) пуст, но с местом на n элементов (len 0, cap n).

    in := []string{"a", "b", "c"} // wrong: three zero Rows first, then the three real onesout := make([]Row, len(in))for _, v := range in {	out = append(out, Row{Key: v})…
  4. 04

    Как сделать независимую копию слайса и почему copy в make([]T, 0, n) ничего не копирует?

    b := a копирует только заголовок, и оба слайса по-прежнему делят один массив; настоящей копии нужна новая память: slices.Clone(a) (Go 1.21+) или make нужной длины плюс copy.

    a := []int{1, 2, 3} b := a // header copy: same arrayb[0] = 99fmt.Println(a[0]) // 99…
  5. 05

    Что выведет этот цикл и как менять элементы слайса внутри цикла range?

    type Item struct {	Name  string	Count int} items := []Item{{"a", 1}, {"b", 2}}for _, it := range items {	it.Count++}fmt.Println(items)

    Выведет [{a 1} {b 2}]: в переменную it в range попадает копия каждого элемента, поэтому it.Count++ меняет копию, а слайс остаётся прежним.

  6. 06

    Почему len("Дмитрий") возвращает 14 и как считать, индексировать и перебирать символы строки в Go?

    Строка в Go устроена как неизменяемая последовательность байтов, обычно в UTF-8, поэтому len считает байты, а s[0] даёт один byte.

    name := "Дмитрий" fmt.Println(len(name))                    // 14 (bytes)fmt.Println(utf8.RuneCountInString(name)) // 7 (runes)fmt.Println(name[0])                      // 208 (one byte, not "Д")…
  7. 07

    Как проверить, есть ли ключ в мапе, и что будет при записи в мапу, которую не инициализировали?

    Чтение из мапы никогда не падает: отсутствующий ключ даёт нулевое значение, поэтому наличие проверяют отдельно, формой comma-ok v, ok := m[k], где ok равен true, только если ключ действительно есть; запись в nil-мапу паникует, чтение из неё безопасно.

    orders := map[string]int{"ada": 0} fmt.Println(orders["grace"]) // 0 (missing key, no panic)n, ok := orders["ada"]fmt.Println(n, ok) // 0 true (a stored zero)…
  8. 08

    Почему порядок обхода мапы в Go случайный и как получить стабильный порядок?

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

    m := map[string]int{"b": 2, "a": 1, "c": 3} for k, v := range m { // the order differs between runs	fmt.Println(k, v)}…
  9. 09

    Как отсортировать слайс структур по одному полю и по двум полям?

    Через slices.SortFunc(s, cmp) с функцией, которая возвращает отрицательное число, ноль или положительное, обычно собранной из cmp.Compare; cmp.Or добавляет второй ключ, который используется, только когда первый равен.

    type User struct {	Name string	Age  int} users := []User{{"Bob", 30}, {"Alice", 30}, {"Eve", 25}}…
  10. 10

    Слайс передаётся в функцию по значению или по ссылке? Что выведет эта программа?

    func add(s []int) {	s[0] = 100	s = append(s, 4)	s[1] = 200} func main() {	a := make([]int, 3, 10)	add(a)	fmt.Println(a, len(a), a[:4])}

    Go всё передаёт по значению, а значение слайса и есть его заголовок: функция получает свою копию указателя, len и cap, которая указывает на массив вызывающего.

  11. 11

    Как append увеличивает слайс и когда он выделяет новый массив?

    Если новые элементы помещаются в cap, append пишет в существующий массив и ничего не аллоцирует; иначе он выделяет массив побольше, копирует туда элементы и возвращает заголовок, указывающий на него.

    var sink []int var s []intprev := -1for i := 0; i < 2000; i++ {	s = append(s, i)…
  12. 12

    Из одного базового слайса делают два append. Что выведет этот код и почему?

    base := make([]int, 2, 3) // [0 0], one free slotx := append(base, 1)y := append(base, 2)fmt.Println(x, y) z := append(x, 3) // x is already fullz[0] = 7fmt.Println(x[0], z[0])

    Выведет [0 0 2] [0 0 2], а затем 0 7.

  13. 13

    Что делает трёхиндексное выражение среза s[low:high:max] и когда оно нужно?

    Оно режет как s[low:high], но ещё и задаёт ёмкость max - low, поэтому результат не дотянется до элементов за max.

    buf := []int{1, 2, 3, 4, 5} head := buf[:2]         // len 2, cap 5head = append(head, 99) // overwrites buf[2]fmt.Println(buf)        // [1 2 99 4 5]…
  14. 14

    Почему сборка строки через s += part в цикле медленная и когда брать strings.Builder, strings.Join или bytes.Buffer?

    Строки неизменяемы, поэтому каждый s += part выделяет новую строку и копирует в неё всё накопленное: n добавлений копируют O(n²) байт.

    // wrong: each += copies everything built so fars := ""for _, line := range lines {	s += line + "\n"}…
  15. 15

    Безопасно ли удалять ключи из мапы или добавлять их во время обхода range?

    Удаление безопасно и чётко определено: элемент, удалённый до того, как цикл до него дошёл, просто не будет выдан.

  16. 16

    Почему m["a"].Count++ не компилируется, если m имеет тип map[string]Item, и как обновить структуру, лежащую в мапе?

    Элементы мапы не адресуемы (мапа может переносить записи при росте), поэтому нельзя присвоить поле значения из мапы или взять его адрес (cannot assign to struct field m["a"].Count in map).

    type Item struct{ Count int } m := map[string]Item{"a": {Count: 1}}m["a"].Count++ // wrong: cannot assign to struct field m["a"].Count in map it := m["a"] // right: copy out, change, store back…
  17. 17

    Как устроена мапа в Go и что изменилось, когда Go 1.24 перешёл на Swiss tables?

    Мапа устроена как хеш-таблица: хеш ключа со случайным для каждой мапы сидом определяет, где искать, поэтому чтение, запись и удаление в среднем O(1).

  18. 18

    Безопасна ли мапа в Go для конкурентного использования? Что будет при одновременной записи и как защитить общую мапу?

    Нет. Одновременные чтения допустимы, но запись параллельно с любым другим обращением считается гонкой данных, и рантайм может остановить процесс с fatal error: concurrent map writes.

    type Cache struct {	mu sync.RWMutex	m  map[string]Entry} func NewCache() *Cache {…
  19. 19

    Что заменяют пакеты slices и maps и какие функции slices меняют переданный слайс?

    С Go 1.21 они закрывают циклы, которые раньше все писали руками (slices.Contains, Index, Sort, BinarySearch, Compact, maps.Clone).

    ids := []int{3, 1, 3, 2}fmt.Println(slices.Contains(ids, 2), slices.Index(ids, 3)) // true 0 slices.Sort(ids)                // in place: [1 2 3 3]ids = slices.Compact(ids)       // [1 2 3]ids = slices.Insert(ids, 1, 10) // [1 10 2 3]…
  20. 20

    Найдите баг: после removeExpired часть просроченных сессий остаётся в слайсе.

    type Session struct {	ID        string	ExpiresAt time.Time} // removeExpired drops every session that has expired.func removeExpired(sessions []Session, now time.Time) []Session {	for i := 0; i < len(sessions); i++ {		if sessions[i].ExpiresAt.Before(now) {			sessions = append(sessions[:i], sessions[i+1:]...)		}	}	return sessions}

    Цикл увеличивает i после удаления элемента, но append(sessions[:i], sessions[i+1:]...) только что сдвинул следующую сессию на индекс i, поэтому она не проверяется: из двух подряд идущих просроченных сессий вторая остаётся.

  21. 21

    Освобождает ли s = s[:5] остаток большого слайса? Как маленький подслайс удерживает в памяти большой массив?

    Нет. Пересрез меняет только заголовок; нижележащий массив остаётся выделенным, пока на него указывает хоть один слайс, поэтому, сохранив body[:64] от тела запроса в 4 МБ, вы держите живыми все 4 МБ.

    var keep []byte keep = body[:64] // wrong: pins the whole 4 MB bodykeep = bytes.Clone(body[:64]) // right: copy what you keep clear(s[n:]) // slice of pointers: drop the stale references…
  22. 22

    Удаление элемента через s = append(s[:i], s[i+1:]...) работает, но как же оно может давать утечку памяти и что делает иначе slices.Delete?

    append сдвигает хвост влево внутри того же массива и возвращает заголовок на один элемент короче, но в старом последнем слоте остаётся значение.

    // the old last pointer stays in the slot past len(sessions)sessions = append(sessions[:i], sessions[i+1:]...) // Go 1.22+: the same shift, the vacated slot zeroedsessions = slices.Delete(sessions, i, i+1)…
  23. 23

    Могут ли несколько горутин использовать один слайс или буфер []byte и как делать это безопасно?

    Чтение слайса, в который никто не пишет, безопасно; запись, пока другая горутина читает или пишет тот же элемент, считается гонкой данных (data race), и append тоже гоняется, потому что обновляет заголовок и может писать в общий массив.

    func process(buf []byte, workers int) {	size := (len(buf) + workers - 1) / workers	var wg sync.WaitGroup	for lo := 0; lo < len(buf); lo += size {		hi := min(lo+size, len(buf))		wg.Add(1)…
  24. 24

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

    type Registry struct {	users []User        // keeps insertion order for List	byID  map[int]*User // fast lookup by ID} func (r *Registry) Add(u User) {	r.users = append(r.users, u)	r.byID[u.ID] = &r.users[len(r.users)-1]} func (r *Registry) Rename(id int, name string) {	r.byID[id].Name = name} func (r *Registry) List() []User { return r.users }

    Add сохраняет указатель на текущий нижележащий массив слайса, но когда у append кончается ёмкость, он копирует users в новый массив; старые указатели по-прежнему смотрят на старую копию, поэтому Rename пишет в память, которую List никогда не читает.