1Wyjaśnij, w jaki sposób wartości zerowe w języku Go działają dla typów wbudowanych i typów przypominających referencje (reference-like), oraz dlaczego mają one znaczenie podczas deklarowania zmiennych bez ich jawnej inicjalizacji.
W języku Go zmienna zadeklarowana bez jawnego inicjalizatora jest automatycznie inicjowana wartością zerową (zero value) przypisaną do danego typu. Typy liczbowe przyjmują wartość 0, bool staje się false, string to "", a tablice lub struktury są zerowane element po elemencie, bądź pole po polu. Typy wskaźnikowe lub przypominające referencje, takie jak wskaźniki, wycinki (slices), mapy, kanały, funkcje oraz interfejsy, mają nil jako swoją wartość zerową. Ma to kluczowe znaczenie, ponieważ zmienne w Go oraz pomijane pola w strukturach zawsze uruchamiają się w zdeterminowanym stanie, a nie przechowują wartości losowe (garbage), a wiele interfejsów API zostało zaprojektowanych tak, aby ich wartość zerowa stanowiła użyteczną wartość domyślną, choć niektóre wartości nil nadal mogą wymagać inicjalizacji przed wykonaniem określonych operacji.
2Jak Go obsługuje równość struktur i co się dzieje, gdy struktura zawiera nieporównywalne pola?
Wartości struktur w Go mogą być porównywane za pomocą `==` i `!=` tylko wtedy, gdy każde pole w strukturze jest porównywalne. Równość opiera się na porównaniu odpowiadających sobie pól, korzystając z reguły równości dla każdego z nich. Jeśli struktura zawiera nieporównywalne pole, takie jak wycinek (slice), mapa lub funkcja, cały typ struktury staje się nieporównywalny, a porównanie dwóch wartości takiego typu za pomocą `==` powoduje błąd kompilacji. W przypadku takich struktur należy użyć niestandardowej logiki porównywania lub odpowiedniej funkcji pomocniczej do głębokiego porównywania (deep equality), co znajduje zastosowanie szczególnie w testach.
3Wyjaśnij niemutowalność ciągów znaków (string) w Go oraz związek pomiędzy typami `string`, `[]byte`, `bytes.Buffer` i `strings.Builder`.
W języku Go `string` to niemutowalna sekwencja bajtów, będąca w większości przypadków tekstem w formacie UTF-8, choć nie ma odgórnego wymogu posiadania przez nią poprawnego kodowania UTF-8. Zawartości `string` nie można modyfikować w miejscu (in-place); w tym celu zazwyczaj najpierw konwertuje się ciąg na tablicę `[]byte` dla edycji na poziomie bajtów, bądź `[]rune` dla edycji na punktach kodowych (code points), by potem powrócić do formatu bazowego. Zwykłe konwersje na linii `string` i `[]byte` połączone są z kopiowaniem i alokacją w pamięci, z tej racji powtarzalne konwersje czy wielokrotna konkatenacja ciągów wewnątrz pętli okazują się dość kosztowne. Biblioteczna struktura `strings.Builder` uchodzi za zoptymalizowaną pod kątem sprawnego konstruowania łańcuchów znaków, tymczasem `bytes.Buffer` operuje mutowalnym buforem, wyjątkowo dogodnym do zadań wykorzystujących dane na poziomie bajtów z operacjami wejścia/wyjścia, ale z możliwością późniejszej produkcji formy końcowej dla obiektu typu `string`.
4Jak działają wskaźniki w Go i jakie operacje są celowo niedozwolone w porównaniu do języka C?
Wskaźnik w Go jest typowaną wartością, która przechowuje adres innej wartości, na przykład *int wskazuje na wartość typu int. Używa się operatora &x, aby pobrać adres wartości, oraz *p, aby wyłuskać wartość wskaźnika (dereference) w celu odczytania lub zapisania docelowej wartości. Przekazanie lub przechowywanie wskaźnika pozwala wielu miejscom w kodzie obserwować lub modyfikować tę samą wartość bazową, a ponadto wskaźnik może przyjmować wartość nil. W przeciwieństwie do języka C, bezpieczny kod w Go celowo nie zezwala na arytmetykę wskaźników ani na dowolne manipulowanie surowymi adresami pamięci; operacje niskopoziomowe wymagają użycia pakietu unsafe.
5Wyjaśnij, jak działają stałe i iota w Go, uwzględniając stałe typowane i nietypowane.
Stałe w Go są wartościami czasu kompilacji: logicznymi, znakowymi (string) lub liczbowymi. Stała typowana ma określony typ. Stała nietypowana nie ma ustalonego konkretnego typu, dopóki nie zostanie użyta w kontekście typowanym, a liczbowe stałe nietypowane są do tego czasu reprezentowane dokładnie (z wysoką precyzją); wartość ta musi być możliwa do przedstawienia w wybranym typie. `iota` to predefiniowany identyfikator używany w deklaracjach `const`: zaczyna się od 0 w każdym bloku const i rośnie o jeden przy każdej specyfikacji stałej, co czyni ją przydatną dla stałych przypominających typ wyliczeniowy (enum) oraz flag bitowych.
6Jak działają jawne konwersje w Go i dlaczego język ten unika wielu niejawnych konwersji?
Go na ogół wymaga jawnych konwersji przy użyciu składni `T(x)` w przypadku zmiany typu wartości, na przykład `int64(i)` lub `MyID(n)`. Dzięki temu zmiany typów są widoczne, co pozwala uniknąć zaskakujących niejawnych konwersji typów liczbowych, logicznych lub znakowych. Konwersje są dozwolone tylko zgodnie z regułami konwersji Go i mogą zmieniać wartość, na przykład przez przepełnienie lub obcięcie liczby całkowitej, obcięcie liczby zmiennoprzecinkowej do całkowitej w kierunku zera lub utratę precyzji. Nietypowane stałe (untyped constants) są bardziej elastyczne: mogą być użyte w otypowanym kontekście, o ile wartość stałej jest reprezentowalna w danym typie.
7Opisz różnicę między tablicami (arrays) a wycinkami (slices) w języku Go, uwzględniając zachowanie długości, pojemności oraz mechanizmu przechowywania pod spodem.
Tablica w Go ma stałą długość, która jest częścią jej typu, na przykład `[3]int`; przechowuje ona swoje elementy bezpośrednio, a przypisanie lub przekazanie tablicy jako argumentu powoduje skopiowanie całej jej wartości. Wycinek, taki jak `[]int`, to mały deskryptor wskazujący na tablicę bazową: koncepcyjnie zawiera wskaźnik do elementów, długość oraz pojemność. Długość wycinka to liczba widocznych w nim elementów; jego pojemność określa, ile elementów można użyć od początku wycinka do końca tablicy bazowej. Wycinki są elastyczne: ponowne wycinanie (reslicing) zmienia deskryptor, a funkcja `append` może ponownie wykorzystać tę samą tablicę bazową, jeśli pozwala na to pojemność, lub zaalokować nową w przeciwnym wypadku.
8Jak zachowuje się typ mapy w języku Go w odniesieniu do typów kluczy, brakujących kluczy, map o wartości nil oraz kolejności iteracji?
Typy kluczy map w Go muszą być porównywalne; wycinki (slices), mapy i funkcje nie mogą być używane bezpośrednio jako klucze. Wyszukiwanie nieobecnego klucza zwraca wartość zerową (zero value) dla typu elementu, dlatego idiom "comma-ok" (`v, ok := m[k]`) jest używany do odróżnienia braku klucza od obecności wartości zerowej. Mapę o wartości `nil` można odczytywać i iterować po niej, ale przypisanie do niej wywołuje panikę (panic); należy ją zainicjować przed zapisem. Kolejność iteracji po mapie jest nieokreślona i kod nie może na niej polegać.
9Kiedy w języku Go należy przekazywać strukturę (struct) lub inną wartość przez wartość, a kiedy przez wskaźnik?
Przekazuj przez wartość, gdy rozmiar wartości jest niewielki, ma ona pozostać tylko do odczytu w kontekście danego wywołania, lub jeśli chcesz zapewnić niezależną semantykę kopii. Z kolei przez wskaźnik należy przekazywać dane wtedy, gdy wywoływana funkcja ma na celu modyfikację wartości u jej nadawcy, gdy skopiowanie informacji byłoby nazbyt kosztownym procesem, dany w ogóle typ nie powinien podlegać procedurze skopiowania bądź jeżeli wartość `nil` lub tożsamość współdzielona pełnią rolę elementu publicznego API (Application Programming Interface). W żadnym razie nie powinno się przyjmować założenia, iż użycie wskaźnika jest nieustannie wariantem szybszym pod kątem wykonania: ich użycie może wprowadzić narzuty wynikające z aliasingu (współdzielenie tej samej lokalizacji pamięci), ucieczek danych na stertę (heap escapes), narzuconego obciążenia mechanizmów oczyszczania GC (Garbage Collectora) a także skutkować mniejszym w kontekście wydajności dostosowaniem w odniesieniu do pamięci podręcznej (cache). Zaleca się zatem skupiać w pierwszej kolejności na klarownej dla odbiorcy semantyce kodu i jednocześnie poddawać próbom badawczym wycinki aplikacji wrażliwe pod kątem optymalizacyjnym celem zbadania ich obiektywnej skuteczności pod względem użycia zasobów wykonując testy wydajnościowe.
10Jak zachowuje się przypisywanie i kopiowanie dla struktur, tablic, map i wycinków (slices) w języku Go?
Przypisanie w Go kopiuje przypisywaną wartość, ale efekt zależy od typu. Przypisanie struktury kopiuje jej pola, a przypisanie tablicy kopiuje każdy jej element. Przypisanie wycinka (slice) kopiuje jego nagłówek – wskaźnik, długość i pojemność – więc wycinki zazwyczaj współdzielą tę samą tablicę bazową (backing array). Przypisanie mapy kopiuje deskryptor mapy działający jak referencja, więc obie zmienne odnoszą się do tych samych danych mapy. Aby uzyskać niezależną własność (independent ownership), należy wykonać kopię defensywną (defensive copy), na przykład używając wbudowanej funkcji `copy` lub `slices.Clone` dla wycinków, a dla map tworząc nową mapę i kopiując jej wpisy.
11Jak domknięcia (closures) przechwytują zmienne w Go i jakie błędy mogą wystąpić w kontekście pętli i gorutyn (goroutines)?
Domknięcie (closure) w Go to wartość funkcyjna, która odwołuje się do zmiennych ze swojego bezpośredniego zasięgu leksykalnego. Przechwytuje ono same zmienne, dzięki czemu ich mutacje mogą być obserwowane przez domknięcie, a przechwycone zmienne mogą żyć dłużej niż funkcja tworząca, jeśli domknięcie ucieka ze stosu (escapes). Błędy w kontekście pętli i gorutyn powstają, gdy domknięcia współdzielą lub obserwują zmienną po tym, jak uległa zmianie. Często prowadzi to do sytuacji, w której gorutyny lub opóźnione wywołania zwrotne (callbacks) widzą niepożądaną wartość lub powodują wyścigi danych (data races) na współdzielonym stanie. Typowym rozwiązaniem jest przekazanie oczekiwanej wartości jako argumentu domknięcia lub utworzenie nowej, lokalnej kopii zmiennej w każdej iteracji pętli, a także w razie potrzeby synchronizacja dostępu współbieżnego. Począwszy od wersji Go 1.22, zmienne pętli deklarowane za pomocą `for`/`range` są alokowane na każdą iterację, co eliminuje wiele klasycznych błędów z przechwytywaniem takich zmiennych. Jednakże użycie istniejących zmiennych z zewnątrz pętli oraz każdy inny współdzielony, mutowalny stan nadal mogą powodować problemy.
12Opisz semantykę działania pętli `range` iterującej po tablicach, wycinkach (slices), mapach, łańcuchach znaków i kanałach.
Pętla `range` iteruje w zależności od typu operandu. W przypadku tablicy (array) lub wycinka (slice), zwraca jego indeks oraz wartość elementu; wartość ta jest tworzona jako kopia, więc przypisywanie do niej nie zmodyfikuje samej kolekcji. Iterowanie poprzez `range` po całości wartości tablicowej będzie ją w pełni kopiować na potrzeby iteracji, natomiast iteracja po wycinku wykorzystuje tylko jego nagłówek (slice header) i indeksuje dane prosto z tablicy bazowej. Przy mapach zwracany jest zarówno klucz, jak i wartość (będąca kopią) – odbywa się to w nieokreślonym z góry porządku. Iterowanie po ciągach znaków (string) daje z kolei bajtowy indeks i odkodowany punkt kodowy Unicode (jako `rune`), co nie jest tożsame z indeksem samego znaku. Na kanałach operacja pętli w ten sposób przyjmuje wartości (receives values), aż nastąpi zamknięcie (close) i opróżnienie z nich kanału; na kanałach z wartością nil wywoływanie iteracji kończy się zawieszeniem (block forever).
13Jaka jest różnica między odbiornikami przez wartość (value receivers) a odbiornikami przez wskaźnik (pointer receivers) i jak wybór odbiornika wpływa na mutowalność, kopiowanie oraz spełnianie interfejsów?
Metoda z odbiornikiem przez wartość (value receiver) otrzymuje kopię odbiornika, więc nadaje się do zachowań tylko do odczytu i dla małych typów w stylu niemutowalnym; zmiany dokonane na kopii odbiornika nie są widoczne dla funkcji wywołującej. Metoda z odbiornikiem przez wskaźnik (pointer receiver) otrzymuje kopię wskaźnika na odbiornik, dzięki czemu może mutować oryginał i unikać kopiowania dużych wartości lub takich, których kopiować się nie powinno. Wybór odbiornika wpływa na zbiór metod (`method set`): metody z odbiornikami przez wartość znajdują się w `method set` zarówno dla typu `T`, jak i `*T`, podczas gdy metody z odbiornikami przez wskaźnik należą wyłącznie do `method set` dla `*T`. W rezultacie, interfejs wymagający metody z odbiornikiem przez wskaźnik jest spełniany przez `*T`, a nie przez `T`, mimo że adresowalna wartość typu `T` może wywołać taką metodę używając standardowej składni wywoływania metod.
14Opisz, jak działają interfejsy w języku Go i co oznacza niejawne (implicit) spełnianie interfejsu przez dany typ.
Interfejs w języku Go definiuje zbiór wymaganych metod. Konkretny typ spełnia interfejs niejawnie, gdy jego zbiór metod (method set) zawiera te metody; nie używa się tu deklaracji implementacji. Zmienna interfejsowa może przechowywać konkretną wartość dynamiczną, której typ spełnia dany interfejs, a wywołania metod interfejsu są przekazywane (dispatch) do implementacji tej dynamicznej wartości. Pusty interfejs, zapisywany jako interface{} lub częściej any, nie ma żadnych wymaganych metod, więc spełnia go każdy typ. To, czy interfejs jest spełniany przez T, *T, czy przez oba, zależy od ich zbioru metod i wyboru wskaźnika lub wartości przy deklaracji metody (receiver choices).
15Jak asercje typu (type assertions) i instrukcje wyboru typu (type switches) działają z wartościami interfejsów?
Asercja typu sprawdza dynamiczną wartość przechowywaną w interfejsie: `v := x.(T)` kończy się sukcesem, jeśli dynamicznym typem wartości interfejsu jest `T`, lub jeśli `T` jest interfejsem zaimplementowanym przez dynamiczną wartość. Forma z jednym wynikiem wywołuje panikę (panic) w przypadku niepowodzenia. Konstrukcja z dwoma wynikami (comma-ok), `v, ok := x.(T)`, informuje o sukcesie bez wywoływania paniki. Instrukcja wyboru typu (type switch), `switch v := x.(type)`, wykonuje rozgałęzienie na podstawie dynamicznego typu wartości interfejsu, przy czym zmienna `v` przyjmuje typ zgodny z dopasowanym przypadkiem (case).