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:
- Pełne wyrażenie wycinania
s[low:high:max]ogranicza pojemność domax-low. Bez wolnej pojemności następnyappendmusi alokować.first := base[:2:2]sprawia, że pierwszy przykład jest bezpieczny. - Jawne kopiowanie, gdy slice przeżyje wywołanie albo będzie niezależnie rozszerzany:
slices.Clone(prefix)lubappend([]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.
| Funkcja | Co robi |
|---|---|
Contains, Index | znajdują wartość (ContainsFunc, IndexFunc przyjmują predykat) |
Sort, SortFunc, SortStableFunc | sortują w miejscu |
BinarySearch | przeszukuje posortowany slice |
Insert, Delete, DeleteFunc | wstawiają lub usuwają, zwracają nowy slice |
Compact | usuwa kolejne duplikaty |
Equal, Compare | porównują element po elemencie |
Clone, Reverse, Max, Min | robią to, co sugeruje nazwa |
Collect, Sorted, Values, All | pracują 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 przypisanieappend(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 potemappend. Zostawia n zer na początku.- Indeks poza zakresem.
s[len(s)]wywołuje panic. Ostatni element tos[len(s)-1]. - Porównywanie slice'ów przez
==. Kompiluje się tylkos == nil. Użyjslices.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 [].