Menu

Slice w Golang: append, make, len a cap i błąd współdzielenia

Slice'y to codzienny typ listy w Go. Poznaj make, append i wzrost, len a cap, to, jak wycinanie współdzieli tablicę bazową (i jaki błąd z tego wynika), copy, pakiet slices, slice'y 2D oraz nil a pusty slice.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Slice w jednym przykładzie

Slice to rosnący widok na tablicę. Tworzysz go literałem albo przez make, powiększasz przez append, a rozmiar odczytujesz przez len.

[]string nie ma długości w nawiasach. Tym różni się od typu tablicy, takiego jak [4]string, którego rozmiar jest stały.

Czym jest slice

Wartość slice'a to mały nagłówek z trzema polami:

  • wskaźnik na element w tablicy bazowej
  • długość: ile elementów widzi slice
  • pojemność: ile elementów istnieje od tego wskaźnika do końca tablicy bazowej

Kopiowanie slice'a (przypisanie, przekazanie do funkcji) kopiuje tylko ten nagłówek. Obie kopie wskazują na te same elementy. Prawie każda niespodzianka ze slice'ami wynika z zapomnienia o tym.

make: długość i pojemność

make([]T, len, cap) alokuje tablicę bazową i zwraca slice na niej. Pojemność jest opcjonalna i domyślnie równa długości.

Częsta pomyłka: make([]int, 5), a po nim pięć wywołań append, daje dziesięć elementów, z których pierwsze pięć to zera. Użyj make([]int, 0, 5), gdy planujesz dopisywać, albo make([]int, 5), gdy planujesz przypisywać po indeksie.

Rezerwowanie pojemności, gdy znasz końcowy rozmiar, pozwala uniknąć wielokrotnego powiększania. To optymalizacja, a nie wymóg.

append i wzrost

append dodaje elementy za bieżącą długością. Jeśli pojemność wystarcza, zapisuje w miejscu. Jeśli nie, alokuje większą tablicę, kopiuje do niej stare elementy i zwraca slice wskazujący na nową tablicę.

Pojemność mniej więcej się podwaja, dopóki slice jest mały, a po przekroczeniu 256 elementów rośnie wolniej (zbliżając się do 1,25x). Dokładne liczby to szczegół runtime'u, który zmieniał się między wersjami Go, więc nigdy nie pisz kodu, który od nich zależy.

Ponieważ append może zwrócić inną tablicę, musisz użyć jego wyniku:

append(s, 4)     // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct

Dopisanie całego slice'a używa składni rozwinięcia: s = append(s, other...).

Wycinanie współdzieli tablicę bazową

s[low:high] tworzy nowy nagłówek slice'a na tej samej tablicy, obejmujący indeksy od low do high, bez high. Każdą z granic można pominąć. Żadne elementy nie są kopiowane.

Wynik:

[10 99 30 40 50]
3 4

To współdzielenie sprawia, że wycinanie jest tanie. Jest też źródłem najsłynniejszego błędu ze slice'ami.

Błąd aliasingu przy append

Gdy podslice ma wolną pojemność, dopisywanie do niego zapisuje w tablicy rodzica, nadpisując elementy, których rodzic nadal używa.

Wynik:

[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]

a i b współdzielą miejsce w tablicy za "cmd", więc drugie dopisanie nadpisało pierwsze. Kod wygląda dobrze i działa za każdym razem, gdy pojemność akurat jest pełna, dlatego ten błąd pojawia się tylko czasami.

Dwa sposoby naprawy:

  1. Pełne wyrażenie wycinania s[low:high:max] ogranicza pojemność do max-low. Bez wolnej pojemności następny append musi alokować. first := base[:2:2] sprawia, że pierwszy przykład jest bezpieczny.
  2. Jawne kopiowanie, gdy slice przeżyje wywołanie albo będzie niezależnie rozszerzany: slices.Clone(prefix) lub append([]string(nil), prefix...).

Pokrewna pułapka: mały slice ogromnej tablicy utrzymuje dla garbage collectora całą tablicę przy życiu. Jeśli wczytasz plik 100 MB i zachowasz data[:10], całe 100 MB zostaje w pamięci. Sklonuj fragment, który zachowujesz.

copy

Wbudowana funkcja copy(dst, src) kopiuje min(len(dst), len(src)) elementów i zwraca ich liczbę. Nigdy nie powiększa dst.

copy poprawnie obsługuje nakładające się źródło i cel, więc copy(s[1:], s) przesuwa elementy w prawo bez uszkodzenia danych.

Pakiet slices

Od Go 1.21 standardowy pakiet slices ma generyczne funkcje pomocnicze do operacji, które kiedyś pisało się ręcznie.

FunkcjaCo robi
Contains, Indexznajdują wartość (ContainsFunc, IndexFunc przyjmują predykat)
Sort, SortFunc, SortStableFuncsortują w miejscu
BinarySearchprzeszukuje posortowany slice
Insert, Delete, DeleteFuncwstawiają lub usuwają, zwracają nowy slice
Compactusuwa kolejne duplikaty
Equal, Compareporównują element po elemencie
Clone, Reverse, Max, Minrobią to, co sugeruje nazwa
Collect, Sorted, Values, Allpracują z iteratorami (Go 1.23)

Delete i Insert zwracają slice, który musisz przypisać, tak jak append. Sortowanie ma własną stronę: sortowanie.

Usuwanie elementów podczas pętli po indeksach pomija elementy. Użyj zamiast tego slices.DeleteFunc:

s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens

Slice'y 2D

Slice slice'ów daje siatkę, w której wiersze mogą mieć różną długość. Każdy wiersz trzeba zaalokować osobno.

Pominięcie wewnętrznego make zostawia każdy wiersz jako nil, a grid[1][2] = 7 wywołuje panic z komunikatem index out of range [2] with length 0.

Slice'y nil i puste slice'y

Slice nil zachowuje się jak pusty przy len, cap, range i append, więc jako stan zerowy wybieraj var s []T. Inicjalizuj do []T{} tylko wtedy, gdy różnica ma znaczenie, czyli głównie przy wyjściu JSON (null a []). Pustość sprawdzaj przez len(s) == 0, a nie s == nil.

Slice'y i funkcje

Funkcja, która dostaje slice, może zmieniać jego elementy, a wywołujący widzi te zmiany. Nie może zmienić długości slice'a wywołującego, bo dostała kopię nagłówka. Funkcja, która dopisuje elementy, musi zwrócić nowy slice:

func addAll(s []int, vals ...int) []int {
	return append(s, vals...)
}

Dlatego append, slices.Delete i slices.Insert zwracają slice.

Typowe błędy

  • Ignorowanie wyniku append. Całkowite odrzucenie wyniku się nie kompiluje, ale przypisanie append(s, x) do innej zmiennej niż ta, której dalej używasz, to błąd logiczny.
  • Dwa dopisania do jednej bazy. Mogą współdzielić pamięć. Sklonuj bazę albo użyj pełnego wyrażenia wycinania.
  • make([]T, n), a potem append. Zostawia n zer na początku.
  • Indeks poza zakresem. s[len(s)] wywołuje panic. Ostatni element to s[len(s)-1].
  • Porównywanie slice'ów przez ==. Kompiluje się tylko s == nil. Użyj slices.Equal.

Najczęściej zadawane pytania

Czym różni się długość od pojemności slice'a w Go?

len(s) to liczba elementów, które slice aktualnie zawiera. cap(s) to liczba elementów, które mieszczą się w tablicy bazowej, licząc od pierwszego elementu slice'a. append zapisuje w wolnej pojemności, jeśli jakaś jest, a gdy jej brak, alokuje nową, większą tablicę.

Jak dodać element do slice'a w Go?

Wywołaj wbudowaną funkcję append i przypisz wynik z powrotem: s = append(s, x). Może dodać kilka wartości (append(s, 1, 2, 3)) albo inny slice (append(s, other...)). Zawsze używaj zwróconej wartości, bo append może zwrócić slice wskazujący na nową tablicę.

Jak usunąć element ze slice'a w Go?

Użyj slices.Delete(s, i, i+1) (Go 1.21), która przesuwa dalsze elementy w dół i zwraca krótszy slice. Od Go 1.22 zeruje też zwolnione miejsca na końcu. Jeśli kolejność nie ma znaczenia, zamień element z ostatnim i skróć slice: s[i] = s[len(s)-1]; s = s[:len(s)-1].

Jak sprawdzić, czy slice zawiera wartość w Go?

Użyj slices.Contains(s, v) ze standardowego pakietu slices (Go 1.21). slices.Index(s, v) zwraca pozycję albo -1, a slices.ContainsFunc przyjmuje predykat. Przy wielokrotnym wyszukiwaniu w dużych danych zbuduj zamiast tego mapę.

Czym różni się slice nil od pustego slice'a w Go?

var s []int to nil; s := []int{} jest pusty, ale nie jest nil. Oba mają długość 0 i oba działają z len, range i append. Różnica wychodzi przy sprawdzaniu s == nil oraz w encoding/json, który koduje slice nil jako null, a pusty slice jako [].

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ