You are on page 1of 357

Spis treści

Przedmowa . ..............................................................................................9
Pochodzenie języka Go . ..................................................................................................................10
Projekt Go . ........................................................................................................................................11
Struktura książki . .............................................................................................................................13
Gdzie można znaleźć więcej informacji . .......................................................................................14
Podziękowania . ................................................................................................................................15

Rozdział 1. Przewodnik . ............................................................................17


1.1. Witaj, świecie ..............................................................................................................................17
1.2. Argumenty wiersza poleceń . ...................................................................................................19
1.3. Wyszukiwanie zduplikowanych linii ......................................................................................23
1.4. Animowane GIF-y . ...................................................................................................................28
1.5. Pobieranie zawartości adresu URL . .......................................................................................30
1.6. Pobieranie zawartości kilku adresów URL równolegle . ......................................................32
1.7. Serwer WWW . ..........................................................................................................................33
1.8. Kilka pominiętych kwestii . ......................................................................................................37

Rozdział 2. Struktura programu . ...............................................................41


2.1. Nazwy . ........................................................................................................................................41
2.2. Deklaracje . .................................................................................................................................42
2.3. Zmienne . ....................................................................................................................................43
2.4. Przypisania . ...............................................................................................................................50
2.5. Deklaracje typów . .....................................................................................................................52
2.6. Pakiety i pliki ..............................................................................................................................54
2.7. Zakres . ........................................................................................................................................58
6 SPIS TREŚCI

Rozdział 3. Podstawowe typy danych . .......................................................63


3.1. Liczby całkowite . .......................................................................................................................63
3.2. Liczby zmiennoprzecinkowe ....................................................................................................68
3.3. Liczby zespolone . ......................................................................................................................72
3.4. Wartości logiczne . ....................................................................................................................75
3.5. Łańcuchy znaków . ....................................................................................................................75
3.6. Stałe . ............................................................................................................................................86

Rozdział 4. Typy złożone . ..........................................................................91


4.1. Tablice . .......................................................................................................................................91
4.2. Wycinki . .....................................................................................................................................94
4.3. Mapy . ....................................................................................................................................... 102
4.4. Struktury . ................................................................................................................................ 108
4.5. JSON . ....................................................................................................................................... 114
4.6. Szablony tekstowe i HTML . ................................................................................................. 120

Rozdział 5. Funkcje . ...............................................................................125


5.1. Deklaracje funkcji . ................................................................................................................. 125
5.2. Rekurencja . ............................................................................................................................. 127
5.3. Zwracanie wielu wartości . .................................................................................................... 130
5.4. Błędy . ....................................................................................................................................... 132
5.5. Wartości funkcji . .................................................................................................................... 137
5.6. Funkcje anonimowe . ............................................................................................................. 139
5.7. Funkcje o zmiennej liczbie argumentów . ........................................................................... 146
5.8. Odroczone wywołania funkcji . ............................................................................................ 147
5.9. Procedura panic . .................................................................................................................... 152
5.10. Odzyskiwanie sprawności . ................................................................................................. 154

Rozdział 6. Metody . ...............................................................................157


6.1. Deklaracje metod . .................................................................................................................. 157
6.2. Metody z odbiornikiem wskaźnikowym . ........................................................................... 159
6.3. Komponowanie typów poprzez osadzanie struktur .......................................................... 162
6.4. Wartości i wyrażenia metod .................................................................................................. 165
6.5. Przykład: typ wektora bitowego . ......................................................................................... 166
6.6. Hermetyzacja . ......................................................................................................................... 169

Rozdział 7. Interfejsy . .............................................................................173


7.1. Interfejsy jako kontrakty . ...................................................................................................... 173
7.2. Typy interfejsowe . ................................................................................................................. 176
SPIS TREŚCI 7

7.3. Spełnianie warunków interfejsu . ......................................................................................... 177


7.4. Parsowanie flag za pomocą interfejsu flag.Value ............................................................... 180
7.5. Wartości interfejsów . ............................................................................................................ 182
7.6. Sortowanie za pomocą interfejsu sort.Interface ................................................................. 187
7.7. Interfejs http.Handler . .......................................................................................................... 191
7.8. Interfejs error . ........................................................................................................................ 196
7.9. Przykład: ewaluator wyrażeń . .............................................................................................. 197
7.10. Asercje typów . ...................................................................................................................... 203
7.11. Rozróżnianie błędów za pomocą asercji typów . .............................................................. 205
7.12. Kwerendowanie zachowań za pomocą interfejsowych asercji typów . ......................... 207
7.13. Przełączniki typów . ............................................................................................................. 209
7.14. Przykład: dekodowanie XML oparte na tokenach . ......................................................... 211
7.15. Kilka porad . .......................................................................................................................... 214

Rozdział 8. Funkcje goroutine i kanały . ...................................................215


8.1. Funkcje goroutine . ................................................................................................................. 215
8.2. Przykład: współbieżny serwer zegara . ................................................................................. 217
8.3. Przykład: współbieżny serwer echo ...................................................................................... 220
8.4. Kanały . ..................................................................................................................................... 222
8.5. Zapętlenie równoległe . .......................................................................................................... 231
8.6. Przykład: współbieżny robot internetowy . ......................................................................... 235
8.7. Multipleksowanie za pomocą instrukcji select ................................................................... 239
8.8. Przykład: współbieżna trawersacja katalogów . .................................................................. 242
8.9. Anulowanie . ........................................................................................................................... 246
8.10. Przykład: serwer czatu . ....................................................................................................... 248

Rozdział 9. Współbieżność ze współdzieleniem zmiennych . ......................253


9.1. Sytuacje wyścigu . ................................................................................................................... 253
9.2. Wzajemne wykluczanie: sync.mutex . ................................................................................. 258
9.3. Muteksy odczytu/zapisu: sync.RWMutex . ......................................................................... 261
9.4. Synchronizacja pamięci . ....................................................................................................... 262
9.5. Leniwe inicjowanie: sync.Once . ........................................................................................... 264
9.6. Detektor wyścigów . ............................................................................................................... 266
9.7. Przykład: współbieżna nieblokująca pamięć podręczna ................................................... 267
9.8. Funkcje goroutine i wątki . .................................................................................................... 274
8 SPIS TREŚCI

Rozdział 10. Pakiety i narzędzie go . ........................................................277


10.1. Wprowadzenie . .................................................................................................................... 277
10.2. Ścieżki importów . ................................................................................................................ 278
10.3. Deklaracja package . ............................................................................................................. 279
10.4. Deklaracje import . ............................................................................................................... 279
10.5. Puste importy . ...................................................................................................................... 280
10.6. Pakiety i nazewnictwo . ........................................................................................................ 282
10.7. Narzędzie go . ........................................................................................................................ 284

Rozdział 11. Testowanie . ........................................................................295


11.1. Narzędzie go test . ................................................................................................................. 296
11.2. Funkcje testujące . ................................................................................................................. 296
11.3. Pokrycie . ............................................................................................................................... 310
11.4. Funkcje benchmarkujące ..................................................................................................... 313
11.5. Profilowanie . ........................................................................................................................ 315
11.6. Funkcje przykładów . ........................................................................................................... 318

Rozdział 12. Refleksja . ...........................................................................321


12.1. Dlaczego refleksja? . ............................................................................................................. 321
12.2. reflect.Type i reflect.Value . ................................................................................................. 322
12.3. Display — rekurencyjny wyświetlacz wartości ................................................................. 324
12.4. Przykład: kodowanie S-wyrażeń . ....................................................................................... 329
12.5. Ustawianie zmiennych za pomocą reflect.Value .............................................................. 332
12.6. Przykład: dekodowanie S-wyrażeń . .................................................................................. 334
12.7. Uzyskiwanie dostępu do znaczników pól struktury . ...................................................... 337
12.8. Wyświetlanie metod typu .................................................................................................... 340
12.9. Słowo ostrzeżenia . ............................................................................................................... 341

Rozdział 13. Programowanie niskiego poziomu . .......................................343


13.1. Funkcje unsafe.Sizeof, Alignof i Offsetof . ........................................................................ 344
13.2. Typ unsafe.Pointer . ............................................................................................................. 346
13.3. Przykład: głęboka równoważność . .................................................................................... 348
13.4. Wywoływanie kodu C za pomocą narzędzia cgo . ........................................................... 351
13.5. Kolejne słowo ostrzeżenia . ................................................................................................. 355

Skorowidz . .............................................................................................356
Przedmowa

„Go jest językiem programowania open source, który ułatwia budowanie prostego, niezawodnego
i wydajnego oprogramowania” (ze strony internetowej Go: https://golang.org/).

Go został stworzony we wrześniu 2007 r. przez Roberta Griesemera, Roba Pike’a i Kena Thompsona
(wszyscy z Google) i pojawił się w listopadzie 2009 r. Celem tego języka i towarzyszących mu narzędzi
było zapewnienie ekspresyjności, wydajności zarówno w kompilowaniu, jak i w wykonywaniu oraz
efektywności w pisaniu solidnych i niezawodnych programów.
Język Go przypomina język C i podobnie jak on jest narzędziem dla profesjonalnych programi-
stów, pozwalającym osiągać maksymalny efekt przy minimalnych nakładach. Jest jednak czymś
więcej niż zaktualizowaną wersją C. Zapożycza i adaptuje dobre pomysły z wielu innych języków
programowania, unikając jednocześnie funkcji, które prowadzą do powstawania komplikacji i nie-
spójnego kodu. Wprowadzane przez Go udogodnienia w zakresie współbieżności są nowe i efektywne,
a jego podejście do abstrakcji danych i programowania obiektowego jest niezwykle elastyczne. Go
posiada automatyczne zarządzanie pamięcią, czyli mechanizm odzyskiwania pamięci (ang. garbage
collection).
Go szczególnie dobrze nadaje się do budowania infrastruktur takich jak serwery sieciowe oraz na-
rzędzi i systemów dla programistów, ale jest to język prawdziwie ogólnego przeznaczenia i znajduje
zastosowanie w dziedzinach tak różnych jak: grafika, aplikacje mobilne i uczenie maszynowe.
Stał się popularny jako zamiennik dla nietypowanych języków skryptowych, ponieważ równoważy
ekspresyjność z bezpieczeństwem: programy napisane w języku Go zazwyczaj działają szybciej
niż programy napisane w językach dynamicznych oraz doświadczają znacznie mniej awarii
spowodowanych nieoczekiwanymi błędami typów.
Język Go jest projektem open source, więc kod źródłowy dla jego kompilatora, bibliotek i narzędzi
jest bezpłatnie dostępny dla każdego. Udział w tym projekcie ma aktywna społeczność z całego
świata. Go działa na systemach: uniksowych (Linux, FreeBSD, OpenBSD, Mac OS X), Plan 9
i Microsoft Windows. Programy napisane w jednym z tych środowisk zasadniczo będą działać
bez modyfikacji w innych.
Ta książka ma na celu pomoc w natychmiastowym rozpoczęciu efektywnej pracy z językiem Go
oraz umożliwienie wykorzystania w pełni wszystkich jego funkcji i standardowych bibliotek do
pisania jasnych i wydajnych programów.
10 PRZEDMOWA

Pochodzenie języka Go
Podobnie jak gatunki biologiczne, odnoszące sukcesy języki wydają potomstwo, które łączy w sobie
zalety swoich przodków. Krzyżowanie gatunków niekiedy prowadzi do wykształcenia zaskaku-
jących atutów, a od czasu do czasu powstają nowe, rewolucyjne funkcje. Obserwując te oddzia-
ływania, możemy dowiedzieć się wiele o tym, dlaczego język jest ukształtowany w dany sposób i do
jakiego środowiska został przystosowany.
Poniższy rysunek przedstawia najważniejsze wpływy wcześniejszych języków programowania
na konstrukcję języka Go.

Go jest czasem opisywany jako „język podobny do C” lub „język C XXI wieku”. Po języku C odzie-
dziczył składnię wyrażeń, instrukcje przepływu sterowania, podstawowe typy danych, przekazywanie
parametrów wywołania przez wartość (ang. call-by-value parameter passing), wskaźniki, a przede
wszystkim nacisk, jaki język C kładzie na programy, które są kompilowane do postaci wydajnego
kodu maszynowego i w naturalny sposób współpracują z abstrakcjami bieżących systemów opera-
cyjnych.
W drzewie genealogicznym języka Go są też inni przodkowie. Jeden z głównych strumieni wpływów
pochodzi od języków stworzonych przez Niklausa Wirtha, poczynając od Pascala. Koncepcję pakietu
zainspirował język Modula-2. Oberon wyeliminował rozróżnienie pomiędzy plikami interfejsu
modułu i plikami implementacji modułu. Oberon-2 wpłynął na składnię pakietów, importów
i deklaracji, a Object Oberon dostarczył składnię dla deklaracji metod.
PROJEKT GO 11

Kolejną linią wśród przodków języka Go, wyróżniającą go spośród najnowszych języków progra-
mowania, jest sekwencja mało znanych języków badawczych opracowanych w Bell Labs, a zainspi-
rowanych koncepcją komunikacji procesów sekwencyjnych (ang. communicating sequential pro-
cesses — CSP) zaczerpniętą z nowatorskiego opracowania naukowego Tony’ego Hoare’a z 1978 r.,
poświęconego fundamentom współbieżności. W CSP program jest równoległą kompozycją proce-
sów, które nie mają współdzielonego stanu. Procesy komunikują i synchronizują się ze sobą za
pomocą kanałów. Jednak CSP Hoare’a była formalnym językiem do opisywania podstawowych
koncepcji współbieżności, a nie językiem programowania służącym do pisania wykonywalnych
programów.
Rob Pike i inni programiści zaczęli eksperymentować z implementacjami CSP jako rzeczywistymi
językami. Pierwsza implementacja nazywała się Squeak („język do komunikacji z myszką”) i zapew-
niała język do obsługi zdarzeń myszki i klawiatury za pomocą statycznie tworzonych kanałów. Na-
stępny był Newsqueak, który oferował charakterystyczne dla języka C instrukcje i składnię wyrażeń
oraz charakterystyczną dla Pascala notację typów. Był to język czysto funkcyjny z mechanizmem
odzyskiwania pamięci i również przeznaczony do zarządzania zdarzeniami klawiatury, myszki
i okien. Kanały stały się typami pierwszoklasowymi, tworzonymi dynamicznie i przechowywanymi
w zmiennych.
System operacyjny Plan 9 rozwinął te koncepcje w języku o nazwie Alef. Alef próbował uczynić
z Newsqueaka prawdziwy język programowania systemowego, ale z powodu pominięcia mechani-
zmu odzyskiwania pamięci współbieżność okazała się źródłem zbyt wielu problemów.
Inne konstrukcje w języku Go wykazują tu i ówdzie wpływ genów niedziedziczonych, np. funkcja
iota jest luźno zaczerpnięta z APL, a zakres leksykalny z funkcjami zagnieżdżonymi pochodzi
z języka Scheme (i z większości języków powstałych od tego czasu). Można w nim również znaleźć
oryginalne mutacje. Innowacyjne wycinki Go zapewniają dynamiczne tablice z efektywnym dostę-
pem losowym, ale także pozwalają na zaawansowane współdzielenie przypominające listy powią-
zane. Nowa w Go jest też instrukcja defer.

Projekt Go
Wszystkie języki programowania odzwierciedlają filozofię programowania wyznawaną przez ich
twórców, która często obejmuje istotny element reakcji na postrzegane niedostatki wcześniej-
szych języków. Projekt Go narodził się w firmie Google z frustracji kilkoma systemami opro-
gramowania, które cierpiały na eksplozję złożoności. (Ten problem w żaden sposób nie jest uni-
katowy dla Google).
Jak ujął to Rob Pike, „złożoność jest multiplikatywna”: rozwiązywanie problemu poprzez zwięk-
szenie złożoności jednej części systemu powoli, ale nieuchronnie zwiększa złożoność innych czę-
ści. Przy stałej presji dodawania funkcjonalności, opcji i konfiguracji oraz szybkiego przekazania
kodu łatwo jest zaniedbać prostotę, nawet jeśli na dłuższą metę prostota jest kluczem do dobre-
go oprogramowania.
Prostota wymaga więcej pracy na początku projektu, aby zredukować koncepcję do jej istoty,
oraz więcej dyscypliny przez cały czas życia projektu, aby odróżnić dobre zmiany od złych lub
szkodliwych. Przy odpowiednim wysiłku dobra zmiana może być zaadaptowana bez narażania
tego, co Fred Brooks nazwał integralnością konceptualną projektu, ale zła zmiana nie; natomiast
zmiana szkodliwa przehandlowuje prostotę na jej płytką kuzynkę — wygodę. Tylko poprzez
prostotę projektu rozrastający się system może pozostać stabilny, bezpieczny i spójny.
12 PRZEDMOWA

Projekt Go obejmuje sam język, jego narzędzia i standardowe biblioteki oraz ostatni, ale nie mniej
ważny element — kulturę radykalnej prostoty. Jako niedawno powstały język wysokiego poziomu,
Go oferuje zalety spojrzenia z perspektywy czasu i posiada dobrze opracowane podstawy: mecha-
nizm odzyskiwania pamięci, system pakietów, typy pierwszoklasowe, zakres leksykalny, interfejs
wywołań systemowych oraz niemutowalne łańcuchy znaków, w których tekst jest zasadniczo
kodowany w UTF-8. Ma jednak dość niewiele funkcji i jest mało prawdopodobne, aby zostały do-
dane kolejne. Nie ma np. żadnych konwersji numerycznych, konstruktorów lub destruktorów,
przeciążenia operatorów, domyślnych wartości parametrów, dziedziczenia, typów generycznych,
wyjątków, makr, adnotacji ani pamięci lokalnej dla wątków. Ten język jest dojrzały i stabilny.
Gwarantuje także kompatybilność wsteczną: starsze programy Go mogą być kompilowane i uru-
chamiane z nowszymi wersjami kompilatorów i standardowych bibliotek.
Go ma wystarczający system typów, aby uniknąć większości wynikających z niedbałości błędów,
które dręczą programistów w dynamicznych językach, ale ten system jest prostszy niż w innych
porównywalnych językach typowanych. Takie podejście może czasem prowadzić do powstawania
wyizolowanych wysp „nietypowanego” programowania w ramach szerszego frameworku typów,
a programiści Go nie muszą zadawać sobie tyle trudu, ile muszą zadawać programiści języków
C++ lub Haskell, aby wyrazić właściwości bezpieczeństwa jako dowody oparte na typach. Jednak
w praktyce Go daje programistom wiele korzyści z zakresu bezpieczeństwa i wydajności w czasie
wykonywania, charakterystycznych dla stosunkowo silnie typowanego systemu, ale bez ciężaru
systemu cechującego się złożonością.
Go promuje świadomość współczesnych systemów projektowania komputerów, a szczególnie zna-
czenie lokalności. Jego wbudowane typy danych oraz większość struktur danych biblioteki zostały
opracowane w taki sposób, aby działać naturalnie bez bezpośredniego inicjowania lub pośrednich
konstruktorów, więc w kodzie ukrytych jest niezbyt wiele alokacji pamięci i zapisów do pamięci.
Typy złożone języka Go (struktury i tablice) przechowują swoje elementy bezpośrednio, co wyma-
ga mniej pamięci oraz mniejszej liczby alokacji i mniejszej pośredniości wskaźników niż w przy-
padku języków wykorzystujących pola pośrednie. A ponieważ nowoczesny komputer jest ma-
szyną równoległą, Go posiada funkcje współbieżności oparte na CSP, jak wspomniano wcześniej.
Zmiennych rozmiarów stosy lekkich wątków Go zwanych funkcjami goroutine są początkowo
tak małe, że utworzenie jednej funkcji goroutine jest tanie, a utworzenie miliona jest praktyczne.
Standardowa biblioteka języka Go (która powoduje, że język ten jest często opisywany jako dostar-
czany „w zestawie z bateriami”) zapewnia zgrabne elementy konstrukcyjne oraz interfejsy API
dla operacji we-wy, przetwarzania tekstu, grafiki, kryptografii, aspektów sieciowych i aplikacji
rozproszonych, z obsługą wielu standardowych formatów plików i protokołów. Te biblioteki
i narzędzia szeroko stosują konwencje, aby zmniejszyć zapotrzebowanie na konfiguracje i obja-
śnienia, co upraszcza logikę programu i sprawia, że różnorodne programy Go są do siebie bardziej
podobne, a więc łatwiejsze do opanowania. Projekty skompilowane za pomocą narzędzia go używają
tylko nazw plików i identyfikatorów oraz okazjonalnie specjalnego komentarza do określenia
wszystkich bibliotek, plików wykonywalnych, testów, benchmarków, przykładów, wariantów cha-
rakterystycznych dla platform oraz dokumentacji dla projektu. Sam plik źródłowy Go zawiera spe-
cyfikację kompilacji.
STRUKTURA KSIĄŻKI 13

Struktura książki
Zakładamy, że programowałeś w co najmniej jednym innym języku kompilowanym (jak C, C++
i Java) lub interpretowanym (jak Python, Ruby i JavaScript), więc nie będziemy przeliterowywać
wszystkiego, jak dla absolutnie początkujących programistów. Składnia powierzchniowa będzie
znajoma, tak jak znajome będą zmienne i stałe, wyrażenia, przepływ sterowania i funkcje.
Rozdział 1. jest przewodnikiem po podstawowych konstrukcjach Go, wprowadzonych przez kil-
kanaście programów do wykonywania codziennych zadań, takich jak: odczytywanie i zapisywanie
plików, formatowanie tekstu, tworzenie obrazów i komunikowanie się z klientami i serwerami
internetowymi.
Rozdział 2. opisuje strukturalne elementy programu Go: deklaracje, zmienne, nowe typy, pakiety
i pliki oraz zakres. Rozdział 3. omawia liczby, wartości logiczne, łańcuchy znaków i stałe oraz
wyjaśnia, w jaki sposób przetwarzać Unicode. Rozdział 4. opisuje typy złożone, czyli zbudowane
z innych prostszych typów, przy użyciu tablic, map, struktur i wycinków. Te ostatnie stanowią
podejście Go do dynamicznych list. Rozdział 5. poświęcony jest funkcjom i omawia obsługę
błędów, funkcje panic i recover oraz instrukcję defer.
Rozdziały 1. – 5. stanowią zatem podstawy, czyli elementy będące częścią każdego języka impera-
tywnego głównego nurtu. Składnia i styl języka Go czasem różnią się od innych języków, ale
większość programistów szybko je podchwyci. Pozostałe rozdziały skupiają się na tematach, w któ-
rych podejście Go jest mniej konwencjonalne: metodach, interfejsach, współbieżności, pakietach,
testowaniu i refleksji.
Język Go ma niezwykłe podejście do programowania obiektowego. Nie ma hierarchii klas, a w rze-
czywistości nie ma w ogóle żadnych klas. Złożone zachowania obiektów są tworzone z prostszych
zachowań poprzez kompozycję, a nie dziedziczenie. Metody mogą być powiązane z dowolnym
typem zdefiniowanym przez użytkownika, a nie tylko ze strukturami, a relacja między typami kon-
kretnymi i abstrakcyjnymi (interfejsami) jest pośrednia, więc typ konkretny może spełniać warunki
interfejsu, o którym projektant typu nawet nie wiedział. Metody są omówione w rozdziale 6.,
a interfejsy w rozdziale 7.
Rozdział 8. prezentuje podejście języka Go do współbieżności, oparte na koncepcji komunikacji
procesów sekwencyjnych (CSP) wcielonej w życie poprzez funkcje goroutine i kanały. Rozdział 9.
wyjaśnia bardziej tradycyjne aspekty współbieżności opartej na współdzielonych zmiennych.
Rozdział 10. opisuje pakiety, czyli mechanizm organizowania bibliotek. Ten rozdział prezentuje
również sposób efektywnego wykorzystania narzędzia go, które służy do kompilowania, testowania,
benchmarkowania, formatowania programu, dokumentacji i wielu innych zadań, a wszystko w poje-
dynczym poleceniu.
Rozdział 11. jest poświęcony testowaniu, do którego Go ma szczególnie lekkie podejście, unikając
obciążonych abstrakcjami frameworków na rzecz prostych bibliotek i narzędzi. Biblioteki testo-
wania zapewniają fundament, na którym w razie potrzeby można budować bardziej złożone
abstrakcje.
Rozdział 12. omawia refleksję, czyli zdolność programu do badania własnej reprezentacji w trakcie
wykonywania. Refleksja jest potężnym narzędziem, ale należy jej używać ostrożnie. Ten rozdział
wyjaśnia, jak znaleźć odpowiednią równowagę, pokazując sposób wykorzystania refleksji do
zaimplementowania kilku ważnych bibliotek Go. Rozdział 13. opisuje mrożące krew w żyłach
szczegóły programowania niskiego poziomu, które wykorzystuje pakiet unsafe do obejścia systemu
typów języka Go i wskazuje, kiedy jest to właściwe.
14 PRZEDMOWA

Każdy rozdział zawiera szereg ćwiczeń, które można wykorzystać, aby sprawdzić swoją wiedzę
na temat Go i zbadać rozszerzenia oraz alternatywne rozwiązania dla przykładów z książki.
Wszystkie oprócz najbardziej trywialnych przykładów kodu z książki można pobrać z serwera wy-
dawnictwa Helion: ftp://ftp.helion.pl/przyklady/jgopop.zip. Po rozpakowaniu każdy przykład jest
identyfikowany przez swoją ścieżkę importu pakietu i może być wygodnie pobrany, a następnie
skompilowany i zainstalowany za pomocą polecenia go get. W tym celu należy najpierw w katalogu
domowym ($HOME) utworzyć katalog (nazwijmy go gobook), który będzie Twoim obszarem
roboczym, i ustawić zmienną środowiskową GOPATH, aby wskazywała ten katalog:
$ export GOPATH=$HOME/gobook # wybór katalogu przestrzeni roboczej
Następnie w katalogu gobook należy utworzyć katalog src i umieścić w nim rozpakowany katalog
z kodami (nazwijmy go code). Teraz możemy skompilować i uruchomić przykładowy program:
$ go get code/r01/helloworld # pobranie, skompilowanie, instalacja
$ $GOPATH/bin/helloworld # uruchomienie
Witaj, 世界
Aby uruchamiać przykłady z książki, potrzebna będzie co najmniej wersja 1.5 języka Go.
$ go version
go version go1.5 linux/amd64
Jeśli na Twoim komputerze nie ma zainstalowanego narzędzia go lub jest zainstalowana starsza
wersja, postępuj zgodnie z instrukcjami ze strony: https://golang.org/doc/install.

Gdzie można znaleźć więcej informacji


Najlepszym źródłem informacji na temat Go jest oficjalna strona internetowa projektu, https://
golang.org, która zapewnia dostęp do dokumentacji, w tym specyfikacji języka (Go Programming
Language Specification), standardowych pakietów itp. Dostępne są również samouczki pokazujące,
jak pisać programy Go i jak robić to dobrze, oraz różne zasoby internetowe tekstów i materiałów
wideo, które będą cennym uzupełnieniem tej książki. Blog Go na stronie blog.golang.org zawiera
jedne z najlepszych publikacji dotyczących Go, w tym artykuły na tematy takie jak: stan języka,
plany na przyszłość, sprawozdania z konferencji oraz szczegółowe objaśnienia różnych kwestii
związanych z Go.
Jednym z najbardziej przydatnych aspektów dostępu online do materiałów związanych z Go (co
jest niestety ograniczeniem papierowej książki) jest możliwość uruchamiania programów z poziomu
opisujących je stron internetowych. Ta funkcjonalność jest dostarczana przez Go Playground na
stronie play.golang.org i może być osadzana na innych stronach, takich jak strona główna
golang.org lub strony dokumentacji serwowane przez narzędzie godoc.
Funkcjonalność Playground umożliwia wygodne przeprowadzanie prostych eksperymentów
w celu sprawdzenia własnej wiedzy na temat składni, semantyki lub pakietów biblioteki z krótkimi
programami i na wiele sposobów zastępuje interaktywne środowisko REPL (ang. read-eval-print
loop) stosowane w innych językach. Jego trwałe adresy URL są idealne do dzielenia się fragmentami
kodu Go z innymi użytkownikami, raportowania błędów lub czynienia sugestii.
Zbudowany na bazie Playground przewodnik Go Tour dostępny na stronie tour.golang.org jest
sekwencją krótkich interaktywnych lekcji na temat podstawowych koncepcji i konstrukcji Go,
w uporządkowany sposób opisujących kolejne kwestie związane z tym językiem.
PODZIĘKOWANIA 15

Podstawowym mankamentem narzędzi Playground i Tour jest to, że umożliwiają importowanie


tylko standardowych bibliotek, a wiele funkcjonalności bibliotek (np. sieci) jest ograniczonych
z powodów praktycznych lub ze względów bezpieczeństwa. Wymagają one również dostępu do
internetu, aby skompilować i uruchomić każdy program. W przypadku bardziej skomplikowa-
nych eksperymentów trzeba więc uruchamiać programy Go na własnym komputerze. Na szczęście
proces pobierania jest prosty, więc pobranie dystrybucji Go ze strony golang.org i rozpoczęcie
pisania oraz uruchamiania własnych programów Go nie powinno zająć więcej niż kilka minut.
Ponieważ Go jest projektem open source, można przeczytać kod dla każdego typu lub funkcji ze
standardowej biblioteki online dostępnej na stronie: https://golang.org/pkg. Ten sam kod jest
częścią pobieranej dystrybucji. Używaj go, aby dowiedzieć się, jak działają różne mechanizmy,
znaleźć odpowiedzi na pytania dotyczące szczegółów lub choćby po to, aby zobaczyć, jak eksperci
piszą naprawdę dobry Go.

Podziękowania
Rob Pike i Russ Cox, kluczowi członkowie zespołu Go, przeczytali rękopis kilkakrotnie z ogromną
uwagą. Ich komentarze dotyczące wszystkiego — od doboru słów po ogólną strukturę i organizację
książki — okazały się bezcenne. Podczas przygotowywania japońskiego tłumaczenia Yoshiki
Shibata wyszedł daleko poza swoje obowiązki. Jego skrupulatne oko dostrzegło liczne nieścisłości
w oryginalnym tekście oraz błędy w kodzie. Jesteśmy bardzo wdzięczni za gruntowne recenzje
i krytyczne uwagi na temat całego rękopisu, których dostarczyli: Brian Goetz, Corey Kosak, Arnold
Robbins, Josh Bleecher Snyder oraz Peter Weinberger.
Następujące osoby zasłużyły na naszą wdzięczność za wiele pożytecznych sugestii: Sameer Ajmani,
Ittai Balaban, David Crawshaw, Billy Donahue, Jonathan Feinberg, Andrew Gerrand, Robert
Griesemer, John Linderman, Minux Ma, Bryan Mills, Bala Natarajan, Cosmos Nicolaou, Paul
Staniforth, Nigel Tao oraz Howard Trickey. Dziękujemy również Davidowi Brailsfordowi i Ra-
phowi Levienowi za doradztwo w kwestii składu oraz Chrisowi Loperowi za wyjaśnienie wielu
tajemnic dotyczących produkcji e-booków.
Nasz redaktor Greg Doench z Addison-Wesley wszystko to zainicjował i był od początku nieprze-
rwanie pomocny. Znakomity był zespół produkcyjny AW — John Fuller, Dayna Isley, Julie Nahil,
Chuti Prasertsith i Barbara Wood. Autorzy nie mogli liczyć na lepsze wsparcie.
Alan Donovan pragnie podziękować Sameerowi Ajmaniemu, Chrisowi Demetriou, Waltowi
Drummondowi i Reidowi Tatge’owi z Google za umożliwienie mu poświęcania czasu na pisanie.
Stephenowi Donovanowi za jego rady i pojawiające się w odpowiednich chwilach zachęty. A przede
wszystkim swojej żonie Leili Kazemi za jej stanowczy entuzjazm i niezachwiane wsparcie dla tego
projektu, mimo długich godzin nieobecności w życiu rodzinnym, które za sobą pociągał.
Brian Kernighan jest głęboko wdzięczny przyjaciołom i znajomym za ich cierpliwość i wyrozu-
miałość, gdy powoli szedł ścieżką do zrozumienia, a w szczególności swojej żonie Meg, która
nieustannie wspierała go w kwestiach pisania książki i w tak wielu innych.
Nowy Jork
Październik 2015
16 PRZEDMOWA
Rozdział 1

Przewodnik

Ten rozdział stanowi przegląd podstawowych komponentów języka Go. Mamy nadzieję, że zapewni
on wystarczająco dużo informacji i przykładów, abyś mógł poderwać się do lotu i zacząć robić
użyteczne rzeczy tak szybko, jak to możliwe. Przedstawione tu (i w całej książce) przykłady są ukie-
runkowane na zadania, z którymi możesz mieć do czynienia w prawdziwym świecie. W tym rozdziale
postaramy się dać przedsmak różnorodności możliwych do napisania w języku Go programów, po-
cząwszy od prostego przetwarzania plików i odrobiny grafiki, skończywszy na działających współ-
bieżnie klientach i serwerach internetowych. Oczywiście nie wyjaśnimy wszystkiego w pierwszym
rozdziale, ale analizowanie takich programów w nowym języku może być efektywnym sposobem
rozpoczęcia nauki.
Gdy uczysz się nowego języka, masz naturalną skłonność do pisania kodu w sposób, w jaki napi-
sałbyś go w języku już poznanym. Podczas nauki języka Go bądź świadomy tego nawyku i próbuj
go unikać. Postaraliśmy się zilustrować i wyjaśnić sposób pisania dobrych programów Go, więc
gdy będziesz pisał własny kod, użyj przedstawionego tutaj kodu jako przewodnika.

1.1. Witaj, świecie


Zaczniemy od stającego się już powoli tradycją przykładu „witaj, świecie” (ang. hello, world), który
pojawił się w opublikowanej w oryginale w 1978 r. książce Język C. C jest jednym z języków, które
miały najbardziej bezpośredni wpływ na Go, a przykład „witaj, świecie” ilustruje wiele głównych
koncepcji.
code/r01/helloworld
package main

import "fmt"

func main() {
fmt.Println("Witaj, 世界")
}
Go jest językiem kompilowanym. Łańcuch narzędzi Go konwertuje program źródłowy i elementy,
od których jest on zależny, na instrukcje natywnego języka maszynowego komputera. Dostęp do
tych narzędzi jest uzyskiwany za pomocą pojedynczego polecenia o nazwie go, które ma wiele
podpoleceń. Najprostszym z tych podpoleceń jest run, które kompiluje kod źródłowy z jednego pliku
źródłowego lub kilku plików źródłowych o nazwach kończących się na .go, linkuje go z bibliotekami,
18 ROZDZIAŁ 1. PRZEWODNIK

a następnie uruchamia wynikowy plik wykonywalny. (W całej książce będziemy używać symbolu
$ jako znaku zgłoszenia wiersza poleceń).
$ go run helloworld.go
Nic dziwnego, że to polecenie wyświetla następujący komunikat:
Witaj, 世界
Go natywnie obsługuje Unicode, więc może przetwarzać tekst w dowolnym języku świata.
Jeśli program jest czymś więcej niż tylko jednorazowym eksperymentem, prawdopodobnie bę-
dziesz chciał raz go skompilować i zapisać skompilowany rezultat do późniejszego wykorzystania.
Odbywa się to za pomocą polecenia go build:
$ go build helloworld.go
Powstaje wykonywalny plik binarny o nazwie helloworld, który może być uruchamiany w dowol-
nym momencie bez dalszego przetwarzania:
$ ./helloworld
Witaj, 世界
Każdy istotny przykład został odpowiednio oznaczony w celu przypomnienia, że można go znaleźć
w kodzie źródłowym dołączonym do tej książki:
code/r01/helloworld

Porozmawiajmy teraz o samym programie. Kod Go jest zorganizowany w pakiety, które są podobne
do bibliotek lub modułów w innych językach. Pakiet (ang. package) składa się z co najmniej jed-
nego pliku źródłowego .go umieszczonego w pojedynczym katalogu definiującym funkcje danego
pakietu. Każdy plik źródłowy rozpoczyna się od deklaracji package (w tym przypadku package
main) stwierdzającej, do którego pakietu należy dany plik. Dalej wymienione są inne importowane
przez ten plik pakiety, a następnie przechowywane w tym pliku deklaracje programu.
Standardowa biblioteka języka Go ma ponad 100 pakietów dla typowych zadań, takich jak: opera-
cje na danych wejściowych i wyjściowych, sortowanie oraz manipulowanie tekstem. Przykładowo:
pakiet fmt zawiera funkcje do wyświetlania danych wyjściowych i skanowania danych wejścio-
wych. Jedną z podstawowych funkcji wyjściowych w pakiecie fmt jest Println. Wyświetla ona
jedną wartość lub kilka wartości oddzielonych spacjami ze znakiem nowej linii na końcu, aby
wartości były umieszczone w pojedynczej linii danych wyjściowych.
Pakiet main jest wyjątkowy. Definiuje on samodzielny program wykonywalny, a nie bibliotekę.
W pakiecie main wyjątkowa jest również funkcja main — tam zaczyna się wykonywanie programu.
Program wykonuje wszystko to, co wykonuje funkcja main. Oczywiście w celu wykonywania
dużej ilości pracy funkcja main będzie zwykle wywoływać funkcje z innych pakietów, np. funkcję
fmt.Println.
Musimy wskazać kompilatorowi, które pakiety są wymagane przez dany plik źródłowy. Jest to
rola deklaracji import, która następuje po deklaracji package. Program „witaj, świecie” wykorzy-
stuje tylko jedną funkcję z jednego innego pakietu, ale większość programów będzie importować
więcej pakietów.
Powinieneś zaimportować dokładnie te pakiety, których potrzebujesz. Program nie będzie się
kompilować, jeśli brakuje jakichś importów lub jeśli są jakieś niepotrzebne. Ten ścisły wymóg
zapobiega akumulowaniu referencji do niewykorzystanych pakietów w trakcie ewaluowania
programów.
1.2. ARGUMENTY WIERSZA POLECEŃ 19

Deklaracje import muszą następować po deklaracji package. W dalszej części program składa
się z deklaracji funkcji, zmiennych, stałych oraz typów (wprowadzanych przez słowa kluczowe
func, var, const i type). W większości przypadków kolejność deklaracji nie ma znaczenia. Ten
program jest tak krótki, jak to możliwe, ponieważ deklaruje tylko jedną funkcję, która z kolei wy-
wołuje tylko jedną inną funkcję. W celu zaoszczędzenia miejsca czasem podczas prezentacji
przykładów nie będziemy pokazywać deklaracji package i import, ale są one w pliku źródłowym
i muszą tam być, aby skompilować kod.
Deklaracja funkcji składa się ze słowa kluczowego func, nazwy funkcji, listy parametrów (pusta
dla main), listy wyników (także pusta tutaj) oraz zawartego w nawiasach klamrowych ciała funkcji
(instrukcji definiujących działanie funkcji). Funkcjom przyjrzymy się bliżej w rozdziale 5.
Go nie wymaga średników na końcach instrukcji lub deklaracji, z wyjątkiem sytuacji, gdy w tej
samej linii pojawia się więcej niż jedna instrukcja lub deklaracja. W efekcie znaki nowej linii nastę-
pujące po określonych symbolach są przekształcane w średniki, więc miejsce umieszczania znaków
nowej linii jest istotne dla właściwego parsowania kodu Go. Przykładowo: otwarcie klamry { funkcji
musi się znajdować w tej samej linii co koniec deklaracji func, a nie w osobnej linii, a w wyraże-
niu x + y znak nowej linii jest dozwolony po operatorze +, ale nie przed nim.
Język Go jest w kwestii formatowania kodu bardzo rygorystyczny. Narzędzie gofmt przepisuje
kod na standardowy format, a podkomenda fmt narzędzia go aplikuje gofmt do wszystkich plików
w określonym pakiecie lub domyślnie do tych znajdujących się w bieżącym katalogu. Wszystkie
pliki źródłowe Go w tej książce zostały przepuszczone poprzez gofmt i powinieneś wypracować
sobie zwyczaj robienia tego samego z własnym kodem. Deklarowanie standardowego formatu po-
przez odgórny nakaz eliminuje wiele bezsensownej dyskusji o drobiazgach i, co ważniejsze, pozwala
na dokonywanie wielu automatycznych przekształceń kodu źródłowego, które byłyby niewyko-
nalne, jeśli dozwolone byłoby dowolne formatowanie.
Wiele edytorów tekstu można skonfigurować do uruchamiania narzędzia gofmt przy każdym
zapisywaniu pliku, aby kod źródłowy był zawsze prawidłowo sformatowany. Podobne narzędzie
o nazwie goimports dodatkowo zarządza wstawianiem i usuwaniem deklaracji import w razie
potrzeby. Nie jest ono częścią standardowej dystrybucji, ale możesz je pobrać za pomocą tego
polecenia:
$ go get golang.org/x/tools/cmd/goimports
Większość użytkowników do takich czynności, jak pobieranie i kompilowanie pakietów, urucha-
mianie testów czy wyświetlanie dokumentacji, używa zazwyczaj narzędzia go, któremu przyjrzymy
się w podrozdziale 10.7.

1.2. Argumenty wiersza poleceń


Większość programów przetwarza jakieś dane wejściowe, aby wygenerować pewne dane wyjściowe.
Przypomina to definicję obliczeń. Ale w jaki sposób program ma uzyskać dane wejściowe, na któ-
rych będzie działać? Niektóre programy generują własne dane, jednak częściej dane wejściowe
pochodzą z zewnętrznego źródła, takiego jak: plik, połączenie sieciowe, dane wyjściowe z innego
programu, informacje wprowadzane przez użytkownika za pomocą klawiatury, argumenty wiersza
poleceń itd. Na podstawie kilku kolejnych przykładów omówimy niektóre z tych możliwości,
zaczynając od argumentów wiersza poleceń.
20 ROZDZIAŁ 1. PRZEWODNIK

Pakiet os zapewnia funkcje i inne wartości dla obsługi systemu operacyjnego w sposób niezależny
od platformy. Argumenty wiersza poleceń są dostępne dla programu w zmiennej o nazwie Args,
która jest częścią pakietu os. Dlatego wszędzie poza pakietem os nazwa tej zmiennej to os.Args.
Zmienna os.Args jest wycinkiem (ang. slice) łańcuchów znaków. Wycinki są podstawowym poję-
ciem w Go i wkrótce dowiesz się więcej na ich temat. Na razie potraktuj wycinek jako dynamicznie
ustalanego rozmiaru sekwencję s elementów tablicy, gdzie poszczególne elementy mogą być do-
stępne jako s[i], a ciągła podsekwencja może być dostępna jako s[m:n]. Liczba elementów jest
dana przez len(s). Jak w większości innych języków programowania, całe indeksowanie w Go
używa przedziałów jednostronnie otwartych, które obejmują pierwszy indeks, ale wyłączają
ostatni, ponieważ upraszcza to logikę. Przykładowo: wycinek s[m: n], gdzie 0 ≤ m ≤ n ≤ len(s),
zawiera n-m elementów.
Pierwszy element zmiennej os.Args, czyli os.Args[0], to nazwa samego polecenia. Pozostałe
elementy są argumentami, które zostały przedstawione programowi, gdy rozpoczął wykonywanie.
Wyrażenie wycinkowe w postaci s[m:n] daje wycinek, który odnosi się do elementów od m do
n-1, więc elementy potrzebne do naszego następnego przykładu to te w wycinku os.Args[1:
len(os.Args)]. Jeśli pominięte zostanie m lub n, domyślnie daje to odpowiednio wartość 0 lub
len(s), więc możemy skrócić żądany wycinek jako os.Args[1:].
Poniżej została przedstawiona implementacja uniksowego polecenia echo, wyświetlająca swoje
argumenty wiersza poleceń w jednej linii. Importuje ona dwa pakiety, które są podane w postaci
listy w nawiasach, a nie jako poszczególne deklaracje import. Obie formy są prawidłowe, ale
tradycyjnie używana jest lista. Kolejność importów nie ma znaczenia. Narzędzie gofmt sortuje
nazwy pakietów w kolejności alfabetycznej. (Jeśli istnieje kilka wersji przykładu, najczęściej bę-
dziemy je numerować, żebyś był pewien, o której z nich mówimy).
code/r01/echo1
// Echo1 wyświetla swoje argumenty wiersza poleceń.
package main

import (
"fmt"
"os"
)

func main() {
var s, sep string
for i := 1; i < len(os.Args); i++ {
s += sep + os.Args[i]
sep = " "
}
fmt.Println(s)
}
Komentarze w kodzie rozpoczynają się od podwójnego ukośnika (//). Cały tekst po ukośnikach,
aż do końca linii, jest ignorowanym przez kompilator komentarzem dla programistów. Zgodnie
z konwencją opisujemy każdy pakiet w komentarzu bezpośrednio poprzedzającym deklarację pa-
kietu. W przypadku pakietu main tym komentarzem jest co najmniej jedno pełne zdanie opisu-
jące program jako całość.
Deklaracja var deklaruje dwie zmienne typu string: s oraz sep. Zmienna może być inicjowana
w ramach swojej deklaracji. Jeśli nie jest bezpośrednio inicjowana, jest domyślnie inicjowana do
wartości zerowej dla danego typu, którą jest 0 dla typów numerycznych i pusty łańcuch znaków ""
1.2. ARGUMENTY WIERSZA POLECEŃ 21

dla typów łańcuchowych. Tak więc w tym przykładzie deklaracja domyślnie inicjuje zmienne
s i sep jako puste łańcuchy znaków. Szerzej pomówimy na temat zmiennych i deklaracji w rozdziale 2.
W przypadku typów numerycznych Go zapewnia zwykłe operatory arytmetyczne i logiczne.
Jednak operator + po zastosowaniu do typów łańcuchowych konkatenuje wartości, więc wyraże-
nie sep + os.Args[i] reprezentuje konkatenację łańcuchów znaków sep i os.Args[i]. Użyta
w programie instrukcja s += sep + os.Args[i] jest instrukcją przypisania, która konkatenuje
starą wartość zmiennej s z wartościami sep i os.Args[i], a następnie przypisuje ją z powrotem
do zmiennej s. Jest to równoważne z instrukcją s = s + sep + os.Args[i]. Operator += jest
operatorem przypisania. Każdy operator arytmetyczny i logiczny, taki jak + lub *, ma odpowiadający
mu operator przypisania.
Program echo mógłby wyświetlić swoje dane wyjściowe w pętli, po jednym kawałku na raz, ale ta
wersja zamiast tego buduje łańcuch znaków poprzez wielokrotne dodawanie nowego tekstu na
końcu. Łańcuch s jest w momencie utworzenia pusty (czyli ma wartość ""), a każde przejście
przez pętlę dodaje do niego jakiś fragment tekstu. Po pierwszej iteracji wstawiana jest również spacja,
więc gdy pętla zostanie zakończona, będzie jedna spacja pomiędzy każdym argumentem. Jest to
proces kwadratowy, który może być kosztowny, jeśli liczba argumentów jest duża, ale w przypadku
echo jest to mało prawdopodobne. W tym i w następnym rozdziale pokażemy kilka ulepszonych
wersji programu echo, które będą radzić sobie z wszelkimi rzeczywistymi niewydolnościami.
Zmienna i indeksu pętli jest zadeklarowana w pierwszej części pętli for. Symbol := jest częścią
krótkiej deklaracji zmiennych, czyli instrukcji, która deklaruje jedną zmienną lub więcej zmiennych
i nadaje im odpowiednie typy na podstawie wartości inicjatora. Więcej informacji na ten temat
znajdziesz w następnym rozdziale.
Instrukcja inkrementacji i++ dodaje 1 do wartości zmiennej i. Jest to równoważne z instrukcją
i += 1, która z kolei jest równoważna z instrukcją i = i + 1. Istnieje analogiczna instrukcja de-
krementacji i--, która odejmuje 1. Są to instrukcje, a nie wyrażenia, jak w większości języków
z rodziny C, więc j = i++ jest nieprawidłowe. Są one również zapisywane w tylko notacji postfikso-
wej, więc --i także nie jest prawidłowe.
Pętla for jest jedyną instrukcją pętli w języku Go. Ma ona wiele form, a jedna z nich jest następująca:
for inicjacja; warunek; publikacja {
// Zero lub więcej instrukcji.
}
Wokół tych trzech komponentów pętli for nigdy nie są używane nawiasy. Nawiasy klamrowe są
jednak obowiązkowe i klamrowy nawias otwierający musi znajdować się w tej samej linii co in-
strukcja publikacja.
Opcjonalna instrukcja inicjacja jest wykonywana przed rozpoczęciem pętli. Jeśli jest obecna,
musi być instrukcją prostą, czyli krótką deklaracją zmiennych, instrukcją inkrementacji lub przy-
pisania albo wywołaniem funkcji. Komponent warunek jest wyrażeniem logicznym, które jest
ewaluowane na początku każdej iteracji pętli. Jeśli ewaluacja daje wartość true (prawda), wykonywa-
ne są instrukcje kontrolowane przez pętle. Instrukcja publikacja jest wykonywana po ciele pętli,
a następnie warunek jest ponownie ewaluowany. Pętla kończy się, gdy warunek staje się fałszywy.
Każda z tych części może być pominięta. Jeśli nie ma instrukcji inicjacja i publikacja, można
również pominąć dwukropki:
// Tradycyjna pętla "while".
for warunek {
// …
}
22 ROZDZIAŁ 1. PRZEWODNIK

Jeśli warunek zostanie całkowicie pominięty w którejś z tych form, np.:


// Tradycyjna pętla nieskończona.
for {
// …
}
pętla jest nieskończona, choć pętle w tej formie można przerwać w inny sposób, m.in. za pomocą
instrukcji break lub return.
Inna forma pętli for iteruje przez zakres wartości danych określonego typu, taki jak łańcuch
znaków lub wycinek. Ilustruje to druga wersja programu echo:
code/r01/echo2
// Echo2 wyświetla swoje argumenty wiersza poleceń.
package main

import (
"fmt"
"os"
)

func main() {
s, sep := "", ""
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
W każdej iteracji pętli range (zakres) generuje parę wartości: indeks i wartość elementu o tym
indeksie. W tym przykładzie nie potrzebujemy indeksu, ale składnia pętli range wymaga go, jeśli
mamy do czynienia z elementem. Jednym z pomysłów byłoby przypisanie indeksu do oczywistej
zmiennej tymczasowej, takiej jak temp, i zignorowanie jego wartości, ale Go nie dopuszcza nie-
używanych zmiennych lokalnych, więc spowodowałoby to błąd kompilacji.
Rozwiązaniem jest skorzystanie z pustego identyfikatora (ang. blank identifier), którego nazwą
jest _ (czyli podkreślnik). Pusty identyfikator może być używany wszędzie tam, gdzie składnia
wymaga nazwy zmiennej, ale logika programu nie, np. w celu pozbycia się niechcianego indeksu
pętli, gdy potrzebujemy jedynie wartości elementu. Większość programistów Go prawdopodob-
nie użyłaby do napisania programu echo pętli range i pustego identyfikatora _, tak jak przedsta-
wiono powyżej, ponieważ indeksowanie na os.Args jest domyślne, a nie bezpośrednie, dlatego
łatwiej jest zrobić to prawidłowo.
Ta wersja programu korzysta z krótkiej deklaracji zmiennych w celu zadeklarowania i zainicjowa-
nia zmiennych s oraz sep, ale może równie dobrze zadeklarować te zmienne osobno. Istnieje kilka
sposobów deklarowania zmiennej typu string. Poniższe przykłady są równoważne:
s := ""
var s string
var s = ""
var s string = ""
Czym powinniśmy się kierować przy wybieraniu tej, a nie innej formy? Pierwsza forma, czyli
krótka deklaracja zmiennych, jest najbardziej zwarta, lecz może być stosowana wyłącznie w obrębie
funkcji, a nie dla zmiennych poziomu pakietu. Druga forma opiera się na domyślnym inicjowaniu
1.3. WYSZUKIWANIE ZDUPLIKOWANYCH LINII 23

do wartości zerowej dla łańcuchów znaków, którą jest "". Trzecia forma jest rzadko stosowana,
z wyjątkiem przypadku deklarowania wielu zmiennych. Czwarta forma wyraźnie wskazuje typ
zmiennej, co jest zbędne, gdy jest on taki sam jak typ początkowej wartości, ale konieczne w in-
nych przypadkach, gdy te zmienne nie są tego samego typu. W praktyce należy zasadniczo uży-
wać jednej z pierwszych dwóch form: z bezpośrednim inicjowaniem, aby wskazać, że wartość
początkowa jest ważna, i z domyślnym inicjowaniem, aby wskazać, że wartość początkowa nie ma
znaczenia.
Jak stwierdzono powyżej, przy każdym przejściu pętli łańcuch s otrzymuje całkowicie nową za-
wartość. Instrukcja += tworzy nowy łańcuch znaków przez konkatenację starego łańcucha, znaku
spacji oraz następnego argumentu, a następnie przypisuje ten nowy łańcuch znaków do zmiennej s.
Stara zawartość zmiennej s nie jest już używana, więc w odpowiednim czasie zostanie usunięta
przez mechanizm oczyszczania pamięci.
Jeśli ilość zaangażowanych danych jest duża, może to być kosztowne. Prostszym i bardziej efek-
tywnym rozwiązaniem byłoby użycie funkcji Join z pakietu strings:
code/r01/echo3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
Jeśli nie dbamy o format, ale po prostu chcemy zobaczyć wartości (np. dla debugowania), mo-
żemy pozwolić funkcji Println sformatować wyniki dla nas:
fmt.Println(os.Args[1:])
Dane wyjściowe z tej instrukcji są takie, jakie otrzymalibyśmy z funkcji strings.Join, ale zostały
umieszczone w nawiasach kwadratowych. W ten sposób może być wyświetlany każdy wycinek.
Ćwiczenie 1.1. Zmodyfikuj program echo w taki sposób, aby wyświetlał również os.Args[0],
czyli nazwę wywołującego go polecenia.
Ćwiczenie 1.2. Zmodyfikuj program echo w taki sposób, aby wyświetlał indeks i wartość każdego
ze swoich argumentów w osobnych liniach.
Ćwiczenie 1.3. Przeprowadź eksperyment, aby zmierzyć różnicę w czasie działania między na-
szymi potencjalnie nieefektywnymi wersjami i wersją wykorzystującą funkcję strings.Join.
(Podrozdział 1.6 ilustruje część pakietu time, a podrozdział 11.4 pokazuje, jak pisać benchmarki
do systematycznej oceny wydajności).

1.3. Wyszukiwanie zduplikowanych linii


Programy wykonujące takie zadania, jak kopiowanie, wyświetlanie, wyszukiwanie, sortowanie
czy liczenie, mają podobną strukturę: pętla dla danych wejściowych, pewne obliczenia na każdym
elemencie oraz generowanie danych wyjściowych w locie lub na końcu. Pokażemy trzy warianty
programu o nazwie dup. Jest on częściowo inspirowany uniksowym poleceniem uniq, które wy-
szukuje sąsiadujące ze sobą zduplikowane linie. Wykorzystane struktury i pakiety są modelami,
które można łatwo dostosować.
Pierwsza wersja programu dup wyświetla każdą linię, która pojawia się więcej niż raz na stan-
dardowym wejściu, i poprzedza ją liczbą wystąpień. Program ten wprowadza instrukcję if, typ
danych map oraz pakiet bufio.
24 ROZDZIAŁ 1. PRZEWODNIK

code/r01/dup1
// Dup1 wyświetla tekst każdej linii, która pojawia się na standardowym wejściu więcej niż raz,
// i poprzedza go liczbą wystąpień.
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
counts[input.Text()]++
}
// UWAGA: ignorowanie potencjalnych błędów z funkcji input.Err().
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
Tak jak w przypadku pętli for, w instrukcji if warunek nigdy nie jest umieszczany w nawiasach,
ale dla ciała instrukcji wymagane są nawiasy klamrowe. Opcjonalną część instrukcji stanowi
blok else, który jest wykonywany, jeśli warunek jest fałszywy.
Mapa przechowuje zbiór par klucz-wartość i zapewnia wykonywanie w stałym czasie operacji
przechowywania, pobierania lub testowania elementu znajdującego się w tym zbiorze. Klucz
może być dowolnym spośród tych typów danych, których wartości mogą być porównywane za
pomocą operatora ==; najbardziej powszechnym przykładem jest typ string. Wartość może być
całkowicie dowolnym typem danych. W tym przykładzie klucze są łańcuchami znaków, a wartości są
typami int. Wbudowana funkcja make tworzy nową pustą mapę, ale ma też inne zastosowania.
Mapy zostały szczegółowo omówione w podrozdziale 4.3.
Za każdym razem, gdy program dup odczytuje linię danych wejściowych, ta linia jest używana jako
klucz do mapy i odpowiadająca jej wartość jest zwiększana. Instrukcja counts[input.Text()]++
jest równoważna z poniższymi dwiema instrukcjami:
line := input.Text()
counts[line] = counts[line] + 1
Nie jest problemem, jeśli mapa nie zawiera jeszcze danego klucza. Przy każdorazowym rozpozna-
niu nowej linii wyrażenie counts[line] po prawej stronie instrukcji ewaluuje do wartości zerowej
dla swojego typu, którą jest 0 dla typu int.
Aby wyświetlić wyniki, używamy kolejnej pętli for opartej na zakresie (range), tym razem zapę-
tlając mapę counts. Tak jak poprzednio każda iteracja generuje dwa wyniki: klucz i wartość
elementu mapy dla tego klucza. Kolejność iteracji po elementach mapy nie jest określona, ale
w praktyce jest przypadkowa, zmieniając się przy każdym uruchomieniu. Takie zachowanie jest
celowe, ponieważ zapobiega poleganiu przez programy na jakiejś konkretnej kolejności tam,
gdzie żadna kolejność nie jest gwarantowana.
Przejdźmy do pakietu bufio, który pomaga zapewnić efektywność i wygodę operacji wejścia
i wyjścia. Jedną z najbardziej przydatnych funkcjonalności tego pakietu jest typ o nazwie Scanner
1.3. WYSZUKIWANIE ZDUPLIKOWANYCH LINII 25

(skaner), który odczytuje dane wejściowe i rozdziela je na linie lub słowa. Często najprostszym
sposobem jest przetwarzanie danych wejściowych, które naturalnie podawane są w postaci linii.
Program korzysta z krótkiej deklaracji zmiennych, aby utworzyć nową zmienną input, która od-
wołuje się do bufio.Scanner:
input := bufio.NewScanner(os.Stdin)
Skaner odczytuje ze standardowego wejścia programu. Każde wywołanie input.Scan() odczytuje
następną linię i usuwa znak nowego wiersza z końca. Wyniki mogą być pobierane za pomocą
wywołania input.Text(). Funkcja Scan zwraca true (prawda), jeśli istnieje jakaś linia, i false
(fałsz), jeśli nie ma więcej danych wejściowych.
Funkcja fmt.Printf, tak jak printf w języku C i w innych językach, generuje sformatowane
dane wyjściowe na podstawie listy wyrażeń. Jej pierwszym argumentem jest łańcuch znaków
formatu, który określa, jak powinny być sformatowane kolejne argumenty. Format każdego argu-
mentu jest określany przez znak konwersji, czyli literę umieszczoną po symbolu procentu. Przy-
kładowo: %d formatuje operand liczby całkowitej za pomocą notacji dziesiętnej, a %s rozwija do
wartości operandu w postaci łańcucha znaków.
Funkcja Printf posiada ponad tuzin takich konwersji, które programiści Go nazywają czasowni-
kami (ang. verbs). Poniższa tabela jest daleka od kompletnej specyfikacji, ale pokazuje wiele do-
stępnych funkcji.
%d dziesiętna liczba całkowita
%x, %o, %b liczba całkowita w formacie szesnastkowym, ósemkowym, binarnym
%f, %g, %e liczba zmiennoprzecinkowa: 3.141593, 3.141592653589793, 3.141593e+00
%t wartość logiczna: true (prawda) lub false (fałsz)
%c runa (punkt kodowy Unicode)
%s łańcuch znaków
%q cytowany łańcuch znaków "abc" lub runa 'c'
%v dowolna wartość w naturalnym formacie
%T typ dowolnej wartości
%% literalny znak procentu (bez operandu)

Łańcuch znaków formatu w programie dupl zawiera również tabulator \t i znak nowej linii \n.
Literały łańcuchów znaków mogą zawierać takie sekwencje ucieczki (ang. escape sequences) w celu
reprezentowania znaków w inny sposób niewidocznych. Printf domyślnie nie wypisuje znaku
nowej linii. Zgodnie z konwencją funkcje formatowania, których nazwy kończą się na f (takie jak
log.Printf i fmt.Errorf), używają reguł formatowania funkcji fmt.Printf. Natomiast te o na-
zwach kończących się na ln używają reguł formatowania Println, formatując swoje argumenty
w taki sposób, jakby został użyty czasownik %v, po którym następuje nowa linia.
Wiele programów odczytuje ze swojego standardowego wejścia (tak jak powyżej) lub z sekwencji
nazwanych plików. Kolejna wersja programu dup może czytać ze standardowego wejścia lub ob-
sługiwać listę nazw plików, używając do otwarcia każdego pliku funkcji os.Open:
code/r01/dup2
// Dup2 wyświetla liczbę wystąpień i tekst linii, które w danych wejściowych pojawiają się więcej niż raz.
// Odczytuje ze standardowego wejścia lub z listy nazwanych plików.
26 ROZDZIAŁ 1. PRZEWODNIK

package main

import (
"bufio"
"fmt"
"os"
)

func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}

func countLines(f *os.File, counts map[string]int) {


input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
// UWAGA: ignorowanie potencjalnych błędów z funkcji input.Err().
}
Funkcja os.Open zwraca dwie wartości. Pierwszą jest otwarty plik (*os.File), który jest wykorzysty-
wany przez Scanner w kolejnych odczytach.
Drugim wynikiem funkcji os.Open jest wartość wbudowanego typu error (błąd). Jeśli err równa
się specjalnej wbudowanej wartości nil, plik został otwarty pomyślnie. Plik jest odczytywany,
a gdy zostanie osiągnięty koniec danych wejściowych, funkcja Close zamyka plik i zwalnia wszystkie
zasoby. Z drugiej strony, jeśli err nie równa się nil, coś poszło nie tak. W takim przypadku wartość
błędu opisuje problem. Nasza prosta procedura obsługi błędów wyświetla komunikat w standardo-
wym strumieniu błędów przy użyciu funkcji Fprintf i czasownika %v, który wyświetla wartość
dowolnego typu w formacie domyślnym, po czym program dup przechodzi do następnego pliku. In-
strukcja continue przechodzi do następnej iteracji pętli for, w której jest umieszczona.
Aby zachować rozsądne rozmiary próbek kodu, nasze wczesne przykłady celowo pomijały kwestię
obsługi błędów. Oczywiście musimy sprawdzać błędy z funkcji os.Open. Zignorujemy jednak
mniej prawdopodobną możliwość, że błąd może wystąpić w trakcie odczytywania pliku za pomocą
funkcji input.Scan. Zaznaczymy miejsca, gdzie pominęliśmy sprawdzanie błędów, i szczegółowo
zajmiemy się kwestią obsługi błędów w podrozdziale 5.4.
1.3. WYSZUKIWANIE ZDUPLIKOWANYCH LINII 27

Zwróć uwagę, że wywołanie funkcji countLines poprzedza jej deklarację. Funkcje i inne encje
poziomu pakietu mogą być deklarowane w dowolnej kolejności.
Mapa jest referencją do struktury danych tworzonej za pomocą funkcji make. Kiedy mapa jest
przekazywana do funkcji, ta funkcja otrzymuje kopię referencji, więc wszelkie zmiany wprowa-
dzane przez wywoływaną funkcję w bazowej strukturze danych będą widoczne również poprzez
referencję mapy funkcji wywołującej. W naszym przykładzie wartości wstawiane do mapy counts
przez funkcję countLines są widziane przez funkcję main.
Powyższe wersje programu dup pracują w trybie „strumieniowania”, w którym dane wejściowe
są według potrzeb odczytywane i rozdzielane na linie, więc w zasadzie programy te mogą obsłu-
giwać dowolną ilość danych wejściowych. Alternatywnym podejściem jest wczytanie do pamięci
całych danych wejściowych jednym wielkim haustem, dzielenie ich od razu na linie, a następnie
przetwarzanie linii. W ten sposób działa kolejna wersja programu — dup3. Wprowadza ona
funkcję ReadFile (z pakietu io/ioutil), która odczytuje całą zawartość nazwanego pliku, oraz
funkcję strings.Split, która dzieli łańcuch znaków na wycinek podłańcuchów. (Funkcja Split
jest przeciwieństwem przedstawionej wcześniej funkcji strings.Join).
Uprościliśmy nieco program dup3. Po pierwsze, odczytuje on tylko pliki nazwane, a nie standar-
dowe wejście, ponieważ ReadFile wymaga argumentu nazwy pliku. Po drugie, przenieśliśmy
zliczanie linii z powrotem do funkcji main, ponieważ jest ono teraz potrzebne tylko w jednym
miejscu.
code/r01/dup3
package main

import (
"fmt"
"io/ioutil"
"os"
"strings"
)

func main() {
counts := make(map[string]int)
for _, filename := range os.Args[1:] {
data, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
continue
}
for _, line := range strings.Split(string(data), "\n") {
counts[line]++
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
Funkcja ReadFile zwraca wycinek bajtów, który musi być zamieniony na typ string, aby mógł
zostać podzielony przez funkcję strings.Split. Typy string i wycinki bajtów zostaną szczegóło-
wo omówione w podrozdziale 3.5.4.
28 ROZDZIAŁ 1. PRZEWODNIK

Po kryjomu funkcje bufio.Scanner, ioutil.ReadFile oraz ioutil.WriteFile używają metod


Read i Write funkcji *os.File, ale rzadko kiedy programiści potrzebują uzyskiwać dostęp do
tych niskopoziomowych procedur bezpośrednio. Łatwiejsze w użyciu są funkcje wyższego pozio-
mu, takie jak te z pakietów bufio i io/ioutil.
Ćwiczenie 1.4. Zmodyfikuj program dup2 w taki sposób, aby wyświetlał nazwy wszystkich plików,
w których występuje każda ze zduplikowanych linii.

1.4. Animowane GIF-y


Kolejny program demonstruje podstawowe wykorzystanie standardowych pakietów graficznych
języka Go, których użyjemy do utworzenia sekwencji bitmapowych obrazów, a następnie zako-
dowania tej sekwencji jako animacji GIF. Te obrazy, zwane figurami Lissajous, były wykorzy-
stywane jako podstawowe efekty wizualne w filmach sci-fi w latach 60. ubiegłego wieku. Są to
krzywe parametryczne wytwarzane przez oscylację harmoniczną w dwóch wymiarach, m.in.
przez dwa sygnały sinusoidalne podawane na wejścia X i Y oscyloskopu. Rysunek 1.1 pokazuje
kilka przykładów.

Rysunek 1.1. Cztery figury Lissajous

W tym kodzie pojawia się kilka nowych konstruktów, w tym deklaracje const, struktury oraz lite-
rały kompozytowe. W przeciwieństwie do większości naszych przykładów, ten obejmuje rów-
nież obliczenia zmiennoprzecinkowe. W tym miejscu omówimy jednak te tematy tylko pobieżnie,
pozostawiając większość szczegółów do opisania w późniejszych rozdziałach, ponieważ tutaj
głównym celem jest danie Ci wyobrażenia o tym, jak wygląda język Go i co można łatwo zrobić za
pomocą tego języka i jego bibliotek.
code/r01/lissajous
// Lissajous generuje animacje GIF losowych figur Lissajous.
package main

import (
"image"
"image/color"
"image/gif"
"io"
"math"
"math/rand"
"os"
)

var palette = []color.Color{color.White, color.Black}


1.4. ANIMOWANE GIF-Y 29

const (
whiteIndex = 0 // pierwszy kolor w zmiennej palette
blackIndex = 1 // następny kolor w zmiennej palette
)

func main() {
lissajous(os.Stdout)
}

func lissajous(out io.Writer) {


const (
cycles = 5 // liczba pełnych obiegów oscylatora x
res = 0.001 // rozdzielczość kątowa
size = 100 // rozmiar płótna obrazu [–size..+size]
nframes = 64 // liczba klatek animacji
delay = 8 // opóźnienie między klatkami w jednostkach 10 ms
)
freq := rand.Float64() * 3.0 // częstotliwość względna oscylatora y
anim := gif.GIF{LoopCount: nframes}
phase := 0.0 // przesunięcie fazowe
for i := 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) // UWAGA: ignorowanie błędów kodowania
}
Po zaimportowaniu pakietu, którego ścieżka zawiera kilka elementów (takiego jak image/color),
odwołujemy się do tego pakietu za pomocą nazwy pochodzącej od ostatniego elementu. Tak więc
zmienna color.White należy do pakietu image/color, a zmienna gif.GIF do pakietu image/gif.
Deklaracja const (zob. podrozdział 3.6) nadaje nazwy stałym, czyli wartościom, które są ustawiane
podczas kompilacji, takim jak parametry liczbowe dla cykli, klatek i opóźnienia. Podobnie jak
deklaracje var, deklaracje const mogą występować na poziomie pakietu (wtedy nazwy są wi-
doczne w całym pakiecie) lub w obrębie funkcji (wtedy nazwy są widoczne tylko w tej funkcji).
Wartość stałej musi być liczbą, łańcuchem znaków lub wartością logiczną.
Wyrażenia []color.Color{...} oraz gif.GIF{...} są literałami złożonymi (zob. podrozdział
4.2 i punkt 4.4.1), czyli kompaktową notacją do tworzenia instancji któregokolwiek z typów zło-
żonych języka Go z sekwencji wartości elementów. W tym przypadku pierwsze wyrażenie jest
wycinkiem, a drugie strukturą.
Typ gif.GIF jest strukturą (zostanie omówiona w podrozdziale 4.4). Struktura jest grupą warto-
ści zwanych polami, często różnego typu, zebranych razem w pojedynczym obiekcie, który może
być traktowany jako jednostka. Zmienna anim jest strukturą typu gif.GIF. Literał struktury
tworzy wartość struktury, której pole LoopCount jest ustawione na nframes. Wszystkie pozostałe
pola mają wartość zerową dla swojego typu. Do poszczególnych pól struktury można uzyskać
30 ROZDZIAŁ 1. PRZEWODNIK

dostęp za pomocą notacji kropkowej, tak jak w dwóch ostatnich przypisaniach, które bezpo-
średnio aktualizują pola Delay i Image zmiennej anim.
Funkcja lissajous ma dwie zagnieżdżone pętle. Zewnętrzna pętla wykonuje 64 iteracje, z których
każda generuje pojedynczą klatkę animacji. Tworzy to nowy obraz 201×201 z paletą (palette)
dwóch kolorów: białego i czarnego. Wszystkie piksele są początkowo ustawione na wartość zerową
zmiennej palette (zerowy kolor w palecie), którą ustawiliśmy jako kolor biały. Każde przejście
przez wewnętrzną pętlę generuje nowy obraz, ustawiając niektóre piksele na czarno. Za pomocą
wbudowanej funkcji append (zob. punkt 4.2.l) wynik jest dołączany do listy klatek w zmiennej
anim wraz z określonym na 80 milisekund opóźnieniem. Na koniec sekwencja klatek i opóźnień
jest kodowana w formacie GIF i zapisywana w strumieniu wyjściowym out. Typem out jest
io.Writer, który pozwala zapisywać w szerokiej gamie możliwych miejsc docelowych, jak poka-
żemy wkrótce.
Wewnętrzna pętla uruchamia dwa oscylatory. Oscylator x jest po prostu funkcją sinus. Oscylator y
jest również sinusoidą, ale jego częstotliwość względem oscylatora x jest liczbą losową z przedziału
od 0 do 3. Natomiast jego faza w stosunku do oscylatora x jest początkowo zerowa, lecz zwiększa
się z każdą klatką animacji. Pętla działa, dopóki oscylator x nie zakończy pięciu pełnych cykli.
W każdym kroku pętla wywołuje funkcję SetColorIndex, aby pokolorować na czarno (pozycja 1
w palecie) piksel odpowiadający współrzędnym (x, y).
Funkcja main wywołuje funkcję lissajous, wskazując, aby zapisywała do standardowego strumienia
wyjściowego, więc poniższe polecenie tworzy animowany GIF z klatek takich jak te pokazane na
rysunku 1.1:
$ go build code/r01/lissajous
$ ./lissajous > out.gif
Ćwiczenie 1.5. W celu zwiększenia autentyczności zmień paletę kolorów programu Lissajous,
aby generował zielone figury na czarnym tle. Aby utworzyć internetowy kolor #RRGGBB, użyj wy-
rażenia color.RGBA{0xRR, 0xGG, 0xBB, 0xff}, w którym każda para cyfr szesnastkowych re-
prezentuje natężenie w pikselu komponentu czerwonego, zielonego lub niebieskiego.
Ćwiczenie 1.6. Zmodyfikuj program Lissajous w taki sposób, aby generował obrazy w wielu kolo-
rach. Zrób to, dodając więcej wartości do zmiennej palette, a następnie wyświetlając je poprzez
zmienianie trzeciego argumentu SetColorIndex w jakiś ciekawy sposób.

1.5. Pobieranie zawartości adresu URL


Dla wielu aplikacji dostęp do informacji z internetu jest równie ważny jak dostęp do lokalnego
systemu plików. Język Go zapewnia kolekcję pakietów zgrupowanych pod nazwą net, ułatwiają-
cych wysyłanie i odbieranie informacji za pośrednictwem internetu, wykonywanie niskopozio-
mowych połączeń sieciowych oraz konfigurowanie serwerów, dla których funkcje współbieżno-
ści Go (wprowadzone w rozdziale 8.) są szczególnie przydatne.
W celu zilustrowania minimum niezbędnego do pobrania informacji za pośrednictwem proto-
kołu HTTP poniżej został przedstawiony prosty program o nazwie fetch, który pobiera zawartość
każdego z określonych adresów URL i wyświetla ją jako nieinterpretowany tekst. Program ten
jest inspirowany nieocenionym narzędziem curl. Oczywiście można by zrobić coś więcej z takimi
danymi, ale pokazuje to podstawową ideę. W całej książce często będziemy z tego programu
korzystać.
1.5. POBIERANIE ZAWARTOŚCI ADRESU URL 31

code/r01/fetch
// Fetch wyświetla zawartość znalezioną pod adresem URL.
package main

import (
"fmt"
"io/ioutil"
"net/http"
"os"
)

func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: odczytywanie %s: %v\n", url, err)
os.Exit(1)
}
fmt.Printf("%s", b)
}
}
Program ten wprowadza funkcje z dwóch pakietów: net/http oraz io/ioutil. Funkcja http.Get
wykonuje żądanie HTTP i jeśli nie ma błędu, zwraca wynik w strukturze odpowiedzi resp. Pole
Body struktury resp zawiera odpowiedź serwera w formie możliwego do odczytania strumienia.
Następnie funkcja ioutil.ReadAll odczytuje całą odpowiedź. Wynik jest przechowywany w b.
Strumień Body jest zamykany, aby uniknąć wyciekania zasobów, a funkcja Printf wypisuje odpo-
wiedź na standardowy strumień wyjściowy.
$ go build code/r01/fetch
$ ./fetch http://golang.org
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#375EAB">

<title>The Go Programming Language</title>


...
Jeśli żądanie HTTP się nie powiedzie, program fetch zamiast powyższego zgłasza niepowodzenie:
$ ./fetch http://badgolang.org
fetch: Get http://badgolang.org: dial tcp: lookup badgolang.org: no such host
W każdym przypadku wystąpienia błędu funkcja os.Exit(1) powoduje wyjście z procesu z ko-
dem statusu 1.
Ćwiczenie 1.7. Wywołanie funkcji io.Copy(dst, src) odczytuje ze źródła src i zapisuje w miejscu
docelowym dst. Użyj tej funkcji zamiast ioutil.ReadAll do skopiowania treści odpowiedzi do
os.Stdout bez konieczności zastosowania na tyle dużego bufora, aby pomieścił cały strumień.
Pamiętaj, aby sprawdzić wynik błędu io.Copy.
32 ROZDZIAŁ 1. PRZEWODNIK

Ćwiczenie 1.8. Zmodyfikuj program fetch w taki sposób, aby dodawał prefiks http:// do każdego
argumentu URL, jeśli tego prefiksu brakuje. Możesz użyć funkcji strings.HasPrefix.
Ćwiczenie 1.9. Zmodyfikuj program fetch w taki sposób, aby wyświetlał również kod statusu
HTTP, który można znaleźć w resp.Status.

1.6. Pobieranie zawartości kilku adresów URL równolegle


Jednym z najbardziej interesujących i nowatorskich aspektów języka Go jest obsługa programo-
wania równoległego. Jest to obszerny temat, któremu poświęcone są rozdziały 8. i 9. W tym miej-
scu damy Ci tylko przedsmak głównych mechanizmów, funkcji goroutine i kanałów współbieżności
języka Go.
Kolejny program, o nazwie fetchall, wykonuje takie samo pobieranie zawartości adresu URL
jak poprzedni. Pobiera jednak zawartości wielu adresów URL równolegle w taki sposób, że cały
proces nie potrwa dłużej niż czas trwania najdłuższego pobierania, czyli będzie krótszy niż suma
wszystkich czasów pobierania. Ta wersja programu fetchall porzuca wszystkie odpowiedzi, ale
raportuje rozmiar i czas pobierania dla każdej z nich:
code/r01/fetchall
// Fetchall pobiera równolegle zawartości kilku adresów URL i raportuje czasy pobierania
// oraz rozmiary odpowiedzi.
package main

import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"time"
)

func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch) // rozpoczęcie funkcji goroutine
}
for range os.Args[1:] {
fmt.Println(<-ch) // odbieranie z kanału ch
}
fmt.Printf("%.2fs upłynęło\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {


start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err) // wysyłanie do kanału ch
return
}
nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close() // aby nie wyciekały zasoby
if err != nil {
ch <- fmt.Sprintf("podczas odczytywania %s: %v", url, err)
1.7. SERWER WWW 33

return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}
Oto przykład:
$ go build code/r01/fetchall
$ ./fetchall http://helion.pl http://onepress.pl http://sensus.pl
0.12s 63902 http://helion.pl
0.14s 58088 http://onepress.pl
0.14s 101711 http://sensus.pl0.14s upłynęło
Goroutine jest równoległym wykonywaniem funkcji. Kanał (ang. channel) jest mechanizmem
komunikacji, który pozwala jednej funkcji goroutine przekazywać wartości określonego typu do
innej funkcji goroutine. Funkcja main jest uruchamiana w funkcji goroutine, a instrukcja go tworzy
dodatkowe funkcje goroutine.
Funkcja main tworzy kanał łańcuchów znaków za pomocą funkcji make. Dla każdego argumentu
wiersza poleceń instrukcja go w pierwszej pętli zakresowej rozpoczyna nową funkcję goroutine,
która wywołuje funkcję fetch asynchronicznie, aby pobrać zawartość adresu URL za pomocą
funkcji http.Get. Funkcja io.Copy odczytuje treść odpowiedzi i porzuca ją, zapisując do strumie-
nia danych wyjściowych ioutil.Discard. Copy zwraca liczbę bajtów razem z wszelkimi błędami,
które wystąpiły. Przy każdym dostarczeniu rezultatu funkcja fetch wysyła linię podsumowującą
przez kanał ch. Druga pętla zakresowa w funkcji main odbiera i wyświetla te linie.
Gdy jedna funkcja goroutine próbuje wysyłać lub odbierać przez kanał, zakłada blokadę, dopóki inna
funkcja goroutine nie podejmie próby wykonania odpowiadającej operacji odbierania lub wysyłania,
co spowoduje przesłanie danej wartości, po czym obie funkcje goroutine będą mogły kontynuować
swoje działanie. W tym przykładzie każda funkcja fetch wysyła wartość (ch <- wyrażenie)
przez kanał ch, a funkcja main odbiera wszystkie wartości (<- ch). Ponieważ funkcja main wykonuje
wszystkie operacje wyświetlania, mamy pewność, że dane wyjściowe z każdej funkcji goroutine
będą przetwarzane jako jednostka bez obawy przeplatania, jeśli dwie funkcje goroutine zakończą
działanie w tym samym czasie.
Ćwiczenie 1.10. Znajdź stronę internetową, która wytwarza duże ilości danych. Zbadaj buforo-
wanie poprzez uruchomienie programu fetchall dwa razy z rzędu, aby zobaczyć, czy raportowany
czas pobierania bardzo się zmieni. Czy za każdym razem otrzymujesz taką samą zawartość? Zmody-
fikuj program fetchall w taki sposób, aby zapisywał dane wyjściowe do pliku, by można je było
zbadać.
Ćwiczenie 1.11. Spróbuj uruchomić program fetchall z dłuższą listą argumentów, takich jak
próbki z miliona najbardziej popularnych stron internetowych dostępne w serwisie alexa.com.
Jak zachowuje się program, jeśli strona internetowa po prostu nie odpowiada? (Mechanizmy ra-
dzenia sobie w takich sytuacjach zostały opisane w podrozdziale 8.9).

1.7. Serwer WWW


Biblioteki języka Go ułatwiają napisanie serwera WWW, który odpowiada na żądania klientów, takie
jak te wysyłane przez program fetch. W tym podrozdziale przedstawimy prosty serwer, który zwraca
komponent ścieżki adresu URL używany do uzyskania dostępu do serwera. Oznacza to, że jeżeli
żądanie dotyczy adresu: http://localhost:8000/hello, odpowiedzią będzie: URL.Path = "/hello".
34 ROZDZIAŁ 1. PRZEWODNIK

code/r01/server1
// Server1 jest minimalnym serwerem "echo".
package main

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", handler) // każde żądanie wywołuje funkcję handler
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler zwraca komponent Path żądanego adresu URL.


func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
Program ma tylko kilka linii, ponieważ większość pracy wykonują funkcje biblioteki. Funkcja
main łączy funkcję handler z przychodzącymi adresami URL, których ścieżka zaczyna się od /,
czyli ze wszystkimi adresami URL, oraz uruchamia serwer nasłuchujący przychodzących żądań na
porcie 8000. Żądanie jest reprezentowane jako struktura typu http.Request zawierająca szereg
powiązanych pól — jednym z nich jest adres URL przychodzącego żądania. Przybywające żąda-
nie jest przekazywane do funkcji handler, która wydobywa komponent ścieżki (/hello)
z adresu URL żądania i odsyła go jako odpowiedź, używając funkcji fmt.Fprintf. Serwery WWW
zostaną szczegółowo omówione w podrozdziale 7.7.
Uruchommy ten serwer w tle. W systemach Mac OS X i Linux należy dodać do polecenia znak
et (&). W systemach Microsoft Windows należy uruchomić polecenie (bez znaku et) w osobnym
oknie poleceń.
$ go run src/code/r01/server1/main.go &
Teraz możemy wysyłać żądania klienta z poziomu wiersza poleceń:
$ go build code/r01/fetch
$ ./fetch http://localhost:8000
URL.Path = "/"
$ ./fetch http://localhost:8000/help
URL.Path = "/help"
Alternatywnie można uzyskać dostęp do serwera z poziomu przeglądarki internetowej, tak jak
pokazano na rysunku 1.2.

Rysunek 1.2. Odpowiedź z serwera echo


1.7. SERWER WWW 35

Dodawanie funkcji do serwera jest proste. Przydatnym dodatkiem jest określony adres URL,
który zwraca pewnego rodzaju status. Przedstawiona poniżej wersja wykonuje np. to samo echo,
ale zlicza również liczbę żądań. Żądanie wysyłane do adresu URL /count zwraca aktualną liczbę
żądań wykonanych do tej pory, ale z wyłączeniem samych żądań /count:
code/r01/server2
// Server2 jest minimalnym serwerem "echo" i serwerem zliczającym.
package main

import (
"fmt"
"log"
"net/http"
"sync"
)

var mu sync.Mutex
var count int

func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler zwraca komponent Path żądanego adresu URL.


func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// counter zwraca liczbę dotychczas wykonanych wywołań.


func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Liczba wywołań %d\n", count)
mu.Unlock()
}
Ten serwer posiada dwie procedury obsługi (ang. handlers), a żądanie URL określa, która z nich
jest wywoływana: żądanie dla /count wywołuje funkcję counter, a wszystkie pozostałe żądania
wywołują funkcję handler. Wzorzec procedury obsługi kończący się ukośnikiem dopasowuje
dowolny adres URL, który ma ten wzorzec jako prefiks. Za kulisami serwer uruchamia procedurę
obsługi dla każdego przychodzącego żądania w osobnej funkcji goroutine, więc może obsługiwać
wiele żądań jednocześnie. Jeśli dwa równoległe żądania próbują zaktualizować zmienną count
(licznik) w tym samym momencie, może ona nie być zwiększana w sposób spójny. Taki program
będzie miał poważny błąd, który nazywa się sytuacją wyścigu (omówioną w podrozdziale 9.l). Aby
uniknąć tego problemu, musimy się upewnić, że co najwyżej jedna funkcja goroutine w danym mo-
mencie uzyskuje dostęp do danej zmiennej. Służą temu wywołania mu.Lock() i mu.Unlock(),
które okalają każdą operację uzyskiwania dostępu do zmiennej count. Współbieżności ze współ-
dzielonymi zmiennymi przyjrzymy się bliżej w rozdziale 9.
W bardziej rozwiniętym przykładzie funkcja handler może raportować informacje o nagłówkach
i formatować otrzymywane dane, dzięki czemu serwer staje się użyteczny dla żądań inspekcji
i debugowania:
36 ROZDZIAŁ 1. PRZEWODNIK

code/r01/server3
// handler zwraca żądanie HTTP.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}
Ten program wykorzystuje pola struktury http.Request, aby wygenerować dane wyjściowe, takie
jak te przedstawione w poniższym listingu:
GET /?q=query HTTP/1.1
Header["Accept-Encoding"] = ["gzip, deflate, sdch"]
Header["Accept-Language"] = ["en-US,en;q=0.8"]
Header["Connection"] = ["keep-alive"]
Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."]
Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."]
Host = "localhost:8000"
RemoteAddr = "127.0.0.1:59911"
Form["q"] = ["query"]
Zwróć uwagę na sposób zagnieżdżenia wywołania ParseForm w instrukcji if. Go pozwala, aby
proste instrukcje, takie jak deklaracja zmiennej lokalnej, poprzedzały warunek if, co jest szczegól-
nie użyteczne dla obsługi błędów, tak jak w tym przypadku. Moglibyśmy zapisać to następująco:
err := r.ParseForm()
if err != nil {
log.Print(err)
}
Jednak łączenie instrukcji jest krótsze i zmniejsza zakres zmiennej err, co jest dobrą praktyką.
Zakres zdefiniujemy w podrozdziale 2.7.
W tych programach widzieliśmy trzy bardzo różne typy używane jako strumienie wyjściowe.
Program fetch kopiował dane odpowiedzi HTTP do strumienia os.Stdout lub do pliku, podobnie
jak program lissajous. Program fetchall porzucał odpowiedź (licząc przy tym jej długość)
poprzez kopiowanie jej do trywialnego ujścia ioutil.Discard. Natomiast powyższy serwer WWW
wykorzystywał funkcję fmt.Fprintf w celu zapisywania do funkcji http.ResponseWriter repre-
zentującej przeglądarkę internetową.
Mimo że te trzy typy różnią się w szczegółach dotyczących tego, co robią, wszystkie spełniają
wymagania typowego interfejsu, co pozwala na użycie każdego z nich wszędzie tam, gdzie potrzebny
jest strumień wyjściowy. Ten interfejs, zwany io.Writer, zostanie omówiony w podrozdziale 7.1.
Mechanizm interfejsu języka Go jest tematem rozdziału 7., ale żeby dać wyobrażenie o jego
możliwościach, zobaczmy, jak łatwo jest połączyć serwer WWW z funkcją lissajous, aby animo-
wane GIF-y nie były zapisywane do standardowego strumienia wyjściowego, lecz do klienta HTTP.
W tym celu należy po prostu skopiować do serwera WWW funkcję lissajous (wraz z odpowied-
nimi importami) oraz dodać następujące linie kodu:
1.8. KILKA POMINIĘTYCH KWESTII 37

handler := func(w http.ResponseWriter, r *http.Request) {


lissajous(w)
}
http.HandleFunc("/", handler)
Można też zastosować równoważny zapis w tej postaci:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
})
Drugi argument wywołania funkcji HandleFunc w powyższym alternatywnym zapisie jest litera-
łem funkcji, czyli anonimową funkcją definiowaną w miejscu jej użycia. Zostanie to szczegółowo
wyjaśnione w podrozdziale 5.6.
Po dokonaniu tej zmiany wpisz w przeglądarce adres: http://localhost:8000. Przy każdym przełado-
waniu strony zobaczysz nową animację, taką jak ta przedstawiona na rysunku 1.3.

Rysunek 1.3. Animowane figury Lissajous w przeglądarce

Ćwiczenie 1.12. Zmodyfikuj serwer Lissajous, aby odczytywał wartości parametrów z adresu URL.
Można to zrobić np. w taki sposób, żeby adres URL, taki jak: http://localhost:8000/?cycles=20,
ustawiał liczbę cykli na 20 zamiast domyślnych 5. Użyj funkcji strconv.Atoi do przekonwertowania
parametru z łańcucha znaków na liczbę całkowitą. Dokumentację tej funkcji możesz wyświetlić
za pomocą polecenia go doc strconv.Atoi.

1.8. Kilka pominiętych kwestii


Istnieje jeszcze wiele kwestii dotyczących języka Go, których nie omówiliśmy w tym krótkim
wprowadzeniu. W tym podrozdziale opiszemy kilka tematów do tej pory ledwo zaznaczonych
lub całkowicie pominiętych. Zanim opiszemy je szczegółowo, poświęcimy im tylko tyle miejsca,
aby były znajome, gdy pojawią się wcześniej w którymś miejscu książki.
38 ROZDZIAŁ 1. PRZEWODNIK

Przepływ sterowania. Omówiliśmy dwie podstawowe instrukcje przepływu sterowania: if oraz


for, ale nie wspomnieliśmy o instrukcji switch, która jest warunkiem wielokrotnego wyboru.
Oto krótki przykład:
switch coinflip() {
case "awers":
heads++
case "rewers":
tails++
default:
fmt.Println("Wylądowała na krawędzi!")
}
Wynik wywołania funkcji coinflip (rzut monetą) jest porównany z wartościami każdej instrukcji
case, czyli każdego przypadku. Przypadki są ewaluowane w kolejności od góry do dołu, więc
wykonywany jest ten, który pierwszy zostanie dopasowany. Opcjonalny przypadek domyślny (in-
strukcja default) jest wykonywany, jeśli nie zostanie dopasowany żaden inny przypadek. Instruk-
cja default może być umieszczona w dowolnym miejscu. Instrukcje case nie są wykonywane
po kolei, jedna po drugiej, jak w językach takich jak C (chociaż istnieje rzadko używana instrukcja
fallthrough, która umożliwia takie zachowanie).
Instrukcja switch nie potrzebuje operandu. Może to być tylko lista przypadków, z których każdy
jest wyrażeniem logicznym:
func Signum(x int) int {
switch {
case x > 0:
return +1
default:
return 0
case x < 0:
return -1
}
}
Ta forma jest nazywana niezadeklarowaną instrukcją switch (ang. tagless switch), co jest równo-
ważne z instrukcją switch true.
Podobnie jak instrukcje for oraz if, instrukcja switch może zawierać opcjonalną instrukcję
prostą (krótką deklarację zmiennych, instrukcję inkrementacji lub przypisania czy wywołanie
funkcji), która może być użyta do ustawienia wartości, zanim zostanie ona przetestowana.
Instrukcje break i continue modyfikują przepływ sterowania. Instrukcja break powoduje, że
sterowanie wznawia wykonywanie w następnej instrukcji po najbardziej zagnieżdżonej instrukcji
for, switch lub select (co zobaczymy później), a jak widziałeś w podrozdziale 1.3, instrukcja
continue powoduje, że najbardziej zagnieżdżona pętla for rozpoczyna swoją kolejną iterację.
Instrukcje mogą mieć etykiety, aby instrukcje break i continue mogły się do nich odwoływać,
np. aby za jednym razem wyjść z kilku zagnieżdżonych pętli lub rozpocząć kolejną iterację pętli
zewnętrznej. Istnieje nawet instrukcja goto, choć jest ona przeznaczona dla kodu generowanego
maszynowo, a nie do regularnego stosowania przez programistów.
Typy nazwane. Deklaracja type umożliwia nadanie nazwy istniejącemu typowi. Ponieważ struktury
są często typami long, prawie zawsze są nazwane. Znanym przykładem jest definicja typu Point
dla systemu grafiki 2D:
1.8. KILKA POMINIĘTYCH KWESTII 39

type Point struct {


X, Y int
}
var p Point
Deklaracje typów i typy nazwane zostaną opisane w rozdziale 2.
Wskaźniki. Język Go zapewnia wskaźniki, czyli wartości zawierające adres zmiennej. W niektórych
językach, zwłaszcza w C, wskaźniki są stosunkowo nieograniczone. W innych językach wskaź-
niki są ukryte jako „referencje” i nie można z nimi zbyt wiele zrobić poza ich przekazywaniem.
Go mieści się gdzieś pośrodku. Wskaźniki są bezpośrednio widoczne. Operator & daje adres
zmiennej, a operator * pobiera zmienną, do której odnosi się wskaźnik, ale nie istnieje arytmetyka
wskaźnika. Wskaźniki zostaną omówione w podrozdziale 2.3.2.
Metody i interfejsy. Metoda jest funkcją powiązaną z typem nazwanym. Język Go jest nietypowy
pod tym względem, że metody mogą być doczepiane do prawie każdego typu nazwanego. Metody
zostaną omówione w rozdziale 6. Interfejsy są typami abstrakcyjnymi pozwalającymi traktować
w ten sam sposób różne typy konkretne na podstawie posiadanych przez nie metod, a nie według
tego, jak są reprezentowane lub zaimplementowane. Interfejsy są przedmiotem rozdziału 7.
Pakiety. Go jest dostarczany z obszerną standardową biblioteką użytecznych pakietów, a społeczność
Go stworzyła i udostępniła ich jeszcze więcej. W programowaniu często chodzi raczej o użycie
istniejących pakietów niż o pisanie własnego oryginalnego kodu. W całej książce przedstawimy
kilkadziesiąt najważniejszych pakietów standardowych, ale jest ich o wiele więcej — niestety, nie
ma tu na nie miejsca i nie możemy zapewnić zdalnie czegoś takiego jak kompletne referencje dla
każdego pakietu.
Zanim rozpoczniesz pracę nad jakimkolwiek nowym programem, dobrym pomysłem jest spraw-
dzenie, czy istnieją już jakieś pakiety, które mogą ułatwić Ci wykonanie zadania. Indeks stan-
dardowych pakietów możesz znaleźć na stronie: https://golang.org/pkg, a listę pakietów przygoto-
wanych przez społeczność na stronie: https://godoc.org/. Dostęp do tych dokumentacji można
łatwo uzyskać z poziomu wiersza poleceń za pomocą narzędzia go doc:
$ go doc http.ListenAndServe
package http // import "net/http"

func ListenAndServe(addr string, handler Handler) error

ListenAndServe listens on the TCP network address addr and then


calls Serve with handler to handle requests on incoming connections.
...
Komentarze. Wspomnieliśmy już o komentarzach dokumentujących umieszczanych na początku
programu lub pakietu. W dobrym stylu jest także pisanie komentarzy przed deklaracją każdej
funkcji, aby określić jej zachowanie. Te konwencje są ważne, ponieważ są one wykorzystywane
przez narzędzia, takie jak go doc i godoc, w celu lokalizowania i wyświetlania dokumentacji
(zob. punkt 10.7.4).
Dla komentarzy, które obejmują wiele linii lub pojawiają się w ramach wyrażenia lub instrukcji,
dostępna jest również notacja /* ... */ znana z innych języków. Takie komentarze są czasem
stosowane na początku pliku dla dużego bloku tekstu objaśniającego, aby uniknąć używania po-
dwójnych ukośników (//) w każdej linii. Znaki // oraz /* wewnątrz komentarza nie mają spe-
cjalnego znaczenia, więc komentarze się nie zagnieżdżają.
40 ROZDZIAŁ 1. PRZEWODNIK
Rozdział 2

Struktura programu

W Go, tak jak w każdym innym języku programowania, duże programy tworzy się z niewielkiego
zbioru podstawowych konstrukcji. Zmienne przechowują wartości. Proste wyrażenia są łączone
w większe za pomocą operacji takich jak dodawanie i odejmowanie. Podstawowe typy są groma-
dzone w agregacjach takich jak tablice i struktury. Wyrażenia są używane w instrukcjach, których
kolejność wykonywania jest określana przez instrukcje sterowania przepływem takie jak if oraz for.
Instrukcje są grupowane w funkcje w celach wyizolowania i ponownego użycia. Funkcje są zbie-
rane w pliki źródłowe i pakiety.
Większość przykładów dotyczących tych zagadnień widziałeś w poprzednim rozdziale. W tym
rozdziale szczegółowo omówimy podstawowe elementy strukturalne programu Go. Przykładowe
programy są celowo uproszczone, tak abyśmy mogli się skupić na języku bez rozpraszania przez
skomplikowane algorytmy i struktury danych.

2.1. Nazwy
Nazwy funkcji, zmiennych, stałych, typów, etykiet instrukcji oraz pakietów stosują się do jednej
prostej reguły: nazwa zaczyna się od litery (czyli wszystkiego tego, co Unicode uznaje za literę)
lub podkreślenia i może zawierać dowolną liczbę dodatkowych liter, cyfr i znaków podkreślenia.
Wielkość liter ma znaczenie: heapSort i Heapsort to różne nazwy.
Język Go ma 25 słów kluczowych, takich jak np. if i switch, które mogą być stosowane tylko
wtedy, gdy pozwala na to składnia. Nie mogą być one wykorzystywane jako nazwy.
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
Ponadto istnieje niemal 40 predeklarowanych nazw, takich jak np. int oraz true, dla wbudowa-
nych stałych, typów i funkcji.
Stałe:
true false iota nil
42 ROZDZIAŁ 2. STRUKTURA PROGRAMU

Typy:
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
Funkcje:
make len cap new append copy close delete
complex real imag
panic recover
Te nazwy nie są zarezerwowane, więc można ich używać w deklaracjach. Zobaczymy kilka miejsc,
w których ponowne zadeklarowanie jednej z nich ma sens, ale należy uważać na możliwość
wprowadzenia zamieszania.
Jeśli encja jest zadeklarowana w ramach funkcji, jest dla niej lokalna. Jeśli jednak jest zadeklaro-
wana poza funkcją, jest widoczna we wszystkich plikach pakietu, do którego należy. Widoczność
encji poza granicami pakietu jest określona przez wielkość pierwszej litery jej nazwy. Jeśli nazwa
rozpoczyna się wielką literą, jest eksportowana, czyli jest widoczna i dostępna poza własnym pa-
kietem i mogą się do niej odwoływać inne części programu, tak jak w przypadku funkcji Printf
z pakietu fmt. Nazwy samych pakietów zaczynają się zawsze małą literą.
Nie istnieją ograniczenia dotyczące długości nazwy, ale konwencja i praktyka w programach Go
skłania się w stronę krótkich nazw, szczególnie dla zmiennych lokalnych z małymi zakresami.
Większe jest prawdopodobieństwo napotkania zmiennej i niż theLoopIndex. Zasadniczo im
większy zakres nazwy, tym powinna być ona dłuższa i bardziej znacząca.
Stylistycznie programiści Go przy formatowaniu nazw poprzez łączenie słów używają notacji
typu camelCase. Oznacza to, że wewnątrz nazw składających się z kilku słów preferowane są wiel-
kie litery zamiast podkreślników. Dlatego standardowe biblioteki mają funkcje o nazwach takich
jak QuoteRuneToASCII i parseRequestLine, ale nigdy quote_rune_to_ASCII lub parse_request_line.
Litery akronimów i skrótowców takich jak ASCII oraz HTML są zawsze zapisywane znakami
o takiej samej wielkości, więc funkcja może być nazwana: htmlEscape, HTMLEscape lub escapeHTML,
ale nigdy escapeHtml.

2.2. Deklaracje
Deklaracja nazywa encję programu i określa niektóre lub wszystkie jej właściwości. Istnieją
cztery główne rodzaje deklaracji: var, const, type oraz func. Zmienne i typy omówimy w tym
rozdziale, stałe w rozdziale 3., a funkcje w rozdziale 5.
Program Go jest przechowywany w jednym pliku lub w większej liczbie plików, których nazwy
kończą się na .go. Każdy plik rozpoczyna się od deklaracji package, wskazującej pakiet, którego
częścią jest dany plik. Po deklaracji package następują wszelkie deklaracje import, a następnie se-
kwencja deklaracji poziomu pakietu dla typów, zmiennych, stałych i funkcji, w dowolnej kolejności.
Przedstawiony poniżej program deklaruje np. stałą, funkcję i parę zmiennych:
code/r02/boiling
// Boiling wyświetla temperaturę wrzenia wody.
package main

import "fmt"
2.3. ZMIENNE 43

const boilingF = 212.0

func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("Temperatura wrzenia = %g°F lub %g°C\n", f, c)
// Output:
// temperatura wrzenia = 212°F lub 100°C
}
Stała boilingF jest deklaracją poziomu pakietu (tak jak main), natomiast zmienne f i c są lokalne
dla funkcji main. Nazwa każdej encji poziomu pakietu jest widoczna nie tylko w całym pliku
źródłowym zawierającym deklarację tej encji, ale również we wszystkich plikach danego pakietu.
Z drugiej strony, deklaracje lokalne są widoczne tylko w obrębie funkcji, w której zostały zade-
klarowane, i prawdopodobnie tylko w obrębie jej małej części.
Deklaracja funkcji ma nazwę, listę parametrów (zmiennych, których wartości są dostarczane
przez podmioty wywołujące tę funkcję), opcjonalną listę wyników oraz ciało funkcji, zawierające
instrukcje definiujące sposób działania funkcji. Lista wyników jest pomijana, jeśli funkcja niczego
nie zwraca. Wykonywanie funkcji zaczyna się od pierwszej instrukcji i trwa do momentu napotka-
nia instrukcji return lub osiągnięcia końca funkcji, która nie ma wyników. Sterowanie i ewentualne
wyniki są następnie zwracane podmiotowi wywołującemu.
Widzieliśmy już sporo funkcji i zobaczymy jeszcze o wiele więcej (włączając w to obszerne
omówienie w rozdziale 5.), więc jest to tylko szkic. Przedstawiona poniżej funkcja fToC hermety-
zuje logikę konwersji temperatury w taki sposób, że jest ona definiowana tylko raz, ale może być
używana w wielu miejscach. W tym przypadku main wywołuje ją dwa razy, używając wartości
dwóch różnych stałych lokalnych:
code/r02/ftoc
// Ftoc wyświetla wyniki dwóch konwersji ze stopni Fahrenheita na Celsjusza.
package main
import "fmt"
func main() {
const freezingF, boilingF = 32.0, 212.0
fmt.Printf("%g°F = %g°C\n", freezingF, fToC(freezingF)) // "32°F = 0°C"
fmt.Printf("%g°F = %g°C\n", boilingF, fToC(boilingF)) // "212°F = 100°C"
}
func fToC(f float64) float64 {
return (f - 32) * 5 / 9
}

2.3. Zmienne
Deklaracja var tworzy zmienną konkretnego typu, dołącza do niej nazwę i ustawia jej wartość
początkową. Każda deklaracja ma następującą postać ogólną:
var nazwa typ = wyrażenie
Pominięta może zostać część typ lub = wyrażenie, ale nigdy oba te elementy jednocześnie. Jeśli
pominięty zostanie typ, jest on określany przez wyrażenie inicjatora. Jeśli pominięte zostanie wy-
rażenie, wartością początkową jest wartość zerowa dla danego typu, którą jest 0 dla liczb, false
dla wartości logicznych, "" dla łańcuchów znaków oraz nil dla interfejsów i typów referencyjnych
44 ROZDZIAŁ 2. STRUKTURA PROGRAMU

(wycinka, wskaźnika, mapy, kanału i funkcji). Wartość zerowa typu złożonego, takiego jak tablica
lub struktura, ma wartość zerową wszystkich swoich elementów lub pól.
Mechanizm wartości zerowej zapewnia, że zmienna zawsze będzie przechowywała poprawną
wartość swojego typu. W języku Go nie ma czegoś takiego jak niezainicjowana zmienna.
Upraszcza to kod i często zapewnia rozsądne zachowanie warunków brzegowych bez dodatkowej
pracy. Dla przykładu: kod
var s string
fmt.Println(s) // ""
wyświetla pusty łańcuch znaków, zamiast powodować jakieś błędy lub nieprzewidywalne za-
chowania. Programiści Go często wkładają nieco wysiłku, by wartość zerowa bardziej skompliko-
wanego typu miała pewne znaczenie, aby zmienne zaczynały życie w użytecznym stanie.
Możliwe jest zadeklarowanie i opcjonalnie zainicjowanie zestawu zmiennych w jednej deklaracji
z odpowiadającą im listą wyrażeń. Pominięcie typu umożliwia deklarację wielu zmiennych różnych
typów:
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "cztery" // bool, float64, string
Inicjatory mogą być wartościami literalnymi lub dowolnymi wyrażeniami. Zmienne poziomu
pakietu są inicjowane przed rozpoczęciem wykonywania funkcji main (zob. punkt 2.6.2), a zmienne
lokalne są inicjowane, gdy ich deklaracje zostają napotkane podczas wykonywania funkcji.
Zestaw zmiennych może być również zainicjowany przez wywołanie funkcji, która zwraca wiele
wartości:
var f, err = os.Open(name) // os.Open zwraca plik i błąd

2.3.1. Krótkie deklaracje zmiennych


W obrębie funkcji do deklarowania i inicjowania zmiennych lokalnych można użyć alternatywnej
formy zwanej krótką deklaracją zmiennych. Przyjmuje ona postać nazwa := wyrażenie, a typ ele-
mentu nazwa jest określany przez typ elementu wyrażenie. Oto trzy z wielu krótkich deklaracji
zmiennych w funkcji lissajous (zob. podrozdział 1.4):
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
Ze względu na ich zwięzłość i elastyczność krótkie deklaracje zmiennych są używane do deklaro-
wania i inicjowania większości zmiennych lokalnych. Deklaracja var z reguły jest zarezerwowana
dla zmiennych lokalnych wymagających wyraźnego typu, który różni się od typu wyrażenia ini-
cjatora, lub dla przypadku, gdy wartość zmiennej zostanie przypisana później, a jej wartość począt-
kowa jest nieistotna.
i := 100 // int
var boiling float64 = 100 // float64
var names []string
var err error
var p Point
Podobnie jak w przypadku deklaracji var, zestaw wielu zmiennych może być deklarowany i ini-
cjowany w tej samej krótkiej deklaracji zmiennych:
2.3. ZMIENNE 45

i, j := 0, 1
Jednak deklaracje z wieloma wyrażeniami inicjatora powinny być stosowane tylko wtedy, gdy
pomagają zwiększyć czytelność, np. dla krótkich i naturalnych grupowań, takich jak inicjowanie
części pętli for.
Należy pamiętać, że := jest deklaracją, natomiast = jest przypisaniem. Deklaracji wielu zmien-
nych nie należy mylić z przypisaniem krotki (zob. punkt 2.4.l), w którym każda zmienna po lewej
stronie jest przypisywana do odpowiedniej wartości z prawej strony:
i, j = j, i // zamiana wartości zmiennych i oraz j
Podobnie jak zwykłe deklaracje var, krótkie deklaracje zmiennych mogą być wykorzystywane
do wywołań funkcji, takich jak np. os.Open, które zwracają dwie lub więcej wartości:
f, err := os.Open(name)
if err != nil {
return err
}
// …użycie f…
f.Close()
Należy zwrócić uwagę na jedną subtelną, ale istotną kwestię: krótka deklaracja zmiennych nie-
koniecznie deklaruje wszystkie zmienne umieszczone po lewej stronie. Jeśli niektóre z nich zostały
już zadeklarowane w tym samym bloku leksykalnym (zob. punkt 2.7), wtedy dla tych zmiennych
krótka deklaracja zmiennych działa jak przypisanie.
W poniższym kodzie pierwsza instrukcja deklaruje obie zmienne: in oraz err. Druga instrukcja
deklaruje zmienną out, ale tylko przypisuje wartość do istniejącej zmiennej err.
in, err := os.Open(infile)
// …
out, err := os.Create(outfile)
Krótka deklaracja zmiennych musi jednak zadeklarować co najmniej jedną nową zmienną, więc
ten kod nie będzie się kompilował:
f, err := os.Open(infile)
// …
f, err := os.Create(outfile) // błąd kompilacji: brak nowych zmiennych
Można to poprawić poprzez użycie dla drugiej instrukcji zwykłego przypisania.
Krótka deklaracja zmiennych działa jak przypisanie tylko dla zmiennych, które zostały już zade-
klarowane w tym samym bloku leksykalnym. Deklaracje w bloku zewnętrznym są ignorowane.
Przykład tego zostanie przedstawiony na końcu rozdziału.

2.3.2. Wskaźniki
Zmienna jest fragmentem pamięci zawierającym wartość. Zmienne tworzone przez deklaracje
są identyfikowane po nazwie, takiej jak x, ale wiele zmiennych jest identyfikowanych tylko poprzez
wyrażenia takie jak x[i] lub x.f. Wszystkie te wyrażenia odczytują wartość zmiennej, z wyjątkiem
przypadku, gdy pojawiają się po lewej stronie przypisania — wtedy do zmiennej jest przypisywana
nowa wartość.
Wartością wskaźnika jest adres zmiennej. Wskaźnik jest więc lokalizacją, w której przechowywana
jest pewna wartość. Nie każda wartość ma adres, ale ma go każda zmienna. Za pomocą wskaźnika
46 ROZDZIAŁ 2. STRUKTURA PROGRAMU

możemy przeczytać lub zaktualizować wartość zmiennej pośrednio, bez użycia lub nawet zna-
jomości nazwy danej zmiennej, jeśli ta zmienna w ogóle ma nazwę.
Jeśli zmienna jest zadeklarowana za pomocą instrukcji var x int, wyrażenie &x („adres zmiennej x”)
daje wskaźnik do zmiennej będącej liczbą całkowitą, czyli wartość typu *int, co jest wymawiane
jako „wskaźnik do int”. Jeśli ta wartość nazywa się p, mówimy, że „p wskazuje na x” lub równo-
ważnie „p zawiera adres x”. Zmienna, na którą wskazuje p, jest zapisywana jako *p. Wyrażenie
*p daje wartość tej zmiennej (liczbę całkowitą), ponieważ jednak *p oznacza zmienną, może rów-
nież pojawić się po lewej stronie przypisania — w takim przypadku to przypisanie aktualizuje
zmienną.
x := 1
p := &x // wskaźnik p typu *int wskazuje na x
fmt.Println(*p) // "1"
*p = 2 // równoważne z x = 2
fmt.Println(x) // "2"
Każdy komponent zmiennej typu złożonego (pole struktury lub element tablicy) jest również zmien-
ną, a tym samym także ma adres.
Zmienne są czasami określane jako wartości adresowalne. Wyrażenia oznaczające zmienne są
jedynymi wyrażeniami, do których może być stosowany operator adresu: &.
Wartością zerową wskaźnika jakiegokolwiek typu jest nil. Test p != nil jest prawdziwy, jeśli p
wskazuje na zmienną. Wskaźniki można porównywać. Dwa wskaźniki są równe wtedy i tylko
wtedy, jeśli wskazują na tę samą zmienną lub jeśli oba mają wartość nil.
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
Zwracanie adresu zmiennej lokalnej jest całkowicie bezpieczne dla funkcji. Przykładowo: w poniż-
szym kodzie zmienna lokalna v utworzona przez to konkretne wywołanie funkcji f będzie nadal
istnieć nawet po zwróceniu wywołania, a wskaźnik p nadal będzie się do niej odwoływał:
var p = f()

func f() *int {


v := 1
return &v
}
Każde wywołanie funkcji f zwraca odrębną wartość:
fmt.Println(f() == f()) // "false"
Ponieważ wskaźnik zawiera adres zmiennej, przekazywanie wskaźnika jako argumentu do funkcji
umożliwia aktualizowanie przez tę funkcję zmiennej, która została już przekazana pośrednio.
Poniższa funkcja np. inkrementuje zmienną, na którą wskazuje jej argument, i zwraca nową war-
tość zmiennej, więc może być ona użyta w wyrażeniu:
func incr(p *int) int {
*p++ // inkrementuje to, na co wskazuje p; nie zmienia p
return *p
}

v := 1
incr(&v) // efekt uboczny: v wynosi teraz 2
fmt.Println(incr(&v)) // "3" (i v wynosi 3)
2.3. ZMIENNE 47

Za każdym razem, gdy pobieramy adres zmiennej lub kopiujemy wskaźnik, tworzymy nowe
aliasy lub sposoby identyfikowania tej samej zmiennej, np. *p jest aliasem dla v. Tworzenie aliasów
za pomocą wskaźników jest użyteczne, ponieważ pozwala uzyskiwać dostęp do zmiennej bez
używania jej nazwy, ale jest to miecz obosieczny. Aby znaleźć wszystkie instrukcje, które uzyskują
dostęp do zmiennej, musimy znać wszystkie jej aliasy. Nie tylko wskaźniki tworzą aliasy. Ma to
również miejsce, gdy kopiujemy wartości innych typów referencyjnych, takich jak wycinki, mapy
i kanały, a nawet struktury, tablice i interfejsy zawierające te typy.
Wskaźniki są kluczem do pakietu flag, który używa argumentów wiersza poleceń programu,
aby ustawić wartości określonych zmiennych rozproszonych po całym programie. Aby to zilu-
strować, wykorzystamy wariację wcześniejszego polecenia echo przyjmującego dwie opcjonalne
flagi: -n powoduje pominięcie przez echo końcowego znaku nowej linii, który normalnie byłby
wyświetlany, a -s sep powoduje rozdzielenie argumentów wyjściowych zawartością łańcucha
sep zamiast domyślną pojedynczą spacją. Ponieważ to jest nasza czwarta wersja, pakiet nazywa się
code/r02/echo4.
code/r02/echo4
// Echo4 wyświetla swoje argumenty wiersza poleceń.
package main

import (
"flag"
"fmt"
"strings"
)

var n = flag.Bool("n", false, "pominięcie na końcu znaku nowej linii")


var sep = flag.String("s", " ", "separator")

func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
Funkcja flag.Bool tworzy nową zmienną flagi typu bool. Przyjmuje ona trzy argumenty: nazwę
flagi ("n"), domyślną wartość zmiennej (false) oraz komunikat, który będzie wyświetlany, jeśli
użytkownik dostarczy nieprawidłowy argument, nieprawidłową flagę lub wpisze -h albo -help.
Podobnie funkcja flag.String przyjmuje nazwę, wartość domyślną oraz komunikat i tworzy
zmienną string. Zmienne sep i n są wskaźnikami do zmiennych flagi, do których dostęp musi być
uzyskiwany pośrednio jako *sep i *n.
Gdy program zostanie uruchomiony, musi wywołać funkcję flag.Parse, zanim flagi zostaną
użyte. Ma to na celu aktualizację zmiennych flag z ich wartości domyślnych. Niebędące flagami
argumenty są dostępne przy użyciu funkcji flag.Args() jako wycinki łańcuchów znaków. Jeśli
funkcja flag.Parse napotyka błąd, wyświetla komunikat informujący o sposobie stosowania
polecenia i wywołuje funkcję os.Exit(2), aby zakończyć program.
Uruchommy kilka przypadków testowych dla programu echo:
$ go build code/r02/echo4
$ ./echo4 a bc def
a bc def
48 ROZDZIAŁ 2. STRUKTURA PROGRAMU

$ ./echo4 -s ! a bc def
a!bc!def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:
-n pominięcie na końcu znaku nowej linii
-s string
separator (default " ")

2.3.3. Funkcja new


Innym sposobem tworzenia zmiennych jest użycie wbudowanej funkcji new. Wyrażenie new(T)
tworzy nienazwaną zmienną typu T, inicjuje ją do wartości zerowej dla T i zwraca jej adres, który
jest wartością typu *T.
p := new(int) // p typu *int wskazuje na nienazwaną zmienną int
fmt.Println(*p) // "0"
*p = 2 // ustawia dla nienazwanej zmiennej int wartość 2
fmt.Println(*p) // "2"
Zmienna tworzona za pomocą funkcji new nie różni się od zwykłej zmiennej lokalnej, której adres
jest przyjmowany, poza tym że nie musimy wymyślać (i deklarować) jakiejś sztucznej nazwy i mo-
żemy użyć new(T) w wyrażeniu. Funkcja new jest więc tylko wygodą składniową, a nie fundamen-
talnym pojęciem: poniższe dwie funkcje newInt mają identyczne zachowania.
func newInt() *int { func newInt() *int {
return new(int) var dummy int
} return &dummy
}
Każde wywołanie funkcji new zwraca odrębną zmienną z unikatowym adresem:
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
Istnieje jeden wyjątek od tej reguły: dwie zmienne (takie jak struct{} lub [0]int), których typ
nie zawiera żadnej informacji i w związku z tym ma zerowy rozmiar, mogą w zależności od im-
plementacji mieć ten sam adres.
Funkcja new jest stosunkowo rzadko stosowana, ponieważ najbardziej typowe zmienne nienazwane
są typami struct, dla których bardziej elastyczna jest składnia literału struktury (zob. punkt
4.4.1).
Ponieważ new jest predeklarowaną funkcją, a nie słowem kluczowym, możliwe jest ponowne
zdefiniowanie tej nazwy dla czegoś innego w obrębie jakiejś funkcji, np.:
func delta(old, new int) int { return new - old }
Oczywiście w obrębie funkcji delta wbudowana funkcja new jest niedostępna.

2.3.4. Czas życia zmiennych


Czas życia zmiennej jest przedziałem czasu, w którym zmienna istnieje w trakcie wykonywania
programu. Czasem życia zmiennej poziomu pakietu jest całe wykonywanie programu. Natomiast
zmienne lokalne mają dynamiczne żywotności: nowa instancja jest tworzona za każdym razem,
2.3. ZMIENNE 49

gdy wykonywana jest instrukcja deklaracji, a zmienna żyje, dopóki nie stanie się nieosiągalna —
na tym etapie może zostać odzyskana pamięć. Parametry funkcji i wyniki również są zmiennymi
lokalnymi. Są one tworzone za każdym razem, gdy wywoływana jest zawierająca je funkcja.
W zamieszczonym poniżej fragmencie kodu z programu Lissajous z podrozdziału 1.4 zmienna t
jest np. tworzona za każdym razem, gdy rozpoczyna się pętla for, a nowe zmienne x i y są two-
rzone przy każdej iteracji tej pętli.
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex)
}
Skąd mechanizm odzyskiwania pamięci ma wiedzieć, że można odzyskać pamięć przechowującą
jakąś zmienną? Pełny opis tej kwestii jest zbyt szczegółowy na nasze potrzeby, ale podstawowa
idea jest taka, że każda zmienna poziomu pakietu i każda zmienna lokalna każdej aktywnej funkcji
może potencjalnie być początkiem lub korzeniem ścieżki do wspomnianej w pytaniu zmiennej,
śledząc wskaźniki i inne rodzaje referencji ostatecznie prowadzących do tej zmiennej. Jeśli taka
ścieżka nie istnieje, zmienna stała się nieosiągalna, więc nie może już wpływać na resztę obliczeń.
Ponieważ czas życia zmiennej jest określany wyłącznie przez to, czy jest ona osiągalna, zmienna
lokalna może żyć dłużej niż trwa jedna iteracja zawierającej ją pętli. Może istnieć dalej nawet po
zwróceniu wartości przez zawierającą ją funkcję.
Kompilator może alokować (lub nie) zmienne lokalne w stercie lub stosie, ale (co może nieco dzi-
wić) wybór ten nie zależy od tego, czy do zadeklarowania danej zmiennej użyto słowa kluczowego
var, czy funkcji new.
var global *int

func f() { func g() {


var x int y := new(int)
x = 1 *y = 1
global = &x }
}
Tutaj zmienna x musi być alokowana w stercie, ponieważ jest nadal osiągalna ze zmiennej global po
powrocie z funkcji f, mimo że jest zadeklarowana jako zmienna lokalna. Mówimy, że x ucieka z f.
Natomiast po powrocie z funkcji g zmienna *y staje się nieosiągalna i może być poddana odzyski-
waniu pamięci. Ponieważ *y nie ucieka z g, kompilator może bezpiecznie alokować *y w stosie,
chociaż została alokowana za pomocą funkcji new. W każdym z przypadków pojęcie uciekania nie
jest czymś, o co trzeba się martwić, jeśli chodzi o kwestię napisania poprawnego kodu. Dobrze
jest jednak o tym pamiętać podczas przeprowadzania optymalizacji, ponieważ każda uciekająca
zmienna wymaga dodatkowego przydziału pamięci.
Mechanizm odzyskiwania pamięci jest ogromną pomocą w pisaniu poprawnych programów, ale
nie zwalnia Cię z obowiązku myślenia o pamięci. Nie trzeba bezpośrednio alokować i uwalniać
pamięci, ale żeby pisać efektywne programy, nadal trzeba mieć świadomość czasu życia zmiennych.
Utrzymywanie np. niepotrzebnych wskaźników do obiektów krótko żyjących w obrębie obiektów
długo żyjących, zwłaszcza zmiennych globalnych, uniemożliwi mechanizmowi odzyskiwania
pamięci wyczyszczenie obiektów krótko żyjących.
50 ROZDZIAŁ 2. STRUKTURA PROGRAMU

2.4. Przypisania
Wartość przechowywana przez zmienną jest aktualizowana za pomocą instrukcji przypisania,
która w swojej najprostszej postaci ma zmienną na lewo od znaku = i wyrażenie po prawej stronie.
x = 1 // zmienna nazwana
*p = true // zmienna pośrednia
person.name = "bob" // pole struktury
count[x] = count[x] * scale // tablica, wycinek lub element mapy
Każdy z operatorów arytmetycznych i bitowych binarnych ma odpowiadający mu operator przypi-
sania, dzięki czemu można np. zapisać tę ostatnią instrukcję jako count[x] *= scale. Nie musimy
wtedy powtarzać (i ponownie ewaluować) wyrażenia dla zmiennej.
Zmienne liczbowe mogą być również inkrementowane i dekrementowane za pomocą instrukcji ++ i --:
v := 1
v++ // to samo co v = v + 1; v uzyskuje wartość 2
v-- // to samo co v = v – 1; v ponownie ma wartość 1

2.4.1. Przypisanie krotki


Kolejna forma przypisania, znana jako przypisanie krotki (ang. tuple assignment), umożliwia
przypisanie jednocześnie kilku zmiennych. Wszystkie wyrażenia znajdujące się po prawej stronie
są ewaluowane, zanim którakolwiek ze zmiennych zostanie zaktualizowana. Dzięki temu ta
forma jest najbardziej użyteczna, gdy niektóre zmienne pojawiają się po obu stronach przypisa-
nia, co ma miejsce np. przy zamianie wartości dwóch zmiennych:
x, y = y, x
a[i], a[j] = a[j], a[i]
lub przy obliczaniu największego wspólnego dzielnika (ang. greatest common divisor — GCD)
dwóch liczb całkowitych:
func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
lub podczas obliczania iteracyjnie n-tego wyrazu ciągu Fibonacciego:
func fib(n int) int {
x, y := 0, 1
for i := 0; i < n; i++ {
x, y = y, x+y
}
return x
}
Przypisanie krotki może również uczynić sekwencję banalnych przypisań bardziej zwartą:
i, j, k = 2, 3, 5
Jeśli jednak chodzi o styl, należy unikać formy krotki, gdy wyrażenia są złożone. Sekwencja
osobnych instrukcji jest łatwiejsza do odczytania.
2.4. PRZYPISANIA 51

Niektóre wyrażenia, takie jak wywołanie funkcji z wieloma wynikami, produkują kilka wartości.
Gdy takie wywołanie jest stosowane w instrukcji przypisania, lewa strona musi mieć tyle zmien-
nych, ile funkcja ma wyników.
f, err = os.Open("foo.txt") // wywołanie funkcji zwraca dwie wartości
Często funkcje wykorzystują dodatkowe wyniki, aby wskazać jakiś błąd poprzez zwrócenie
error (tak jak w wywołaniu funkcji os.Open) lub typu bool, zwykle o nazwie ok. Jak zobaczymy
w kolejnych rozdziałach, istnieją trzy operatory, które czasem również zachowują się w ten sposób.
Jeśli w przypisaniu, w którym oczekiwane są dwa wyniki, pojawiają się: przeszukiwanie mapy
(zob. podrozdział 4.3), asercja typów (zob. podrozdział 7.10) lub odbieranie z kanału (zob.
punkt 8.4.2), każde z przypisań daje dodatkowy wynik logiczny:
v, ok = m[key] // przeszukiwanie mapy
v, ok = x.(T) // asercja typów
v, ok = <-ch // odbieranie z kanału
Podobnie jak w przypadku deklaracji zmiennych, możemy przypisać niechciane wartości do pu-
stego identyfikatora:
_, err = io.Copy(dst, src) // porzuca licznik bajtów
_, ok = x.(T) // sprawdza typ, ale porzuca wynik

2.4.2. Przypisywalność
Instrukcje przypisania są bezpośrednią formą przypisania, ale w programie jest wiele miejsc,
w których przypisanie pojawia się w sposób pośredni (ang. implicitly): wywołanie funkcji pośrednio
przypisuje wartości argumentów do odpowiednich zmiennych parametrów, instrukcja return
pośrednio przypisuje operandy return do odpowiednich zmiennych wynikowych, a wyrażenie
literalne typu złożonego (zob. podrozdział 4.2), takie jak ten wycinek:
medals := []string{"złoto", "srebro", "brąz"}
pośrednio przypisuje każdy element, jakby zostało to zapisane tak:
medals[0] = "złoto"
medals[1] = "srebro"
medals[2] = "brąz"
Elementy map i kanałów również podlegają podobnym przypisaniom pośrednim, chociaż nie są
zwykłymi zmiennymi.
Przypisanie, bezpośrednie czy pośrednie, zawsze jest prawidłowe, jeśli lewa strona (zmienna) i prawa
strona (wartość) mają ten sam typ. Mówiąc bardziej ogólnie: przypisanie jest prawidłowe tylko
wtedy, gdy wartość jest przypisywalna do typu zmiennej.
Reguły związane z przypisywalnością mają różne wersje dla różnych typów, więc będziemy obja-
śniać odpowiednie przypadki w trakcie wprowadzania każdego nowego typu. Dla typów omówionych
do tej pory reguły są proste: typy muszą być dokładnie takie same, a wartość nil może być przypi-
sana do dowolnej zmiennej interfejsu lub typu referencyjnego. Stałe (zob. podrozdział 3.6) mają bar-
dziej elastyczne reguły przypisywalności, aby uniknąć potrzeby najbardziej bezpośrednich kon-
wersji.
Możliwość porównywania dwóch wartości za pomocą operatorów == oraz != jest związana
z przypisywalnością: w każdym porównaniu pierwszy operand musi być przypisywalny do typu
52 ROZDZIAŁ 2. STRUKTURA PROGRAMU

drugiego operandu i vice versa. Tak jak dla przypisywalności, będziemy omawiać odpowiednie
przypadki porównywalności podczas prezentowania kolejnych nowych typów.

2.5. Deklaracje typów


Typ zmiennej lub wyrażenia definiuje charakterystyki wartości, jaką zmienna lub wyrażenie
mogą przyjmować, takie jak: rozmiar (np. liczba bitów lub liczba elementów), sposób wewnętrznej
reprezentacji, wewnętrzne operacje, które można na nich przeprowadzać, oraz powiązane z nimi
metody.
W każdym programie istnieją zmienne, które dzielą tę samą reprezentację, ale wyrażają bardzo
różne koncepcje. Typ int może być np. używany do reprezentowania: indeksu pętli, znacznika
czasu, deskryptora pliku lub miesiąca. Typ float64 może reprezentować prędkość w metrach na
sekundę lub temperaturę w jednej z kilku skal, a typ string może reprezentować hasło lub nazwę
koloru.
Deklaracja type definiuje nowy typ nazwany, który ma ten sam typ bazowy co istniejący typ.
Typ nazwany zapewnia sposób rozdzielenia różnych i być może niekompatybilnych zastosowań
typu bazowego, aby nie można ich było wymieszać w sposób niezamierzony.
type nazwa typ_bazowy
Deklaracje typów najczęściej pojawiają się na poziomie pakietu, gdzie typ nazwany jest widoczny
w całym pakiecie, a jeśli nazwa jest eksportowana (zaczyna się od wielkiej litery), jest również dostęp-
na z innych pakietów.
Aby zilustrować deklaracje typów, przekonwertujmy różne skale temperatury na różne typy:
code/r02/tempconv0
// Package tempconv wykonuje przeliczenia temperatur w skalach Celsjusza i Fahrenheita.
package tempconv

import "fmt"

type Celsius float64


type Fahrenheit float64

const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }


Ten pakiet definiuje dwa typy: Celsius i Fahrenheit, dla dwóch jednostek temperatury. Chociaż
oba te typy mają ten sam typ bazowy (float64), to nie są tego samego typu, więc nie mogą być
porównywane lub łączone w wyrażeniach arytmetycznych. Rozróżnianie typów umożliwia unika-
nie błędów takich jak przypadkowe łączenie temperatur w dwóch różnych skalach. Bezpośrednia
konwersja typów, taka jak Celsius(t) lub Fahrenheit(t), jest wymagana do przekonwertowania
z float64. Celsius(t) i Fahrenheit(t) to konwersje, a nie wywołania funkcji. Nie zmieniają
w żaden sposób wartości lub reprezentacji, ale dzięki nim zmiana znaczenia jest jawna. Z drugiej
2.5. DEKLARACJE TYPÓW 53

strony, funkcje CToF i FToC dokonują konwersji między dwoma skalami. Te funkcje zwracają
różne wartości.
Dla każdego typu T istnieje odpowiadająca mu operacja konwersji T(x), która konwertuje wartość x
na typ T. Konwersja z jednego typu na drugi jest dozwolona, jeśli oba typy mają ten sam typ bazowy
lub jeśli oba są nienazwanymi typami wskaźnika, które wskazują na zmienne tego samego typu
bazowego. Te konwersje zmieniają typ, ale nie reprezentację wartości. Jeśli x jest przypisywalne do
T, konwersja jest dozwolona, ale zazwyczaj zbędna.
Dozwolone są również konwersje między typami liczbowymi oraz między łańcuchem znaków
a niektórymi typami wycinków, jak zobaczymy w następnym rozdziale. Te konwersje mogą zmie-
nić reprezentację wartości. Przykładowo: konwersja liczby zmiennoprzecinkowej na liczbę całko-
witą porzuca część ułamkową, a konwersja łańcucha znaków na wycinek []byte alokuje kopię
danych łańcucha znaków. W żadnym z tych przypadków konwersja nigdy nie zawodzi w czasie
wykonywania programu.
Typ bazowy typu nazwanego określa jego strukturę i reprezentację, a także zestaw obsługiwanych
operacji wewnętrznych, które są takie same jak przy bezpośrednim stosowaniu typu bazowego.
Oznacza to, że operatory arytmetyczne działają tak samo dla typów Celsius i Fahrenheit jak
dla float64, czego można się było spodziewać.
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC) // błąd kompilacji: niezgodność typów
Operatory porównania, takie jak == oraz <, mogą być również wykorzystywane do porównywa-
nia wartości typu nazwanego z inną wartością tego samego typu lub z wartością typu bazowego.
Ale dwie wartości różnych typów nazwanych nie mogą być porównywane bezpośrednio:
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // błąd kompilacji: niezgodność typów
fmt.Println(c == Celsius(f)) // "true"!
Przyjrzyj się uważnie ostatniemu przypadkowi. Mimo swojej nazwy konwersja typu Celsius(f)
nie zmienia wartości argumentu, tylko jego typ. Test jest prawdą, ponieważ c i f wynoszą zero.
Typ nazwany może zapewnić wygodę w notacji, jeśli pomaga uniknąć ciągłego zapisywania od
nowa typów złożonych. Jeśli typ bazowy jest prosty (tak jak float64), korzyść jest niewielka, ale
może być duża dla skomplikowanych typów, jak zobaczymy podczas omawiania typów struct,
czyli struktur.
Typy nazwane pozwalają także definiować nowe zachowania dla wartości danego typu. Te za-
chowania są wyrażane jako zestaw funkcji związanych z tym typem, zwany metodami typu.
Metodom przyjrzymy się szczegółowo w rozdziale 6., ale damy przedsmak tego mechanizmu tutaj.
Poniższa deklaracja, w której parametr c typu Celsius pojawia się przed nazwą funkcji, wiąże
z typem Celsius metodę o nazwie String, zwracającą wartość liczbową c wraz z symbolem °C:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
Wiele typów deklaruje metodę String w tej formie, ponieważ kontroluje ona sposób prezento-
wania wartości danego typu, gdy są one wyświetlane jako łańcuch znaków przez pakiet fmt, jak
zobaczymy w podrozdziale 7.1.
54 ROZDZIAŁ 2. STRUKTURA PROGRAMU

c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; nie ma potrzeby wywoływania metody String bezpośrednio
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; nie wywołuje metody String
fmt.Println(float64(c)) // "100"; nie wywołuje metody String

2.6. Pakiety i pliki


Pakiety w języku Go służą tym samym celom co biblioteki lub moduły w innych językach, ob-
sługując modułowość, hermetyzację, oddzielną kompilację i ponowne wykorzystywanie. Kod źró-
dłowy pakietu znajduje się w co najmniej jednym pliku .go, zwykle w katalogu, którego nazwa
kończy się ścieżką importu, np. pliki pakietu code/r01/helloworld są przechowywane w katalogu
$GOPATH/src/code/r01/helloworld.
Każdy pakiet służy jako odrębna przestrzeń nazw dla swoich deklaracji. Przykładowo: w obrębie
pakietu image identyfikator Decode odnosi się do innej funkcji niż ten sam identyfikator w pa-
kiecie unicode/utf16. Aby odwołać się do funkcji spoza pakietu, musimy dokonać kwalifikacji
identyfikatora, żeby wyraźnie określić, czy mamy na myśli image.Decode, czy utf16.Decode.
Pakiety pozwalają również ukrywać informacje poprzez kontrolowanie, które nazwy są widoczne
na zewnątrz pakietu, czyli eksportowane. W języku Go kwestią tego, które pakiety są eksporto-
wane, rządzi prosta zasada: eksportowane identyfikatory rozpoczynają się wielką literą.
Aby zilustrować podstawy, załóżmy, że nasze oprogramowanie do konwersji temperatury stało
się popularne i chcemy udostępnić je społeczności Go w postaci nowego pakietu. Jak to zrobić?
Utwórzmy pakiet o nazwie code/r02/tempconv, będący wariacją na temat poprzedniego przy-
kładu. (Tutaj zrobiliśmy wyjątek od naszej zwykłej zasady numeracji przykładów w kolejności,
tak aby ścieżka pakietu mogła być bardziej realistyczna). Sam pakiet jest przechowywany w dwóch
plikach, aby pokazać, w jaki sposób uzyskiwany jest dostęp do deklaracji w oddzielnych plikach
pakietu. W rzeczywistości tak mały pakiet wymagałby tylko jednego pliku.
Deklaracje typów, ich stałych oraz metod umieściliśmy w pliku tempconv.go:
code/r02/tempconv
// Package tempconv wykonuje konwersje skal Celsjusza i Fahrenheita.
package tempconv

import "fmt"

type Celsius float64


type Fahrenheit float64

const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }


func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
2.6. PAKIETY I PLIKI 55

Natomiast funkcje konwersji umieściliśmy w pliku conv.go:


package tempconv

// CToF konwertuje temperaturę w stopniach Celsjusza na stopnie Fahrenheita.


func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

// FToC konwertuje temperaturę w stopniach Fahrenheita na stopnie Celsjusza.


func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
Każdy plik zaczyna się od deklaracji package, która definiuje nazwę pakietu. Gdy pakiet jest im-
portowany, do jego elementów odwołujemy się jako tempconv.CToF itd. Nazwy poziomu pakietu,
takie jak typy i stałe zadeklarowane w jednym pliku pakietu, są widoczne dla wszystkich pozo-
stałych plików z tego pakietu, tak jakby kod źródłowy znajdował się w pojedynczym pliku. Należy
zauważyć, że plik tempconv.go importuje pakiet fmt, ale plik conv.go go nie importuje, ponieważ
nie używa niczego z tego pakietu.
Ponieważ nazwy stałych const poziomu pakietu zaczynają się wielkimi literami, są również do-
stępne poprzez kwalifikowane nazwy, takie jak tempconv.AbsoluteZeroC:
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! –273.15°C"
Aby przekonwertować temperaturę w stopniach Celsjusza na stopnie Fahrenheita w pakiecie,
który importuje code/r02/tempconv, możemy napisać następujący kod:
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
Komentarz dokumentujący (zob. punkt l0.7.4), bezpośrednio poprzedzający deklarację package,
dokumentuje pakiet jako całość. Tradycyjnie powinien się rozpoczynać podsumowującym zdaniem
w stylu zaprezentowanym w przykładzie. Tylko jeden plik w każdym pakiecie powinien zawierać
komentarz dokumentujący dany pakiet. Obszerne komentarze dokumentujące są często umieszcza-
ne w osobnym pliku, umownie zwanym doc.go.
Ćwiczenie 2.1. Dodaj do pakietu tempconv typy, stałe i funkcje do przetwarzania temperatury
w skali Kelvina, w której zero wynosi –273,15°C, a różnica 1 K jest równa różnicy 1°C.

2.6.1. Importy
W ramach programu Go każdy pakiet jest identyfikowany przez unikatowy łańcuch znaków na-
zywany jego ścieżką importu (ang. import path). Są to łańcuchy znaków pojawiające się w de-
klaracji import, takie jak "code/r02/tempconv". Specyfikacja języka nie definiuje, skąd biorą się
te łańcuchy lub co znaczą. Ich interpretacja jest zadaniem narzędzi. Gdy korzystamy z narzędzia
go (zob. rozdział 10.), ścieżka importu oznacza katalog zawierający jeden plik źródłowy Go lub
kilka takich plików, które razem składają się na pakiet.
Oprócz swojej ścieżki importu każdy pakiet ma nazwę pakietu, która jest krótką (niekoniecznie
unikatową) nazwą pojawiającą się w deklaracji package. Umownie nazwa pakietu odpowiada
ostatniemu segmentowi jej ścieżki importu, dzięki czemu łatwo jest przewidzieć, że nazwą pakietu
code/r02/tempconv jest tempconv.
Aby użyć pakietu code/r02/tempconv, trzeba go zaimportować:
code/r02/cf
// Cf konwertuje swój argument liczbowy na wartość w skalach Celsjusza i Fahrenheita.
package main
56 ROZDZIAŁ 2. STRUKTURA PROGRAMU

import (
"fmt"
"os"
"strconv"
"code/r02/tempconv"
)

func main() {
for _, arg := range os.Args[1:] {
t, err := strconv.ParseFloat(arg, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "cf: %v\n", err)
os.Exit(1)
}
f := tempconv.Fahrenheit(t)
c := tempconv.Celsius(t)
fmt.Printf("%s = %s, %s = %s\n",
f, tempconv.FToC(f), c, tempconv.CToF(c))
}
}
Deklaracja importu wiąże krótką nazwę z importowanym pakietem, dzięki czemu ta nazwa mo-
że być używana w całym pliku do odwoływania się do zawartości danego pakietu. Powyższa de-
klaracja import pozwala nam odwoływać się do nazw w pakiecie code/r02/tempconv za pomocą
identyfikatora kwalifikowanego, takiego jak tempconv.CToF. Domyślnie krótka nazwa jest nazwą
pakietu (w tym przypadku tempconv), ale deklaracja importu może określić alternatywną nazwę,
aby uniknąć konfliktu (zob. podrozdział l0.3).
Program cf konwertuje pojedynczy liczbowy argument wiersza poleceń na wartość w obu skalach
(Celsjusza i Fahrenheita):
$ go build code/r02/cf
$ ./cf 32
32°F = 0°C, 32°C = 89.6°F
$ ./cf 212
212°F = 100°C, 212°C = 413.6°F
$ ./cf -40
-40°F = -40°C, -40°C = -40°F
Błędem jest importowanie pakietu i nieodwoływanie się do niego. Kontrola importów pakietów po-
maga wyeliminować zależności, które stają się zbędne, gdy kod ewoluuje. Może to być jednak uciąż-
liwe podczas debugowania, ponieważ wykomentowanie linii kodu takiej jak log.Print("dotarliśmy
tutaj!") może usunąć jedyną referencję do nazwy pakietu log, co spowoduje wyemitowanie
błędu przez kompilator. W takiej sytuacji trzeba wykomentować lub usunąć niepotrzebną dekla-
rację import.
Lepszym rozwiązaniem może być użycie narzędzia golang.org/x/tools/cmd/goimports, które
w razie potrzeby automatycznie wstawia pakiety do deklaracji importu lub usuwa je z niej. Większość
edytorów można skonfigurować w taki sposób, aby narzędzie goimports było uruchamiane przy
każdym zapisywaniu pliku. Podobnie jak narzędzie gofmt, również wyświetla ono ładnie sfor-
matowane pliki źródłowe Go w postaci kanonicznej.
Ćwiczenie 2.2. Napisz program do konwersji jednostek ogólnego przeznaczenia, analogiczny do
cf, który odczytuje liczby z argumentów wiersza poleceń (lub ze standardowego wejścia, jeśli
nie ma argumentów) i konwertuje każdą liczbę na jednostki, takie jak: temperatura w skalach
Celsjusza i Fahrenheita, długość w stopach i metrach, waga w funtach i kilogramach itp.
2.6. PAKIETY I PLIKI 57

2.6.2. Inicjowanie pakietu


Inicjowanie pakietu rozpoczyna się od zainicjowania zmiennych poziomu pakietu w kolejności,
w jakiej zostały zadeklarowane, z tym wyjątkiem, że najpierw rozwiązywane są zależności:
var a = b + c // zmienna a zainicjowana jako trzecia do wartości 3
var b = f() // zmienna b zainicjowana jako druga do wartości 2 przez wywołanie funkcji f
var c = 1 // zmienna c zainicjowana jako pierwsza do wartości 1
func f() int { return c + 1 }
Jeśli pakiet ma wiele plików .go, są one inicjowane w kolejności przekazywania ich do kompilatora.
Narzędzie go sortuje pliki .go według nazwy przed wywołaniem kompilatora.
Każda zmienna zadeklarowana na poziomie pakietu rozpoczyna życie z wartością wyrażenia
swojego inicjatora, jeśli taka wartość istnieje. Jednak dla niektórych zmiennych, takich jak tablice,
wyrażenie inicjatora może nie być najprostszym sposobem ustawiania wartości początkowej.
W takim przypadku prostszy może być mechanizm funkcji init. Każdy plik może zawierać do-
wolną liczbę funkcji, których deklaracją jest po prostu:
func init() { /* ... */ }
Takie funkcje init mogą być wywoływane lub można się do nich odwoływać, ale poza tym są to
normalne funkcje. W ramach każdego pliku funkcje init są wykonywane automatycznie po
uruchomieniu programu, w kolejności, w jakiej zostały zadeklarowane.
Pakiety są inicjowane pojedynczo w kolejności importów w programie (zależności najpierw), więc
w przypadku pakietu p importującego pakiet q można być pewnym, że q będzie w pełni zaini-
cjowany przed rozpoczęciem inicjowania p. Proces inicjowania jest wykonywany z dołu do góry.
Pakiet main jest inicjowany jako ostatni. W ten sposób wszystkie pakiety są w pełni zainicjowane
przed rozpoczęciem funkcji main aplikacji.
Przedstawiony poniżej pakiet definiuje funkcję PopCount zwracającą liczbę ustawionych bitów
(czyli bitów o wartości 1) w wartości uint64, co nazywany liczebnością populacji (ang. population
count) tej wartości. Pakiet wykorzystuje funkcję init do wstępnego obliczenia tablicy wyników (pc)
dla każdej możliwej wartości 8-bitowej, aby funkcja PopCount nie musiała wykonywać 64 kroków, ale
mogła po prostu zwrócić sumę ośmiu wyszukiwań tablicy. (Z pewnością nie jest to najszybszy algo-
rytm do liczenia bitów, ale jest wygodny do zilustrowania funkcji init oraz pokazania sposobu
wstępnego obliczania wartości tablicy, co często bywa użyteczną techniką programowania).
code/r02/popcount
package popcount

// pc[i] jest liczebnością populacji i.


var pc [256]byte

func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}

// PopCount zwraca liczebność populacji (liczbę ustawionych bitów) dla x.


func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
58 ROZDZIAŁ 2. STRUKTURA PROGRAMU

pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}
Należy zwrócić uwagę, że pętla range w funkcji init używa tylko indeksu. Wartość jest niepo-
trzebna, więc nie musi być uwzględniana. Tę pętlę można również zapisać jako:
for i, _ := range pc {
Inne zastosowania funkcji init zobaczymy w następnym podrozdziale oraz w podrozdziale
10.5.
Ćwiczenie 2.3. Przepisz funkcję PopCount w taki sposób, aby używała pętli zamiast pojedyncze-
go wyrażenia. Porównaj wydajność obu wersji. (Sposób systematycznego porównywania wydaj-
ności różnych implementacji został omówiony w podrozdziale 11.4).
Ćwiczenie 2.4. Napisz wersję funkcji PopCount, która zlicza bity, przesuwając swój argument
przez 64 pozycje bitowe, testując za każdym razem najmniej znaczący bit. Porównaj jej wydajność
z wersją przeszukującą tablicę.
Ćwiczenie 2.5. Wyrażenie x&(x-1) czyści najmniej znaczący niezerowy bit wartości x. Napisz wer-
sję funkcji PopCount, która liczy bity, wykorzystując ten fakt, oraz oceń jej wydajność.

2.7. Zakres
Deklaracja dokonuje powiązania nazwy z encją programu, taką jak funkcja lub zmienna. Zakres
(ang. scope) deklaracji jest tą częścią kodu źródłowego, w której użycie zadeklarowanej nazwy
odwołuje się do tej deklaracji.
Nie należy mylić zakresu z czasem życia. Zakres deklaracji jest regionem tekstu programu. Jest
to właściwość momentu kompilacji. Czas życia zmiennej jest przedziałem czasu w trakcie wy-
konywania programu, w którym inne części programu mogą się odwoływać do tej zmiennej.
Jest to właściwość momentu wykonywania.
Blok składniowy (ang. syntatic block) jest sekwencją instrukcji ujętą w nawiasy klamrowe, takie
jak te otaczające ciało funkcji lub pętli. Nazwa zadeklarowana wewnątrz bloku składniowego nie
jest widoczna poza tym blokiem. Blok obejmuje swoje deklaracje i określa ich zakres. Możemy
uogólnić pojęcie bloku, aby obejmowało inne grupy deklaracji, które nie są bezpośrednio otoczone
klamrami w kodzie źródłowym. Nazwijmy takie bloki blokami leksykalnymi (ang. lexical blocks).
Istnieje blok leksykalny dla całego kodu źródłowego, zwany blokiem uniwersum (ang. universe
block). Obejmuje on każdy pakiet, każdy plik, każdą instrukcję for, if i switch, każdy przypa-
dek w instrukcjach switch lub select oraz oczywiście każdy wyraźnie wyodrębniony blok skła-
dniowy.
Blok leksykalny deklaracji określa jej zakres, który może być szeroki lub wąski. Deklaracje typów
wbudowanych, funkcji oraz stałych takich jak int, len i true znajdują się w bloku uniwersum
i można się do nich odwoływać w całym programie. Do deklaracji znajdujących się poza jakąkol-
wiek funkcją, czyli na poziomie pakietu, można się odwoływać z dowolnego pliku w tym samym
pakiecie. Pakiety importowane, takie jak pakiet fmt w przykładzie tempconv, są deklarowane na
poziomie pliku, więc można się do nich odwoływać z tego samego pliku, ale nie z innego pliku
w tym samym pakiecie, chyba że zastosujemy w nim kolejną deklarację import. Wiele deklaracji,
2.7. ZAKRES 59

takich jak deklaracja zmiennej c w funkcji tempconv.CToF, jest lokalnych, więc można się do
nich odwoływać tylko w obrębie tej samej funkcji lub nawet tylko w obrębie jej części.
Zakresem etykiet przepływu sterowania, takich jak te wykorzystywane przez instrukcje break,
continue i goto, jest cała obejmująca je funkcja.
Program może zawierać wiele deklaracji o tej samej nazwie, pod warunkiem że każda z tych dekla-
racji znajduje się w innym bloku leksykalnym. Można np. zadeklarować zmienną lokalną o tej samej
nazwie co zmienna poziomu pakietu. Można też, tak jak to pokazano w punkcie 2.3.3, zadeklaro-
wać parametr funkcji o nazwie new, chociaż funkcja o tej nazwie jest predeklarowana w bloku
uniwersum. Nie przesadzaj z tym jednak. Im większy zakres ponownej deklaracji, tym większe
prawdopodobieństwo zaskoczenia osoby czytającej kod.
Gdy kompilator napotka referencję do nazwy, szuka deklaracji, począwszy od najbardziej we-
wnętrznego, zamykającego bloku leksykalnego i przechodząc po kolei aż do bloku uniwersum.
Jeśli nie znajdzie deklaracji, zgłasza błąd „niezadeklarowana nazwa” (ang. undeclared name). Jeśli
nazwa jest zadeklarowana zarówno w bloku zewnętrznym, jak i wewnętrznym, wewnętrzna dekla-
racja zostanie znaleziona jako pierwsza. W takim przypadku mówi się, że wewnętrzna deklaracja
przesłania lub przykrywa deklarację zewnętrzną, czyniąc ją niedostępną:
func f() {}

var g = "g"

func main() {
f := "f"
fmt.Println(f) // "f"; lokalna deklaracja var f przykrywa deklarację func f poziomu pakietu
fmt.Println(g) // "g"; deklaracja var poziomu pakietu
fmt.Println(h) // błąd kompilacji: niezdefiniowane h
}
W obrębie funkcji bloki leksykalne mogą być zagnieżdżane na dowolną głębokość, więc jedna
deklaracja lokalna może przesłaniać inną. Większość bloków jest tworzona przez konstrukcje
przepływu sterowania takie jak instrukcje if i pętle for. Poniższy program ma trzy różne zmienne
o nazwie x, ponieważ każda deklaracja pojawia się w innym bloku leksykalnym. (Ten przykład
ilustruje reguły zakresu i nie reprezentuje dobrego stylu!).
func main() {
x := "witaj!"
for i := 0; i < len(x); i++ {
x := x[i]
if x != '!' {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "WITAJ" (jedna litera na każdą iterację)
}
}
}
Wyrażenia x[i] oraz x + 'A' - 'a' odnoszą się do deklaracji zmiennej x z bloku zewnętrznego.
Wyjaśnimy to za chwilę. (Należy zwrócić uwagę, że to ostatnie wyrażenie nie jest równoznaczne
z unicode.ToUpper).
Jak wspomniano powyżej, nie wszystkie bloki leksykalne odpowiadają wyraźnie ograniczonym
nawiasami klamrowymi sekwencjom instrukcji. Niektóre są jedynie dorozumiane. Pętla for w po-
wyższym przykładzie tworzy dwa bloki leksykalne: wyraźny blok dla pętli ciała i dorozumiany
blok, który dodatkowo obejmuje zmienne zadeklarowane przez klauzulę inicjowania, takie jak i.
60 ROZDZIAŁ 2. STRUKTURA PROGRAMU

Zakresem zmiennej zadeklarowanej w bloku dorozumianym są: warunek, instrukcja publikacji


(i++) oraz ciało instrukcji for.
Poniższy przykład także ma trzy zmienne o nazwie x. Każda z nich jest zadeklarowana w innym
bloku (pierwsza w ciele funkcji, druga w bloku instrukcji for, a trzecia w ciele pętli), ale tylko dwa
bloki są wyraźnie wyodrębnione:
func main() {
x := "witaj"
for _, x := range x {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "WITAJ" (jedna litera na każdą iterację)
}
}
Podobnie jak pętle for, instrukcje if oraz switch również mogą tworzyć dodatkowe dorozumiane
bloki oprócz bloków ich ciała. Kod w poniższym łańcuchu if-else pokazuje zakres zmiennych x i y:
if x := f(); x == 0 {
fmt.Println(x)
} else if y := g(x); x == y {
fmt.Println(x, y)
} else {
fmt.Println(x, y)
}
fmt.Println(x, y) // błąd kompilacji: x i y nie są tutaj widoczne
Druga instrukcja if jest zagnieżdżona w pierwszej, więc zmienne zadeklarowane w obrębie inicjatora
pierwszej instrukcji są widoczne w drugiej. Podobne zasady mają zastosowanie do każdego przy-
padku (case) instrukcji switch: jest blok dla warunku i blok dla każdego ciała przypadku.
Na poziomie pakietu kolejność, w jakiej występują deklaracje, nie ma wpływu na ich zakres, więc de-
klaracja może się odnosić do samej siebie lub do innej następującej po niej deklaracji. Umożli-
wia to deklarowanie rekurencyjnych lub wzajemnie rekurencyjnych typów i funkcji. Kompilator
zgłosi jednak błąd, jeśli deklaracja stałej lub zmiennej będzie się odnosić do samej siebie.
W poniższym programie zakresem zmiennej f jest tylko instrukcja if, więc zmienna f nie jest
dostępna dla następujących później instrukcji, co wywołuje błędy kompilatora. W zależności od
kompilatora może się pojawić dodatkowy błąd komunikujący, że zmienna lokalna f nigdy nie
została użyta.
if f, err := os.Open(fname); err != nil { // błąd kompilacji: nie zostało użyte: f
return err
}
f.ReadByte() // błąd kompilacji: niezdefiniowane f
f.Close() // błąd kompilacji: niezdefiniowane f
Dlatego często konieczne jest zadeklarowanie zmiennej f przed warunkiem, aby była dostępna po:
f, err := os.Open(fname)
if err != nil {
return err
}
f.ReadByte()
f.Close()
2.7. ZAKRES 61

Można ulec pokusie, aby uniknąć deklarowania zmiennych f i err w bloku zewnętrznym po-
przez przeniesienie wywołań do funkcji ReadByte i Close wewnątrz bloku else:
if f, err := os.Open(fname); err != nil {
return err
} else {
// f i err są widoczne również tutaj.
f.ReadByte()
f.Close()
}
Jednak normalną praktyką w języku Go jest obsłużenie błędu w bloku if, a następnie jego zwróce-
nie, tak aby ścieżka udanego wykonywania nie miała wcięcia.
Krótkie deklaracje zmiennych wymagają świadomości zakresu. Rozważmy przedstawiony poniżej
program, który rozpoczyna się od pobrania jego bieżącego katalogu roboczego i zapisania go
w zmiennej poziomu pakietu. Można to zrobić poprzez wywołanie os.Getwd w funkcji main, ale
może lepiej będzie oddzielić tę kwestię od podstawowej logiki, zwłaszcza jeśli niepowodzenie
pobrania katalogu jest błędem krytycznym. Funkcja log.Fatalf wyświetla podany komunikat
i wywołuje funkcję os.Exit(1).
var cwd string

func init() {
cwd, err := os.Getwd() // błąd kompilacji: nie zostało użyte: cwd
if err != nil {
log.Fatalf("Wywołanie funkcji os.Getwd nie powiodło się: %v", err)
}
}
Ponieważ zmienne cwd i err nie są jeszcze zadeklarowane w bloku funkcji init, instrukcja :=
deklaruje je obie jako zmienne lokalne. Wewnętrzna deklaracja zmiennej cwd sprawia, że ze-
wnętrzna deklaracja jest niedostępna, więc dana instrukcja nie aktualizuje zmiennej cwd poziomu
pakietu, tak jak planowano.
Aktualne kompilatory języka Go wykryją, że lokalna zmienna cwd nigdy nie jest używana, i zgłoszą
to jako błąd, ale nie muszą one ściśle przestrzegać wykonywania tej kontroli. Ponadto drobna
zmiana, taka jak dodanie instrukcji rejestrowania odwołującej się do lokalnej zmiennej cwd,
udaremniłaby tę kontrolę.
var cwd string

func init() {
cwd, err := os.Getwd() // UWAGA: źle!
if err != nil {
log.Fatalf("Wywołanie funkcji os.Getwd nie powiodło się: %v", err)
}
log.Printf("Katalog roboczy = %s", cwd)
}
Zmienna globalna cwd pozostaje niezainicjowana i najwidoczniej normalne dane wyjściowe z funkcji
rejestrowania zaciemniają błąd.
62 ROZDZIAŁ 2. STRUKTURA PROGRAMU

Istnieje wiele sposobów radzenia sobie z tym potencjalnym problemem. Najbardziej bezpośred-
nim jest unikanie := poprzez deklarowanie err w oddzielnej deklaracji var:
var cwd string

func init() {
var err error
cwd, err = os.Getwd()
if err != nil {
log.Fatalf("Wywołanie funkcji os.Getwd nie powiodło się: %v", err)
}
}
Dowiedziałeś się, w jaki sposób pakiety, pliki, deklaracje i instrukcje wyrażają strukturę programów.
W następnych dwóch rozdziałach przyjrzymy się strukturze danych.
Rozdział 3

Podstawowe typy danych

Oczywiście na samym dole znajdują się tylko bity, ale komputery zasadniczo operują na liczbach
o ustalonym rozmiarze, zwanych słowami (ang. words), które są interpretowane jako liczby cał-
kowite, liczby zmiennoprzecinkowe, zbiory bitów lub adresy pamięci. Są one następnie łączone
w większe agregacje, które reprezentują pakiety, piksele, portfolia, poezję i wszystko inne. Go ofe-
ruje wiele różnych sposobów organizowania danych, zapewniając spektrum typów danych, któ-
re z jednej strony dopasowują się do charakterystyk sprzętu, a z drugiej dostarczają tego, czego
potrzebują programiści, aby dogodnie reprezentować skomplikowane struktury danych.
W języku Go typy dzielą się na cztery kategorie: typy podstawowe, typy złożone, typy referen-
cyjne i typy interfejsowe. Tematem tego rozdziału są typy podstawowe, w tym liczby, łańcuchy
znaków i wartości logiczne. Typy złożone, takie jak tablice (zob. podrozdział 4.1) i struktury (zob.
podrozdział 4.4), formują bardziej skomplikowane typy danych, łącząc wartości kilku prostszych
typów. Typy referencyjne stanowią zróżnicowaną grupę, która zawiera wskaźniki (zob. punkt 2.3.2),
wycinki (zob. podrozdział 4.2), mapy (zob. podrozdział 4.3), funkcje (zob. rozdział 5.) i kanały (zob.
rozdział 8.), ale ich wspólną cechą jest to, że wszystkie one odwołują się pośrednio do zmiennych
lub stanu programu, więc efekt operacji zastosowanej do jednej referencji jest obserwowany przez
wszystkie kopie tej referencji. Wreszcie o typach interfejsowych porozmawiamy w rozdziale 7.

3.1. Liczby całkowite


Numeryczne typy danych w języku Go obejmują kilka rozmiarów liczb całkowitych (ang. integers),
liczb zmiennoprzecinkowych (ang. floating-point numbers) i liczb zespolonych (ang. complex
numbers). Każdy typ numeryczny określa wielkość swoich wartości i określa, czy są to wartości ze
znakiem, czy bez znaku. Zacznijmy od liczb całkowitych.
Go zapewnia arytmetykę liczb całkowitych zarówno ze znakiem, jak i bez znaku. Istnieją cztery
różne rozmiary liczb całkowitych ze znakiem (8-, 16-, 32- i 64-bitowe), reprezentowane przez
typy: int8, int16, int32 oraz int64. Odpowiadające im wersje typów reprezentujących liczby
całkowite bez znaku to: uint8, uint16, uint32 oraz uint64.
Istnieją również dwa typy, zwane po prostu int i uint, które są naturalnym i najbardziej efektywnym
rozmiarem dla liczb całkowitych ze znakiem i bez znaku na określonej platformie. Typ int jest
zdecydowanie najszerzej stosowanym typem numerycznym. Oba te typy mają ten sam rozmiar: 32
lub 64 bity, ale nie można zakładać, który z tych rozmiarów ma być zastosowany. Różne kompilatory
mogą dokonywać różnych wyborów, nawet na identycznym sprzęcie komputerowym.
64 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

Typ rune jest synonimem typu int32 i zgodnie z konwencją wskazuje, że dana wartość jest
punktem kodowym Unicode. Te dwie nazwy mogą być używane zamiennie. Podobnie typ byte
jest synonimem uint8 i podkreśla, że dana wartość jest fragmentem surowych danych, a nie
niewielką wartością liczbową.
Istnieje również typ uintptr liczby całkowitej bez znaku, którego szerokość nie jest określona,
ale wystarczy, żeby pomieścić wszystkie bity wartości wskaźnika. Typ uintptr jest używany tylko do
programowania niskiego poziomu, np. na granicy programu Go z biblioteką C lub systemem ope-
racyjnym. Przykłady tego zobaczymy w rozdziale 13., gdy będziemy omawiać pakiet unsafe.
Niezależnie od swoich wielkości, typy int, uint i uintptr są innymi typami niż ich odpowiedniki
z wyraźnie określonymi rozmiarami. Tak więc int nie jest tego samego typu co int32 (nawet jeśli
naturalna wielkość liczb całkowitych wynosi 32 bity) i jeśli chcemy użyć wartości int tam, gdzie
wymagana jest wartość int32 i odwrotnie, konieczna jest odpowiednia konwersja.
Liczby ze znakiem są przedstawiane za pomocą kodu uzupełnień do dwóch, w którym najbardziej
znaczący bit jest zarezerwowany dla znaku liczby, a zakresem wartości liczby n-bitowej jest prze-
dział od –2n–1 do 2n–1–1. Liczby całkowite bez znaku wykorzystują pełny zakres bitów dla warto-
ści nieujemnych, więc mają zakres od 0 do 2n–1. Przykładowo: zakres typu int8 wynosi od –128
do 127, a typu uint8 od 0 do 255.
Operatory języka Go dla operacji arytmetycznych, logicznych i porównywania zostały wymienione
poniżej w kolejności malejącego pierwszeństwa:
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||
Istnieje tylko pięć poziomów pierwszeństwa dla operatorów binarnych. Operatory na tym samym
poziomie są lewostronnie łączne, więc nawiasy mogą być wymagane dla jasności lub po to, aby
operatory ewaluowały w zamierzonej kolejności w wyrażeniu, np. mask & (1 << 28).
Każdy operator z dwóch pierwszych linii powyższego zestawienia (np. +) ma odpowiadający mu
operator przypisania, taki jak +=, który może być użyty do skrócenia instrukcji przypisania.
Operatory arytmetyczne liczb całkowitych: +, -, * oraz / mogą być stosowane do liczb całkowitych,
zmiennoprzecinkowych oraz zespolonych, ale operator modulo (reszta z dzielenia) % ma zastosowa-
nie tylko do liczb całkowitych. Zachowanie operatora % dla liczb ujemnych różni się w zależności
od języka programowania. W języku Go znak reszty jest zawsze taki sam jak znak dzielnej, więc
zarówno -5%3, jak i -5%-3 daje -2. Zachowanie operatora / zależy od tego, czy jego operandy są licz-
bami całkowitymi, więc 5,0/4,0 daje 1,25, ale 5/4 daje 1, ponieważ przy dzieleniu liczby całkowitej
wynik jest zaokrąglany w dół do całości.
Jeśli wynik operacji arytmetycznej, ze znakiem lub bez znaku, ma więcej bitów niż może być repre-
zentowane w typie wyniku, mówi się o przepełnieniu (ang. overflow). Najbardziej znaczące bity,
które się nie mieszczą, są po cichu porzucane. Jeśli oryginalna liczba jest typem ze znakiem, wynik
może być ujemny, pod warunkiem że najmniej znaczący bit ma wartość 1, jak w poniższym przy-
kładzie int8:
var u uint8 = 255
fmt.Println(u, u+1, u*u) // "255 0 1"
3.1. LICZBY CAŁKOWITE 65

var i int8 = 127


fmt.Println(i, i+1, i*i) // "127 –128 1"
Dwie liczby całkowite tego samego typu mogą być porównywane za pomocą zamieszczonych
poniżej binarnych operatorów porównania. Typem wyrażenia porównania jest wartość logiczna.
== równa się
!= nie równa się
< mniejsze niż
<= mniejsze niż lub równe
> większe niż
>= większe niż lub równe

W rzeczywistości wszystkie wartości typów podstawowych (wartości logicznych, liczb i łańcuchów


znaków) są porównywalne, co oznacza, że dwie wartości tego samego typu mogą być porówny-
wane za pomocą operatorów == i !=. Ponadto liczby całkowite, liczby zmiennoprzecinkowe
i łańcuchy znaków są porządkowane przez operatory porównania. Wartości wielu innych typów
nie są porównywalne, a żadne inne typy nie są porządkowane. Podczas omawiania każdego z typów
będziemy prezentować reguły dotyczące porównywalności ich wartości.
Istnieją również jednoargumentowe operatory dodawania i odejmowania:
+ plus jednoargumentowy (brak efektu)
- negacja jednoargumentowa

Dla liczb całkowitych +x jest skrótem od 0+x, a -x jest skrótem od 0-x. Dla liczb zmiennoprzecin-
kowych i liczb zespolonych +x to po prostu x, a -x jest negacją x.
Język Go zapewnia również wymienione poniżej bitowe operatory binarne. Pierwsze cztery z nich traktują
swoje operandy jako wzorce bitowe bez koncepcji arytmetycznego przeniesienia lub znaku:
& bitowe AND
| bitowe OR
^ bitowe XOR
&^ czyszczenie bitu (AND NOT)
<< przesunięcie w lewo
>> przesunięcie w prawo

Operator ^ jest bitowym wykluczającym OR (XOR), gdy jest stosowany jako operator binarny,
ale pod warunkiem, że używany jako jednoargumentowy operator prefiksowy jest bitową negacją
lub bitowym uzupełnieniem. Oznacza to, że zwraca wartość z odwróconym każdym bitem w ope-
randzie. Operator &^ to czyszczenie bitu (AND NOT): w wyrażeniu z = x &^ y każdy bit argu-
mentu z ma wartość 0, jeśli odpowiadający mu bit argumentu y ma wartość 1. W przeciwnym wy-
padku jest równy odpowiadającemu bitowi argumentu x.
66 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

Poniższy kod pokazuje, w jaki sposób operacje bitowe mogą być używane do interpretacji wartości
uint8 jako kompaktowego i efektywnego zbioru ośmiu niezależnych bitów. Wykorzystuje cza-
sownik %b funkcji Printf, aby wyświetlić cyfry binarne danej liczby. 08 modyfikuje %b (przysłówek!),
żeby dopełnić wynik zerami do dokładnie ośmiu cyfr.
var x uint8 = 1<<1 | 1<<5
var y uint8 = 1<<1 | 1<<2

fmt.Printf("%08b\n", x) // "00100010", zbiór {1, 5}


fmt.Printf("%08b\n", y) // "00000110", zbiór {1, 2}

fmt.Printf("%08b\n", x&y) // "00000010", część wspólna zbiorów {1}


fmt.Printf("%08b\n", x|y) // "00100110", suma zbiorów {1, 2, 5}
fmt.Printf("%08b\n", x^y) // "00100100", różnica symetryczna zbiorów {2, 5}
fmt.Printf("%08b\n", x&^y) // "00100000", różnica zbiorów {5}

for i := uint(0); i < 8; i++ {


if x&(1<<i) != 0 { // test przynależności
fmt.Println(i) // "1", "5"
}
}

fmt.Printf("%08b\n", x<<1) // "01000100", zbiór {2, 6}


fmt.Printf("%08b\n", x>>1) // "00010001", zbiór {0, 4}
(W podrozdziale 6.5 zostanie przedstawiona implementacja zbiorów liczb całkowitych, które mo-
gą być znacznie większe niż bajt).
W operacjach przesunięcia x<<n oraz x>>n operand n określa liczbę pozycji bitów, o które należy
dokonać przesunięcia, i musi być bez znaku. Operand x może być bez znaku lub ze znakiem.
Arytmetycznie przesunięcie w lewo x<<n jest równoważne z pomnożeniem przez 2n, a przesunięcie
w prawo x>>n jest równoważne z zaokrąglonym w dół do liczby całkowitej wynikiem z dzielenia
przez 2n.
Przesunięcie w lewo wypełnia opuszczone bity zerami, tak jak przesunięcie w prawo liczb bez
znaku, ale przesunięcie w prawo liczb ze znakiem wypełnia opuszczone bity kopiami bitu znaku.
Dlatego ważne jest używanie arytmetyki bez znaku, gdy traktujesz liczbę całkowitą jako wzorzec
bitowy.
Chociaż Go zapewnia liczby bez znaku i arytmetykę, z reguły korzystamy z formy int ze znakiem
nawet dla wartości, które nie mogą być ujemne, takie jak długość tablicy, chociaż typ uint mógłby
się wydawać bardziej oczywistym wyborem. W rzeczywistości wbudowana funkcja len zwraca
int ze znakiem, jak w pętli, która wyświetla medale w odwrotnej kolejności:
medals := []string{"złoto", "srebro", "brąz"}
for i := len(medals) - 1; i >= 0; i-- {
fmt.Println(medals[i]) // "brąz", "srebro", "złoto"
}
Alternatywa byłaby zgubna. Jeśli funkcja len zwróciłaby liczbę bez znaku, wtedy i również byłoby
typem uint, a warunek i >= 0 zawsze byłby prawdziwy z definicji. Po trzeciej iteracji, w której
i == 0, instrukcja i-- spowodowałaby, że i stałoby się nie -l, ale maksymalną wartością uint
(np. 264–1), oraz ewaluacja medals[i] zawiodłaby w czasie wykonywania programu lub wywołała
procedurę panic (zob. podrozdział 5.9), próbując uzyskać dostęp do elementu poza granicami
wycinka.
3.1. LICZBY CAŁKOWITE 67

Z tego powodu liczby bez znaku są z reguły używane tylko wtedy, gdy wymagane są ich operatory
bitowe lub szczególne operatory arytmetyczne, tak jak przy implementowaniu zbiorów bitów,
parsowaniu plików w formatach binarnych lub do haszowania i kryptografii. Nie są one zwykle
używane dla wartości zaledwie nieujemnych.
Zasadniczo bezpośrednia konwersja jest wymagana w celu przekształcenia wartości z jednego typu
na drugi, a operatory binarne dla operacji arytmetycznych i logicznych (z wyjątkiem przesunięć)
muszą mieć operandy tego samego typu. Chociaż czasem prowadzi to do powstawania dłuższych
wyrażeń, to również eliminuje całą klasę problemów i sprawia, że programy są łatwiejsze do
zrozumienia.
Rozważmy poniższą sekwencję, jako przykład znany z innych kontekstów:
var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // błąd kompilacji
Próba kompilacji tych trzech deklaracji generuje komunikat o błędzie:
invalid operation: apples + oranges (mismatched types int32 and int16)
Tę niezgodność typów można łatwo naprawić różnymi sposobami, a najbardziej bezpośrednio
przez przekształcenie wszystkiego na wspólny typ:
var compote = int(apples) + int(oranges)
Jak opisano w podrozdziale 2.5, dla każdego typu T operacja konwersji T(x) przekształca wartość x
na typ T, jeśli taka konwersja jest dozwolona. Wiele konwersji liczby całkowitej na liczbę całkowitą
nie pociąga za sobą żadnych zmian wartości. Po prostu informują one kompilator, jak interpretować
wartość. Jednak konwersja, która zawęża dużą liczbę całkowitą do mniejszej, lub konwersja liczby
całkowitej na liczbę zmiennoprzecinkową (albo odwrotnie) może zmienić wartość lub spowodować
utratę dokładności:
f := 3.141 // float64
i := int(f)
fmt.Println(f, i) // "3.141 3"
f = 1.99
fmt.Println(int(f)) // "1"
Podczas konwersji liczby zmiennoprzecinkowej na liczbę całkowitą porzucana jest część ułamkowa.
Należy unikać konwersji, w których operand jest spoza zakresu dla typu docelowego, ponieważ to
zachowanie zależy od implementacji:
f := 1e100 // float64
i := int(f) // wynik zależy od implementacji
Literały liczb całkowitych dowolnej wielkości i dowolnego typu mogą być zapisywane jako zwykłe licz-
by dziesiętne lub liczby ósemkowe, jeśli rozpoczynają się od 0 (jak 0666), albo w systemie szes-
nastkowym, jeśli rozpoczynają się od 0x lub 0X (jak 0xdeadbeef). Cyfry w zapisie szesnastkowym
mogą być duże lub małe. Obecnie liczby ósemkowe wydają się być używane w dokładnie jednym
celu (dla praw dostępu do plików POSIX), ale liczby szesnastkowe są szeroko stosowane, aby wyróżnić
wzorzec bitowy liczby ponad jej wartość numeryczną.
Podczas wyświetlania liczb przy użyciu pakietu fmt możemy kontrolować ich podstawę i format
za pomocą czasowników %d, %o oraz %x, tak jak pokazano w poniższym przykładzie:
68 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
Zwróć uwagę na użycie dwóch sztuczek pakietu fmt. Po pierwsze, łańcuch znaków formatowa-
nia funkcji Printf zawierający wiele czasowników % wymagałby tej samej liczby dodatkowych
argumentów, ale „przysłówki” [1] po znaku % wskazują tej funkcji, aby używać w kółko pierwszego
argumentu. Po drugie, przysłówek # dla %o, %x lub %X wskazuje funkcji Printf, aby emitować
odpowiednio prefiks 0, 0x lub 0X.
Literały run są zapisywane jako znak w pojedynczych cudzysłowach. Najprostszym przykładem
jest znak ASCII, taki jak 'a', ale możliwe jest zapisanie dowolnego punktu kodowego Unicode
bezpośrednio lub za pomocą numerycznej sekwencji ucieczki, jak zobaczymy wkrótce.
Runy są wyświetlane za pomocą czasownika %c lub za pomocą %q, jeśli wymagane jest cytowanie:
ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'
fmt.Printf("%d %[1]q\n", newline) // "10 '\n'"

3.2. Liczby zmiennoprzecinkowe


Język Go oferuje dwa rozmiary liczb zmiennoprzecinkowych: float32 i float64. Ich właściwości
arytmetyczne są uregulowane w standardzie IEEE 754 implementowanym przez wszystkie no-
woczesne procesory.
Wartości tych typów numerycznych wahają się od niewielkich do ogromnych. Granice wartości
zmiennoprzecinkowych można znaleźć w pakiecie math. Stała math.MaxFloat32, największa
wartość float32, wynosi ok. 3.4e38, a stała math.MaxFloat64 ok. 1.8e308. Najmniejsze wartości
dodatnie wynoszą w przybliżeniu odpowiednio 1.4e-45 i 4.9e-324.
Typ float32 zapewnia dokładność rzędu ok. sześciu cyfr dziesiętnych, natomiast float64 zapew-
nia dokładność ok. piętnastu cyfr. Typ float64 powinien być preferowany w większości zastoso-
wań, ponieważ obliczenia float32 mogą gwałtownie kumulować błąd, jeśli nie jest się wystarczająco
ostrożnym, a najmniejsza dodatnia liczba całkowita, która nie może być dokładnie przedstawiona
jako float32, nie jest duża:
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // "true"!
Liczby zmiennoprzecinkowe mogą być zapisywane literalnie za pomocą ułamków dziesiętnych, np.:
const e = 2.71828 // (w przybliżeniu)
Cyfry mogą być pominięte przed kropką dziesiętną (.707) lub po niej (1.). Bardzo małe lub
bardzo duże liczby lepiej zapisywać w notacji naukowej, z literą e lub E poprzedzającą wykładnik
dziesiętny:
const Avogadro = 6.02214129e23
const Planck = 6.62606957e-34
3.2. LICZBY ZMIENNOPRZECINKOWE 69

Wartości zmiennoprzecinkowe można w wygodny sposób wyświetlać za pomocą czasownika %g


funkcji Printf, który wybiera najbardziej zwartą reprezentację posiadającą odpowiednią dokład-
ność, ale dla tabel danych bardziej właściwe mogą być formy %e (z wykładnikiem) lub %f (bez
wykładnika). Wszystkie te trzy czasowniki pozwalają kontrolować szerokość pola i dokładność
liczbową.
for x := 0; x < 8; x++ {
fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}
Powyższy kod wyświetla potęgi liczby e z dokładnością do trzech cyfr po przecinku, wyrównane
w polu ośmioznakowym:
x = 0 e^x = 1.000
x = 1 e^x = 2.718
x = 2 e^x = 7.389
x = 3 e^x = 20.086
x = 4 e^x = 54.598
x = 5 e^x = 148.413
x = 6 e^x = 403.429
x = 7 e^x = 1096.633
Oprócz dużej kolekcji typowych funkcji matematycznych, pakiet math ma funkcje do tworzenia
i wykrywania wartości specjalnych definiowanych przez standard IEEE 754: nieskończoności
dodatnich i ujemnych, które reprezentują liczby o nadmiernej wielkości i wynik dzielenia przez
zero, oraz wartości NaN (nie-liczby), będących wynikiem takich wątpliwych operacji matema-
tycznych jak 0/0 lub Sqrt(-1).
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 –0 +Inf –Inf NaN"
Funkcja math.IsNaN testuje, czy jej argument jest wartością NaN, a funkcja math.NaN zwraca taką
wartość. Kuszące jest użycie NaN w obliczeniach liczbowych jako wartości sygnalizującej, ale te-
stowanie, czy konkretny wynik obliczeniowy jest równy NaN, jest obarczone niebezpieczeństwem,
ponieważ wszelkie porównania z NaN zawsze dają false (fałsz):
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
Jeśli wykonywanie funkcji zwracającej wynik zmiennoprzecinkowy może się nie powieść, lepiej
raportować to niepowodzenie osobno, np. w taki sposób:
func compute() (value float64, ok bool) {
// …
if failed {
return 0, false
}
return result, true
}
Następny program ilustruje zmiennoprzecinkowe obliczenia graficzne. Rysuje wykres funkcji dwóch
zmiennych z = f(x, y) w postaci siatki powierzchniowej 3D za pomocą grafiki wektorowej (SVG),
będącej standardowym formatem XML do rysowania linii. Na rysunku 3.1 przedstawiono przykład
danych wyjściowych tego programu dla funkcji sin(r)/r, gdzie r wynosi sqrt(x*x+y*y).
70 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

Rysunek 3.1. Wykres powierzchniowy dla funkcji sin(r)/r

code/r03/surface
// Surface oblicza renderowanie SVG funkcji powierzchniowej 3D.
package main

import (
"fmt"
"math"
)

const (
width, height = 600, 320 // rozmiar płótna w pikselach
cells = 100 // liczba komórek siatki
xyrange = 30.0 // zakresy osi (–xyrange..+xyrange)
xyscale = width / 2 / xyrange // liczba pikseli na jednostkę x lub y
zscale = height * 0.4 // liczba pikseli na jednostkę z
angle = math.Pi / 6 // kąt nachylenia osi x, y (=30°)
)
var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°)

func main() {
fmt.Printf("<svg xmlns='http://www.w3.org/2000/svg' "+
"style='stroke: grey; fill: white; stroke-width: 0.7' "+
"width='%d' height='%d'>", width, height)
for i := 0; i < cells; i++ {
for j := 0; j < cells; j++ {
ax, ay := corner(i+1, j)
bx, by := corner(i, j)
cx, cy := corner(i, j+1)
dx, dy := corner(i+1, j+1)
fmt.Printf("<polygon points='%g,%g %g,%g %g,%g %g,%g'/>\n",
ax, ay, bx, by, cx, cy, dx, dy)
}
}
fmt.Println("</svg>")
}
3.2. LICZBY ZMIENNOPRZECINKOWE 71

func corner(i, j int) (float64, float64) {


// Znajdowanie punktu (x, y) w rogu komórki (i, j).
x := xyrange * (float64(i)/cells - 0.5)
y := xyrange * (float64(j)/cells - 0.5)

// Obliczenie wysokości z powierzchni.


z := f(x, y)

// Rzutowanie (x, y, z) izometrycznie na płótno 2D SVG (sx, sy).


sx := width/2 + (x-y)*cos30*xyscale
sy := height/2 + (x+y)*sin30*xyscale - z*zscale
return sx, sy
}

func f(x, y float64) float64 {


r := math.Hypot(x, y) // odległość od punktu (0, 0)
return math.Sin(r) / r
}
Należy zwrócić uwagę, że funkcja corner zwraca dwie wartości: współrzędne rogu komórki.
Wyjaśnienie sposobu działania programu wymaga tylko podstawowej geometrii, ale można to
pominąć, ponieważ chodzi o zilustrowanie obliczeń zmiennoprzecinkowych. Istotą programu jest
odwzorowanie między trzema różnymi układami współrzędnych przedstawione na rysunku 3.2.
Pierwszym z nich jest siatka 2D o rozmiarach 100×100 komórek identyfikowanych za pomocą
współrzędnych całkowitych (i, j), która rozpoczyna się w punkcie (0, 0) w najbardziej oddalonym
rogu. Wykres jest kreślony od tyłu do przodu, więc wielokąty znajdujące się na drugim planie
mogą być zasłonięte przez te, które znajdują się na planie pierwszym.

Rysunek 3.2. Trzy różne układy współrzędnych

Drugim układem współrzędnych jest siatka 3D współrzędnych zmiennoprzecinkowych (x, y, z),


gdzie x i y są funkcjami liniowymi współrzędnych i oraz j przełożonymi w taki sposób, że początek
układu znajduje się w środku, i skalowanymi przez stałą xyrange. Wysokość z jest wartością funkcji
powierzchniowej f(x, y).
Trzecim układem współrzędnych jest płótno obrazu 2D z punktem (0, 0) w lewym górnym rogu.
Punkty na tej płaszczyźnie są oznaczane jako (sx, sy). Używamy rzutu izometrycznego do odwzo-
rowania każdego punktu 3D (x, y, z) na płótno 2D. Punkt pojawia się tym bardziej na prawo na
płótnie, im większa jest jego wartość x lub im mniejsza jest jego wartość y. Ponadto punkt pojawia
się tym niżej na płótnie, im większa jest jego wartość x lub wartość y, a im mniejsza jest jego
wartość z. Pionowe i poziome współczynniki skalowania dla x i y są wywodzone z funkcji sinus
i cosinus kąta o mierze 30°. Współczynnik skalowania 0,4 dla z jest dowolnym parametrem.
72 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

Dla każdej komórki w siatce 2D funkcja main oblicza współrzędne na płótnie obrazu dla czterech
rogów wielokąta ABCD, w którym współrzędne wierzchołka B odpowiadają współrzędnym (i, j),
a wierzchołki A, C i D są jego sąsiadami, a następnie drukuje instrukcje SVG do jego narysowania.
Ćwiczenie 3.1. Jeśli funkcja f zwraca nieokreśloną wartość float64, plik SVG będzie zawierać
nieprawidłowe elementy <polygon> (chociaż wiele programów renderujących SVG zgrabnie to
obsługuje). Zmodyfikuj program w taki sposób, aby pomijał nieprawidłowe wielokąty.
Ćwiczenie 3.2. Poeksperymentuj z wizualizacjami innych funkcji z pakietu math. Potrafisz wyge-
nerować wytłoczkę na jajka, muldy lub siodło?
Ćwiczenie 3.3. Pokoloruj każdy wielokąt w zależności od jego położenia, tak aby szczyty były
w kolorze czerwonym (#ff0000), a doliny w kolorze niebieskim (#0000ff).
Ćwiczenie 3.4. Wykorzystując podejście zastosowane w przykładzie Lissajous w podrozdziale 1.7,
zbuduj serwer WWW, który oblicza powierzchnie i zapisuje dane w formacie SVG do klienta. Serwer
musi ustawiać nagłówek Content-Type w następujący sposób:
w.Header().Set("Content-Type", "image/svg+xml")
(Ten krok nie był wymagany w przykładzie Lissajous, ponieważ serwer wykorzystywał standardo-
we heurystyki rozpoznawania popularnych formatów, takich jak PNG, na podstawie pierwszych
512 bajtów odpowiedzi i generował właściwy nagłówek). Pozwól klientowi określić jako parametry
żądania HTTP wartości takie jak wysokość, szerokość i kolor.

3.3. Liczby zespolone


Go zapewnia dwa rozmiary liczb zespolonych: complex64 i complex128, których komponentami
są odpowiednio float32 i float64. Wbudowana funkcja complex tworzy liczbę zespoloną z jej
części rzeczywistej i urojonej, a wbudowane funkcje real i imag wyodrębniają te części:
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(–5+10i)"
fmt.Println(real(x*y)) // "–5"
fmt.Println(imag(x*y)) // "10"
Jeśli bezpośrednio po literale liczby zmiennoprzecinkowej lub dziesiętnej liczby całkowitej następuje
jednostka urojona i (np. 3.141592i lub 2i), staje się on literałem urojonym (ang. imaginary literal),
oznaczającym liczbę zespoloną z zerową częścią rzeczywistą:
fmt.Println(1i * 1i) // "(–1+0i)", i^2 = –1
Według zasad arytmetyki stałych stałe zespolone mogą być dodawane do innych stałych liczbo-
wych (liczb całkowitych lub zmiennoprzecinkowych, rzeczywistych lub urojonych), co pozwala
zapisywać liczby zespolone w sposób naturalny, tak jak 1+2i lub równoważnie 2i+1. Powyższe
deklaracje x i y można więc uprościć:
x := 1 + 2i
y := 3 + 4i
Liczby zespolone można porównać za pomocą operatorów == oraz !=. Dwie liczby zespolone są
równe, jeśli ich części rzeczywiste są równe i ich części urojone są równe.
Pakiet math/cmplx zapewnia funkcje biblioteki do pracy z liczbami zespolonymi, takie jak zespo-
lony pierwiastek kwadratowy i funkcje potęgowania.
fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"
3.3. LICZBY ZESPOLONE 73

Poniższy program używa arytmetyki complex128 do wygenerowania zbioru Mandelbrota.


code/r03/mandelbrot
// Mandelbrot emituje obraz PNG fraktala Mandelbrota.
package main

import (
"image"
"image/color"
"image/png"
"math/cmplx"
"os"
)

func main() {
const (
xmin, ymin, xmax, ymax = -2, -2, +2, +2
width, height = 1024, 1024
)

img := image.NewRGBA(image.Rect(0, 0, width, height))


for py := 0; py < height; py++ {
y := float64(py)/height*(ymax-ymin) + ymin
for px := 0; px < width; px++ {
x := float64(px)/width*(xmax-xmin) + xmin
z := complex(x, y)
// Punkt obrazu (px, py) reprezentuje wartość zespoloną z.
img.Set(px, py, mandelbrot(z))
}
}
png.Encode(os.Stdout, img) // UWAGA: ignorowanie błędów
}

func mandelbrot(z complex128) color.Color {


const iterations = 200
const contrast = 15

var v complex128
for n := uint8(0); n < iterations; n++ {
v = v*v + z
if cmplx.Abs(v) > 2 {
return color.Gray{255 - contrast*n}
}
}
return color.Black
}
Dwie zagnieżdżone pętle iterują przez każdy punkt obrazu rastrowego 1024×1024 w skali szarości
reprezentujący obszar od –2 do +2 płaszczyzny zespolonej. Program testuje, czy wielokrotne
podnoszenie do kwadratu i dodawanie liczby, którą reprezentuje ten punkt, w końcu doprowadzi
do „ucieczki” z okręgu o promieniu 2. Jeśli tak, punkt jest cieniowany na podstawie liczby iteracji
wymaganych do ucieczki. Jeśli nie, dana wartość należy do zbioru Mandelbrota, a punkt pozostaje
czarny. Na koniec program przekazuje do swojego standardowego strumienia wyjściowego za-
kodowany w formacie PNG obraz kultowego fraktala przedstawiony na rysunku 3.3.
74 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

Rysunek 3.3. Zbiór Mandelbrota

Ćwiczenie 3.5. Zaimplementuj pełnokolorowy zbiór Mandelbrota, wykorzystując funkcję


image.NewRGBA oraz typ color.RGBA lub color.YCbCr.
Ćwiczenie 3.6. Supersampling jest techniką służącą do redukcji efektu pikselizacji poprzez obli-
czanie wartości koloru w kilku punktach w ramach każdego piksela i przyjmowanie średniej.
Najprostszym sposobem jest podzielenie każdego piksela na cztery „podpiksele”. Zaimplementuj
tę technikę.
Ćwiczenie 3.7. Kolejny prosty fraktal wykorzystuje metodę Newtona do znalezienia zespolonych
rozwiązań dla funkcji takiej jak z4–1=0. Zacieniuj każdy punkt początkowy na podstawie liczby
iteracji potrzebnych do zbliżenia się do jednego z czterech pierwiastków funkcji. Pokoloruj każdy
punkt na podstawie pierwiastka, do którego się zbliża.
Ćwiczenie 3.8. Renderowanie fraktali przy wysokich poziomach przybliżenia wymaga dużej do-
kładności arytmetycznej. Zaimplementuj ten sam fraktal za pomocą czterech różnych reprezenta-
cji liczb: complex64, complex128, big.Float oraz big.Rat. (Te dwa ostatnie typy można znaleźć
w pakiecie math/big. Typ Float wykorzystuje duże liczby zmiennoprzecinkowe, ale o ograniczonej
dokładności. Typ Rat wykorzystuje liczby wymierne o nieograniczonej dokładności). Jak wypada
ich porównanie pod względem wydajności i wykorzystania pamięci? Przy jakim poziomie przybli-
żenia renderowane artefakty stają się widoczne?
Ćwiczenie 3.9. Napisz serwer WWW, który renderuje fraktale i zapisuje dane obrazu do klienta.
Pozwól klientowi określić wartości x, y oraz przybliżenia jako parametry żądania HTTP.
3.4. WARTOŚCI LOGICZNE 75

3.4. Wartości logiczne


Typ bool, czyli typ logiczny (ang. boolean), przyjmuje tylko dwie możliwe wartości: true (prawda)
i false (fałsz). Warunki w instrukcjach if oraz for są wartościami logicznymi, a operatory po-
równania, takie jak == i <, generują wynik logiczny. Operator jednoargumentowy ! jest logiczną
negacją, więc !true to false lub, formalnie, (!true==false)==true. Jednak zgodnie z konwencją
zawsze upraszczamy zbędne wyrażenia logiczne, takie jak x==true, do x.
Wartości logiczne mogą być łączone za pomocą operatorów && (AND) oraz || (OR), które cha-
rakteryzują się ewaluacją minimalną: jeśli odpowiedź jest już określona przez wartość lewego
operandu, prawy operand nie jest ewaluowany, więc bezpieczne jest napisanie takiego wyrażenia:
s != "" && s[0] == 'x'
gdzie s[0] wywołałoby procedurę panic, jeśli zostałoby zastosowane do pustego ciągu.
Ponieważ operator && ma wyższy priorytet niż operator || (do zapamiętania: && jest logicznym
mnożeniem, a || jest logicznym dodawaniem), dla warunków w poniższej formie nie są wymagane
nawiasy:
if 'a' <= c && c <= 'z' ||
'A' <= c && c <= 'Z' ||
'0' <= c && c <= '9' {
// …litera lub cyfra ASCII…
}
Nie istnieje pośrednia konwersja z wartości logicznej na wartość liczbową, taką jak 0 lub 1, ani od-
wrotnie. Trzeba użyć instrukcji if bezpośrednio, tak jak tu:
i := 0
if b {
i = 1
}
Jeśli ta operacja byłaby często wymagana, być może warto napisać funkcję konwersji:
// btoi zwraca 1, jeśli b jest prawdą, lub 0, jeśli b jest fałszem.
func btoi(b bool) int {
if b {
return 1
}
return 0
}
Operacja odwrotna jest tak prosta, że nie wymaga funkcji, ale dla symetrii można ją napisać:
// itob zgłasza, czy i nie równa się zero.
func itob(i int) bool { return i != 0 }

3.5. Łańcuchy znaków


Łańcuch znaków (ang. string) jest niemutowalną sekwencją bajtów. Łańcuchy znaków mogą za-
wierać dowolne dane, w tym bajty o wartości 0, ale zwykle zawierają tekst czytelny dla człowieka.
Tekstowe łańcuchy znaków są umownie interpretowane jako zakodowane w UTF-8 sekwencje
punktów kodowych Unicode (runy), które wkrótce szczegółowo omówimy.
Wbudowana funkcja len zwraca liczbę bajtów (nie run) w łańcuchu znaków, a operacja indeksowa
s[i] pobiera i-ty bajt łańcucha s, gdzie 0 ≤ i < len(s).
76 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

s := "witaj, świecie"
fmt.Println(len(s)) // "15"
fmt.Println(s[0], s[7]) // "119 197" ('w' i 'ś')
Próba uzyskania dostępu do bajtu spoza tego zakresu powoduje uruchomienie procedury panic:
c := s[len(s)] // panic: indeks poza zakresem
Nie zawsze i-ty bajt łańcucha jest i-tym znakiem łańcucha, ponieważ kodowanie UTF-8 punktu
kodowego spoza ASCII wymaga dwóch lub więcej bajtów. Praca ze znakami zostanie omówiona
wkrótce.
Operacja podłańcucha s[i:j] daje nowy łańcuch składający się z bajtów oryginalnego łańcucha.
Ten nowy łańcuch rozpoczyna się w indeksie i i ciągnie się do bajtu w indeksie j, ale go nie obejmuje.
Wynik zawiera j-i bajtów.
fmt.Println(s[0:5]) // "witaj"
Wywołanie panic ma także miejsce, jeśli indeks znajduje się poza zakresem lub j jest mniejsze niż i.
Można pominąć jeden z argumentów: i lub j, albo oba. W takim przypadku przyjmowane są odpo-
wiednio domyślne wartości 0 (początek łańcucha) i len(s) (koniec łańcucha).
fmt.Println(s[:5]) // "witaj"
fmt.Println(s[7:]) // "świecie"
fmt.Println(s[:]) // "witaj, świecie"
Operator + tworzy nowy łańcuch znaków, konkatenując dwa łańcuchy:
fmt.Println("żegnaj" + s[5:]) // "żegnaj, świecie"
Łańcuchy znaków mogą być porównywane za pomocą operatorów porównania takich jak == i <.
Porównywanie odbywa się bajt po bajcie, więc wynik jest naturalnym uporządkowaniem leksy-
kograficznym.
Wartości łańcucha znaków są niemutowalne: sekwencja bajtów zawartych w wartości łańcucha
nigdy nie może być zmieniona, chociaż możemy oczywiście przypisać nową wartość do zmiennej
łańcucha. Aby dołączyć np. jeden łańcuch do drugiego, można napisać:
s := "lewa stopa"
t := s
s += ", prawa stopa"
Nie zmienia to łańcucha znaków przechowywanego pierwotnie przez zmienną s, ale powoduje,
że s przechowuje teraz nowy łańcuch utworzony przez instrukcję +=. Tymczasem t nadal zawiera
stary łańcuch znaków.
fmt.Println(s) // "lewa stopa, prawa stopa"
fmt.Println(t) // "lewa stopa"
Ponieważ łańcuchy znaków są niemutowalne, konstrukcje, które próbują modyfikować dane
łańcucha in situ, nie są dozwolone:
s[0] = 'L' // błąd kompilacji: nie można przypisać do s[0]
Niemutowalność oznacza, że dwie kopie łańcucha znaków mogą bezpiecznie współdzielić tę sa-
mą pamięć bazową, dzięki czemu kopiowanie łańcuchów o dowolnej długości wiąże się z niskimi
kosztami. Podobnie łańcuch s i podłańcuch taki jak s[7:] mogą bezpiecznie współdzielić te sa-
me dane, więc operacja podłańcucha jest również niskokosztowa. W żadnym z tych przypadków
nie jest alokowana nowa pamięć. Rysunek 3.4 przedstawia układ łańcucha i dwóch jego podłań-
cuchów współdzielących tę samą bazową tablicę bajtów.
3.5. ŁAŃCUCHY ZNAKÓW 77

Rysunek 3.4. Łańcuch znaków „witaj, świecie” i dwa podłańcuchy

3.5.1. Literały łańcuchów znaków


Wartość łańcucha znaków może być zapisana jako literał łańcucha znaków, czyli sekwencja
bajtów zamknięta w podwójnych cudzysłowach:
"Witaj, 世界"
Ponieważ pliki źródłowe Go są zawsze kodowane w UTF-8, a tekstowe łańcuchy znaków są
umownie interpretowane jako UTF-8, możemy w literałach łańcuchów znaków załączać punkty
kodowe Unicode.
W obrębie umieszczonego w podwójnych cudzysłowach literału łańcucha znaków mogą być
używane rozpoczynające się od lewego ukośnika (\) sekwencje ucieczki (ang. escape squences),
które umożliwiają wstawianie do łańcucha dowolnych wartości bajtowych. Jeden z zestawów
znaków ucieczki obsługuje kody sterowania ASCII, takie jak znak nowej linii, powrót karetki
i tabulator:
\a „alarm” lub dzwonek
\b Backspace
\f znak wysunięcia strony
\n znak nowej linii
\r znak powrotu karetki
\t tabulacja
\v tabulacja pionowa
\' pojedynczy cudzysłów (tylko w literale runy '\'')
\" podwójny cudzysłów (tylko w literałach "...")
\\ lewy ukośnik

Dowolne bajty mogą być również wstawiane do literałów łańcuchów znaków za pomocą szesnast-
kowych lub ósemkowych znaków ucieczki. Szesnastkowy znak ucieczki jest zapisywany jako
\xhh z dokładnie dwiema cyframi szesnastkowymi h (wielkie lub małe litery). Ósemkowy znak
ucieczki jest zapisywany jako \ooo z dokładnie trzema cyframi ósemkowymi o (od 0 do 7) i nie
przekracza wartości \377. Oba oznaczają pojedynczy bajt z określoną wartością. Później zobaczymy,
w jaki sposób kodować numerycznie punkty kodowe Unicode w literałach łańcuchów znaków.
78 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

Surowy literał łańcucha znaków jest zapisywany w postaci ` ... ` z wykorzystaniem znaków
grawis zamiast podwójnych cudzysłowów. W obrębie surowego literału łańcucha znaków nie są
przetwarzane żadne sekwencje ucieczek. Cała zawartość jest brana dosłownie, w tym lewe ukośniki
i znaki nowej linii, więc surowy literał łańcucha znaków może się rozciągać na kilka linii w źródle
programu. Jedynym przetwarzaniem jest usuwanie znaków powrotu karetki, aby wartość łańcucha
znaków była taka sama na wszystkich platformach, również na tych, które umownie umieszczają
znak powrotu karetki w plikach tekstowych.
Surowe literały łańcuchów znaków są wygodnym sposobem zapisu wyrażeń regularnych, które
zwykle mają wiele lewych ukośników. Są również przydatne m.in.: dla szablonów HTML, lite-
rałów JSON i komunikatów informujących o sposobie stosowania poleceń, które często rozciągają
się na wiele linii.
const GoUsage = ` Go is a tool for managing Go source code.

Usage:
go command [arguments]
... `

3.5.2. Unicode
Dawno temu życie było proste i istniał tylko jeden (przynajmniej z prowincjonalnego punktu
widzenia) zestaw znaków, z którym trzeba było sobie radzić: ASCII (ang. American Standard
Code for Information Interchange). ASCII, lub precyzyjniej: US-ASCII, używa siedmiu bitów do
reprezentowania 128 „znaków”: małych i wielkich liter alfabetu angielskiego, cyfr oraz wielu
znaków interpunkcyjnych i sterujących. W początkach komputeryzacji było to odpowiednie dla
znacznej liczby zastosowań, ale bardzo duża część ludności świata nie była w stanie używać
w komputerach własnych systemów pisma. Wraz z rozwojem internetu dane w niezliczonych języ-
kach stały się znacznie bardziej powszechne. Jak można w ogóle radzić sobie z taką bogatą róż-
norodnością i w miarę możliwości robić to w sposób efektywny?
Odpowiedzią jest Unicode (unicode.org), który gromadzi wszystkie znaki we wszystkich świato-
wych systemach pisma, plus akcenty i inne znaki diakrytyczne, kody sterujące (takie jak tabula-
cja i znak powrotu karetki) oraz mnóstwo ezoteryki, i przypisuje każdemu znakowi standardo-
wy numer zwany punktem kodowym Unicode lub w terminologii Go — runą.
Unicode w wersji 8 definiuje punkty kodowe dla ponad 120 tys. znaków w ponad 100 językach
i skryptach. W jaki sposób są one reprezentowane w programach i danych komputerowych? Natu-
ralnym typem danych do przechowywania pojedynczej runy jest int32 i ten typ wykorzystuje
język Go, który dokładnie do tego celu posiada również synonim o nazwie rune.
Sekwencję run można by reprezentować jako sekwencję wartości int32. W tej reprezentacji,
zwanej UTF-32 lub UCS-4, kodowanie każdego punktu kodowego Unicode ma taki sam rozmiar,
czyli 32 bity. Jest to proste i jednolite, ale zużywa o wiele więcej miejsca niż to konieczne, ponieważ
większość tekstu czytelnego dla komputera jest zakodowana w ASCII, który wymaga tylko 8 bitów
(1 bajtu) na znak. Wszystkich znaków będących w powszechnym użyciu jest nadal mniej niż
65 536, co zmieściłoby się w 16 bitach. Można zrobić to lepiej?
3.5. ŁAŃCUCHY ZNAKÓW 79

3.5.3. UTF-8
UTF-8 jest zmiennej długości kodowaniem punktów kodowych Unicode jako bajtów. Kodowa-
nie UTF-8 zostało wynalezione przez Kena Thompsona i Roba Pike’a, zaliczanych do twórców
języka Go, a obecnie jest standardem Unicode. Wykorzystuje ono do reprezentowania każdej
runy od 1 do 4 bajtów, ale tylko 1 bajt jest przeznaczony dla znaków ASCII, a jedynie 2 lub 3
bajty dla większości run będących w powszechnym użyciu. Najstarsze bity w pierwszym bajcie
kodowania runy wskazują, ile bajtów następuje po pierwszym bajcie. Najstarszy bit 0 oznacza
siedmiobitowe kodowanie ASCII, w którym każda runa zajmuje tylko l bajt, więc jest to identyczne
z konwencjonalnym ASCII. Najstarsze bity 110 wskazują, że runa zajmuje 2 bajty. Drugi bajt
rozpoczyna się od 10. Większe runy mają analogiczne kodowanie.

0xxxxxxx runy 0 − 127 (ASCII)


110xxxxx 10xxxxxx 128 − 2047 (wartości <128 są nieużywane)
1110xxxx 10xxxxxx 10xxxxxx 2048 − 65 535 (wartości <2048 są nieużywane)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65 536 − 0x10ffff (pozostałe wartości są nieużywane)

Kodowanie o zmiennej długości wyklucza bezpośrednie indeksowanie w celu uzyskiwania dostępu


do n-tego znaku łańcucha, ale UTF-8 ma wiele pożądanych właściwości, które to rekompensują.
To kodowanie jest zwarte, kompatybilne z ASCII i samosynchronizujące. Można znaleźć po-
czątek znaku, cofając się nie więcej niż o 3 bajty. Jest to również kod prefiksowy, a więc może być
dekodowany od lewej do prawej bez żadnej dwuznaczności lub wybiegania w przód. Żadne ko-
dowanie runy nie jest podłańcuchem innej runy, ani nawet sekwencją innych run, dzięki czemu
można wyszukiwać runę, po prostu szukając jej bajtów, nie przejmując się poprzedzającym
kontekstem. Leksykograficzna kolejność bajtów jest taka sama jak porządek punktów kodowych
Unicode, więc sortowanie UTF-8 działa w sposób naturalny. Nie ma osadzonych bajtów NUL (zero),
co jest wygodne dla języków programowania, które używają NUL do kończenia łańcuchów znaków.
Pliki źródłowe Go są zawsze kodowane w UTF-8, a UTF-8 jest preferowanym kodowaniem tek-
stowych łańcuchów znaków manipulowanych przez programy Go. Pakiet unicode zapewnia
funkcje do pracy z poszczególnymi runami (takie jak odróżnianie liter od liczb lub konwersja
wielkich liter na małe), a pakiet unicode/utf8 zapewnia funkcje do kodowania i dekodowania
run jako bajtów za pomocą UTF-8.
Wiele znaków Unicode trudno jest wstukać na klawiaturze lub odróżnić wizualnie od innych
znaków o podobnym wyglądzie. Niektóre są nawet niewidoczne. Znaki ucieczki Unicode w litera-
łach łańcuchów znaków języka Go pozwalają określać te wspomniane znaki po wartości nu-
merycznej ich punktu kodowego. Istnieją dwie formy: \uhhhh dla wartości 16-bitowych
i \Uhhhhhhhh dla wartości 32-bitowych, gdzie każde h jest cyfrą szesnastkową. Potrzeba zastosowa-
nia formy 32-bitowej pojawia się bardzo rzadko. Każda z tych form oznacza kodowanie UTF-8
określonego punktu kodowego. Tak więc przykładowo wszystkie poniższe literały łańcuchów
znaków reprezentują ten sam sześciobajtowy łańcuch:
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"
80 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

Te trzy powyższe sekwencje ucieczki zapewniają alternatywną notację dla pierwszego łańcucha
znaków, ale oznaczane przez nie wartości są identyczne.
Znaki ucieczki Unicode mogą być również używane w literałach run. Te trzy literały są równoważne:
'世' ' \u4e16' ' \U00004e16'
Runa, której wartość jest mniejsza niż 256, może być zapisana za pomocą pojedynczego szesnast-
kowego znaku ucieczki, takiego jak '\x41' dla 'A', ale dla wyższych wartości trzeba użyć znaków
ucieczki \u lub \U. W konsekwencji zapis '\xe4\xb8\x96' nie jest prawidłowym literałem runy,
chociaż te 3 bajty są poprawnym kodowaniem UTF-8 dla pojedynczego punktu kodowego.
Dzięki przyjemnym właściwościom UTF-8 wiele operacji łańcuchów znaków nie wymaga deko-
dowania. Używając dla tekstu zakodowanego w UTF-8 tej samej logiki co dla surowych bajtów,
można sprawdzić, czy jeden łańcuch zawiera inny jako prefiks:
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
lub jako przyrostek:
func HasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
lub jako podłańcuch:
func Contains(s, substr string) bool {
for i := 0; i < len(s); i++ {
if HasPrefix(s[i:], substr) {
return true
}
}
return false
}
Nie jest to prawdą dla innych kodowań. (Powyższe funkcje zostały zaczerpnięte z pakietu strings,
chociaż jego implementacja funkcji Contains wykorzystuje do bardziej efektywnego przeszukiwania
technikę tworzenia skrótu).
Z drugiej strony, jeśli naprawdę zależy nam na konkretnych znakach Unicode, musimy użyć in-
nych mechanizmów. Rozważmy łańcuch znaków z naszego pierwszego przykładu, który zawiera
dwa znaki wschodnioazjatyckie. Na rysunku 3.5 przedstawiono reprezentację tego łańcucha znaków
w pamięci. Łańcuch zawiera 13 bajtów, ale interpretowany jako UTF-8 koduje tylko dziewięć
punktów kodowych lub run:
import "unicode/utf8"

s := "Witaj, 世界"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"
Aby przetworzyć te znaki, potrzebujemy dekodera UTF-8. Pakiet unicode/utf8 zapewnia taki
dekoder, którego możemy użyć w następujący sposób:
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}
3.5. ŁAŃCUCHY ZNAKÓW 81

Rysunek 3.5. Pętla range dekoduje łańcuch znaków zakodowany w UTF-8

Każde wywołanie funkcji DecodeRuneInString zwraca r (samą runę) oraz size (rozmiar), czyli
liczbę bajtów zajętych przez kodowanie UTF-8 runy r. Rozmiar jest używany do aktualizacji in-
deksu bajtowego i następnej runy w łańcuchu. Jest to jednak niezgrabne i za każdym razem po-
trzebujemy tego rodzaju pętli. Na szczęście, gdy pętla range języka Go jest zastosowana do łańcu-
cha znaków, wykonuje pośrednie dekodowanie UTF-8. Dane wyjściowe z przedstawionej poniżej
pętli pokazano również na rysunku 3.5. Należy zauważyć, że dla każdej runy niebędącej znakiem
ASCII indeks zwiększa o więcej niż l.
for i, r := range "Witaj, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
Moglibyśmy użyć prostej pętli range, aby policzyć runy w łańcuchu znaków:
n := 0
for _, _ = range s {
n++
}
Podobnie jak w przypadku innych form pętli range, możemy pominąć niepotrzebne zmienne:
n := 0
for range s {
n++
}
Możemy też po prostu wywołać funkcję utf8.RuneCountInString(s).
Wspomnieliśmy wcześniej, że jest to głównie kwestia konwencji w języku Go, że tekstowe łańcuchy
znaków są interpretowane jako zakodowane w UTF-8 sekwencje punktów kodowych Unicode,
ale w przypadku prawidłowego korzystania z pętli range na łańcuchach to więcej niż konwencja
— to konieczność. Co się stanie, jeśli wykonamy pętlę range na łańcuchu znaków zawierającym
dowolne dane binarne lub dane UTF-8 zawierające błędy?
Za każdym razem, gdy dekoder UTF-8 (bezpośrednio w wywołaniu funkcji utf8.DecodeRuneInString
lub pośrednio w pętli range) konsumuje niespodziewany bajt wejściowy, generuje specjalny
znak zastępczy Unicode, '\uFFFD', który jest zwykle wyświetlany jako biały znak zapytania
wewnątrz czarnego sześciokątnego lub przypominającego romb pola: �. Kiedy program napotka
82 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

tę wartość runiczną, oznacza to często, że jakaś nadrzędna część systemu, która wygenerowała te
dane w postaci łańcucha znaków, niewłaściwie obsłużyła kodowanie tekstu.
Kodowanie UTF-8 jest wyjątkowo wygodne jako format wymiany, ale w programie runy mogą
być wygodniejsze, ponieważ są jednakowej wielkości, więc można je łatwo indeksować w tablicach
i wycinkach.
Konwersja []rune zastosowana do łańcucha znaków zakodowanego w UTF-8 zwraca sekwencję
punktów kodowych Unicode, które ten łańcuch koduje:
// Słowo "program" w japońskiej katakanie.
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"
(Czasownik %x w pierwszej funkcji Printf wstawia spację między każdą parą cyfr szesnastkowych).
Jeśli wycinek run jest konwertowany na łańcuch znaków, generuje konkatenację kodowań UTF-8
każdej runy:
fmt.Println(string(r)) // "プログラム"
Konwersja wartości liczby całkowitej na łańcuch znaków interpretuje liczbę całkowitą jako wartość
runiczną i otrzymujemy reprezentację UTF-8 tej runy:
fmt.Println(string(65)) // "A", a nie "65"
fmt.Println(string(0x4eac)) // "京"
Jeśli runa jest nieprawidłowa, podstawiany jest znak zastępczy:
fmt.Println(string(1234567)) // "�"

3.5.4. Łańcuchy znaków i wycinki bajtów


W manipulowaniu łańcuchami znaków szczególnie istotne są cztery standardowe pakiety: bytes,
strings, strconv i unicode. Pakiet strings zapewnia wiele funkcji do wyszukiwania, podsta-
wiania, porównywania, przycinania, dzielenia i łączenia łańcuchów znaków.
Pakiet bytes zawiera podobne funkcje służące do manipulowania wycinkami bajtów, typu [ ]byte,
które mają pewne wspólne właściwości z łańcuchami znaków. Ponieważ łańcuchy są niemutowalne,
budowanie łańcuchów inkrementacyjnie może wymagać sporo alokowania i kopiowania. W ta-
kich przypadkach bardziej efektywne jest użycie typu bytes.Buffer, który pokażemy za chwilę.
Pakiet strconv zapewnia funkcje do konwersji wartości logicznych, liczb całkowitych i liczb
zmiennoprzecinkowych na ich reprezentację łańcuchową i odwrotnie oraz funkcje do cytowania
łańcuchów znaków.
Pakiet unicode zapewnia funkcje do klasyfikowania run, takie jak: IsDigit, IsLetter, IsUpper
oraz IsLower. Każda funkcja przyjmuje pojedynczy argument runiczny i zwraca wartość logiczną.
Funkcje konwersji, takie jak ToLower (na małą literę) i ToUpper (na wielką literę), konwertują
runę na daną wielkość, jeśli jest to litera. Wszystkie te funkcje stosują standardowe kategorie Uni-
code dla liter, cyfr itd. Pakiet strings ma podobne funkcje, również zwane ToUpper i ToLower,
które zwracają nowy łańcuch z określoną transformacją zastosowaną do każdego znaku oryginal-
nego łańcucha.
3.5. ŁAŃCUCHY ZNAKÓW 83

Poniższa funkcja basename została zainspirowana narzędziem powłoki uniksowej o tej samej nazwie.
W naszej wersji basename(s) usuwa wszystkie prefiksy s, które wyglądają jak ścieżka systemu plików
z elementami oddzielonymi ukośnikami, oraz wszelkie przyrostki wyglądające jak typ pliku:
fmt.Println(basename("a/b/c.go")) // "c"
fmt.Println(basename("c.d.go")) // "c.d"
fmt.Println(basename("abc")) // "abc"
Pierwsza wersja basename wykonuje całą pracę bez pomocy bibliotek:
code/r03/basename1
// basename usuwa komponenty ścieżki katalogu oraz przyrostek po kropce,
// np.: a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c.
func basename(s string) string {
// Porzuca ostatni znak '/' i wszystko, co znajduje się przed nim.
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '/' {
s = s[i+1:]
break
}
}
// Zachowuje wszystko przed ostatnim znakiem '.'.
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '.' {
s = s[:i]
break
}
}
return s
}
Prostsza wersja używa funkcji biblioteki strings.LastIndex:
code/r03/basename2
func basename(s string) string {
slash := strings.LastIndex(s, "/") // –1, jeśli nie znaleziono "/"
s = s[slash+1:]
if dot := strings.LastIndex(s, "."); dot >= 0 {
s = s[:dot]
}
return s
}
Pakiety path i path/filepath zapewniają bardziej ogólny zestaw funkcji do manipulowania hie-
rarchicznymi nazwami. Pakiet path działa ze ścieżkami rozdzielanymi ukośnikami na dowolnej
platformie. Nie powinien być używany do nazw plików, ale jest właściwy dla innych dziedzin,
takich jak komponent ścieżki adresu URL. Natomiast pakiet path/filepath manipuluje nazwami
plików, wykorzystując reguły platformy hosta, np. /foo/bar dla POSIX lub c:\foo\bar w syste-
mach Microsoft Windows.
Przejdźmy do kolejnego przykładu podłańcucha. Zadanie polega na tym, aby w łańcuchowej re-
prezentacji liczby całkowitej, takiej jak "12345", wstawić przecinki co trzy miejsca, np. "12,345".
Ta wersja działa tylko dla liczb całkowitych. Obsługę liczb zmiennoprzecinkowych pozostawiamy
jako ćwiczenie.
code/r03/comma
// Comma wstawia przecinki w łańcuchu nieujemnej dziesiętnej liczby całkowitej.
func comma(s string) string {
84 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

n := len(s)
if n <= 3 {
return s
}
return comma(s[:n-3]) + "," + s[n-3:]
}
Argumentem dla funkcji comma jest łańcuch znaków. Jeśli jego długość jest mniejsza niż lub
równa 3, nie jest potrzebny żaden przecinek. W przeciwnym razie funkcja comma wywołuje reku-
rencyjnie samą siebie z podłańcuchem obejmującym wszystkie znaki oprócz ostatnich trzech
i dopisuje do wyniku wywołania rekurencyjnego przecinek i trzy ostatnie znaki.
Łańcuch zawiera tablicę bajtów, która po utworzeniu jest niemutowalna. W przeciwieństwie do
tego elementy wycinka bajtów można dowolnie modyfikować.
Łańcuchy mogą być konwertowane na wycinki bajtów, a następnie z powrotem na łańcuchy:
s := "abc"
b := []byte(s)
s2 := string(b)
Zgodnie z koncepcją konwersja []byte(s) alokuje nową tablicę bajtów przechowującą kopię
bajtów zmiennej s i daje wycinek, który odwołuje się do całości tej tablicy. Kompilator optyma-
lizujący może być w stanie uniknąć alokowania i kopiowania w niektórych przypadkach, ale za-
sadniczo kopiowanie jest wymagane w celu zapewnienia, że bajty zmiennej s pozostaną nie-
zmienione, nawet gdy bajty zmiennej b będą później modyfikowane. Konwersja z wycinka bajtów
z powrotem na łańcuch znaków za pomocą string(b) również tworzy kopię, aby zapewnić
niemutowalność powstałego w ten sposób łańcucha s2.
Aby uniknąć konwersji i niepotrzebnych alokacji pamięci, wiele funkcji narzędziowych z pakietu
bytes ma swoje bezpośrednie odpowiedniki w pakiecie strings. Oto np. sześć funkcji z pakietu
strings:
func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string
A to odpowiadające im funkcje z pakietu bytes:
func Contains(b, subslice []byte) bool
func Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool
func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte
Jedyną różnicą jest to, że łańcuchy znaków zostały zastąpione przez wycinki bajtów.
Pakiet bytes zapewnia typ Buffer do efektywnej manipulacji wycinkami bajtów. Na początku
Buffer jest pusty, ale rośnie wraz z zapisywaniem w nim typów danych, takich jak: string, byte
oraz []byte. Jak pokazuje poniższy przykład, zmienna bytes.Buffer nie wymaga inicjowania,
ponieważ jej wartość zerowa nadaje się do użytku:
code/r03/printints
// intsToString jest taka sama jak fmt.Sprintf(values), ale dodaje przecinki.
func intsToString(values []int) string {
3.5. ŁAŃCUCHY ZNAKÓW 85

var buf bytes.Buffer


buf.WriteByte('[')
for i, v := range values {
if i > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "%d", v)
}
buf.WriteByte(']')
return buf.String()
}

func main() {
fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]"
}
Podczas dopisywania do zmiennej bytes.Buffer kodowania UTF-8 dowolnej runy najlepiej jest
użyć metody WriteRune typu bytes.Buffer, ale metoda WriteByte nadaje się do znaków ASCII
takich jak '[' i ']'.
Typ bytes.Buffer jest niezwykle uniwersalny i podczas omawiania interfejsów w rozdziale 7.
zobaczymy, w jaki sposób może być wykorzystywany jako zamiennik pliku za każdym razem,
gdy funkcja „we-wy” wymaga ujścia dla bajtów (io.Writer), jak funkcja Fprintf powyżej, lub
źródła bajtów (io.Reader).
Ćwiczenie 3.10. Napisz nierekurencyjną wersję funkcji comma, wykorzystując typ bytes.Buffer
zamiast konkatenacji łańcuchów znaków.
Ćwiczenie 3.11. Popraw funkcję comma, aby poprawnie radziła sobie z liczbami zmiennoprze-
cinkowymi i opcjonalnym znakiem.
Ćwiczenie 3.12. Napisz funkcję, która raportuje, czy dwa łańcuchy znaków są wzajemnymi ana-
gramami, czyli zawierają te same litery w odwrotnej kolejności.

3.5.5. Konwersje między łańcuchami znaków i liczbami


Oprócz konwersji pomiędzy łańcuchami znaków, runami i bajtami często niezbędna jest konwersja
pomiędzy wartościami liczbowymi i ich reprezentacjami łańcuchowymi. Odbywa się to za pomocą
funkcji z pakietu strconv.
Jedną z opcji przekonwertowania liczby całkowitej na łańcuch znaków jest użycie funkcji
fmt.Sprintf. Inną jest użycie funkcji strconv.Itoa („liczba całkowita na ASCII”):
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
Funkcje FormatInt i FormatUint mogą być wykorzystywane do przeformatowywania liczb na inne
podstawy systemu liczbowego:
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
Czasowniki %b, %d, %u oraz x% funkcji fmt.Printf są często bardziej wygodne niż funkcje
Format, zwłaszcza jeśli chcemy dołączyć dodatkowe informacje oprócz samej liczby:
s := fmt.Sprintf("x=%b", x) // "x=1111011"
86 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

Do parsowania łańcucha znaków reprezentującego liczbę całkowitą należy użyć funkcji Atoi lub
ParseInt pakietu strconv, albo ParseUint dla liczb całkowitych bez znaku:
x, err := strconv.Atoi("123") // x jest typem int
y, err := strconv.ParseInt("123", 10, 64) // podstawa 10, do 64 bitów
Trzeci argument funkcji ParseInt podaje rozmiar typu int, w którym musi się zmieścić wynik,
np. 16 oznacza int16, a specjalna wartość 0 oznacza int. W każdym z przypadków typem wyniku y
jest zawsze int64, który można następnie przekonwertować na mniejszy typ.
Czasami funkcja fmt.Scanf jest przydatna do parsowania danych wejściowych składających się
z uporządkowanych mieszanin łańcuchów znaków i liczb umieszczonych w jednej linii, ale może być
nieelastyczna, szczególnie w przypadku niekompletnych lub nieregularnych danych wejściowych.

3.6. Stałe
Stałe są wyrażeniami, których wartość jest znana kompilatorowi i których ewaluacja jest gwaran-
towana podczas kompilacji, a nie w czasie wykonywania programu. Bazowym typem każdej stałej
jest typ podstawowy: wartość logiczna, łańcuch znaków lub liczba.
Deklaracja const definiuje wartości nazwane wyglądające składniowo jak zmienne, ale których
wartość jest stała, co zapobiega dokonywaniu przypadkowych (lub złośliwych) zmian w czasie
wykonywania programu. Stała jest bardziej odpowiednia niż zmienna np. dla stałej matematycznej,
takiej jak pi, ponieważ jej wartość się nie zmienia:
const pi = 3.14159 // przybliżenie; lepszym przybliżeniem jest math.Pi
Podobnie jak w przypadku zmiennych, sekwencje stałych mogą się pojawiać w jednej deklaracji. Może
to być odpowiednie dla grupy wartości powiązanych:
const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
Wiele obliczeń wykonywanych na stałych może być całkowicie ewaluowanych w czasie kompilacji,
redukując ilość pracy niezbędnej podczas wykonywania programu i umożliwiając inne optyma-
lizacje kompilatora. Błędy zwykle wykrywane w czasie wykonywania mogą być zgłaszane w czasie
kompilacji, gdy ich operandami są stałe. Mogą to być błędy takie jak dzielenie liczby całkowitej
przez zero, łańcuch indeksowany poza zakresem oraz wszelkie operacje zmiennoprzecinkowe,
których wynikiem jest wartość nieokreślona.
Wyniki wszystkich operacji arytmetycznych, logicznych i porównania zastosowane do operandów
stałej same są stałymi, tak jak są nimi wyniki konwersji i wywołań niektórych wbudowanych funkcji,
do których należą np.: len, cap, real, imag, complex oraz unsafe.Sizeof (zob. podrozdział 13.1).
Ponieważ ich wartości są znane kompilatorowi, wyrażenia stałych mogą występować w typach,
szczególnie jako długość typu tablicowego:
const IPv4Len = 4

// parseIPv4 parsuje adres IPv4 (d.d.d.d).


func parseIPv4(s string) IP {
var p [IPv4Len]byte
// …
}
3.6. STAŁE 87

Deklaracja stałej może określać zarówno typ, jak i wartość, ale w przypadku braku wyraźnego
typu jest on wnioskowany z wyrażenia po prawej stronie. W poniższym przykładzie time.Duration
jest typem nazwanym, którego typem bazowym jest int64, a time.Minute jest stałą tego typu.
Dlatego obie z zadeklarowanych poniżej stałych mają również typ time.Duration, co pokazuje %T:
const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"
Gdy sekwencja stałych jest zadeklarowana jako grupa, wyrażenie po prawej stronie może być
pominięte dla wszystkich stałych poza pierwszą stałą w grupie, co oznacza, że poprzednie wyra-
żenie i jego typ powinny być użyte ponownie, np.:
const (
a = 1
b
c = 2
d
)

fmt.Println(a, b, c, d) // "1 1 2 2"


Nie jest to zbyt przydatne, jeżeli domyślnie skopiowane wyrażenie z prawej strony zawsze ewaluuje
do tej samej wartości. Co jednak, gdyby ta wartość mogła się zmieniać? Prowadzi nas to do generato-
ra stałych iota.

3.6.1. Generator stałych iota


Deklaracja const może wykorzystywać generator stałych iota, który jest używany do tworzenia
sekwencji powiązanych wartości bez bezpośredniego precyzowania każdej z nich. W deklaracji const
wartość iota zaczyna się od zera i jest zwiększana o jeden dla każdego elementu w sekwencji.
Oto przykład z pakietu time, który definiuje stałe nazwane typu Weekday dla dni tygodnia, począwszy
od zera dla niedzieli (Sunday). Typy tego rodzaju są często nazywane enumeracjami.
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
Ten kod deklaruje, że Sunday to 0, Monday 1 itd.
Możemy użyć generatora iota również w bardziej złożonych wyrażeniach, tak jak w tym przykładzie
z pakietu net, w którym każdemu z pięciu najmłodszych bitów liczby całkowitej bez znaku nadawa-
na jest odrębna nazwa i logiczna interpretacja:
type Flags uint
const (
FlagUp Flags = 1 << iota // jest włączony
88 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

FlagBroadcast // obsługuje dostęp broadcastowy


FlagLoopback // jest interfejsem pętli zwrotnej
FlagPointToPoint // należy do łącza punkt-punkt
FlagMulticast // obsługuje dostęp multicastowy
)
W inkrementacjach generatora iota każdej stałej przypisywana jest wartość wyrażenia 1 << iota
ewaluująca do kolejnych potęg liczby 2, z których każda odpowiada pojedynczemu bitowi. Mo-
żemy użyć tych stałych w ramach funkcji, która testuje, ustawia albo czyści jeden lub kilka spośród
bitów:
code/r03/netflag
func IsUp(v Flags) bool { return v&FlagUp == FlagUp }
func TurnDown(v *Flags) { *v &^= FlagUp }
func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 }

func main() {
var v Flags = FlagMulticast | FlagUp
fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true"
TurnDown(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false"
SetBroadcast(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false"
fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true"
}
Ta deklaracja, jako bardziej złożony przykład generatora iota, nazywa potęgi liczby 1024:
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (przekracza 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (przekracza 1 << 64)
YiB // 1208925819614629174706176
)
Mechanizm iota ma swoje granice. Niemożliwe jest np. wygenerowanie bardziej znajomych potęg
liczby 1000 (kB, MB itd.), ponieważ nie ma operatora potęgowania.
Ćwiczenie 3.13. Napisz najzwięźlej jak możesz deklarację const dla kB, MB, aż do YB.

3.6.2. Stałe nietypowane


Stałe w języku Go są nieco niezwykłe. Chociaż stała może mieć dowolny z podstawowych ty-
pów danych, takich jak int lub float64, włączając w to nazwane typy podstawowe, takie jak
time.Duration, wiele stałych nie jest przydzielonych do konkretnego typu. Kompilator reprezentuje
te nieprzydzielone stałe ze znacznie większą dokładnością liczbową niż wartości podstawowych
typów, a ich arytmetyka jest bardziej dokładna niż arytmetyka maszyn. Można przyjąć co najmniej
256-bitową dokładność. Istnieje sześć odmian tych niepowiązanych stałych, które nazywają się
następująco: nietypowana wartość logiczna, nietypowana liczba całkowita, nietypowana runa,
nietypowana liczba zmiennoprzecinkowa, nietypowana liczba zespolona oraz nietypowany łańcuch
znaków.
3.6. STAŁE 89

Dzięki powstrzymaniu się od tego przydzielania nietypowane stałe nie tylko zachowują do póź-
niejszego wykorzystania swoją większą dokładność, ale mogą również bez konieczności konwersji
być używane w znacznie większej liczbie wyrażeń niż stałe powiązane z typem. Wartości ZiB i YiB
z poprzedniego przykładu są zbyt duże do zapisania w jakiejkolwiek zmiennej liczby całkowitej,
ale są prawidłowymi stałymi, które można np. wykorzystać w takim wyrażeniu:
fmt.Println(YiB/ZiB) // "1024"
Innym przykładem jest możliwość użycia stałej zmiennoprzecinkowej math.Pi wszędzie tam,
gdzie wymagana jest jakakolwiek wartość zmiennoprzecinkowa lub zespolona:
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
Jeśli stała math.Pi zostałaby przydzielona do określonego typu, takiego jak float64, wynik nie
byłby tak precyzyjny i niezbędne byłyby konwersje typów, aby użyć tej stałej, gdy wymagana
byłaby wartość float32 lub complex128:
const Pi64 float64 = math.Pi

var x float32 = float32(Pi64)


var y float64 = Pi64
var z complex128 = complex128(Pi64)
W przypadku literałów ich rodzaj determinuje składnia. Literały 0, 0.0, 0i oraz '\u0000' ozna-
czają stałą o tej samej wartości, ale innego rodzaju, odpowiednio: nietypowaną liczbę całkowitą,
nietypowaną liczbę zmiennoprzecinkową, nietypowaną liczbę zespoloną oraz nietypowaną runę
bez typu. Podobnie true i false są nietypowanymi wartościami logicznymi, a literały łańcuchów
znaków są nietypowanymi łańcuchami.
Przypomnijmy, że znak / może reprezentować dzielenie liczb całkowitych lub zmiennoprzecin-
kowych, w zależności od jego operandów. W konsekwencji wybór literału może wpływać na wynik
wyrażenia dzielenia stałej:
var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // "100"; (f – 32) * 5 to typ float64
fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 to nietypowana liczba całkowita, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 to nietypowana liczba zmiennoprzecinkowa
Tylko stałe mogą być nietypowane. Gdy nietypowana stała zostanie przypisana do zmiennej (tak jak
w pierwszej z poniższych deklaracji) lub występuje po prawej stronie definicji zmiennej z wyraźnie
określonym typem (tak jak w pozostałych trzech przypisaniach), jest pośrednio konwertowana
na typ danej zmiennej, jeśli jest to możliwe.
var f float64 = 3 + 0i // nietypowana liczba zespolona -> float64
f = 2 // nietypowana liczba całkowita -> float64
f = 1e123 // nietypowana liczba zmiennoprzecinkowa -> float64
f = 'a' // nietypowana runa -> float64
Powyższe instrukcje są zatem równoważne z tymi:
var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')
Bez względu na to, czy bezpośrednia czy pośrednia, konwersja stałej z jednego typu na inny wymaga,
aby typ docelowy mógł reprezentować oryginalną wartość. Zaokrąglanie jest dozwolone dla rze-
czywistych i zespolonych liczb zmiennoprzecinkowych:
90 ROZDZIAŁ 3. PODSTAWOWE TYPY DANYCH

const (
deadbeef = 0xdeadbeef // nietypowany int z wartością 3735928559
a = uint32(deadbeef) // uint32 z wartością 3735928559
b = float32(deadbeef) // float32 z wartością 3735928576 (zaokrągloną w górę)
c = float64(deadbeef) // float64 z wartością 3735928559 (dokładną)
d = int32(deadbeef) // błąd kompilacji: stała przepełnia int32
e = float64(1e309) // błąd kompilacji: stała przepełnia float64
f = uint(-1) // błąd kompilacji: stała niedopełnia uint
)
W deklaracji zmiennej bez wyraźnego typu (w tym w krótkich deklaracjach zmiennych) rodzaj
nietypowanej stałej pośrednio określa domyślny typ zmiennej, tak jak w tych przykładach:
i := 0 // nietypowana liczba całkowita; pośrednio int(0)
r : = '\000' // nietypowana runa; pośrednio rune('\000')
f := 0.0 // nietypowana liczba zmiennoprzecinkowa; pośrednio float64(0.0)
c := 0i // nietypowana liczba zespolona; pośrednio complex128(0i)
Należy zwrócić uwagę na asymetrię: nietypowane liczby całkowite są konwertowane na typ int,
którego rozmiar nie jest gwarantowany, ale nietypowane liczby zmiennoprzecinkowe i liczby zespo-
lone są konwertowane na typy float64 i complex128 z wyraźnie określonym rozmiarem. Ten język
nie ma typów float i complex z nieustalonym rozmiarem, analogicznych do typu int z nieustalo-
nym rozmiarem, ponieważ bardzo trudno jest napisać poprawne algorytmy liczbowe bez zna-
jomości rozmiaru swoich zmiennoprzecinkowych typów danych.
Aby nadać zmiennej inny typ, należy bezpośrednio przekonwertować nietypowaną stałą na żądany
typ lub podać żądany typ w deklaracji zmiennej, tak jak w tych przykładach:
var i = int8(0)
var i int8 = 0
Te domyślne konwersje są szczególnie istotne, gdy konwertujemy nietypowaną stałą na wartość
interfejsu (zob. rozdział 7.), ponieważ określają jej dynamiczny typ.
fmt.Printf("%T\n", 0) // "int"
fmt.Printf("%T\n", 0.0) // "float64"
fmt.Printf("%T\n", 0i) // "complex128"
fmt.Printf("%T\n", '\000' ) // "int32" (rune)
Omówiliśmy podstawowe typy danych języka Go. Następnym krokiem jest pokazanie, w jaki
sposób mogą one być łączone w większe grupy, takie jak tablice i struktury, a następnie w struktury
danych służące do rozwiązywania rzeczywistych problemów programowania. Jest to tematem
rozdziału 4.
Rozdział 4

Typy złożone

W rozdziale 3. omówione zostały podstawowe typy, które służą jako elementy składowe do struktur
danych w programie Go. Są to atomy naszego uniwersum. W tym rozdziale przyjrzymy się typom
złożonym, czyli zbitkom tworzonym przez łączenie na różne sposoby typów podstawowych. Omó-
wimy cztery takie typy (tablice, wycinki, mapy i struktury), a na końcu rozdziału pokażemy, jak
ustrukturyzowane dane wykorzystujące te typy mogą być kodowane jako dane JSON i parsowane
z formatu JSON oraz używane do generowania dokumentów HTML z szablonów.
Tablice i struktury są typami złożonymi. Ich wartości są konkatenacjami innych wartości prze-
chowywanych w pamięci. Tablice są jednorodne (wszystkie ich elementy mają ten sam typ), pod-
czas gdy struktury są heterogeniczne. Tablice i struktury mają stały rozmiar. Natomiast wycinki
i mapy są dynamicznymi strukturami danych, które rosną wraz z dodawaniem wartości.

4.1. Tablice
Tablica jest sekwencją o stałej długości zera lub większej liczby elementów określonego typu. Z po-
wodu stałej długości tablice są rzadko stosowane w języku Go bezpośrednio. Wycinki, które
mogą rozszerzać się i kurczyć, są dużo bardziej przydatne, aby jednak je zrozumieć, musimy naj-
pierw zrozumieć tablice.
Dostęp do poszczególnych elementów tablicy jest uzyskiwany za pomocą konwencjonalnej notacji
indeksowej, w której indeksy biegną od zera do wartości o jeden mniejszej niż długość tablicy.
Wbudowana funkcja len zwraca liczbę elementów w tablicy.
var a [3]int // tablica trzech liczb całkowitych
fmt.Println(a[0]) // wyświetla pierwszy element
fmt.Println(a[len(a)-1]) // wyświetla ostatni element, a[2]

// Wyświetla indeksy i elementy.


for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}

// Wyświetla tylko elementy.


for _, v := range a {
fmt.Printf("%d\n", v)
}
92 ROZDZIAŁ 4. TYPY ZŁOŻONE

Domyślnie elementy nowej zmiennej tablicy są początkowo ustawiane na wartość zerową dla
typu danego elementu, którą dla liczb jest 0. Możemy użyć literału tablicy, aby zainicjować tablicę
z listą wartości:
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
Jeśli w literale tablicy w miejscu długości pojawia się wielokropek (...), długość tablicy jest określana
przez liczbę inicjatorów. Definicję q można uprościć do następującej postaci:
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
Rozmiar tablicy jest częścią jej typu, więc [3]int oraz [4]int to różne typy. Rozmiar musi być wy-
rażeniem stałej, czyli wyrażeniem, którego wartość może być obliczona w trakcie kompilowania
programu.
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // błąd kompilacji: nie można przypisać [4]int do [3]int
Jak zobaczymy, składnia literału jest podobna dla tablic, wycinków, map i struktur. Szczególna
forma przedstawiona powyżej jest listą wartości w kolejności, ale jest również możliwe określenie
listy indeksu i par wartości w taki sposób:
type Currency int

const (
USD Currency = iota
EUR
GBP
RMB
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"


W tej postaci indeksy mogą występować w dowolnej kolejności, a niektóre z nich mogą być pomi-
nięte. Tak jak poprzednio niesprecyzowane wartości, przyjmują wartość zerową dla typu danego
elementu. Poniższy zapis definiuje np. tablicę r ze 100 elementami, z których wszystkie mają war-
tość zerową poza ostatnim o wartości -1:
r := [...]int{99: -1}
Jeśli typ elementu tablicy jest porównywalny, to typ tablicy również jest porównywalny, więc mo-
żemy bezpośrednio porównać dwie tablice tego typu za pomocą operatora ==, który raportuje, czy
wszystkie odpowiadające elementy są równe. Operator != jest jego negacją.
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // błąd kompilacji: nie można porównać [2]int == [3]int
Bardziej przekonującym przykładem może być funkcja Sum256 z pakietu crypto/sha256, która
generuje skrót kryptograficzny SHA256, czyli skrót komunikatu przechowywanego w dowolnym
wycinku bajtów. To streszczenie ma 256 bitów, więc jego typem jest [32]byte. Jeśli dwa skróty
są takie same, jest bardzo prawdopodobne, że te dwa komunikaty są takie same. Jeśli skróty się
różnią, oba komunikaty są różne. Ten program wyświetla i porównuje skróty SHA256 dla "x" i "X":
4.1. TABLICE 93

code/r04/sha256
import "crypto/sha256"

func main() {
c1 := sha256.Sum256([]byte("x"))
c2 := sha256.Sum256([]byte("X"))
fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1)
// Output:
// 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
// 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
// false
// [32]uint8
}
Dane wejściowe różnią się tylko jednym bitem, ale w skrótach różni się w przybliżeniu połowa
bitów. Należy zwrócić uwagę na czasowniki funkcji Printf: %x do wyświetlania wszystkich ele-
mentów tablicy lub wycinka bajtów w postaci szesnastkowej, %t do wyświetlania wartości logicznej
oraz %T do wyświetlania typu wartości.
Kiedy wywoływana jest funkcja, kopia każdej wartości argumentu jest przypisywana do odpo-
wiedniej zmiennej parametru, więc funkcja otrzymuje kopię, a nie oryginał. Przekazywanie
w ten sposób dużych tablic może być nieefektywne, a wszelkie zmiany wprowadzane przez funkcję
w elementach tablicy wpływają tylko na kopię, a nie na oryginał. Pod tym względem Go traktuje
tablice jak każdy inny typ, ale to zachowanie różni się od zachowania innych języków, które po-
średnio przekazują tablice przez referencję.
Oczywiście możemy bezpośrednio przekazywać wskaźnik do tablicy, aby wszystkie modyfikacje
wprowadzane przez funkcję w elementach tablicy były widoczne dla podmiotu wywołującego.
Poniższa funkcja zeruje zawartość tablicy [32]byte:
func zero(ptr *[32]byte) {
for i := range ptr {
ptr[i] = 0
}
}
Literał tablicy [32]byte{} daje tablicę 32 bajtów. Każdy element tej tablicy ma wartość zerową typu
byte, która jest równa zero. Możemy wykorzystać ten fakt, aby napisać inną wersję funkcji zero:
func zero(ptr *[32]byte) {
*ptr = [32]byte{}
}
Użycie wskaźnika do tablicy jest efektywne i pozwala wywoływanej funkcji mutować zmienną
podmiotu wywołującego, jednak tablice pozostają z natury nieelastyczne ze względu na ich stały
rozmiar. Funkcja zero nie będzie np. akceptować wskaźnika do zmiennej [16]byte i nie ma żad-
nego sposobu dodania lub usunięcia elementów tablicy. Z tych powodów tablice są rzadko używa-
ne jako parametry funkcji, poza szczególnymi przypadkami, takimi jak stałego rozmiaru skrót
SHA256. Zamiast tego używamy wycinków.
Ćwiczenie 4.1. Napisz funkcję zliczającą liczbę bitów, które są różne w dwóch skrótach SHA256.
(Zob. funkcja PopCount w punkcie 2.6.2).
Ćwiczenie 4.2. Napisz program, który domyślnie wyświetla skrót SHA256 swojego standardowego
strumienia wejściowego, ale obsługuje flagę wiersza poleceń do wyświetlania zamiast tego skrótu
SHA384 lub SHA512.
94 ROZDZIAŁ 4. TYPY ZŁOŻONE

4.2. Wycinki
Wycinki reprezentują sekwencje o zmiennej długości, których wszystkie elementy mają ten sam typ.
Typ wycinka jest zapisywany jako []T, gdzie elementy mają typ T. Wygląda to jak typ tablicowy bez
rozmiaru.
Tablice i wycinki są ze sobą ściśle powiązane. Wycinek jest lekką strukturą danych, dającą dostęp
do podsekwencji (albo do wszystkich) elementów tablicy, która jest określana jako bazowa ta-
blica wycinka. Wycinek ma trzy komponenty: wskaźnik, długość i pojemność. Wskaźnik wskazuje
pierwszy z elementów tablicy osiągalny poprzez dany wycinek, ale nie jest to koniecznie pierwszy
w kolejności element tej tablicy. Długość jest liczbą elementów wycinka i nie może przekraczać
jego pojemności, która jest zwykle liczbą elementów pomiędzy początkiem wycinka a końcem
bazowej tablicy. Te wartości zwracają wbudowane funkcje len (długość) i cap (pojemność).
Wiele wycinków może współdzielić tę samą tablicę bazową i odwoływać się do zachodzących na
siebie części tej tablicy. Rysunek 4.1 pokazuje tablicę łańcuchów znaków dla miesięcy w roku oraz
dwa nakładające się wycinki tej tablicy. Tablica jest deklarowana jako:
months := [...]string{1: "Styczeń", /* ... */, 12: "Grudzień"}

Rysunek 4.1. Dwa nakładające się wycinki tablicy miesięcy

Tak więc styczeń to months[1], a grudzień to months[12]. Zwykle element tablicy o indeksie 0 zawie-
rałby pierwszą wartość, ponieważ jednak miesiące są zawsze numerowane od 1, możemy ten
element zostawić poza deklaracją i będzie on inicjowany jako pusty łańcuch znaków.
4.2. WYCINKI 95

Operator wycinka s[i:j], gdzie 0 ≤ i ≤ j ≤ cap(s), tworzy nowy wycinek odwołujący się do ele-
mentów od i do j-1 sekwencji s, która może być zmienną tablicy, wskaźnikiem do tablicy lub
innym wycinkiem. Powstały wycinek ma j-i elementów. Jeśli pominięte zostało i, to i wynosi 0,
a jeśli pominięte zostało j, to j wynosi len(s). Tak więc wycinek months[1:13] odnosi się do ca-
łego zakresu prawidłowych miesięcy, tak samo jak wycinek [1:]. Wycinek months[:] odwołuje
się do całej tablicy. Zdefiniujmy nakładające się wycinki dla drugiego kwartału i lata na półkuli
północnej:
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) // ["Kwiecień" "Maj" "Czerwiec"]
fmt.Println(summer) // ["Czerwiec" "Lipiec" "Sierpień"]
Czerwiec jest włączony do każdego z tych wycinków i jest jedynym wynikiem tego (nieefektywnego)
testu dla wspólnych elementów:
for _, s := range summer {
for _, q := range Q2 {
if s == q {
fmt.Printf("%s pojawia się w obu\n", s)
}
}
}
Określanie zakresu wycinka wykraczającego poza cap(s) wywołuje procedurę panic, ale określanie
zakresu wykraczającego poza len(s) rozszerza dany wycinek, więc wynik może być dłuższy niż
oryginał:
fmt.Println(summer[:20]) // panic: poza zakresem

endlessSummer := summer[:5] // rozszerza wycinek (w obrębie pojemności)


fmt.Println(endlessSummer) // "[Czerwiec Lipiec Sierpień Wrzesień Październik]"
Na marginesie: należy zwrócić uwagę na podobieństwo operacji podłańcucha wykonywanej na
łańcuchach znaków do operatora wycinka operującego na wycinkach []byte. Obie są zapisywane jako
x[m:n] i zwracają podłańcuch oryginalnych bajtów, współdzieląc bazową reprezentację, więc czas
trwania obu tych operacji jest stały. Wyrażenie x[m:n] daje łańcuch znaków, jeśli x jest łańcuchem
znaków, lub []byte, jeśli x jest typem []byte.
Ponieważ wycinek zawiera wskaźnik do elementu tablicy, przekazywanie wycinka do funkcji
pozwala jej modyfikować elementy bazowej tablicy. Innymi słowy: kopiowanie wycinka tworzy
alias (zob. punkt 2.3.2) dla tablicy bazowej. Funkcja reverse odwraca in situ kolejność elementów
wycinka []int i może być stosowana do wycinków dowolnej długości.
code/r04/rev
// reverse odwraca in situ kolejność elementów wycinka []int.
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
Tutaj odwrócimy całą tablicę a:
a := [...]int{0, 1, 2, 3, 4, 5}
reverse(a[:])
fmt.Println(a) // "[5 4 3 2 1 0]"
96 ROZDZIAŁ 4. TYPY ZŁOŻONE

Prostym sposobem obrócenia wycinka w lewo o n elementów jest zastosowanie funkcji reverse
trzy razy: najpierw do pierwszych n elementów, następnie do pozostałych elementów, a na końcu
do całego wycinka. (Aby obrócić w prawo, najpierw wykonaj trzecie wywołanie funkcji).
s := []int{0, 1, 2, 3, 4, 5}
// Obrócenie wycinka s w lewo o dwie pozycje.
reverse(s[:2])
reverse(s[2:])
reverse(s)
fmt.Println(s) // "[2 3 4 5 0 1]"
Należy zwrócić uwagę, w jaki sposób wyrażenie inicjujące wycinek s różni się od wyrażenia inicjują-
cego tablicę a. Literał wycinka wygląda tak jak literał tablicy, czyli jest sekwencją wartości od-
dzielonych przecinkami i otoczonych klamrami, ale nie jest podany rozmiar. To wszystko pośred-
nio tworzy zmienną tablicową właściwego rozmiaru i daje wycinek, który na nią wskazuje. Tak
jak w przypadku literałów tablic, literały wycinków mogą określać wartości w kolejności, podawać
bezpośrednio ich indeksy lub używać kombinacji tych dwóch stylów.
W przeciwieństwie do tablic wycinki nie są porównywalne, nie możemy więc użyć operatora ==
do przetestowania, czy dwa wycinki zawierają te same elementy. Standardowa biblioteka zapewnia
wysoce zoptymalizowaną funkcję bytes.Equal do porównywania dwóch wycinków bajtów ([]byte),
ale dla innych typów wycinków musimy sami przeprowadzić porównanie:
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
}
Biorąc pod uwagę, jak naturalny jest ten „głęboki” test równości, oraz to, że nie jest bardziej kosz-
towny w czasie wykonywania programu niż operator == dla tablic łańcuchów znaków, może być
zastanawiające, iż porównania wycinków również nie działają w ten sposób. Istnieją dwa powody,
dla których głęboka równoważność jest problematyczna. Po pierwsze, w przeciwieństwie do
elementów tablicy, elementy wycinka są pośrednie, przez co możliwe jest, aby wycinek zawierał
samego siebie. Chociaż istnieją sposoby radzenia sobie z takimi przypadkami, żaden z nich nie
jest prosty, efektywny i, co najważniejsze, oczywisty.
Po drugie, ponieważ elementy wycinka są pośrednie, ustalona wartość wycinka może na różnych
etapach zawierać różne elementy, gdy zawartość bazowej tablicy będzie modyfikowana. Ponie-
waż tablica mieszająca, taka jak typ mapy języka Go, tworzy tylko płytkie kopie swoich kluczy,
wymagane jest, aby równość dla każdego klucza pozostawała taka sama przez cały czas życia ta-
blicy mieszającej. Dlatego głębokie równoważności sprawiłyby, że wycinki nie nadawałyby się
do wykorzystania jako klucze mapy. Dla typów referencyjnych, takich jak wskaźniki i kanały,
operator == testuje tożsamość referencji (ang. reference identity), czyli to, czy dwie encje odwołują
się do tej samej rzeczy. Analogiczny test „płytkiej” równości dla wycinków mógłby być użytecz-
ny i rozwiązać problem z mapami, ale niespójne traktowanie wycinków i tablic przez operator
== byłoby mylące. Najbezpieczniejszym wyborem jest całkowite uniemożliwienie porównywania
wycinków.
4.2. WYCINKI 97

Jedynym prawidłowym porównaniem wycinków jest porównanie względem wartości nil, tak jak
w tym wyrażeniu:
if summer == nil { /* ... */ }
Wartością zerową typu wycinka jest nil. Wycinek nil nie ma tablicy bazowej. Ma zerową długość
i pojemność, ale są również wycinki o zerowej długości i pojemności niebędące wycinkami nil,
takie jak []int{} lub make([]int, 3)[3:]. Jak w przypadku każdego typu, który może mieć
wartość nil, tę wartość dla konkretnego typu wycinka można zapisać za pomocą wyrażenia
konwersji, takiego jak []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
Jeśli więc potrzebujesz przetestować, czy wycinek jest pusty, użyj len(s) == 0, a nie s == nil.
Poza przypadkami porównywania do nil, wycinek nil zachowuje się jak każdy inny wycinek
o zerowej długości. Całkowicie bezpieczne jest np. reverse(nil). Jeśli nie zostało wyraźnie udo-
kumentowane inaczej, funkcje Go powinny traktować wszystkie wycinki o zerowej długości w ten
sam sposób, niezależnie od tego, czy są wycinkami nil, czy nie.
Wbudowana funkcja make tworzy wycinek o określonych: typie elementu, długości i pojemności.
Argument dotyczący pojemności może zostać pominięty i wtedy pojemność jest równa długości.
make([]T, len)
make([]T, len, cap) // to samo co make([]T, cap)[:len]
Pod maską funkcja make tworzy nienazwaną zmienną tablicową i zwraca jej wycinek. Dostęp do
tej tablicy jest możliwy tylko poprzez zwrócony wycinek. W pierwszej formie wycinek jest obrazem
całej tablicy. W drugiej wycinek jest obrazem tylko pierwszych elementów len tablicy, ale jego po-
jemność obejmuje całą tablicę. Dodatkowe elementy są przeznaczone na przyszłe powiększanie
wycinka.

4.2.1. Funkcja append


Wbudowana funkcja append dodaje elementy do wycinków:
var runes []rune
for _, r := range "Witaj, 世界" {
runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['W' 'i' 't' 'a' 'j' ',' ' ' '世' '界']"
Ta pętla wykorzystuje funkcję append do zbudowania wycinka dziewięciu run zakodowanych
poprzez literał łańcucha znaków, chociaż ten konkretny problem można rozwiązać wygodniej,
korzystając z wbudowanej konwersji []rune("Witaj, 世界").
Funkcja append ma kluczowe znaczenie dla zrozumienia sposobu działania wycinków, rzućmy
więc okiem na to, co się w niej dzieje. Oto wersja o nazwie appendInt, która jest wyspecjalizowana
dla wycinków []int:
code/r04/append
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
98 ROZDZIAŁ 4. TYPY ZŁOŻONE

// Jest miejsce na powiększanie. Rozszerzamy wycinek.


z = x[:zlen]
} else {
// Nie ma wystarczającej ilości miejsca. Alokujemy nową tablicę.
// Powiększanie przez podwajanie dla zamortyzowanej złożoności liniowej.
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // funkcja wbudowana; została opisana poniżej
}
z[len(x)] = y
return z
}
Każde wywołanie funkcji appendInt musi sprawdzić, czy wycinek ma wystarczającą pojemność
do przechowywania nowych elementów w istniejącej tablicy. Jeśli tak, funkcja rozszerza wycinek, de-
finiując większy wycinek (nadal w ramach oryginalnej tablicy), kopiuje element y do nowej prze-
strzeni i zwraca ten wycinek. Wejściowe x i wynikowe z współdzielą tę samą tablicę bazową.
Jeśli jest za mało miejsca na powiększanie, funkcja appendInt musi alokować nową tablicę (wy-
starczająco dużą, aby pomieścić wynik), skopiować do niej wartości z x, a następnie dołączyć
nowy element y. Wynik z odwołuje się teraz do innej tablicy bazowej niż tablica, do której odwo-
łuje się x.
Dość łatwo byłoby skopiować elementy za pomocą wyraźnych pętli, ale prościej jest użyć wbudo-
wanej funkcji copy, która kopiuje elementy z jednego wycinka do drugiego wycinka tego samego
typu. Pierwszym argumentem tej funkcji jest miejsce docelowe, a drugim źródło, co przypomina
kolejność operandów w przypisaniu takim jak dst = src. Wycinki mogą się odwoływać do tej samej
tablicy bazowej. Mogą się nawet częściowo pokrywać. Chociaż nie używamy tego tutaj, funkcja
copy zwraca liczbę faktycznie skopiowanych elementów, która to liczba jest wartością mniejszej
z dwóch długości wycinków, więc nie ma niebezpieczeństwa dociągnięcia do końca lub nadpisania
czegoś poza zakresem.
Dla efektywności nowa tablica jest zwykle nieco większa niż minimum niezbędne do przechowy-
wania x i y. Rozszerzenie tablicy poprzez podwajanie za każdym razem jej wielkości pozwala uniknąć
nadmiernej liczby alokacji i zapewnia, że dodawanie pojedynczego elementu będzie w ujęciu
uśrednionym zajmować stałą ilość czasu. Ten program przedstawia efekt:
func main() {
var x, y []int
for i := 0; i < 10; i++ {
y = appendInt(x, i)
fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
x = y
}
}
Każda zmiana pojemności oznacza alokację i kopię:
0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
4.2. WYCINKI 99

7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
Przyjrzyjmy się bliżej iteracji i=3. Wycinek x zawiera trzy elementy [0 1 2], ale ma pojemność 4,
więc jest miejsce na pojedynczy element na końcu, a funkcja appendInt elementu 3 może konty-
nuować bez ponownej alokacji. Powstały wycinek y ma długość i pojemność 4 i tę samą tablicę
bazową co pierwotny wycinek x, tak jak pokazano na rysunku 4.2.

Rysunek 4.2. Dołączanie przy wystarczającej ilości miejsca na powiększanie

W kolejnej iteracji (i=4) nie ma żadnego wolnego miejsca, więc funkcja appendInt alokuje nową
tablicę o rozmiarze 8, kopiuje cztery elementy [0 1 2 3] wycinka x i dołącza 4, czyli wartość i.
Powstały wycinek y ma długość 5, ale pojemność 8. Trzy wolne miejsca pozwolą uniknąć koniecz-
ności ponownej alokacji przez trzy kolejne iteracje. Wycinki x i y są obrazami różnych tablic. Ta
operacja została przedstawiona na rysunku 4.3.

Rysunek 4.3. Dołączanie przy braku miejsca na powiększanie

Wbudowana funkcja append może wykorzystywać bardziej zaawansowane strategie powiększa-


nia niż uproszczona strategia appendInt. Zazwyczaj nie wiemy, czy dane wywołanie funkcji append
spowoduje ponowną alokację, więc nie możemy zakładać, że oryginalny wycinek odwołuje się
do tej samej tablicy co powstały wycinek ani też, że odwołuje się do innej. Podobnie nie możemy
zakładać, że przypisania do elementów starego wycinka zostaną (lub nie) odzwierciedlone w nowym
wycinku. W związku z tym zwykle przypisuje się wynik wywołania funkcji append do tej samej
zmiennej wycinka, której wartość przekazaliśmy tej funkcji:
runes = append(runes, r)
100 ROZDZIAŁ 4. TYPY ZŁOŻONE

Aktualizacja zmiennej wycinka jest wymagana nie tylko podczas wywoływania funkcji append,
ale także w przypadku każdej funkcji, która może zmienić długość lub pojemność wycinka albo
sprawić, że będzie się on odwoływał do innej tablicy bazowej. Aby używać wycinków prawidłowo,
należy pamiętać, że choć elementy tablicy bazowej są pośrednie, to wskaźnik, długość i pojemność
wycinka już nie. Aby je zaktualizować, wymagane jest przypisanie, jak to powyższe. Pod tym wzglę-
dem wycinki nie są „czystymi” typami referencyjnymi, ale przypominają typ złożony, taki jak ta
struktura:
type IntSlice struct {
ptr *int
len, cap int
}
Nasza funkcja appendInt dodaje do wycinka pojedynczy element, ale wbudowana funkcja
append pozwala dodawać więcej niż jeden nowy element lub nawet cały wycinek elementów.
var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // dodaje wycinek x
fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"
Za pomocą pokazanej poniżej niewielkiej modyfikacji możemy oddać zachowanie wbudowanej
funkcji append. Wielokropek (...) w deklaracji czyni z appendInt funkcję o zmiennej liczbie
argumentów: akceptuje ona dowolną liczbę finalnych argumentów. Ten sam wielokropek w powyż-
szym wywołaniu funkcji append pokazuje, jak dostarczyć listę argumentów z wycinka. Wyjaśnimy
ten mechanizm szczegółowo w podrozdziale 5.7.
func appendInt(x []int, y ...int) []int {
var z []int
zlen := len(x) + len(y)
// …rozszerzanie z co najmniej do długości zlen…
copy(z[len(x):], y)
return z
}
Logika rozszerzania tablicy bazowej wycinka z pozostaje bez zmian i nie została tu pokazana.

4.2.2. Techniki in situ wycinka


Zobaczmy więcej przykładów funkcji, które podobnie jak reverse modyfikują in situ elementy
wycinka. Mając daną listę łańcuchów znaków, funkcja nonempty zwraca niepuste łańcuchy:
code/r04/nonempty
// Nonempty jest przykładem algorytmu in situ wycinka.
package main

import "fmt"
// nonempty zwraca wycinek przechowujący tylko niepuste łańcuchy.
// Podczas wywoływania funkcji modyfikowana jest tablica bazowa.
func nonempty(strings []string) []string {
i := 0
for _, s := range strings {
if s != "" {
strings[i] = s
i++
4.2. WYCINKI 101

}
}
return strings[:i]
}
Subtelnością jest to, że wycinki wejściowy i wyjściowy współdzielą tę samą tablicę bazową. Pozwala
to uniknąć konieczności alokowania kolejnej tablicy, choć oczywiście zawartość data jest częściowo
nadpisywana, co pokazuje druga instrukcja Printf:
data := []string{"jeden", "", "trzy"}
fmt.Printf("%q\n", nonempty(data)) // ` ["jeden" "trzy"] `
fmt.Printf("%q\n", data) // ` ["jeden" "trzy" "trzy"] `
Dlatego zwykle napisalibyśmy: data = nonempty(data).
Funkcję nonempty można również zapisać przy użyciu append:
func nonempty2(strings []string) []string {
out := strings[:0] // zerowej długości wycinek oryginału
for _, s := range strings {
if s != "" {
out = append(out, s)
}
}
return out
}
Którykolwiek wariant zastosujemy, ponowne wykorzystanie tablicy w ten sposób wymaga, aby co
najwyżej jedna wartość wyjściowa była generowana dla każdej wartości wejściowej, co jest prawdą
dla wielu algorytmów odfiltrowujących elementy sekwencji lub łączących sąsiadujące elementy.
Takie skomplikowane wykorzystanie wycinka jest wyjątkiem, a nie regułą, ale czasami może to
być jasne, efektywne i użyteczne.
Wycinek może być użyty do zaimplementowania stosu. Mając początkowo pusty wycinek stack,
możemy umieścić na końcu wycinka nową wartość za pomocą funkcji append:
stack = append(stack, v) // umieszczenie wartości v
Wierzchołkiem stosu jest ostatni element:
top := stack[len(stack)-1] // wierzchołek stosu
Stos można zmniejszyć, zdejmując ten element:
stack = stack[:len(stack)-1] // zdjęcie elementu ze stosu
Aby usunąć element ze środka wycinka przy zachowaniu kolejności pozostałych elementów, należy
użyć funkcji copy w celu przesunięcia o jeden w dół elementów o wyższych numerach, żeby wy-
pełnić lukę:
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}

func main() {
s := [ ]int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 8 9]"
}
Jeśli zaś nie musimy zachowywać porządku, możemy po prostu przenieść ostatni element do po-
wstałej luki:
102 ROZDZIAŁ 4. TYPY ZŁOŻONE

func remove(slice []int, i int) []int {


slice[i] = slice[len(slice)-1]
return slice[:len(slice)-1]
}

func main() {
s := [ ]int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 9 8]"
}

Ćwiczenie 4.3. Przepisz funkcję reverse, tak aby używała wskaźnika tablicy zamiast wycinka.
Ćwiczenie 4.4. Napisz wersję sposobu obracania wycinka, który działa w pojedynczym przekazaniu.
Ćwiczenie 4.5. Napisz funkcję in situ do eliminowania sąsiednich duplikatów w wycinku []string.
Ćwiczenie 4.6. Napisz funkcję in situ, która w zakodowanym w UTF-8 wycinku []byte ściska każdą
serię sąsiadujących ze sobą spacji Unicode (zob. unicode.IsSpace) w pojedynczą spację ASCII.
Ćwiczenie 4.7. Zmodyfikuj funkcję reverse, aby odwracała in situ znaki wycinka []byte repre-
zentującego zakodowany w UTF-8 łańcuch znaków. Możesz to zrobić bez alokowania nowej
pamięci?

4.3. Mapy
Tablica mieszająca jest jedną z najbardziej pomysłowych i wszechstronnych ze wszystkich struktur
danych. Jest to nieuporządkowany zbiór par klucz-wartość, w którym wszystkie klucze są różne,
a wartość powiązana z danym kluczem może być pobierana, aktualizowana lub usuwana za pomocą
stałej (w ujęciu uśrednionym) liczby porównań kluczy, bez względu na wielkość tablicy mieszającej.
W języku Go mapa jest referencją do tablicy mieszającej, a typ mapy jest zapisywany jako map[K]V,
gdzie K i V są typami jej kluczy i wartości. Wszystkie klucze w danej mapie są tego samego typu
i wszystkie wartości są tego samego typu, ale klucze nie muszą być tego samego typu co wartości.
Typ klucza K musi być porównywany za pomocą operatora ==, aby mapa mogła być testowana pod
kątem tego, czy dany klucz jest równy kluczowi już znajdującemu się w mapie. Chociaż liczby
zmiennoprzecinkowe są porównywalne, złym pomysłem jest porównywanie ich w kategoriach
równości, szczególnie (jak wspomniano w rozdziale 3.) jeśli możliwą wartością jest NaN. Nie ma
żadnych ograniczeń co do typu wartości V.
Do tworzenia mapy można użyć wbudowanej funkcji make:
ages := make(map[string]int) // mapowanie z łańcuchów znaków na liczby całkowite
Możemy również użyć literału mapy do tworzenia nowej mapy zapełnionej kilkoma początko-
wymi parami klucz-wartość:
ages := map[string]int{
"alicja": 31,
"krzysiek": 34,
}
Jest to równoważne z zapisem:
ages := make(map[string]int)
ages["alicja"] = 31
ages["krzysiek"] = 34
4.3. MAPY 103

Alternatywnym wyrażeniem dla nowej pustej mapy jest więc map[string]int{}.


Dostęp do elementów mapy uzyskuje się za pośrednictwem zwykłej notacji indeksowej:
ages["alicja"] = 32
fmt.Println(ages["alicja"]) // "32"
Natomiast usuwanie elementów odbywa się za pomocą wbudowanej funkcji delete:
delete(ages, "alicja") // usuwa element ages["alicja"]
Wszystkie te operacje są bezpieczne, nawet gdy dany element nie znajduje się w mapie. Przeszuki-
wanie mapy przy użyciu klucza, którego w niej nie ma, zwraca wartość zerową dla jego typu, więc
np. poniższa instrukcja działa, nawet jeśli "robert" nie jest jeszcze kluczem w mapie, ponieważ
wartością ages["robert"] będzie 0.
ages["robert"] = ages["robert"] + 1 // Sto lat!
Skrócone formy przypisania x += y oraz x++ również działają dla elementów mapy, więc możemy
przepisać powyższą instrukcję jako:
ages["robert"] += 1
Lub jeszcze bardziej zwięźle:
ages["robert"]++
Jednak element mapy nie jest zmienną i nie możemy pobrać jego adresu:
_ = &ages["robert"] // błąd kompilacji: nie można pobrać adresu elementu mapy
Jednym z powodów, dla których nie możemy pobrać adresu elementu mapy, jest to, że powięk-
szająca się mapa może spowodować ponowne mieszanie istniejących elementów w nowe miejsca
przechowywania, tym samym potencjalnie unieważniając adres.
Aby dokonać enumeracji wszystkich par klucz-wartość w mapie, używamy pętli for opartej na
zakresie (range), podobnej do pętli, których używaliśmy dla wycinków. Kolejne iteracje pętli powo-
dują, że zmienne name i age są ustawiane na następną parę klucz-wartość.
for name, age := range ages {
fmt.Printf("%s\t%d\n", name, age)
}
Kolejność iteracji mapy jest nieokreślona, a różne implementacje mogą używać innej funkcji mie-
szającej, co prowadzi do powstawania różnej kolejności. W praktyce kolejność jest losowa i różni
się dla każdego kolejnego wykonywania. Jest to celowe. Zmienianie sekwencji pomaga wymusić
niezawodność programów w różnych implementacjach. Aby dokonać enumeracji par klucz-wartość
w kolejności, musimy bezpośrednio posortować klucze, np. przy użyciu funkcji Strings z pakietu
sort, jeśli klucze są łańcuchami znaków. Oto typowy wzorzec:
import "sort"

var names []string


for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
104 ROZDZIAŁ 4. TYPY ZŁOŻONE

Ponieważ od początku znamy ostateczny rozmiar wycinka names, bardziej efektywne jest alokowanie
z góry tablicy o wymaganym rozmiarze. Poniższa instrukcja tworzy wycinek, który początkowo
jest pusty, ale ma wystarczającą pojemność, aby pomieścić wszystkie klucze mapy ages:
names := make([]string, 0, len(ages))
W pierwszej pętli range powyżej wymagane są tylko klucze mapy ages, więc pomijamy drugą
zmienną pętli. W drugiej pętli wymagane są tylko elementy wycinka names, więc używamy pustego
identyfikatora _ do zignorowania pierwszej zmiennej, czyli indeksu.
Wartością zerową dla typu mapy jest nil, czyli brak referencji do jakiejkolwiek tabeli mieszającej.
var ages map[string]int
fmt.Println(ages == nil) // "true"
fmt.Println(len(ages) == 0) // "true"
Większość operacji na mapach, w tym przeszukiwanie, delete, len oraz pętle range, można bez-
piecznie wykonywać na referencji mapy nil, ponieważ zachowuje się jak pusta mapa. Zapisywanie
w mapie nil wywołuje jednak procedurę panic:
ages["karolina"] = 21 // panic: przypisanie do wpisu w mapie nil
Trzeba najpierw alokować mapę, aby móc w niej zapisywać.
Uzyskiwanie dostępu do elementu mapy poprzez indeksowanie zawsze daje jakąś wartość. Jeśli
dany klucz jest obecny w mapie, otrzymujesz odpowiednią wartość. Jeśli nie, otrzymujesz wartość
zerową dla danego typu elementu, tak jak w przypadku ages["robert"]. Jest to odpowiednie
dla wielu zastosowań, ale czasami trzeba wiedzieć, czy element rzeczywiście tam jest, czy nie. Jeśli
typem elementu jest np. wartość liczbowa, można za pomocą poniższego testu odróżnić nieistnie-
jący element od elementu, który ma akurat wartość zerową:
age, ok := ages["robert"]
if !ok { /* "robert" nie jest kluczem w tej mapie; age == 0. */ }
Często napotkasz te dwie instrukcje w następującym połączeniu:
if age, ok := ages["robert"]; !ok { /* ... */ }
Indeksowanie mapy w tym kontekście daje dwie wartości. Drugą z nich jest wartość logiczna ra-
portująca, czy dany element był obecny. Zmienna logiczna jest często nazywana ok, zwłaszcza jeśli
jest użyta w warunku if na początku.
Tak jak wycinki, mapy nie mogą być ze sobą porównywane. Jedynym prawidłowym porównaniem
jest porównanie z wartością nil. Aby przetestować, czy dwie mapy zawierają te same klucze i te
same powiązane wartości, musimy napisać pętlę:
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
4.3. MAPY 105

Zaobserwuj, w jaki sposób używamy !ok do odróżnienia przypadków „wartość brakująca” i „wartość
istniejąca, ale zerowa”. Gdybyśmy naiwnie napisali xv != y[k], poniższe wywołanie niewłaściwie
zgłosiłoby swoje argumenty jako równe:
// Prawda, jeśli równość jest zapisana nieprawidłowo.
equal(map[string]int{"A": 0}, map[string]int{"B": 42})
Język Go nie zapewnia typu set, ponieważ jednak klucze mapy są różne, mapa może spełniać
swoje zadanie. Ilustruje to program dedup, który odczytuje sekwencję linii i wyświetla tylko pierw-
sze wystąpienie każdej odrębnej linii. (Jest to wariant programu dup, który pokazaliśmy w podroz-
dziale 1.3). Program dedup wykorzystuje mapę z kluczami reprezentującymi zbiór linii, które
się już raz pojawiły, aby zapewnić, że kolejne wystąpienia nie będą wyświetlane.
code/r04/dedup
func main() {
seen := make(map[string]bool) // zbiór łańcuchów znaków
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
if !seen[line] {
seen[line] = true
fmt.Println(line)
}
}
if err := input.Err(); err != nil {
fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
os.Exit(1)
}
}
Programiści Go często bez zbędnych ceregieli opisują używaną w ten sposób mapę jako „zbiór
łańcuchów znaków”, ale uważaj, bo nie wszystkie wartości map[string]bool są prostymi zbiorami.
Niektóre z nich mogą zawierać zarówno wartości true, jak i false.
Czasami potrzebujemy mapy lub zbioru, których klucze są wycinkami, ale ponieważ klucze mapy
muszą być porównywalne, nie można tego wyrazić bezpośrednio. Można to jednak zrobić w dwóch
etapach. Najpierw definiujemy funkcję pomocniczą k mapującą każdy klucz na łańcuch znaków,
która ma właściwość taką, że k(x) == k(y) wtedy i tylko wtedy, gdy uznajemy x i y za równoważne.
Następnie tworzymy mapę, której klucze są łańcuchami znaków, stosując funkcję pomocniczą do
każdego klucza, zanim uzyskamy dostęp do mapy.
Poniższy przykład wykorzystuje mapę do rejestrowania liczby wywołań funkcji Add z daną listą
łańcuchów znaków. Używamy funkcji fmt.Sprintf do przekonwertowania wycinka łańcuchów
znaków na pojedynczy łańcuch znaków będący odpowiednim kluczem mapy, cytując każdy element
wycinka za pomocą %q, aby wiernie zarejestrować granice łańcucha:
var m = make(map[string]int)

func k(list []string) string { return fmt.Sprintf("%q", list) }

func Add(list []string) { m[k(list)]++ }


func Count(list []string) int { return m[k(list)] }
To samo podejście może być wykorzystywane do wszystkich nieporównywalnych typów kluczy,
nie tylko do wycinków. Jest to nawet przydatne dla porównywalnych typów kluczy, gdy potrzebna
jest definicja równości inna niż ==, np. w przypadku porównywania łańcuchów znaków, w których
106 ROZDZIAŁ 4. TYPY ZŁOŻONE

wielkość liter nie ma znaczenia. Ponadto typ k(x) nie musi być łańcuchem znaków. Nadaje się
każdy typ z żądaną właściwością równoważności, taki jak liczba całkowita, tablica lub struktura.
Kolejnym przykładem map w akcji jest program, który zlicza wystąpienia każdego odrębnego
punktu kodowego Unicode w swoich danych wejściowych. Ponieważ istnieje duża liczba możli-
wych znaków, z których tylko niewielka część pojawia się w każdym przykładowym dokumencie,
mapa jest naturalnym sposobem śledzenia odnotowanych znaków i odpowiadających im liczb
ich wystąpień.
code/r04/charcount
// Charcount liczy wystąpienia znaków Unicode.
package main

import (
"bufio"
"fmt"
"io"
"os"
"unicode"
"unicode/utf8"
)

func main() {
counts := make(map[rune]int) // zliczanie wystąpień znaków Unicode
var utflen [utf8.UTFMax + 1]int // zliczanie długości kodowań UTF-8
invalid := 0 // liczba nieprawidłowych znaków UTF-8

in := bufio.NewReader(os.Stdin)
for {
r, n, err := in.ReadRune() // zwraca runę, liczbę bajtów i błąd
if err == io.EOF {
break
}
if err != nil {
fmt.Fprintf(os.Stderr, "charcount: %v\n", err)
os.Exit(1)
}
if r == unicode.ReplacementChar && n == 1 {
invalid++
continue
}
counts[r]++
utflen[n]++
}
fmt.Printf("runa\tliczba wystąpień\n")
for c, n := range counts {
fmt.Printf("%q\t%d\n", c, n)
}
fmt.Print("\ndługość\tliczba wystąpień\n")
for i, n := range utflen {
if i > 0 {
fmt.Printf("%d\t%d\n", i, n)
}
}
if invalid > 0 {
fmt.Printf("\n%d niewłaściwych znaków UTF-8\n", invalid)
}
}
4.3. MAPY 107

Metoda ReadRune wykonuje dekodowanie UTF-8 i zwraca trzy wartości: zdekodowaną runę,
długość jej kodowania UTF-8 w bajtach oraz wartość błędu. Jedynym błędem, jakiego się spodzie-
wamy, jest end-of-file, czyli koniec pliku. Jeśli dane wejściowe nie są prawidłowym kodowaniem
UTF-8 runy, zwracaną runą jest unicode.ReplacementChar, a długość wynosi 1.
Program charcount wyświetla również policzone wystąpienia określonych długości kodowań
UTF-8 dla run, które pojawiły się w danych wejściowych. Mapa nie jest najlepszą strukturą danych
do takich zastosowań. Ponieważ długości kodowań mieszczą się w zakresie od 1 do utf8.UTFMax
(który ma wartość 4), tablica jest bardziej zwarta.
W ramach eksperymentu na pewnym etapie uruchomiliśmy program charcount, używając jako
danych wejściowych tekstu tej książki w oryginale. Chociaż ten tekst jest głównie w języku angiel-
skim, ma oczywiście sporo znaków spoza kodu ASCII. Oto znaki z pierwszej dziesiątki:
° 27 世 15 界 14 é 13 A 10 ≤ 5  5 京 4 � 4 + 3
A to jest rozkład długości wszystkich kodowań UTF-8:
długość liczba wystąpień
1 765391
2 60
3 70
4 0
Typ wartości mapy sam może być typem złożonym, takim jak mapa lub wycinek. W poniższym
kodzie typem klucza mapy graph jest string, a typem wartości jest map[string]bool, reprezen-
tujący zbiór łańcuchów znaków. Koncepcyjnie graph mapuje łańcuch znaków na zbiór powiązanych
łańcuchów, czyli jego następców w grafie skierowanym.
code/r04/graph
var graph = make(map[string]map[string]bool)

func addEdge(from, to string) {


edges := graph[from]
if edges == nil {
edges = make(map[string]bool)
graph[from] = edges
}
edges[to] = true
}

func hasEdge(from, to string) bool {


return graph[from][to]
}
Funkcja addEdge pokazuje idiomatyczny sposób leniwego zapełniania mapy, czyli inicjowania każdej
wartości, gdy jej klucz pojawia się po raz pierwszy. Funkcja hasEdge pokazuje, w jaki sposób często
wykorzystywana jest wartość zerowa brakującego wpisu mapy: nawet jeśli nieobecne są from i to,
graph[from][to] zawsze daje znaczący wynik.
Ćwiczenie 4.8. Zmodyfikuj program charcount, aby liczył litery, cyfry itd. w ich kategoriach
Unicode, przy użyciu funkcji takich jak np. unicode.IsLetter.
Ćwiczenie 4.9. Napisz program wordfreq służący do raportowania częstotliwości występowania
każdego słowa w wejściowym pliku tekstowym. Przed pierwszym wywołaniem funkcji Scan
wywołaj funkcję input.Split (bufio.ScanWords), aby rozdzielić dane wejściowe na słowa zamiast
na linie.
108 ROZDZIAŁ 4. TYPY ZŁOŻONE

4.4. Struktury
Struktura (ang. struct) jest złożonym typem danych, który grupuje zero lub więcej nazwanych war-
tości dowolnych typów w postaci pojedynczej encji. Każda wartość jest nazywana polem. Klasycznym
przykładem struktury z dziedziny przetwarzania danych jest rekord pracownika, zawierający pola ta-
kie jak: unikatowy identyfikator, nazwisko, adres, data urodzenia, stanowisko, wynagrodzenie, iden-
tyfikator kierownika itp. Wszystkie te pola są zebrane w pojedynczą encję, która może być kopiowa-
na jako jednostka, przekazywana do funkcji i zwracana przez nie, zapisywana w tablicach itd.
Poniższe dwie instrukcje deklarują typ struct o nazwie Employee oraz zmienną dilbert, która
jest instancją struktury Employee:
type Employee struct {
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}

var dilbert Employee


Dostęp do poszczególnych pól zmiennej dilbert jest uzyskiwany za pomocą notacji kropkowej,
np. dilbert.name i dilbert.DoB. Ponieważ dilbert jest zmienną, jej pola również są zmiennymi,
więc możemy przypisywać wartości do pola:
dilbert.Salary -= 5000 // zdegradowany za napisanie zbyt małej ilości kodu
Możemy też wziąć adres zmiennej i uzyskać do niej dostęp za pomocą wskaźnika:
position := &dilbert.Position
*position = "Starszy " + *position // awansowany za outsourcing dla Elbonii
Zapis kropkowy działa również ze wskaźnikiem do struktury:
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proaktywny gracz zespołowy)"
Ta ostatnia instrukcja jest równoważna z tą:
(*employeeOfTheMonth).Position += " (proaktywny gracz zespołowy)"
Mając dany unikatowy identyfikator pracownika, funkcja EmployeeByID zwraca wskaźnik do struk-
tury Employee. Możemy użyć notacji kropkowej, aby uzyskać dostęp do jej pól:
func EmployeeByID(id int) *Employee { /* ... */ }

fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Rogatowłosy szef"

id := dilbert.ID
EmployeeByID(id).Salary = 0 // zwolniony za… właściwie bez powodu
Ostatnia instrukcja aktualizuje strukturę Employee, na którą wskazuje wynik wywołania funkcji
EmployeeByID. Jeśli typ wyniku wywołania funkcji EmployeeByID zostałby zmieniony na Employee
zamiast *Employee, instrukcja przypisania nie skompilowałaby się, ponieważ jej lewa strona nie
identyfikowałaby zmiennej.
Pola są zwykle zapisywane po jednym w linii, a nazwa pola poprzedza jego typ. Kolejne pola tego
samego typu mogą być jednak łączone w tej samej linii, tak jak Name i Address tutaj:
4.4. STRUKTURY 109

type Employee struct {


ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
Kolejność pól jest istotna dla tożsamości typu. Gdybyśmy połączyli również deklarację pola
Position (także łańcuch znaków) lub zamienili Name i Address, zdefiniowalibyśmy inny typ struct.
Zazwyczaj łączymy tylko deklaracje powiązanych pól.
Nazwa pola struktury jest eksportowana, jeśli zaczyna się wielką literą. Jest to główny mecha-
nizm kontroli dostępu w języku Go. Typ struktury może zawierać mieszaninę eksportowanych
i nieeksportowanych pól.
Typy struct bywają rozwlekłe, ponieważ często posiadają po jednej linii dla każdego pola. Chociaż
moglibyśmy zapisywać cały typ za każdym razem, gdy jest potrzebny, powtarzanie stałoby się
męczące. Zamiast tego typy struct zwykle występują w ramach deklaracji typu nazwanego, takie-
go jak Employee.
Nazwany typ struktury S nie może deklarować pola tego samego typu S: wartość zagregowana
nie może zawierać samej siebie. (Analogiczne ograniczenie dotyczy tablic). Jednak S może zade-
klarować pole typu wskaźnika *S, które pozwala tworzyć rekurencyjne struktury danych, takie
jak lista powiązana i drzewa. Zostało to zilustrowane w poniższym kodzie, który wykorzystuje
binarne drzewo do zaimplementowania sortowania przez wstawianie:
code/r04/treesort
type tree struct {
value int
left, right *tree
}

// Sort sortuje wartości in situ.


func Sort(values []int) {
var root *tree
for _, v := range values {
root = add(root, v)
}
appendValues(values[:0], root)
}

// appendValues dołącza elementy t do wartości w kolejności i zwraca powstały wycinek.


func appendValues(values []int, t *tree) []int {
if t != nil {
values = appendValues(values, t.left)
values = append(values, t.value)
values = appendValues(values, t.right)
}
return values
}

func add(t *tree, value int) *tree {


if t == nil {
// Równoważne z return &tree{value: value}.
t = new(tree)
t.value = value
110 ROZDZIAŁ 4. TYPY ZŁOŻONE

return t
}
if value < t.value {
t.left = add(t.left, value)
} else {
t.right = add(t.right, value)
}
return t
}
Wartość zerowa dla struktury składa się z wartości zerowych każdego z jej pól. Zwykle pożądane jest,
aby wartość zerowa była naturalną lub rozsądną wartością domyślną. Przykładowo: w bytes.Buffer
wartością początkową struktury jest gotowy do użycia pusty bufor, a wartością zerową sync.Mutex
(omówionego w rozdziale 9.) jest gotowy do użycia odblokowany mutex. Niekiedy takie rozsądne
zachowanie początkowe otrzymujemy za darmo, ale czasami projektant typów musi nad tym po-
pracować.
Typ struct bez pól nazywany jest pustą strukturą, zapisywaną jako struct{}. Pusta struktura
ma rozmiar zerowy i nie przenosi żadnej informacji, ale mimo to może być użyteczna. Niektórzy
programiści Go używają jej zamiast bool jako typu wartości mapy reprezentującej zbiór, aby podkre-
ślić, że istotne są jedynie klucze. Jednak oszczędność miejsca jest marginalna, a składnia bardziej
kłopotliwa, więc zasadniczo powinno się tego unikać.
seen := make(map[string]struct{}) // zbiór łańcuchów znaków
// …
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
// …pierwsze napotkanie s…
}

4.4.1. Literały struktur


Wartość typu struktury może być zapisywana za pomocą literału struktury, który określa wartości
dla jej pól.
type Point struct{ X, Y int }

p := Point{1, 2}
Istnieją dwie formy literału struktury. Pokazana powyżej pierwsza forma wymaga, aby wartość
była określona dla każdego pola w odpowiedniej kolejności. Obciąża to piszącego (i czytającego) kod
koniecznością pamiętania, jakie dokładnie są to pola, i sprawia, że kod staje się kruchy, jeśli w przy-
szłości zbiór pól się powiększy lub zmieni się kolejność pól. W związku z tym ta forma jest z reguły
używana tylko w ramach pakietu definiującego dany typ struktury lub w mniejszych typach struktury
z oczywistą umowną kolejnością pól takich jak image.Point{x, y} lub color.RGBA{red, green,
blue, alpha}.
Częściej używana jest forma druga, w której wartość struktury jest inicjowana poprzez wymienie-
nie wszystkich lub niektórych nazw pól i odpowiadających im wartości, tak jak w poniższej instrukcji
zaczerpniętej z programu Lissajous z podrozdziału 1.4:
anim := gif.GIF{LoopCount: nframes}
Jeśli w tego rodzaju literale pominięte zostanie pole, jest ono ustawiane na wartość zerową dla jego
typu. Ponieważ podawane są nazwy, kolejność nie ma znaczenia.
4.4. STRUKTURY 111

Te dwie formy nie mogą być mieszane w tym samym literale. Nie można też używać pierwszej
formy (opartej na kolejności) literału, aby ominąć regułę, że do niewyeksportowanych identyfi-
katorów nie można się odwoływać z poziomu innego pakietu.
package p
type T struct{ a, b int } // a i b nie są wyeksportowane

package q
import "p"
var _ = p.T{a: 1, b: 2} // błąd kompilacji: nie można się odwołać do a, b
var _ = p.T{1, 2} // błąd kompilacji: nie można się odwołać do a, b
Chociaż w ostatniej linii powyższego kodu nie są wymienione niewyeksportowane identyfikatory
pól, to w rzeczywistości są użyte pośrednio, więc nie jest to dozwolone.
Wartości struktury mogą być przekazywane jako argumenty do funkcji i zwracane z niej. Poniższa
funkcja np. skaluje Point według określonego współczynnika:
func Scale(p Point, factor int) Point {
return Point{p.X * factor, p.Y * factor}
}

fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"


Dla lepszej efektywności większe typy struct są zwykle przekazywane do funkcji lub zwracane
z nich pośrednio za pomocą wskaźnika:
func Bonus(e *Employee, percent int) int {
return e.Salary * percent / 100
}
Jest to wymagane, jeśli funkcja musi zmodyfikować swój argument, ponieważ w językach wywo-
ływania przez wartość, takich jak Go, wywoływana funkcja otrzymuje tylko kopię argumentu, a nie
referencję do oryginalnego argumentu.
func AwardAnnualRaise(e *Employee) {
e.Salary = e.Salary * 105 / 100
}
Ponieważ ze strukturami zazwyczaj obchodzimy się poprzez wskaźniki, można użyć tej skróconej
notacji do utworzenia i zainicjowania zmiennej struct oraz uzyskania jej adresu:
pp := &Point{1, 2}
Jest to dokładnie równoważne z poniższym zapisem, ale &Point{1, 2} może być używane bezpo-
średnio w wyrażeniu takim jak wywołanie funkcji.
pp := new(Point)
*pp = Point{1, 2}

4.4.2. Porównywanie struktur


Jeśli wszystkie pola struktury są porównywalne, sama struktura jest porównywalna, więc dwa wy-
rażenia tego typu mogą być porównywane za pomocą == lub !=. Operacja == porównuje odpowia-
dające pola dwóch struktur w kolejności, więc dwa poniższe wyświetlone wyrażenia są równoważne:
type Point struct{ X, Y int }

p := Point{1, 2}
q := Point{2, 1}
112 ROZDZIAŁ 4. TYPY ZŁOŻONE

fmt.Println(p.X == q.X && p.Y == q.Y) // "false"


fmt.Println(p == q) // "false"
Porównywalne typy struct, podobnie jak inne porównywalne typy, mogą być używane jako typ
klucza mapy.
type address struct {
hostname string
port int
}

hits := make(map[address]int)
hits[address{"golang.org", 443}]++

4.4.3. Osadzanie struktur i anonimowe pola


W tym punkcie zobaczymy, jak niezwykły mechanizm osadzania struktur języka Go pozwala
używać jednego nazwanego typu struct jako anonimowego pola innego typu struct. Zapewnia
to wygodny skrót składniowy, dzięki któremu proste wyrażenie kropkowe, takie jak x.f, może
oznaczać łańcuch pól, taki jak x.d.e.f.
Rozważmy program do rysowania grafiki dwuwymiarowej, który zapewnia bibliotekę kształtów
takich jak prostokąty, elipsy, gwiazdy i koła. Oto dwa z typów, które może on definiować:
type Circle struct {
X, Y, Radius int
}

type Wheel struct {


X, Y, Radius, Spokes int
}
Typ Circle (okrąg) ma pola dla współrzędnych X i Y jego środka oraz pole Radius (promień).
Typ Wheel (koło) posiada wszystkie cechy typu Circle oraz dodatkowo pole Spokes oznaczające
liczbę wpisanych w okrąg promienistych szprych. Utwórzmy koło:
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20
Wraz z powiększaniem się zbioru kształtów na pewno zauważymy wśród nich podobieństwa
i powtórzenia, więc wygodne może być wyodrębnienie ich części wspólnych:
type Point struct {
X, Y int
}

type Circle struct {


Center Point
Radius int
}

type Wheel struct {


Circle Circle
Spokes int
}
4.4. STRUKTURY 113

Aplikacja może być dzięki temu bardziej zrozumiała, ale ta zmiana sprawia, że uzyskiwanie dostępu
do pól typu Wheel jest bardziej rozwlekłe:
var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
Go pozwala deklarować pole z typem, ale bez nazwy. Takie pola zwane są polami anonimowymi.
Typ takiego pola musi być typem nazwanym lub wskaźnikiem do typu nazwanego. Poniższe typy
Circle i Wheel mają po jednym polu anonimowym. Mówimy, że Point jest osadzone w Circle,
a Circle jest osadzone w Wheel.
type Circle struct {
Point
Radius int
}

type Wheel struct {


Circle
Spokes int
}
Dzięki osadzaniu możemy się odwoływać do nazw z liści domyślnego drzewa bez podawania nazw
znajdujących się pomiędzy:
var w Wheel
w.X = 8 // równoważne z w.Circle.Point.X = 8
w.Y = 8 // równoważne z w.Circle.Point.Y = 8
w.Radius = 5 // równoważne z w.Circle.Radius = 5
w.Spokes = 20
Bezpośrednie formy przedstawione w powyższych komentarzach są jednak nadal prawidłowe,
co pokazuje, że „anonimowe pole” jest nieco niewłaściwym określeniem. Pola Circle i Point
mają nazwy (pochodzące od typów nazwanych), ale te nazwy są opcjonalne w wyrażeniach
kropkowych. Możemy pominąć dowolne pole anonimowe lub wszystkie te pola przy wybieraniu
ich podpól.
Niestety, nie ma odpowiedniego skrótu dla składni literału struktury, więc żadne z poniższych nie
będzie się kompilować:
w = Wheel{8, 8, 5, 20} // błąd kompilacji: nieznane pola
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // błąd kompilacji: nieznane pola
Literał struktury musi mieć ten sam kształt co deklaracja typu, więc musimy użyć jednej z dwóch
poniższych form, które są sobie równoważne:
code/r04/embed
w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // UWAGA: przecinek na końcu jest tu konieczny (tak jak przy Radius)
}
fmt.Printf("%#v\n", w)
114 ROZDZIAŁ 4. TYPY ZŁOŻONE

// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}

w.X = 42

fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
Należy zwrócić uwagę, że przysłówek # powoduje, iż czasownik %v funkcji Printf wyświetla war-
tości w formie zbliżonej do składni języka Go. W przypadku wartości struktury ta forma zawiera
nazwę każdego pola.
Ponieważ „anonimowe” pola mają pośrednie nazwy, nie można mieć dwóch anonimowych pól
tego samego typu, ponieważ ich nazwy kolidowałyby ze sobą. A ponieważ nazwa pola jest pośrednio
determinowana przez jego typ, to samo dotyczy widoczności tego pola. W powyższych przykładach
anonimowe pola Point i Circle są eksportowane. Gdyby były niewyeksportowane (point
i circle), moglibyśmy nadal używać formy skróconej:
w.X = 8 // równoważne z w.circle.point.X = 8
Jednak bezpośrednia długa forma przedstawiona w komentarzu byłaby zabroniona poza pakietem
deklarującym, ponieważ circle i point byłyby niedostępne.
To, co zobaczyliśmy do tej pory w temacie osadzania struktur, jest tylko posypką na torcie notacji
składniowej służącej do wyboru pól struktury. Później się przekonamy, że anonimowe pola
nie muszą być typami struct. Może to być dowolny typ nazwany lub wskaźnik do typu nazwanego.
Ale po co mielibyśmy osadzać typ, który nie ma podpól?
Odpowiedź jest związana z metodami. Skrótowa notacja używana do wybierania pól typu osadzo-
nego działa również w przypadku wybierania jego metod. W efekcie zewnętrzny typ struct zy-
skuje nie tylko pola typu osadzonego, ale także jego metody. Ten mechanizm jest głównym sposo-
bem komponowania złożonych zachowań obiektów z prostszych zachowań. Kompozycja ma
zasadnicze znaczenie dla programowania obiektowego w języku Go i zajmiemy się tym obszerniej
w podrozdziale 6.3.

4.5. JSON
JSON (ang. JavaScript Object Notation) to standardowa notacja do wysyłania i odbierania ustruktu-
ryzowanych informacji. Nie jest to jedyna taka notacja. XML (zob. podrozdział 7.14), ASN.1 oraz
format Protocol Buffers firmy Google służą podobnym celom i każdy ma swoją niszę, ale ze wzglę-
du na swoją prostotę, czytelność i uniwersalne wsparcie, JSON jest najszerzej stosowany.
Go ma doskonałe wsparcie dla kodowania i dekodowania tych formatów, zapewniane przez stan-
dardowe pakiety biblioteczne, takie jak m.in.: encoding/json, encoding/xml, encoding/asn1,
a wszystkie te pakiety mają podobne interfejsy API. Ten podrozdział zawiera krótki przegląd naj-
ważniejszych części pakietu encoding/json.
JSON jest kodowaniem wartości JavaScript (łańcuchów znaków, liczb, wartości logicznych, tablic
i obiektów) w postaci tekstu Unicode. Jest efektywną, a przy tym czytelną reprezentacją podsta-
wowych typów danych opisanych w rozdziale 3. i typów złożonych z tego rozdziału — tablic,
wycinków, struktur i map.
4.5. JSON 115

Podstawowymi typami JSON są liczby (w notacji dziesiętnej lub naukowej), wartości logiczne
(true lub false) oraz łańcuchy znaków, które są sekwencjami punktów kodowych Unicode
zamkniętymi w podwójnych cudzysłowach, gdzie lewy ukośnik znaków ucieczki wykorzystuje
notację podobną do Go, chociaż numeryczne sekwencje ucieczki \Uhhhh formatu JSON oznaczają
kody UTF-16, a nie runy.
Te podstawowe typy mogą być łączone rekurencyjnie za pomocą tablic i obiektów JSON. Tablica
JSON jest uporządkowaną sekwencją wartości, zapisaną jako rozdzielona przecinkami lista za-
mknięta w nawiasach kwadratowych. Tablice JSON są używane do kodowania tablic i wycinków Go.
Obiekt JSON jest mapowaniem z łańcuchów znaków na wartości zapisanym jako sekwencja par
nazwa:wartość rozdzielonych przecinkami i otoczonych nawiasami klamrowymi. Obiekty JSON są
używane do kodowania map (z kluczami w postaci łańcuchów znaków) i struktur Go. Na przykład:
boolean true
number -273.15
string "Powiedziała \"Witaj, 世界\""
array ["złoto", "srebro", "brąz"]
object {"rok": 1980,
"zawody": "łucznictwo",
"medale": ["złoto", "srebro", "brąz"]}
Rozważmy aplikację, która gromadzi recenzje filmów i oferuje rekomendacje. Jej typ danych
Movie i typowa lista wartości są zadeklarowane poniżej. (Literały łańcuchów znaków po deklara-
cjach pól Year i Color są znacznikami pól. Objaśnimy je za chwilę).
code/r04/movie
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}

var movies = []Movie{


{Title: "Casablanca", Year: 1942, Color: false,
Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
{Title: "Nieugięty Luke", Year: 1967, Color: true,
Actors: []string{"Paul Newman"}},
{Title: "Bullitt", Year: 1968, Color: true,
Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
// …
}
Struktury danych takie jak ta doskonale pasują do formatu JSON i łatwo jest konwertować je w obu
kierunkach. Konwersja struktury danych Go takiej jak movies na format JSON nazywa się
marshalingiem. Marshaling jest wykonywany przez funkcję json.Marshal:
data, err := json.Marshal(movies)
if err != nil {
log.Fatalf("Marshaling na format JSON nie powiódł się: %s", err)
}
fmt.Printf("%s\n", data)
Funkcja Marshal produkuje wycinek bajtów zawierający bardzo długi łańcuch znaków bez żadnych
ubocznych znaków niedrukowalnych. Wiersze zostały zawinięte, aby łańcuch zmieścił się na
stronie:
116 ROZDZIAŁ 4. TYPY ZŁOŻONE

[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
id Bergman"]},{"Title":"Nieugięty Luke","released":1967,"color":true,"Ac
tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
Actors":["Steve McQueen","Jacqueline Bisset"]}]
Ta kompaktowa reprezentacja zawiera wszystkie informacje, ale jest trudna do odczytania. Wariant
zwany json.MarshalIndent generuje ładnie wcięte dane wyjściowe, które są łatwiejsze do od-
czytania przez człowieka. Dwa dodatkowe argumenty definiują prefiks dla każdej linii danych
wyjściowych i łańcuch znaków dla każdego poziomu wcięcia:
data, err := json.MarshalIndent(movies, "", " ")
if err != nil {
log.Fatalf("Marshaling na format JSON nie powiódł się: %s", err)
}
fmt.Printf("%s\n", data)
Powyższy kod wyświetla następujące dane wyjściowe:
[
{
"Title": "Casablanca",
"released": 1942,
"Actors": [
"Humphrey Bogart",
"Ingrid Bergman"
]
},
{
"Title": "Nieugięty Luke",
"released": 1967,
"color": true,
"Actors": [
"Paul Newman"
]
},
{
"Title": "Bullitt",
"released": 1968,
"color": true,
"Actors": [
"Steve McQueen",
"Jacqueline Bisset"
]
}
]
Marshaling wykorzystuje nazwy pól struktury Go jako nazwy pól dla obiektów JSON (poprzez
refleksję, jak zobaczymy w podrozdziale 12.6). Marshalowane są tylko wyeksportowane pola, dla-
tego wybraliśmy zapis wielką literą dla wszystkich nazw pól Go.
Pewnie zauważyłeś, że w danych wyjściowych nazwa pola Year zmieniła się na released, a nazwa
pola Color zmieniła się na color. To z powodu znaczników pól. Znacznik pola jest łańcuchem
metadanych powiązywanych w czasie kompilacji z danym polem struktury:
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Znacznik pola może być dowolnym łańcuchem znaków, ale jest umownie interpretowany jako
rozdzielana spacjami lista par klucz:"wartość". Ponieważ znaczniki pól zawierają znaki podwój-
nego cudzysłowu, są zazwyczaj zapisywane za pomocą surowych literałów łańcuchów znaków.
4.5. JSON 117

Klucz json kontroluje zachowanie pakietu encoding/json, a pozostałe pakiety encoding/...


również stosują tę konwencję. Pierwsza część znacznika json określa alternatywną nazwę JSON
dla pola Go. Znaczniki pól są często używane do określania idiomatycznych nazw JSON, takich jak
total_count dla nazwy TotalCount pola Go. Znacznik dla pola Color ma dodatkową opcję
omitempty, która wskazuje, że żadne dane wyjściowe JSON nie powinny być generowane, jeśli pole
ma wartość zerową dla swojego typu (w tym przypadku false) lub jest w inny sposób puste. I rze-
czywiście: dane wyjściowe JSON dla czarno-białego filmu Casablanca nie mają pola color.
Operacja odwrotna do marshalingu, czyli dekodowanie danych JSON i zapełnianie struktury
danych Go, jest nazywana unmarshalingiem i wykonywana przez funkcję json.Unmarshal. Po-
niższy kod unmarshaluje dane o filmach z formatu JSON na wycinek struktur, których jedynym
polem jest Title. Przez definiowanie w ten sposób odpowiednich struktur danych Go możemy
wybierać, które części danych wejściowych JSON dekodować, a które porzucać. Gdy funkcja Unmarshal
powróci z wykonywania, wycinek będzie zapełniony informacjami z pola Title. Pozostałe nazwy
z danych JSON zostaną zignorowane.
var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
log.Fatalf("Unmarshaling z formatu JSON nie powiódł się: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Nieugięty Luke} {Bullitt}]"
Wiele usług internetowych zapewnia interfejs JSON — wysyłasz żądanie HTTP i w odpowiedzi
otrzymujesz wymagane informacje w formacie JSON. Zilustrujmy to na podstawie kwerendy sys-
temu zgłoszeń GitHuba, używając jego interfejsu usługi internetowej. Najpierw zdefiniujemy nie-
zbędne typy i stałe:
code/r04/github
// Package github zapewnia interfejs API języka Go dla systemu zgłoszeń GitHuba.
// Zobacz: https://developer.github.com/v3/search/#search-issues.
package github

import "time"

const IssuesURL = "https://api.github.com/search/issues"

type IssuesSearchResult struct {


TotalCount int `json:"total_count"`
Items []*Issue
}

type Issue struct {


Number int
HTMLURL string `json:"html_url"`
Title string
State string
User *User
CreatedAt time.Time ` json:"created_at"`
Body string // w formacie Markdown
}

type User struct {


Login string
HTMLURL string ` json:"html_url"`
}
118 ROZDZIAŁ 4. TYPY ZŁOŻONE

Tak jak poprzednio nazwy wszystkich pól struktury muszą rozpoczynać się wielką literą, nawet
jeśli ich nazwy JSON są zapisane małymi literami. Jednak proces dopasowywania, który dokonuje
powiązania nazw JSON z nazwami struktur Go podczas unmarshalingu, nie uwzględnia wielkości
liter, więc użycie znacznika pola jest konieczne tylko wtedy, gdy w nazwie JSON pojawia się pod-
kreślnik, którego nie ma w nazwie Go. Ponownie selektywnie traktujemy pola, które mają być deko-
dowane. Odpowiedź przeszukiwania GitHuba zawiera znacznie więcej informacji niż tu pokazujemy.
Funkcja SearchIssues wysyła żądanie HTTP i dekoduje wynik do postaci JSON. Ponieważ wa-
runki zapytania przedstawione przez użytkownika mogą zawierać znaki takie jak ? oraz &, które
mają specjalne znaczenie w adresie URL, używamy funkcji url.QueryEscape, aby zapewnić, że
będą one przyjmowane literalnie.
code/r04/github
package github
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
// SearchIssues kwerenduje system zgłoszeń GitHuba.
func SearchIssues(terms []string) (*IssuesSearchResult, error) {
q := url.QueryEscape(strings.Join(terms, " "))
resp, err := http.Get(IssuesURL + "?q=" + q)
if err != nil {
return nil, err
}
// Musimy zamknąć resp.Body we wszystkich ścieżkach wykonywania.
// (W rozdziale 5. przedstawiona zostanie instrukcja 'defer', która to ułatwia).
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("Kwerenda wyszukiwania nie powiodła się: %s",
resp.Status)
}
var result IssuesSearchResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
resp.Body.Close()
return nil, err
}
resp.Body.Close()
return &result, nil
}
Wcześniejsze przykłady wykorzystywały funkcję json.Unmarshal do dekodowania całej zawarto-
ści wycinka bajtów jako pojedynczej encji JSON. Dla odmiany w tym przykładzie wykorzystywany
jest dekoder strumieniowy json.Decoder, który umożliwia dekodowanie kilku encji JSON w se-
kwencji z tego samego strumienia, chociaż nie potrzebujemy tutaj tej funkcji. Jak można się spo-
dziewać, istnieje też odpowiedni koder strumieniowy o nazwie json.Encoder.
Wywołanie funkcji Decode zapełnia zmienną result. Istnieją różne sposoby przyzwoitego sfor-
matowania jej wartości. Najprostszym sposobem, przedstawionym poniżej za pomocą polecenia
issues, jest tabela tekstowa ze stałą szerokością kolumn, ale w następnym podrozdziale zobaczymy
bardziej wyrafinowane podejście oparte na szablonach.
4.5. JSON 119

code/r04/issues
// Issues wyświetla tabelę tematów GitHuba odpowiadających kryteriom wyszukiwania.
package main
import (
"fmt"
"log"
"os"
"code/r04/github"
)
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d tematów:\n", result.TotalCount)
for _, item := range result.Items {
fmt.Printf("#%-5d %9.9s %.55s\n",
item.Number, item.User.Login, item.Title)
}
}
Argumenty wiersza poleceń określają warunki wyszukiwania. Poniższe polecenie kwerenduje
system śledzenia problemów projektu Go pod kątem listy otwartych błędów związanych z deko-
dowaniem JSON:
$ go build code/r04/issues
$ ./issues repo:golang/go is:open json decoder
13 tematów:
#5680 eaigner encoding/json: set key converter on en/decoder
#6050 gopherbot encoding/json: provide tokenizer
#8658 gopherbot encoding/json: use bufio
#8462 kortschak encoding/json: UnmarshalText confuses json.Unmarshal
#5901 rsc encoding/json: allow override type marshaling
#9812 klauspost encoding/json: string tag not symmetric
#7872 extempora encoding/json: Encoder internally buffers full output
#9650 cespare encoding/json: Decoding gives errPhase when unmarshalin
#6716 gopherbot encoding/json: include field name in unmarshal error me
#6901 lukescott encoding/json, encoding/xml: option to treat unknown fi
#6384 joeshaw encoding/json: encode precise floating point integers u
#6647 btracey x/tools/cmd/godoc: display type kind of each named type
#4237 gjemiller encoding/base64: URLEncoding padding is optional
Interfejs usługi internetowej GitHuba, opisany na stronie: https://developer.github.com/v3/, ma wiele
więcej funkcji niż jesteśmy w stanie tutaj przedstawić.
Ćwiczenie 4.10. Zmodyfikuj program issues, aby raportował wyniki w kategoriach ich wieku,
np.: młodsze niż miesiąc, młodsze niż rok oraz starsze niż rok.
Ćwiczenie 4.11. Zbuduj narzędzie, które pozwala użytkownikom tworzyć, czytać, aktualizować
i usuwać tematy GitHuba z poziomu wiersza poleceń i wywołuje preferowany edytor tekstu, gdy
wymagane jest wprowadzenie znacznej ilości tekstu.
Ćwiczenie 4.12. Popularny komiks internetowy xkcd posiada interfejs JSON. Na przykład żądanie
wysłane do adresu: https://xkcd.com/571/info.0.json generuje szczegółowy opis 571. numeru komiksu,
jednego z wielu ulubionych. Pobierz zawartość każdego adresu URL (raz!) i zbuduj indeks w trybie
offline. Napisz narzędzie xkcd, które wykorzystując ten indeks, wyświetla adres URL i transkrypcję
każdego komiksu odpowiadającego warunkom wyszukiwania podanym w wierszu poleceń.
120 ROZDZIAŁ 4. TYPY ZŁOŻONE

Ćwiczenie 4.13. Oparta na formacie JSON usługa internetowa serwisu Open Movie Database
umożliwia wyszukiwanie filmu na stronie: https://omdbapi.com/ po nazwie i pobieranie obrazu
jego plakatu. Napisz narzędzie poster, które pobiera obraz plakatu do filmu o nazwie podanej
w wierszu poleceń.

4.6. Szablony tekstowe i HTML


Poprzedni przykład wykonuje tylko najprostsze możliwe formatowanie, dla którego funkcja
Printf jest całkowicie odpowiednia. Czasami jednak formatowanie musi być bardziej wyrafino-
wane i pożądane jest wyraźniejsze oddzielenie formatu od kodu. Można to zrobić za pomocą
pakietów text/template i html/template, które zapewniają mechanizm do podstawiania wartości
zmiennych w szablonie tekstowym lub HTML.
Szablon jest łańcuchem znaków lub plikiem zawierającym jedną część lub więcej części umiesz-
czonych w podwójnych nawiasach klamrowych {{. . .}} i nazywanych akcjami. Większość
łańcucha znaków jest wyświetlana literalnie, ale akcje wywołują inne zachowania. Każda akcja
zawiera wyrażenie w języku szablonu (w prostej, ale wszechstronnej notacji do wyświetlania war-
tości), które wybiera pola struktury, wywołuje funkcje i metody, określa sterowanie przepływem
(takie jak instrukcje if-else i pętle range) i tworzy instancje innych szablonów. Prosty łańcuch
szablonu został pokazany poniżej:
code/r04/issuesreport
const templ = `Liczba znalezionych tematów {{.TotalCount}}:
{{range .Items}}----------------------------------------
Numer: {{.Number}}
Użytkownik: {{.User.Login}}
Tytuł: {{.Title | printf "%.64s"}}
Utworzony: {{.CreatedAt | daysAgo}} dni temu
{{end}}`
Ten szablon najpierw wyświetla liczbę pasujących tematów, a następnie dla każdego z nich wy-
świetla numer, użytkownika, tytuł oraz liczbę dni, które upłynęły od momentu utworzenia.
W ramach akcji istnieje pojęcie wartości bieżącej, które jest określane jako „punkt” i zapisywane
za pomocą kropki (.). Punkt początkowo odnosi się do parametru szablonu, którym w tym
przykładzie będzie github.IssuesSearchResult. Akcja {{.TotalCount}} rozwija się do wartości
pola TotalCount wyświetlanej w typowy sposób. Akcje {{range .Items}} oraz {{end}} tworzą
pętlę, więc tekst między nimi jest rozwijany wiele razy, z punktem powiązanym z kolejnymi elemen-
tami Items.
W ramach akcji znak | sprawia, że wynik jednej operacji staje się argumentem drugiej, analogicznie
do uniksowego mechanizmu potoku. W przypadku Title drugą operacją jest funkcja printf,
która jest wbudowanym synonimem dla fmt.Sprintf we wszystkich szablonach. Dla Age drugą
operacją jest następująca po niej funkcja daysAgo, konwertująca pole CreatedAt z wykorzystaniem
time.Since na czas, który upłynął:
func daysAgo(t time.Time) int {
return int(time.Since(t).Hours() / 24)
}
Należy zwrócić uwagę, że typem CreatedAt jest time.Time, a nie string. W ten sam sposób, w jaki
typ może kontrolować swoje formatowanie łańcucha znaków (zob. podrozdział 2.5) poprzez de-
finiowanie określonych metod, typ może również definiować metody do kontrolowania swoich
4.6. SZABLONY TEKSTOWE I HTML 121

zachowań marshalingu i unmarshalingu JSON. Wartość typu time.Time po marshalingu JSON


jest łańcuchem znaków w standardowym formacie.
Generowanie danych wyjściowych za pomocą szablonu jest procesem dwuetapowym. Najpierw
musimy przeprowadzić parsowanie szablonu na odpowiednią reprezentację wewnętrzną, a następnie
wykonać ją na konkretnych danych wejściowych. Parsowanie musi być przeprowadzone tylko
raz. Poniższy kod tworzy i parsuje zdefiniowany wcześniej szablon templ. Należy zwrócić uwagę
na łańcuchowanie wywołań metod. Metoda template.New tworzy i zwraca szablon. Funcs dodaje
daysAgo do zbioru funkcji dostępnych w ramach tego szablonu, a następnie zwraca ten szablon.
Na koniec na wyniku wywoływana jest metoda Parse.
report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ)
if err != nil {
log.Fatal(err)
}
Ponieważ szablony są zwykle ustalane w czasie kompilacji, niepowodzenie parsowania szablonu
wskazuje na błąd krytyczny w programie. Funkcja pomocnicza template.Must sprawia, że obsługa
błędów staje się wygodniejsza: przyjmuje szablon i błąd, sprawdza, czy błąd jest nil (w przeciwnym
przypadku uruchamia procedurę panic), a następnie zwraca szablon. Wrócimy do tej koncepcji
w podrozdziale 5.9.
Gdy szablon zostanie utworzony, rozszerzony o daysAgo, sparsowany i sprawdzony, możemy go
wykonać, używając github.IssuesSearchResult jako źródła danych i os.Stdout jako miejsca
docelowego:
var report = template.Must(template.New("issuelist").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ))

func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
if err := report.Execute(os.Stdout, result); err != nil {
log.Fatal(err)
}
}
Ten program wyświetla prosty raport tekstowy:
$ go build code/r04/issuesreport
$ ./issuesreport repo:golang/go is:open json decoder
Liczba znalezionych tematów 13:
----------------------------------------
Numer: 5680
Użytkownik: eaigner
Tytuł: encoding/json: set key converter on en/decoder
Utworzony: 750 dni temu
----------------------------------------
Numer: 6050
Użytkownik: gopherbot
Tytuł: encoding/json: provide tokenizer
Utworzony: 695 dni temu
----------------------------------------
...
122 ROZDZIAŁ 4. TYPY ZŁOŻONE

Wróćmy teraz do pakietu html/template. Używa on tego samego interfejsu API i języka wyrażeń
co pakiet text/template, ale dodaje funkcje do automatycznego i odpowiedniego dla kontekstu
stosowania sekwencji ucieczek dla łańcuchów znaków pojawiających się w kodach HTML, Java-
Script, CSS lub adresach URL. Funkcje te mogą pomóc w uniknięciu wiecznego problemu bezpie-
czeństwa generowania plików HTML, czyli ataku wstrzyknięcia, w którym atakujący preparuje
wartość łańcucha znaków, taką jak tytuł tematu, aby załączyć złośliwy kod. Jeśli ten łańcuch zostanie
nieprawidłowo zacytowany przez szablon, da atakującemu kontrolę nad stroną.
Poniższy szablon wyświetla listę zagadnień w postaci tabeli HTML. Należy zwrócić uwagę na
inny import:
code/r04/issueshtml
import "html/template"

var issueList = template.Must(template.New("issuelist").Parse(`


<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<h1>Liczba znalezionych tematów {{.TotalCount}}</h1>
<table>
<tr style='text-align: left'>
<th>#</th>
<th>Stan</th>
<th>Użytkownik</th>
<th>Tytuł</th>
</tr>
{{range .Items}}
<tr>
<td><a href='{{.HTMLURL}}'>{{.Number}}</td>
<td>{{.State}}</td>
<td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
<td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))
Poniższe polecenie uruchamia ten nowy szablon na wynikach nieco innego zapytania (wyszukiwanie
według komentującego):
$ go build code/r04/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html
Na rysunku 4.4 został pokazany wygląd tabeli w przeglądarce internetowej. Linki łączą z odpowied-
nimi stronami GitHuba.
Żaden z tematów widocznych na rysunku 4.4 nie stanowi problemu dla HTML, ale możemy lepiej
zobaczyć ten efekt na przykładzie tematów, których tytuły zawierają metaznaki HTML takie jak
& i <. Dla tego przykładu wybraliśmy dwa tematy:
$ ./issueshtml repo:golang/go 3133 10535 >issues2.html
4.6. SZABLONY TEKSTOWE I HTML 123

Rysunek 4.4. Tabela HTML zawierająca listę tematów projektu Go związanych z kodowaniem JSON

Wynik tego zapytania został pokazany na rysunku 4.5. Zauważ, że pakiet html/template automa-
tycznie zastosował dla tytułów sekwencje ucieczki HTML, aby były wyświetlane dosłownie. Gdyby-
śmy omyłkowo użyli pakietu text/template, czteroznakowy łańcuch "&lt;" zostałby wyrende-
rowany jako znak mniejszości "<", a łańcuch "<1ink>" stałby się elementem link, zmieniając
strukturę dokumentu HTML i być może narażając jego bezpieczeństwo.

Rysunek 4.5. Metaznaki HTML w tytułach tematów są wyświetlone prawidłowo

Używając nazwanego typu łańcucha znaków, template.HMTL, zamiast typu string, możemy
wyłączyć to zachowanie autoucieczki dla pól, które zawierają zaufane dane HTML. Podobne typy
nazwane istnieją dla zaufanych danych JavaScript, CSS i adresów URL. Poniższy program demon-
struje tę zasadę za pomocą dwóch pól o tej samej wartości, ale o różnych typach: A jest łańcuchem
znaków, a B jest typem nazwanym template.HTML.
124 ROZDZIAŁ 4. TYPY ZŁOŻONE

code/r04/autoescape
func main() {
const templ = ` <p>A: {{.A}}</p><p>B: {{.B}}</p>`
t := template.Must(template.New("escape").Parse(templ))
var data struct {
A string // niezaufany zwykły tekst
B template.HTML // zaufany HTML
}
data.A = "<b>Witaj!</b>"
data.B = "<b>Witaj!</b>"
if err := t.Execute(os.Stdout, data); err != nil {
log.Fatal(err)
}
}
Na rysunku 4.6 zostały pokazane dane wyjściowe szablonu wyświetlane w przeglądarce. Możemy
zobaczyć, że dla A zastosowano sekwencje ucieczki, ale dla B nie.

Rysunek 4.6. Dla wartości typu string stosowane są sekwencje ucieczki, ale dla wartości template.HTML już nie

Starczyło nam miejsca, aby pokazać tylko najbardziej podstawowe funkcje systemu szablonów. Jak
zawsze więcej informacji można znaleźć w dokumentacji pakietu:
$ go doc text/template
$ go doc html/template
Ćwiczenie 4.14. Utwórz serwer WWW, który kwerenduje GitHub jeden raz, a następnie pozwala
nawigować po liście raportów o błędach, kamieniach milowych i użytkownikach.
Rozdział 5

Funkcje

Funkcja pozwala opakować sekwencję instrukcji jako jednostkę, która może być wywoływana
z innego miejsca w programie, być może wielokrotnie. Funkcje umożliwiają rozbijanie większych
zadań na mniejsze porcje, które równie dobrze mogą być pisane przez różnych ludzi oddzielonych
od siebie w czasie i przestrzeni. Funkcja ukrywa przed użytkownikami szczegóły swojej im-
plementacji. Z tych wszystkich powodów funkcje są kluczowym elementem każdego języka
programowania.
Widzieliśmy już wiele funkcji. Poświęćmy teraz trochę czasu na bardziej szczegółową dyskusję.
Działającym przykładem z tego rozdziału jest robot internetowy, czyli komponent silnika wyszu-
kiwania odpowiedzialny za pobieranie stron internetowych, odkrywanie umieszczonych wewnątrz
nich linków, pobieranie stron identyfikowanych przez te linki itd. Robot internetowy daje nam
wiele okazji eksploracji takich kwestii jak: rekurencja, funkcje anonimowe, obsługa błędów oraz
te aspekty funkcji, które są unikatowe dla języka Go.

5.1. Deklaracje funkcji


Deklaracja funkcji ma nazwę, opcjonalną listę parametrów, opcjonalną listę wyników oraz ciało:
func nazwa(lista_parametrów) (lista_wyników) {
ciało_funkcji
}
Lista parametrów określa nazwy i typy parametrów funkcji, czyli zmiennych lokalnych, których
wartości lub argumenty są dostarczane przez podmiot wywołujący. Lista wyników określa typy
wartości zwracanych przez funkcję. Jeśli funkcja zwraca jeden nienazwany wynik lub nie zwraca
żadnych wyników, nawiasy są opcjonalne i zazwyczaj pomijane. Pominięcie listy wyników całości
oznacza zadeklarowanie funkcji, która nie zwraca żadnej wartości i jest wywoływana dla jej efek-
tów. W poniższej funkcji hypot zmienne x i y są parametrami w deklaracji, 3 i 4 są argumentami
wywołania, a funkcja zwraca wartość typu float64.
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}

fmt.Println(hypot(3, 4)) // "5"


126 ROZDZIAŁ 5. FUNKCJE

Tak jak parametry, wyniki mogą być nazwane. W takim przypadku każda nazwa deklaruje lokalną
zmienną inicjowaną do wartości zerowej dla swojego typu.
Funkcja, która ma listę wyników, musi się kończyć instrukcją return, chyba że wykonywanie
wyraźnie nie może dotrzeć do końca tej funkcji, być może dlatego, że funkcja kończy się wywołaniem
procedury panic lub nieskończoną pętlą for bez instrukcji break.
Jak widzieliśmy w funkcji hypot, sekwencja parametrów lub wyników tego samego typu może być
wydzielona w taki sposób, że sam typ jest zapisywany tylko raz. Te dwie deklaracje są równoważne:
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
Oto cztery sposoby deklarowania funkcji z dwoma parametrami i jednym wynikiem, w której
wszystkie te elementy są typu int. Pusty identyfikator może być używany do podkreślenia, że jakiś
parametr jest niewykorzystywany.
func add(x int, y int) int { return x + y }
func sub(x, y int) (z int) { z = x - y; return }
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }

fmt.Printf("%T\n", add) // "func(int, int) int"


fmt.Printf("%T\n", sub) // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"
Typ funkcji jest czasami nazywany jej sygnaturą. Dwie funkcje mają ten sam typ lub sygnaturę,
jeśli mają taką samą sekwencję typów parametrów i taką samą sekwencję typów wyników. Nazwy
parametrów i wyników nie wpływają na typ, tak samo jak nie ma na to wpływu fakt, czy zostały
one zadeklarowane za pomocą postaci sfaktoryzowanej, czy nie.
Każde wywołanie funkcji musi dostarczyć argument dla każdego parametru w kolejności, w jakiej
parametry zostały zadeklarowane. W języku Go nie funkcjonuje pojęcie domyślnych wartości
parametrów i nie istnieje żaden sposób określania argumentów przez nazwę, więc nazwy parame-
trów i wyników nie mają znaczenia dla podmiotu wywołującego, z wyjątkiem przypadków opisa-
nych w dokumentacji.
Parametry są zmiennymi lokalnymi w obrębie ciała funkcji, a ich wartości początkowe są ustawiane
na argumenty dostarczane przez podmiot wywołujący. Parametry i nazwane wyniki funkcji są zmien-
nymi w tym samym bloku leksykalnym co najbardziej zewnętrzne zmienne lokalne tej funkcji.
Argumenty są przekazywane przez wartość, więc funkcja otrzymuje kopię każdego argumentu.
Modyfikacje kopii nie mają wpływu na podmiot wywołujący. Jeśli jednak argument zawiera jakieś
referencje (jak wskaźnik, wycinek, mapa, funkcja lub kanał), wtedy na podmiot wywołujący mogą mieć
wpływ wszelkie modyfikacje wprowadzane przez funkcję w zmiennych, do których dany argument
odwołuje się pośrednio.
Można niekiedy napotkać deklarację funkcji bez ciała, co wskazuje, że dana funkcja jest zaimple-
mentowana w języku innym niż Go. Taka deklaracja definiuje sygnaturę funkcji.
package math

func Sin(x float64) float64 // zaimplementowana w języku asemblera


5.2. REKURENCJA 127

5.2. Rekurencja
Funkcje mogą być rekurencyjne, czyli mogą wywoływać same siebie bezpośrednio lub pośrednio.
Rekurencja jest wszechstronną techniką pozwalającą rozwiązywać wiele problemów i oczywiście
jest niezbędna do przetwarzania rekurencyjnych struktur danych. W podrozdziale 4.4 użyliśmy
rekurencji dla drzewa, aby zaimplementować proste sortowanie przez wstawianie. W tym pod-
rozdziale użyjemy jej ponownie do przetwarzania dokumentów HTML.
Przedstawiony poniżej przykładowy program wykorzystuje niestandardowy pakiet golang.org/
x/net/html, który zapewnia parser HTML. Repozytoria golang.org/x/... przechowują pakiety
zaprojektowane i utrzymywane przez zespół Go do zastosowań takich jak: aplikacje sieciowe,
przetwarzanie umiędzynarodowionego tekstu, platformy mobilne, manipulowanie obrazami,
kryptografia i narzędzia dla deweloperów. Te pakiety nie znajdują się w standardowej bibliotece,
ponieważ są ciągle w fazie rozwoju lub rzadko są wykorzystywane przez większość programistów Go.
Potrzebne nam części interfejsu API pakietu golang.org/x/net/html przedstawiono poniżej.
Funkcja html.Parse odczytuje sekwencję bajtów, parsuje je i zwraca korzeń drzewa dokumentu HTML,
którym jest węzeł html.Node. HTML ma kilka rodzajów węzłów (tekstowe, komentarzowe itd.),
ale tutaj zajmujemy się tylko węzłami elementów w postaci <nazwa klucz='wartość'>.
golang.org/x/net/html
package html

type Node struct {


Type NodeType
Data string
Attr []Attribute
FirstChild, NextSibling *Node
}

type NodeType int32

const (
ErrorNode NodeType = iota
TextNode
DocumentNode
ElementNode
CommentNode
DoctypeNode
)

type Attribute struct {


Key, Val string
}

func Parse(r io.Reader) (*Node, error)


Funkcja main parsuje standardowy strumień wejściowy jako HTML, wyodrębnia linki za pomocą
rekurencyjnej funkcji visit i wyświetla każdy znaleziony link:
code/r05/findlinks1
// Findlinks1 wyświetla linki znalezione w dokumencie HTML
// odczytanym ze standardowego strumienia wejściowego.
package main
128 ROZDZIAŁ 5. FUNKCJE

import (
"fmt"
"os"

"golang.org/x/net/html"
)

func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err)
os.Exit(1)
}
for _, link := range visit(nil, doc) {
fmt.Println(link)
}
}
Funkcja visit trawersuje drzewo węzłów HTML, wyodrębnia link z atrybutu href każdego
elementu kotwicy <a href='...'>, dodaje linki do wycinka łańcuchów znaków i zwraca powstały
wycinek:
// visit dołącza do zmiennej links każdy link znaleziony w n, a następnie zwraca wynik.
func visit(links []string, n *html.Node) []string {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
links = append(links, a.Val)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = visit(links, c)
}
return links
}
Aby przejść po drzewie do węzła n, funkcja visit wywołuje rekurencyjnie samą siebie dla każdego
z potomków węzła n, które są przechowywane w liście powiązanej FirstChild.
Uruchommy program findlinks dla strony głównej projektu Go, przekierowując za pomocą po-
toku strumień wyjściowy programu fetch (zob. podrozdział 1.5) do strumienia wejściowego pro-
gramu findlinks. Dla zwięzłości dane wyjściowe zostały nieco wyedytowane.
$ go build code/r01/fetch
$ go build code/r05/findlinks1
$ ./fetch https://golang.org | ./findlinks1
#
/doc/
/pkg/
/help/
/blog/
http://play.golang.org/
//tour.golang.org/
https://golang.org/dl/
//blog.golang.org/
/LICENSE
/doc/tos.html
http://www.google.com/intl/en/policies/privacy/
5.2. REKURENCJA 129

Należy zwrócić uwagę na różnorodność form linków, które pojawiają się na tej stronie. Później
zobaczymy, jak rozwiązać je względem bazowego adresu URL, https://golang.org/, aby uzyskać bez-
względne adresy URL.
Następny program używa rekurencji na drzewie węzłów HTML, aby wyświetlić w zarysie strukturę
tego drzewa. Znacznik każdego napotykanego elementu jest umieszczany na stosie, który jest
następnie wyświetlany.
code/r05/outline
func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "outline: %v\n", err)
os.Exit(1)
}
outline(nil, doc)
}

func outline(stack []string, n *html.Node) {


if n.Type == html.ElementNode {
stack = append(stack, n.Data) // umieszczenie znacznika na stosie
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
}
Należy zwrócić uwagę na pewien szczegół: chociaż funkcja outline umieszcza element na stosie
(stack), nie istnieje odpowiadająca jej funkcja zdejmująca element ze stosu. Gdy funkcja outline
wywołuje rekurencyjnie samą siebie, wywoływana funkcja otrzymuje kopię argumentu stack.
Chociaż wywoływana funkcja może dołączać elementy do tego wycinka, modyfikując jego tablicę
bazową lub nawet alokując nową tablicę, to nie zmienia początkowych elementów, które są widoczne
dla podmiotu wywołującego. Dlatego gdy funkcja powraca z wykonywania, argument stack
podmiotu wywołującego jest taki, jaki był przed wywołaniem.
Oto zarys drzewa strony: https://golang.org/, ponownie wyedytowany dla zwięzłości:
$ go build code/r05/outline
$ ./fetch https://golang.org | ./outline
[html]
[html head]
[html head meta]
[html head title]
[html head link]
[html body]
[html body div]
[html body div]
[html body div div]
[html body div div form]
[html body div div form div]
[html body div div form div a]
...
Jak widać po eksperymencie z funkcją outline, większość dokumentów HTML można przetworzyć
jedynie za pomocą kilku poziomów rekurencji, ale nie jest trudno skonstruować patologiczne
strony internetowe, które wymagają bardzo głębokiej rekurencji.
130 ROZDZIAŁ 5. FUNKCJE

Wiele implementacji języków programowania używa stałych rozmiarów stosu wywołań funkcji.
Typowe są rozmiary od 64 kB do 2 MB. Stosy o stałym rozmiarze nakładają ograniczenia na głę-
bokość rekurencji, należy więc być ostrożnym, aby uniknąć przepełnienia stosu podczas rekuren-
cyjnej trawersacji dużych struktur danych. Stosy o stałych rozmiarach mogą nawet stanowić za-
grożenie dla bezpieczeństwa. W przeciwieństwie do tego typowe implementacje Go używają stosów
o zmiennym rozmiarze, które na początku są niewielkie i rosną w miarę potrzeb do rozmiarów
rzędu gigabajtów. Pozwala to bezpiecznie korzystać z rekurencji, bez obawy o przepełnienie.
Ćwiczenie 5.1. Zmień program findlinks w taki sposób, aby trawersował listę powiązaną
n.FirstChild za pomocą rekurencyjnych wywołań funkcji visit zamiast pętli.
Ćwiczenie 5.2. Napisz funkcję, która zapewnia mapowanie nazw elementów (p, div, span itd.) na
liczbę elementów o danej nazwie znajdujących się w drzewie dokumentu HTML.
Ćwiczenie 5.3. Napisz funkcję służącą do wyświetlania zawartości wszystkich węzłów tekstowych
znajdujących się w drzewie dokumentu HTML. Nie schodź do elementów <script> lub <style>,
ponieważ ich zawartość nie jest widoczna w przeglądarce internetowej.
Ćwiczenie 5.4. Rozszerz funkcję visit w taki sposób, aby wyodrębniała z dokumentu inne rodzaje
linków, takie jak obrazy, skrypty i arkusze stylów.

5.3. Zwracanie wielu wartości


Funkcja może zwracać więcej niż jeden wynik. Widzieliśmy wiele przykładów funkcji ze standar-
dowych pakietów, które zwracają dwie wartości: żądany wynik obliczeń oraz wartość błędu lub
wartość logiczną, wskazującą, czy obliczenia zadziałały. Kolejny przykład pokazuje, jak napisać
własną funkcję tego typu.
Poniższy program jest wariacją programu findlinks, która sama wysyła żądanie HTTP, więc
nie trzeba już uruchamiać programu fetch. Ponieważ operacje HTTP i parsowania mogą się nie
powieść, funkcja findLinks deklaruje dwa wyniki: listę wykrytych linków oraz błąd. Nawiasem
mówiąc, parser HTML może zwykle odzyskać sprawność po nieprawidłowych danych wejścio-
wych i skonstruować dokument zawierający węzły błędów, więc funkcja Parse rzadko zawodzi.
Kiedy już jednak zawiedzie, zazwyczaj jest to spowodowane bazowymi błędami we-wy.
code/r05/findlinks2
func main() {
for _, url := range os.Args[1:] {
links, err := findLinks(url)
if err != nil {
fmt.Fprintf(os.Stderr, "findlinks2: %v\n", err)
continue
}
for _, link := range links {
fmt.Println(link)
}
}
}

// findLinks wysyła żądanie HTTP GET dla adresu URL,


// parsuje odpowiedź jako HTML, a następnie wyodrębnia i zwraca linki.
func findLinks(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
5.3. ZWRACANIE WIELU WARTOŚCI 131

}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("pobieranie %s: %s", url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsowanie %s as HTML: %v", url, err)
}
return visit(nil, doc), nil
}
W funkcji findLinks istnieją cztery instrukcje return, z których każda zwraca parę wartości.
Pierwsze trzy instrukcje return powodują przekazywanie przez funkcję do podmiotu wywołujące-
go bazowych błędów z pakietów http i html. W pierwszym przypadku błąd jest zwracany w nie-
zmienionej postaci. W drugim i trzecim jest wzbogacany o dodatkowe informacje kontekstowe
przez funkcję fmt.Errorf (zob. podrozdział 7.8). Jeśli wykonywanie funkcji findLinks się powie-
dzie, ostatnia instrukcja return zwraca wycinek linków bez żadnego błędu.
Trzeba się upewnić, że resp.Body zostanie zamknięte, aby zasoby sieciowe zostały prawidłowo
zwolnione nawet w przypadku błędu. Mechanizm odzyskiwania pamięci języka Go odzyskuje
nieużywaną pamięć, ale nie należy zakładać, że zwolni niewykorzystywane zasoby systemu ope-
racyjnego, takie jak otwarte pliki i połączenia sieciowe. Należy je zamknąć w sposób bezpośredni.
Wynikiem wywołania funkcji zwracającej wiele wartości jest krotka wartości. Podmiot wywołujący
taką funkcję musi bezpośrednio przypisać wartości do zmiennych, jeśli któraś z nich ma zostać
użyta:
links, err := findLinks(url)
Aby zignorować jedną z wartości, należy przypisać ją do pustego identyfikatora:
links, _ := findLinks(url) // błędy są ignorowane
Wynik wywołania wielowartościowego sam może być zwracany z funkcji wywołującej (wielowar-
tościowej), tak jak w tej funkcji, która zachowuje się jak findLinks, ale rejestruje swój argument:
func findLinksLog(url string) ([]string, error) {
log.Printf("findLinks %s", url)
return findLinks(url)
}
Wywołanie wielowartościowe może się pojawić jako jedyny argument przy wywoływaniu funkcji
o wielu parametrach. Choć ta cecha jest rzadko wykorzystywana w kodzie działającym w środowisku
produkcyjnym, to bywa czasem wygodna podczas debugowania, ponieważ pozwala wyświetlać
wszystkie wyniki wywołania przy użyciu pojedynczej instrukcji. Poniższe dwie instrukcje mają ten
sam efekt.
log.Println(findLinks(url))

links, err := findLinks(url)


log.Println(links, err)
Dobrze dobrane nazwy mogą dokumentować znaczenie wyników funkcji. Nazwy są szczególnie
cenne, gdy funkcja zwraca wiele wyników tego samego typu, np.:
func Size(rect image.Rectangle) (width, height int)
func Split(path string) (dir, file string)
func HourMinSec(t time.Time) (hour, minute, second int)
132 ROZDZIAŁ 5. FUNKCJE

Nie zawsze jednak konieczne jest nazywanie takich wyników wyłącznie dla celów dokumentacyj-
nych, np. zgodnie z konwencją końcowy wynik bool oznacza powodzenie, a wynik error najczęściej
nie wymaga wyjaśnienia.
W funkcji z wynikami nazwanymi operandy instrukcji return mogą być pomijane. Nazywa się to
nagim zwracaniem (ang. bare return).
// CountWordsAndImages wysyła żądanie HTTP GET dla adresu URL
// dokumentu HTML i zwraca liczbę znajdujących się w nim słów i obrazów.
func CountWordsAndImages(url string) (words, images int, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
err = fmt.Errorf("parsowanie HTML: %s", err)
return
}
words, images = countWordsAndImages(doc)
return
}

func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }


Nagie zwracanie jest skróconym sposobem zwracania każdej ze zmiennych wyników nazwanych
w kolejności, więc w powyższej funkcji każda instrukcja return jest równoważna z instrukcją:
return words, images, err
W funkcjach takich jak ta, z wieloma instrukcjami return i kilkoma wynikami, nagie zwracanie
może zredukować duplikowanie kodu, ale rzadko ułatwia jego zrozumienie. Nie jest np. oczywiste
na pierwszy rzut oka, że dwie pierwsze instrukcje return są równoważne z return 0, 0, err
(ponieważ zmienne wynikowe words i images są inicjowane do swoich wartości zerowych), a ostatnia
instrukcja return jest równoważna z return words, images, err. Z tego powodu najlepiej stoso-
wać nagie zwracanie z umiarem.
Ćwiczenie 5.5. Zaimplementuj funkcję CountWordsAndImages. (Wskazówka dotycząca rozdziela-
nia tekstu na słowa została zamieszczona w ćwiczeniu 4.9).
Ćwiczenie 5.6. Zmodyfikuj funkcję corner w programie code/r03/surface (zob. podrozdział
3.2), aby używała wyników nazwanych i instrukcji nagiego zwracania.

5.4. Błędy
Niektóre funkcje zawsze z powodzeniem wykonują swoje zadania. Funkcje strings.Contains
i strconv.FormatBool mają np. dobrze zdefiniowane wyniki dla wszystkich możliwych wartości
argumentów i nie mogą zawieść — jeśli nie brać pod uwagę katastroficznych i nieprzewidywal-
nych scenariuszy, takich jak wyczerpanie się pamięci, w przypadku których objawy leżą daleko od
przyczyny i istnieje znikoma nadzieja na odzyskanie sprawności.
Inne funkcje są wykonywane z powodzeniem zawsze, jeśli spełnione są ich warunki wstępne.
Przykładowo: funkcja time.Date zawsze konstruuje typ time.Time ze swoich komponentów (rok,
miesiąc itd.), chyba że ostatni argument (strefa czasowa) ma wartość nil, co wywołuje procedurę
5.4. BŁĘDY 133

panic. Uruchomienie tej procedury jest pewną oznaką błędu w kodzie wywołującym i nigdy nie
powinno się zdarzyć w dobrze napisanym programie.
Powodzenie wykonywania wielu innych funkcji, nawet w dobrze napisanym programie, nie jest
gwarantowane, ponieważ zależy od czynników niezależnych od programistów. Każda funkcja,
która wykonuje np. operacje we-wy, musi się zmierzyć z możliwością wystąpienia błędu i tylko
naiwny programista wierzy, że proste operacje odczytu lub zapisu nie mogą się nie powieść. To
właśnie wtedy, gdy niespodziewanie nie udają się najbardziej niezawodne operacje, zachodzi
najwyższa potrzeba dowiedzenia się, dlaczego tak jest.
Błędy są zatem ważnym elementem interfejsu API pakietu lub interfejsu użytkownika aplikacji,
a niepowodzenie jest tylko jednym z kilku oczekiwanych zachowań. Takie jest właśnie podejście
do obsługi błędów w języku Go.
Funkcja, dla której niepowodzenie jest oczekiwanym zachowaniem, zwraca dodatkowy wynik,
umownie ostatni. Jeżeli niepowodzenie ma tylko jedną możliwą przyczynę, wynik jest wartością lo-
giczną, zwykle nazywaną ok, tak jak w tym przykładzie przeszukiwania pamięci podręcznej, które
zawsze się udaje, chyba że nie było wpisu dla danego klucza:
value, ok := cache.Lookup(key)
if !ok {
// …cache[key] nie istnieje…
}
Znacznie częściej, szczególnie w przypadku operacji we-wy, awaria może mieć wiele przyczyn,
dla których podmiot wywołujący potrzebuje wyjaśnienia. W takich przypadkach typem dodat-
kowego wyniku jest error.
Wbudowany typ error jest typem interfejsowym. Szerzej o jego znaczeniu i implikacjach dla
obsługi błędów porozmawiamy w rozdziale 7. Na razie wystarczy wiedzieć, że typ error może
mieć wartość nil (co oznacza powodzenie) lub wartość inną niż nil (co oznacza niepowodzenie).
Ponadto typ error o wartości innej niż nil ma łańcuch znaków dla komunikatu błędu, który
można uzyskać poprzez wywołanie jego metody Error albo wyświetlić poprzez wywołanie me-
tody fmt.Println(err) lub fmt.Printf("%v", err).
Zwykle, gdy funkcja zwraca błąd inny niż nil, jej pozostałe wyniki są niezdefiniowane i powinny
być ignorowane. Jednak kilka funkcji może w przypadkach błędów zwracać częściowe wyniki.
Jeśli błąd występuje np. podczas odczytywania danych z pliku, wywołanie funkcji Read zwraca
liczbę bajtów, które udało się odczytać, oraz wartość error opisującą problem. Do poprawnego
zachowania niektóre podmioty wywołujące mogą wymagać przetworzenia niekompletnych da-
nych przed rozpoczęciem obsługi błędu, więc ważne jest, aby takie funkcje jasno dokumentowały
swoje wyniki.
Podejście języka Go odróżnia go od wielu innych języków programowania, w których błędy są
zgłaszane za pomocą wyjątków, a nie zwykłych wartości. Chociaż Go posiada pewnego rodzaju
mechanizm obsługi wyjątków (jak zobaczymy w podrozdziale 5.9), jest on używany tylko do ra-
portowania naprawdę nieoczekiwanych awarii, które wskazują na usterkę, a nie na rutynowe
błędy wykonywania, które powinny być przewidziane podczas budowania solidnego programu.
Powodem takiego rozwiązania jest to, że wyjątki zwykle oplątują opis błędu przepływem sterowa-
nia wymaganym do jego obsługi, co często prowadzi do niepożądanego rezultatu: rutynowe błędy
są zgłaszane użytkownikowi końcowemu w postaci niezrozumiałego śladu stosu, pełnego informacji
o strukturze programu, ale niezawierającego jasnego kontekstu dotyczącego tego, co poszło źle.
134 ROZDZIAŁ 5. FUNKCJE

Natomiast programy Go używają do reagowania na błędy zwykłych mechanizmów przepływu


sterowania, takich jak instrukcje if oraz return. Ten styl niezaprzeczalnie wymaga zwracania
większej uwagi na logikę obsługi błędów, ale o to właśnie chodzi.

5.4.1. Strategie obsługi błędów


Gdy wywołanie funkcji zwraca błąd, obowiązkiem podmiotu wywołującego jest jego sprawdzenie
i podjęcie odpowiedniego działania. W zależności od sytuacji może istnieć wiele możliwości.
Przyjrzyjmy się pięciu z nich.
Pierwszą i najbardziej powszechną jest propagacja błędu, aby awaria w podprocedurze stawała
się awarią procedury wywołującej. Widzieliśmy tego przykłady w funkcji findLinks w podroz-
dziale 5.3. Jeśli nie powiedzie się wywołanie funkcji http.Get, funkcja findLinks bez ceregieli
zwróci podmiotowi wywołującemu błąd HTTP:
resp, err := http.Get(url)
if err != nil {
return nil, err
}
W przeciwieństwie do tego, jeśli zawiedzie wywołanie funkcji html.Parse, funkcja findLinks
nie zwróci błędu parsera HTML bezpośrednio, ponieważ brakuje mu dwóch zasadniczych frag-
mentów informacji: tego, że błąd wystąpił w parserze, oraz adresu URL dokumentu, który był par-
sowany. W takim przypadku findLinks konstruuje nowy komunikat o błędzie, który obejmuje
oba fragmenty informacji oraz bazowy błąd parsowania:
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsowanie %s jako HTML: %v", url, err)
}
Funkcja fmt.Errorf formatuje komunikat o błędzie przy użyciu funkcji fmt.Sprintf i zwraca nową
wartość error. Używamy jej do zbudowania opisowych błędów poprzez sukcesywne poprzedzanie
oryginalnego komunikatu o błędzie dodatkowymi informacjami kontekstowymi. Gdy błąd zo-
stanie ostatecznie obsłużony przez funkcję main programu, powinien zostać przedstawiony ja-
sny łańcuch przyczynowy prowadzący od głównego problemu do ogólnej awarii, przypominający
raport NASA z badania wypadku:
geneza: kraksa: brak spadochronu: awaria przełącznika G: nieprawidłowe umieszczenie
przekaźnika
Ponieważ komunikaty o błędach są często połączone w łańcuch, łańcuchy znaków komunikatów nie
powinny być rozpoczynane wielką literą i należy unikać znaków nowej linii. Powstałe błędy mo-
gą być długie, ale będą samowystarczalne, gdy zostaną znalezione przez narzędzia takie jak grep.
Przy projektowaniu komunikatów o błędach należy postępować w sposób przemyślany, aby każdy
komunikat zawierał znaczący opis problemu z wystarczającą ilością istotnych szczegółów. Komuni-
katy powinny również być spójne, aby błędy zwracane przez tę samą funkcję lub grupę funkcji w tym
samym pakiecie miały podobną formę i aby można było radzić sobie z nimi w ten sam sposób.
Pakiet os gwarantuje np., że każdy błąd zwracany przez operacje plikowe (takie jak os.Open) albo me-
tody (Read, Write lub Close) otwartego pliku będzie opisywał nie tylko naturę awarii (brak dostępu,
nie ma takiego katalogu itd.), ale również nazwę pliku, aby podmiot wywołujący nie musiał zawie-
rać tej informacji w konstruowanym przez siebie komunikacie o błędzie.
5.4. BŁĘDY 135

Zasadniczo wywołanie funkcji f(x) jest odpowiedzialne za zgłoszenie próby wykonania operacji
f oraz wartości argumentu x, ponieważ odnoszą się one do kontekstu błędu. Podmiot wywołujący jest
odpowiedzialny za załączenie posiadanych przez siebie dalszych informacji, których nie posiada wywo-
łanie f(x), takich jak adres URL w przedstawionym wcześniej wywołaniu funkcji html.Parse.
Przejdźmy do drugiej strategii obsługi błędów. W przypadku błędów, które reprezentują przej-
ściowe lub nieprzewidziane problemy, sensowne może być ponawianie nieudanej operacji, być
może z pewnym opóźnieniem pomiędzy próbami, a być może także z limitem liczby prób lub
czasu poświęconego na ponawianie przed całkowitym zrezygnowaniem z kolejnych prób.
code/r05/wait
// WaitForServer próbuje się połączyć z serwerem adresu URL.
// Próby są ponawiane przez minutę z wykorzystaniem algorytmu exponential back-off.
// Jeśli wszystkie próby zawiodą, raportowany jest błąd.
func WaitForServer(url string) error {
const timeout = 1 * time.Minute
deadline := time.Now().Add(timeout)
for tries := 0; time.Now().Before(deadline); tries++ {
_, err := http.Head(url)
if err == nil {
return nil // powodzenie
}
log.Printf("serwer nie odpowiada (%s); ponawianie...", err)
time.Sleep(time.Second << uint(tries)) // exponential back-off
}
return fmt.Errorf("serwer %s nie odpowiedział po %s", url, timeout)
}
W przypadku trzeciej strategii, jeśli postęp jest niemożliwy, podmiot wywołujący może wyświetlić
błąd i z wdziękiem zatrzymać program, ale takie postępowanie powinno być zasadniczo zarezerwo-
wane dla głównego pakietu programu. Funkcje biblioteki powinny zazwyczaj propagować błędy do
podmiotu wywołującego, chyba że dany błąd jest oznaką wewnętrznej niespójności, czyli usterką.
// (W funkcji main).
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Strona nie działa: %v\n", err)
os.Exit(1)
}
Wygodniejszym sposobem osiągnięcia tego samego efektu jest wywołanie funkcji log.Fatalf.
Tak jak wszystkie funkcje log, domyślnie poprzedza ona komunikat o błędzie czasem i datą.
if err := WaitForServer(url); err != nil {
log.Fatalf("Strona nie działa: %v\n", err)
}
Ten domyślny format jest pomocny w przypadku długo działającego serwera, ale już mniej
dla interaktywnego narzędzia:
2006/01/02 15:04:05 Strona nie działa: nie ma takiej domeny: bad.gopl.io
Aby uzyskać bardziej atrakcyjne dane wyjściowe, możemy dla prefiksu używanego przez pakiet
log ustawić nazwę polecenia i wyłączyć wyświetlanie daty i czasu:
log.SetPrefix("wait: ")
log.SetFlags(0)
W niektórych przypadkach stosowana jest czwarta strategia, która polega na tym, że wystarczy
po prostu zarejestrować błąd, a następnie kontynuować wykonywanie, być może z ograniczoną
136 ROZDZIAŁ 5. FUNKCJE

funkcjonalnością. Tu również istnieje wybór między wykorzystaniem pakietu log, który dodaje
zwyczajowy prefiks:
if err := Ping(); err != nil {
log.Printf("ping nie odpowiada: %v; sieć wyłączona", err)
}
i przekazywaniem bezpośrednio do standardowego strumienia błędów:
if err := Ping(); err != nil {
fmt.Fprintf(os.Stderr, "ping nie odpowiada: %v; sieć wyłączona\n", err)
}
(Wszystkie funkcje log dołączają znak nowej linii, jeśli nie jest już obecny).
I wreszcie, w rzadkich przypadkach, możemy zastosować piątą strategię i całkowicie zignorować błąd:
dir, err := ioutil.TempDir("", "scratch")
if err != nil {
return fmt.Errorf("nie udało się utworzyć katalogu tymczasowego: %v", err)
}

// …użycie katalogu tymczasowego…

os.RemoveAll(dir) // ignorowanie błędów; $TMPDIR jest czyszczony okresowo


Wywołanie funkcji os.RemoveAll może się nie powieść, ale program to ignoruje, ponieważ system
operacyjny okresowo czyści katalog tymczasowy. W tym przypadku odrzucenie błędu było zamie-
rzone, ale logika programu byłaby taka sama, gdybyśmy zapomnieli się tym zająć. Powinieneś
wypracować sobie zwyczaj brania pod uwagę błędów po każdym wywołaniu funkcji, a kiedy świa-
domie zignorujesz jakiś błąd, wyraźnie udokumentuj swoje intencje.
Obsługa błędów w języku Go ma szczególny rytm. Po sprawdzeniu błędu awaria jest zwykle
rozpatrywana przed rozpatrywaniem powodzenia operacji. Jeśli awaria powoduje powrót funkcji
z wykonywania, logika dla udanej operacji nie jest wcięta w ramach bloku else, ale jest kontynu-
owana na poziomie zewnętrznym. Funkcje z reguły wykazują typową strukturę z serią kontroli
początkowych w celu odrzucenia błędów, a na końcu znajduje się minimalnie wcięta istota funkcji.

5.4.2. End-of-file (EOF)


Zazwyczaj wiele błędów możliwych do zwrócenia przez funkcję jest interesujących dla użytkownika
końcowego, ale nie dla interweniującej logiki programu. Czasem jednak program musi podjąć
różne działania w zależności od rodzaju błędu, który wystąpił. Rozważmy próbę odczytu n bajtów
danych z pliku. Jeśli zdecydujemy, że n jest długością pliku, każdy błąd będzie oznaczał awarię.
Z drugiej strony, jeśli podmiot wywołujący będzie wielokrotnie próbował odczytać kawałki o stałym
rozmiarze aż do wyczerpania pliku, będzie musiał reagować inaczej na warunek końca pliku (ang.
end-of-file) niż na wszystkie pozostałe błędy. Z tego powodu pakiet io gwarantuje, że każde niepo-
wodzenie odczytu spowodowanego przez warunek końca pliku będzie zawsze zgłaszane przez szcze-
gólny błąd io.EOF, który jest definiowany w następujący sposób:
package io
import "errors"
// EOF jest błędem zwracanym przez metodę Read, gdy nie ma już dostępnych żadnych kolejnych danych
// wejściowych.
var EOF = errors.New("EOF")
5.5. WARTOŚCI FUNKCJI 137

Podmiot wywołujący może wykryć ten warunek za pomocą prostego porównania, takiego jak
w poniższej pętli, która odczytuje runy ze standardowego strumienia wejściowego. (Bardziej kom-
pletny przykład zapewnia program charcount z podrozdziału 4.3).
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // odczyt zakończony
}
if err != nil {
return fmt.Errorf("odczyt się nie powiódł: %v", err)
}
// …użycie r…
}
Ponieważ w warunku końca pliku nie ma do zgłoszenia żadnych informacji oprócz faktu jego wystą-
pienia, io.EOF ma ustalony komunikat o błędzie: "EOF". W przypadku innych błędów może za-
chodzić potrzeba zgłoszenia zarówno jakości, jak i „masy” błędu, więc stała wartość błędu się nie
sprawdzi. W podrozdziale 7.11 przedstawimy bardziej systematyczny sposób odróżniania pew-
nych wartości błędów od innych.

5.5. Wartości funkcji


W języku Go funkcje są typami pierwszoklasowymi (ang. first-class values): podobnie jak inne
wartości, wartości funkcji mają typy, które mogą być przypisywane do zmiennych albo przekazy-
wane do funkcji lub z nich zwracane. Wartość funkcji może być wywoływana jak każda inna
funkcja, np.:
func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }

f := square
fmt.Println(f(3)) // "9"

f = negative
fmt.Println(f(3)) // "–3"
fmt.Printf("%T\n", f) // "func(int) int"

f = product // błąd kompilacji: nie można przypisać f(int, int) int do f(int) int
Wartością zerową typu funkcji jest nil. Wywołanie wartości funkcji nil powoduje uruchomienie
procedury panic:
var f func(int) int
f(3) // panic: wywołanie funkcji nil
Wartości funkcji mogą być porównywane z wartością nil:
var f func(int) int
if f != nil {
f(3)
}
Nie są jednak porównywalne, a więc nie mogą być porównywane ze sobą lub wykorzystywane
jako klucze w mapie.
138 ROZDZIAŁ 5. FUNKCJE

Wartości funkcji pozwalają parametryzować funkcje nie tylko w zakresie danych, ale również
zachowania. Standardowe biblioteki zawierają wiele przykładów. Przykładowo: strings.Map
stosuje funkcję do każdego znaku łańcucha, łącząc wyniki w celu utworzenia kolejnego łańcucha
znaków.
func add1(r rune) rune { return r + 1 }

fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"


fmt.Println(strings.Map(add1, "VMS")) // "WNT"

fmt.Println(strings.Map(add1, "Admix")) // "Benjy"


Funkcja findLinks z podrozdziału 5.2 wykorzystuje funkcję pomocniczą visit, aby odwiedzić
wszystkie węzły w dokumencie HTML i zastosować akcję do każdego z nich. Używając wartości
funkcji, możemy oddzielić logikę dla trawersacji drzewa od logiki dla akcji, która ma być zastoso-
wana do każdego węzła, co pozwala ponownie wykorzystywać tę trawersację z innymi akcjami.
code/r05/outline2
// forEachNode wywołuje funkcje pre(x) i post(x) dla każdego węzła x w drzewie o korzeniu w n.
// Obie funkcje są opcjonalne. Funkcja pre jest wywoływana przed odwiedzeniem węzłów potomnych
// (przejście wzdłużne),
// a funkcja post jest wywoływana po ich odwiedzeniu (przejście wsteczne).
func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
if pre != nil {
pre(n)
}

for c := n.FirstChild; c != nil; c = c.NextSibling {


forEachNode(c, pre, post)
}

if post != nil {
post(n)
}
}
Funkcja forEachNode przyjmuje dwie funkcje jako argumenty: jeden wywoływany przed odwie-
dzeniem potomków danego węzła, drugi wywoływany po ich odwiedzeniu. Takie rozwiązanie
daje podmiotowi wywołującemu dużą elastyczność. Funkcje startElement i endElement wyświetlają
np. początkowe i końcowe znaczniki elementu HTML, takie jak <b>...</b>:
var depth int

func startElement(n *html.Node) {


if n.Type == html.ElementNode {
fmt.Printf("%*s<%s>\n", depth*2, "", n.Data)
depth++
}
}

func endElement(n *html.Node) {


if n.Type == html.ElementNode {
depth--
fmt.Printf("%*s</%s>\n", depth*2, "", n.Data)
}
}
5.6. FUNKCJE ANONIMOWE 139

Te funkcje powodują również wcięcie danych wyjściowych za pomocą kolejnej sztuczki fmt.Printf.
Przysłówek * w %*s wyświetla łańcuch znaków dopełniony zmienną liczbą spacji. Szerokość oraz
łańcuch znaków są dostarczane przez argumenty depth*2 oraz "".
Jeśli wywołamy funkcję forEachNode dla dokumentu HTML w ten sposób:
forEachNode(doc, startElement, endElement)
otrzymamy bardziej wyrafinowaną wariację na temat danych wyjściowych wcześniejszego programu
outline:
$ go build code/r05/outline2
$ ./outline2 http://gopl.io
<html>
<head>
<meta>
</meta>
<title>
</title>
<style>
</style>
</head>
<body>
<table>
<tbody>
<tr>
<td>
<a>
<img>
</img>
...
Ćwiczenie 5.7. Rozwiń funkcje startElement i endElement w ogólny pretty-printer HTML. Wy-
świetlaj węzły komentarzowe, węzły tekstowe oraz atrybuty każdego elementu (<a href ="...">).
Jeśli element nie ma potomków, używaj krótkich form, takich jak <img/> zamiast <img></img>.
Napisz test, aby się upewnić, że dane wyjściowe mogą być parsowane z powodzeniem. (Zob. roz-
dział 11.).
Ćwiczenie 5.8. Zmodyfikuj funkcję forEachNode w taki sposób, żeby funkcje pre i post zwracały
wynik logiczny wskazujący, czy kontynuować trawersację. Użyj jej do napisania funkcji ElementByID
z poniższą sygnaturą, znajdującej pierwszy element HTML z określonym atrybutem id. Funkcja
powinna zatrzymywać trawersację, gdy tylko znalezione zostanie dopasowanie.
func ElementByID(doc *html.Node, id string) *html.Node
Ćwiczenie 5.9. Napisz funkcję expand(s string, f func(string) string) string, która zastę-
puje każdy podłańcuch "$foo" w argumencie s tekstem zwracanym przez f("foo").

5.6. Funkcje anonimowe


Funkcje nazwane mogą być deklarowane tylko na poziomie pakietu, ale możemy używać lite-
rału funkcji do oznaczenia wartości funkcji w jakimkolwiek wyrażeniu. Literał funkcji jest zapi-
sywany jak deklaracja funkcji, ale bez nazwy po słowie kluczowym func. Jest to wyrażenie, a jego
wartość nazywa się funkcją anonimową.
140 ROZDZIAŁ 5. FUNKCJE

Literały funkcji pozwalają definiować funkcję w miejscu jej użycia. Wcześniejsze wywołanie
funkcji strings.Map można np. zapisać jako:
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
Co ważniejsze, zdefiniowane w ten sposób funkcje mają dostęp do całego środowiska leksykalnego,
więc funkcja wewnętrzna może się odwoływać do zmiennych zawierającej ją funkcji, tak jak po-
kazuje ten przykład:
code/r05/squares
// squares zwraca funkcję zwracającą kolejną liczbę kwadratową przy każdym wywołaniu.
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}

func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}
Funkcja squares zwraca kolejną funkcję typu func() int. Wywołanie funkcji squares tworzy lo-
kalną zmienną x i zwraca anonimową funkcję, która przy każdym wywołaniu inkrementuje
zmienną x i zwraca jej kwadrat. Drugie wywołanie funkcji squares utworzyłoby drugą zmienną
x i zwróciłoby nową funkcję anonimową inkrementującą tę zmienną.
Przykład funkcji squares pokazuje, że wartości funkcji są nie tylko kodem, ale mogą mieć stan.
Anonimowa funkcja wewnętrzna może uzyskać dostęp do zmiennej lokalnej zawierającej ją funk-
cji squares i zaktualizować tę zmienną. Z powodu tych ukrytych referencji zmiennych klasyfiku-
jemy funkcje jako typy referencyjne i dlatego wartości funkcji nie są porównywalne. Wartości
funkcji takie jak te są implementowane za pomocą techniki zwanej domknięciami, a programiści
Go często używają tego terminu dla wartości funkcji.
Mamy więc ponownie do czynienia z przykładem, w którym czas życia zmiennej nie jest określany
przez jej zakres: zmienna x nadal istnieje po powrocie z funkcji squares w ramach funkcji main,
choć x jest ukryte wewnątrz f.
Jako nieco akademicki przykład funkcji anonimowych rozważmy problem wyznaczania sekwencji
kursów informatycznych, która pozwala spełnić wymagania wstępne dla każdego z nich. Te wy-
magania wstępne zostały podane w poniższej tabeli prereqs, będącej mapowaniem każdego kursu
na listę kursów, które muszą zostać ukończone najpierw.
code/r05/toposort
// prereqs mapuje kursy informatyczne na ich wymagania wstępne.
var prereqs = map[string][]string{
"algorytmy": {"struktury danych"},
"rachunek różniczkowy i całkowy": {"algebra liniowa"},

"kompilatory": {
"struktury danych",
"języki formalne",
5.6. FUNKCJE ANONIMOWE 141

"organizacja procesora",
},

"struktury danych": {"matematyka dyskretna"},


"bazy danych": {"struktury danych"},
"matematyka dyskretna": {"wstęp do programowania"},
"języki formalne": {"matematyka dyskretna"},
"sieci": {"systemy operacyjne"},
"systemy operacyjne": {"struktury danych", "organizacja procesora"},
"języki programowania": {"struktury danych", "organizacja procesora"},
}
Ten typ problemu jest znany jako sortowanie topologiczne. Koncepcyjnie informacje o warunkach
wstępnych tworzą graf skierowany z węzłem dla każdego kursu i krawędziami z każdego kursu
do kursu, od którego dany kurs zależy. Ten graf jest acykliczny: z żadnego kursu nie ma ścieżki,
która prowadzi z powrotem do samej siebie. Za pomocą poniższego kodu możemy obliczyć pra-
widłową sekwencję, używając algorytmu przeszukiwania grafu zwanego przeszukiwaniem w głąb:
func main() {
for i, course := range topoSort(prereqs) {
fmt.Printf("%d:\t%s\n", i+1, course)
}
}

func topoSort(m map[string][]string) []string {


var order []string
seen := make(map[string]bool)
var visitAll func(items []string)
visitAll = func(items []string) {
for _, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}

var keys []string


for key := range m {
keys = append(keys, key)
}

sort.Strings(keys)
visitAll(keys)
return order
}
Gdy anonimowa funkcja wymaga rekurencji, jak w tym przykładzie, musimy najpierw zadeklaro-
wać zmienną, a następnie przypisać do niej tę funkcję anonimową. Gdyby te dwa kroki zostały
połączone w deklaracji, literał funkcji nie znajdowałby się w zakresie zmiennej visitAll, więc
nie miałby sposobu wywoływania samego siebie rekurencyjnie:
visitAll := func(items []string) {
// …
visitAll(m[item]) // błąd kompilacji: niezdefiniowane: visitAll
// …
}
142 ROZDZIAŁ 5. FUNKCJE

Dane wyjściowe z programu toposort przedstawiono poniżej. Jest to deterministyczna, często po-
żądana właściwość, którą nie zawsze otrzymujemy za darmo. W tym przypadku wartościami mapy
prereqs są wycinki, a nie kolejne mapy, więc ich kolejność iteracji jest deterministyczna, a klucze
prereqs zostały posortowane przed początkowymi wywołaniami visitAll.
1: wstęp do programowania
2: matematyka dyskretna
3: struktury danych
4: algorytmy
5: bazy danych
6: języki formalne
7: organizacja procesora
8: języki programowania
9: kompilatory
10: algebra liniowa
11: rachunek różniczkowy i całkowy
12: systemy operacyjne
13: sieci
Wróćmy do naszego przykładu funkcji findLinks. Przenieśliśmy funkcję wyodrębniania linków
links.Extract do osobnego pakietu, ponieważ będziemy jej używać ponownie w rozdziale 8.
Zastąpiliśmy funkcję visit anonimową funkcją, która bezpośrednio dołącza linki do wycinka
links, i użyliśmy funkcji forEachNode do obsługi trawersacji. Ponieważ Extract potrzebuje tylko
funkcji pre, przekazuje wartość nil dla argumentu funkcji post.
code/r05/links
// Package links zapewnia funkcję wyodrębniania linków.
package links

import (
"fmt"
"net/http"

"golang.org/x/net/html"
)

// Extract wysyła żądanie HTTP GET do określonego adresu URL,


// parsuje odpowiedź jako HTML i zwraca linki znajdujące się w dokumencie HTML.
func Extract(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("pobieranie %s: %s", url, resp.Status)
}

doc, err := html.Parse(resp.Body)


resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsowanie %s jako HTML: %v", url, err)
}

var links []string


visitNode := func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
5.6. FUNKCJE ANONIMOWE 143

if a.Key != "href" {
continue
}
link, err := resp.Request.URL.Parse(a.Val)
if err != nil {
continue // ignorowanie złych adresów URL
}
links = append(links, link.String())
}
}
}
forEachNode(doc, visitNode, nil)
return links, nil
}
Zamiast dołączać do wycinka links surową wartość atrybutu href, ta wersja parsuje go jako adres
URL względny w stosunku do bazowego adresu URL dokumentu — resp.Request.URL. Powstały
link ma postać bezwzględną, nadającą się do stosowania w wywołaniu http.Get.
Indeksowanie stron internetowych jest w gruncie rzeczy problemem trawersacji grafu. Przykład
topoSort pokazał algorytm przeszukiwania w głąb. Dla naszego robota internetowego użyjemy
algorytmu przeszukiwania wszerz, przynajmniej na początku. W rozdziale 8. przyjrzymy się tra-
wersacji równoległej.
Poniższa funkcja hermetyzuje istotę przechodzenia wszerz. Podmiot wywołujący zapewnia po-
czątkową listę (worklist) pozycji do odwiedzenia oraz wartość funkcji f, która ma być wywołana
dla każdej pozycji. Każda pozycja jest identyfikowana przez łańcuch znaków. Funkcja f zwraca
listę nowych pozycji, które mają być dołączone do listy worklist. Funkcja breadthFirst powraca,
gdy wszystkie pozycje zostaną odwiedzone. Utrzymuje ona zbiór łańcuchów znaków, aby się upew-
nić, że żadna pozycja nie zostanie odwiedzona dwa razy.
code/r05/findlinks3
// breadthFirst wywołuje funkcję f dla każdej pozycji z listy worklist.
// Wszystkie pozycje zwrócone przez funkcję f są dodawane do worklist.
// Funkcja f jest wywoływana co najwyżej raz dla każdej pozycji.
func breadthFirst(f func(item string) []string, worklist []string) {
seen := make(map[string]bool)
for len(worklist) > 0 {
items := worklist
worklist = nil
for _, item := range items {
if !seen[item] {
seen[item] = true
worklist = append(worklist, f(item)...)
}
}
}
}
Jak wspomniano w rozdziale 3., argument „f (item)...” powoduje, że wszystkie pozycje z listy
zwracanej przez funkcję f są dołączane do listy worklist.
W naszym programie robota internetowego pozycjami są adresy URL. Funkcja crawl, którą dostar-
czymy do funkcji breadthFirst, wyświetla adres URL, wyodrębnia jego linki i zwraca je, aby rów-
nież zostały odwiedzone.
func crawl(url string) []string {
fmt.Println(url)
144 ROZDZIAŁ 5. FUNKCJE

list, err := links.Extract(url)


if err != nil {
log.Print(err)
}
return list
}
Do uruchomienia robota będziemy używać argumentów wiersza poleceń jako początkowych adre-
sów URL.
func main() {
// Indeksowanie stron internetowych za pomocą przechodzenia wszerz,
// rozpoczynane od argumentów wiersza poleceń.
breadthFirst(crawl, os.Args[1:])
}
Zacznijmy indeksowanie stron od adresu: https://golang.org/. Oto niektóre z wynikowych linków:
$ go build code/r05/findlinks3
$ ./findlinks3 https://golang.org
https://golang.org/
https://golang.org/doc/
https://golang.org/pkg/
https://golang.org/project/
https://code.google.com/p/go-tour/
https://golang.org/doc/code.html
https://www.youtube.com/watch?v=XCsL89YtqCs
http://research.swtch.com/gotour
https://vimeo.com/53221560
...
Proces kończy się, gdy wszystkie osiągalne strony internetowe zostaną zaindeksowane lub wyczer-
pie się pamięć komputera.
Ćwiczenie 5.10. Przepisz funkcję topoSort w taki sposób, aby używała map zamiast wycinków,
i wyeliminuj sortowanie wstępne. Upewnij się, że wyniki, choć niedeterministyczne, są prawidłowym
uporządkowaniem topologicznym.
Ćwiczenie 5.11. Wykładowca kursu algebry liniowej postanawia, że rachunek różniczkowy i całkowy
jest teraz warunkiem wstępnym. Rozszerz funkcję topoSort, aby raportowała cykle.
Ćwiczenie 5.12. Funkcje startElement i endElement w programie code/r05/outline2 (zob. pod-
rozdział 5.5) współdzielą zmienną globalną depth. Przekształć je w funkcje anonimowe, które
współdzielą zmienną lokalną dla funkcji outline.
Ćwiczenie 5.13. Zmodyfikuj program crawl w taki sposób, aby tworzył lokalne kopie znalezionych
stron, w razie potrzeby tworząc katalogi. Nie rób kopii stron, które pochodzą z innej domeny.
Przykładowo: jeśli oryginalna strona pochodzi z golang.org, zapisz wszystkie pliki stamtąd, ale wy-
łącz te z vimeo.com.
Ćwiczenie 5.14. Zastosuj funkcję breadthFirst do zbadania innej struktury. Możesz np. użyć
zależności kursów z przykładu topoSort (graf skierowany), hierarchii systemu plików na Twoim
komputerze (drzewo) lub listy tras autobusów komunikacji miejskiej pobranej ze strony interne-
towej przedsiębiorstwa komunikacyjnego działającego w Twoim meście (graf nieskierowany).
5.6. FUNKCJE ANONIMOWE 145

5.6.1. Zastrzeżenie: przechwytywanie zmiennych iteracji


W tym punkcie przyjrzymy się pułapkom reguł zakresu leksykalnego języka Go, które mogą wywo-
łać zaskakujące rezultaty. Ważne jest, żebyś zrozumiał ten problem przed przejściem dalej, ponie-
waż taka pułapka może usidlić nawet doświadczonych programistów.
Rozważmy program, który musi utworzyć zbiór katalogów, a następnie je usunąć. Możemy użyć
wycinka wartości funkcji do przechowywania operacji czyszczenia. (W tym przykładzie dla zwięzłości
pominięta została cała obsługa błędów).
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // UWAGA: niezbędne!
os.MkdirAll(dir, 0755) // tworzy również katalogi nadrzędne
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}

// …wykonywanie pewnych zadań…

for _, rmdir := range rmdirs {


rmdir() // czyszczenie
}
Być może zastanawiasz się, dlaczego przypisaliśmy zmienną pętli d do nowej zmiennej lokalnej
dir w ciele pętli, zamiast po prostu nazwać zmienną pętli dir, tak jak w tym subtelnie nieprawi-
dłowym wariancie:
var rmdirs []func()
for _, dir := range tempDirs() {
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir) // UWAGA: nieprawidłowe!
})
}
Jest to konsekwencją reguł zakresu dla zmiennych pętli. W powyższym wariancie pętla for
wprowadza nowy blok leksykalny, w którym deklarowana jest zmienna dir. Wszystkie wartości
funkcji utworzone przez tę pętlę „przechwytują” i współdzielą tę samą zmienną — adresowalną
lokalizację przechowywania, a nie jej wartość w danym momencie. Wartość zmiennej dir jest aktu-
alizowana w kolejnych iteracjach, więc do czasu wywołania funkcji czyszczenia zmienna dir zo-
stanie zaktualizowana kilka razy przez zakończoną wtedy pętlę. Dlatego zmienna dir będzie prze-
chowywać wartość z ostatniej iteracji, a w konsekwencji wszystkie wywołania funkcji os.RemoveAll
będą próbowały usunąć ten sam katalog.
Najczęściej zmiennej wewnętrznej wprowadzanej w celu obejścia tego problemu (w naszym
przypadku dir) nadawana jest dokładnie taka sama nazwa co zmiennej zewnętrznej, której jest
kopią, co prowadzi do powstania dziwnie wyglądających, ale kluczowych deklaracji zmiennych,
takich jak te:
for _, dir := range tempDirs() {
dir := dir // deklaruje wewnętrzną zmienną dir inicjowaną do zewnętrznej zmiennej dir
// …
}
146 ROZDZIAŁ 5. FUNKCJE

Takie ryzyko nie jest przypisane wyłącznie do pętli for opartych na zakresach (range). Pętla w poniż-
szym przykładzie ma ten sam problem spowodowany niezamierzonym przechwyceniem zmien-
nej indeksowej i.
var rmdirs []func()
dirs := tempDirs()
for i := 0; i < len(dirs); i++ {
os.MkdirAll(dirs[i], 0755) // OK
rmdirs = append(rmdirs, func() {
os.RemoveAll(dirs[i]) // UWAGA: nieprawidłowe!
})
}
Problem przechwytywania zmiennej iteracji jest najczęściej spotykany podczas korzystania z in-
strukcji go (zob. rozdział 8.) lub defer (co zobaczymy za chwilę), ponieważ obie mogą opóźniać
wykonanie wartości funkcji do czasu zakończenia pętli. Problem nie tkwi jednak w samych in-
strukcjach go lub defer.

5.7. Funkcje o zmiennej liczbie argumentów


Funkcja wariadyczna (ang. variadic function) to taka, która może być wywoływana z różną liczbą
argumentów. Najbardziej znane przykłady to funkcja fmt.Printf i jej odmiany. Printf wy-
maga jednego stałego argumentu na początku, a następnie przyjmuje dowolną liczbę kolej-
nych argumentów.
Aby zadeklarować funkcję o zmiennej liczbie argumentów, należy typ ostatniego parametru po-
przedzić wielokropkiem (...), co wskazuje, że dana funkcja może być wywoływana z dowolną
liczbą argumentów tego typu.
code/r05/sum
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
Powyższa funkcja sum zwraca sumę zera lub większej liczby argumentów int. W ciele funkcji typem
zmiennej vals jest wycinek int[]. Gdy wywoływana jest funkcja sum, dla jej parametru vals
można dostarczyć dowolną liczbę wartości.
fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
Podmiot wywołujący pośrednio alokuje tablicę, kopiuje do niej argumenty i przekazuje do funkcji
wycinek całej tablicy. Dlatego ostatnie z powyższych wywołań zachowuje się tak samo jak wywo-
łanie przedstawione poniżej, które pokazuje sposób wywołania funkcji wariadycznej, gdy argu-
menty znajdują się już w wycinku: należy wstawić wielokropek po ostatnim argumencie.
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
Chociaż parametr ...int w ciele funkcji zachowuje się jak wycinek, typ funkcji o zmiennej liczbie
argumentów różni się od typu funkcji z parametrem w postaci zwykłego wycinka.
5.8. ODROCZONE WYWOŁANIA FUNKCJI 147

func f(...int) {}
func g([]int) {}

fmt.Printf("%T\n", f) // "func(…int)"
fmt.Printf("%T\n", g) // "func([]int)"
Funkcje o zmiennej liczbie argumentów są często używane do formatowania łańcucha znaków.
Poniższa funkcja errorf konstruuje sformatowany komunikat o błędzie z numerem linii na po-
czątku. Przyrostek f jest szeroko stosowaną konwencją nazewnictwa dla funkcji wariadycznych,
które akceptują łańcuch znaków formatowania w stylu Printf.
func errorf(linenum int, format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "Linia %d: ", linenum)
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
}

linenum, name := 12, "count"


errorf(linenum, "niezdefiniowane: %s", name) // "Linia 12: niezdefiniowane: count"
Typ interface{} oznacza, że dana funkcja może dla swoich ostatnich argumentów przyjmować
całkowicie dowolne wartości, co wyjaśnimy w rozdziale 7.
Ćwiczenie 5.15. Napisz funkcje wariadyczne max i min, analogiczne do funkcji sum. Co powinny
robić te funkcje, gdy zostaną wywołane bez argumentów? Napisz warianty, które wymagają co
najmniej jednego argumentu.
Ćwiczenie 5.16. Napisz wariadyczną wersję funkcji strings.Join.
Ćwiczenie 5.17. Napisz funkcję wariadyczną ElementsByTagName, która przy danym drzewie węzłów
HTML oraz zerowej lub większej liczbie nazw zwraca wszystkie elementy pasujące do jednej
z tych nazw. Oto dwa przykładowe wywołania:
func ElementsByTagName(doc *html.Node, name ...string) []*html.Node

images := ElementsByTagName(doc, "img")


headings := ElementsByTagName(doc, "h1", "h2", "h3", "h4")

5.8. Odroczone wywołania funkcji


Nasz przykład findLinks wykorzystywał dane wyjściowe z funkcji http.Get jako dane wejścio-
we dla funkcji html.Parse. Dobrze się to sprawdza, jeśli treścią żądanego adresu URL jest rzeczy-
wiście HTML, ale wiele stron zawiera obrazy, zwykły tekst oraz inne formaty plików. Podawanie
takich plików do parsera HTML może mieć niepożądane skutki.
Poniższy program pobiera dokument HTML i wyświetla jego tytuł. Funkcja title sprawdza na-
główek Content-Type odpowiedzi serwera i zwraca błąd, jeśli dokumentem nie jest HTML.
code/r05/title1
func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}

// Sprawdza, czy Content-Type to HTML (np. "text/html; charset=utf-8").


ct := resp.Header.Get("Content-Type")
148 ROZDZIAŁ 5. FUNKCJE

if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {


resp.Body.Close()
return fmt.Errorf("%s ma typ %s, a nie text/html", url, ct)
}

doc, err := html.Parse(resp.Body)


resp.Body.Close()
if err != nil {
return fmt.Errorf("parsowanie %s jako HTML: %v", url, err)
}

visitNode := func(n *html.Node) {


if n.Type == html.ElementNode && n.Data == "title" &&
n.FirstChild != nil {
fmt.Println(n.FirstChild.Data)
}
}
forEachNode(doc, visitNode, nil)
return nil
}
Oto typowa sesja, lekko wyedytowana dla zwięzłości:
$ go build code/r05/title1
$ ./title1 http://gopl.io
The Go Programming Language
$ ./title1 https://golang.org/doc/effective_go.html
Effective Go - The Go Programming Language
$ ./title1 https://golang.org/doc/gopher/frontpage.png
tytuł: https://golang.org/doc/gopher/frontpage.png ma typ image/png, a nie text/html
Zwróć uwagę na zduplikowane wywołanie resp.Body.Close(), które gwarantuje, że funkcja
title zamknie połączenie sieciowe na wszystkich ścieżkach wykonywania, wliczając niepowodze-
nie. Gdy funkcje robią się bardziej złożone i muszą obsługiwać coraz więcej błędów, takie dupli-
kowanie logiki czyszczenia może stać się problematyczne w utrzymywaniu. Zobaczmy, w jaki spo-
sób upraszcza to nowatorski mechanizm defer języka Go.
Składniowo instrukcja defer jest zwykłym wywołaniem funkcji lub metody poprzedzonym sło-
wem kluczowym defer. Wyrażenia funkcji i argumentów są ewaluowane, gdy ta instrukcja jest
wykonywana, ale rzeczywiste wywołanie jest odraczane do czasu, aż funkcja zawierająca instruk-
cję defer zostanie zakończona w sposób normalny (przez wykonanie instrukcji return lub dotar-
cie do końca) lub anormalny (przez procedurę panic). Odraczać można dowolną liczbę wywo-
łań. Są one wykonywane w kolejności odwrotnej do tej, w jakiej zostały odroczone.
Instrukcja defer jest często stosowana w operacjach występujących w parach (takich jak: otwie-
ranie i zamykanie, łączenie i rozłączanie lub blokowanie i odblokowywanie) w celu zapewnienia,
że we wszystkich przypadkach zasoby zostaną zwolnione, bez względu na stopień złożoności
przepływu sterowania. Właściwym miejscem dla instrukcji defer zwalniającej zasób jest wstawie-
nie jej bezpośrednio po udanym pobraniu zasobu. W poniższej funkcji title pojedyncze wywołanie
odroczone zastępuje oba wcześniejsze wywołania resp.Body.Close():
code/r05/title2
func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
5.8. ODROCZONE WYWOŁANIA FUNKCJI 149

defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
return fmt.Errorf("%s ma typ %s, a nie text/html", url, ct)
}
doc, err := html.Parse(resp.Body)
if err != nil {
return fmt.Errorf("parsowanie %s jako HTML: %v", url, err)
}
// …wyświetlanie elementu title dokumentu…
return nil
}
Ten sam wzorzec może być używany dla innych zasobów poza połączeniami sieciowymi, np. w celu
zamknięcia otwartego pliku:
io/ioutil
package ioutil
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ReadAll(f)
}
lub do odblokowania muteksu (zob. podrozdział 9.2):
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
Instrukcja defer może być również używana do łączenia w pary akcji „na wejście” i „na wyjście”
podczas debugowania funkcji złożonej. Poniższa funkcja bigSlowOperation od razu wywołuje
funkcję trace wykonującą akcję „na wejście”, a następnie zwracającą wartość funkcji, która gdy
zostanie wywołana, wykonuje odpowiadającą akcję „na wyjście”. Poprzez odroczenie w ten sposób
wywołania funkcji zwracanej możemy instrumentować punkt wejścia i wszystkie punkty wyjścia
funkcji w jednej instrukcji, a nawet przekazywać pomiędzy tymi dwiema akcjami wartości takie jak
czas start. Nie zapomnij jednak o końcowych nawiasach w instrukcji defer, inaczej akcja „na
wejście” wydarzy się na wyjściu, a akcja „na wyjście” nie wydarzy się w ogóle!
code/r05/trace
func bigSlowOperation() {
defer trace("bigSlowOperation")() // nie zapomnij o dodatkowych nawiasach
// …dużo pracy…
time.Sleep(10 * time.Second) // symulowanie powolnej operacji za pomocą funkcji Sleep
}
150 ROZDZIAŁ 5. FUNKCJE

func trace(msg string) func() {


start := time.Now()
log.Printf("punkt wejścia %s", msg)
return func() { log.Printf("punkt wyjścia %s (%s)", msg, time.Since(start)) }
}
Przy każdym wywołaniu funkcja bigSlowOperation rejestruje swój punkt wejścia i wyjścia oraz
czas, jaki upłynął między nimi. (Użyliśmy funkcji time.Sleep, aby zasymulować powolną operację).
$ go build code/r05/trace
$ ./trace
2015/11/18 09:53:26 punkt wejścia bigSlowOperation
2015/11/18 09:53:36 punkt wyjścia bigSlowOperation (10.000589217s)
Funkcje odroczone są uruchamiane po zaktualizowaniu przez instrukcje return zmiennych
wynikowych danej funkcji. Ponieważ anonimowa funkcja może uzyskać dostęp do zmiennych
(w tym wyników nazwanych) zawierającej ją funkcji, odroczona funkcja anonimowa może ob-
serwować wyniki tej funkcji.
Rozważmy funkcję double:
func double(x int) int {
return x + x
}
Poprzez nazwanie jej zmiennej wynikowej i dodanie instrukcji defer możemy sprawić, że ta funkcja
przy każdym wywołaniu będzie wyświetlała swoje argumenty i wyniki.
func double(x int) (result int) {
defer func() { fmt.Printf("double(%d) = %d\n", x, result) }()
return x + x
}

_ = double(4)
// Output:
// "double(4) = 8"
Ta sztuczka jest przesadą dla funkcji tak prostej jak double, ale może być przydatna w funkcjach
z wieloma instrukcjami return.
Odroczona funkcja anonimowa może nawet zmieniać wartości, które zawierająca ją funkcja
zwraca swojemu podmiotowi wywołującemu:
func triple(x int) (result int) {
defer func() { result += x }()
return double(x)
}

fmt.Println(triple(4)) // "12"
Ponieważ funkcje odroczone nie są wykonywane aż do samego końca wykonywania danej funkcji,
instrukcja defer w pętli zasługuje na dodatkowe zbadanie. W poniższym kodzie mogą wyczerpać
się deskryptory plików, ponieważ żaden plik nie zostanie zamknięty, dopóki nie zostaną prze-
tworzone wszystkie pliki:
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
5.8. ODROCZONE WYWOŁANIA FUNKCJI 151

defer f.Close() // UWAGA: ryzykowne; mogą wyczerpać się deskryptory plików


// …przetwarzanie f…
}
Jednym z rozwiązań jest przeniesienie ciała pętli, w tym instrukcji defer, do innej funkcji, która jest
wywoływana w każdej iteracji.
for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}

func doFile(filename string) error {


f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// …przetwarzanie f…
}
Poniższy przykład jest ulepszonym programem fetch (zob. podrozdział 1.5), który zapisuje od-
powiedź HTTP w lokalnym pliku zamiast do standardowego strumienia wyjściowego. Wywodzi na-
zwę pliku z ostatniego komponentu ścieżki URL, którą uzyskuje za pomocą funkcji path.Base.
code/r05/fetch
// Fetch pobiera zawartość adresu URL i zwraca nazwę oraz wielkość lokalnego pliku.
func fetch(url string) (filename string, n int64, err error) {
resp, err := http.Get(url)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()

local := path.Base(resp.Request.URL.Path)
if local == "/" {
local = "index.html"
}
f, err := os.Create(local)
if err != nil {
return "", 0, err
}
n, err = io.Copy(f, resp.Body)
// Zamykanie pliku, przy czym preferowane jest zgłaszanie błędu z funkcji Copy,
// jeśli w ogóle wystąpi jakiś błąd.
if closeErr := f.Close(); err == nil {
err = closeErr
}
return local, n, err
}
Odroczone wywołanie funkcji resp.Body.Close powinno być już znajome. Kuszące jest użycie
drugiego wywołania odroczonego funkcji f.Close, aby zamknąć lokalny plik, ale byłoby to nieco
błędne, ponieważ os.Create otwiera plik do zapisywania, tworząc go w razie potrzeby. W wielu
systemach plików, zwłaszcza w NFS, błędy zapisu nie są zgłaszane natychmiast, ale mogą być
odraczane do czasu zamknięcia pliku. Niepowodzenie w sprawdzeniu wyniku operacji zamknięcia
może spowodować, że poważna utrata danych przejdzie niezauważona. Jeśli jednak zawiodą obie
152 ROZDZIAŁ 5. FUNKCJE

funkcje, io.Copy i f.Close, powinniśmy preferować zgłaszanie błędu z funkcji io.Copy, ponieważ
wystąpił on najpierw i jest bardziej prawdopodobne, że wskaże nam główną przyczynę.
Ćwiczenie 5.18. Bez zmiany jej zachowania przepisz funkcję fetch, aby używała instrukcji defer
do zamykania zapisywalnego pliku.

5.9. Procedura panic


System typów języka Go wyłapuje wiele błędów podczas kompilacji, ale inne błędy, takie jak próba
uzyskania dostępu poza zakresem tablicy (ang. out-of-bounds array access) lub wyłuskanie
wskaźnika nil (ang. nil pointer dereference), wymagają kontroli w czasie wykonywania. Gdy środo-
wisko wykonawcze języka Go wykrywa te błędy, uruchamia procedurę panic (panika).
Podczas typowej procedury panic normalne wykonywanie jest zatrzymywane, wykonywane są
wszystkie odroczone wywołania funkcji w danej procedurze goroutine, a program ulega awarii
i generowany jest komunikat dziennika. Taki komunikat dziennika zawiera: wartość paniki, którą
jest zwykle pewnego rodzaju komunikat błędu, oraz dla każdej procedury goroutine ślad stosu,
pokazujący stos wywołań funkcji, które były aktywne w momencie paniki. Komunikat dziennika
często ma wystarczającą ilość informacji do zdiagnozowania głównej przyczyny problemu bez
uruchamiania ponownie programu, więc zawsze powinien być uwzględniony w raporcie o błędach
dotyczącym panikującego programu.
Nie wszystkie paniki pochodzą ze środowiska wykonawczego. Wbudowana funkcja panic może
być wywoływana bezpośrednio i przyjmuje dowolną wartość jako argument. Procedura panic
jest często najlepszym rozwiązaniem, gdy przydarzają się jakieś „niemożliwe” sytuacje, np. wyko-
nywanie dojdzie do przypadku (case), który logicznie nie może się zdarzyć:
switch s := suit(drawCard()); s {
case "Pik": // …
case "Kier": // …
case "Karo": // …
case "Trefl": // …
default:
panic(fmt.Sprintf("nieprawidłowy kolor %q", s)) // Dżoker?
}
Dobrą praktyką jest zapewnianie, że warunki wstępne funkcji zostaną zachowane, ale łatwo
można posunąć się do przesady. Jeśli nie można zapewnić bardziej informacyjnego komunikatu
o błędzie lub wykryć błędu wcześniej, nie ma sensu wstawianie asercji warunku, który środowisko
wykonawcze sprawdzi za Ciebie.
func Reset(x *Buffer) {
if x == nil {
panic("x jest nil") // Niepotrzebne!
}
x.elements = nil
}
Chociaż mechanizm paniki języka Go przypomina wyjątki w innych językach, sytuacje, w których
wykorzystywana jest procedura panic, są zupełnie inne. Ponieważ procedura panic powoduje
awarię programu, jest zasadniczo wykorzystywana w przypadku poważnych błędów, takich jak
logiczne niespójności w programie. Staranni programiści traktują każdą awarię jako dowód na
błąd w kodzie. Solidny program powinien elegancko obsługiwać „spodziewane” błędy, takie jak
te, które wynikają z nieprawidłowych danych wejściowych, niewłaściwej konfiguracji lub nie-
powodzenia operacji we-wy. Najlepiej radzić sobie z nimi za pomocą wartości error.
5.9. PROCEDURA PANIC 153

Rozważmy funkcję regexp.Compile, która kompiluje wyrażenie regularne na formę efektywną


dla dopasowywania. Funkcja zwraca error, gdy zostanie wywołana za pomocą źle utworzonego
wzorca, ale sprawdzanie tego błędu jest niepotrzebne i uciążliwe, jeśli podmiot wywołujący wie,
że określone wywołanie nie może się nie powieść. W takich przypadkach rozsądne dla podmiotu
wywołującego jest obsłużenie błędu poprzez procedurę panic, ponieważ ten błąd jest uznawany
za niemożliwy.
Ponieważ większość wyrażeń regularnych w kodzie źródłowym programu to literały, pakiet
regexp zapewnia funkcję opakowującą regexp.MustCompile, która przeprowadza tę kontrolę:
package regexp

func Compile(expr string) (*Regexp, error) { /* ... */ }

func MustCompile(expr string) *Regexp {


re, err := Compile(expr)
if err != nil {
panic(err)
}
return re
}
Funkcja opakowująca umożliwia wygodne dla klientów inicjowanie zmiennej poziomu pakietu
za pomocą skompilowanego wyrażenia regularnego, tak jak poniżej:
var httpSchemeRE = regexp.MustCompile(`^https?:` ) // "http:" lub "https:"
Oczywiście funkcja MustCompile nie powinna być wywoływana z niezaufanymi wartościami wej-
ściowymi. Prefiks Must jest powszechną konwencją nazewnictwa dla funkcji tego rodzaju, takich
jak template.Must z podrozdziału 4.6.
Gdy ma miejsce panika, wszystkie odroczone funkcje są uruchamiane w odwrotnej kolejności,
poczynając od tych należących do funkcji z wierzchołka stosu i przechodząc po kolei do funkcji
main, tak jak pokazuje poniższy program:
code/r05/defer1
func main() {
f(3)
}

func f(x int) {


fmt.Printf("f(%d)\n", x+0/x) // panika, jeśli x == 0
defer fmt.Printf("defer %d\n", x)
f(x - 1)
}
Po uruchomieniu program przekazuje następujące dane do standardowego strumienia wyjściowego:
f(3)
f(2)
f(1)
defer 1
defer 2
defer 3
Panika podczas wywołania f(0) powoduje wykonanie trzech odroczonych wywołań funkcji
fmt.Printf. Następnie środowisko wykonawcze kończy program, przekazując do standardowego
strumienia błędów (uproszczonego dla jasności) komunikat paniki i zrzut stosu:
154 ROZDZIAŁ 5. FUNKCJE

panic: runtime error: integer divide by zero


main.f(0)
src/code/r05/defer1/defer.go:14
main.f(1)
src/code/r05/defer1/defer.go:16
main.f(2)
src/code/r05/defer1/defer.go:16
main.f(3)
src/code/r05/defer1/defer.go:16
main.main()
src/code/r05/defer1/defer.go:10
Jak zobaczymy za chwilę, funkcja może odzyskać sprawność po panice, aby nie kończyła ona
programu.
Dla celów diagnostycznych pakiet runtime pozwala programiście wykonać zrzut stosu za pomocą
tego samego mechanizmu. Poprzez odroczenie wywołania funkcji printStack w funkcji main:
code/r05/defer2
func main() {
defer printStack()
f(3)
}

func printStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
os.Stdout.Write(buf[:n])
}
do standardowego strumienia wyjściowego przekazywany jest następujący dodatkowy tekst (ponow-
nie uproszczony dla jasności):
goroutine 1 [running]:
main.printStack()
src/code/r05/defer2/defer.go:20
main.f(0)
src/code/r05/defer2/defer.go:27
main.f(1)
src/code/r05/defer2/defer.go:29
main.f(2)
src/code/r05/defer2/defer.go:29
main.f(3)
src/code/r05/defer2/defer.go:29
main.main()
src/code/r05/defer2/defer.go:15
Czytelnicy zaznajomieni z wyjątkami w innych językach mogą być zaskoczeni, że funkcja
runtime.Stack może wyświetlać informacje o funkcjach, które wydają się już „odwinięte”. Mecha-
nizm paniki języka Go uruchamia odroczone funkcje, zanim odwinie stos.

5.10. Odzyskiwanie sprawności


Rezygnacja jest zwykle właściwą reakcją na procedurę panic, ale nie zawsze. Czasami istnieje
możliwość odzyskania sprawności w jakiś sposób, a przynajmniej posprzątania bałaganu przed
zakończeniem programu. Serwer WWW, który napotka niespodziewany problem, może np. zamknąć
połączenie zamiast pozostawić oczekującego klienta, a podczas fazy rozwoju oprogramowania może
zgłaszać ten błąd klientowi.
5.10. ODZYSKIWANIE SPRAWNOŚCI 155

Jeśli wbudowana funkcja recover jest wywoływana w ramach funkcji odroczonej, a funkcja zawie-
rająca instrukcję defer panikuje, recover kończy aktualny stan procedury panic i zwraca wartość
paniki. Funkcja, która uruchomiła procedurę panic, nie będzie kontynuowana od miejsca prze-
rwania, ale powróci normalnie. Jeśli funkcja recover jest wywoływana w każdym innym czasie,
nie wywołuje żadnego efektu i zwraca nil.
Aby to zilustrować, rozważmy przykład opracowywania parsera dla jakiegoś języka. Nawet jeśli
na pozór wszystko działa dobrze, to biorąc pod uwagę złożoność tego zadania, błędy wciąż mogą
się czaić w mrocznych przypadkach patologicznych. Możemy preferować, aby zamiast awarii parser
zamieniał te paniki na zwykłe błędy parsowania, być może z dodatkowym komunikatem zachę-
cającym użytkownika do wysłania raportu o błędach.
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("błąd wewnętrzny: %v", p)
}
}()
// …parser…
}
Odroczona funkcja w funkcji Parse odzyskuje sprawność po procedurze panic, używając wartości
paniki do skonstruowania komunikatu o błędzie. Bardziej wyszukana wersja mogłaby załączać
cały stos wywołań za pomocą funkcji runtime.Stack. Następnie ta funkcja odroczona przypisuje
do err wynik, który jest zwracany do podmiotu wywołującego.
Nieprzemyślane odzyskiwanie sprawności jest wątpliwą praktyką, ponieważ stan zmiennych pa-
kietu po panice rzadko jest dobrze zdefiniowany lub udokumentowany. Być może krytyczna
aktualizacja struktury danych była niekompletna, plik lub połączenie sieciowe zostało otwarte,
ale nie zostało zamknięte, albo blokada została założona, ale nie została zwolniona. Ponadto w wyni-
ku zastąpienia awarii np. wpisem w pliku dziennika nieprzemyślane odzyskiwanie sprawności
może spowodować, że błędy przejdą niezauważone.
Odzyskiwanie sprawności po procedurze panic w ramach tego samego pakietu może uprościć obsłu-
gę złożonych lub nieoczekiwanych błędów, ale co do zasady nie należy próbować odzyskiwania
sprawności po procedurze panic innego pakietu. Publiczne interfejsy API powinny zgłaszać awarie
jako typy error. Nie należy także odzyskiwać sprawności po procedurze panic, która może przejść
przez funkcję nieutrzymywaną przez Ciebie, taką jak wywołanie zwrotne dostarczane przez
podmiot wywołujący, ponieważ nie możesz zadbać o jej bezpieczeństwo.
Pakiet net/http zapewnia np. serwer WWW, który rozsyła przychodzące żądania do funkcji ob-
sługi dostarczonych przez użytkownika. Zamiast dopuścić, aby procedura panic w jednej z tych
funkcji obsługi zakończyła proces, serwer wywołuje funkcję recover, wyświetla ślad stosu i konty-
nuuje pracę. Jest to wygodne w praktyce, ale stwarza ryzyko wycieku zasobów lub pozostawienia
nieudanej funkcji obsługi w nieokreślonym stanie, który może prowadzić do innych problemów.
Z wszystkich powyższych powodów najbezpieczniej jest odzyskiwać sprawność selektywnie, jeśli
w ogóle. Innymi słowy: odzyskiwać sprawność tylko po tych panikach, dla których odzyskanie
sprawności było zamierzone, co powinno być rzadkie. Ten zamiar może być zakodowany za po-
mocą odrębnego, nieeksportowanego typu dla wartości paniki i testowania, czy wartość zwracana
przez funkcję recover ma ten typ. (Jeden ze sposobów na to zobaczymy w następnym przykładzie).
Jeśli tak, raportujemy panikę jako zwykły typ error. Jeżeli nie, wywołujemy funkcję panic z tą
samą wartością, aby wznowić stan paniki.
156 ROZDZIAŁ 5. FUNKCJE

Poniższy przykład jest wariacją na temat programu title, która zgłasza błąd, jeśli dokument
HTML zawiera wiele elementów <title>. Jeśli tak jest, przerywa rekurencję, wywołując funkcję
panic z wartością specjalnego typu bailout.
code/r05/title3
// soleTitle zwraca tekst pierwszego niepustego elementu title w dokumencie doc oraz error,
// jeśli nie było dokładnie jednego.
func soleTitle(doc *html.Node) (title string, err error) {
type bailout struct{}

defer func() {
switch p := recover(); p {
case nil:
// nie ma paniki
case bailout{}:
// "oczekiwana" panika
err = fmt.Errorf("wiele elementów title")
default:
panic(p) // nieoczekiwana panika; kontynuowanie paniki
}
}()

// Wyjście z rekurencji, jeśli znaleziony zostanie więcej niż jeden niepusty element title.
forEachNode(doc, func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "title" &&
n.FirstChild != nil {
if title != "" {
panic(bailout{}) // wiele elementów title
}
title = n.FirstChild.Data
}
}, nil)
if title == "" {
return "", fmt.Errorf("brak elementu title")
}
return title, nil
}
Odroczona funkcja obsługi wywołuje funkcję recover, sprawdza wartość paniki i raportuje zwykły
error, jeśli wartością paniki było bailout{}. Wszystkie inne wartości niebędące nil wskazują nie-
oczekiwaną panikę, a w takim przypadku funkcja obsługi wywołuje funkcję panic z tą wartością,
odkręcając efekt funkcji recover i przywracając pierwotny stan paniki. (Ten przykład w pewien
sposób łamie nasze zalecenia dotyczące nieużywania paniki dla „spodziewanych” błędów, ale zapew-
nia zwartą ilustrację mechanizmów).
W niektórych warunkach nie można odzyskać sprawności. Wyczerpanie się pamięci powoduje
np., że środowisko wykonawcze języka Go kończy program z błędem krytycznym.
Ćwiczenie 5.19. Użyj funkcji panic i recover do napisania funkcji, która nie zawiera żadnych in-
strukcji return, a mimo to zwraca wartość niezerową.
Rozdział 6

Metody

Od wczesnych lat 90. ubiegłego wieku programowanie obiektowe (ang. object-oriented programming
— OOP) jest dominującym paradygmatem programowania w przemyśle oraz edukacji i niemal
wszystkie powszechnie używane języki opracowane od tego czasu zapewniają wsparcie dla tego
paradygmatu. Język Go nie jest wyjątkiem.
Choć nie ma żadnej powszechnie akceptowanej definicji programowania obiektowego, dla naszych
celów możemy przyjąć, że obiekt jest po prostu wartością lub zmienną, która posiada metody,
a metoda jest funkcją powiązaną z określonym typem. Program obiektowy to taki, który używa me-
tod do wyrażania właściwości i operacji każdej struktury danych, aby klienty nie musiały uzyskiwać
dostępu bezpośrednio do reprezentacji danego obiektu.
W poprzednich rozdziałach regularnie stosowaliśmy metody ze standardowej biblioteki, takie jak
metoda Seconds typu time.Duration:
const day = 24 * time.Hour
fmt.Println(day.Seconds()) // "86400"
W podrozdziale 2.5 zdefiniowaliśmy również własną metodę String dla typu Celsius:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
W tym rozdziale (pierwszym z dwóch poświęconych programowaniu obiektowemu) pokażemy, jak
efektywnie definiować i stosować metody. Omówimy również dwie podstawowe zasady programo-
wania obiektowego: hermetyzację i kompozycję.

6.1. Deklaracje metod


Metoda jest deklarowana za pomocą pewnego wariantu zwykłej deklaracji funkcji, w której przed
nazwą funkcji pojawia się dodatkowy parametr. Ten parametr dołącza daną funkcję do swojego
typu.
Napiszmy naszą pierwszą metodę w prostym pakiecie dla geometrii płaskiej:
code/r06/geometry
package geometry
import "math"
type Point struct{ X, Y float64 }
158 ROZDZIAŁ 6. METODY

// Tradycyjna funkcja.
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// To samo, ale jako metoda typu Point.
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
Dodatkowy parametr p nazywa się odbiornikiem metody, co zostało odziedziczone po wczesnych
językach obiektowych, które opisują wywołanie metody jako „wysyłanie komunikatu do obiektu”.
W języku Go nie używamy dla odbiornika specjalnej nazwy, takiej jak this lub self. Nazwy odbior-
ników możemy wybierać tak samo jak nazwy dla wszystkich innych parametrów. Ponieważ nazwa
odbiornika będzie często używana, dobrym pomysłem jest wybrać coś krótkiego i spójnego dla
wszystkich metod. Powszechnym wyborem jest pierwsza litera nazwy typu, tak jak p dla Point.
W wywołaniu metody argument odbiornika pojawia się przed nazwą metody. Jest to analogiczne
do deklaracji, w której parametr odbiornika również pojawia się przed nazwą metody.
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", wywołanie funkcji
fmt.Println(p.Distance(q)) // "5", wywołanie metody
Pomiędzy dwiema powyższymi deklaracjami funkcji o nazwie Distance nie ma konfliktu. Pierw-
sza deklaruje funkcję poziomu pakietu, nazwaną geometry.Distance. Druga deklaruje metodę dla
typu Point, więc jej nazwą jest point.Distance.
Wyrażenie p.Distance jest zwane selektorem, ponieważ wybiera odpowiednią metodę Distance
dla odbiornika p typu Point. Selektory są również używane do wybierania pól typów struct, np.
p.X. Ponieważ metody i pola zamieszkują tę samą przestrzeń nazw, deklarowanie metody X na typie
struktury Point byłoby dwuznaczne i kompilator by to odrzucił.
Ze względu na to, że każdy typ ma swoją własną przestrzeń nazw dla metod, możemy używać
nazwy Distance dla innych metod, pod warunkiem że należą one do innych typów. Zdefiniujmy
typ Path, który reprezentuje sekwencję odcinków, i również dajmy mu metodę Distance.
// Path to ścieżka łącząca punkty za pomocą linii prostych.
type Path []Point
// Distance zwraca odległość pokonaną wzdłuż ścieżki.
func (path Path) Distance() float64 {
sum := 0.0
for i := range path {
if i > 0 {
sum += path[i-1].Distance(path[i])
}
}
return sum
}
Path jest nazwanym typem wycinka, a nie typem struktury, tak jak Point, ale mimo to nadal mo-
żemy zdefiniować dla niego metody. W kwestii dopuszczania powiązywania metod z dowolnym
typem Go różni się od wielu innych języków obiektowych. Często wygodnie jest definiować dodat-
kowe zachowania dla prostych typów, takich jak: liczby, łańcuchy znaków, wycinki, mapy, a czasami
nawet funkcje. Metody mogą być deklarowane na dowolnym typie nazwanym zdefiniowanym w tym
samym pakiecie, pod warunkiem że jego typem bazowym nie jest wskaźnik ani interfejs.
6.2. METODY Z ODBIORNIKIEM WSKAŹNIKOWYM 159

Te dwie metody Distance mają różne typy. Nie są ze sobą w ogóle powiązane, chociaż
Path.Distance używa Point.Distance wewnętrznie do obliczania długości każdego odcinka, który
łączy sąsiadujące punkty.
Wywołajmy tę nową metodę, aby obliczyć obwód trójkąta prostokątnego:
perim := geometry.Path{
{1, 1},
{5, 1},
{5, 4},
{1, 1},
}
fmt.Println(perim.Distance()) // "12"

W dwóch przedstawionych powyżej wywołaniach metod o nazwie Distance kompilator określa,


którą funkcję wywołać, na podstawie nazwy metody oraz typu odbiornika. W pierwszym przypadku
path[i-1] ma typ Point, więc wywoływana jest metoda Point.Distance. W drugim przypadku
perim ma typ Path, więc wywoływana jest metoda Path.Distance.
Wszystkie metody danego typu muszą mieć unikatowe nazwy, ale różne typy mogą używać tej
samej nazwy dla jakiejś metody, tak jak nazwa metody Distance dla typów Point i Path. Nie ma
potrzeby kwalifikowania nazw funkcji (np. PathDistance) w celu ujednoznacznienia. Widzimy
tutaj pierwszą zaletę używania metod zamiast zwykłych funkcji: nazwy metod mogą być krótsze.
Korzyść jest jeszcze większa w przypadku wywołań pochodzących spoza pakietu, ponieważ mogą
one wykorzystywać krótszą nazwę oraz pomijać nazwę pakietu:
import "code/r06/geometry"

perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}}


fmt.Println(geometry.Path.Distance(perim)) // "12", metoda typu geometry.Path
fmt.Println(perim.Distance()) // "12", samodzielna funkcja

6.2. Metody z odbiornikiem wskaźnikowym


Ponieważ wywołanie funkcji tworzy kopię każdej wartości argumentu, jeśli funkcja musi zaktuali-
zować zmienną lub jeśli argument jest tak duży, że chcemy uniknąć jego kopiowania, musimy prze-
kazać adres zmiennej za pomocą wskaźnika. To samo odnosi się do metod, które potrzebują zaktu-
alizować zmienną odbiornika: doczepiamy je do typu wskaźnika, takiego jak *Point.
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
Nazwą tej metody jest (*Point).ScaleBy. Nawiasy są konieczne. Bez nich wyrażenie byłoby par-
sowane jako *(Point.ScaleBy).
160 ROZDZIAŁ 6. METODY

W rzeczywistym programie konwencja nakazuje, że jeśli jakakolwiek metoda typu Point posiada
odbiornik wskaźnikowy, wszystkie metody typu Point powinny mieć odbiornik wskaźnikowy,
nawet te, które nie do końca go potrzebują. Złamaliśmy tę regułę dla Point, żeby móc pokazać oba
rodzaje metod.
Typy nazwane (Point) i wskaźniki do nich (*Point) są jedynymi typami, które mogą się pojawiać
w deklaracji odbiornika. Ponadto, aby uniknąć wieloznaczności, deklaracje metod nie są dopusz-
czalne na typach nazwanych, które same są typami wskaźnika:
type P *int
func (P) f() { /* ... */ } // błąd kompilacji: nieprawidłowy typ odbiornika
Metoda (*Point).ScaleBy może być wywoływana poprzez dostarczanie odbiornika *Point w taki
sposób:
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
albo w taki:
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p) // "{2, 4}"
lub w taki:
p := Point{1, 2}
(&p).ScaleBy(2)
fmt.Println(p) // "{2, 4}"
Te dwa ostatnie przypadki są jednak niezgrabne. Na szczęście z pomocą przychodzi nam język Go.
Jeśli odbiornik p jest zmienną typu Point, ale metoda wymaga odbiornika *Point, możemy użyć
skrótu p.ScaleBy(2), a kompilator wykona na zmiennej niejawną operację &p. Działa to tylko dla
zmiennych, w tym pól struktury, takich jak p.X, oraz elementów tablicy lub wycinka, takich jak
perim[0]. Nie możemy wywołać metody *Point na nieadresowalnym odbiorniku Point, ponieważ
nie ma sposobu uzyskania adresu wartości tymczasowej.
Point{1, 2}.ScaleBy(2) // błąd kompilacji: nie można przyjąć adresu literału Point
Możemy jednak wywołać metodę typu Point, taką jak Point.Distance, za pomocą odbiornika
*Point, ponieważ istnieje sposób uzyskania wartości z danego adresu: wystarczy załadować war-
tość wskazywaną przez odbiornik. Kompilator wstawia za nas niejawną operację *. Te dwa wywoła-
nia funkcji są równoważne:
pptr.Distance(q)
(*pptr).Distance(q)
Podsumujmy jeszcze raz te trzy przypadki, ponieważ są one częstym źródłem niejasności. W każdym
prawidłowym wyrażeniu wywołania metody dokładnie jedno z tych trzech stwierdzeń jest prawdziwe.
Argument odbiornika ma ten sam typ co parametr odbiornika, np. oba mają typ T lub oba mają typ *T:
Point{1, 2}.Distance(q) // Point
pptr.ScaleBy(2) // *Point
Albo argument odbiornika jest zmienną typu T, a parametr odbiornika ma typ *T. Kompilator po-
średnio pobiera adres zmiennej:
p.ScaleBy(2) // niejawnie (&p)
6.2. METODY Z ODBIORNIKIEM WSKAŹNIKOWYM 161

Lub też argument odbiornika ma typ *T, a parametr odbiornika ma typ T. Kompilator pośrednio
wyłuskuje odbiornik, innymi słowy: ładuje wartość:
pptr.Distance(q) // niejawnie (*pptr)
Jeśli wszystkie metody nazwanego typu T mają również typ odbiornika T (a nie *T), bezpieczne
jest kopiowanie instancji tego typu. Wywołanie jego dowolnej metody niekoniecznie tworzy kopię.
Wartości time.Duration są np. swobodnie kopiowane, w tym jako argumenty do funkcji. Jeśli
jednak jakaś metoda ma odbiornik wskaźnikowy, należy unikać kopiowania instancji typu T,
ponieważ może to naruszać wewnętrzne niezmienniki. Kopiowanie np. instancji bytes.Buffer
spowodowałoby, że oryginał i kopia byłyby aliasami (zob. punkt 2.3.2) tej samej bazowej tablicy
bajtów. Późniejsze wywołania metod miałyby nieprzewidywalne skutki.

6.2.1. Nil jest prawidłową wartością odbiornika


Tak jak niektóre funkcje dopuszczają wskaźniki nil jako argumenty, tak samo jest z niektórymi
metodami i ich odbiornikami, zwłaszcza jeśli nil jest znaczącą wartością zerową danego typu,
jak w przypadku map i wycinków. W tej prostej liście powiązanej liczb całkowitych nil reprezentuje
pustą listę:
// IntList jest listą powiązaną liczb całkowitych.
// nil *IntList reprezentuje pustą listę.
type IntList struct {
Value int
Tail *IntList
}

// Sum zwraca sumę elementów listy.


func (list *IntList) Sum() int {
if list == nil {
return 0
}
return list.Value + list.Tail.Sum()
}
Przy definiowaniu typu, którego metody dopuszczają nil jako wartość odbiornika, warto wyraźnie
to zaznaczyć w jego komentarzu dokumentującym, tak jak zrobiliśmy powyżej.
Oto część definicji typu Values z pakietu net/url:
net/url
package url

// Values mapuje klucz będący łańcuchem znaków na listę wartości.


type Values map[string][]string

// Get zwraca pierwszą wartość powiązaną z podanym kluczem lub pusty łańcuch "",
// jeśli nie ma żadnej wartości.
func (v Values) Get(key string) string {
if vs := v[key]; len(vs) > 0 {
return vs[0]
}
return ""
}

// Add dodaje wartość do klucza.


// Dołącza do wszystkich istniejących wartości powiązanych z danym kluczem.
162 ROZDZIAŁ 6. METODY

func (v Values) Add(key, value string) {


v[key] = append(v[key], value)
}
Ten typ udostępnia swoją reprezentację w postaci mapy, ale zapewnia również metody upraszczające
dostęp do tej mapy, których wartościami są wycinki łańcuchów znaków — jest to multimapa.
Klienty mogą używać jej wewnętrznych operatorów (make, literałów wycinków, m[key] itd.), jej
metod albo, jeśli wolą, jednego i drugiego:
code/r06/urlvalues
m := url.Values{"lang": {"en"}} // konstrukcja bezpośrednia
m.Add("item", "1")
m.Add("item", "2")

fmt.Println(m.Get("lang")) // "en"
fmt.Println(m.Get("q")) // ""
fmt.Println(m.Get("item")) // "1" (pierwsza wartość)
fmt.Println(m["item"]) // "[1 2]" (bezpośredni dostęp do mapy)

m = nil
fmt.Println(m.Get("item")) // ""
m.Add("item", "3") // panic: przypisanie do wpisu w mapie nil
W ostatnim wywołaniu metody Get odbiornik nil zachowuje się jak pusta mapa. Moglibyśmy
równoważnie zapisać to jako Values(nil).Get("item")), ale nil.Get("item") nie będzie się kom-
pilować, ponieważ typ wartości nil nie został określony. Natomiast ostatnie wywołanie metody Add
wywołuje panikę, ponieważ próbuje zaktualizować mapę nil.
Ponieważ url.Values jest typem mapy, a mapa odwołuje się do swoich par klucz-wartość pośred-
nio, wszelkie operacje aktualizacji i usuwania, które url.Values.Add przeprowadza na elemen-
tach mapy, są widoczne dla podmiotu wywołującego. Jednak, tak jak w przypadku zwykłych
funkcji, wszelkie zmiany, jakie metoda wykonuje w samej referencji, takie jak ustawienie jej na
wartość nil lub sprawienie, że będzie się odnosić do innej struktury danych mapy, nie zostaną
odzwierciedlone w podmiocie wywołującym.

6.3. Komponowanie typów poprzez osadzanie struktur


Rozważmy typ ColoredPoint:
code/r06/coloredpoint
import "image/color"

type Point struct{ X, Y float64 }

type ColoredPoint struct {


Point
Color color.RGBA
}
Mogliśmy zdefiniować ColoredPoint jako strukturę trzech pól, ale zamiast tego osadziliśmy typ
Point, aby zapewnić pola X i Y. Jak widzieliśmy w punkcie 4.4.3, osadzanie pozwala nam skorzy-
stać ze skrótu składniowego przy definiowaniu typu ColoredPoint, który zawiera wszystkie pola typu
Point oraz kilka innych pól. Jeśli chcemy, możemy wybierać pola ColoredPoint, które zostały
wniesione przez osadzony typ Point, bez wymieniania typu Point:
6.3. KOMPONOWANIE TYPÓW POPRZEZ OSADZANIE STRUKTUR 163

var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"
Podobny mechanizm ma zastosowanie do metod typu Point. Możemy wywołać metody osadzonego
pola Point, używając odbiornika typu ColoredPoint, chociaż ColoredPoint nie ma żadnych
zadeklarowanych metod:
red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
Metody typu Point zostały promowane do typu ColoredPoint. W ten sposób osadzanie pozwala
budować typy złożone z wieloma metodami poprzez kompozycję kilku pól, z których każde zapew-
nia kilka metod.
Czytelnicy zaznajomieni z językami obiektowymi opartymi na klasach mogą ulec pokusie, aby po-
traktować Point jako klasę bazową, a ColoredPoint jako podklasę lub klasę pochodną, albo zin-
terpretować relację pomiędzy tymi typami, tak jakby ColoredPoint był „jakimś” typem Point.
Byłoby to jednak błędem. Zwróć uwagę na powyższe wywołania metody Distance. Ma ona parametr
typu Point, a q nie jest typem Point, więc choć q ma osadzone pole tego typu, musimy wybrać
je bezpośrednio. Próba przekazania q wywołałaby błąd:
p.Distance(q) // błąd kompilacji: nie można użyć q (ColoredPoint) jako Point
ColoredPoint nie jest typem Point, ale „ma” Point i posiada dwie dodatkowe metody: Distance
i ScaleBy, promowane z typu Point. Jeśli wolisz myśleć w kategoriach implementacji, osadzone
pole instruuje kompilator, aby generował dodatkowe metody opakowujące równoważne z poniż-
szymi, które delegują wykonywanie do zadeklarowanych metod:
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}

func (p *ColoredPoint) ScaleBy(factor float64) {


p.Point.ScaleBy(factor)
}
Gdy metoda Point.Distance jest wywoływana przez pierwszą z tych metod opakowujących,
wartością jej odbiornika jest p.Point, a nie p, i nie ma żadnego sposobu, aby ta metoda uzyskała
dostęp do ColoredPoint, w którym osadzony jest Point.
Typem anonimowego pola może być wskaźnik do typu nazwanego, a w takim przypadku pola
i metody są promowane pośrednio ze wskazywanego obiektu. Dodanie kolejnego poziomu
pośredniości pozwala współdzielić typowe struktury i dynamicznie zmieniać relacje między obiek-
tami. Poniższa deklaracja ColoredPoint osadza *Point:
type ColoredPoint struct {
*Point
Color color.RGBA
}
164 ROZDZIAŁ 6. METODY

p := ColoredPoint{&Point{1, 1}, red}


q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point // p i q współdzielą teraz ten sam typ Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
Typ struktury może mieć więcej niż jedno anonimowe pole. Gdybyśmy zadeklarowali ColoredPoint
jako
type ColoredPoint struct {
Point
color.RGBA
}
wówczas wartość tego typu miałaby wszystkie metody typu Point, wszystkie metody RGBA oraz
wszelkie dodatkowe metody zadeklarowane bezpośrednio na typie ColoredPoint. Gdy kompilator
rozwiązuje na metodę selektor, taki jak p.ScaleBy, najpierw szuka bezpośrednio zadeklarowanej
metody o nazwie ScaleBy, następnie metod promowanych jeden raz z osadzonych pól ColoredPoint,
później metod promowanych dwukrotnie z osadzonych pól w Point i RGBA itd. Kompilator zgłasza
błąd, jeśli selektor był niejednoznaczny, ponieważ dwie metody zostały promowane z tej samej
rangi.
Metody mogą być deklarowane tylko na typach nazwanych (takich jak Point) i wskaźnikach do nich
(*Point), ale dzięki osadzaniu jest możliwe (i czasami przydatne), aby nienazwane typy struktury
również miały metody.
Oto przyjemna sztuczka, która to ilustruje. Ten przykład przedstawia część prostej pamięci podręcznej
zaimplementowanej z wykorzystaniem dwóch zmiennych poziomu pakietu: muteksu (zob. podroz-
dział 9.2) i strzeżonej przez niego mapy:
var (
mu sync.Mutex // strzeże mapowania
mapping = make(map[string]string)
)

func Lookup(key string) string {


mu.Lock()
v := mapping[key]
mu.Unlock()
return v
}
Poniższa wersja jest funkcjonalnie równoważna, ale grupuje te dwie powiązane zmienne w pojedyn-
czą zmienną poziomu pakietu o nazwie cache:
var cache = struct {
sync.Mutex
mapping map[string]string
} {
mapping: make(map[string]string),
}

func Lookup(key string) string {


cache.Lock()
v := cache.mapping[key]
cache.Unlock()
return v
}
6.4. WARTOŚCI I WYRAŻENIA METOD 165

Ta nowa zmienna nadaje bardziej ekspresyjne nazwy zmiennym powiązanym z cache, a ponieważ
pole sync.Mutex jest w niej osadzone, jego metody Lock i Unlock są promowane do nienazwanego
typu struct, co pozwala blokować cache za pomocą oczywistej składni.

6.4. Wartości i wyrażenia metod


Zazwyczaj wybieramy i wywołujemy metodę w tym samym wyrażeniu, tak jak w p.Distance(),
ale możliwe jest rozdzielenie tych dwóch operacji. Selektor p.Distance daje wartość metody,
czyli funkcję, która wiąże metodę (Point.Distance) z konkretną wartością p odbiornika. Ta
funkcja może być następnie wywoływana bez wartości odbiornika. Potrzebuje tylko argumentów
innych niż odbiornik.
p := Point{1, 2}
q := Point{4, 6}

distanceFromP := p.Distance // wartość metody


fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", √5

scaleP := p.ScaleBy // wartość metody


scaleP(2) // p staje się (2, 4)
scaleP(3) // następnie (6, 12)
scaleP(10) // następnie (60, 120)
Wartości metod są użyteczne, gdy interfejs API pakietu wymaga wartości funkcji, a pożądanym
zachowaniem klienta dla tej funkcji jest wywołanie metody na konkretnym odbiorniku. Przykła-
dowo: funkcja time.AfterFunc wywołuje wartość funkcji po określonym opóźnieniu. Ten program
używa jej do odpalenia rakiety r po 10 sekundach:
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }

r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
Składnia wartości metody jest krótsza:
time.AfterFunc(10 * time.Second, r.Launch)
Podobne do wartości metody jest wyrażenie metody. Przy wywoływaniu metody, w przeciwień-
stwie do zwykłej funkcji, musimy dostarczyć odbiornik w szczególny sposób z użyciem składni
selektora. Wyrażenie metody, zapisywane jako T.f lub (*T).f, gdzie T jest typem, daje wartość
funkcji, w której regularny pierwszy parametr zajmuje miejsce odbiornika, więc można ją wywoły-
wać w zwykły sposób.
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // wyrażenie metody
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"
166 ROZDZIAŁ 6. METODY

Wyrażenie metody może być przydatne, gdy potrzebna jest wartość do reprezentowania wyboru
spośród kilku metod należących do tego samego typu, tak aby można było wywołać wybraną metodę
z wieloma różnymi odbiornikami. W poniższym przykładzie zmienna op reprezentuje metodę
dodawania lub odejmowania typu Point, a Path.TranslateBy wywołuje ją dla każdego punktu na
ścieżce (Path):
type Point struct{ X, Y float64 }

func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }

type Path []Point

func (path Path) TranslateBy(offset Point, add bool) {


var op func(p, q Point) Point
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
// Wywołanie path[i].Add(offset) lub path[i].Sub(offset).
path[i] = op(path[i], offset)
}
}

6.5. Przykład: typ wektora bitowego


Zbiory w języku Go są zwykle implementowane jako map[T]bool, gdzie T jest typem elementu.
Zbiór reprezentowany przez mapę jest bardzo elastyczny, ale z pewnych względów lepiej może
się sprawdzać reprezentacja wyspecjalizowana. Przykładowo: w dziedzinach takich jak analiza prze-
pływu danych, gdzie elementy zbioru są małymi liczbami całkowitymi nieujemnymi, zbiory mają
wiele elementów, a operacje takie jak sumowanie i wyznaczanie części wspólnych są powszechne,
idealny jest wektor bitowy.
Wektor bitowy używa wycinka wartości całkowitych bez znaku, czyli „słów”, których każdy bit
reprezentuje możliwy element zbioru. Zbiór zawiera i, jeśli ustawiony jest i-ty bit. Poniższy program
demonstruje prosty typ wektora bitowego z trzema metodami:
code/r06/intset
// IntSet jest zbiorem niewielkich liczb całkowitych nieujemnych.
// Jego wartość zerową reprezentuje pusty zbiór.
type IntSet struct {
words []uint64
}

// Has raportuje, czy zbiór zawiera nieujemną wartość x.


func (s *IntSet) Has(x int) bool {
word, bit := x/64, uint(x%64)
return word < len(s.words) && s.words[word]&(1<<bit) != 0
}

// Add dodaje do zbioru nieujemną wartość x.


func (s *IntSet) Add(x int) {
word, bit := x/64, uint(x%64)
for word >= len(s.words) {
6.5. PRZYKŁAD: TYP WEKTORA BITOWEGO 167

s.words = append(s.words, 0)
}
s.words[word] |= 1 << bit
}

// UnionWith ustawia s na sumę zbiorów s i t.


func (s *IntSet) UnionWith(t *IntSet) {
for i, tword := range t.words {
if i < len(s.words) {
s.words[i] |= tword
} else {
s.words = append(s.words, tword)
}
}
}
Ponieważ każde słowo ma 64 bity, aby zlokalizować bit dla wartości x, używamy ilorazu x/64 jako
indeksu słowa, a reszty z dzielenia x%64 jako indeksu bitów wewnątrz tego słowa. Operacja
UnionWith używa bitowego operatora OR |, aby obliczyć sumę 64 elementów na raz. (Do wyboru
64-bitowych słów powrócimy w ćwiczeniu 6.5).
W tej implementacji brakuje wielu pożądanych funkcji, z których kilka zostało zadanych jako
problem w poniższych ćwiczeniach, ale bez jednej trudno się obyć: bez sposobu wyświetlania
IntSet jako łańcucha znaków. Dodajmy metodę String, tak jak zrobiliśmy w przypadku typu
Celsius w podrozdziale 2.5:
// String zwraca zbiór jako łańcuch znaków w postaci "{1 2 3}".
func (s *IntSet) String() string {
var buf bytes.Buffer
buf.WriteByte('{')
for i, word := range s.words {
if word == 0 {
continue
}
for j := 0; j < 64; j++ {
if word&(1<<uint(j)) != 0 {
if buf.Len() > len("{") {
buf.WriteByte(' ' )
}
fmt.Fprintf(&buf, "%d", 64*i+j)
}
}
}
buf.WriteByte('}')
return buf.String()
}
Należy zwrócić uwagę na podobieństwo powyższej metody String do metody intsToString
z punktu 3.5.4. Typ bytes.Buffer jest często stosowany w ten sposób w metodach String. Pa-
kiet fmt traktuje typy z metodą String wyjątkowo, aby wartości skomplikowanych typów mogły
być wyświetlane w sposób przyjazny dla użytkownika. Zamiast wyświetlać surową reprezentację
wartości (w tym przypadku strukturę), fmt wywołuje metodę String. Ten mechanizm opiera się na
interfejsach i asercjach typów, które wyjaśnimy w rozdziale 7.
Możemy teraz zademonstrować IntSet w akcji:
var x, y IntSet
x.Add(1)
x.Add(144)
168 ROZDZIAŁ 6. METODY

x.Add(9)
fmt.Println(x.String()) // "{1 9 144}"

y.Add(9)
y.Add(42)
fmt.Println(y.String()) // "{9 42}"

x.UnionWith(&y)
fmt.Println(x.String()) // "{1 9 42 144}"

fmt.Println(x.Has(9), x.Has(123)) // "true false"


Jedna uwaga: zadeklarowaliśmy String i Has jako metody typu wskaźnika *IntSet nie z koniecz-
ności, ale dla zachowania spójności z pozostałymi dwiema metodami, które wymagają odbiornika
wskaźnikowego, ponieważ przypisują do s.words. W konsekwencji wartość IntSet nie posiada
metody String, co czasami prowadzi do niespodzianek takich jak ta:
fmt.Println(&x) // "{1 9 42 144}"
fmt.Println(x.String()) // "{1 9 42 144}"
fmt.Println(x) // "{[4398046511618 0 65536]}"
W pierwszym przypadku wyświetlamy wskaźnik *IntSet, który ma metodę String. W drugim
przypadku wywołujemy String() na zmiennej IntSet. Kompilator wstawia niejawną operację
&, dając nam wskaźnik, który ma metodę String. Jednak w trzecim przypadku, ponieważ wartość
IntSet nie ma metody String, fmt.Println wyświetla zamiast tego reprezentację struktury.
Ważne jest, aby nie zapominać o operatorze &. Uczynienie String metodą IntSet, a nie *IntSet,
może być dobrym pomysłem, ale ocena zależy od indywidualnego przypadku.
Ćwiczenie 6.1. Zaimplementuj te dodatkowe metody:
func (*IntSet) Len() int // zwraca liczbę elementów
func (*IntSet) Remove(x int) // usuwa x ze zbioru
func (*IntSet) Clear() // usuwa wszystkie elementy ze zbioru
func (*IntSet) Copy() *IntSet // zwraca kopię zbioru
Ćwiczenie 6.2. Zdefiniuj metodę (*IntSet).AddAll(...int) o zmiennej liczbie argumentów,
która pozwala dodawać listę wartości, np. s.AddAll(1, 2, 3).
Ćwiczenie 6.3. (*IntSet).UnionWith oblicza sumę dwóch zbiorów przy użyciu |, czyli wykonywa-
nego równolegle bitowego operatora OR. Zaimplementuj metody: IntersectWith (część wspólna),
DifferenceWith (różnica) oraz symmetricDifference (różnica symetryczna) dla odpowiednich
operacji na zbiorach. (Różnica symetryczna dwóch zbiorów zawiera elementy należące do jednego
lub drugiego zbioru, ale nie do obu jednocześnie).
Ćwiczenie 6.4. Dodaj metodę Elems, która zwraca wycinek zawierający elementy zbioru, odpo-
wiedni do przeprowadzenia na nim iteracji za pomocą pętli range.
Ćwiczenie 6.5. Typem każdego słowa używanym przez IntSet jest uint64, ale 64-bitowa arytme-
tyka może być nieskuteczna na platformie 32-bitowej. Zmodyfikuj program do korzystania z typu
uint, który jest najbardziej efektywnym typem liczby całkowitej bez znaku dla tej platformy.
Zamiast dzielić przez 64, zdefiniuj stałą przechowującą efektywny rozmiar uint w bitach 32 lub
64. Możesz użyć do tego celu być może zbyt sprytnego wyrażenia << (^uint(0) >> 63).
6.6. HERMETYZACJA 169

6.6. Hermetyzacja
Mówi się, że zmienna lub metoda obiektu jest zhermetyzowana, jeśli jest niedostępna dla klientów
obiektu. Hermetyzacja (ang. encapsulation), nazywana czasem ukrywaniem informacji (ang.
information hiding), jest kluczowym aspektem programowania obiektowego.
Język Go ma tylko jeden mechanizm kontrolowania widoczności nazw: identyfikatory napisane
wielką literą są eksportowane z pakietu, w którym są zdefiniowane, a nazwy napisane małą literą
nie są. Ten sam mechanizm, który ogranicza dostęp do elementów pakietu, ogranicza także dostęp
do pól struktury lub metod typu. W konsekwencji, aby zhermetyzować obiekt, musimy uczynić go
strukturą.
Z tego powodu typ IntSet z poprzedniego podrozdziału został zadeklarowany jako typ struct,
chociaż ma tylko jedno pole:
type IntSet struct {
words []uint64
}
Moglibyśmy zamiast tego zdefiniować IntSet jako typ wycinka w sposób przedstawiony poniżej,
choć oczywiście musielibyśmy zamienić w jego metodach każde wystąpienie s.words na *s:
type IntSet []uint64
Chociaż ta wersja IntSet zasadniczo byłaby równoważna, pozwoliłaby klientom z innych pakietów
bezpośrednio odczytywać i modyfikować ten wycinek. Innymi słowy: podczas gdy wyrażenie *s
może być używane w dowolnym pakiecie, s.words może się pojawić tylko w pakiecie definiującym
IntSet.
Kolejną konsekwencją tego mechanizmu opartego na nazwach jest to, że jednostką hermetyzacji
jest pakiet, a nie typ, tak jak w wielu innych językach. Pola typu struct są widoczne dla całego kodu
w obrębie tego samego pakietu. Nie ma różnicy, czy kod pojawia się w funkcji, czy w metodzie.
Hermetyzacja zapewnia trzy korzyści. Po pierwsze, ponieważ klienty nie mogą bezpośrednio
modyfikować zmiennych obiektu, trzeba sprawdzić mniejszą liczbę instrukcji, aby zrozumieć
możliwe wartości tych zmiennych.
Po drugie, ukrywanie szczegółów implementacji uniemożliwia klientom uzależnianie się od rzeczy,
które mogą się zmienić, co daje projektantowi większą swobodę rozwijania implementacji bez
naruszania kompatybilności interfejsu API.
Jako przykład rozważmy typ bytes.Buffer. Jest on często używany do gromadzenia bardzo krót-
kich łańcuchów znaków, więc opłacalną optymalizacją jest zarezerwowanie niewielkiej ilości do-
datkowej przestrzeni w obiekcie, aby uniknąć alokacji pamięci w tym powszechnym przypadku.
Ponieważ Buffer jest typem struktury, ta przestrzeń ma formę dodatkowego pola typu [64]byte
z nazwą pisaną małą literą. Ponieważ to pole nie jest eksportowane, to gdy zostaje dodane, klienty
typu Buffer poza pakietem bytes są nieświadome żadnej zmiany, z wyjątkiem zwiększenia wy-
dajności. Typ Buffer i jego metoda Grow zostały przedstawione poniżej (uproszczone dla jasności):
type Buffer struct {
buf []byte
initial [64]byte
/* ... */
}

// Grow rozszerza w razie potrzeby pojemność bufora, aby zagwarantować przestrzeń


// dla kolejnych n bajtów. […]
170 ROZDZIAŁ 6. METODY

func (b *Buffer) Grow(n int) {


if b.buf == nil {
b.buf = b.initial[:0] // używa początkowo wcześniej alokowanej przestrzeni
}
if len(b.buf)+n > cap(b.buf) {
buf := make([]byte, b.Len(), 2*cap(b.buf) + n)
copy(buf, b.buf)
b.buf = buf
}
}
Trzecią korzyścią z hermetyzacji, w wielu przypadkach najważniejszą, jest to, że zapobiega ona
dowolnemu ustawianiu zmiennych obiektu przez klienty. Ponieważ zmienne obiektów mogą być
ustawiane tylko przez funkcje w tym samym pakiecie, autor tego pakietu może zapewnić, że
wszystkie te funkcje będą utrzymywały wewnętrzne niezmienniki tego obiektu. Przedstawiony
poniżej typ Counter umożliwia np. klientom zwiększanie licznika lub resetowanie go do zera, ale
nie umożliwia ustawiania dla niego jakiejś dowolnej wartości:
type Counter struct { n int }

func (c *Counter) N() int { return c.n }


func (c *Counter) Increment() { c.n++ }
func (c *Counter) Reset() { c.n = 0 }
Funkcje, które jedynie uzyskują dostęp do wewnętrznych wartości typu lub je modyfikują (takie jak
poniższe metody typu Logger z pakietu log), są nazywane metodami pobierającymi (ang. getters)
i ustawiającymi (ang. setters). Podczas nazywania metody pobierającej zwykle pomijamy jednak
prefiks Get. To preferowanie zwięzłości rozciąga się na wszystkie metody, nie tylko na akcesory
pól, a także na inne zbędne prefiksy, takie jak Fetch, Find i Lookup.
package log

type Logger struct {


flags int
prefix string
// …
}

func (l *Logger) Flags() int


func (l *Logger) SetFlags(flag int)
func (l *Logger) Prefix() string
func (l *Logger) SetPrefix(prefix string)
Styl języka Go nie zabrania stosowania eksportowanych pól. Oczywiście gdy pole zostanie raz wy-
eksportowane, nie można tego cofnąć bez niekompatybilnych zmian w API, więc początkowy wy-
bór powinien być przemyślany i uwzględniać złożoność niezmienników, które muszą być utrzy-
mywane, a także prawdopodobieństwo przyszłych zmian oraz ilości kodu klienta, na który będzie
mieć wpływ taka zmiana.
Hermetyzacja nie zawsze jest pożądana. Poprzez ujawnienie swojej reprezentacji jako liczby nano-
sekund int64 typ time.Duration pozwala nam używać całej standardowej arytmetyki oraz operacji
porównania z czasami trwania, a nawet definiować stałe tego typu:
const day = 24 * time.Hour
fmt.Println(day.Seconds()) // "86400"
6.6. HERMETYZACJA 171

W kolejnym przykładzie przeciwstawmy IntSet typowi geometry.Path z początku tego rozdziału.


Path został zdefiniowany jako typ wycinka, co pozwala jego klientom konstruować instancje przy
użyciu składni literału wycinka, iterować przez wszystkie jego punkty za pomocą pętli range itd.,
podczas gdy te operacje są zabronione dla klientów typu IntSet.
Oto zasadnicza różnica: typ geometry.Path jest z natury ni mniej, ni więcej sekwencją punktów i nie
przewidujemy dodawania do niego nowych pól. Jest więc sensowne, aby pakiet geometry ujawniał,
że Path jest wycinkiem. W przeciwieństwie do tego typ IntSet jest jedynie reprezentowany jako wy-
cinek []uint64. Mógłby być reprezentowany za pomocą []uint lub czegoś zupełnie innego dla
zbiorów, które są rozproszone lub bardzo małe, i być może skorzystałby na dodatkowych funkcjach,
takich jak dodatkowe pole do rejestrowania liczby elementów w zbiorze. Z tych względów sen-
sowne jest, aby typ IntSet był nieprzezroczysty.
W tym rozdziale nauczyliśmy się, jak wiązać metody z typami nazwanymi i jak wywoływać te metody.
Chociaż metody są kluczowe w programowaniu obiektowym, stanowią tylko połowę obrazu.
Aby uzupełnić ten obraz, potrzebujemy interfejsów, które są tematem następnego rozdziału.
172 ROZDZIAŁ 6. METODY
Rozdział 7

Interfejsy

Typy interfejsowe wyrażają uogólnienia lub abstrakcje dotyczące zachowań innych typów. Dzięki
uogólnianiu interfejsy pozwalają pisać funkcje, które są bardziej elastyczne i adaptowalne, ponieważ
nie są związane ze szczegółami jednej konkretnej implementacji.
Wiele języków obiektowych ma jakąś koncepcję interfejsów, ale interfejsy języka Go wyróżnia to, że
ich warunki są spełniane pośrednio. Innymi słowy: nie ma potrzeby deklarowania wszystkich in-
terfejsów, których warunki spełnia konkretny typ. Wystarczy po prostu posiadanie niezbędnych
metod. Taka konstrukcja pozwala na tworzenie nowych interfejsów, których warunki są spełniane
przez istniejące konkretne typy bez ich zmieniania, co jest szczególnie przydatne dla typów zde-
finiowanych w niekontrolowanych przez Ciebie pakietach.
Ten rozdział rozpoczniemy od przyjrzenia się podstawowym mechanizmom typów interfejso-
wych i ich wartościom. Potem przestudiujemy kilka ważnych interfejsów ze standardowej bi-
blioteki — wielu programistów języka Go korzysta ze standardowych interfejsów w równym stopniu
jak ze swoich własnych. Na koniec przyjrzymy się asercjom typów (zob. podrozdział 7.10) oraz
przełącznikom typów (zob. podrozdział 7.13) i zobaczymy, w jaki sposób umożliwiają stoso-
wanie innego rodzaju ogólności.

7.1. Interfejsy jako kontrakty


Wszystkie omawiane do tej pory typy były typami konkretnymi. Typ konkretny określa dokładną
reprezentację swoich wartości i udostępnia wewnętrzne operacje tej reprezentacji, takie jak
arytmetyka dla liczb albo indeksowanie, append i range dla wycinków. Konkretny typ może
również zapewniać dodatkowe zachowania poprzez swoje metody. Gdy masz wartość konkretnego
typu, wiesz dokładnie, czym ona jest i co możesz z nią zrobić.
W języku Go istnieje jeszcze inny rodzaj typu, zwany typem interfejsowym. Interfejs jest typem
abstrakcyjnym. Nie udostępnia reprezentacji czy wewnętrznej struktury swoich wartości ani też
zbioru podstawowych obsługiwanych operacji. Ujawnia tylko niektóre swoje metody. Gdy masz
wartość typu interfejsowego, nie wiesz nic na temat tego, czym ona jest. Wiesz tylko, co może robić,
lub, mówiąc bardziej precyzyjnie, jakie zachowania są dostarczane przez jej metody.
Dotychczas używaliśmy dwóch podobnych funkcji do formatowania łańcuchów znaków: fmt.Printf,
która zapisuje wynik do standardowego strumienia wyjściowego (pliku), oraz fmt.Sprintf, która
zwraca wynik jako string. Byłoby niedobrze, gdyby ta trudna część, jaką jest formatowanie wyniku,
174 ROZDZIAŁ 7. INTERFEJSY

musiała być duplikowana ze względu na te drobne różnice w sposobie wykorzystywania wyniku.


Dzięki interfejsom tak nie jest. Obie te funkcje są w istocie funkcjami opakowującymi trzecią funkcję,
fmt.Fprintf, która jest neutralna w kwestii tego, co się dzieje z obliczanym przez nią wynikiem:
package fmt

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)

func Printf(format string, args ...interface{}) (int, error) {


return Fprintf(os.Stdout, format, args...)
}

func Sprintf(format string, args ...interface{}) string {


var buf bytes.Buffer
Fprintf(&buf, format, args...)
return buf.String()
}
Prefiks F w nazwie Fprintf oznacza plik (ang. file) i wskazuje, że sformatowane dane wyjściowe
powinny zostać zapisane w pliku dostarczonym jako pierwszy argument. W przypadku Printf
tym argumentem (os.Stdout) jest *os.File. Jednak w przypadku Sprintf tym argumentem nie
jest plik, chociaż pozornie go przypomina: &buf jest wskaźnikiem do bufora pamięci, w którym
mogą być zapisywane bajty.
Pierwszym parametrem funkcji Fprintf również nie jest plik. Jest nim io.Writer, czyli typ interfej-
sowy z następującą deklaracją:
package io

// Writer jest interfejsem, który opakowuje podstawową metodę Write.


type Writer interface {
// Write zapisuje len(p) bajtów ze zmiennej p do bazowego strumienia danych.
// Zwraca liczbę bajtów zapisanych z p (0 <= n <= len(p))
// oraz każdy napotkany błąd, który spowodował przedwczesne zatrzymanie zapisywania.
// Metoda Write musi zwracać błąd niebędący nil, jeśli zwraca n < len(p).
// Metoda Write nie może modyfikować danych wycinka, nawet tymczasowo.
//
// Implementacje nie mogą przechowywać w pamięci zmiennej p.
Write(p []byte) (n int, err error)
}
Interfejs io.Writer definiuje kontrakt między funkcją Fprintf a wywołującymi ją podmiotami.
Z jednej strony, kontrakt wymaga, aby podmiot wywołujący zapewnił wartość konkretnego typu,
takiego jak *os.File lub *bytes.Buffer, który ma metodę o nazwie Write z odpowiednimi sy-
gnaturą i zachowaniem. Z drugiej strony, kontrakt gwarantuje, że funkcja Fprintf będzie wyko-
nywać swoje zadania, mając daną dowolną wartość, która spełnia warunki interfejsu io.Writer.
Funkcja Fprintf nie może zakładać, że zapisuje w pliku lub pamięci, tylko że może wywołać
metodę Write.
Ponieważ funkcja fmt.Fprintf nie zakłada niczego w kwestii reprezentacji wartości i opiera się
tylko na zachowaniach gwarantowanych przez kontrakt io.Writer, możemy do tej funkcji bez-
piecznie przekazywać jako pierwszy argument wartość dowolnego typu konkretnego, który spełnia
warunki interfejsu io.Writer. Ta swoboda zastępowania jednego typu innym typem, który
spełnia warunki tego samego interfejsu, nazywa się podstawialnością i jest charakterystyczną
cechą programowania obiektowego.
7.1. INTERFEJSY JAKO KONTRAKTY 175

Sprawdźmy to przy użyciu nowego typu. Przedstawiona poniżej metoda Write typu *ByteCounter
jedynie zlicza zapisane w nim bajty przed ich porzuceniem. (Wymagana jest konwersja, aby za-
pewnić dopasowanie typów dla len(p) i *c w instrukcji przypisania +=).
code/r07/bytecounter
type ByteCounter int

func (c *ByteCounter) Write(p []byte) (int, error) {


*c += ByteCounter(len(p)) // konwersja int na ByteCounter
return len(p), nil
}
Ponieważ *ByteCounter spełnia warunki kontraktu io.Writer, możemy przekazać go do funkcji
Fprintf, która przeprowadza formatowanie swojego łańcucha znaków bez świadomości tej zmiany.
ByteCounter poprawnie akumuluje długość wyniku.
var c ByteCounter
c.Write([]byte("witaj"))
fmt.Println(c) // "5", = len("witaj")

c = 0 // resetowanie licznika
var name = "Marta"
fmt.Fprintf(&c, "witaj, %s", name)
fmt.Println(c) // "12", = len("witaj, Marta")
Poza io.Writer istnieje jeszcze inny bardzo ważny interfejs dla pakietu fmt. Funkcje Fprintf
i Fprintln zapewniają typom możliwość kontrolowania sposobu, w jaki wyświetlane są ich wartości.
W podrozdziale 2.5 zdefiniowaliśmy metodę String dla typu Celsius, aby temperatury były wy-
świetlane jako "100 °C", a w podrozdziale 6.5 wyposażyliśmy typ *IntSet w metodę String,
aby zbiory były przedstawiane z wykorzystaniem tradycyjnej notacji, np. "{1 2 3}". Zadeklaro-
wanie metody String sprawia, że dany typ spełnia warunki jednego z najszerzej stosowanych in-
terfejsów, czyli interfejsu fmt.Stringer:
package fmt

// String jest wykorzystywana do wyświetlania wartości przekazywanych


// jako operand do dowolnego formatu, który akceptuje łańcuch znaków,
// lub do funkcji wypisywania bez ustawionego formatu, takiej jak Print.
type Stringer interface {
String() string
}
W podrozdziale 7.10 wyjaśnimy, w jaki sposób pakiet fmt wykrywa, które wartości spełniają warunki
tego interfejsu.
Ćwiczenie 7.1. Wykorzystując pomysły z ByteCounter, zaimplementuj liczniki dla słów i linii. Przy-
dać Ci się może funkcja bufio.ScanWords.
Ćwiczenie 7.2. Napisz funkcję CountingWriter (z przedstawioną poniżej sygnaturą), która mając
dany interfejs io.Writer, zwraca nowy Writer, opakowujący oryginalny, oraz wskaźnik do zmiennej
int64, która w każdym momencie zawiera liczbę bajtów aktualnie zapisanych do nowego interfejsu
Writer.
func CountingWriter(w io.Writer) (io.Writer, *int64)
Ćwiczenie 7.3. Napisz metodę String dla typu *tree z programu code/r04/treesort (zob.
podrozdział 4.4), która ujawnia sekwencję wartości w drzewie.
176 ROZDZIAŁ 7. INTERFEJSY

7.2. Typy interfejsowe


Typ interfejsowy określa zestaw metod, które musi posiadać typ konkretny, aby został uznany
za instancję danego interfejsu.
Typ io.Writer jest jednym z najszerzej stosowanych interfejsów, ponieważ zapewnia abstrakcję
wszystkich typów, do których mogą być zapisywane bajty, co obejmuje: pliki, bufory pamięci,
połączenia sieciowe, klienty HTTP, programy archiwizujące, programy szyfrujące itd. Wiele innych
użytecznych interfejsów definiuje pakiet io. Interfejs Reader reprezentuje dowolny typ, z którego
można odczytywać bajty, a Closer jest dowolną wartością, którą można zamknąć, taką jak plik
lub połączenie sieciowe. (Prawdopodobnie dostrzegłeś już konwencję nazewnictwa stosowaną
dla wielu posiadających jedną metodę interfejsów języka Go).
package io

type Reader interface {


Read(p []byte) (n int, err error)
}

type Closer interface {


Close() error
}
Jeśli poszukamy dalej, znajdziemy deklaracje nowych typów interfejsów jako kombinacje istnieją-
cych. Oto dwa przykłady:
type ReadWriter interface {
Reader
Writer
}

type ReadWriteCloser interface {


Reader
Writer
Closer
}
Użyta powyżej składnia, przypominająca osadzanie struktur, pozwala nam określać kolejny inter-
fejs jako skrót służący do wypisywania wszystkich jego metod. Nazywa się to osadzaniem inter-
fejsu. Moglibyśmy zapisać interfejs io.ReadWriter bez osadzania (chociaż mniej zwięźle) w nastę-
pujący sposób:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
Moglibyśmy nawet pomieszać te dwa style:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
Wszystkie trzy deklaracje mają ten sam efekt. Kolejność, w jakiej pojawiają się metody, jest nie-
istotna. Znaczenie ma jedynie zestaw metod.
7.3. SPEŁNIANIE WARUNKÓW INTERFEJSU 177

Ćwiczenie 7.4. Funkcja strings.NewReader zwraca wartość, która spełnia warunki interfejsu
io.Reader (i innych), odczytując z jego argumentu, czyli łańcucha znaków. Zaimplementuj prostą
wersję funkcji NewReader i użyj jej, aby umożliwić przyjmowanie przez parser HTML (zob. podroz-
dział 5.2) danych wejściowych z łańcucha znaków.
Ćwiczenie 7.5. Funkcja LimitReader z pakietu io akceptuje argument r interfejsu io.Reader
oraz liczbę bajtów n, a następnie zwraca kolejny Reader, który odczytuje z argumentu r, ale zgłasza stan
końca pliku po n bajtach. Zaimplementuj ją.
func LimitReader(r io.Reader, n int64) io.Reader

7.3. Spełnianie warunków interfejsu


Typ spełnia warunki interfejsu, jeśli ma wszystkie metody, których wymaga dany interfejs. Przy-
kładowo: typ *os.File spełnia warunki interfejsów io.Reader, Writer, Closer i ReadWriter.
Typ *bytes.Buffer spełnia warunki interfejsów Reader, Write i ReadWriter, ale nie spełnia wa-
runków interfejsu Closer, ponieważ nie ma metody Close. Programiści języka Go często stosują
skrót myślowy, mówiąc, że konkretny typ „jest” określonym typem interfejsowym, co oznacza, że
spełnia warunki tego interfejsu, np. *bytes.Buffer „jest” typem io.Writer, a *os.File „jest” typem
io.ReadWriter.
Reguła przypisywalności (zob. punkt 2.4.2) dla interfejsów jest bardzo prosta: wyrażenie może być
przypisane do interfejsu tylko wtedy, gdy spełnia warunki tego interfejsu. Więc:
var w io.Writer
w = os.Stdout // OK: *os.File ma metodę Write
w = new(bytes.Buffer) // OK: *bytes.Buffer ma metodę Write
w = time.Second // błąd kompilacji: time.Duration nie ma metody Write
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File ma metody Read, Write i Close
rwc = new(bytes.Buffer) // błąd kompilacji: *bytes.Buffer nie ma metody Close
Ta reguła ma zastosowanie nawet wtedy, gdy prawa strona sama jest interfejsem:
w = rwc // OK: io.ReadWriteCloser ma metodę Write
rwc = w // błąd kompilacji: io.Writer nie ma metody Close
Ponieważ ReadWriter i ReadWriteCloser obejmują wszystkie metody interfejsu Writer, każdy
typ spełniający warunki interfejsu ReadWriter lub ReadWriteCloser siłą rzeczy spełnia warunki
interfejsu Writer.
Zanim przejdziemy dalej, powinniśmy wyjaśnić kwestię tego, co znaczy, że jakiś typ ma metodę.
Przypomnijmy z punktu 6.2, że dla każdego nazwanego typu konkretnego T niektóre jego metody
same posiadają odbiornik typu T, podczas gdy inne wymagają wskaźnika *T. Przypomnijmy rów-
nież, że prawidłowe jest wywołanie metody *T na argumencie typu T, pod warunkiem że ten ar-
gument jest zmienną. Kompilator pośrednio pobiera jej adres. Ale to jest zwykły lukier skła-
dniowy: wartość typu T nie posiada wszystkich metod, które ma wskaźnik *T, w wyniku czego
może spełniać wymagania mniejszej liczby interfejsów.
Wyjaśnimy to na przykładzie. Metoda String typu IntSet z podrozdziału 6.5 wymaga odbiornika
wskaźnikowego, więc nie możemy wywołać tej metody na nieadresowalnej wartości IntSet:
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // błąd kompilacji: String wymaga odbiornika *IntSet
178 ROZDZIAŁ 7. INTERFEJSY

Możemy jednak wywołać ją na zmiennej IntSet:


var s IntSet
var _ = s.String() // OK: s jest zmienną, a &s ma metodę String
Ponieważ jednak tylko *IntSet ma metodę String, tylko *IntSet spełnia warunki interfejsu
fmt.Stringer:
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // błąd kompilacji: IntSet nie ma metody String
Podrozdział 12.8 zawiera program wyświetlający metody dowolnej wartości, a narzędzie godoc
-analysis=type (zob. punkt 10.7.4) wyświetla metody każdego typu i relacje pomiędzy interfejsami
i typami konkretnymi.
Tak jak koperta opakowuje i ukrywa przechowywany list, tak interfejs opakowuje i ukrywa typ
konkretny i przechowywaną przez niego wartość. Wywoływane mogą być tylko metody ujawnione
przez typ interfejsowy, nawet jeśli dany typ konkretny ma jeszcze inne:
os.Stdout.Write([]byte("witaj")) // OK: *os.File ma metodę Write
os.Stdout.Close() // OK: *os.File ma metodę Close

var w io.Writer
w = os.Stdout
w.Write([]byte("witaj")) // OK: io.Writer ma metodę Write
w.Close() // błąd kompilacji: io.Writer nie ma metody Close
Interfejs z większą liczbą metod (taki jak io.ReadWriter) daje nam więcej informacji o wartościach,
jakie zawiera, i stawia większe wymagania dotyczące typów, które go implementują, niż interfejs
z mniejszą liczbą metod (taki jak io.Reader). Jakie więc informacje daje nam typ interface{},
który w ogóle nie ma żadnych metod, na temat typów konkretnych spełniających jego warunki?
Zgadza się: nie daje żadnych. Może się to wydawać bezużyteczne, ale w rzeczywistości typ interface{},
zwany pustym typem interfejsowym, jest nieodzowny. Ponieważ pusty typ interfejsowy nie stawia
żadnych wymagań dotyczących typów spełniających jego warunki, możemy do niego przypisać
dowolną wartość.
var any interface{}
any = true
any = 12.34
any = "witaj"
any = map[string]int{"jeden": 1}
any = new(bytes.Buffer)
Chociaż nie było oczywiste, używaliśmy typu pustego interfejsu już od pierwszego przykładu przed-
stawionego w tej książce, ponieważ pozwala on funkcjom takim jak fmt.Println lub errorf z pod-
rozdziału 5.7 akceptować argumenty dowolnego typu.
Oczywiście gdy utworzymy wartość interface{} zawierającą wartość logiczną, liczbę zmienno-
przecinkową, łańcuch znaków, mapę, wskaźnik lub dowolny inny typ, nie możemy bezpośrednio
nic zrobić z tą przechowywaną przez niego wartością, ponieważ ten interfejs nie ma metod. Musi-
my znaleźć sposób, aby ponownie wydobyć tę wartość. W podrozdziale 7.10 zobaczymy, jak to zrobić
przy użyciu asercji typów.
Ponieważ spełnienie warunków interfejsu zależy tylko od metod dwóch zaangażowanych w to ty-
pów, nie ma potrzeby deklarowania relacji między typem konkretnym a interfejsem, którego wa-
runki on spełnia. Mimo to czasami przydatne jest udokumentowanie i założenie danej relacji, gdy
jest ona zamierzona, ale w żaden inny sposób nie jest egzekwowana przez program. Poniższa
7.3. SPEŁNIANIE WARUNKÓW INTERFEJSU 179

deklaracja stwierdza w czasie kompilacji, że wartość typu *bytes.Buffer spełnia warunki interfejsu
io.Writer:
// Typ *bytes.Buffer musi spełniać warunki interfejsu io.Writer.
var w io.Writer = new(bytes.Buffer)
Nie musimy alokować nowej zmiennej, ponieważ nada się każda wartość typu *bytes.Buffer,
nawet nil, którą zapisujemy jako (*bytes.Buffer)(nil), używając konwersji bezpośredniej.
A ponieważ nigdy nie zamierzamy odwoływać się do zmiennej w, możemy zastąpić ją pustym iden-
tyfikatorem. Wszystkie te zmiany zebrane razem dają nam ten bardziej oszczędny wariant:
// Typ *bytes.Buffer musi spełniać warunki interfejsu io.Writer.
var _ io.Writer = (*bytes.Buffer)(nil)
Warunki niepustych typów interfejsowych, takich jak io.Writer, są najczęściej spełniane przez
typ wskaźnika, w szczególności gdy jedna z metod tego interfejsu lub kilka z nich zakłada pewien
rodzaj mutacji odbiornika, tak jak metoda Write. Wskaźnik do struktury jest szczególnie popu-
larnym typem przenoszącym metody.
Jednak typy wskaźników wcale nie są jedynymi typami, które spełniają warunki interfejsów, i nawet
warunki interfejsów z metodami modyfikującymi mogą być spełnione przez jeden z innych typów
referencyjnych języka Go. Widzieliśmy przykłady typów wycinka z metodami (geometry.Path,
podrozdział 6.1) i typów map z metodami (url.Values, punkt 6.2.1), a później zobaczymy typ
funkcji z metodami (http.HandlerFunc, podrozdział 7.7). Nawet podstawowe typy mogą
spełniać warunki interfejsów. Jak zobaczymy w podrozdziale 7.4, typ time.Duration spełnia
warunki interfejsu fmt.Stringer.
Typ konkretny może spełniać warunki wielu niepowiązanych interfejsów. Rozważmy program,
który organizuje lub sprzedaje cyfrowe artefakty kulturowe, takie jak muzyka, filmy i książki.
Może on definiować następujący zestaw typów konkretnych:
Album
Book
Movie
Magazine
Podcast
TVEpisode
Track
Każdą interesującą nas abstrakcję możemy wyrazić jako interfejs. Niektóre właściwości są wspólne
dla wszystkich artefaktów, np.: tytuł, data utworzenia oraz lista twórców (autorów lub artystów).
type Artifact interface {
Title() string
Creators() []string
Created() time.Time
}
Inne właściwości są ograniczone do określonych typów artefaktów. Właściwości słowa drukowanego
dotyczą tylko książek i czasopism, podczas gdy rozdzielczość mają tylko filmy i seriale telewizyjne.
type Text interface {
Pages() int
Words() int
PageSize() int
}

type Audio interface {


Stream() (io.ReadCloser, error)
180 ROZDZIAŁ 7. INTERFEJSY

RunningTime() time.Duration
Format() string // np. "MP3", "WAV"
}
type Video interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // np. "MP4", "WMV"
Resolution() (x, y int)
}
Interfejsy są tylko jednym z wielu sposobów grupowania powiązanych typów konkretnych i wyra-
żania ich wspólnych aspektów. Inne sposoby grupowania poznamy później. Jeśli okazałoby się
np., że potrzebujemy obsługiwać elementy Audio i Video w taki sam sposób, moglibyśmy zdefinio-
wać interfejs Streamer służący do reprezentowania ich wspólnych aspektów bez zmiany istniejących
deklaracji typów.
type Streamer interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string
}
Każde grupowanie typów konkretnych oparte na ich wspólnych zachowaniach może być wyrażone
jako typ interfejsowy. W przeciwieństwie do języków opartych na klasach, w których zestaw interfej-
sów o warunkach spełnianych przez jakąś klasę jest wyraźnie określony, w języku Go możemy
definiować nowe abstrakcje lub grupy interesów w razie potrzeby, bez modyfikacji deklaracji typu
konkretnego. Jest to szczególnie przydatne, gdy dany typ konkretny pochodzi z pakietu napisanego
przez innego autora. Oczywiście muszą istnieć bazowe podobieństwa w tych typach konkretnych.

7.4. Parsowanie flag za pomocą interfejsu flag.Value


W tym podrozdziale zobaczymy, jak inny standardowy interfejs, flag.Value, pomaga nam defi-
niować nowe notacje dla flag wiersza poleceń. Rozważmy poniższy program, który zostaje uśpiony
na określony czas.
code/r07/sleep
var period = flag.Duration("period", 1*time.Second, "sleep period")

func main() {
flag.Parse()
fmt.Printf("Śpi przez %v...", *period)
time.Sleep(*period)
fmt.Println()
}
Zanim program zostanie uśpiony, wyświetla przedział czasu. Pakiet fmt wywołuje metodę
String typu time.Duration, żeby wyświetlić przedział czasu, ale nie w nanosekundach, tylko
w sposób przyjazny dla użytkownika:
$ go build code/r07/sleep
$ ./sleep
Śpi przez 1s...
Domyślnym okresem uśpienia jest jedna sekunda, ale można go kontrolować za pomocą flagi
wiersza poleceń -period. Funkcja flag.Duration tworzy zmienną flagi o typie time.Duration
i pozwala użytkownikowi określać czas trwania uśpienia w różnych przyjaznych dla użytkownika
7.4. PARSOWANIE FLAG ZA POMOCĄ INTERFEJSU FLAG.VALUE 181

formatach, również w tej samej notacji, która jest wyświetlana przez metodę String. Ta symetria
rozwiązania pozwala uzyskać miły interfejs użytkownika.
$ ./sleep -period 50ms
Śpi przez 50ms...
$ ./sleep -period 2m30s
Śpi przez 2m30s...
$ ./sleep -period 1.5h
Śpi przez 1h30m0s...
$ ./sleep -period "1 dzień"
invalid value "1 dzień" for flag -period: time: unknown unit dzień in duration 1 dzień
Ponieważ flagi czasu trwania są tak przydatne, ta funkcja jest wbudowana w pakiet flag, ale łatwo
jest zdefiniować nową notację flag dla własnych typów danych. Trzeba tylko zdefiniować typ
spełniający warunki interfejsu flag.Value, którego deklaracja została przedstawiona poniżej:
package flag

// Value jest interfejsem dla wartości przechowywanej we fladze.


type Value interface {
String() string
Set(string) error
}
Metoda String formatuje wartość flagi do wykorzystywania w komunikatach pomocy wiersza
poleceń. Zatem każdy interfejs flag.Value jest również interfejsem fmt.Stringer. Metoda Set
parsuje swój argument w postaci łańcucha znaków i aktualizuje wartość flagi. W efekcie metoda Set
stanowi odwrotność metody String i jest dobrą praktyką, aby obie te metody korzystały z tej
samej notacji.
Zdefiniujmy typ celsiusFlag, który pozwala określać temperaturę w stopniach Celsjusza lub Fahren-
heita z odpowiednią konwersją. Należy zwrócić uwagę, że celsiusFlag osadza typ Celsius (zob.
podrozdział 2.5), a tym samym uzyskuje za darmo metodę String. Aby spełnić warunki inter-
fejsu flag.Value, trzeba tylko zadeklarować metodę Set:
code/r07/tempconv
// Typ *celsiusFlag spełnia warunki interfejsu flag.Value.
type celsiusFlag struct{ Celsius }

func (f *celsiusFlag) Set(s string) error {


var unit string
var value float64
fmt.Sscanf(s, "%f%s", &value, &unit) // nie potrzeba kontroli błędów
switch unit {
case "C", "°C":
f.Celsius = Celsius(value)
return nil
case "F", "°F":
f.Celsius = FToC(Fahrenheit(value))
return nil
}
return fmt.Errorf("nieprawidłowa temperatura %q", s)
}
Wywołanie funkcji fmt.Sscanf parsuje liczbę zmiennoprzecinkową (value) i łańcuch znaków
(unit) z danych wejściowych s. Chociaż zwykle trzeba sprawdzać wynik błędu funkcji Sscanf,
w tym przypadku nie musimy tego robić, ponieważ jeśli wystąpiłby problem, nie zostałby dopasowa-
ny żaden przypadek instrukcji switch.
182 ROZDZIAŁ 7. INTERFEJSY

To wszystko opakowuje przedstawiona poniżej funkcja CelsiusFlag. Zwraca ona podmiotowi


wywołującemu wskaźnik do pola Celsius osadzonego w zmiennej f typu celsiusFlag. Pole
Celsius jest zmienną, która będzie aktualizowana przez metodę Set w trakcie przetwarzania flag.
Wywołanie Var dodaje daną flagę do zestawu flag wiersza poleceń aplikacji, czyli zmiennej globalnej
flag.CommandLine. Programy z niezwykle skomplikowanymi interfejsami wiersza poleceń mogą
mieć kilka zmiennych tego typu. Wywołanie Var przypisuje argument *celsiusFlag do parametru
flag.Value, powodując, że kompilator sprawdza, czy *celsiusFlag posiada niezbędne metody.
// CelsiusFlag definiuje flagę Celsius z określoną nazwą, domyślną wartością
// oraz informacją o sposobie wykorzystania i zwraca adres zmiennej flagi.
// Argument flagi musi podawać ilość i jednostkę, np. "100C".
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
f := celsiusFlag{value}
flag.CommandLine.Var(&f, name, usage)
return &f.Celsius
}
Teraz możemy zacząć używać tej nowej flagi w naszych programach:
code/r07/tempflag
var temp = tempconv.CelsiusFlag("temp", 20.0, "temperatura")

func main() {
flag.Parse()
fmt.Println(*temp)
}
Oto typowa sesja:
$ go build code/r07/tempflag
$ ./tempflag
20°C
$ ./tempflag -temp -18C
-18°C
$ ./tempflag -temp 212°F
100°C
$ ./tempflag -temp 273.15K
invalid value "273.15K" for flag -temp: nieprawidłowa temperatura "273.15K"
Usage of ./tempflag:
-temp value
temperatura (default 20°C)
$ ./tempflag -help
Usage of ./tempflag:
-temp value
temperatura (default 20°C)
Ćwiczenie 7.6. Dodaj do programu tempflag wsparcie dla temperatury w skali Kelvina.
Ćwiczenie 7.7. Wyjaśnij, dlaczego komunikat pomocy zawiera symbol °C, podczas gdy domyślna
wartość 20.0 go nie zawiera.

7.5. Wartości interfejsów


Koncepcyjnie wartość typu interfejsowego, czyli inaczej wartość interfejsu, obejmuje dwa kompo-
nenty: typ konkretny i wartość tego typu. Są one nazywane dynamicznym typem i dynamiczną
wartością interfejsu.
7.5. WARTOŚCI INTERFEJSÓW 183

W przypadku języka typowanego statycznie, takiego jak Go, typy są pojęciem czasu kompilacji,
więc typ nie jest wartością. W naszym modelu koncepcyjnym zestaw wartości, zwanych deskryp-
torami typów, dostarcza na temat każdego typu informacji takich jak jego nazwa i metody. W warto-
ści interfejsu komponent typu jest reprezentowany przez odpowiedni deskryptor typu.
W czterech poniższych instrukcjach zmienna w przyjmuje trzy różne wartości. (Wartość początkowa
jest taka sama jak końcowa).
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
Przyjrzyjmy się bliżej wartości i dynamicznemu zachowaniu zmiennej w po każdej instrukcji.
Pierwsza instrukcja deklaruje w:
var w io.Writer
W języku Go zmienne są zawsze inicjowane do wyraźnie określonej wartości, a interfejsy nie są wy-
jątkiem. Wartość zerowa dla interfejsu ma ustawione na nil oba jego komponenty: typ i wartość
(rysunek 7.1).

Rysunek 7.1. Wartość nil interfejsu

Wartość interfejsu jest opisywana jako nil lub różna od nil na podstawie jego dynamicznego
typu, więc to jest wartość nil interfejsu. Możesz sprawdzić, czy wartością interfejsu jest nil, za
pomocą w == nil lub w != nil. Wywołanie jakiejkolwiek metody wartości nil interfejsu wywołuje
procedurę panic:
w.Write([]byte("witaj")) // panic: wyłuskanie wskaźnika nil
Druga instrukcja przypisuje do zmiennej w wartość typu *os.File:
w = os.Stdout
To przypisanie obejmuje pośrednią konwersję z typu konkretnego na typ interfejsowy i jest równo-
ważne z bezpośrednią konwersją io.Writer(os.Stdout). Konwersja tego rodzaju, pośrednia lub
bezpośrednia, przechwytuje typ i wartość jego operandu. Typ dynamiczny interfejsu jest ustawiany
na deskryptor typu dla typu wskaźnika *os.File, a jego wartość dynamiczna przechowuje kopię
os.Stdout, która jest wskaźnikiem do zmiennej os.File reprezentującej standardowy strumień
wyjściowy procesu (rysunek 7.2).

Rysunek 7.2. Wartość interfejsu zawierająca wskaźnik *os.File


184 ROZDZIAŁ 7. INTERFEJSY

Wywołanie metody Write na wartości interfejsu zawierającej wskaźnik *os.File powoduje wywoła-
nie metody (*os.File).Write. To wywołanie wyświetla "witaj".
w.Write([]byte("witaj")) // "witaj"
Zasadniczo podczas kompilacji nie możemy wiedzieć, jaki będzie dynamiczny typ wartości inter-
fejsu, więc wywołanie poprzez interfejs musi używać dynamicznego rozdzielania (ang. dynamic
dispatch). Zamiast bezpośredniego wywołania kompilator musi wygenerować kod, aby uzyskać adres
metody o nazwie Write z deskryptora typu, a następnie wykonać pośrednie wywołanie tego adre-
su. Argumentem odbiornika dla tego wywołania jest kopia wartości dynamicznej interfejsu —
os.Stdout. Efekt jest taki, jakbyśmy wykonali to wywołanie bezpośrednio:
os.Stdout.Write([]byte("witaj")) // "witaj"
Trzecia instrukcja przypisuje wartość typu *bytes.Buffer do wartości interfejsu:
w = new(bytes.Buffer)
Typem dynamicznym jest teraz *bytes.Buffer, a wartością dynamiczną jest wskaźnik do nowo alo-
kowanego bufora (rysunek 7.3).

Rysunek 7.3. Wartość interfejsu zawierająca wskaźnik *bytes.Buffer

Wywołanie metody Write wykorzystuje ten sam mechanizm co poprzednio:


w.Write([]byte("witaj")) // zapisuje "witaj" w bytes.Buffer
Tym razem deskryptorem typu jest *bytes.Buffer, więc wywoływana jest metoda (*bytes.
Buffer).Write z adresem bufora jako wartością parametru odbiornika. To wywołanie dołącza
"witaj" do bufora.
Na koniec czwarta instrukcja przypisuje nil do wartości interfejsu:
w = nil
To przypisanie resetuje oba jego komponenty do wartości nil, przywracając zmienną w do tego same-
go stanu, w jakim była po zadeklarowaniu, czyli do stanu, który został przedstawiony na rysunku 7.1.
Wartość interfejsu może przechowywać dowolnie duże wartości dynamiczne. Przykładowo: typ
time.Time, który reprezentuje moment w czasie, jest typem struct z kilkoma niewyeksporto-
wanymi polami. Jeśli utworzymy z niego wartość interfejsu
var x interface{} = time.Now()
wynik może wyglądać tak, jak pokazano na rysunku 7.4. Koncepcyjnie wartość dynamiczna zawsze
mieści się wewnątrz wartości interfejsu, bez względu na to, jak duży jest jego typ. (To tylko model
koncepcyjny. Rzeczywista implementacja jest zupełnie inna)
7.5. WARTOŚCI INTERFEJSÓW 185

Rysunek 7.4. Wartość interfejsu przechowuje strukturę time.Time

Wartości interfejsów mogą być porównywane za pomocą operatorów == i !=. Dwie wartości in-
terfejsów są równe, jeśli obie są nil lub jeśli ich typy dynamiczne są identyczne, a ich wartości
dynamiczne są równe zgodnie ze standardowym zachowaniem operatora == dla danego typu.
Ponieważ wartości interfejsów są porównywalne, mogą być stosowane jako klucze map lub jako
operandy instrukcji switch.
Jeśli jednak porównywane są dwie wartości interfejsów i mają one ten sam typ dynamiczny, który
jednak nie jest porównywalny (np. wycinek), porównanie nie powiedzie się i wywoła panikę:
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: porównywanie nieporównywalnego typu []int
Pod tym względem typy interfejsów są ewenementem. Inne typy są bezpiecznie porównywalne
(typy podstawowe i wskaźniki) albo w ogóle nieporównywalne (wycinki, mapy i funkcje), ale kie-
dy porównujemy wartości interfejsów lub typy złożone, które zawierają wartości interfejsów,
musimy być świadomi możliwości uruchomienia procedury panic. Podobne ryzyko istnieje, gdy
używamy interfejsów jako kluczy map lub operandów instrukcji switch. Wartości interfejsów na-
leży porównywać tylko wtedy, kiedy ma się pewność, że zawierają one dynamiczne wartości po-
równywalnych typów.
Podczas obsługi błędów lub debugowania często pomocne jest raportowanie dynamicznego typu
wartości interfejsu. W tym celu używamy czasownika %T pakietu fmt:
var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"

w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"

w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"
Wewnętrznie pakiet fmt do uzyskania nazwy dynamicznego typu interfejsu wykorzystuje refleksję.
Refleksji przyjrzymy się w rozdziale 12.

7.5.1. Zastrzeżenie: interfejs zawierający wskaźnik nil jest różny od nil


Wartość nil interfejsu, który nie zawiera w ogóle żadnej wartości, nie jest tym samym co wartość
interfejsu zawierającego wskaźnik, który akurat jest nil. Ta subtelna różnica tworzy pułapkę, w którą
wpadł chyba każdy programista Go.
Rozważmy poniższy program. Przy stałej debug ustawionej na wartość true funkcja main gromadzi
dane wyjściowe z funkcji f w typie bytes.Buffer.
186 ROZDZIAŁ 7. INTERFEJSY

const debug = true

func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // włączenie gromadzenia danych wyjściowych
}
f(buf) // Uwaga: subtelnie nieprawidłowe!
if debug {
// …użycie buf…
}
}

// Jeśli parametr out jest różny od nil, dane wyjściowe będą w nim zapisywane.
func f(out io.Writer) {
// …coś do zrobienia…
if out != nil {
out.Write([]byte("zrobione!\n"))
}
}
Można by się spodziewać, że zmiana debug na false wyłączy gromadzenie danych wyjścio-
wych, ale w rzeczywistości powoduje, że program uruchamia procedurę panic podczas wywołania
out.Write:
if out != nil {
out.Write([]byte("zrobione!\n")) // panic: wyłuskanie wskaźnika nil
}
Gdy funkcja main wywołuje funkcję f, przypisuje do parametru out wskaźnik nil typu *bytes.
Buffer, więc wartością dynamiczną parametru out jest nil. Jego typem dynamicznym jest jednak
*bytes.Buffer, co oznacza, że out jest interfejsem różnym od nil, zawierającym wartość wskaź-
nika nil (rysunek 7.5), więc sprawdzenie defensywne out != nil jest nadal prawdziwe.

Rysunek 7.5. Różny od nil interfejs zawierający wskaźnik nil

Tak jak wcześniej dynamiczny mechanizm rozdzielania określa, że wywołana musi być metoda
(*bytes.Buffer).Write, ale tym razem z wartością odbiornika, którą jest nil. Dla niektórych typów,
takich jak *os.File, nil jest prawidłowym odbiornikiem (zob. punkt 6.2.1), ale *bytes.Buffer
do nich nie należy. Ta metoda jest wywoływana, ale uruchamia procedurę panic, ponieważ próbuje
uzyskać dostęp do bufora.
Problem polega na tym, że chociaż wskaźnik nil typu *bytes.Buffer ma metody niezbędne do
spełnienia warunków tego interfejsu, to nie spełnia jego wymagań behawioralnych. Wywołanie
to narusza w szczególności dorozumiany warunek wstępny metody (*bytes.Buffer).Write,
który zakłada, że jej odbiornik nie będzie nil, więc przypisanie wskaźnika nil do tego interfejsu
było błędem. Rozwiązaniem jest zmiana typu zmiennej buf w funkcji main na io.Writer, co po-
zwala przede wszystkim uniknąć przypisania do interfejsu dysfunkcyjnej wartości:
7.6. SORTOWANIE ZA POMOCĄ INTERFEJSU SORT.INTERFACE 187

var buf io.Writer


if debug {
buf = new(bytes.Buffer) // włączenie gromadzenia danych wyjściowych
}
f(buf) // OK
Omówiliśmy mechanizmy wartości interfejsu, więc przyjrzyjmy się teraz kilku ważniejszym inter-
fejsom ze standardowej biblioteki języka Go. W trzech kolejnych podrozdziałach zobaczymy, w jaki
sposób interfejsy są używane do sortowania, serwowania zawartości WWW i obsługi błędów.

7.6. Sortowanie za pomocą interfejsu sort.Interface


Tak jak formatowanie łańcuchów znaków sortowanie jest często używaną operacją w wielu pro-
gramach. Chociaż minimalny algorytm sortowania szybkiego (ang. quicksort) można zmieścić w ja-
kichś 15 liniach kodu, solidna implementacja jest znacznie dłuższa i nie jest to rodzaj kodu, który
za każdym razem chcielibyśmy pisać od nowa lub kopiować, gdy jest on potrzebny.
Na szczęście pakiet sort zapewnia sortowanie in situ dowolnej sekwencji według którejkolwiek
funkcji porządkującej. Jego konstrukcja jest dość niezwykła. W wielu językach algorytm sortowa-
nia jest powiązany z sekwencyjnym typem danych, podczas gdy funkcja porządkująca jest powią-
zana z typem elementów. Natomiast funkcja sort.Sort języka Go nie zakłada niczego na temat
reprezentacji sekwencji lub jej elementów. Zamiast tego wykorzystuje interfejs sort.Interface,
służący do określania kontraktu pomiędzy ogólnym algorytmem sortowania i każdym typem se-
kwencyjnym, który może być sortowany. Implementacja tego interfejsu określa zarówno konkretną
reprezentację sekwencji (którą często jest wycinek), jak i wymagany porządek jej elementów.
Algorytm sortowania in situ potrzebuje trzech rzeczy (długości sekwencji, zasad porównywania
dwóch elementów i sposobu zamieniania dwóch elementów), więc interfejs sort.Interface ma
trzy metody:
package sort

type Interface interface {


Len() int
Less(i, j int) bool // i, j to indeksy elementów sekwencji
Swap(i, j int)
}
Aby posortować dowolną sekwencję, musimy zdefiniować typ implementujący te trzy metody, a na-
stępnie zastosować funkcję sort.Sort do instancji tego typu. Rozważmy sortowanie wycinka łań-
cuchów znaków jako być może najprostszy przykład. Poniżej pokazano nowy typ StringSlice
i jego metody Len, Less i Swap.
type StringSlice []string

func (p StringSlice) Len() int { return len(p) }


func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
Teraz możemy posortować wycinek łańcuchów znaków (names) poprzez przekonwertowanie go
na typ StringSlice w taki sposób:
sort.Sort(StringSlice(names))
Ta konwersja daje wartość wycinka o tej samej długości, pojemności i tablicy bazowej co names,
ale z typem, który ma trzy metody wymagane do sortowania.
188 ROZDZIAŁ 7. INTERFEJSY

Sortowanie wycinka łańcuchów znaków jest tak powszechne, że pakiet sort zapewnia typ
StringSlice oraz funkcję o nazwie Strings, więc powyższe wywołanie można uprościć do postaci
sort.Strings(names).
Przedstawioną tu technikę można łatwo dostosować do innych porządków sortowania, np. do
ignorowania wielkich liter lub znaków specjalnych. (Program Go sortujący tematy skorowidza
i numery stron dla tej książki robi to przy dodatkowej logice dla cyfr rzymskich). Do skompliko-
wanego sortowania używamy tej samej koncepcji, ale z bardziej skomplikowanymi strukturami
danych lub z bardziej skomplikowanymi implementacjami metod interfejsu sort.Interface.
Naszym działającym przykładem sortowania będzie lista odtwarzania utworów muzycznych wy-
świetlona w postaci tabeli. Każdy utwór będzie pojedynczym wierszem, a każda kolumna będzie
atrybutem tego utworu, takim jak: artysta, tytuł i czas odtwarzania. Wyobraź sobie, że graficzny
interfejs użytkownika prezentuje tabelę, a kliknięcie nagłówka wybranej kolumny powoduje po-
sortowanie listy odtwarzania według tego atrybutu. Ponowne kliknięcie nagłówka tej samej ko-
lumny odwraca kolejność. Zobaczmy, co się może zdarzyć w reakcji na każde kliknięcie.
Użyta poniżej zmienna tracks zawiera listę odtwarzania. (Jeden z autorów przeprasza za gust mu-
zyczny drugiego). Każdy element jest pośredni — jest wskaźnikiem do typu Track. Chociaż poniższy
kod działałby, gdybyśmy przechowywali elementy Track bezpośrednio, funkcja sortowania za-
mieni wiele par elementów, więc będzie działać szybciej, jeżeli każdy element będzie wskaźnikiem,
czyli pojedynczym słowem maszynowym, a nie całym typem Track, który może mieć osiem
słów lub więcej.
code/r07/sorting
type Track struct {
Title string
Artist string
Album string
Year int
Length time.Duration
}
var tracks = []*Track{
{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
{"Go", "Moby", "Moby", 1992, length("3m37s")},
{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
}
func length(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
panic(s)
}
return d
}
Funkcja printTracks wyświetla listę odtwarzania w postaci tabeli. Wyświetlenie graficzne było-
by ładniejsze, ale ta prosta procedura wykorzystuje pakiet text/tabwriter do wygenerowania tabeli,
której kolumny są starannie wyrównane i dopełnione, tak jak pokazano nieco niżej. Zauważmy,
że *tabwriter.Writer spełnia warunki interfejsu io.Writer. Gromadzi każdy zapisany w nim
fragment danych. Jego metoda Flush formatuje całą tabelę i zapisuje ją do os.Stdout.
func printTracks(tracks []*Track) {
const format = "%v\t%v\t%v\t%v\t%v\t\n"
7.6. SORTOWANIE ZA POMOCĄ INTERFEJSU SORT.INTERFACE 189

tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0)


fmt.Fprintf(tw, format, "Tytuł", "Artysta", "Album", "Rok", "Długość")
fmt.Fprintf(tw, format, "-----", "-------", "-----", "---", "-------")
for _, t := range tracks {
fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
}
tw.Flush() // oblicza szerokości kolumn i wyświetla tabelę
}
Aby posortować listę utworów według pola Artist, definiujemy nowy typ wycinka z niezbędnymi
metodami Len, Less i Swap, analogicznie do tego, co zrobiliśmy dla StringSlice.
type byArtist []*Track

func (x byArtist) Len() int { return len(x) }


func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist }
func (x byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
Aby wywołać ogólną procedurę sortowania, musimy najpierw przekonwertować tracks na no-
wy typ byArtist, który definiuje porządek:
sort.Sort(byArtist(tracks))
Po posortowaniu wycinka według artysty dane wyjściowe z funkcji printTracks są następujące:
Tytuł Artysta Album Rok Długość
----- ------- ----- --- -------
Go Ahead Alicia Keys As I Am 2007 4m36s
Go Delilah From the Roots Up 2012 3m38s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Moby Moby 1992 3m37s
Jeśli użytkownik po raz drugi zażąda „sortuj według artysty”, utwory zostaną posortowane w od-
wrotnej kolejności. Nie musimy jednak definiować nowego typu byReverseArtist z odwróconą
metodą Less, ponieważ pakiet sort zapewnia funkcję Reverse, która przekształca dowolny porzą-
dek sortowania na jego odwrotność:
sort.Sort(sort.Reverse(byArtist(tracks)))
Po odwrotnym posortowaniu wycinka według artysty dane wyjściowe z funkcji printTracks są
następujące:
Tytuł Artysta Album Rok Długość
----- ------- ----- --- -------
Go Moby Moby 1992 3m37s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
Funkcja sort.Reverse zasługuje na uwagę, ponieważ wykorzystuje kompozycję (zob. podrozdział
6.3), która jest ważną koncepcją. Pakiet sort definiuje niewyeksportowany typ reverse, który jest
strukturą osadzającą sort.Interface. Metoda Less dla typu reverse wywołuje metodę Less osa-
dzonej wartości sort.Interface, ale z odwróconymi indeksami, co odwraca kolejność wyników
sortowania.
package sort
type reverse struct{ Interface } // czyli sort.Interface
func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }
190 ROZDZIAŁ 7. INTERFEJSY

Pozostałe dwie metody typu reverse, czyli Len i Swap, są pośrednio dostarczane przez oryginal-
ną wartość sort.Interface, ponieważ jest to osadzone pole. Wyeksportowana funkcja Reverse
zwraca instancję typu reverse, która zawiera oryginalną wartość sort.Interface.
Aby posortować według innej kolumny, musimy zdefiniować nowy typ, np. byYear (według roku):
type byYear []*Track

func (x byYear) Len() int { return len(x) }


func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year }
func (x byYear) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
Po posortowaniu trakcs według roku za pomocą sort.Sort(byYear(tracks)) funkcja printTracks
wyświetla chronologiczny wykaz:
Tytuł Artysta Album Rok Długość
----- ------- ----- --- -------
Go Moby Moby 1992 3m37s
Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Delilah From the Roots Up 2012 3m38s
Dla każdego typu elementu wycinka i każdej funkcji porządkowania, której potrzebujemy, dekla-
rujemy nową implementację sort.Interface. Jak widać, metody Len i Swap mają identyczne
definicje dla wszystkich typów wycinka. W następnym przykładzie typ konkretny customSort
łączy wycinek z funkcją, pozwalając nam zdefiniować nowy porządek poprzez napisanie jedynie
funkcji porównania. Nawiasem mówiąc, konkretne typy implementujące sort.Interface nie
zawsze są wycinkami. Typ customSort jest typem struct.
type customSort struct {
t [ ]*Track
less func(x, y *Track) bool
}

func (x customSort) Len() int { return len(x.t) }


func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }
Zdefiniujmy wielopoziomową funkcję porządkowania, której głównym kluczem sortowania jest
Title (tytuł), kluczem wtórnym jest Year (rok), a kluczem trzeciorzędowym jest Length (długość
utworu). Oto wywołanie funkcji Sort wykorzystujące anonimową funkcję porządkowania:
sort.Sort(customSort{tracks, func(x, y *Track) bool {
if x.Title != y.Title {
return x.Title < y.Title
}
if x.Year != y.Year {
return x.Year < y.Year
}
if x.Length != y.Length {
return x.Length < y.Length
}
return false
}})
A oto wynik. Należy zwrócić uwagę, że powiązanie między dwoma utworami zatytułowanymi Go
zostało rozerwane na korzyść utworu starszego.
7.7. INTERFEJS HTTP.HANDLER 191

Tytuł Artysta Album Rok Długość


----- ------- ----- --- -------
Go Moby Moby 1992 3m37s
Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Chociaż sortowanie sekwencji o długości n wymaga O(n log n) operacji porównania, sprawdzenie,
czy sekwencja została już posortowana, wymaga co najwyżej n–1 porównań. Funkcja IsSorted
z pakietu sort sprawdza to za nas. Podobnie jak sort.Sort, abstrahuje zarówno sekwencję, jak
i jej funkcję porządkującą za pomocą sort.Interface, ale nigdy nie wywołuje metody Swap.
Poniższy kod demonstruje funkcje IntsAreSorted i Ints oraz typ IntSlice:
values := []int{3, 1, 4, 1}
fmt.Println(sort.IntsAreSorted(values)) // "false"
sort.Ints(values)
fmt.Println(values) // "[1 1 3 4]"
fmt.Println(sort.IntsAreSorted(values)) // "true"
sort.Sort(sort.Reverse(sort.IntSlice(values)))
fmt.Println(values) // "[4 3 1 1]"
fmt.Println(sort.IntsAreSorted(values)) // "false"
Dla wygody pakiet sort zapewnia wersje swoich funkcji i typów wyspecjalizowane dla []int,
[]string i []float64 z wykorzystaniem ich naturalnych porządkowań. W przypadku innych
typów, takich jak []int64 lub []uint, jesteśmy zdani na siebie, choć droga jest krótka.
Ćwiczenie 7.8. Wiele graficznych interfejsów użytkownika zapewnia widżet tabeli z wielopozio-
mowym sortowaniem stanowym: podstawowym kluczem sortowania jest ostatnio kliknięty na-
główek kolumny, wtórnym kluczem sortowania jest kliknięty przedostatnio nagłówek kolumny
itd. Zdefiniuj implementację sort.Interface do wykorzystania przez taką tabelę. Porównaj to
podejście z wielokrotnym sortowaniem przy użyciu sort.Stable.
Ćwiczenie 7.9. Użyj pakietu html/template (zob. podrozdział 4.6), aby zastąpić printTracks
funkcją, która wyświetla utwory w postaci tabeli HTML. Użyj rozwiązania poprzedniego ćwicze-
nia, aby zaaranżować, że każde kliknięcie nagłówka kolumny będzie wysyłać żądanie HTTP w celu
posortowania tabeli.
Ćwiczenie 7.10. Typ sort.Interface może być zaadaptowany do innych zastosowań. Napisz
funkcję IsPalindrome(s sort.Interface) bool, która informuje, że sekwencja s jest palindro-
mem, czyli że odwrócenie sekwencji nie zmienia jej. Zakładamy, że elementy w indeksach i oraz j
są równe, jeśli !s.Less(i, j) && !s.Less(j, i).

7.7. Interfejs http.Handler


W rozdziale 1. zobaczyliśmy przelotnie, jak skorzystać z pakietu net/http w celu zaimplemento-
wania klientów (podrozdział 1.5) i serwerów WWW (podrozdział 1.7). W tym podrozdziale przyj-
rzymy się bliżej interfejsowi API serwera, którego podstawą jest interfejs http.Handler:
net/http
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
192 ROZDZIAŁ 7. INTERFEJSY

Funkcja ListenAndServe wymaga adresu serwera, np. "localhost:8000", oraz instancji interfejsu
Handler, do którego kierowane będą wszystkie żądania. Ta funkcja działa w nieskończoność lub
do momentu awarii serwera (lub niepowodzenia jego uruchomienia) z błędem, zawsze innym niż nil,
który jest przez tę funkcję zwracany.
Wyobraźmy sobie stronę e-commerce z bazą danych mapującą rzeczy na sprzedaż na ich ceny
w złotych. Poniższy program pokazuje najprostszą możliwą implementację. Modeluje ona magazyn
jako typ mapy (database), do którego doczepiamy metodę ServeHTTP, aby ten typ spełniał wyma-
gania interfejsu http.Handler. Procedura obsługi iteruje przez mapę za pomocą pętli range i wy-
świetla jej elementy.
code/r07/http1
func main() {
db := database{"buty": 50, "skarpety": 5}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("%.2f PLN", d) }

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {


for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
Uruchommy serwer:
$ go build code/r07/http1
$ ./http1 &
Jeśli połączymy się z nim za pomocą programu fetch z podrozdziału 1.5 (lub za pomocą przeglą-
darki internetowej, jeśli wolisz), otrzymamy następujące dane wyjściowe:
$ go build code/r01/fetch
$ ./fetch http://localhost:8000
buty: 50.00 PLN
skarpety: 5.00 PLN
Na razie serwer może wyświetlać tylko cały inwentarz i zrobi to dla każdego żądania, niezależnie
od adresu URL. W bardziej realistycznym przypadku dla serwera definiuje się wiele różnych adre-
sów URL, z których każdy wyzwala inne zachowanie. Nazwijmy istniejące żądanie /list i dodajmy
jeszcze jedno, o nazwie /price, które raportuje cenę pojedynczego elementu, określonego jako
parametr żądania, np. /price?item=skarpety.
code/r07/http2
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := db[item]
7.7. INTERFEJS HTTP.HANDLER 193

if !ok {
w.WriteHeader(http.StatusNotFound) // błąd 404
fmt.Fprintf(w, "nie ma takiej pozycji: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
default:
w.WriteHeader(http.StatusNotFound) // błąd 404
fmt.Fprintf(w, "nie ma takiej strony: %s\n", req.URL)
}
}
Teraz procedura obsługi na podstawie komponentu ścieżki URL, req.URL.Path, decyduje, jaką
logikę wykonać. Jeżeli procedura obsługi nie rozpozna ścieżki, zgłasza klientowi błąd HTTP po-
przez wywołanie w.WriteHeader(http.StatusNotFound). Należy to zrobić przed zapisaniem
jakiegokolwiek tekstu w parametrze w. (Nawiasem mówiąc, http.ResponseWriter jest kolejnym
interfejsem. Poszerza on interfejs io.Writer o metody do wysyłania nagłówków odpowiedzi HTTP).
Równoważnie możemy użyć funkcji narzędziowej http.Error:
msg := fmt.Sprintf("nie ma takiej strony: %s\n", req.URL)
http.Error(w, msg, http.StatusNotFound) // błąd 404
Instrukcja case dla żądania /price wywołuje metodę Query adresu URL w celu parsowania para-
metrów żądania HTTP jako mapy, a dokładniej multimapy typu url.Values (zob. punkt 6.2.l)
z pakietu net/url. Następnie znajduje pierwszy parametr item i szuka jego ceny. Jeśli element nie
zostanie znaleziony, zgłasza błąd.
Oto przykładowa sesja z nowym serwerem:
$ go build code/r07/http2
$ go build code/r01/fetch
$ ./http2 &
$ ./fetch http://localhost:8000/list
buty: 50.00 PLN
skarpety: 5.00 PLN
$ ./fetch http://localhost:8000/price?item=skarpety
5.00 PLN
$ ./fetch http://localhost:8000/price?item=buty
50.00 PLN
$ ./fetch http://localhost:8000/price?item=kapelusz
nie ma takiej pozycji: "kapelusz"
$ ./fetch http://localhost:8000/help
nie ma takiej strony: /help
Oczywiście do metody ServeHTTP moglibyśmy dodawać kolejne przypadki (case), ale w realistycznej
aplikacji wygodniej jest zdefiniować logikę dla każdego przypadku w osobnej funkcji lub metodzie.
Ponadto powiązane adresy URL mogą wymagać podobnej logiki. Kilka plików obrazów może
mieć np. adresy URL w postaci /images/*.png. Z tych powodów pakiet net/http zapewnia
ServeMux, czyli multiplekser żądań, aby uprościć powiązania między adresami URL i procedura-
mi obsługi. ServeMux łączy kolekcję procedur obsługi http.Handler w pojedynczy http.Handler.
Ponownie widzimy, że różne typy spełniające warunki tego samego interfejsu są podstawialne:
serwer WWW może rozsyłać żądania do dowolnej procedury obsługi http.Handler niezależnie
od tego, jaki typ konkretny się za nią kryje.
Dla bardziej złożonych aplikacji można skomponować kilka multiplekserów ServeMux do obsługi
bardziej zawiłych wymagań rozsyłania żądań. Język Go nie posiada kanonicznego frameworku WWW,
analogicznego do Rails dla Ruby lub Django dla Pythona. To nie znaczy, że takie frameworki nie
194 ROZDZIAŁ 7. INTERFEJSY

istnieją, ale elementy konstrukcyjne w standardowej bibliotece języka Go są tak elastyczne, że frame-
worki często bywają niepotrzebne. Ponadto, chociaż frameworki są wygodne we wczesnych fazach
projektu, ich dodatkowa złożoność może utrudnić długoterminowe utrzymywanie oprogramowania.
W poniższym programie utworzymy ServeMux i użyjemy go do skojarzenia adresów URL z odpo-
wiednimi procedurami obsługi dla operacji /list i /price, które zostały rozdzielone na osobne
metody. Następnie użyjemy multipleksera ServeMux jako głównej procedury obsługi w wywołaniu
funkcji ListenAndServe.
code/r07/http3
func main() {
db := database{"buty": 50, "skarpety": 5}
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
mux.Handle("/price", http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
type database map[string]dollars
func (db database) list(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (db database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // błąd 404
fmt.Fprintf(w, "nie ma takiej pozycji: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
}
Skupmy się na dwóch wywołaniach mux.Handle, które rejestrują procedury obsługi. W pierwszym
db.list jest wartością metody (zob. podrozdział 6.4), czyli wartością typu
func(w http.ResponseWriter, req *http.Request)
który po wywołaniu wywołuje metodę database.list z wartością odbiornika db. Zatem db.list
jest funkcją, która implementuje zachowanie procedury obsługi, ale ponieważ nie ma żadnych
metod, nie spełnia warunków interfejsu http.Handler i nie może być przekazywana bezpośrednio
do mux.Handle.
Wyrażenie http.HandlerFunc(db.list) jest konwersją, a nie wywołaniem funkcji, ponieważ
http.HandlerFunc jest typem. Posiada następującą definicję:
net/http
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
7.7. INTERFEJS HTTP.HANDLER 195

HandlerFunc zawiera kilka niezwykłych cech mechanizmu interfejsu języka Go. Jest to typ funkcji,
który ma metody i spełnia warunki interfejsu http.Handler. Zachowanie jego metody ServeHTTP
polega na wywołaniu funkcji bazowej. Dlatego HandlerFunc jest adapterem, który pozwala wartości
funkcji spełnić warunki interfejsu, gdzie funkcja i jedyna metoda interfejsu mają tę samą sygnatu-
rę. W efekcie ta sztuczka pozwala pojedynczemu typowi, takiemu jak database, spełnić warunki
interfejsu http.Handler na kilka różnych sposobów: raz poprzez jego metodę list, raz poprzez jego
metodę price itd.
Ponieważ rejestrowanie procedury obsługi w ten sposób jest tak powszechne, ServeMux ma złożoną
metodę o nazwie HandleFunc, która robi to za nas, więc możemy uprościć kod rejestracji procedury
obsługi do tej postaci:
code/r07/http3a
mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)
Na podstawie powyższego kodu łatwo zrozumieć, w jaki sposób można by skonstruować program,
w którym istnieją dwa różne serwery WWW, nasłuchujące na różnych portach, definiujące różne ad-
resy URL i rozsyłające żądania do różnych procedur obsługi. Należałoby po prostu skonstruować
kolejny ServeMux i wykonać, być może równolegle, kolejne wywołanie funkcji ListenAndServe.
Jednak w większości programów jeden serwer WWW jest wystarczający. Ponadto typowe jest defi-
niowanie procedur obsługi HTTP w wielu plikach aplikacji i uciążliwe byłoby, gdyby wszystkie
one musiały zostać bezpośrednio zarejestrowane w instancji ServeMux tej aplikacji.
Tak więc dla wygody pakiet net/http zapewnia globalną instancję ServeMux o nazwie Default
ServeMux oraz funkcje poziomu pakietu o nazwach http.Handle i http.HandleFunc. Aby użyć
DefaultServeMux jako głównej procedury obsługi serwera, nie musimy przekazywać jej do funkcji
ListenAndServe — zrobi to nil.
Funkcję main serwera można wtedy uprościć do tej postaci:
code/r07/http4
func main() {
db := database{"buty": 50, "skarpety": 5}
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
Na koniec ważne przypomnienie. Jak wspomniano w podrozdziale 1.7, serwer WWW wywołuje
każdą procedurę obsługi w nowej funkcji goroutine. Dlatego procedury obsługi muszą podejmo-
wać środki ostrożności, takie jak blokowanie podczas uzyskiwania dostępu do zmiennych, do któ-
rych dostęp mogą uzyskiwać inne funkcje goroutine, w tym inne żądania do tej samej procedury
obsługi. O współbieżności porozmawiamy w następnych dwóch rozdziałach.
Ćwiczenie 7.11. Dodaj kolejne procedury obsługi, aby klienty mogły tworzyć, czytać, aktualizować
i usuwać wpisy bazy danych. Przykładowo: żądanie w postaci /update?item=skarpety&price=6
zaktualizuje cenę przedmiotu w inwentarzu i zgłosi błąd, jeśli element nie istnieje lub jeśli cena
jest nieprawidłowa. (Uwaga: ta zmiana wprowadza równoległe aktualizacje zmiennych).
Ćwiczenie 7.12. Zmień procedurę obsługi dla operacji /list, aby wyświetlała swoje dane wyjścio-
we w postaci tabeli HTML, a nie tekstu. Przydatny może być pakiet html/template (zob. podroz-
dział 4.6).
196 ROZDZIAŁ 7. INTERFEJSY

7.8. Interfejs error


Od początku tej książki używaliśmy wartości tajemniczego predeklarowanego typu error, nie wy-
jaśniając, czym on właściwie jest. W rzeczywistości to po prostu typ interfejsowy z pojedynczą meto-
dą, która zwraca komunikat o błędzie:
type error interface {
Error() string
}
Najprostszym sposobem utworzenia instancji error jest wywołanie funkcji errors.New, która
zwraca nowy error dla danego komunikatu o błędzie. Cały pakiet errors ma tylko cztery linie:
package errors

func New(text string) error { return &errorString{text} }

type errorString struct { text string }

func (e *errorString) Error() string { return e.text }


Typem bazowym errorString jest struktura, a nie łańcuch znaków, aby chronić jego reprezenta-
cję od przypadkowych (lub zamierzonych) aktualizacji. A powodem, dla którego to typ wskaźnika
*errorString, a nie sam errorString spełnia warunki interfejsu error, jest to, że każde wywołanie
New alokuje odrębną instancję error, która nie jest równa żadnej innej. Nie chcielibyśmy, aby
taki wyróżniający się błąd jak io.EOF był równy z innym, który akurat ma ten sam komunikat.
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"
Wywołania errors.New są stosunkowo rzadkie, ponieważ istnieje wygodna funkcja opakowująca,
fmt.Errorf, która wykonuje również formatowanie łańcucha znaków. Użyliśmy jej kilka razy
w rozdziale 5.
package fmt

import "errors"

func Errorf(format string, args ...interface{}) error {


return errors.New(Sprintf(format, args...))
}
Chociaż *errorString może być najprostszym typem interfejsu error, na pewno nie jest jedynym.
Pakiet syscall zapewnia np. interfejs API niskopoziomowych wywołań systemowych języka Go.
Na wielu platformach definiuje on typ liczbowy Errno spełniający warunki interfejsu error, a na
platformach uniksowych metoda Error typu Errno przeprowadza wyszukiwanie w tabeli łańcuchów
znaków, tak jak pokazano poniżej:
package syscall

type Errno uintptr // kody błędów systemu operacyjnego

var errors = [...]string{


1: "niedozwolona operacja", // EPERM
2: "nie ma takiego pliku lub katalogu", // ENOENT
3: "nie ma takiego procesu", // ESRCH
// …
}

func (e Errno) Error() string {


if 0 <= int(e) && int(e) < len(errors) {
7.9. PRZYKŁAD: EWALUATOR WYRAŻEŃ 197

return errors[e]
}
return fmt.Sprintf("errno %d", e)
}
Poniższa instrukcja tworzy wartość interfejsu przechowującą wartość 2 typu Errno, oznaczającą
stan ENOENT systemu POSIX:
var err error = syscall.Errno(2)
fmt.Println(err.Error()) // "nie ma takiego pliku lub katalogu"
fmt.Println(err) // "nie ma takiego pliku lub katalogu"
Wartość err została przedstawiona graficznie na rysunku 7.6.

Rysunek 7.6. Wartość interfejsu przechowująca wartość całkowitą typu syscall.Errno

Errno jest efektywną reprezentacją błędów wywołań systemowych zaczerpniętą ze skończonego


zbioru i spełnia warunki standardowego interfejsu error. Inne typy spełniające warunki tego inter-
fejsu zobaczymy w podrozdziale 7.11.

7.9. Przykład: ewaluator wyrażeń


W tym podrozdziale zbudujemy ewaluator dla prostych wyrażeń arytmetycznych. Użyjemy inter-
fejsu Expr do reprezentowania dowolnego wyrażenia w tym języku. Na razie ten interfejs nie po-
trzebuje żadnych metod, ale dodamy kilka później.
// Expr jest wyrażeniem arytmetycznym.
type Expr interface{}
Nasz język wyrażeń składa się z literałów zmiennoprzecinkowych, operatorów binarnych +, -, *
oraz /, operatorów jednoargumentowych -x i +x, wywołań funkcji pow(x,y), sin(x) i sqrt(x),
zmiennych takich jak x i pi oraz oczywiście nawiasów i standardowego pierwszeństwa operatorów.
Wszystkie wartości są typami float64. Oto kilka przykładowych wyrażeń:
sqrt(A / pi)
pow(x, 3) + pow(y, 3)
(F - 32) * 5 / 9
Pięć poniższych typów konkretnych reprezentuje poszczególne rodzaje wyrażeń. Typ Var reprezen-
tuje referencję do zmiennej. (Wkrótce zobaczymy, dlaczego jest wyeksportowany). Typ literal
reprezentuje stałą zmiennoprzecinkową. Typy unary i binary reprezentują wyrażenia operatorów
z jednym operandem lub dwoma, które mogą być dowolnym rodzajem Expr. Typ call reprezen-
tuje wywołanie funkcji. Ograniczymy jej pole fn do pow, sin lub sqrt.
code/r07/eval
// Typ Var identyfikuje zmienną, np. x.
type Var string
// Typ literal jest stałą liczbową, np. 3.141.
type literal float64
198 ROZDZIAŁ 7. INTERFEJSY

// Typ unary reprezentuje wyrażenia operatora jednoargumentowego, np. –x.


type unary struct {
op rune // możliwe wartości: '+', '–'
x Expr
}

// Typ binary reprezentuje wyrażenie operatora binarnego, np. x+y.


type binary struct {
op rune // możliwe wartości: '+', '–', '*', '/'
x, y Expr
}

// Typ call reprezentuje wyrażenie wywołania funkcji, np. sin(x).


type call struct {
fn string // możliwe wartości: "pow", "sin", "sqrt"
args []Expr
}
Aby dokonać ewaluacji wyrażenia zawierającego zmienne, potrzebujemy środowiska (ang.
environment), które mapuje nazwy zmiennych na wartości:
type Env map[Var]float64
Będziemy również potrzebować każdego rodzaju wyrażenia do zdefiniowania metody Eval, która
zwraca wartość wyrażenia w danym środowisku. Ponieważ każde wyrażenie musi zapewniać tę
metodę, dodamy ją do interfejsu Expr. Ten pakiet eksportuje tylko typy Expr, Env i Var. Klienty
mogą używać ewaluatora bez dostępu do pozostałych typów wyrażeń.
type Expr interface {
// Eval zwraca wartość tego wyrażenia Expr w środowisku env.
Eval(env Env) float64
}
Konkretne metody Eval przedstawiono poniżej. Metoda dla typu Var wykonuje przeszukiwanie
środowiska, co zwraca zero, jeśli zmienna nie jest zdefiniowana. Natomiast metoda dla typu literal
po prostu zwraca wartość literału.
func (v Var) Eval(env Env) float64 {
return env[v]
}

func (l literal) Eval(_ Env) float64 {


return float64(l)
}
Metody Eval dla typów unary i binary rekurencyjnie ewaluują swoje operandy, a następnie sto-
sują do nich operację op. Nie traktujemy dzielenia przez zero lub nieskończoność jako błędu, po-
nieważ te działania generują wynik, aczkolwiek nieokreślony. Wreszcie metoda dla typu call
ewaluuje argumenty dla funkcji pow, sin lub sqrt, a następnie wywołuje odpowiednią funkcję
z pakietu math.
func (u unary) Eval(env Env) float64 {
switch u.op {
case '+':
return +u.x.Eval(env)
case '-':
return -u.x.Eval(env)
}
panic(fmt.Sprintf("nieobsługiwany operator jednoargumentowy: %q", u.op))
}
7.9. PRZYKŁAD: EWALUATOR WYRAŻEŃ 199

func (b binary) Eval(env Env) float64 {


switch b.op {
case '+':
return b.x.Eval(env) + b.y.Eval(env)
case '-':
return b.x.Eval(env) - b.y.Eval(env)
case '*':
return b.x.Eval(env) * b.y.Eval(env)
case '/':
return b.x.Eval(env) / b.y.Eval(env)
}
panic(fmt.Sprintf("nieobsługiwany operator binarny: %q", b.op))
}

func (c call) Eval(env Env) float64 {


switch c.fn {
case "pow":
return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))
case "sin":
return math.Sin(c.args[0].Eval(env))
case "sqrt":
return math.Sqrt(c.args[0].Eval(env))
}
panic(fmt.Sprintf("nieobsługiwane wywołanie funkcji: %s", c.fn))
}
Wykonanie niektórych z tych metod może się nie powieść. Wyrażenie call może mieć np. nieznaną
funkcję lub niewłaściwą liczbę argumentów. Możliwe jest również skonstruowanie wyrażenia
unary lub binary z nieprawidłowym operatorem, takim jak ! lub < (chociaż wspomniana poniżej
funkcja Parse nigdy tego nie zrobi). Te błędy powodują, że Eval uruchamia procedurę panic.
Inne błędy, takie jak ewaluacja zmiennej Var nieobecnej w środowisku, powodują jedynie, że Eval
zwraca niewłaściwy wynik. Wszystkie te błędy mogą być wykrywane poprzez sprawdzenie wyra-
żenia Expr przed jego ewaluacją. To będzie zadaniem metody Check, którą pokażemy wkrótce, ale
najpierw przetestujmy metodę Eval.
Przedstawiona poniżej funkcja TestEval jest testem ewaluatora. Wykorzystuje pakiet testing,
który omówimy w rozdziale 11., ale na razie wystarczy wiedzieć, że wywołanie funkcji t.Errorf
zgłasza błąd. Funkcja wykonuje pętlę przez tablicę danych wejściowych, która definiuje trzy wy-
rażenia i inne środowisko dla każdego z nich. Pierwsze wyrażenie oblicza promień koła, mając
dane jego pole powierzchni A, drugie oblicza sumę sześcianów dwóch zmiennych x i y, a trzecie
przekształca temperaturę Fahrenheita F na stopnie Celsjusza.
func TestEval(t *testing.T) {
tests := []struct {
expr string
env Env
want string
}{
{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
{"5 / 9 * (F - 32)", Env{"F": 32}, "0"},
{"5 / 9 * (F - 32)", Env{"F": 212}, "100"},
}
var prevExpr string
for _, test := range tests {
200 ROZDZIAŁ 7. INTERFEJSY

// Wyświetla expr tylko wtedy, gdy się zmienia.


if test.expr != prevExpr {
fmt.Printf("\n%s\n", test.expr)
prevExpr = test.expr
}
expr, err := Parse(test.expr)
if err != nil {
t.Error(err) // parsuje błąd
continue
}
got := fmt.Sprintf("%.6g", expr.Eval(test.env))
fmt.Printf("\t%v => %s\n", test.env, got)
if got != test.want {
t.Errorf("%s.Eval() in %s = %q, want %q\n",
test.expr, test.env, got, test.want)
}
}
}
Dla każdego wpisu w tablicy ten test parsuje wyrażenie, ewaluuje je w danym środowisku i wyświetla
wynik. Nie mamy miejsca, aby pokazać tu funkcję Parse, ale znajdziesz ją, jeśli pobierzesz ten pakiet
za pomocą polecenia go get.
Polecenie go test (zob. podrozdział 11.1) uruchamia testy pakietu:
$ go test -v code/r07/eval
Flaga -v pozwala nam zobaczyć wyświetlone dane wyjściowe z testu, które są zazwyczaj ukryte dla
udanego testu, takiego jak ten. Oto dane wyjściowe z instrukcji fmt.Printf tego testu:
sqrt(A / pi)
map[A:87616 pi:3.141592653589793] => 167

pow(x, 3) + pow(y, 3)
map[x:12 y:1] => 1729
map[x:9 y:10] => 1729

5 / 9 * (F - 32)
map[F:-40] => -40
map[F:32] => 0
map[F:212] => 100
Na szczęście do tej pory wszystkie dane wejściowe były poprawne składniowo, ale nasze szczęście
może nie potrwać długo. Nawet w językach interpretowanych powszechne jest sprawdzanie skład-
ni pod kątem błędów statycznych, czyli takich, które można wykryć bez uruchamiania programu.
Dzięki oddzieleniu kontroli statycznych od dynamicznych możemy wykrywać błędy wcześniej
i wykonywać wiele kontroli tylko raz, a nie przy każdej ewaluacji wyrażenia.
Dodajmy do interfejsu Expr kolejną metodę. Metoda Check sprawdza błędy statyczne w drzewie
składniowym wyrażenia. Jej parametr vars omówimy za chwilę.
type Expr interface {
Eval(env Env) float64
// Check zgłasza błędy w tym wyrażeniu Expr i dodaje do zbioru swoje wartości Var.
Check(vars map[Var]bool) error
}
Konkretne metody Check przedstawiono poniżej. Ewaluacja typów literal i Var nie może się
nie powieść, więc metody Check dla tych typów zwracają nil. Metody dla typów unary i binary
najpierw sprawdzają, czy operator jest prawidłowy, a następnie rekurencyjnie sprawdzają operandy.
7.9. PRZYKŁAD: EWALUATOR WYRAŻEŃ 201

Podobnie metoda dla typu call — najpierw sprawdza, czy funkcja jest znana i ma właściwą
liczbę argumentów, a następnie rekurencyjnie sprawdza każdy argument.
func (v Var) Check(vars map[Var]bool) error {
vars[v] = true
return nil
}

func (literal) Check(vars map[Var]bool) error {


return nil
}

func (u unary) Check(vars map[Var]bool) error {


if !strings.ContainsRune("+-", u.op) {
return fmt.Errorf("nieoczekiwany operator jednoargumentowy %q", u.op)
}
return u.x.Check(vars)
}

func (b binary) Check(vars map[Var]bool) error {


if !strings.ContainsRune("+-*/", b.op) {
return fmt.Errorf("nieoczekiwany operator binarny %q", b.op)
}
if err := b.x.Check(vars); err != nil {
return err
}
return b.y.Check(vars)
}

func (c call) Check(vars map[Var]bool) error {


arity, ok := numParams[c.fn]
if !ok {
return fmt.Errorf("nieznana funkcja %q", c.fn)
}
if len(c.args) != arity {
return fmt.Errorf("wywołanie %s ma argumentów %d, wymaga %d",
c.fn, len(c.args), arity)
}
for _, arg := range c.args {
if err := arg.Check(vars); err != nil {
return err
}
}
return nil
}

var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}


Poniżej w dwóch grupach została przedstawiona lista wadliwych danych wejściowych i wywoły-
wanych przez nie błędów. Funkcja Parse (niepokazana) zgłasza błędy składniowe, a funkcja Check
zgłasza błędy semantyczne.
x % 2 nieoczekiwane '%'
math.Pi nieoczekiwane '.'
!true nieoczekiwane '!'
"hello" nieoczekiwane '"'

log(10) nieznana funkcja "log"


sqrt(1, 2) wywołanie sqrt ma argumentów 2, wymaga 1
202 ROZDZIAŁ 7. INTERFEJSY

Argument metody Check, czyli zbiór wartości Var, gromadzi zbiór nazw zmiennych znalezionych
w wyrażeniu. Aby ewaluacja się powiodła, każda z tych zmiennych musi być obecna w danym
środowisku. Ten zbiór jest logicznie wynikiem wywołania Check, ponieważ jednak ta metoda jest
rekurencyjna, wygodniej jest dla niej zapełniać zbiór przekazywany jako parametr. W początkowym
wywołaniu klient musi dostarczyć pusty zbiór.
W podrozdziale 3.2 rysowaliśmy wykres funkcji f(x,y), który był definiowany w czasie kompila-
cji. Ponieważ możemy teraz parsować, sprawdzać i ewaluować wyrażenia w łańcuchach znaków,
możemy zbudować aplikację internetową, która w trakcie działania otrzymuje od klienta wyraże-
nie i rysuje wykres powierzchniowy danej funkcji. Możemy użyć zbioru vars, żeby sprawdzić, czy
wyrażenie jest funkcją tylko dwóch zmiennych: x i y — w rzeczywistości trzech, ponieważ dla wy-
gody zapewnimy promień r. Użyjemy też metody Check do odrzucania niepoprawnych skła-
dniowo wyrażeń przed rozpoczęciem ewaluacji, aby nie powtarzać tych kontroli podczas
40 000 ewaluacji (100×100 komórek, każda z czterema rogami) poniższej funkcji.
Te etapy parsowania i sprawdzania łączy w sobie funkcja parseAndCheck:
code/r07/surface
import "code/r07/eval"

func parseAndCheck(s string) (eval.Expr, error) {


if s == "" {
return nil, fmt.Errorf("puste wyrażenie")
}
expr, err := eval.Parse(s)
if err != nil {
return nil, err
}
vars := make(map[eval.Var]bool)
if err := expr.Check(vars); err != nil {
return nil, err
}
for v := range vars {
if v != "x" && v != "y" && v != "r" {
return nil, fmt.Errorf("niezdefiniowana zmienna: %s", v)
}
}
return expr, nil
}
Aby ta aplikacja stała się aplikacją internetową, potrzebujemy tylko poniższej funkcji plot, która
ma znajomą sygnaturę funkcji http.HandlerFunc.
func plot(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
expr, err := parseAndCheck(r.Form.Get("expr"))
if err != nil {
http.Error(w, "nieprawidłowe wyrażenie: "+err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "image/svg+xml")
surface(w, func(x, y float64) float64 {
r := math.Hypot(x, y) // odległość od punktu (0,0)
return expr.Eval(eval.Env{"x": x, "y": y, "r": r})
})
}
7.10. ASERCJE TYPÓW 203

Funkcja plot parsuje i sprawdza wyrażenie określone w żądaniu HTTP, i używa go do utworzenia
anonimowej funkcji o dwóch zmiennych. Ta anonimowa funkcja ma taką samą sygnaturę jak
sztywno ustalona funkcja f z oryginalnego programu drukowania wykresu powierzchniowego,
ale ewaluuje wyrażenie dostarczane przez użytkownika. Środowisko definiuje x, y oraz promień r.
Na koniec funkcja plot wywołuje funkcję surface, która jest po prostu funkcją main z programu
code/r03/surface, zmodyfikowaną, by przyjmować jako parametry funkcję drukowania wykresu
oraz dane wyjściowe z io.Writer, zamiast używać sztywno ustalonej funkcji f i os.Stdout. Na
rysunku 7.7 przedstawiono trzy wykresy powierzchniowe wygenerowane przez ten program.
Ćwiczenie 7.13. Dodaj do typu Expr metodę String, aby w ładny sposób formatować drzewo
składniowe. Sprawdź, czy wyniki po ponownym parsowaniu dają równoważne drzewo.
Ćwiczenie 7.14. Zdefiniuj nowy typ konkretny, który spełnia warunki interfejsu Expr i zapewnia
nową operację, taką jak obliczanie minimalnej wartości swoich operandów. Ponieważ funkcja
Parse nie tworzy instancji tego nowego typu, aby go użyć, trzeba będzie zbudować drzewo skła-
dniowe bezpośrednio (lub rozszerzyć parser).
Ćwiczenie 7.15. Napisz program, który odczytuje pojedyncze wyrażenie ze standardowego strumie-
nia wejściowego, prosi użytkownika o podanie wartości dla dowolnych zmiennych, a następnie
ewaluuje to wyrażenie w powstałym środowisku. Obsłuż elegancko wszystkie błędy.
Ćwiczenie 7.16. Napisz internetową aplikację kalkulatora.

7.10. Asercje typów


Asercja typu (ang. type assertion) jest operacją stosowaną do wartości interfejsu. Składniowo wyglą-
da to jak x.(T), gdzie x jest wyrażeniem typu interfejsu, a T jest typem (zwanym typem zakłada-
nym). Asercja typu sprawdza, czy dynamiczny typ jej operandu jest zgodny z typem zakładanym.
Istnieją dwie możliwości. Po pierwsze, jeśli zakładany typ T jest typem konkretnym, asercja typu
sprawdza, czy dynamiczny typ wyrażenia x jest identyczny z T. Jeżeli to sprawdzenie zakończy
się pomyślnie, wynikiem asercji typu jest dynamiczna wartość wyrażenia x, której typem jest
oczywiście T. Innymi słowy: asercja typu do typu konkretnego wyodrębnia konkretną wartość ze
swojego operandu. Jeśli sprawdzenie się nie powiedzie, operacja uruchamia procedurę panic.
Oto przykład:
var w io.Writer
w = os.Stdout
f := w.(*os.File) // powodzenie: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interfejs przechowuje typ *os.File, a nie *bytes.Buffer
W drugim przypadku, jeśli zakładanym typem T jest typ interfejsowy, asercja typu sprawdza,
czy dynamiczny typ wyrażenia x spełnia warunki typu T. Jeżeli kontrola zakończy się powodzeniem,
wartość dynamiczna nie jest wyodrębniana. Wynikiem jest nadal wartość interfejsu z tymi samymi
komponentami typu i wartości, ale wynik posiada typ interfejsowy T. Innymi słowy: asercja typu
do typu interfejsowego zmienia typ wyrażenia, udostępniając inny (i zwykle większy) zestaw metod,
ale zachowuje wewnątrz wartości interfejsu komponenty, którymi są dynamiczny typ i dynamiczna
wartość.
204 ROZDZIAŁ 7. INTERFEJSY

Rysunek 7.7. Wykresy powierzchniowe trzech funkcji: (a) sin(–x)*pow(1.5,–r),


(b) pow(2,sin(y))*pow(2,sin(x))/12, (c) sin(x*y/10)/10
7.11. ROZRÓŻNIANIE BŁĘDÓW ZA POMOCĄ ASERCJI TYPÓW 205

Po pierwszej, poniższej asercji typu zarówno w, jak i rw przechowują os.Stdout, więc każda ma typ
dynamiczny *os.File, ale zmienna w typu *io.Writer udostępnia tylko metodę Write danego
pliku, podczas gdy rw udostępnia również jego metodę Read.
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // powodzenie: *os.File ma obie metody: Read i Write

w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter nie ma metody Read
Bez względu na to, jaki był typ zakładany, asercja typu nie powiedzie się, jeśli operandem jest war-
tość nil interfejsu. Asercja typu do mniej restrykcyjnego typu interfejsowego (takiego z mniejszą licz-
bą metod) jest rzadko potrzebna, ponieważ zachowuje się jak przypisanie, z wyjątkiem przypadku nil.
w = rw // io.ReadWriter jest przypisywalny do io.Writer
w = rw.(io.Writer) // nie powiedzie się tylko, jeśli rw == nil
Często nie jesteśmy pewni dynamicznego typu wartości interfejsu i chcielibyśmy sprawdzić, czy to
jest jakiś szczególny typ. Jeśli asercja typu pojawia się w przypisaniu, w którym oczekiwane są
dwa wyniki (tak jak w poniższych deklaracjach), operacja nie wywołuje paniki w przypadku nie-
powodzenia, ale zamiast tego zwraca dodatkowy drugi wynik, czyli wartość logiczną wskazującą
powodzenie:
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // powodzenie: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // niepowodzenie: !ok, b == nil
Ten drugi wynik jest tradycyjnie przypisywany do zmiennej o nazwie ok. Jeśli operacja się nie po-
wiedzie, ok jest fałszem, a pierwszy wynik jest równy wartości zerowej zakładanego typu, którą
w tym przykładzie jest *bytes.Buffer z wartością nil.
Wynik jest często od razu wykorzystywany do zdecydowania, co robić dalej. Rozszerzona forma
instrukcji if pozwala zapisać to dość zwięźle:
if f, ok := w.(*os.File); ok {
// …użycie f…
}
Jeśli operand asercji typu jest zmienną, to zamiast wymyślonej kolejnej nazwy dla nowej zmiennej
lokalnej można czasem zobaczyć ponownie wykorzystaną pierwotną nazwę przesłaniającą
oryginał, np.:
if w, ok := w.(*os.File); ok {
// …użycie w…
}

7.11. Rozróżnianie błędów za pomocą asercji typów


Rozważmy zestaw błędów zwracanych przez operacje plików w pakiecie os. Operacje we-wy mogą
się nie powieść z wielu różnych powodów, ale trzy rodzaje awarii często muszą być obsługiwane od-
miennie: plik już istnieje (dla operacji tworzenia), nie znaleziono pliku (dla operacji odczytu) oraz
odmowa dostępu. Pakiet os zapewnia te trzy funkcje pomocnicze do klasyfikowania błędu sygnali-
zowanego przez daną wartość error:
206 ROZDZIAŁ 7. INTERFEJSY

package os

func IsExist(err error) bool


func IsNotExist(err error) bool
func IsPermission(err error) bool
Naiwna implementacja jednego z tych predykatów może sprawdzać, czy komunikat o błędzie zawiera
określony podłańcuch znaków:
func IsNotExist(err error) bool {
// UWAGA: to nie jest solidne rozwiązanie!
return strings.Contains(err.Error(), "plik nie istnieje")
}
Ponieważ jednak logika wykorzystywana do obsługi błędów we-wy może się różnić w zależności
od platformy, podejście to nie jest solidne i ta sama awaria może być raportowana za pomocą
wielu różnych komunikatów błędów. Sprawdzanie podłańcuchów komunikatów o błędach może
być przydatne podczas testowania, które ma na celu upewnienie się, że funkcje zawodzą w oczeki-
wany sposób, ale jest nieodpowiednie dla kodu działającego w środowisku produkcyjnym.
Bardziej niezawodnym podejściem jest reprezentowanie ustrukturyzowanych wartości błędów
za pomocą dedykowanego typu. Pakiet os definiuje typ o nazwie PathError, służący do opisywa-
nia awarii z udziałem operacji na ścieżce pliku, takich jak Open lub Delete. Definiuje też wariant
o nazwie LinkError, opisujący awarie operacji z udziałem dwóch ścieżek plików, takie jak Symlink
i Rename. Oto typ os.PathError:
package os

// Typ PathError rejestruje błąd oraz operację i ścieżkę pliku, które go wywołały.
type PathError struct {
Op string
Path string
Err error
}

func (e *PathError) Error() string {


return e.Op + " " + e.Path + ": " + e.Err.Error()
}
Większość klientów jest nieświadoma typu PathError i radzi sobie ze wszystkimi błędami w jedno-
lity sposób, wywołując swoje metody Error. Chociaż metoda Error typu PathError formuje ko-
munikat, po prostu konkatenując pola, struktura PathError zachowuje bazowe komponenty błę-
du. Klienty wymagające odróżniania jednego rodzaju awarii od innego mogą użyć asercji typu do
wykrywania określonego rodzaju błędu. Taki określony rodzaj błędu zapewnia więcej szczegółów
niż prosty łańcuch znaków.
_, err := os.Open("/plik/nie/istnieje")
fmt.Println(err) // "open /plik/nie/istnieje: nie ma takiego pliku lub katalogu"
fmt.Printf("%#v\n", err)
// Output:
// &os.PathError{Op:"open", Path:"/plik/nie/istnieje", Err:0x2}
Oto, jak działają te trzy funkcje pomocnicze. Przykładowo: pokazana poniżej funkcja IsNotExist ra-
portuje, czy błąd jest równy syscall.ENOENT (zob. podrozdział 7.8), czy szczególnemu błędowi
os.ErrNotExist (zob. io.EOF w punkcie 5.4.2), albo czy jest typem *PathError, którego błędem
jest jeden z tych dwóch.
7.12. KWERENDOWANIE ZACHOWAŃ ZA POMOCĄ INTERFEJSOWYCH ASERCJI TYPÓW 207

import (
"errors"
"syscall"
)

var ErrNotExist = errors.New("plik nie istnieje")

// IsNotExist zwraca wartość logiczną wskazującą, czy błąd jest znany,


// aby zgłosić, że plik lub katalog nie istnieją. Jej warunki są spełniane
// przez ErrNotExist oraz przez niektóre błędy wywołań systemowych.
func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
A tutaj ta funkcja w akcji:
_, err := os.Open("/plik/nie/istnieje")
fmt.Println(os.IsNotExist(err)) // "true"
Oczywiście struktura PathError zostaje utracona, jeśli komunikat o błędzie zostanie połączony
w większy łańcuch znaków, np. poprzez wywołanie funkcji fmt.Errorf. Rozróżnianie błędów
musi być przeprowadzane natychmiast po nieudanej operacji, zanim błąd zostanie propagowany
do podmiotu wywołującego.

7.12. Kwerendowanie zachowań


za pomocą interfejsowych asercji typów
Poniższa logika jest podobna do części serwera WWW net/http odpowiedzialnego za wypisy-
wanie pól nagłówka HTTP takich jak "Content-type: text/html". Zmienna w typu io.Writer
reprezentuje odpowiedź HTTP. Zapisywane w niej bajty są ostatecznie wysłane do czyjejś przeglą-
darki internetowej.
func writeHeader(w io.Writer, contentType string) error {
if _, err := w.Write([]byte("Content-Type: ")); err != nil {
return err
}
if _, err := w.Write([]byte(contentType)); err != nil {
return err
}
// …
}
Ponieważ metoda Write wymaga wycinka bajtów, a wartość, którą chcemy zapisać, to łańcuch zna-
ków, wymagana jest konwersja []byte(...). Ta konwersja alokuje pamięć i tworzy kopię, ale kopia
jest wyrzucana niemal natychmiast po utworzeniu. Udajmy, że jest to główny element serwera
WWW, a nasze profilowanie wykazało, że ta alokacja pamięci go spowalnia. Czy możemy w tym
przypadku uniknąć alokowania pamięci?
Interfejs io.Writer wskazuje nam tylko jeden fakt na temat typu konkretnego przechowywanego
przez zmienną w: można zapisywać w nim bajty. Jeśli zajrzymy za kulisy pakietu net/http, zobaczy-
my, że dynamiczny typ przechowywany w tym programie przez w ma również metodę WriteString,
która umożliwia efektywne zapisywanie w nim łańcuchów znaków, co pozwala uniknąć konieczności
alokowania tymczasowej kopii. (Może to wyglądać na strzał w ciemno, ale wiele ważnych typów
208 ROZDZIAŁ 7. INTERFEJSY

spełniających warunki interfejsu io.Writer również ma metodę WriteString, a należą do nich


m.in.: *bytes.Buffer, *os.File i *bufio.Writer).
Nie możemy zakładać, że dowolna zmienna w typu io.Writer ma również metodę WriteString.
Ale możemy zdefiniować nową instancję, która ma właśnie tę metodę, i użyć asercji typu w celu
sprawdzenia, czy dynamiczny typ zmiennej w spełnia warunki tego nowego interfejsu.
// writeString zapisuje s w zmiennej w.
// Jeśli w ma metodę WriteString, jest ona wywoływana zamiast w.Write.
func writeString(w io.Writer, s string) (n int, err error) {
type stringWriter interface {
WriteString(string) (n int, err error)
}
if sw, ok := w.(stringWriter); ok {
return sw.WriteString(s) // unikanie tworzenia kopii
}
return w.Write([]byte(s)) // alokowanie tymczasowej kopii
}

func writeHeader(w io.Writer, contentType string) error {


if _, err := writeString(w, "Content-Type: "); err != nil {
return err
}
if _, err := writeString(w, contentType); err != nil {
return err
}
// …
}
Aby uniknąć powtarzania się, przenieśliśmy to sprawdzanie do funkcji narzędziowej writeString,
ale jest ona tak bardzo przydatna, że standardowa biblioteka zapewnia ją jako io.WriteString.
Jest to rekomendowany sposób zapisywania łańcucha znaków do io.Writer.
W tym przypadku ciekawe jest to, że nie ma standardowego interfejsu, który definiowałby metodę
WriteString i określał jej wymagane zachowanie. Ponadto kwestia spełniania przez konkretny typ
warunków interfejsu stringWriter zależy wyłącznie od jego metod, a nie od jakiejkolwiek relacji
między nim a typem interfejsowym. Oznacza to, że zastosowana w powyższym przykładzie technika
opiera się na założeniu, że jeśli typ spełnia warunki poniższego interfejsu, wtedy WriteString(s)
musi mieć taki sam efekt co Write([]byte(s)).
interface {
io.Writer
WriteString(s string) (n int, err error)
}
Chociaż io.WriteString dokumentuje swoje założenie, prawdopodobnie niewiele wywołujących
ją funkcji dokumentuje, że również przyjmuje to samo założenie. Definiowanie metody określone-
go typu jest przyjmowane jako dorozumiane wyrażenie zgody na konkretny kontrakt behawioralny.
Początkujący programiści języka Go, zwłaszcza ci z doświadczeniem w silnie typowanych językach,
mogą uznać ten brak wyraźnej intencji za niepokojący, ale nieczęsto jest to problemem w praktyce.
Z wyjątkiem pustego interfejsu interface{}, warunki typów interfejsowych są rzadko spełniane
w wyniku przypadkowego zbiegu okoliczności.
Powyższa funkcja writeString używa asercji typu do sprawdzenia, czy wartość ogólnego typu
interfejsowego spełnia również warunki bardziej szczegółowego typu interfejsowego, i jeśli tak jest,
używa zachowań tego bardziej szczegółowego interfejsu. Z tej techniki można zrobić dobry użytek
7.13. PRZEŁĄCZNIKI TYPÓW 209

bez względu na to, czy kwerendowany interfejs jest standardowy, jak io.ReadWriter, czy zdefinio-
wany przez użytkownika, jak stringWriter.
Chodzi również o to, w jaki sposób funkcja fmt.Fprintf odróżnia wartości spełniające warunki in-
terfejsu error lub fmt.Stringer od wszystkich pozostałych wartości. W ramach funkcji fmt.Fprintf
wykonywana jest czynność konwertująca pojedynczy operand na łańcuch znaków, która wygląda
mniej więcej tak:
package fmt

func formatOneValue(x interface{}) string {


if err, ok := x.(error); ok {
return err.Error()
}
if str, ok := x.(Stringer); ok {
return str.String()
}
// …wszystkie pozostałe typy…
}
Jeśli x spełnia warunki jednego z tych dwóch interfejsów, określa to sposób formatowania wartości.
Jeśli nie, domyślny przypadek obsługuje wszystkie pozostałe typy mniej lub bardziej jednolicie za
pomocą refleksji. Zobaczymy, jak to działa, w rozdziale 12.
Ponownie przyjęte jest założenie, że każdy typ z metodą String spełnia warunek behawioralnego
kontraktu interfejsu fmt.Stringer, jakim jest zwracanie łańcucha znaków odpowiedniego do
wyświetlania.

7.13. Przełączniki typów


Interfejsy są używane w dwóch różnych stylach. W pierwszym stylu, którego przykładami są:
io.Reader, io.Writer, fmt.Stringer, sort.Interface, http.Handler i error, metody interfejsów
wyrażają podobieństwa typów konkretnych, spełniających warunki danego interfejsu, ale ukrywają
szczegóły reprezentacji i wewnętrzne operacje tych typów konkretnych. Nacisk kładzie się na metody,
a nie na typy konkretne.
Drugi styl wykorzystuje zdolność wartości interfejsu do przechowywania wartości różnych typów
konkretnych i traktuje interfejs jako unię tych typów. Asercje typów są wykorzystywane do rozróż-
niania tych typów dynamicznie i traktowania każdego przypadku odmiennie. W tym stylu nacisk
kładzie się na typy konkretne, które spełniają warunki danego interfejsu, a nie na metody tego in-
terfejsu (jeśli w rzeczywistości w ogóle ma jakieś), i nie ma ukrywania informacji. Używane w ten
sposób interfejsy opiszemy jako unie rozróżnialne (ang. discriminated unions).
Jeśli jesteś zaznajomiony z programowaniem obiektowym, możesz rozpoznać te dwa style jako po-
limorfizm podtypowy i polimorfizm ad hoc, ale nie musisz zapamiętywać tych pojęć. W pozostałej
części tego rozdziału będziemy prezentować przykłady drugiego stylu.
Interfejs API języka Go do kwerendowania bazy danych SQL, tak jak interfejsy innych języków,
pozwala precyzyjnie oddzielić stałą część zapytania od części zmiennych. Przykładowy klient może
wyglądać tak:
import "database/sql"

func listTracks(db sql.DB, artist string, minYear, maxYear int) {


result, err := db.Exec(
210 ROZDZIAŁ 7. INTERFEJSY

"SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?",
artist, minYear, maxYear)
// …
}
Metoda Exec zastępuje w łańcuchu zapytania każdy znak '?' literałem SQL oznaczającym odpo-
wiednią wartość argumentu, która może być wartością logiczną, liczbą, łańcuchem znaków lub
wartością nil. Konstruowanie zapytań w ten sposób pomaga uniknąć ataków wstrzykiwania SQL,
w których atakujący przejmuje kontrolę nad zapytaniem, wykorzystując niewłaściwe cytowanie
danych wejściowych. W ramach metody Exec moglibyśmy znaleźć funkcję taką jak poniższa, która
konwertuje każdą wartość argumentu na notację w postaci literału SQL.
func sqlQuote(x interface{}) string {
if x == nil {
return "NULL"
} else if _, ok := x.(int); ok {
return fmt.Sprintf("%d", x)
} else if _, ok := x.(uint); ok {
return fmt.Sprintf("%d", x)
} else if b, ok := x.(bool); ok {
if b {
return "TRUE"
}
return "FALSE"
} else if s, ok := x.(string); ok {
return sqlQuoteString(s) // (niepokazane)
} else {
panic(fmt.Sprintf("nieoczekiwany typ %T: %v", x, x))
}
}
Instrukcja przełącznika (switch) upraszcza łańcuch if-else, który wykonuje serię testów porównań
wartości. Analogiczna instrukcja przełącznika typów upraszcza łańcuch if-else asercji typów.
W najprostszej formie przełącznik typów wygląda jak zwykła instrukcja switch, w której operandem
jest x.(type) — jest to dosłownie słowo kluczowe type — a każdy przypadek (case) ma jeden
typ lub kilka typów. Przełącznik typów umożliwia tworzenie wielokrotnego wyboru na podsta-
wie dynamicznego typu wartości interfejsu. Przypadek nil zostaje dopasowany, jeśli x == nil,
a przypadek default zostaje dopasowany, jeśli nie pasuje żaden inny. Przełącznik typów dla
funkcji sqlQuote miałby następujące przypadki:
switch x.(type) {
case nil: // …
case int, uint: // …
case bool: // …
case string: // …
default: // …
}
Podobnie jak w zwykłej instrukcji przełącznika (zob. podrozdział 1.8), przypadki są rozpatrywane
w kolejności, a gdy zostaje znalezione dopasowanie, wykonywane jest ciało danego przypadku. Po-
rządek przypadków staje się istotny, gdy jeden typ przypadku lub kilka typów przypadku to interfej-
sy, ponieważ wtedy istnieje możliwość dopasowania dwóch przypadków. Pozycja przypadku
default w stosunku do pozostałych jest nieistotna. Nie jest dozwolone wykonywanie wszystkich
przypadków po kolei (fallthrough).
7.14. PRZYKŁAD: DEKODOWANIE XML OPARTE NA TOKENACH 211

Należy zwrócić uwagę, że w pierwotnej funkcji logika dla przypadków bool i string wymaga
dostępu do wartości wyodrębnianej przez asercję typu. Ponieważ jest to typowe, instrukcja przełą-
czania typów ma rozszerzoną formę, która wiąże wyodrębnioną wartość z nową zmienną w obrębie
każdego przypadku:
switch x := x.(type) { /* ... */ }
Tutaj również nazwaliśmy nowe zmienne x. Podobnie jak w przypadku asercji typów, ponowne
wykorzystywanie nazw zmiennych jest powszechne. Tak jak instrukcja switch, przełącznik typów
domyślnie tworzy blok leksykalny, więc deklaracja nowej zmiennej o nazwie x nie koliduje ze
zmienną x w bloku zewnętrznym. Każda instrukcja case również domyślnie tworzy osobny blok
leksykalny.
Przepisanie funkcji sqlQuote tak, aby wykorzystywała rozszerzoną formę przełącznika typów,
sprawia, że staje się ona znacznie jaśniejsza:
func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x) // x ma tutaj typ interface{}
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuoteString(x) // (niepokazane)
default:
panic(fmt.Sprintf("nieoczekiwany typ %T: %v", x, x))
}
}
W tej wersji w obrębie bloku każdego przypadku dla pojedynczego typu zmienna x ma ten sam
typ co przypadek. Zmienna x ma np. typ bool w obrębie przypadku bool, a typ string w obrębie
przypadku string. We wszystkich pozostałych przypadkach x ma (interfejsowy) typ operandu
instrukcji switch, którym w tym przykładzie jest interface{}. Gdy ta sama akcja jest wymagana
dla kilku przypadków, tak jak dla int i uint, przełącznik typów ułatwia ich połączenie.
Chociaż sqlQuote przyjmuje argument dowolnego typu, funkcja ta może być wykonana do końca
tylko wtedy, kiedy typ argumentu odpowiada jednemu z przypadków umieszczonych w przełącz-
niku typów. W przeciwnym razie uruchamiana jest procedura panic z komunikatem „nieoczeki-
wany typ”. Chociaż typem zmiennej x jest interface{}, traktujemy ją jako unię rozróżnialną typów
int, uint, bool, string i nil.

7.14. Przykład: dekodowanie XML oparte na tokenach


W podrozdziale 4.5 pokazaliśmy, jak dekodować dokumenty JSON na struktury danych języka
Go za pomocą funkcji Marshal i Unmarshal z pakietu encoding/json. Pakiet encoding/xml zapew-
nia podobny interfejs API. To podejście jest wygodne, gdy chcemy zbudować reprezentację drze-
wa dokumentu, ale w wielu programach jest ono zbędne. Ten pakiet zapewnia również interfejs
API niskiego poziomu oparty na tokenach, przeznaczony do dekodowania dokumentów XML.
212 ROZDZIAŁ 7. INTERFEJSY

W stylu opartym na tokenach parser konsumuje dane wejściowe i wytwarza strumień tokenów,
głównie czterech rodzajów (StartElement, EndElement, CharData i Comment), z których każdy jest ty-
pem konkretnym w pakiecie encoding/xml. Każde wywołanie (*xml.Decoder).Token zwraca token.
Istotne części tego interfejsu API zostały pokazane poniżej:
encoding/xml
package xml
type Name struct {
Local string // np. "Tytuł" lub "id"
}
type Attr struct { // np. name="wartość"
Name Name
Value string
}
// Typ Token obejmuje typy: StartElement, EndElement, CharData
// i Comment oraz kilka innych osobliwych typów (niepokazanych).
type Token interface{}
type StartElement struct { // np. <name>
Name Name
Attr []Attr
}
type EndElement struct { Name Name } // np. </name>
type CharData []byte // np. <p>CharData</p>
type Comment []byte // np. <!-- Comment -->
type Decoder struct{ /* ... */ }
func NewDecoder(io.Reader) *Decoder
func (*Decoder) Token() (Token, error) // zwraca następny Token w sekwencji
Interfejs Token, który nie ma metod, jest również przykładem unii rozróżnialnej. Celem tradycyjnego
interfejsu, takiego jak io.Reader, jest ukrycie szczegółów dotyczących typów konkretnych speł-
niających jego warunki, aby można było tworzyć nowe implementacje. Każdy typ konkretny jest
traktowany jednakowo. Natomiast zestaw typów konkretnych spełniających warunki unii rozróż-
nialnej jest z założenia ustalony i jest udostępniany, a nie ukryty. Typy unii rozróżnialnej mają
niewiele metod. Operujące na nich funkcje są za pomocą przełącznika typów wyrażane jako zestaw
przypadków, z różną logiką w każdym przypadku.
Poniższy program xmlselect wyodrębnia i wyświetla tekst znajdujący się pomiędzy określonymi
elementami w drzewie dokumentu XML. Jeśli skorzystamy z powyższego interfejsu API, ten
program może wykonać swoje zadanie w pojedynczym przejściu przez dane wejściowe w ogóle bez
konieczności materializowania drzewa.
code/r07/xmlselect
// Xmlselect wyświetla tekst wybranych elementów dokumentu XML.
package main
import (
"encoding/xml"
"fmt"
"io"
"os"
"strings"
)
7.14. PRZYKŁAD: DEKODOWANIE XML OPARTE NA TOKENACH 213

func main() {
dec := xml.NewDecoder(os.Stdin)
var stack []string // stos nazw elementów
for {
tok, err := dec.Token()
if err == io.EOF {
break
} else if err != nil {
fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err)
os.Exit(1)
}
switch tok := tok.(type) {
case xml.StartElement:
stack = append(stack, tok.Name.Local) // umieszczenie na stosie
case xml.EndElement:
stack = stack[:len(stack)-1] // zdjęcie ze stosu
case xml.CharData:
if containsAll(stack, os.Args[1:]) {
fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok)
}
}
}
}

// containsAll raportuje, czy x zawiera elementy y w kolejności.


func containsAll(x, y []string) bool {
for len(y) <= len(x) {
if len(y) == 0 {
return true
}
if x[0] == y[0] {
y = y[1:]
}
x = x[1:]
}
return false
}
Za każdym razem, gdy pętla w funkcji main napotka token StartElement, umieszcza nazwę danego
elementu na stosie, a dla każdego tokena EndElement zdejmuje nazwę ze stosu. Interfejs API gwa-
rantuje, że kolejność tokenów StartElement i EndElement zostanie prawidłowo dopasowana nawet
w niepoprawnym składniowo dokumencie XML. Tokeny Comment są ignorowane. Gdy program
xmlselect napotyka token CharData, wyświetla tekst tylko wtedy, gdy stos zawiera w kolejności wszyst-
kie elementy nazwane przez argumenty wiersza poleceń.
Poniższe polecenie wyświetla teksty wszystkich elementów h2 pojawiających się pod dwoma pozio-
mami elementów div. Jego dane wejściowe to specyfikacja XML, która sama jest dokumentem XML.
$ go build code/r01/fetch
$ ./fetch http://www.w3.org/TR/2006/REC-xml11-20060816 | ./xmlselect div div h2
html body div div h2: 1 Introduction
html body div div h2: 2 Documents
html body div div h2: 3 Logical Structures
html body div div h2: 4 Physical Structures
html body div div h2: 5 Conformance
html body div div h2: 6 Notation
html body div div h2: A References
html body div div h2: B Definitions for Character Normalization
...
214 ROZDZIAŁ 7. INTERFEJSY

Ćwiczenie 7.17. Rozszerz program xmlselect w taki sposób, żeby elementy można było wybierać
nie tylko według nazwy, ale również według ich atrybutów, tak jak w CSS, aby np. element <div
id="Page" class="wide"> można było wybrać poprzez dopasowanie zarówno atrybutu id lub
class, jak i jego nazwy.
Ćwiczenie 7.18. Używając interfejsu dekodera API opartego na tokenach, napisz program, który
odczyta dowolny dokument XML i zbuduje reprezentujące go drzewo węzłów generycznych. Istnieją
dwa rodzaje węzłów: węzły CharData reprezentują tekstowe łańcuchy znaków, a węzły Element repre-
zentują nazwane elementy i ich atrybuty. Każdy węzeł Element ma wycinek węzłów potomnych.
Pomocna może się okazać następująca deklaracja.
import "encoding/xml"

type Node interface{} // CharData lub *Element

type CharData string

type Element struct {


Type xml.Name
Attr []xml.Attr
Children []Node
}

7.15. Kilka porad


Przy projektowaniu nowego pakietu początkujący programiści Go często zaczynają od utworze-
nia zestawu interfejsów, a dopiero później definiują typy konkretne, które spełniają ich warunki.
Takie podejście prowadzi do powstawania wielu interfejsów, z których każdy ma tylko jedną
implementację. Nie rób tak. Takie interfejsy są niepotrzebnymi abstrakcjami. Mają też swoje
koszty w czasie wykonywania programu. Wykorzystując mechanizm eksportu (zob. podrozdział
6.6), można ograniczyć, które metody typu lub pola struktury są widoczne na zewnątrz pakietu. In-
terfejsy są potrzebne tylko wtedy, gdy istnieją dwa konkretne typy (lub więcej), które muszą być
obsługiwane w jednolity sposób.
Robimy wyjątek od tej reguły, gdy warunki interfejsu są spełniane przez pojedynczy typ konkretny,
ale ten typ nie może istnieć w tym samym pakiecie co interfejs z powodu swoich zależności.
W takim przypadku interfejs jest dobrym sposobem na oddzielenie dwóch pakietów.
Ponieważ interfejsy są używane w języku Go tylko wtedy, gdy ich warunki są spełniane przez dwa ty-
py lub większą liczbę typów, z konieczności abstrahują od szczegółów jakiejkolwiek konkretnej
implementacji. Rezultatem są mniejsze interfejsy z mniejszą liczbą prostszych metod, a często tylko
z jedną, tak jak w przypadku io.Writer lub fmt.Stringer. Niewielkie interfejsy pozwalają łatwiej
spełnić ich warunki, gdy pojawiają się nowe typy. Dobrą zasadą praktyczną przy projektowaniu
interfejsów jest prosić tylko o to, czego się potrzebuje.
Na tym kończy się nasz przewodnik po metodach i interfejsach. Język Go zapewnia doskonałe
wsparcie dla obiektowego stylu programowania, ale nie oznacza to, że należy używać wyłącznie
tego stylu. Nie wszystko musi być obiektem. Samodzielne funkcje mają swoje miejsce, podobnie
jak niezhermetyzowane typy danych. Należy zwrócić uwagę, że przykłady z pierwszych pięciu roz-
działów tej książki wywołują nie więcej niż dwa tuziny metod, takich jak np. input.Scan, w przeci-
wieństwie do wywołań zwykłych funkcji, takich jak np. fmt.Printf.
Rozdział 8

Funkcje goroutine i kanały

Programowanie współbieżne, które polega na wyrażaniu programu jako kompozycji kilku auto-
nomicznych aktywności, nigdy nie było ważniejsze niż obecnie. Serwery WWW obsługują żądania
tysięcy klientów na raz. Aplikacje na tablety i telefony renderują animacje w interfejsie użyt-
kownika, jednocześnie wykonując obliczenia i wysyłając żądania sieciowe w tle. Nawet w przypad-
ku tradycyjnych problemów przetwarzania wsadowego, takich jak odczytywanie danych, wyko-
nywanie obliczeń i zapisywanie danych wyjściowych, używana jest współbieżność w celu ukrycia
opóźnień operacji we-wy oraz wykorzystania wielu procesorów nowoczesnego komputera, które
z roku na rok stają się coraz liczniejsze, ale nie przyspieszają.
Język Go umożliwia stosowanie dwóch stylów programowania współbieżnego. Ten rozdział
przedstawia funkcje goroutine i kanały, które obsługują CSP (ang. communicating sequential processes),
czyli model współbieżności, w którym wartości są przekazywane między niezależnymi aktywno-
ściami (funkcjami goroutine), ale zmienne są w znacznej części ograniczone do pojedynczej aktyw-
ności. Rozdział 9. opisuje niektóre aspekty bardziej tradycyjnego modelu wielowątkowości pamięci
współdzielonej (ang. shared memory multithreading), które będą znajome, jeśli używałeś wątków
w innych głównych językach programowania. Rozdział 9. zwraca również uwagę na kilka istotnych
zagrożeń i pułapek programowania współbieżnego, w które nie będziemy zagłębiać się w tym
rozdziale.
Choć wsparcie języka Go dla współbieżności stanowi jedną z jego wielkich zalet, zrozumienie pro-
gramów współbieżnych jest z natury trudniejsze niż zrozumienie programów sekwencyjnych, a na-
wyki nabyte podczas programowania sekwencyjnego mogą czasami sprowadzić nas na manowce.
Jeśli jest to Twoje pierwsze spotkanie ze współbieżnością, warto poświęcić trochę więcej czasu
na przemyślenie przedstawionych w tych dwóch rozdziałach przykładów.

8.1. Funkcje goroutine


W języku Go każda współbieżnie wykonywana aktywność nazywa się funkcją goroutine. Rozważmy
program, który ma dwie funkcje: jedna wykonuje pewne obliczenia, a druga zapisuje jakieś dane
wyjściowe. Zakładamy, że żadna z tych funkcji nie wywołuje drugiej. Program sekwencyjny może
wywołać jedną funkcję, a następnie drugą, ale w programie współbieżnym z co najmniej dwoma
funkcjami goroutine wywołanie obu funkcji może być aktywne w tym samym czasie. Zobaczymy
taki program za chwilę.
216 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

Jeśli używałeś wątków systemu operacyjnego lub wątków w innych językach, możesz na razie
przyjąć, że funkcja goroutine jest podobna do wątku, a będziesz w stanie pisać prawidłowe pro-
gramy. Różnice między wątkami i funkcjami goroutine są w zasadzie ilościowe, a nie jakościowe,
i zostaną opisane w podrozdziale 9.8.
Po uruchomieniu programu jego jedyną funkcją goroutine jest ta, która wywołuje funkcję main,
więc nazywamy ją główną funkcją goroutine. Nowe funkcje goroutine są tworzone przez instruk-
cję go. Pod względem składniowym instrukcja go jest wywołaniem zwykłej funkcji lub metody
poprzedzonym słowem kluczowym go. Instrukcja go powoduje, że funkcja jest wywoływana w nowo
utworzonej funkcji goroutine. Sama instrukcja go zostaje wypełniona natychmiast:
f() // wywołanie funkcji f(); oczekiwanie na jej powrót
go f() // utworzenie funkcji goroutine, która wywołuje funkcję f(); nie czekamy
W poniższym przykładzie główna funkcja goroutine oblicza 45. liczbę Fibonacciego. Ponieważ
używa strasznie niewydajnego algorytmu rekurencyjnego, ma odczuwalny czas wykonywania,
podczas którego chcielibyśmy zapewnić użytkownikowi wizualne wskazanie, że program nadal działa,
wyświetlając animowany tekstowy wskaźnik ładowania (ang. spinner).
code/r08/spinner
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n) // działa powoli
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

func spinner(delay time.Duration) {


for {
for _, r := range ` -\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}

func fib(x int) int {


if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
Po kilku sekundach animacji wywołanie fib(45) kończy się, a funkcja main wyświetla jego wynik:
Fibonacci(45) = 1134903170
Następnie kończy się funkcja main. Kiedy to następuje, wszystkie funkcje goroutine zostają nagle
zakończone i program kończy działanie. Poza powróceniem z funkcji main lub wyjściem z pro-
gramu nie ma żadnego programowego sposobu, aby jedna funkcja goroutine zatrzymała drugą, ale
jak zobaczymy później, istnieją sposoby skomunikowania się z funkcją goroutine w celu zażądania,
żeby się zatrzymała.
Należy zwrócić uwagę, że program jest wyrażony jako kompozycja dwóch autonomicznych akcji:
obracania wskaźnika ładowania i obliczeń Fibonacciego. Każda z nich jest napisana jako odrębna
funkcja, ale postęp działania obu z nich odbywa się równolegle.
8.2. PRZYKŁAD: WSPÓŁBIEŻNY SERWER ZEGARA 217

8.2. Przykład: współbieżny serwer zegara


Sieci to naturalna dziedzina, w której używa się współbieżności, ponieważ serwery zazwyczaj
obsługują jednocześnie wiele połączeń z klientami, a każdy klient jest zasadniczo niezależny
od innych. W tym podrozdziale wprowadzimy pakiet net, który dostarcza komponenty do budo-
wania sieciowych programów klientów i serwerów komunikujących się za pośrednictwem proto-
kołu TCP, UDP lub uniksowych gniazd domenowych. Pakiet net/http, którego używaliśmy
od rozdziału 1., jest zbudowany na bazie funkcji z pakietu net.
Naszym pierwszym przykładem jest sekwencyjny serwer zegara, który wypisuje do klienta aktualny
czas raz na sekundę:
code/r08/clock1
// Clock1 jest serwerem TCP, który periodycznie zapisuje czas.
package main

import (
"io"
"log"
"net"
"time"
)

func main() {
listener, err := net.Listen("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err) // np. przerwano połączenie
continue
}
handleConn(conn) // obsługa jednego połączenia na raz
}
}

func handleConn(c net.Conn) {


defer c.Close()
for {
_, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
if err != nil {
return // np. klient rozłączony
}
time.Sleep(1 * time.Second)
}
}
Funkcja Listen tworzy obiekt net.Listener, który nasłuchuje połączeń przychodzących na porcie
sieciowym, w tym przypadku na porcie TCP localhost:8000. Metoda Accept nasłuchiwacza
blokuje, dopóki nie zostanie wykonane żądanie połączenia przychodzącego, a następnie zwraca
obiekt net.Conn, reprezentujący dane połączenie.
Funkcja handleConn obsługuje jedno ustanowione połączenie klienta. W pętli wypisuje do klienta aktu-
alny czas time.Now(). Ponieważ net.Conn spełnia warunki interfejsu io.Writer, możemy zapisywać
bezpośrednio do niego. Pętla kończy się, gdy nie powiedzie się operacja zapisu, najprawdopodobniej
218 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

dlatego, że klient się rozłączył. W tym momencie handleConn zamyka swoją stronę połączenia za
pomocą odroczonego wywołania Close i powraca do oczekiwania na kolejne żądanie połączenia.
Metoda time.Time.Format zapewnia sposób formatowania informacji o dacie i godzinie poprzez
podanie przykładu. Jej argumentem jest szablon wskazujący sposób formatowania czasu odniesie-
nia, np. Mon Jan 2 03:04:05PM 2006 UTC-0700. Czas odniesienia ma osiem komponentów (dzień
tygodnia, miesiąc, dzień miesiąca itd.). Każda kolekcja komponentów może występować w łańcuchu
Format w dowolnej kolejności i w różnych formatach. Wybrane komponenty daty i czasu będą
wyświetlane w wybranym formacie. Tu używamy tylko godziny, minuty i sekundy. Pakiet time
definiuje dla wielu standardowych formatów czasu szablony, takie jak time.RFC1123. Ten sam me-
chanizm jest używany w odwrotnym kierunku podczas parsowania czasu za pomocą time.Parse.
Aby połączyć się z serwerem, potrzebujemy programu klienckiego, takiego jak nc („netcat”), będą-
cego standardowym programem narzędziowym do manipulacji połączeniami sieciowymi:
$ go build code/r08/clock1
$ ./clock1 &
$ nc localhost 8000
13:58:54
13:58:55
13:58:56
13:58:57
^C
Klient wyświetla czas wysyłany przez serwer co sekundę, dopóki działanie klienta nie zostanie prze-
rwane za pomocą kombinacji przycisków Ctrl+C, która w systemach uniksowych jest prezentowa-
na przez powłokę jako ^C. Jeśli nc lub netcat nie jest zainstalowany w Twoim systemie, możesz
użyć programu telnet lub prostej wersji programu netcat napisanej w języku Go, która wykorzy-
stuje net.Dial do połączenia się z serwerem TCP:
code/r08/netcat1
// Netcat1 jest klientem TCP tylko do odczytu.
package main

import (
"io"
"log"
"net"
"os"
)

func main() {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
mustCopy(os.Stdout, conn)
}

func mustCopy(dst io.Writer, src io.Reader) {


if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
8.2. PRZYKŁAD: WSPÓŁBIEŻNY SERWER ZEGARA 219

Ten program odczytuje dane z połączenia i zapisuje je do standardowego strumienia wyjściowe-


go, dopóki nie wystąpi warunek końca pliku lub błąd. Funkcja mustCopy to narzędzie wykorzysty-
wane w kilku przykładach w tym podrozdziale. Uruchommy dwa klienty w tym samym czasie
na różnych terminalach (jeden pokazano po lewej, a drugi po prawej stronie):
$ go build code/r08/netcat1
$ ./netcat1
13:58:54 $ ./netcat1
13:58:55
13:58:56
^C
13:58:57
13:58:58
13:58:59
^C
$ killall clock1
Polecenie killall jest uniksowym narzędziem, które zamyka wszystkie procesy o podanej nazwie.
Drugi klient musi czekać, aż pierwszy klient zakończy swoje działanie, ponieważ ten serwer jest
sekwencyjny. Obsługuje tylko jednego klienta na raz. Aby ten serwer stał się współbieżny, potrzebna
jest tylko jedna mała zmiana: dodanie do wywołania funkcji handleConn słowa kluczowego go
powoduje, że każde wywołanie uruchamiane jest w swojej własnej funkcji goroutine.
code/r08/clock2
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err) // np. przerwano połączenie
continue
}
go handleConn(conn) // obsługuje połączenia równolegle
}
Teraz wiele klientów może pobierać czas jednocześnie:
$ go build code/r08/clock2
$ ./clock2 &
$ go build gopl.io/ch8/netcat1
$ ./netcat1
14:02:54 $ ./netcat1
14:02:55 14:02:55
14:02:56 14:02:56
14:02:57 ^C
14:02:58
14:02:59 $ ./netcat1
14:03:00 14:03:00
14:03:01 14:03:01
^C 14:03:02
^C

$ killall clock2
Ćwiczenie 8.1. Zmodyfikuj program clock2 w taki sposób, aby przyjmował numer portu, i napisz
program o nazwie clockwall, który działa jako klient kilku serwerów zegara jednocześnie, odczy-
tując czasy z każdego z nich i wyświetlając wyniki w tabeli na wzór ściany z zegarami spotykanej
w niektórych biurach. Jeśli masz dostęp do komputerów rozproszonych geograficznie, uruchom
instancje zdalnie. W przeciwnym razie uruchom lokalne instancje na różnych portach z fałszywymi
strefami czasowymi.
220 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

$ TZ=US/Eastern ./clock2 -port 8010 &


$ TZ=Asia/Tokyo ./clock2 -port 8020 &
$ TZ=Europe/London ./clock2 -port 8030 &
$ clockwall NewYork=localhost:8010 London=localhost:8020 Tokyo=localhost:8030
Ćwiczenie 8.2. Zaimplementuj współbieżny serwer FTP (ang. File Transfer Protocol). Serwer
powinien z każdego klienta interpretować polecenia takie jak: cd do zmiany katalogu, ls do wyświe-
tlania listy katalogów, get do wysyłania zawartości pliku oraz close do zamykania połączenia. Jako
klienta możesz użyć standardowego polecenia ftp lub możesz napisać własnego klienta.

8.3. Przykład: współbieżny serwer echo


Serwer zegara używał jednej funkcji goroutine na każde połączenie. W tym podrozdziale zbuduje-
my serwer echo, który wykorzystuje wiele funkcji goroutine na połączenie. Większość serwerów
echo jedynie wypisuje to, co przeczyta, co można zrobić za pomocą tej trywialnej wersji handleConn:
func handleConn(c net.Conn) {
io.Copy(c, c) // UWAGA: ignorowanie błędów
c.Close()
}
Bardziej interesujący serwer echo może symulować pogłos prawdziwego echa, dając na początku
głośną odpowiedź ("WITAJ!"), następnie po pewnym opóźnieniu umiarkowaną ("Witaj!"), po czym
przed całkowitym wygaśnięciem cichą ("witaj!"), tak jak w tej wersji handleConn:
code/r08/reverb1
func echo(c net.Conn, shout string, delay time.Duration) {
fmt.Fprintln(c, "\t", strings.ToUpper(shout))
time.Sleep(delay)
fmt.Fprintln(c, "\t", shout)
time.Sleep(delay)
fmt.Fprintln(c, "\t", strings.ToLower(shout))
}

func handleConn(c net.Conn) {


input := bufio.NewScanner(c)
for input.Scan() {
echo(c, input.Text(), 1*time.Second)
}
// UWAGA: ignorowanie potencjalnych błędów z input.Err().
c.Close()
}
Musimy rozszerzyć nasz program klienta w taki sposób, aby wysyłał do serwera dane wejściowe
z terminala, kopiując jednocześnie odpowiedź serwera do strumienia wyjściowego, co stwarza kolej-
ną możliwość wykorzystania współbieżności:
code/r08/netcat2
func main() {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
go mustCopy(os.Stdout, conn)
mustCopy(conn, os.Stdin)
}
8.3. PRZYKŁAD: WSPÓŁBIEŻNY SERWER ECHO 221

Podczas gdy główna funkcja goroutine odczytuje dane ze standardowego strumienia wejściowego
i wysyła je do serwera, druga funkcja goroutine odczytuje i wyświetla odpowiedź serwera. Gdy
główna funkcja goroutine napotka koniec danych wejściowych, np. gdy użytkownik wciśnie
w terminalu kombinację przycisków Ctrl+D (^D) lub Ctrl+Z w przypadku systemów Microsoft
Windows, program zatrzyma się, nawet jeśli druga funkcja goroutine ma jeszcze zadania do wyko-
nania. (Gdy w punkcie 8.4.1 wprowadzimy kanały, zobaczymy, jak sprawić, by program czekał na
zakończenie zadań przez obie strony).
W poniższej sesji dane wejściowe z klienta są wyrównane do lewej strony, a odpowiedzi serwera są
wcięte. Klient krzyczy do serwera echo trzykrotnie:
$ go build code/r08/reverb1
$ ./reverb1 &
$ go build code/r08/netcat2
$ ./netcat2
Witaj!
WITAJ!
Witaj!
witaj!
Jest tam kto?
JEST TAM KTO?
Juhu!
Jest tam kto?
jest tam kto?
JUHU!
Juhu!
juhu!
^D
$ killall reverb1
Należy zwrócić uwagę, że trzeci krzyk od klienta nie jest obsługiwany, dopóki drugi krzyk nie za-
niknie, co nie jest zbyt realistyczne. Prawdziwe echo obejmowałoby kompozycję trzech niezależnych
krzyków. Aby to zasymulować, będziemy potrzebować więcej funkcji goroutine. Ponownie mu-
simy jedynie dodać słowo kluczowe go, tym razem do wywołania funkcji echo:
code/r08/reverb2
func handleConn(c net.Conn) {
input := bufio.NewScanner(c)
for input.Scan() {
go echo(c, input.Text(), 1*time.Second)
}
// UWAGA: ignorowanie potencjalnych błędów z input.Err().
c.Close()
}
Argumenty przekazywane do funkcji uruchamianej instrukcją go są ewaluowane, gdy wykonywana
jest sama instrukcja go. Dlatego funkcja input.Text() jest ewaluowana w głównej funkcji goroutine.
Teraz echa są równoległe i pokrywają się w czasie:
$ go build gopl.io/ch8/reverb2
$ ./reverb2 &
$ ./netcat2
Jest tam kto?
JEST TAM KTO?
Juhu!
Jest tam kto?
JUHU!
222 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

jest tam kto?


Juhu!
juhu!
^D
$ killall reverb2
Aby serwer używał współbieżności nie tylko do obsługi połączeń z wielu klientów, ale nawet w ra-
mach jednego połączenia, wystarczyło jedynie wstawić dwa słowa kluczowe go.
Dodając jednak te słowa kluczowe, musieliśmy dokładnie rozważyć, czy bezpiecznie jest współ-
bieżnie wywoływać metody net.Conn, co nie jest prawdą dla większości typów. Kluczową koncepcję
bezpieczeństwa współbieżności omówimy w następnym rozdziale.

8.4. Kanały
Jeśli funkcje goroutine są aktywnościami współbieżnego programu Go, kanały (ang. channels) są
połączeniami między nimi. Kanał jest mechanizmem komunikacji, który umożliwia jednej funkcji
goroutine wysyłanie wartości do drugiej funkcji goroutine. Każdy kanał jest przewodem dla wartości
określonego typu, nazywanego typem elementów kanału. Typ kanału, którego elementy mają typ int,
jest zapisywany jako chan int.
Aby utworzyć kanał, używamy wbudowanej funkcji make:
ch := make(chan int) // ch ma typ 'chan int'
Podobnie jak w przypadku map kanał jest referencją do struktury danych utworzonej przez
funkcję make. Kiedy kopiujemy kanał lub przekazujemy go jako argument do funkcji, kopiujemy
referencję, więc podmioty wywołujący i wywoływany odwołują się do tej samej struktury danych.
Tak jak w przypadku innych typów referencyjnych wartością zerową kanału jest nil.
Dwa kanały tego samego typu mogą być porównywane za pomocą operatora ==. Porównanie
jest prawdziwe, jeśli oba są referencjami do tej samej struktury danych kanału. Kanał może być
również porównywany do wartości nil.
Kanał ma dwie podstawowe operacje: wysyłanie i odbieranie, znane pod wspólną nazwą komu-
nikacji. Instrukcja wysyłania przekazuje wartości z jednej funkcji goroutine poprzez kanał do
drugiej funkcji goroutine, wykonującej odpowiednie wyrażenie odbierania. Obie operacje są zapi-
sywane przy użyciu operatora <-. W instrukcji wysyłania operator <- oddziela operandy kanału
i wartości. W wyrażeniu odbierania operator <- poprzedza operand kanału. Wyrażenie odbierania,
którego wynik nie jest używany, jest prawidłową instrukcją.
ch <- x // instrukcja wysyłania

x = <-ch // wyrażenie odbierania w instrukcji przypisania


<-ch // instrukcja odbierania; wynik jest porzucany
Kanały obsługują jeszcze trzecią operację, zamknięcia, która ustawia flagę wskazującą, że przez
dany kanał nie będą już nigdy wysyłane żadne wartości. Kolejne próby wysłania wywołają panikę.
Operacje odbierania na zamkniętym kanale będą dawać wysłane już wartości, dopóki nie będzie
już żadnych więcej wartości. Od tego momentu wszystkie operacje odbierania zostaną natychmiast
zakończone i dadzą wartość zerową dla typu elementów kanału.
Aby zamknąć kanał, należy wywołać wbudowaną funkcję close:
close(ch)
8.4. KANAŁY 223

Kanał utworzony za pomocą prostego wywołania make jest nazywamy kanałem niebuforowanym,
ale make akceptuje opcjonalny drugi argument, czyli liczbę całkowitą zwaną pojemnością kanału.
Jeżeli pojemność jest inna niż zero, make tworzy kanał buforowany.
ch = make(chan int) // kanał niebuforowany
ch = make(chan int, 0) // kanał niebuforowany
ch = make(chan int, 3) // kanał buforowany z pojemnością 3
Kanałom niebuforowanym przyjrzymy się najpierw, a kanały buforowane omówimy w punkcie 8.4.4.

8.4.1. Kanały niebuforowane


Operacja wysyłania na niebuforowanym kanale blokuje wysyłającą funkcję goroutine, dopóki na-
stępna funkcja goroutine nie wykona odpowiedniej operacji odbierania na tym samym kanale.
W tym momencie wartość jest przekazywana i obie funkcje goroutine mogą kontynuować swoje
wykonywanie. I odwrotnie: jeśli operacja odbierania została podjęta jako pierwsza, odbierająca funk-
cja goroutine jest blokowana, dopóki inna funkcja goroutine nie wykona operacji wysyłania na tym
samym kanale.
Komunikacja przez niebuforowany kanał powoduje synchronizację funkcji goroutine wysyłania
i odbierania. Z tego powodu niebuforowane kanały są czasami nazywane kanałami synchronicz-
nymi. Gdy wartość jest wysyłana przez niebuforowany kanał, otrzymanie wartości odbywa się
przed przebudzeniem wysyłającej funkcji goroutine.
Gdy rozmawiając o współbieżności, mówimy, że x dzieje się przed y, nie oznacza to jedynie, że x
nastąpi we wcześniejszym momencie w czasie niż y. Mamy na myśli, że jest to gwarantowane
i że wszystkie wcześniejsze czynności, takie jak aktualizacje zmiennych, odniosą skutek, więc
można na nich polegać.
Gdy x nie dzieje się przed y ani po y, mówimy, że x jest współbieżne z y. Nie oznacza to, że x i y
są koniecznie jednoczesne, a jedynie, że nie możemy zakładać niczego na temat ich kolejności.
Jak zobaczymy w następnym rozdziale, konieczne jest uporządkowanie pewnych zdarzeń podczas
wykonywania programu, aby uniknąć problemów, które pojawiają się, gdy dwie funkcje goroutine
równolegle uzyskują dostęp do tej samej zmiennej.
Program klienta w podrozdziale 8.3 kopiuje dane wejściowe do serwera w swojej głównej funkcji
goroutine, więc ten program kliencki zostaje zakończony, gdy tylko strumień wejściowy zostaje
zamknięty, nawet jeśli funkcja goroutine w tle nadal działa. Aby program przed zamknięciem
poczekał na zakończenie funkcji goroutine działającej w tle, używamy kanału do zsynchronizowa-
nia tych dwóch funkcji goroutine:
code/r08/netcat3
func main() {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // UWAGA: ignorowanie błędów
log.Println("zrobione")
done <- struct{}{} // sygnalizowanie głównej funkcji goroutine
}()
mustCopy(conn, os.Stdin)
224 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

conn.Close()
<-done // oczekiwanie na zakończenie funkcji goroutine działającej w tle
}
Gdy użytkownik zamyka standardowy strumień wejściowy, funkcja mustCopy kończy się, a główna
funkcja goroutine wywołuje conn.Close(), zamykając obie połowy połączenia sieciowego. Zamknię-
cie zapisującej połowy połączenia powoduje, że serwer spostrzega warunek końca pliku. Zamknię-
cie odczytującej połowy powoduje, że wywołanie io.Copy wykonane przez funkcję goroutine w tle
zwraca błąd odczytu z zamkniętego połączenia, dlatego usunęliśmy rejestrowanie błędów. Ćwicze-
nie 8.3 sugeruje lepsze rozwiązanie. (Należy zwrócić uwagę, że instrukcja go wywołuje literalną
funkcję, co jest typową konstrukcją).
Przed zakończeniem funkcja goroutine w tle rejestruje komunikat, a następnie wysyła wartość
na kanał done. Główna funkcja goroutine czeka na otrzymanie tej wartości. W rezultacie program
przed wyjściem zawsze rejestruje komunikat "zrobione".
Komunikaty wysyłane poprzez kanały mają dwa ważne aspekty. Każdy komunikat ma wartość,
ale czasem równie ważny jest sam fakt komunikacji i moment, w którym ma ona miejsce. Gdy
chcemy podkreślić ten aspekt, nazywamy komunikaty zdarzeniami. Kiedy zdarzenie nie przenosi
żadnych dodatkowych informacji, czyli jego jedynym celem jest synchronizacja, będziemy podkreślać
to, używając kanału, którego typem elementów jest struct{}, chociaż powszechne jest użycie dla te-
go samego celu kanału typu bool lub int, ponieważ done <- 1 jest krótsze niż done <- struct{}{}.
Ćwiczenie 8.3. W programie netcat3 wartość interfejsu conn ma typ konkretny *net.TCPConn,
który reprezentuje połączenie TCP. Połączenie TCP składa się z dwóch połówek, które mogą
być zamykane niezależnie za pomocą jego metod CloseRead i CloseWrite. Zmodyfikuj główną
funkcję goroutine programu netcat3 w taki sposób, aby zamykała tylko zapisującą połowę połą-
czenia, żeby program nadal wypisywał końcowe echa z serwera reverb1, nawet po zamknięciu
standardowego strumienia wejściowego. (Zrobienie tego dla serwera reverb2 jest trudniejsze —
zob. ćwiczenie 8.4).

8.4.2. Potoki
Kanały mogą być wykorzystywane do łączenia funkcji goroutine w taki sposób, aby dane wyjścio-
we z jednej funkcji były danymi wejściowymi dla drugiej. Nazywamy to potokiem (ang. pipeline).
Poniższy program składa się z trzech funkcji goroutine połączonych za pomocą dwóch kanałów,
tak jak przedstawiono schematycznie na rysunku 8.1.

Rysunek 8.1. Potok trzyetapowy

Pierwsza funkcja goroutine (licznik) generuje liczby całkowite (0, 1, 2, …) i wysyła je przez kanał
do drugiej funkcji goroutine (potęgi kwadratowej), która odbiera każdą wartość, podnosi ją do
kwadratu i wysyła wynik przez kolejny kanał do trzeciej funkcji goroutine (wyświetlacza), która
odbiera wartości podniesione do kwadratu i wyświetla je. Dla jasności przykładu celowo wybrali-
śmy bardzo proste funkcje, choć oczywiście są one zbyt trywialne obliczeniowo, aby w rzeczywi-
stym programie uzasadniać własne funkcje goroutine.
8.4. KANAŁY 225

code/r08/pipeline1
func main() {
naturals := make(chan int)
squares := make(chan int)

// Licznik.
go func() {
for x := 0; ; x++ {
naturals <- x
}
}()

// Potęga kwadratowa.
go func() {
for {
x := <-naturals
squares <- x * x
}
}()

// Wyświetlacz (w głównej funkcji goroutine).


for {
fmt.Println(<-squares)
}
}
Jak można się spodziewać, program wyświetla nieskończony ciąg liczb podniesionych do kwa-
dratu: 0, 1, 4, 9 itd. Takie potoki można znaleźć w długo działających programach serwerowych,
gdzie kanały są wykorzystywane do trwającej całe życie programu komunikacji między funkcjami
goroutine zawierającymi nieskończone pętle. Co jednak, jeśli chcemy wysłać przez potok jedynie
skończoną liczbę wartości?
Jeżeli nadawca wie, że przez dany kanał nie zostanie już wysłanych więcej wartości, korzystne jest
zakomunikowanie tego faktu funkcjom goroutine odbiorcy, aby mogły przestać czekać. Osiąga
się to poprzez zamknięcie kanału za pomocą wbudowanej funkcji close:
close(naturals)
Po zamknięciu kanału wszelkie dalsze próby wykonania na nim operacji wysyłania spowodują
uruchomienie procedury panic. Gdy zamknięty kanał zostanie osuszony, czyli odebrany zostanie
ostatni wysłany element, wszystkie późniejsze operacje będą procedowane bez blokowania, ale
dadzą wartość zerową. Zamknięcie powyższego kanału naturals spowoduje dalsze wykonywa-
nie pętli funkcji potęgi kwadratowej, ponieważ odbierany będzie niekończący się strumień wartości
zerowych, które będą następnie wysyłane do funkcji wyświetlania.
Nie ma możliwości sprawdzenia bezpośrednio, czy kanał został zamknięty, ale istnieje wariant
operacji odbierania, który generuje dwa wyniki: odebrany element kanału oraz wartość logiczną
zwyczajowo nazywaną ok, która jest prawdziwa (true) dla udanego odbioru i fałszywa (false)
dla odbioru na zamkniętym i osuszonym kanale. Wykorzystując tę cechę, możemy zmodyfikować
pętlę funkcji potęgi kwadratowej, aby była zatrzymywana, gdy osuszony zostanie kanał naturals,
i żeby zamykany był z kolei kanał squares.
// Potęga kwadratowa.
go func() {
for {
x, ok := <-naturals
226 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

if !ok {
break // kanał został zamknięty i osuszony
}
squares <- x * x
}
close(squares)
}()
Ponieważ powyższa składnia jest dość toporna, a wzorzec ten jest powszechny, język Go po-
zwala nam używać pętli range również do iteracji przez kanały. Jest to wygodniejsza składnia dla
odbierania wszystkich wysłanych przez kanał wartości i zakończenia pętli po ostatniej z nich.
W poniższym potoku funkcja goroutine licznika kończy swoją pętlę po 100 elementach i zamyka
kanał naturals, powodując, że funkcja potęgi kwadratowej kończy swoją pętlę i zamyka kanał
squares. (W bardziej złożonym programie może być sensowne, aby funkcje licznika i potęgi
kwadratowej od początku odraczały swoje wywołania close). Na koniec główna funkcja goroutine
kończy swoją pętlę i program zostaje zamknięty.
code/r08/pipeline2
func main() {
naturals := make(chan int)
squares := make(chan int)

// Licznik.
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()

// Potęga kwadratowa.
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()

// Wyświetlacz (w głównej funkcji goroutine).


for x := range squares {
fmt.Println(x)
}
}
Nie trzeba zamykać każdego kanału, gdy skończyłeś z niego korzystać. Konieczne jest tylko zamy-
kanie kanału, gdy ważne jest wskazanie odbierającym funkcjom goroutine, że wszystkie dane zostały
wysłane. Gdy mechanizm odzyskiwania pamięci określi, że jakiś kanał stał się nieosiągalny, od-
zyska jego zasoby bez względu na to, czy został on zamknięty, czy nie. (Nie należy mylić tego z za-
mykaniem operacji dla otwartych plików. Ważne jest wywołanie metody close na każdym pliku,
którego skończyłeś używać).
Próba zamknięcia już zamkniętego kanału powoduje panikę, tak samo jak próba zamknięcia kanału
nil. Zamykanie kanałów ma jeszcze inne zastosowanie — jako mechanizm rozgłaszania, który
omówimy w podrozdziale 8.9.
8.4. KANAŁY 227

8.4.3. Jednokierunkowe typy kanałów


Wraz z rozrastaniem się programu naturalne jest dzielenie dużych funkcji na mniejsze kawałki.
Nasz poprzedni przykład wykorzystywał trzy funkcje goroutine, komunikując się poprzez dwa kana-
ły, które były lokalnymi zmiennymi funkcji main. Ten program w naturalny sposób dzieli się na
trzy funkcje:
func counter(out chan int)
func squarer(out, in chan int)
func printer(in chan int)
Funkcja squarer (umieszczona pośrodku potoku) przyjmuje dwa parametry: kanał wejściowy
i kanał wyjściowy. Oba mają ten sam typ, ale ich przeznaczenie jest przeciwne: kanał in służy tylko
do odbierania z niego wartości, a kanał out służy tylko do wysyłania do niego wartości. Tę intencję
wyrażają nazwy in i out, ale nic nie powstrzymuje funkcji squarer od wysyłania do in lub odbie-
rania z out.
Taka organizacja jest typowa. Gdy kanał jest dostarczany jako parametr funkcji, prawie zawsze
intencja jest taka, aby był wykorzystywany wyłącznie do wysyłania lub wyłącznie do odbierania.
Aby udokumentować ten zamiar i zapobiec niewłaściwemu wykorzystaniu, system typów języka
Go zapewnia jednokierunkowe typy kanałów, które udostępniają tylko jedną lub drugą operację:
wysyłanie lub odbieranie. Typ chan<- int, czyli kanał typów int tylko do wysyłania, pozwala na
wysyłanie, ale nie na odbieranie. Z kolei typ <-chan int, czyli kanał typów int tylko do odbioru,
pozwala na odbieranie, ale nie na wysyłanie. (Pozycja strzałki <- w stosunku do słowa kluczowego
chan jest symboliczna). Naruszenia tego reżimu są wykrywane w czasie kompilacji.
Ponieważ operacja close zakłada, że nie będzie już więcej prób wysyłania na danym kanale, może
ją wywołać tylko wysyłająca funkcja goroutine. Dlatego próba zamknięcia kanału tylko do odbioru
jest błędem podczas kompilacji.
Poniżej został ponownie przedstawiony potok podnoszenia do kwadratu, tym razem z jednokie-
runkowymi typami kanałów:
code/r08/pipeline3
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}

func squarer(out chan<- int, in <-chan int) {


for v := range in {
out <- v * v
}
close(out)
}

func printer(in <-chan int) {


for v := range in {
fmt.Println(v)
}
}
228 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

func main() {
naturals := make(chan int)
squares := make(chan int)

go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
Wywołanie counter(naturals) pośrednio konwertuje naturals, czyli wartość typu chan int,
na typ parametru, czyli chan<- int. Wywołanie printer(squares) wykonuje podobną pośrednią
konwersję na <-chan int. Konwersje z dwukierunkowych na jednokierunkowe typy kanałów są
dozwolone w każdym przypisaniu. Nie ma jednak powrotu: gdy masz już wartość jednokierunko-
wego typu, takiego jak chan<- int, nie ma możliwości uzyskania z niej wartości typu chan int,
który odwołuje się do tej samej struktury danych kanału.

8.4.4. Kanały buforowane


Buforowany kanał ma kolejkę elementów. Maksymalny rozmiar kolejki jest określany poprzez prze-
kazanie do funkcji make argumentu pojemności, gdy kolejka jest tworzona. Poniższa instrukcja
tworzy buforowany kanał, który jest w stanie pomieścić trzy wartości string. Rysunek 8.2 jest
graficzną reprezentacją zmiennej ch i kanału, do którego się ona odwołuje.
ch = make(chan string, 3)

Rysunek 8.2. Pusty kanał buforowany

Operacja wysyłania na kanale buforowanym wstawia element na końcu kolejki, a operacja odbie-
rania usuwa element z przodu kolejki. Jeśli kanał jest pełny, operacja wysyłania blokuje swoją
funkcję goroutine, dopóki inna funkcja goroutine odbierania nie zwolni miejsca. Natomiast jeśli
kanał jest pusty, operacja odbierania blokuje, dopóki jakaś wartość nie zostanie wysłana przez inną
funkcję goroutine.
Na tym kanale możemy wysłać do trzech wartości bez blokowania funkcji goroutine:
ch <- "A"
ch <- "B"
ch <- "C"
W tym momencie kanał jest pełny (rysunek 8.3), a czwarta instrukcja wysyłania będzie blokować.

Rysunek 8.3. W pełni zbuforowany kanał


8.4. KANAŁY 229

Jeśli odbierzemy jedną wartość:


fmt.Println(<-ch) // "A"
kanał nie będzie ani pełny, ani pusty (rysunek 8.4), więc zarówno operacja wysyłania, jak i operacja
odbierania będą mogły przebiegać bez blokowania. W ten sposób bufor kanału oddziela funkcje
goroutine wysyłania i odbierania.

Rysunek 8.4. Częściowo zapełniony kanał buforowany

W mało prawdopodobnym przypadku, gdy program musi poznać pojemność bufora kanału,
można ją uzyskać przez wywołanie wbudowanej funkcji cap:
fmt.Println(cap(ch)) // "3"
Gdy zastosujemy do kanału wbudowaną funkcję len, zwróci ona liczbę aktualnie zbuforowanych
elementów. Ponieważ we współbieżnym programie istnieje duże prawdopodobieństwo, że taka
informacja może stać się przestarzała od razu po odebraniu, jej wartość jest ograniczona, ale może
być ewentualnie przydatna podczas diagnozowania błędów lub optymalizacji wydajności.
fmt.Println(len(ch)) // "2"
Po dwóch kolejnych operacjach odbierania kanał ponownie jest pusty, a czwarta będzie blokować:
fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C"
W tym przykładzie operacje wysyłania i odbierania były wykonywane przez tę samą funkcję
goroutine, ale w rzeczywistych programach zwykle są one realizowane przez różne funkcje goroutine.
Nowicjusze zwabieni ich przyjemnie prostą składnią buforowanych kanałów ulegają czasami
pokusie wykorzystania ich w pojedynczej funkcji goroutine jako kolejki, ale jest to błąd. Kanały
są głęboko połączone z harmonogramem funkcji goroutine i bez innej funkcji goroutine odbierają-
cej z danego kanału nadawca (a być może cały program) ryzykuje zablokowanie na zawsze. Jeśli
potrzebujesz jedynie prostej kolejki, użyj do tego celu wycinka.
Poniższy przykład pokazuje zastosowanie buforowanego kanału. Wysyła on równoległe żądania
do trzech serwerów lustrzanych, czyli równoważnych, ale geograficznie rozproszonych. Wysyła
ich odpowiedzi przez buforowany kanał, a następnie odbiera i zwraca tylko pierwszą odpowiedź,
tę, która przybędzie najszybciej. Dlatego funkcja mirroredQuery zwraca wynik, zanim jeszcze
odpowiedzą dwa wolniejsze serwery. (Nawiasem mówiąc, całkiem normalne jest, aby kilka funkcji
goroutine wysyłało wartości współbieżnie do tego samego kanału, jak w tym przykładzie, albo otrzy-
mywało z tego samego kanału).
func mirroredQuery() string {
responses := make(chan string, 3)
go func() { responses <- request("asia.helion.pl") }()
go func() { responses <- request("europe.helion.pl") }()
go func() { responses <- request("americas.helion.pl") }()
return <-responses // zwraca najszybszą odpowiedź
}

func request(hostname string) (response string) { /* ... */ }


230 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

Gdybyśmy użyli kanału niebuforowanego, dwie wolniejsze funkcje goroutine utknęłyby, próbując
wysłać swoje odpowiedzi na kanale, z którego żadna funkcja goroutine nie będzie nigdy odbierać.
Ta sytuacja, zwana wyciekiem funkcji goroutine (ang. goroutine leak), byłaby usterką programu.
W odróżnieniu od nieużywanych zmiennych wyciekające funkcje goroutine nie są automatycznie
poddawane procesowi odzyskiwania pamięci, więc należy się upewnić, że zakończą one swoje
działanie, gdy nie będą już potrzebne.
Wybór pomiędzy kanałami niebuforowanymi i buforowanymi oraz wybór pojemności kanału
buforowanego mogą wpływać na poprawność programu. Niebuforowane kanały dają większe
gwarancje synchronizacji, ponieważ każda operacja wysyłania jest synchronizowana z odpowia-
dającą jej operacją odbierania. W przypadku kanałów buforowanych operacje te są rozdzielone.
Ponadto gdy znamy górną granicę liczby wartości, które zostaną wysłane na danym kanale, nie jest
niczym niezwykłym utworzenie buforowanego kanału tego rozmiaru i wykonanie wszystkich operacji
wysyłania, zanim pierwsza wartość zostanie odebrana. Nieudana alokacja wystarczającej pojemności
bufora spowodowałaby zakleszczenie programu.
Buforowanie kanału może również wpływać na wydajność programu. Wyobraźmy sobie trzech
cukierników w cukierni: jeden piecze, drugi lukruje, a trzeci umieszcza napisy na każdym cieście
przed przekazaniem go do następnego cukiernika na linii produkcyjnej. W kuchni z niewielką
ilością przestrzeni każdy cukiernik, który ukończył swoją część pracy nad ciastem, musi czekać,
aż następny cukiernik będzie gotowy, żeby je przyjąć. Taka relacja jest analogiczna do komunikacji
poprzez niebuforowany kanał.
Jeśli pomiędzy każdym stanowiskiem jest miejsce na jedno ciasto, cukiernik może umieścić tam
ukończone ciasto i natychmiast rozpocząć pracę nad następnym. Jest to analogiczne do buforowa-
nego kanału o pojemności 1. Więc dopóki cukiernicy pracują mniej więcej w tym samym tempie,
większość z tych przekazań postępuje szybko, niwelując przejściowe różnice w tempie pracy po-
szczególnych cukierników. Większa ilość dostępnej przestrzeni między cukiernikami (większe
bufory) może zniwelować większe przejściowe różnice w ich tempie pracy bez przeciążania linii pro-
dukcyjnej, tak jak ma to miejsce, gdy jeden cukiernik robi sobie krótką przerwę, a potem pospiesznie
nadrabia zaległości.
Z drugiej strony, jeśli jakiś wcześniejszy etap linii produkcyjnej jest stale szybszy niż etap po nim
następujący, bufor między nimi będzie przez większość czasu pełny. Z kolei jeśli późniejszy etap jest
szybszy, bufor będzie zazwyczaj pusty. W tym przypadku bufor nie zapewnia żadnych korzyści.
Metafora linii produkcyjnej jest użyteczna dla kanałów i funkcji goroutine. Jeśli np. drugi etap
jest bardziej skomplikowany, pojedynczy cukiernik może nie być w stanie nadążyć z przejmowa-
niem dostawy od pierwszego cukiernika lub z zaspokojeniem zapotrzebowania ze strony trzeciego.
Aby rozwiązać ten problem, moglibyśmy zatrudnić kolejnego cukiernika, aby pomógł temu
środkowemu, wykonując to samo zadanie, ale działając niezależnie. Jest to analogiczne do utwo-
rzenia kolejnej funkcji goroutine komunikującej się przez te same kanały.
Nie mamy miejsca, aby to tutaj pokazać, ale pakiet code/r08/cake symuluje tę cukiernię z kilkoma
parametrami, które można zmieniać. Zawiera on testy porównawcze (zob. podrozdział 11.4)
dla kilku z opisanych powyżej scenariuszy.
8.5. ZAPĘTLENIE RÓWNOLEGŁE 231

8.5. Zapętlenie równoległe


W tym podrozdziale zbadamy niektóre typowe wzorce współbieżności służące do wykonywania
wszystkich iteracji pętli równolegle. Będziemy rozważać problem generowania miniatur obrazów
z zestawu obrazów pełnowymiarowych. Pakiet code/r08/thumbnail zapewnia funkcję ImageFile,
która umożliwia skalowanie pojedynczego obrazu. Nie pokażemy jej implementacji, ale można
ją pobrać ze strony wydawnictwa Helion, wraz z innymi przykładami kodu.
code/r08/thumbnail
package thumbnail

// ImageFile odczytuje obraz z infile i zapisuje jego miniaturę w tym samym katalogu.
// Zwraca wygenerowaną nazwę pliku, np. "foo.thumb.jpg".
func ImageFile(infile string) (string, error)
Poniższy program tworzy pętlę przez listę nazw plików obrazów i generuje miniatury dla każdego
obrazu:
code/r08/thumbnail
// makeThumbnails tworzy miniatury określonych obrazów.
func makeThumbnails(filenames []string) {
for _, f := range filenames {
if _, err := thumbnail.ImageFile(f); err != nil {
log.Println(err)
}
}
}
Oczywiście kolejność przetwarzania plików nie ma znaczenia, ponieważ każda operacja skalowania
jest niezależna od wszystkich pozostałych. Problemy takie jak ten, składające się wyłącznie z pod-
problemów będących całkowicie niezależnymi od siebie, są opisywane jako zawstydzająco równole-
głe (ang. embarrassingly parallel). Problemy zawstydzająco równoległe są najprostszym rodzajem
problemów, jeśli chodzi o kwestię zaimplementowania współbieżności, i korzystają z wydajności,
która skaluje się liniowo wraz ze stopniem równoległości.
Wykonajmy wszystkie te operacje równolegle, ukrywając w ten sposób opóźnienia operacji we-wy
plików i używając wielu procesorów do obliczeń skalowania obrazów. Nasze pierwsze podejście
do wersji współbieżnej dodaje po prostu słowo kluczowe go. Na razie zignorujemy błędy i zajmiemy
się nimi później.
// UWAGA: nieprawidłowe!
func makeThumbnails2(filenames []string) {
for _, f := range filenames {
go thumbnail.ImageFile(f) // UWAGA: ignorowanie błędów
}
}
Ta wersja działa naprawdę szybko. W rzeczywistości zbyt szybko, ponieważ potrzebuje mniej czasu
niż oryginał, nawet gdy wycinek nazw plików zawiera tylko jeden element. Jeśli nie ma rów-
noległości, w jaki sposób wersja współbieżna może działać szybciej? Odpowiedzią jest to, że funkcja
makeThumbnails powraca, zanim skończy robić to, co zrobić powinna. Uruchamia wszystkie funkcje
goroutine, po jednej na każdą nazwę pliku, ale nie czeka na zakończenie ich wykonywania.
232 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

Nie ma bezpośredniego sposobu na zaczekanie, aż funkcja goroutine zakończy swoje działanie,


ale możemy zmienić wewnętrzną funkcję goroutine w taki sposób, aby zgłaszała zakończenie
swojego wykonywania do zewnętrznej funkcji goroutine poprzez wysyłanie zdarzenia na współdzie-
lonym kanale. Ponieważ wiemy, że jest dokładnie len(filenames) wewnętrznych funkcji goroutine,
zewnętrzna funkcja goroutine musi przed powróceniem naliczyć jedynie taką właśnie liczbę zdarzeń:
// makeThumbnails3 tworzy równolegle miniaturki z określonych plików obrazów.
func makeThumbnails3(filenames []string) {
ch := make(chan struct{})
for _, f := range filenames {
go func(f string) {
thumbnail.ImageFile(f) // UWAGA: ignorowanie błędów
ch <- struct{}{}
}(f)
}

// Oczekiwanie na zakończenie wszystkich funkcji goroutine.


for range filenames {
<-ch
}
}
Należy zwrócić uwagę, że przekazaliśmy wartość f jako bezpośredni argument do literalnej funkcji,
zamiast użyć deklaracji f z zamykającej ją pętli for:
for _, f := range filenames {
go func() {
thumbnail.ImageFile(f) // UWAGA: nieprawidłowe!
// …
}()
}
Przypomnijmy problem przechwytywania zmiennej pętli wewnątrz anonimowej funkcji, opisany
w punkcie 5.6.1. Powyższa pojedyncza zmienna f jest współdzielona przez wszystkie wartości ano-
nimowej funkcji oraz aktualizowana przez kolejne iteracje pętli. Zanim nowe funkcje goroutine
rozpoczną wykonywanie literalnej funkcji, pętla for może zaktualizować zmienną f i rozpocząć
kolejną iterację lub (co bardziej prawdopodobne) całkowicie zakończyć wykonywanie, więc gdy te
funkcje goroutine odczytają wartość zmiennej f, zaobserwują, że zmienna ta ma wartość ostat-
niego elementu wycinka. Poprzez dodanie bezpośredniego parametru mamy pewność, że używa-
my wartości zmiennej f, która jest aktualna, gdy wykonywana jest instrukcja go.
A co, jeśli chcemy zwrócić wartości z każdej roboczej funkcji goroutine do głównej? Jeśli wywoła-
nie funkcji thumbnail.ImageFile nie zdoła utworzyć pliku, zwróci błąd. Kolejna wersja funkcji
makeThumbnails zwraca pierwszy błąd, który odbierze z jakiejkolwiek operacji skalowania:
// makeThumbnails4 tworzy równolegle miniaturki z określonych plików obrazów.
// Zwraca błąd, jeśli jakiś etap zawiedzie.
func makeThumbnails4(filenames []string) error {
errors := make(chan error)

for _, f := range filenames {


go func(f string) {
_, err := thumbnail.ImageFile(f)
errors <- err
}(f)
}
8.5. ZAPĘTLENIE RÓWNOLEGŁE 233

for range filenames {


if err := <-errors; err != nil {
return err // UWAGA: nieprawidłowe: wyciek funkcji goroutine!
}
}

return nil
}
Ta funkcja ma subtelną usterkę. Kiedy napotka pierwszy błąd inny niż nil, zwróci ten błąd pod-
miotowi wywołującemu, nie pozostawiając żadnej funkcji goroutine do osuszania kanału errors.
Każda pozostająca robocza funkcja goroutine będzie blokować w nieskończoność, gdy będzie
próbować wysłać wartość na tym kanale, i nigdy nie zakończy działania. Ta sytuacja, czyli wyciek
funkcji goroutine (zob. punkt 8.4.4), może spowodować, że cały program utknie lub wyczerpie się
pamięć.
Najprostszym rozwiązaniem jest użycie kanału buforowanego z wystarczającą pojemnością, aby
żadna robocza funkcja goroutine nie blokowała, gdy będzie wysyłać komunikat. (Alternatywnym
rozwiązaniem jest utworzenie kolejnej funkcji goroutine do osuszania kanału, podczas gdy główna
funkcja goroutine zwraca pierwszy błąd bez opóźnienia).
Kolejna wersja funkcji makeThumbnails wykorzystuje kanał buforowany do zwracania nazw wyge-
nerowanych plików obrazów wraz z ewentualnymi błędami.
// makeThumbnails5 tworzy równolegle miniaturki z określonych plików obrazów.
// Zwraca wygenerowane nazwy plików w dowolnej kolejności lub błąd, jeśli jakiś etap zawiedzie.
func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
type item struct {
thumbfile string
err error
}

ch := make(chan item, len(filenames))


for _, f := range filenames {
go func(f string) {
var it item
it.thumbfile, it.err = thumbnail.ImageFile(f)
ch <- it
}(f)
}

for range filenames {


it := <-ch
if it.err != nil {
return nil, it.err
}
thumbfiles = append(thumbfiles, it.thumbfile)
}

return thumbfiles, nil


}
Nasza ostatnia wersja funkcji makeThumbnails (przedstawiona poniżej) zwraca całkowitą liczbę
bajtów zajmowanych przez nowe pliki. Jednak, w przeciwieństwie do poprzednich wersji, odbiera
nazwy plików nie jako wycinek, ale poprzez kanał łańcuchów znaków, więc nie możemy prze-
widzieć liczby iteracji pętli.
234 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

Aby się dowiedzieć, kiedy zakończy wykonywanie ostatnia funkcja goroutine (niekoniecznie ta, która
została uruchomiona jako ostatnia), musimy zwiększać licznik przed rozpoczęciem każdej funkcji
goroutine i zmniejszać go, gdy każda funkcja goroutine zakończy swoje działanie. Wymaga to spe-
cjalnego rodzaju licznika, którym można bezpiecznie manipulować z wielu funkcji goroutine i który
zapewnia sposób odczekania, aż się wyzeruje. Ten typ licznika jest znany jako sync.WaitGroup,
a poniższy kod pokazuje, jak z niego korzystać:
// makeThumbnails6 tworzy miniaturki dla każdego pliku otrzymanego z kanału.
// Zwraca liczbę bajtów zajmowanych przez utworzone pliki.
func makeThumbnails6(filenames <-chan string) int64 {
sizes := make(chan int64)
var wg sync.WaitGroup // liczba roboczych funkcji goroutine
for f := range filenames {
wg.Add(1)
// Funkcja robocza.
go func(f string) {
defer wg.Done()
thumb, err := thumbnail.ImageFile(f)
if err != nil {
log.Println(err)
return
}
info, _ := os.Stat(thumb) // można ignorować błąd
sizes <- info.Size()
}(f)
}

// Funkcja zamykania.
go func() {
wg.Wait()
close(sizes)
}()

var total int64


for size := range sizes {
total += size
}
return total
}
Należy zwrócić uwagę na asymetrię w metodach Add i Done. Metoda Add, która zwiększa licznik,
musi być wywołana przed rozpoczęciem roboczej funkcji goroutine, a nie wewnątrz niej. W przeciw-
nym razie nie bylibyśmy pewni, czy Add dzieje się przed tym, jak funkcja goroutine zamykania
wywoła Wait. Ponadto metoda Add przyjmuje parametr, ale metoda Done nie. Wywołanie metody
Done jest równoważne z Add(-1). Używamy instrukcji defer, aby się upewnić, że licznik będzie
zmniejszany nawet w przypadku błędu. Struktura powyższego kodu jest powszechnym i idioma-
tycznym wzorcem dla zapętlania równoległego, gdy nie znamy liczby iteracji.
Kanał sizes przenosi rozmiar każdego pliku z powrotem do głównej funkcji goroutine, która
odbiera te rozmiary za pomocą pętli range i oblicza sumę. Należy zwrócić uwagę, w jaki sposób
tworzymy funkcję goroutine zamykania, która zanim zamknie kanał sizes, czeka na zakończenie
roboczych funkcji goroutine. Te dwie operacje (czekania i zamykania) muszą być współbieżne z pętlą
na kanale sizes. Rozważmy alternatywy: jeśli operacja czekania zostałaby umieszczona w głównej
funkcji goroutine przed pętlą, nigdy by się nie skończyła, a jeśli zostałaby umieszczona po pętli,
byłaby nieosiągalna, ponieważ gdyby nic nie zamykało kanału, pętla nigdy nie zostałaby zakończona.
8.6. PRZYKŁAD: WSPÓŁBIEŻNY ROBOT INTERNETOWY 235

Rysunek 8.5 ilustruje sekwencję zdarzeń w funkcji makeThumbnails6. Pionowe linie reprezentują
funkcje goroutine. Cieńsze odcinki wskazują uśpienie, pogrubione odcinki oznaczają aktywność.
Ukośne strzałki wskazują zdarzenia synchronizacji jednej funkcji goroutine z drugą. Oś upływu
czasu jest skierowana w dół. Należy zwrócić uwagę, że główna funkcja goroutine większość
swojego czasu w pętli range spędza w uśpieniu, czekając, aż robocza funkcja goroutine wyśle
wartość lub zamykająca funkcja goroutine zamknie kanał.

Rysunek 8.5. Sekwencja zdarzeń w funkcji makeThumbnails6

Ćwiczenie 8.4. Zmodyfikuj serwer reverb2 w taki sposób, aby używał licznika sync.WaitGroup
dla każdego połączenia w celu obliczenia liczby aktywnych funkcji goroutine echo. Gdy licznik
spadnie do zera, zamknij zapisującą połowę połączenia TCP, tak jak opisano w ćwiczeniu 8.3. Sprawdź,
czy Twój zmodyfikowany klient netcat3 z tego ćwiczenia czeka na końcowe echa wielu jednocze-
snych krzyknięć nawet po zamknięciu standardowego strumienia wejściowego.
Ćwiczenie 8.5. Weź istniejący program sekwencyjny ograniczony mocą obliczeniową procesora,
taki jak program Mandelbrot z podrozdziału 3.3 lub program do obliczania powierzchni 3D z pod-
rozdziału 3.2, i wykonaj jego główną pętlę równolegle z wykorzystaniem kanałów do komunikacji.
O ile szybciej ten program działa na maszynie wieloprocesorowej? Jaka jest optymalna liczba funkcji
goroutine, których należy użyć?

8.6. Przykład: współbieżny robot internetowy


W podrozdziale 5.6 przygotowaliśmy prostego robota internetowego, który badał graf linków
WWW według algorytmu przeszukiwania wszerz. W tym podrozdziale wprowadzimy do niego
współbieżność, aby niezależne wywołania funkcji crawl mogły wykorzystywać równoległość operacji
we-wy dostępną w internecie. Funkcja crawl pozostaje dokładnie taka sama jak w programie
code/r05/findlinks3:
236 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

code/r08/crawl1
func crawl(url string) []string {
fmt.Println(url)
list, err := links.Extract(url)
if err != nil {
log.Print(err)
}
return list
}
Funkcja main przypomina funkcję breadthFirst (zob. podrozdział 5.6). Tak jak wcześniej lista robocza
(worklist) rejestruje kolejkę elementów, które wymagają przetwarzania, a każdy element jest listą
adresów URL do zindeksowania. Jednak tym razem, zamiast reprezentować kolejkę za pomocą
wycinka, użyjemy kanału. Każde wywołanie funkcji crawl ma miejsce w jej własnej funkcji goroutine
i wysyła wykryte linki z powrotem do worklist.
func main() {
worklist := make(chan []string)

// Uruchamiana argumentami wiersza poleceń.


go func() { worklist <- os.Args[1:] }()

// Współbieżne indeksowanie stron internetowych.


seen := make(map[string]bool)
for list := range worklist {
for _, link := range list {
if !seen[link] {
seen[link] = true
go func(link string) {
worklist <- crawl(link)
}(link)
}
}
}
}
Należy zwrócić uwagę, że funkcja goroutine indeksowania przyjmuje link jako bezpośredni para-
metr, aby uniknąć problemu przechwytywania zmiennej pętli, który poznaliśmy w punkcie 5.6.1.
Należy również zwrócić uwagę, że początkowe wysłanie argumentów wiersza poleceń do listy ro-
boczej musi się odbywać we własnej funkcji goroutine, aby uniknąć zakleszczenia, czyli sytuacji
zablokowania, w której zarówno główna funkcja goroutine, jak i funkcja goroutine indeksowania
próbują wysyłać do siebie nawzajem, podczas gdy żadna z nich nie odbiera. Alternatywnym roz-
wiązaniem jest użycie kanału buforowanego.
Nasz robot internetowy jest teraz wysoce współbieżny i wypisuje burzę adresów URL, ale ma dwa
problemy. Pierwszy problem objawia się jako komunikaty błędów w dzienniku po kilku sekun-
dach działania:
$ go build code/r08/crawl1
$ ./crawl1 http://gopl.io/
http://gopl.io/
https://golang.org/help/
https://golang.org/doc/
https://golang.org/blog/
...
2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host
2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: too many open files
...
8.6. PRZYKŁAD: WSPÓŁBIEŻNY ROBOT INTERNETOWY 237

Początkowy komunikat o błędzie jest zaskakującym raportem na temat niepowodzenia wyszuki-


wania DNS dla wiarygodnej domeny. Przyczynę ujawnia kolejny komunikat o błędzie: program
utworzył tak wiele połączeń sieciowych na raz, że przekroczył limit liczby otwartych plików na
proces, powodując niepowodzenia takich operacji jak wyszukiwania DNS i wywołania net.Dial.
Ten program jest zbyt równoległy. Nieograniczona równoległość rzadko jest dobrym pomysłem,
ponieważ w systemie zawsze jest jakiś czynnik ograniczający: liczba rdzeni procesora dla obciążeń
roboczych uzależnionych od mocy obliczeniowej, liczba osi napędowych i głowic dla operacji we-wy
dysku lokalnego, przepustowość sieci dla przesyłania strumieniowego lub możliwości serwowania
usługi sieciowej. Rozwiązaniem jest ograniczenie liczby równoległych wykorzystywań zasobów,
aby dopasować się do dostępnego poziomu równoległości. Prostym sposobem wprowadzenia tego
w naszym przykładzie jest zapewnienie, aby jednocześnie nie było aktywnych więcej niż n wywołań
links.Extract, gdzie n jest odpowiednio mniejsze niż limit deskryptorów plików — załóżmy
20. Jest to analogiczne do sytuacji, w której bramkarz w zatłoczonym nocnym klubie wpuszcza
nowych gości tylko wtedy, gdy jacyś wcześniejsi goście opuszczą lokal.
Możemy ograniczyć równoległość, używając kanału buforowanego o pojemności n do wymode-
lowania kontroli współbieżności zwanej semaforem zliczającym. Koncepcyjnie każde z n wolnych
gniazd w buforze kanału reprezentuje żeton uprawniający jego posiadacza do procedowania.
Wysłanie wartości do kanału powoduje nabycie żetonu, a odebranie wartości z kanału zwalnia
żeton, tworząc nowe wolne gniazdo. Gwarantuje to, że co najwyżej n operacji wysyłania będzie
mogło nastąpić bez rozdzielającej je operacji odbioru. (Chociaż bardziej intuicyjne może być po-
traktowanie jako żetonów zapełnionych gniazd w buforze kanału, użycie wolnych gniazd eliminuje
konieczność zapełnienia bufora kanału po jego utworzeniu). Ponieważ typ elementów kanału nie
jest istotny, użyjemy typu struct{}, który ma rozmiar zerowy.
Przepiszmy funkcję crawl tak, aby wywołanie funkcji links.Extract było umieszczone pomiędzy
operacjami nabywania i zwalniania żetonu, zapewniając w ten sposób, że w tym samym czasie
aktywnych będzie co najwyżej 20 wywołań tej funkcji. Dobrą praktyką jest utrzymywanie operacji
semafora możliwie blisko regulowanych przez nie operacji we-wy.
code/r08/crawl2
// tokens jest semaforem zliczającym wykorzystywanym do wyegzekwowania limitu 20 współbieżnych żądań.
var tokens = make(chan struct{}, 20)

func crawl(url string) []string {


fmt.Println(url)
tokens <- struct{}{} // nabycie żetonu
list, err := links.Extract(url)
<-tokens // zwolnienie żetonu
if err != nil {
log.Print(err)
}
return list
}
Drugim problemem jest to, że program nigdy się nie kończy, nawet gdy wykryje wszystkie linki
osiągalne z początkowych adresów URL. (Oczywiście istnieje małe prawdopodobieństwo, że za-
uważysz ten problem, chyba że starannie wybierzesz początkowe adresy URL lub zaimplementu-
jesz funkcję ograniczania poziomu zagłębienia z ćwiczenia 8.6). Aby program przerwał działanie,
musimy wyjść z pętli głównej, gdy lista robocza jest pusta oraz nie są aktywne żadne funkcje goroutine
indeksowania.
238 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

func main() {
worklist := make(chan []string)
var n int // liczba oczekujących operacji wysłania do worklist

// Uruchamiana argumentami wiersza poleceń.


n++
go func() { worklist <- os.Args[1:] }()

// Współbieżne indeksowanie stron internetowych.


seen := make(map[string]bool)
for ; n > 0; n-- {
list := <-worklist
for _, link := range list {
if !seen[link] {
seen[link] = true
n++
go func(link string) {
worklist <- crawl(link)
}(link)
}
}
}
}
W tej wersji licznik n śledzi liczbę operacji wysyłania do worklist, które mają jeszcze być wykonane.
Za każdym razem, gdy wiemy, że jakiś element musi być przesłany do worklist, zwiększamy n,
raz przed wysłaniem początkowych argumentów wiersza poleceń i ponownie za każdym razem,
gdy uruchamiamy funkcję goroutine indeksowania. Główna pętla kończy się, gdy n spada do zera,
ponieważ nie ma więcej zadań do wykonania.
Teraz nasz współbieżny robot internetowy działa ok. 20 razy szybciej niż oparty na algorytmie
przeszukiwania wszerz robot internetowy z podrozdziału 5.6. Działa przy tym bezbłędnie i zostaje
prawidłowo zakończony, gdy wykona swoje zadanie.
Poniższy program przedstawia alternatywne rozwiązanie problemu nadmiernej współbieżności.
Ta wersja wykorzystuje pierwotną funkcję crawl (która nie ma semafora zliczającego), ale wywo-
łuje ją z jednej z 20 długo żyjących funkcji goroutine indeksowania, zapewniając, że jednocześnie
aktywnych będzie co najwyżej 20 żądań HTTP.
code/r08/crawl3
func main() {
worklist := make(chan []string) // listy adresów URL; mogą mieć duplikaty
unseenLinks := make(chan string) // adresy URL bez duplikatów

// Dodaje do worklist argumenty wiersza poleceń.


go func() { worklist <- os.Args[1:] }()

// Tworzy 20 funkcji goroutine indeksowania, aby pobrać każdy niewidziany jeszcze link.
for i := 0; i < 20; i++ {
go func() {
for link := range unseenLinks {
foundLinks := crawl(link)
go func() { worklist <- foundLinks }()
}
}()
}
8.7. MULTIPLEKSOWANIE ZA POMOCĄ INSTRUKCJI SELECT 239

// Główna funkcja goroutine usuwa duplikaty elementów worklist


// i wysyła niewidziane jeszcze elementy do funkcji indeksowania.
seen := make(map[string]bool)
for list := range worklist {
for _, link := range list {
if !seen[link] {
seen[link] = true
unseenLinks <- link
}
}
}
}
Wszystkie funkcje goroutine indeksowania są zasilane przez ten sam kanał unseenLinks. Główna
funkcja goroutine jest odpowiedzialna za usuwanie duplikatów elementów odbieranych z worklist,
a następnie wysyłanie każdego niewidzianego jeszcze elementu przez kanał unseenLinks do funkcji
goroutine indeksowania.
Mapa seen jest zamknięta wewnątrz głównej funkcji goroutine, co oznacza, że dostęp do niej
może uzyskiwać tylko ta funkcja goroutine. Podobnie jak inne formy ukrywania informacji zamy-
kanie pomaga nam zrozumieć poprawność konstrukcji programu. Zmienne lokalne nie mogą być
np. wymieniane z nazwy poza funkcją, w której są zadeklarowane. Do zmiennych, które nie uciekają
(zob. punkt 2.3.4) z funkcji, nie można uzyskać dostępu spoza tej funkcji, a do zhermetyzowanych
pól obiektu dostęp mogą uzyskiwać tylko metody tego obiektu. We wszystkich przypadkach ukry-
wanie informacji pomaga ograniczyć niezamierzone interakcje między częściami programu.
Linki znalezione przez funkcję crawl są wysyłane do worklist z dedykowanej funkcji goroutine,
aby uniknąć zakleszczenia.
Aby zaoszczędzić miejsce, w tym przykładzie nie zajmowaliśmy się problemem zakończenia działa-
nia programu.
Ćwiczenie 8.6. Dodaj do współbieżnego robota internetowego funkcję ograniczania poziomu za-
głębienia. Oznacza to, że jeśli użytkownik ustawi -depth=3, wtedy pobierane będą tylko te adresy
URL, które są osiągalne przez co najwyżej trzy linki.
Ćwiczenie 8.7. Napisz współbieżny program, który tworzy lokalną lustrzaną kopię strony inter-
netowej, pobierając każdą osiągalną stronę i zapisując ją w katalogu na dysku lokalnym. Pobierane
powinny być tylko strony zawierające się w oryginalnej domenie (np. golang.org). W razie potrzeby
adresy URL w lustrzanych stronach powinny zostać zmienione w taki sposób, aby odwoływały się
do strony lustrzanej, a nie do oryginalnej.

8.7. Multipleksowanie za pomocą instrukcji select


Poniższy program przeprowadza odliczanie do momentu odpalenia rakiety. Funkcja time.Tick
zwraca kanał, na którym program periodycznie wysyła zdarzenia, działając jak metronom.
Wartością każdego zdarzenia jest znacznik czasu, ale rzadko jest to tak interesujące jak fakt dorę-
czenia samego zdarzenia.
code/r08/countdown1
func main() {
fmt.Println("Rozpoczynam odliczanie.")
tick := time.Tick(1 * time.Second)
240 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

for countdown := 10; countdown > 0; countdown-- {


fmt.Println(countdown)
<-tick
}
launch()
}
Teraz dodajemy możliwość przerwania sekwencji odpalania poprzez naciśnięcie podczas odlicza-
nia przycisku Enter. Najpierw uruchamiamy funkcję goroutine, która próbuje odczytać pojedynczy
bajt ze standardowego strumienia wejściowego, a jeśli ta próba się powiedzie, wysyła wartość na
kanale o nazwie abort.
code/r08/countdown2
abort := make(chan struct{})
go func() {
os.Stdin.Read(make([]byte, 1)) // odczyt pojedynczego bajtu
abort <- struct{}{}
}()
Teraz każda iteracja pętli odliczania musi czekać na zdarzenie, które ma dotrzeć na jednym z dwóch
kanałów: na kanale tickera, jeśli wszystko jest w porządku („nominalnie” w żargonie NASA), lub
na kanale przerwania (abort), jeśli wydarzyła się jakaś „anomalia”. Nie możemy po prostu odbierać
z każdego kanału, ponieważ którąkolwiek z operacji spróbujemy wykonać najpierw, będzie ona
blokować aż do zakończenia. Musimy zmultipleksować te operacje, a w tym celu potrzebujemy
instrukcji select.
select {
case <-ch1:
// …
case x := <-ch2:
// …użycie x…
case ch3 <- y:
// …
default:
// …
}
Ogólna postać instrukcji select została przedstawiona powyżej. Podobnie jak instrukcja switch
ma ona kilka różnych przypadków (case) oraz opcjonalny przypadek domyślny (default). Każdy
przypadek określa komunikację (operację wysyłania lub odbierania na jakimś kanale) i powiązany
blok instrukcji. Wyrażenie odbierania może występować samodzielnie, tak jak w pierwszym
przypadku, lub w krótkiej deklaracji zmiennych, tak jak w drugim przypadku. Druga forma po-
zwala odwoływać się do odebranej wartości.
Instrukcja select czeka, aż komunikacja dla jakiegoś przypadku będzie gotowa do procedowania.
Następnie przeprowadzana jest ta komunikacja i wykonywane są powiązane instrukcje danego
przypadku. Pozostałe komunikacje nie są przeprowadzane. Instrukcja select bez przypadków,
zapisywana jako select{}, czeka w nieskończoność.
Wróćmy do naszego programu odpalania rakiety. Funkcja time.After natychmiast zwraca kanał
i uruchamia nową funkcję goroutine, która po określonym czasie wysyła przez ten kanał pojedynczą
wartość. Poniższa instrukcja select czeka, aż przybędzie pierwsze z dwóch zdarzeń: zdarzenie
przerwania lub zdarzenie wskazujące, że upłynęło 10 sekund. Jeśli 10 sekund upłynie bez przerwania,
przeprowadzane jest odpalanie.
8.7. MULTIPLEKSOWANIE ZA POMOCĄ INSTRUKCJI SELECT 241

func main() {
// …tworzenie kanału abort…

fmt.Println("Rozpoczynam odliczanie. Aby przerwać, wciśnij Enter.")


select {
case <-time.After(10 * time.Second):
// Nic nie rób.
case <-abort:
fmt.Println("Odpalanie przerwane!")
return
}
launch()
}
Przykład zamieszczony poniżej jest bardziej subtelny. Kanał ch, którego rozmiar bufora wynosi l,
jest na przemian pusty i pełny, więc procedować może tylko jeden z przypadków: wysyłanie, gdy
i jest parzyste, lub odbieranie, gdy i jest nieparzyste. Zawsze wyświetla 0 2 4 6 8.
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x) // "0" "2" "4" "6" "8"
case ch <- i:
}
}
Jeśli gotowych jest kilka przypadków, instrukcja select wybiera losowo jeden z nich, co gwaran-
tuje, że każdy kanał ma równe szanse na wybranie. Zwiększenie rozmiaru bufora w poprzednim
przykładzie sprawia, że jego dane wyjściowe stają się niedeterministyczne, ponieważ gdy bufor
nie jest ani pełny, ani pusty, instrukcja select w przenośni rzuca monetą.
Sprawmy, żeby nasz program odpalania rakiety wyświetlał odliczanie. Poniższa instrukcja select
powoduje, że każda iteracja pętli czeka na przerwanie do 1 sekundy, ale nie dłużej.
code/r08/countdown3
func main() {
// …tworzenie kanału abort…

fmt.Println("Rozpoczynam odliczanie. Aby przerwać, wciśnij Enter.")


tick := time.Tick(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select {
case <-tick:
// Nic nie rób.
case <-abort:
fmt.Println("Odpalanie przerwane!")
return
}
}
launch()
}
Funkcja time.Tick zachowuje się tak, jakby tworzyła funkcję goroutine, która wywołuje time.Sleep
w pętli, wysyłając zdarzenie za każdym razem, gdy się przebudzi. Gdy powyższa funkcja odliczania
powraca z wykonywania, przestaje odbierać zdarzenia z kanału tick, ale funkcja goroutine tickera
nadal działa, próbując bezskutecznie wysyłać na kanale, z którego nie odbiera żadna funkcja
goroutine — ma miejsce wyciek funkcji goroutine (zob. punkt 8.4.4).
242 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

Funkcja Tick jest wygodna, ale jest właściwa tylko wtedy, gdy ticki będą potrzebne przez cały czas
życia aplikacji. W przeciwnym razie powinniśmy użyć tego wzorca:
ticker := time.NewTicker(1 * time.Second)
<-ticker.C // odbieranie z kanału tickera
ticker.Stop() // powoduje zakończenie działania funkcji goroutine tickera
Czasami chcemy próbować wykonywać operację wysyłania lub odbierania na kanale, ale unikać
blokowania, jeśli kanał nie jest gotowy. Nazywa się to komunikacją nieblokującą. Robić to może
również instrukcja select. Instrukcja select może mieć przypadek domyślny (default), który
określa, co zrobić, gdy żadna z pozostałych komunikacji nie może być natychmiast wykonana.
Poniższa instrukcja select odbiera wartość z kanału abort, jeśli jest jakaś wartość do odebrania.
W przeciwnym razie nic nie robi. Jest to nieblokująca operacja odbierania. Wykonywanie tego
wielokrotnie jest nazywane odpytywaniem kanału.
select {
case <-abort:
fmt.Printf("Odpalanie przerwane!\n")
return
default:
// Nie rób nic.
}
Wartością zerową dla kanału jest nil. Być może jest to zaskakujące, ale kanały nil są czasem
przydatne. Ponieważ operacje wysyłania i odbierania na kanale nil blokują w nieskończoność,
przypadek w instrukcji select, którego kanałem jest nil, nigdy nie jest wybierany. Pozwala nam to
użyć wartości nil do włączania lub wyłączania przypadków odpowiadających funkcjom takim
jak: obsługa limitów czasu lub anulowania, odpowiadanie na inne zdarzenia wejściowe lub
emitowanie danych wyjściowych. Przykład zobaczymy w następnym podrozdziale.
Ćwiczenie 8.8. Wykorzystując instrukcję select, dodaj limit czasu do serwera echo z podroz-
działu 8.3, aby rozłączał każdego klienta, który nic nie krzyknie w ciągu 10 sekund.

8.8. Przykład: współbieżna trawersacja katalogów


W tym podrozdziale zbudujemy program, który raportuje użycie dysku dla jednego katalogu
lub kilku katalogów określonych w wierszu poleceń tak jak uniksowe polecenie du. Większość
pracy tego programu jest wykonywana przez poniższą funkcję walkDir, która enumeruje wpisy
katalogu dir, wykorzystując funkcję pomocniczą dirents.
code/r08/du1
// walkDir rekurencyjnie przechodzi drzewo plików zakorzenione w dir
// i wysyła rozmiar każdego znalezionego pliku przez kanał fileSizes.
func walkDir(dir string, fileSizes chan<- int64) {
for _, entry := range dirents(dir) {
if entry.IsDir() {
subdir := filepath.Join(dir, entry.Name())
walkDir(subdir, fileSizes)
} else {
fileSizes <- entry.Size()
}
}
}
8.8. PRZYKŁAD: WSPÓŁBIEŻNA TRAWERSACJA KATALOGÓW 243

// dirents zwraca wpisy katalogu dir.


func dirents(dir string) []os.FileInfo {
entries, err := ioutil.ReadDir(dir)
if err != nil {
fmt.Fprintf(os.Stderr, "du1: %v\n", err)
return nil
}
return entries
}
Funkcja ioutil.ReadDir zwraca wycinek typu os.FileInfo — tę samą informację, którą wywołanie
os.Stat zwraca dla pojedynczego pliku. Dla każdego podkatalogu funkcja walkDir rekurencyjnie
wywołuje samą siebie, a dla każdego pliku walkDir wysyła komunikat na kanale fileSizes. Ten
komunikat jest rozmiarem danego pliku w bajtach.
Pokazana poniżej funkcja main wykorzystuje dwie funkcje goroutine. Funkcja goroutine działająca
w tle wywołuje walkDir dla każdego katalogu określonego w wierszu poleceń i na koniec zamyka
kanał fileSizes. Główna funkcja goroutine oblicza sumę rozmiarów plików odbieranych z tego
kanału i na koniec wypisuje wartość całkowitą.
// Du1 oblicza wykorzystanie dysku przez pliki w katalogu.
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
func main() {
// Określa początkowe katalogi.
flag.Parse()
roots := flag.Args()
if len(roots) == 0 {
roots = []string{"."}
}
// Trawersuje drzewo plików.
fileSizes := make(chan int64)
go func() {
for _, root := range roots {
walkDir(root, fileSizes)
}
close(fileSizes)
}()
// Wyświetla wyniki.
var nfiles, nbytes int64
for size := range fileSizes {
nfiles++
nbytes += size
}
printDiskUsage(nfiles, nbytes)
}
func printDiskUsage(nfiles, nbytes int64) {
fmt.Printf("%d plików %.1f GB\n", nfiles, float64(nbytes)/1e9)
}
244 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

Ten program pauzuje przez dłuższą chwilę przed wyświetleniem wyniku:


$ go build code/r08/du1
$ ./du1 $HOME /usr /bin /etc
213201 plików 62.7 GB
Program byłby przyjemniejszy, gdyby informował o postępie. Jednak przeniesienie po prostu wy-
wołania printDiskUsage do pętli spowodowałoby wypisywanie tysięcy linii danych wyjściowych.
Poniższy wariant polecenia du wypisuje sumy periodycznie, ale tylko wtedy, gdy określona jest
flaga -v, ponieważ nie wszyscy użytkownicy będą chcieli widzieć komunikaty o postępie. Funkcja
goroutine w tle, która wykonuje pętle przez roots, pozostaje niezmieniona. Główna funkcja goroutine
używa teraz tickera do generowania zdarzeń co 500 milisekund i spowodowania, że instrukcja
select będzie czekać na komunikat o rozmiarze pliku (w tym przypadku aktualizuje sumy) lub na
zdarzenie tickowe (w tym przypadku wypisuje bieżące sumy). Jeśli flaga -v nie jest określona, kanał
tick pozostaje nil, a jego przypadek w instrukcji select jest w efekcie wyłączony.
code/r08/du2
var verbose = flag.Bool("v", false, "pokazuje rozszerzone komunikaty postępu")

func main() {
// …uruchamianie funkcji goroutine działającej w tle…

// Wyświetla wyniki periodycznie.


var tick <-chan time.Time
if *verbose {
tick = time.Tick(500 * time.Millisecond)
}
var nfiles, nbytes int64
loop:
for {
select {
case size, ok := <-fileSizes:
if !ok {
break loop // kanał fileSizes został zamknięty
}
nfiles++
nbytes += size
case <-tick:
printDiskUsage(nfiles, nbytes)
}
}
printDiskUsage(nfiles, nbytes) // końcowe sumy
}
Ponieważ ten program nie używa już pętli range, pierwszy przypadek instrukcji select musi bez-
pośrednio sprawdzać, czy kanał fileSizes został zamknięty, wykorzystując dwuwynikową formę
operacji odbierania. Jeśli kanał został zamknięty, program wychodzi z pętli. Oznaczona etykietą
instrukcja break wychodzi zarówno z pętli select, jak i z pętli for. Instrukcja break nieoznaczona
etykietą przerwałaby tylko select, powodując rozpoczęcie kolejnej iteracji pętli.
Program daje nam teraz spokojny strumień aktualizacji:
$ go build code/r08/du2
$ ./du2 -v $HOME /usr /bin /etc
28608 plików 8.3 GB
54147 plików 10.3 GB
93591 plików 15.1 GB
8.8. PRZYKŁAD: WSPÓŁBIEŻNA TRAWERSACJA KATALOGÓW 245

127169 plików 52.9 GB


175931 plików 62.2 GB
213201 plików 62.7 GB
Wciąż jednak jego ukończenie trwa zbyt długo. Nie ma powodu, dla którego wszystkie wywołania
funkcji walkDir nie mogłyby być wykonywane współbieżnie, wykorzystując w ten sposób równo-
ległość w systemie dyskowym. Pokazana poniżej trzecia wersja polecenia du tworzy nową funkcję
goroutine dla każdego wywołania walkDir. Wykorzystuje ona licznik sync.WaitGroup (zob.
podrozdział 8.5) do policzenia liczby nadal aktywnych wywołań walkDir oraz zamykającą funkcję
goroutine do zamknięcia kanału fileSizes, gdy licznik spadnie do zera.
code/r08/du3
func main() {
// …określenie korzeni…

// Trawersuje każdy korzeń drzewa plików równolegle.


fileSizes := make(chan int64)
var n sync.WaitGroup
for _, root := range roots {
n.Add(1)
go walkDir(root, &n, fileSizes)
}
go func() {
n.Wait()
close(fileSizes)
}()
// …pętla select…
}

func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {


defer n.Done()
for _, entry := range dirents(dir) {
if entry.IsDir() {
n.Add(1)
subdir := filepath.Join(dir, entry.Name())
go walkDir(subdir, n, fileSizes)
} else {
fileSizes <- entry.Size()
}
}
}
Ponieważ ten program tworzy wiele tysięcy funkcji goroutine podczas szczytowego obciążenia,
musimy zmienić funkcję dirents w taki sposób, aby używała semafora zliczającego w celu unie-
możliwienia otwierania zbyt wielu plików na raz, tak jak zrobiliśmy dla robota internetowego
w podrozdziale 8.6:
// sema jest semaforem zliczającym służącym do ograniczania współbieżności w funkcji dirents.
var sema = make(chan struct{}, 20)

// dirents zwraca wpisy katalogu dir.


func dirents(dir string) []os.FileInfo {
sema <- struct{}{} // nabycie żetonu
defer func() { <-sema }() // zwolnienie żetonu
// …
Ta wersja działa kilka razy szybciej niż poprzednia, chociaż istnieje duża zmienność w zależności
od systemu.
246 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

Ćwiczenie 8.9. Napisz wersję programu du, która oblicza i periodycznie wyświetla oddzielne sumy
dla każdego z katalogów root.

8.9. Anulowanie
Czasami musimy poinstruować funkcję goroutine, aby przestała wykonywać swoje zadanie, np.
w serwerze WWW przeprowadzającym obliczenia dla klienta, który został rozłączony.
Nie ma sposobu na to, aby jedna funkcja goroutine bezpośrednio zatrzymywała inną, ponieważ
ta pozostawiłaby swoje wszystkie współdzielone zmienne w niezdefiniowanym stanie. W programie
odpalania rakiety (zob. podrozdział 8.7) wysyłaliśmy poprzez kanał o nazwie abort pojedynczą wartość,
którą funkcja goroutine odliczania interpretowała jako żądanie, żeby się zatrzymała. Ale co, jeśli
będziemy potrzebowali anulować dwie funkcje goroutine lub dowolną ich liczbę?
Jedną z możliwości może być wysłanie na kanale abort tylu zdarzeń, ile jest funkcji goroutine do
anulowania. Jeśli jednak któreś z funkcji goroutine same już zakończyły swoje działanie, nasz
licznik będzie zbyt duży, a operacje wysyłania utkną. Z drugiej strony, jeśli te funkcje goroutine
uruchomiły inne funkcje goroutine, nasz licznik będzie zbyt mały i niektóre funkcje goroutine
pozostaną nieświadome anulowania. Zasadniczo trudno jest się dowiedzieć, ile funkcji goroutine
pracuje w naszym imieniu w danym momencie. Ponadto gdy funkcja goroutine odbiera wartość
z kanału abort, konsumuje tę wartość, aby inne funkcje goroutine jej nie zobaczyły. W przypadku
anulowania potrzebujemy niezawodnego mechanizmu do rozgłaszania (ang. broadcast) zdarzenia
poprzez kanał, aby wiele funkcji goroutine mogło zobaczyć je, gdy się pojawi, i później widzieć,
że miało miejsce.
Przypomnijmy, że po zamknięciu kanału i osuszeniu wszystkich wysłanych wartości kolejne operacje
odbierania są procedowane od razu, dając wartości zerowe. Możemy to wykorzystać do utworzenia
mechanizmu rozgłaszania: nie wysyłaj wartości na tym kanale, zamknij go.
Do programu du z poprzedniego podrozdziału możemy dodać anulowanie za pomocą kilku
prostych zmian. Najpierw tworzymy kanał anulowania, na którym nigdy nie są wysyłane żadne
wartości, ale którego zamknięcie wskazuje, że nadszedł czas, aby program przestał wykonywać
swoje zadanie. Definiujemy również funkcję narzędziową cancelled, która sprawdza lub, inaczej
mówiąc: odpytuje stan anulowania w momencie, gdy zostaje wywołana.
code/r08/du4
var done = make(chan struct{})

func cancelled() bool {


select {
case <-done:
return true
default:
return false
}
}
Następnie tworzymy funkcję goroutine odczytującą ze standardowego strumienia wejściowego,
który jest zazwyczaj podłączony do terminala. Z chwilą odczytania danych wejściowych (np. gdy
użytkownik wciśnie przycisk Enter) ta funkcja goroutine rozgłasza anulowanie poprzez zamknięcie
kanału done.
8.9. ANULOWANIE 247

// Anulowanie trawersacji, gdy wykryte zostaną dane wyjściowe.


go func() {
os.Stdin.Read(make([]byte, 1)) // odczyt pojedynczego bajtu
close(done)
}()
Teraz musimy sprawić, aby nasza funkcja goroutine reagowała na anulowanie. W głównej funkcji
goroutine dodajemy w instrukcji select trzeci przypadek, który próbuje odbierać z kanału done.
Ta funkcja powraca, jeśli ten przypadek zostanie kiedykolwiek wybrany, ale zanim funkcja powróci,
musi najpierw osuszyć kanał fileSizes, odrzucając wszystkie wartości, aż do momentu zamknięcia
kanału. Robi to w celu zapewnienia, że wszystkie aktywne wywołania funkcji walkDir będą mogły
dobiec do końca bez utknięcia podczas wysyłania do kanału fileSizes.
for {
select {
case <-done:
// Osuszanie kanału fileSizes, aby umożliwić dokończenie wykonywania istniejącym funkcjom
goroutine.
for range fileSizes {
// Nie rób nic.
}
return
case size, ok := <-fileSizes:
// …
}
}
Funkcja goroutine walkDir na początku swojego działania odpytuje status anulowania i powraca
z wykonywania bez przeprowadzenia jakiegokolwiek działania, jeśli ten status jest ustawiony. Powo-
duje to, że wszystkie funkcje goroutine utworzone po anulowaniu działają jak instrukcja pusta:
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
defer n.Done()
if cancelled() {
return
}
for _, entry := range dirents(dir) {
// …
}
}
Korzystne może być ponowne odpytywanie stanu anulowania w ramach pętli walkDir, aby uniknąć
tworzenia funkcji goroutine po zdarzeniu anulowania. Anulowanie wiąże się z pewnym kompromi-
sem. Szybsza reakcja często wymaga bardziej inwazyjnych zmian w logice programu. Zapewnienie,
że żadne kosztowne operacje nie będą nigdy miały miejsca po zdarzeniu anulowania, może wy-
magać uaktualnienia wielu fragmentów kodu, ale często większość z tych korzyści można uzyskać,
sprawdzając anulowanie w kilku ważnych miejscach.
Profilowanie tego programu wykazało, że wąskim gardłem było nabycie żetonu semafora w funkcji
dirents. Poniższa instrukcja select sprawia, że ta operacja może być anulowana, i zmniejsza
typowe opóźnienie anulowania programu z kilkuset do kilkudziesięciu milisekund:
func dirents(dir string) []os.FileInfo {
select {
case sema <- struct{}{}: // nabycie żetonu
case <-done:
return nil // anulowane
}
248 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

defer func() { <-sema }() // zwolnienie żetonu

// …odczyt katalogu…
}
Teraz, gdy będzie miało miejsce anulowanie, wszystkie funkcje goroutine działające w tle szybko
się zatrzymają, a funkcja main powróci z wykonywania. Oczywiście gdy funkcja main powraca,
program zostaje zamknięty, więc może być trudno odróżnić funkcję main, która sprząta po sobie,
od tej, która tego nie robi. Istnieje przydatna sztuczka, której możemy użyć podczas testowania.
Zamiast powracać z funkcji main w przypadku anulowania, wykonujemy wywołanie panic,
a wtedy środowisko wykonawcze wykona zrzut stosu każdej funkcji goroutine w programie. Jeżeli
główna funkcja goroutine jest jedyną, jaka pozostała, wtedy musi posprzątać po sobie. Ale jeśli
pozostają inne funkcje goroutine, być może nie zostały one właściwie anulowane lub może zostały
anulowane, ale wymaga to czasu. Przeprowadzenie niewielkiego dochodzenia może się opłacać.
Zrzut z paniki często zawiera informacje wystarczające do odróżnienia tych przypadków.
Ćwiczenie 8.10. Żądania HTTP mogą być anulowane poprzez zamknięcie opcjonalnego kanału
Cancel w strukturze http.Request. Zmodyfikuj program robota internetowego z podrozdziału 8.6,
aby obsługiwał anulowanie. Podpowiedź: złożona funkcja http.Get nie daje możliwości dostosowania
struktury Request. Zamiast tego możemy utworzyć żądanie za pomocą funkcji http.NewRequest,
ustawić jego pole Cancel, a następnie wykonać żądanie poprzez wywołanie http.DefaultClient.
Do(req).
Ćwiczenie 8.11. Wykorzystując podejście zastosowane w funkcji mirroredQuery z punktu 8.4.4,
zaimplementuj wariant programu fetch, który wysyła żądania kilku adresów URL jednocześnie.
Gdy tylko otrzymana zostanie pierwsza odpowiedź, anuluj pozostałe żądania.

8.10. Przykład: serwer czatu


Zakończymy ten rozdział serwerem czatu, który pozwala wielu użytkownikom rozgłaszać komuni-
katy tekstowe do siebie nawzajem. W tym programie istnieją cztery rodzaje funkcji goroutine. Mamy
po jednej instancji funkcji goroutine main i broadcaster, a dla każdego połączenia klienta istnieje
jedna funkcja goroutine handleConn i jedna clientWriter. Funkcja rozgłaszania (broadcaster) jest
dobrym przykładem wykorzystywania instrukcji select, ponieważ musi reagować na trzy różne
rodzaje komunikatów.
Zadaniem pokazanej poniżej głównej funkcji goroutine jest nasłuchiwanie i akceptowanie przy-
chodzących połączeń sieciowych od klientów. Dla każdego z połączeń tworzy ona nową funkcję
goroutine handleConn, podobnie jak we współbieżnym serwerze echo przedstawionym na początku
tego rozdziału.
code/r08/chat
func main() {
listener, err := net.Listen("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}

go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
8.10. PRZYKŁAD: SERWER CZATU 249

log.Print(err)
continue
}
go handleConn(conn)
}
}
Następna jest funkcja rozgłaszania (broadcaster). Jej lokalna zmienna clients rejestruje aktualny
zbiór podłączonych klientów. Jedyną informacją rejestrowaną na temat każdego klienta jest tożsa-
mość jego kanału komunikatów wychodzących, o czym więcej powiemy później.
type client chan<- string // kanał komunikatów wychodzących

var (
entering = make(chan client)
leaving = make(chan client)
messages = make(chan string) // wszystkie przychodzące komunikaty klientów
)

func broadcaster() {
clients := make(map[client]bool) // wszystkie podłączone klienty
for {
select {
case msg := <-messages:
// Rozgłasza przychodzące komunikaty do kanałów
// komunikatów wychodzących wszystkich klientów.
for cli := range clients {
cli <- msg
}

case cli := <-entering:


clients[cli] = true

case cli := <-leaving:


delete(clients, cli)
close(cli)
}
}
}
Funkcja rozgłaszająca nasłuchuje na globalnych kanałach entering i leaving ogłoszeń przybywają-
cych i odchodzących klientów. Po otrzymaniu jednego z tych zdarzeń aktualizuje zbiór clients,
a jeśli zdarzeniem było odejście klienta, zamyka jego kanał komunikatów wychodzących. Funkcja
rozgłaszania nasłuchuje również zdarzeń na globalnym kanale messages, do którego każdy klient
wysyła wszystkie swoje komunikaty przychodzące. Gdy funkcja rozgłaszająca otrzymuje jedno z tych
zdarzeń, rozgłasza dany komunikat do każdego podłączonego klienta.
Przyjrzyjmy się teraz funkcjom goroutine uruchamianym dla każdego klienta. Funkcja handleConn
tworzy nowy kanał komunikatów wychodzących dla swojego klienta i ogłasza funkcji rozgłaszającej
przez kanał entering przybycie tego klienta. Następnie odczytuje każdy wiersz tekstu od klienta, wy-
syłając go do funkcji rozgłaszającej poprzez globalny kanał komunikatów przychodzących i poprze-
dzając każdy komunikat identyfikatorem jego nadawcy. Gdy nie ma nic więcej do odczytania od
klienta, funkcja handleConn ogłasza na kanale leaving odejście klienta i zamyka połączenie.
func handleConn(conn net.Conn) {
ch := make(chan string) // wychodzące komunikaty klienta
go clientWriter(conn, ch)
250 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY

who := conn.RemoteAddr().String()
ch <- "Jesteś " + who
messages <- who + " przybył"
entering <- ch

input := bufio.NewScanner(conn)
for input.Scan() {
messages <- who + ": " + input.Text()
}
// UWAGA: ignorowanie potencjalnych błędów z input.Err().

leaving <- ch
messages <- who + " odszedł"
conn.Close()
}

func clientWriter(conn net.Conn, ch <-chan string) {


for msg := range ch {
fmt.Fprintln(conn, msg) // UWAGA: ignorowanie błędów sieciowych
}
}
Ponadto handleConn tworzy dla każdego klienta funkcję goroutine clientWriter, która odbiera
komunikaty rozgłaszane do kanału komunikatów wychodzących tego klienta i wypisuje je do jego
połączenia sieciowego. Pętla funkcji zapisującej klienta zostaje zakończona, gdy funkcja rozgłaszająca
zamyka kanał po otrzymaniu powiadomienia leaving.
Poniższy listing przedstawia ten serwer w akcji z dwoma klientami w osobnych oknach na tym
samym komputerze, używających do rozmowy klienta netcat:
$ go build code/r08/chat
$ go build code/r08/netcat3
$ ./chat &
$ ./netcat3
Jesteś 127.0.0.1:64208 $ ./netcat3
127.0.0.1:64211 przybył Jesteś 127.0.0.1:64211
Cześć!
127.0.0.1:64208: Cześć! 127.0.0.1:64208: Cześć!
No cześć.
127.0.0.1:64211: No cześć. 127.0.0.1:64211: No cześć.
^C
127.0.0.1:64208 odszedł
$ ./netcat3
Jesteś 127.0.0.1:64216 127.0.0.1:64216 przybył
Witam.
127.0.0.1:64211: Witam. 127.0.0.1:64211: Witam.
^C
127.0.0.1:64211 odszedł
Ten program, hostując sesję czatu dla n klientów, uruchamia 2n+2 współbieżnych funkcji goroutine
komunikacji, a mimo to nie potrzebuje bezpośrednich operacji blokowania (zob. podrozdział 9.2).
Mapa clients jest zamknięta w pojedynczej funkcji goroutine (broadcaster), więc nie można do niej
uzyskiwać równoległego dostępu. Jedynymi zmiennymi, które są współdzielone przez wiele funkcji
goroutine, są kanały i instancje net.Conn, i obie te zmienne są współbieżnie bezpieczne. Więcej in-
formacji na temat zamykania, bezpieczeństwa współbieżności oraz implikacji współdzielenia zmien-
nych między funkcjami goroutine przedstawimy w następnym rozdziale.
8.10. PRZYKŁAD: SERWER CZATU 251

Ćwiczenie 8.12. Zaimplementuj w funkcji broadcaster, aby ogłaszała aktualny zbiór klientów
każdemu nowo przybyłemu klientowi. Wymaga to, aby zbiór clients oraz kanały entering
i leaving rejestrowały również nazwę klienta.
Ćwiczenie 8.13. Zaimplementuj, aby serwer czatu rozłączał bezczynne klienty, np. te, które nie wy-
słały żadnych komunikatów przez ostatnie 5 minut. Podpowiedź: wywołanie conn.Close() w innej
funkcji goroutine odblokowuje aktywne wywołania Read, tak jak to wykonywane przez input.Scan().
Ćwiczenie 8.14. Zmień protokół sieciowy serwera czatu w taki sposób, aby każdy wchodzący klient
zapewniał swoją nazwę. Użyj tej nazwy zamiast adresu sieciowego podczas poprzedzania każdego
komunikatu tożsamością jego nadawcy.
Ćwiczenie 8.15. Jeśli któremukolwiek programowi klienckiemu nie powiedzie się odczytanie danych
w odpowiednim czasie, ostatecznie utkną wszystkie klienty. Zmodyfikuj funkcję rozgłaszającą, aby
pomijała komunikat zamiast czekać, jeśli funkcja zapisująca klienta nie jest gotowa go przyjąć.
Alternatywnie dodaj buforowanie do kanału komunikatów wychodzących każdego klienta, aby
większość komunikatów nie była porzucana. Funkcja rozgłaszająca powinna używać niebloku-
jącej operacji wysyłania do tego kanału.
252 ROZDZIAŁ 8. FUNKCJE GOROUTINE I KANAŁY
Rozdział 9

Współbieżność
ze współdzieleniem zmiennych

W poprzednim rozdziale przedstawiliśmy kilka programów, które używają funkcji goroutine i kana-
łów do wyrażania współbieżności w sposób bezpośredni i naturalny. Robiąc to, prześlizgnęliśmy się
jednak po wielu ważnych i delikatnych kwestiach, o których programiści muszą pamiętać podczas
pisania współbieżnego kodu.
W tym rozdziale przyjrzymy się bliżej mechanizmom współbieżności. W szczególności zwrócimy
uwagę na kilka problemów związanych ze współdzieleniem zmiennych przez wiele funkcji goroutine,
na techniki analityczne służące do rozpoznawania tych problemów oraz na wzorce do ich rozwiązy-
wania. Na koniec wyjaśnimy niektóre różnice techniczne między funkcjami goroutine i wątkami
systemu operacyjnego.

9.1. Sytuacje wyścigu


W programie sekwencyjnym, czyli posiadającym tylko jedną funkcję goroutine, etapy działania
występują w znanym porządku wykonywania określonym przez logikę programu. Przykładowo:
w sekwencji instrukcji pierwsza z nich ma miejsce przed drugą itd. W programie z dwiema
funkcjami goroutine lub większą ich liczbą etapy w każdej funkcji goroutine wykonywane są
w znanej kolejności, ale zasadniczo nie wiemy, czy zdarzenie x w jednej funkcji goroutine dzieje się
przed zdarzeniem y w innej funkcji goroutine, po tym zdarzeniu, czy równocześnie z nim. Gdy nie
możemy z przekonaniem powiedzieć, że jedno zdarzenie dzieje się przed innym, wtedy zdarzenia
x i y są współbieżne.
Rozważmy funkcję, która działa prawidłowo w programie sekwencyjnym. Ta funkcja jest współ-
bieżnie bezpieczna, jeśli będzie nadal działać poprawnie nawet wtedy, gdy zostanie wywołana
współbieżnie, czyli z co najmniej dwóch funkcji goroutine bez dodatkowej synchronizacji. Można
uogólnić tę myśl w kontekście zbioru współpracujących funkcji, takich jak metody i operacje
określonego typu. Typ jest współbieżnie bezpieczny, jeżeli wszystkie jego dostępne metody i ope-
racje są współbieżnie bezpieczne.
Możemy uczynić program współbieżnie bezpiecznym bez uczynienia współbieżnie bezpiecznym
każdego typu konkretnego w tym programie. W rzeczywistości typy współbieżnie bezpieczne są
raczej wyjątkiem niż regułą, więc należy uzyskiwać dostęp do zmiennej współbieżnie tylko wtedy,
254 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

gdy dokumentacja dla jej typu wskazuje, że jest to bezpieczne. Unikamy współbieżnego dostępu
do większości zmiennych przez zamykanie ich w pojedynczej funkcji goroutine lub utrzymywanie
wyższego poziomu niezmiennika wzajemnego wykluczania. Wyjaśnimy te terminy nieco dalej
w tym rozdziale.
W przeciwieństwie do tego oczekuje się, że eksportowane funkcje poziomu pakietu będą na ogół
współbieżnie bezpieczne. Ponieważ zmienne poziomu pakietu nie mogą być zamykane w poje-
dynczej funkcji goroutine, modyfikujące je funkcje muszą egzekwować wzajemne wykluczanie.
Istnieje wiele powodów, dla których funkcja może nie działać, gdy zostanie wywołana współbieżnie,
w tym: zakleszczenie (ang. deadlock), livelock i zagłodzenie (ang. resource starvation). Nie mamy miej-
sca, aby omówić je wszystkie, więc skoncentrujemy się na najważniejszym, czyli sytuacji wyścigu
(ang. race condition).
Wyścigiem nazywamy sytuację, w której program nie daje poprawnego wyniku dla niektórych prze-
platań operacji wielu funkcji goroutine. Sytuacje wyścigu są szkodliwe, ponieważ mogą pozostawać
w programie w formie utajonej i pojawiają się rzadko, prawdopodobnie tylko przy dużym obciążeniu
lub podczas korzystania z niektórych kompilatorów, platform lub architektur. Z tego powodu trudno
je odtworzyć i zdiagnozować.
Zazwyczaj objaśnia się powagę sytuacji wyścigu poprzez metaforę finansowej straty, rozważmy
więc prosty program konta bankowego.
// Package bank implementuje bank z tylko jednym kontem.
package bank

var balance int

func Deposit(amount int) { balance = balance + amount }

func Balance() int { return balance }


(Mogliśmy zapisać ciało funkcji Deposit jako balance += amount, co jest równoważne, ale dłuższa
forma uprości objaśnienie).
W przypadku tak trywialnego programu możemy na pierwszy rzut oka zobaczyć, że każda sekwencja
wywołań funkcji Deposit i Balance da prawidłową odpowiedź, czyli Balance zgłosi sumę wszystkich
poprzednio wpłaconych kwot. Jeśli jednak wywołamy te funkcje nie w sekwencji, ale współbieżnie,
nie będzie już gwarancji, że Balance da właściwą odpowiedź. Rozważmy następujące dwie funkcje
goroutine, które reprezentują dwie transakcje wykonywane na wspólnym rachunku bankowym:
// Alicja:
go func() {
bank.Deposit(200) // A1
fmt.Println("=", bank.Balance()) // A2
}()

// Robert:
go bank.Deposit(100) // B
Alicja wpłaca 200 zł, a następnie sprawdza swoje saldo, podczas gdy Robert wpłaca 100 zł. Ponieważ
kroki A1 i A2 występują współbieżnie z B, nie możemy przewidzieć kolejności, w jakiej wystąpią.
Intuicyjnie może się wydawać, że istnieją tylko trzy możliwe kolejności, które nazwiemy: „Naj-
pierw Alicja”, „Najpierw Robert” oraz „Alicja-Robert-Alicja”. Poniższa tabela przedstawia wartość
zmiennej balance po każdym kroku. Zacytowane łańcuchy znaków reprezentują wydruki po-
twierdzenia salda.
9.1. SYTUACJE WYŚCIGU 255

Najpierw Alicja Najpierw Robert Alicja-Robert-Alicja


0 0 0
A1 200 B 100 A1 200
A2 "= 200" A1 300 B 300
B 300 A2 "= 300" A2 "= 300"
We wszystkich przypadkach saldo końcowe wynosi 300 zł. Jedyna wariacja polega na tym, czy wy-
druk salda Alicji będzie zawierał transakcję Roberta, czy nie, ale klienci i tak są usatysfakcjonowani.
Jednak to intuicyjne podejście jest błędne. Istnieje czwarty możliwy wynik, w którym wpłata Roberta
ma miejsce w trakcie wpłaty Alicji, po tym jak saldo zostało odczytane (balance + amount), ale
przed tym jak zostało zaktualizowane (balance = ...), co powoduje zniknięcie transakcji
Roberta. Dzieje się tak dlatego, że operacja wpłaty Alicji (A1) jest w rzeczywistości sekwencją dwóch
operacji: odczytu i zapisu. Nazwijmy je A1r i A1w. Oto problematyczne przeplatanie:
Wyścig danych
0
A1r 0 ... = balance + amount
B 100
A1w 200 balance = ...
A2 "= 200"
Po kroku A1r wrażenie balance + amount ewaluuje do 200, więc jest to wartość zapisywana podczas
A1w, pomimo przeplatającej te kroki operacji wpłaty. Końcowe saldo wynosi tylko 200 zł. Bank jest
o 100 zł bogatszy kosztem Roberta.
Ten program zawiera szczególny rodzaj sytuacji wyścigu, zwany wyścigiem danych. Wyścig danych
ma miejsce za każdym razem, gdy dwie funkcje goroutine uzyskują współbieżnie dostęp do tej samej
zmiennej i co najmniej jedna z operacji uzyskania dostępu jest zapisem.
Sytuacja jeszcze bardziej się komplikuje, jeśli wyścig danych obejmuje zmienną typu, który jest większy
niż pojedyncze słowo maszynowe, takiego jak interfejs, łańcuch znaków lub wycinek. Ten kod
współbieżnie aktualizuje zmienną x do postaci dwóch wycinków o różnej długości:
var x []int
go func() { x = make([]int, 10) }()
go func() { x = make([]int, 1000000) }()
x[999999] = 1 // UWAGA: niezdefiniowane zachowanie; możliwe uszkodzenie pamięci!
Wartość zmiennej x w ostatniej instrukcji nie jest zdefiniowana. Może to być nil, wycinek o długości
10 lub wycinek o długości 1 000 000. Przypomnijmy jednak, że istnieją trzy części określające
wycinek: wskaźnik, długość oraz pojemność. Jeśli wskaźnik pochodzi z pierwszego wywołania
make, a długość z drugiego, x będzie chimerą, czyli wycinkiem, którego nominalna długość wynosi
1 000 000, ale bazowa tablica ma tylko 10 elementów. W takim przypadku zapisywanie w elemen-
cie 999 999 nadpisałoby dowolnie odległą lokalizację pamięci z konsekwencjami, które są niemoż-
liwe do przewidzenia oraz trudne do zdebugowania i zlokalizowania. To semantyczne pole mi-
nowe nazywa się niezdefiniowanym zachowaniem i jest dobrze znane programistom języka C.
Na szczęście rzadko jest tak kłopotliwe w Go jak w C.
Nawet określenie współbieżnego programu jako przeplatania się kilku programów sekwencyjnych
jest fałszywą intuicją. Jak zobaczymy w podrozdziale 9.4, wyścigi danych mogą mieć jeszcze
dziwniejsze rezultaty. Wielu programistów (nawet ci bardzo sprytni) od czasu do czasu oferuje
usprawiedliwienia dla znanych wyścigów danych w swoich programach: „Koszt wzajemnego wy-
kluczania jest zbyt wysoki”, „Ta logika jest tylko do rejestrowania”, „Nie przeszkadza mi porzucanie
kilku komunikatów” itd. Brak problemów na danej platformie i danym kompilatorze może za-
pewnić im fałszywą pewność siebie. Dobrą praktyczną zasadą jest to, że nie ma czegoś takiego jak
łagodny wyścig danych. Jak więc unikać wyścigów danych w naszych programach?
256 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

Powtórzmy jeszcze raz definicję, ponieważ jest to wyjątkowo ważne: wyścig danych ma miejsce
za każdym razem, gdy dwie funkcje goroutine uzyskują współbieżnie dostęp do tej samej zmiennej
i co najmniej jedna z operacji uzyskania dostępu jest zapisem. Z tej definicji wynika, że istnieją
trzy sposoby unikania wyścigu danych.
Pierwszym sposobem jest niezapisywanie zmiennej. Rozważmy poniższą mapę, która jest leniwie
zapełniana, gdy każdy klucz jest żądany po raz pierwszy. Jeśli funkcja Icon jest wywoływana sekwen-
cyjnie, program działa prawidłowo, ale jeśli funkcja Icon jest wywoływana współbieżnie, dochodzi
do wyścigu danych podczas uzyskiwania dostępu do mapy.
var icons = make(map[string]image.Image)

func loadIcon(name string) image.Image

// UWAGA: to nie jest współbieżnie bezpieczne!


func Icon(name string) image.Image {
icon, ok := icons[name]
if !ok {
icon = loadIcon(name)
icons[name] = icon
}
return icon
}
Jeśli zamiast tego zainicjujemy mapę ze wszystkimi niezbędnymi wpisami przed utworzeniem do-
datkowych funkcji goroutine i nie będziemy jej nigdy ponownie modyfikować, wtedy dowolna liczba
funkcji goroutine może bezpiecznie wywoływać współbieżnie funkcję Icon, ponieważ każda tylko
czyta mapę.
var icons = map[string]image.Image{
"spades.png": loadIcon("spades.png"),
"hearts.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
}

// Współbieżnie bezpieczne.
func Icon(name string) image.Image { return icons[name] }
W powyższym przykładzie zmienna icons jest przypisywana w trakcie inicjowania pakietu, co
dzieje się przed rozpoczęciem działania funkcji main programu. Po zainicjowaniu zmienna icons
nigdy nie jest modyfikowana. Struktury danych, które nigdy nie są modyfikowane lub są niemu-
towalne, z natury są współbieżnie bezpieczne i nie wymagają synchronizacji. Ale oczywiście nie mo-
żemy używać tego podejścia, jeśli aktualizacje są niezbędne, tak jak w przypadku konta bankowego.
Drugim sposobem uniknięcia wyścigu danych jest unikanie uzyskiwania dostępu do zmiennej
z wielu funkcji goroutine. Jest to podejście przyjęte w wielu programach w poprzednim rozdziale. Dla
przykładu: główna funkcja goroutine we współbieżnym robocie internetowym (zob. podrozdział 8.6)
jest jedyną funkcją goroutine uzyskującą dostęp do mapy seen, a funkcja goroutine broadcaster
w serwerze czatu (zob. podrozdział 8.10) jest jedyną funkcją goroutine, która uzyskuje dostęp do mapy
clients. Te zmienne są zamknięte w pojedynczej funkcji goroutine.
Ponieważ inne funkcje goroutine nie mogą bezpośrednio uzyskiwać dostępu do tej zmiennej, muszą
używać kanału do wysyłania zamykającej funkcji goroutine żądania kwerendy lub aktualizacji zmien-
nej. Takie jest właśnie znaczenie mantry języka Go: „Nie komunikuj się poprzez współdzielenie
pamięci, zamiast tego współdziel pamięć przez komunikowanie się”. Funkcja goroutine, która po-
9.1. SYTUACJE WYŚCIGU 257

średniczy w uzyskiwaniu dostępu do zamkniętej zmiennej za pomocą żądania wysyłanego przez kanał,
jest nazywana monitorującą funkcją goroutine dla tej zmiennej. Funkcja goroutine broadcaster
monitoruje np. dostęp do mapy clients.
Oto przykład banku napisany z użyciem zmiennej balance zamkniętej w monitorującej funkcji
goroutine o nazwie teller:
code/r09/bank1
// Package bank implementuje współbieżnie bezpieczny bank z jednym kontem.
package bank

var deposits = make(chan int) // wysyłanie kwoty do wpłaty


var balances = make(chan int) // odbieranie salda

func Deposit(amount int) { deposits <- amount }


func Balance() int { return <-balances }

func teller() {
var balance int // zmienna balance jest zamknięta w funkcji goroutine teller
for {
select {
case amount := <-deposits:
balance += amount
case balances <- balance:
}
}
}

func init() {
go teller() // uruchomienie monitorującej funkcji goroutine
}
Nawet gdy zmienna nie może być zamknięta w pojedynczej funkcji goroutine przez cały swój
cykl życia, zamykanie nadal może być rozwiązaniem problemu współbieżnego dostępu. Powszechne
jest np. współdzielenie zmiennej między funkcjami goroutine w potoku poprzez przekazywanie
jej adresu z jednego etapu do następnego etapu przez kanał. Jeśli każdy etap potoku powstrzymuje się
od dostępu do zmiennej po wysłaniu jej do kolejnego etapu, wszystkie próby uzyskania dostępu
do tej zmiennej są sekwencyjne. W efekcie zmienna jest zamknięta w jednym etapie potoku, potem
jest zamknięta w następnym itd. Ten rygor jest czasami nazywany zamykaniem szeregowym
(ang. serial confinement).
W poniższym przykładzie zmienna cake (ciasto) jest szeregowo zamykana — najpierw w funkcji
goroutine baker (piekarz), a następnie w funkcji goroutine icer (lukiernik):
type Cake struct{ state string }

func baker(cooked chan<- *Cake) {


for {
cake := new(Cake)
cake.state = "cooked"
cooked <- cake // piekarz już nigdy nie zajmuje się tym ciastem
}
}

func icer(iced chan<- *Cake, cooked <-chan *Cake) {


for cake := range cooked {
cake.state = "iced"
258 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

iced <- cake // lukiernik już nigdy nie zajmuje się tym ciastem
}
}
Trzecim sposobem unikania wyścigu danych jest umożliwienie uzyskiwania dostępu do zmiennej
wielu funkcjom goroutine, ale tylko po jednej na raz. Takie podejście nazywa się wzajemnym wyklu-
czaniem (ang. mutual exclusion) i jest przedmiotem następnego podrozdziału.
Ćwiczenie 9.1. Dodaj do programu code/r09/bank1 funkcję Withdraw(amount int) bool.
Wynik powinien wskazywać, czy dana transakcja wypłaty powiodła się lub nie powiodła się
z powodu niewystarczających środków. Komunikat wysyłany do monitorującej funkcji goroutine
musi zawierać zarówno kwotę do wypłaty, jak i nowy kanał, przez który monitorująca funkcja
goroutine może wysłać wynik logiczny z powrotem do funkcji Withdraw.

9.2. Wzajemne wykluczanie: sync.mutex


W podrozdziale 8.6 użyliśmy buforowanego kanału jako semafora zliczającego, aby zapewnić, że nie
więcej niż 20 funkcji goroutine będzie wysyłać jednoczesne żądania HTTP. W ten sam sposób
możemy użyć kanału o pojemności 1 w celu zapewnienia, że co najwyżej jedna funkcja goroutine bę-
dzie uzyskiwała dostęp do współdzielonej zmiennej. Semafor, który liczy tylko do 1, jest nazywany
semaforem binarnym.
code/r09/bank2
var (
sema = make(chan struct{}, 1) // binarny semafor strzegący zmiennej balance
balance int
)

func Deposit(amount int) {


sema <- struct{}{} // nabycie żetonu
balance = balance + amount
<-sema // zwolnienie żetonu
}

func Balance() int {


sema <- struct{}{} // nabycie żetonu
b := balance
<-sema // zwolnienie żetonu
return b
}
Ten wzorzec wzajemnego wykluczania okazuje się tak przydatny, że jest obsługiwany bezpośrednio
przez typ Mutex z pakietu sync. Jego metoda Lock nabywa żeton (zwany blokadą), a jego metoda
Unlock zwalnia go:
code/r09/bank3
import "sync"

var (
mu sync.Mutex // strzeże zmiennej balance
balance int
)

func Deposit(amount int) {


mu.Lock()
9.2. WZAJEMNE WYKLUCZANIE: SYNC.MUTEX 259

balance = balance + amount


mu.Unlock()
}

func Balance() int {


mu.Lock()
b := balance
mu.Unlock()
return b
}
Za każdym razem, gdy funkcja goroutine uzyskuje dostęp do zmiennych banku (tutaj tylko do
zmiennej balance), musi wywołać metodę Lock muteksu, aby założyć blokadę na wyłączność.
Jeśli jakaś inna funkcja goroutine założy blokadę, ta operacja będzie blokować, dopóki ta funkcja
goroutine nie wywoła metody Unlock i blokada ponownie nie stanie się dostępna. Mutex strzeże
współdzielonych zmiennych. Zgodnie z przyjętą konwencją zmienne strzeżone przez mutex są de-
klarowane natychmiast po deklaracji samego muteksu. Jeśli odstąpisz od tej zasady, należy to udo-
kumentować.
Region kodu między metodami Lock i Unlock, w którym funkcja goroutine może swobodnie czytać
i modyfikować współdzielone zmienne, jest nazywany sekcją krytyczną. Wykonywanie przez
posiadacza zamka wywołania metody Unlock dzieje się przed tym, gdy jakakolwiek inna funkcja
goroutine będzie mogła sama założyć blokadę. Ważne jest, aby dana funkcja goroutine zwolniła
blokadę po skończeniu swojej operacji na wszystkich ścieżkach w funkcji, w tym na ścieżkach
błędów.
Powyższy program banku ilustruje powszechny wzorzec współbieżności. Zestaw eksportowanych
funkcji hermetyzuje jedną zmienną lub więcej zmiennych tak, aby jedynym sposobem uzyskania
dostępu do tych zmiennych było użycie tych funkcji (lub metody w przypadku zmiennych obiektu).
Każda funkcja zakłada blokadę muteksu na początku i zwalnia ją na końcu, zapewniając w ten spo-
sób, że dostęp do współdzielonych zmiennych nie będzie uzyskiwany jednocześnie. Taki układ
funkcji, blokady muteksu i zmiennych nazywa się monitorem. (To starsze użycie słowa „monitor”
zainspirowało termin „monitorująca funkcja goroutine”. Oba użycia współdzielą znaczenie pojęcia
pośrednika, który zapewnia, że dostęp do zmiennych jest uzyskiwany sekwencyjnie).
Ponieważ sekcje krytyczne w funkcjach Deposit i Balance są tak krótkie (pojedyncza linia, brak
rozgałęzień), wywołanie Unlock na końcu jest proste. W bardziej złożonych sekcjach krytycznych,
zwłaszcza w takich, w których błędy muszą być obsługiwane przez wcześniejszy powrót, może
być trudno stwierdzić, czy wywołania metod Lock i Unlock są ściśle połączone w pary na wszyst-
kich ścieżkach. Na ratunek przychodzi instrukcja defer języka Go: poprzez odroczenie wywoła-
nia metody Unlock sekcja krytyczna domyślnie rozciąga się do końca aktualnej funkcji, uwalniając
nas od konieczności pamiętania o wstawieniu wywołania Unlock w jednym miejscu lub w kilku
miejscach oddalonych od wywołania Lock.
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
W powyższym przykładzie wywołanie Unlock jest wykonywane po tym, jak instrukcja return
odczyta wartość zmiennej balance, więc funkcja Balance jest współbieżnie bezpieczna. Bonusem
jest to, że nie potrzebujemy już zmiennej lokalnej b.
260 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

Ponadto odroczone wywołanie Unlock zostanie wykonane nawet wtedy, gdy sekcja krytyczna
uruchomi procedurę panic, co może być ważne w programach wykorzystujących funkcję recover
(zob. podrozdział 5.10). Użycie instrukcji defer jest nieznacznie bardziej kosztowne niż bezpośred-
nie wywołanie metody Unlock, ale nie aż tak, aby uzasadnić mniej jasny kod. Jak zawsze w przypadku
programów współbieżnych, należy preferować przejrzystość i powstrzymywać się od przedwczesnej
optymalizacji. Tam, gdzie to możliwe, używaj instrukcji defer i pozwalaj, by sekcje krytyczne
przedłużały się do końca funkcji.
Rozważmy poniższą funkcję Withdraw. W przypadku powodzenia zmniejsza ona saldo o określoną
kwotę i zwraca true. Jeśli na rachunku nie ma wystarczających środków do przeprowadzenia
transakcji, funkcja Withdraw przywraca saldo i zwraca false.
// Uwaga: ta funkcja nie jest niepodzielna!
func Withdraw(amount int) bool {
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // niewystarczające środki
}
return true
}
Ta funkcja na koniec daje prawidłowy wynik, ale ma nieprzyjemny efekt uboczny. Gdy doko-
nywana jest próba wypłacenia nadmiernej kwoty, saldo przejściowo spada poniżej zera. Może to
spowodować, że równoczesna próba wypłacenia umiarkowanej kwoty zostanie błędnie odrzucona.
Jeśli więc Robert spróbuje kupić sportowy samochód, Alicja nie będzie mogła zapłacić za poranną
kawę. Problem polega na tym, że funkcja Withdraw nie jest niepodzielna: składa się z sekwencji
trzech oddzielnych operacji, z których każda zakłada, a następnie zwalnia blokadę muteksu, ale
nic nie blokuje całej sekwencji.
Najlepiej by było, żeby funkcja Withdraw zakładała blokadę muteksu raz na całą operację. Jednak
taka próba się nie uda:
// UWAGA: nieprawidłowe!
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // niewystarczające środki
}
return true
}
Funkcja Deposit próbuje założyć blokadę muteksu po raz drugi przez wywołanie mu.Lock(),
ponieważ jednak blokady muteksu nie są współużywalne (nie można zablokować muteksu, który
jest już zablokowany), prowadzi to do zakleszczenia, w którym nic nie może być kontynuowane,
a funkcja Withdraw blokuje w nieskończoność.
Istnieje dobry powód, dla którego muteksy Go nie są współużywalne. Celem muteksu jest zapew-
nienie, że określone niezmienniki współdzielonych zmiennych będą utrzymywane w krytycznych
punktach podczas wykonywania programu. Jednym z niezmienników jest to, że „żadna funkcja
goroutine nie uzyskuje dostępu do współdzielonych zmiennych”, ale mogą istnieć dodatkowe
niezmienniki charakterystyczne dla struktur danych strzeżonych przez mutex. Kiedy funkcja goro-
utine zakłada blokadę muteksu, może przyjąć, że niezmienniki są zachowane. Gdy utrzymuje
9.3. MUTEKSY ODCZYTU/ZAPISU: SYNC.RWMUTEX 261

blokadę, może aktualizować współdzielone zmienne, więc niezmienniki są czasowo naruszane.


Jednak gdy zwalnia blokadę, musi zagwarantować, że porządek został przywrócony, a niezmienniki
są ponownie zachowane. Chociaż współużywalny mutex zapewniłby, że żadna inna funkcja
goroutine nie uzyskuje dostępu do współdzielonych zmiennych, nie może chronić dodatkowych
niezmienników tych zmiennych.
Typowym rozwiązaniem jest podział funkcji, takiej jak Deposit, na dwie: nieeksportowaną funkcję
deposit, która zakłada, że blokada jest już założona, i wykonuje rzeczywistą pracę, oraz eksporto-
waną funkcję Deposit, która zakłada blokadę przed wywołaniem deposit. Możemy wtedy wyrazić
funkcję Withdraw w kategoriach deposit w ten sposób:
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false // niewystarczające środki
}
return true
}

func Deposit(amount int) {


mu.Lock()
defer mu.Unlock()
deposit(amount)
}

func Balance() int {


mu.Lock()
defer mu.Unlock()
return balance
}

// Ta funkcja wymaga utrzymywania blokady.


func deposit(amount int) { balance += amount }
Oczywiście pokazana tutaj funkcja deposit jest tak trywialna, że rzeczywista funkcja Withdraw
nie trudziłaby się jej wywoływaniem, ale mimo to ilustruje zasadę.
Hermetyzacja (zob. podrozdział 6.6) pomaga utrzymać niezmienniki struktury danych poprzez
zredukowanie liczby nieoczekiwanych interakcji w programie. Z tego samego powodu hermetyzacja
pomaga również utrzymać niezmienniki współbieżności. Podczas korzystania z muteksu należy
się upewnić, że nie jest on eksportowany i nie są również eksportowane strzeżone przez niego
zmienne, niezależnie od tego, czy są one zmiennymi poziomu pakietu, czy polami struktury.

9.3. Muteksy odczytu/zapisu: sync.RWMutex


W przypływie niepokoju po zobaczeniu, jak wpłacone 100 zł znika bez śladu, Robert pisze program
do sprawdzania swojego salda na rachunku setki razy na sekundę. Uruchamia go w domu, w pracy
i na telefonie. Bank zauważa, że zwiększony ruch opóźnia wpłaty i wypłaty, ponieważ wszystkie żą-
dania Balance są uruchamiane sekwencyjnie, utrzymując blokadę na wyłączność i tymczasowo
uniemożliwiając uruchamianie innych funkcji goroutine.
262 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

Ponieważ funkcja Balance potrzebuje tylko odczytać stan zmiennej, w rzeczywistości bezpieczne
byłoby współbieżne wywoływanie Balance, pod warunkiem że nie jest wykonywane żadne wywoła-
nie funkcji Deposit lub Withdraw. W tym scenariuszu potrzebujemy specjalnego rodzaju blokady,
która pozwala przeprowadzać równolegle operacje tylko odczytu, ale operacjom zapisu daje pełny
dostęp na wyłączność. Ta blokada nazywana jest blokadą wielu odczytów, pojedynczego zapisu,
a w języku Go jest zapewniana przez sync.RWMutex:
var mu sync.RWMutex
var balance int

func Balance() int {


mu.RLock() // blokada odczytów
defer mu.RUnlock()
return balance
}
Funkcja Balance wywołuje teraz metody RLock i RUnlock, aby założyć i zwolnić blokadę odczytów,
czyli blokadę współdzieloną. Funkcja Deposit, która nie uległa zmianie, wywołuje metody
mu.Lock i mu.Unlock, aby założyć i zwolnić blokadę zapisu, czyli blokadę na wyłączność.
Po tej zmianie większość żądań Balance Roberta jest wykonywana równolegle względem siebie
i zostaje szybciej ukończona. Blokada jest dłużej dostępna, a żądania Deposit mogą być procedo-
wane terminowo.
Metoda RLock może być stosowana tylko wtedy, gdy nie ma żadnych zapisów we współdzielonych
zmiennych w sekcji krytycznej. Ogólnie rzecz biorąc, nie powinniśmy zakładać, że funkcje lub
metody logicznie tylko do odczytu nie aktualizują również niektórych zmiennych. Przykładowo:
metoda, która wydaje się być prostym akcesorem, może również inkrementować wewnętrzny
licznik użycia lub aktualizować pamięć podręczną, aby powtarzane wywołania były wykonywane
szybciej. W razie wątpliwości stosuj blokadę na wyłączność.
Użycie RWMutex jest korzystne tylko wtedy, kiedy większość funkcji goroutine zakładających blokadę
jest funkcjami odczytu, a blokada jest przedmiotem rywalizacji, czyli funkcje goroutine stale
muszą czekać, aby ją założyć. RWMutex wymaga bardziej złożonej wewnętrznej ewidencji, przez
co jest wolniejszy niż standardowy mutex dla blokad niebędących przedmiotem rywalizacji.

9.4. Synchronizacja pamięci


Można się zastanawiać, dlaczego metoda Balance potrzebuje wzajemnego wykluczania opartego
na kanale lub muteksie. W końcu, w przeciwieństwie do funkcji Deposit, składa się tylko z jednej
operacji, więc nie ma niebezpieczeństwa, że inna funkcja goroutine będzie wykonywana „w jej
trakcie”. Istnieją dwa powody, dla których potrzebujemy muteksu. Po pierwsze, jest tak samo ważne,
aby metoda Balance nie była wykonywana w środku jakiejś innej operacji, takiej jak Withdraw.
Drugim (i bardziej subtelnym) powodem jest to, że synchronizacja to coś więcej niż tylko kolejność
wykonywania wielu funkcji goroutine. Synchronizacja ma również wpływ na pamięć.
W nowoczesnym komputerze mogą być dziesiątki procesorów, każdy z własną lokalną pamięcią
podręczną pamięci głównej. Dla efektywności operacje zapisu do pamięci są buforowane w ra-
mach każdego procesora i przenoszone do głównej pamięci tylko wtedy, gdy jest to konieczne. Mogą
nawet być umieszczane w pamięci głównej w innej kolejności, niż zostały zapisane przez zapisują-
cą funkcję goroutine. Podstawowe elementy synchronizacji, takie jak komunikacje poprzez kanał
i operacje muteksu, powodują opróżnianie bufora procesora i zapisywanie w pamięci wszystkich jego
9.4. SYNCHRONIZACJA PAMIĘCI 263

nagromadzonych operacji zapisu, aby zagwarantować, żeby efekty wykonywania funkcji do tego
punktu były widoczne dla funkcji goroutine uruchomionych na innych procesorach.
Rozważmy możliwe dane wyjściowe z poniższego fragmentu kodu:
var x, y int
go func() {
x = 1 // A1
fmt.Print("y:", y, " ") // A2
}()
go func() {
y = 1 // B1
fmt.Print("x:", x, " ") // B2
}()
Ponieważ te dwie funkcje goroutine są współbieżne i uzyskują dostęp do współdzielonych zmiennych
bez wzajemnego wykluczania, mamy do czynienia z wyścigiem danych, więc nie powinno
dziwić, że program nie jest deterministyczny. Możemy spodziewać się wyświetlenia dowolnego
z czterech poniższych wyników, które odpowiadają intuicyjnym przeplataniom oznaczonych
etykietami instrukcji programu:
y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1
Czwartą linię można np. wyjaśnić poprzez sekwencję A1, B1, A2, B2 lub B1, A1, A2, B2. Ale te dwa
wyniki mogą być zaskoczeniem:
x:0 y:0
y:0 x:0
Jednak w zależności od kompilatora, procesora i wielu innych czynników również one mogą się
zdarzyć. Jakie możliwe przeplatanie tych czterech instrukcji mogłoby je wyjaśnić?
W ramach pojedynczej funkcji goroutine gwarantowane jest, że efekty każdej instrukcji będą wy-
stępować w kolejności wykonywania. Funkcje goroutine są sekwencyjnie spójne. Jednak w przypad-
ku braku bezpośredniej synchronizacji za pomocą kanału lub muteksu nie ma żadnej gwarancji,
że wydarzenia będą dostrzegane w tej samej kolejności przez wszystkie funkcje goroutine. Chociaż
funkcja goroutine A musi zaobserwować efekt zapisu x = 1, zanim odczyta wartość y, to nie musi
koniecznie dostrzec zapisu w zmiennej y przeprowadzanego przez funkcję goroutine B, więc A może
wypisać nieaktualną wartość y.
Kusząca może być próba zrozumienia współbieżności jako czegoś, co odpowiada pewnemu
przeplataniu instrukcji każdej funkcji goroutine, ale, jak pokazuje powyższy przykład, nie w taki
sposób działa nowoczesny kompilator lub procesor. Ponieważ przypisanie i funkcja Print odwo-
łują się do różnych zmiennych, kompilator może stwierdzić, że kolejność tych dwóch instrukcji
nie może wpływać na wynik, i zamienić je. Jeśli dwie funkcje goroutine są wykonywane na różnych
procesorach, z których każdy ma własną pamięć podręczną, operacje zapisu przeprowadzane
przez jedną funkcję goroutine nie są widoczne dla funkcji Print drugiej goroutine, dopóki pamięci
podręczne nie zostaną zsynchronizowane z pamięcią główną.
Wszystkich tych problemów współbieżności można uniknąć poprzez konsekwentne stosowanie
prostych, ustalonych wzorców. Tam, gdzie to możliwe, należy zamykać zmienne w pojedynczej
funkcji goroutine. Dla wszystkich innych zmiennych należy używać wzajemnego wykluczania.
264 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

9.5. Leniwe inicjowanie: sync.Once


Dobrą praktyką jest odraczanie kosztownego etapu inicjowania do momentu, gdy będzie on wyma-
gany. Inicjowanie zmiennej z góry zwiększa opóźnienie uruchamiania programu i nie jest konieczne,
jeśli wykonywanie nie zawsze dociera do tej części programu, która używa danej zmiennej. Wróćmy
do zmiennej icons, którą widzieliśmy wcześniej w tym rozdziale:
var icons map[string]image.Image
Ta wersja funkcji Icon wykorzystuje leniwe inicjowanie:
func loadIcons() {
icons = map[string]image.Image{
"spades.png": loadIcon("spades.png"),
"hearts.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
}
}

// UWAGA: to nie jest współbieżnie bezpieczne!


func Icon(name string) image.Image {
if icons == nil {
loadIcons() // jednorazowe inicjowanie
}
return icons[name]
}
W przypadku zmiennej, do której dostęp uzyskuje tylko jedna funkcja goroutine, możemy użyć
powyższego wzorca, ale nie jest on bezpieczny, jeśli funkcja Icon jest wywoływana współbieżnie.
Podobnie jak pierwotna funkcja Deposit programu bankowego, funkcja Icon składa się z kilku
etapów: najpierw sprawdza, czy zmienna icons jest równa nil, potem ładuje ikony, a następnie
aktualizuje zmienną icons do wartości innej niż nil. Intuicja może podpowiadać, że najgorszym
możliwym wynikiem powyższej sytuacji wyścigu jest kilkakrotne wywoływanie funkcji loadIcons.
Podczas gdy pierwsza funkcja goroutine jest zajęta ładowaniem ikon, kolejna goroutine wchodząca
w funkcję Icons zobaczyłaby, że zmienna jest wciąż równa nil, i również wywołałaby funkcję
loadIcons.
Ale ta intuicja jest również błędna. (Mamy nadzieję, że wyrobiłeś już sobie intuicyjne podejście do
współbieżności, które mówi, że intuicjom na temat współbieżności nie można ufać!). Przypomnijmy
dyskusję na temat pamięci z podrozdziału 9.4. W przypadku braku bezpośredniej synchronizacji
kompilator i procesor mogą swobodnie zmieniać kolejność uzyskiwania dostępu do pamięci na
wiele sposobów, jeśli tylko zachowanie każdej funkcji goroutine jest sekwencyjnie spójne. Jedna
z możliwych zmian kolejności instrukcji funkcji loadIcons jest pokazana poniżej. Zapisuje ona
pustą mapę w zmiennej icons przed zapełnieniem jej:
func loadIcons() {
icons = make(map[string]image.Image)
icons["spades.png"] = loadIcon("spades.png")
icons["hearts.png"] = loadIcon("hearts.png")
icons["diamonds.png"] = loadIcon("diamonds.png")
icons["clubs.png"] = loadIcon("clubs.png")
}
W konsekwencji funkcja goroutine, która napotka zmienną icons różną od nil, nie może zakładać,
że inicjowanie zmiennej zostało zakończone.
9.5. LENIWE INICJOWANIE: SYNC.ONCE 265

Najprostszym prawidłowym sposobem zapewnienia, że wszystkie funkcje goroutine zauważą efekty


funkcji loadIcons, jest ich zsynchronizowanie za pomocą muteksu:
var mu sync.Mutex // strzeże zmiennej icons
var icons map[string]image.Image

// Współbieżnie bezpieczne.
func Icon(name string) image.Image {
mu.Lock()
defer mu.Unlock()
if icons == nil {
loadIcons()
}
return icons[name]
}
Jednak kosztem egzekwowania wzajemnie wykluczającego się dostępu do zmiennej icons jest to, że
dwie funkcje goroutine nie mogą uzyskiwać dostępu do tej zmiennej jednocześnie, nawet gdy zmien-
na została bezpiecznie zainicjowana i nigdy nie będzie ponownie modyfikowana. Sugeruje to blokadę
wielu odczytów:
var mu sync.RWMutex // strzeże zmiennej icons
var icons map[string]image.Image

// Współbieżnie bezpieczne.
func Icon(name string) image.Image {
mu.RLock()
if icons != nil {
icon := icons[name]
mu.RUnlock()
return icon
}
mu.RUnlock()

// Zakładanie blokady na wyłączność.


mu.Lock()
if icons == nil { // UWAGA: trzeba ponownie sprawdzić pod kątem nil
loadIcons()
}
icon := icons[name]
mu.Unlock()
return icon
}
Teraz istnieją dwie sekcje krytyczne. Funkcja goroutine najpierw zakłada blokadę odczytu, spraw-
dza mapę, a następnie zwalnia blokadę. Jeśli został znaleziony wpis (typowy przypadek), jest on
zwracany. Jeśli nie znaleziono wpisu, funkcja goroutine zakłada blokadę zapisu. Nie ma sposobu
uaktualnienia blokady współdzielonej do postaci blokady na wyłączność bez uprzedniego zwolnienia
blokady współdzielonej, więc musimy ponownie sprawdzić zmienną icons, na wypadek gdyby
druga funkcja goroutine już ją zainicjowała.
Powyższy wzorzec gwarantuje nam większą współbieżność, ale jest skomplikowany, a więc podatny
na błędy. Na szczęście pakiet sync zapewnia wyspecjalizowane rozwiązanie problemu jednorazo-
wego inicjowania: sync.Once. Koncepcyjnie Once składa się z muteksu i zmiennej logicznej, która
rejestruje, czy inicjowanie miało miejsce. Mutex strzeże zarówno zmiennej logicznej, jak i struktur
danych klienta. Jedyna metoda Do przyjmuje jako swój argument funkcję inicjowania. Użyjmy
typu Once do uproszczenia funkcji Icon:
266 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

var loadIconsOnce sync.Once


var icons map[string]image.Image

// Współbieżnie bezpieczne.
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
Każde wywołanie Do(loadIcons) blokuje mutex i sprawdza zmienną logiczną. W pierwszym wy-
wołaniu, w którym zmienna ma wartość false, metoda Do wywołuje funkcję loadIcons i ustawia
zmienną na true. Kolejne wywołania nie robią nic, ale synchronizacja muteksowa zapewnia, że
efekty wywoływane przez loadIcons w pamięci (w szczególności w zmiennej icons) staną się
widoczne dla wszystkich funkcji goroutine. Używając typu sync.Once w ten sposób, możemy unik-
nąć współdzielenia zmiennych z innymi funkcjami goroutine, dopóki te zmienne nie zostaną prawi-
dłowo skonstruowane.
Ćwiczenie 9.2. Przepisz przykład PopCount z punktu 2.6.2 w taki sposób, aby inicjował tablicę wy-
szukiwania przy użyciu sync.Once, gdy będzie wymagana po raz pierwszy. (W rzeczywistości koszt
synchronizacji byłby zbyt wyśrubowany dla małej i wysoce zoptymalizowanej funkcji, takiej jak
PopCount).

9.6. Detektor wyścigów


Nawet przy największej staranności bardzo łatwo jest popełnić błąd związany ze współbieżnością.
Na szczęście środowisko wykonawcze i zestaw narzędzi języka Go są wyposażone w zaawan-
sowane i łatwe w użyciu narzędzie do analizy dynamicznej, czyli detektor wyścigów.
Aby użyć tego narzędzia, po prostu dodaj do polecenia go build, go run lub go test flagę -race.
Spowoduje to, że kompilator skompiluje zmodyfikowaną wersję aplikacji czy testu z dodatkową
instrumentacją, która skutecznie rejestruje wszystkie próby uzyskania dostępu do współdzielonych
zmiennych w trakcie wykonywania, a także rejestruje tożsamości funkcji goroutine odczytują-
cych lub zapisujących zmienną. Ponadto ten zmodyfikowany program rejestruje wszystkie zdarzenia
synchronizacji, takie jak: instrukcje go, operacje na kanałach, wywołania (*sync.Mutex).Lock,
(*sync.WaitGroup).Wait itd. (Kompletny zestaw zdarzeń synchronizacji został określony
w dokumencie The Go Memory Model, który towarzyszy specyfikacji języka).
Detektor wyścigów analizuje ten strumień zdarzeń, szukając przypadków, gdzie jedna funkcja
goroutine odczytuje lub zapisuje współdzieloną zmienną, która została ostatnio zapisana przez inną
funkcję goroutine bez pośredniczącej operacji synchronizacji. Wskazuje to na równoczesny do-
stęp do współdzielonej zmiennej, a więc na wyścig danych. Narzędzie wyświetla raport, który
zawiera tożsamość zmiennej oraz stosy aktywnych wywołań funkcji w odczytującej funkcji goroutine
i zapisującej funkcji goroutine. Zwykle wystarcza to do zidentyfikowania problemu. Podrozdział 9.7
zawiera przykład działania detektora wyścigów.
Detektor wyścigów raportuje wszystkie wyścigi danych, które zostały faktycznie wykonane. Jednak
może wykryć tylko sytuacje wyścigu, które występują podczas działania programu. Nie można
udowodnić, że nigdy nie wystąpią żadne sytuacje wyścigu. Aby osiągnąć najlepsze rezultaty, upewnij
się, że testy analizują pakiety z użyciem współbieżności.
Ze względu na dodatkowe ewidencjonowanie program skompilowany z detektorem wyścigów po-
trzebuje do wykonania więcej czasu i pamięci, ale ten narzut jest tolerowalny nawet dla wielu zadań
9.7. PRZYKŁAD: WSPÓŁBIEŻNA NIEBLOKUJĄCA PAMIĘĆ PODRĘCZNA 267

produkcyjnych. W rzadko występujących sytuacjach wyścigu użycie detektora wyścigów może


zaoszczędzić wiele godzin lub dni debugowania.

9.7. Przykład: współbieżna nieblokująca pamięć podręczna


W tym podrozdziale zbudujemy współbieżną nieblokującą pamięć podręczną, czyli abstrakcję
rozwiązującą problem, który pojawia się często w rzeczywistych współbieżnych programach, ale
nie jest dobrze adresowany przez istniejące biblioteki. Jest to problem memoizacji funkcji, czyli
buforowanie wyniku funkcji, aby musiał być obliczany tylko raz. Nasze rozwiązanie będzie współ-
bieżnie bezpieczne i pozwoli uniknąć rywalizacji związanej z projektami opartymi na pojedynczej
blokadzie dla całej pamięci podręcznej.
Użyjemy poniższej funkcji httpGetBody jako przykładu typu funkcji, który moglibyśmy memoizo-
wać. Funkcja wysyła żądanie HTTP GET i odczytuje ciało odpowiedzi. Wywołania tej funkcji są
stosunkowo kosztowne, więc chcielibyśmy uniknąć powtarzania ich niepotrzebnie.
func httpGetBody(url string) (interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
Końcowa linia kodu kryje drobną subtelność. ReadAll zwraca dwa wyniki: []byte i error,
ponieważ jednak są one przypisywalne do zadeklarowanych typów wyniku funkcji httpGetBody
(odpowiednio interface{} i error), możemy zwrócić wynik tego wywołania bez zbędnych ceregieli.
Wybraliśmy ten typ zwracania dla funkcji httpGetBody, aby był on zgodny z typem funkcji, które
to funkcje nasza pamięć podręczna ma memoizować.
Oto pierwsza wersja pamięci podręcznej:
code/r09/memo1
// Package memo zapewnia memoizację funkcji typu Func.
// Ta memoizacja nie jest współbieżnie bezpieczna.
package memo

// Memo buforuje wyniki wywołania Func.


type Memo struct {
f Func
cache map[string]result
}

// Func jest typem funkcji, która ma być zmemoizowana.


type Func func(key string) (interface{}, error)

type result struct {


value interface{}
err error
}

func New(f Func) *Memo {


return &Memo{f: f, cache: make(map[string]result)}
}
268 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

// UWAGA: to nie jest współbieżnie bezpieczne!


func (memo *Memo) Get(key string) (interface{}, error) {
res, ok := memo.cache[key]
if !ok {
res.value, res.err = memo.f(key)
memo.cache[key] = res
}
return res.value, res.err
}
Instancja Memo przechowuje przeznaczoną do zmemoizowania funkcję f typu Func oraz pamięć
podręczną, która jest mapowaniem z łańcuchów znaków na strukturę result. Każda instancja
result jest po prostu parą wyników zwracanych przez wywołanie funkcji f — wartością i błędem.
Wraz z rozwijaniem programu pokażemy kilka wersji Memo, ale wszystkie będą miały wspólne te
podstawowe aspekty.
Poniżej został pokazany przykład sposobu korzystania ze struktury Memo. Dla każdego elementu
w strumieniu przychodzących adresów URL wywołujemy metodę Get, rejestrując opóźnienie
wywołania oraz ilość danych, które zwraca:
m := memo.New(httpGetBody)
for url := range incomingURLs() {
start := time.Now()
value, err := m.Get(url)
if err != nil {
log.Print(err)
}
fmt.Printf("%s, %s, %d bajtów\n",
url, time.Since(start), len(value.([]byte)))
}
Możemy użyć pakietu testing (zob. rozdział 11.) do systematycznego zbadania efektu memoizacji.
Na podstawie danych wyjściowych poniższego testu możemy stwierdzić, że strumień adresów URL
zawiera duplikaty, i chociaż pierwsze wywołanie (*Memo).Get dla każdego adresu URL trwa setki
milisekund, drugie żądanie zwraca taką samą ilość danych w czasie poniżej milisekundy.
$ go test -v code/r09/memo1
=== RUN Test
https://golang.org, 175.026418ms, 7537 bajtów
https://godoc.org, 172.686825ms, 6878 bajtów
https://play.golang.org, 115.762377ms, 5767 bajtów
http://gopl.io, 749.887242ms, 2856 bajtów
https://golang.org, 721ns, 7537 bajtów
https://godoc.org, 152ns, 6878 bajtów
https://play.golang.org, 205ns, 5767 bajtów
http://gopl.io, 326ns, 2856 bajtów
--- PASS: Test (1.21s)
PASS
ok code/r09/memo1 1.257s
Powyższy test wykonuje wszystkie wywołania Get sekwencyjnie.
Ponieważ żądania HTTP są doskonałą okazją do zastosowania równoległości, zmieńmy test w taki
sposób, aby wykonywał wszystkie żądania współbieżnie. Ten test wykorzystuje sync.WaitGroup,
aby przed powróceniem zaczekać na zakończenie ostatniego żądania.
m := memo.New(httpGetBody)
var n sync.WaitGroup
for url := range incomingURLs() {
n.Add(1)
9.7. PRZYKŁAD: WSPÓŁBIEŻNA NIEBLOKUJĄCA PAMIĘĆ PODRĘCZNA 269

go func(url string) {
start := time.Now()
value, err := m.Get(url)
if err != nil {
log.Print(err)
}
fmt.Printf("%s, %s, %d bajtów\n",
url, time.Since(start), len(value.([]byte)))
n.Done()
}(url)
}
n.Wait()
Test przebiega znacznie szybciej, ale jest mało prawdopodobne, aby działał poprawnie cały czas.
Możemy zauważyć niespodziewane odwołania poza pamięć podręczną albo odwołania do pamięci
podręcznej, które zwracają nieprawidłowe wartości, lub nawet awarie.
Co gorsza, test prawdopodobnie będzie działać poprawnie przez jakiś czas, więc możemy nawet
nie zauważyć, że ma problem. Jeśli jednak uruchomimy go z flagą -race, detektor wyścigów
(zob. podrozdział 9.6) często wyświetli raport taki jak ten:
$ go test -run=TestConcurrent -race -v code/r09/memo1
=== RUN TestConcurrent
...
WARNING: DATA RACE
Write by goroutine 36:
runtime.mapassign1()
~/go/src/runtime/hashmap.go:411 +0x0
code/r09/memo1.(*Memo).Get()
~/gobook/src/code/r09/memo1/memo.go:32 +0x205
...
Previous write by goroutine 35:
runtime.mapassign1()
~/go/src/runtime/hashmap.go:411 +0x0
code/r09/memo1.(*Memo).Get()
~/gobook/src/code/r09/memo1/memo.go:32 +0x205
...
Found 1 data race(s)
FAIL code/r09/memo1 2.393s
Referencja do memo.go:32 mówi nam, że dwie funkcje goroutine zaktualizowały mapę cache bez
żadnej synchronizacji pomiędzy tymi operacjami. Metoda Get nie jest współbieżnie bezpieczna: ma
wyścig danych.
28 func (memo *Memo) Get(key string) (interface{}, error) {
29 res, ok := memo.cache[key]
30 if !ok {
31 res.value, res.err = memo.f(key)
32 memo.cache[key] = res
33 }
34 return res.value, res.err
35 }
Najprostszym sposobem uczynienia pamięci podręcznej współbieżnie bezpieczną jest użycie
synchronizacji opartej na monitorze. Musimy jedynie dodać mutex do Memo, założyć blokadę
muteksu przy uruchomieniu metody Get i zwolnić blokadę, zanim Get powróci, aby obie operacje
cache zaszły w obrębie sekcji krytycznej:
270 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

code/r09/memo2
type Memo struct {
f Func
mu sync.Mutex // strzeże cache
cache map[string]result
}

// Get jest współbieżnie bezpieczna.


func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.mu.Lock()
res, ok := memo.cache[key]
if !ok {
res.value, res.err = memo.f(key)
memo.cache[key] = res
}
memo.mu.Unlock()
return res.value, res.err
}
Teraz detektor wyścigów nie odzywa się, nawet gdy uruchomimy testy współbieżnie. Niestety,
ta zmiana w Memo niweluje nasze wcześniejsze zyski wydajności. W wyniku przytrzymania blokady
na czas każdego wywołania f metoda Get szereguje wszystkie operacje we-wy, które chcieliśmy
zrównoleglić. Potrzebujemy nieblokującej pamięci podręcznej, która nie szereguje memoizowanych
wywołań funkcji.
W przedstawionej poniżej kolejnej implementacji Get wywołująca funkcja goroutine zakłada
blokadę dwukrotnie: pierwszy raz dla wyszukiwania i drugi dla aktualizacji, jeśli wyszukiwanie
niczego nie zwróciło. Pomiędzy tymi blokadami pozostałe funkcje goroutine mogą swobodnie
korzystać z pamięci podręcznej.
code/r09/memo3
func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.mu.Lock()
res, ok := memo.cache[key]
memo.mu.Unlock()
if !ok {
res.value, res.err = memo.f(key)

// Pomiędzy tymi dwiema sekcjami krytycznymi kilka funkcji goroutine może się ścigać,
// aby obliczyć f(key) i zaktualizować mapę.
memo.mu.Lock()
memo.cache[key] = res
memo.mu.Unlock()
}
return res.value, res.err
}
Wydajność ponownie się poprawia, ale teraz widzimy, że niektóre adresy URL są pobierane
dwukrotnie. Dzieje się tak, gdy co najmniej dwie funkcje goroutine wywołują Get dla tego samego
adresu URL mniej więcej w tym samym czasie. Obie sprawdzają pamięć podręczną, nie znajdują
tam wartości, a następnie wywołują powolną funkcję f. Następnie obie aktualizują mapę uzy-
skanym wynikiem. Jeden z wyników jest nadpisywany przez drugi.
Najlepiej byłoby uniknąć tej zbędnej pracy. Taka funkcjonalność jest czasem nazywana ograni-
czeniem duplikatów (ang. duplicate suppression). W poniższej wersji Memo każdy element mapy
jest wskaźnikiem do struktury entry. Tak jak poprzednio każda instancja entry zawiera zmemoizo-
9.7. PRZYKŁAD: WSPÓŁBIEŻNA NIEBLOKUJĄCA PAMIĘĆ PODRĘCZNA 271

wany wynik wywołania funkcji f, ale dodatkowo zawiera kanał o nazwie ready. Zaraz po ustawieniu
wartości result struktury entry ten kanał zostanie zamknięty, aby rozgłaszać (zob. podrozdział 8.9)
do wszystkich innych funkcji goroutine, że teraz mogą bezpiecznie odczytywać wynik z entry.
code/r09/memo4
type entry struct {
res result
ready chan struct{} // zamykany, gdy res jest gotowy
}

func New(f Func) *Memo {


return &Memo{f: f, cache: make(map[string]*entry)}
}

type Memo struct {


f Func
mu sync.Mutex // strzeże cache
cache map[string]*entry
}

func (memo *Memo) Get(key string) (value interface{}, err error) {


memo.mu.Lock()
e := memo.cache[key]
if e == nil {
// To jest pierwsze żądanie dla tego klucza.
// Ta funkcja goroutine staje się odpowiedzialna za obliczanie wartości
// i rozgłaszanie stanu gotowości.
e = &entry{ready: make(chan struct{})}
memo.cache[key] = e
memo.mu.Unlock()

e.res.value, e.res.err = memo.f(key)

close(e.ready) // rozgłasza stan gotowości


} else {
// To jest powtórzone żądanie dla tego klucza.
memo.mu.Unlock()

<-e.ready // czeka na stan gotowości


}
return e.res.value, e.res.err
}
Wywołanie Get obejmuje teraz: założenie blokady muteksu strzegącej mapy cache, wyszukiwanie
w mapie wskaźnika dla istniejącego entry, alokowanie i wstawianie nowego entry, jeśli żaden
wpis nie został znaleziony, a następnie zwolnienie blokady. Jeśli jest istniejący wpis entry, jego
wartość niekoniecznie jest już gotowa (inna funkcja goroutine może nadal wywoływać powolną
funkcję f), więc wywołująca funkcja goroutine musi czekać na stan „gotowości” dla entry, zanim
będzie mogła odczytać jego wartość result. Robi to, odczytując wartość z kanału ready, ponie-
waż ta operacja blokuje, dopóki kanał nie zostanie zamknięty.
Jeśli nie było istniejącego entry, wtedy poprzez wstawienie w mapie nowego wpisu entry „niego-
towy” bieżąca funkcja goroutine staje się odpowiedzialna za wywołanie powolnej funkcji, zaktuali-
zowanie entry i rozgłaszanie gotowości nowego wpisu entry wszystkim pozostałym funkcjom
goroutine, które mogą już na niego czekać.
272 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

Należy zwrócić uwagę, że zmienne e.res.value i e.res.err w entry są współdzielone między


wieloma funkcjami goroutine. Funkcja goroutine, która tworzy entry, ustawia jego wartości,
a inne funkcje goroutine odczytują te wartości, gdy rozgłoszony zostanie stan gotowości. Pomimo
uzyskiwania dostępu przez wiele funkcji goroutine nie ma konieczności stosowania blokady muteksu.
Zamknięcie kanału ready dzieje się, zanim jakakolwiek inna funkcja goroutine odbierze rozgłaszane
zdarzenie, więc operacje zapisu w tych zmiennych w pierwszej funkcji goroutine mają miejsce przed
tym, zanim te zmienne zostaną odczytane przez kolejne funkcje goroutine. Nie ma wyścigu danych.
Nasza współbieżna, ograniczająca duplikaty, nieblokująca pamięć podręczna jest gotowa.
Powyższa implementacja Memo używa muteksu do strzeżenia zmiennej mapy, która jest współdzielo-
na przez każdą funkcję goroutine wywołującą Get. Ciekawe może być porównanie tego rozwiązania
z alternatywnym, polegającym na tym, że zmienna mapy jest zamknięta w monitorującej funkcji
goroutine, do której podmioty wywołujące Get muszą wysyłać komunikaty.
Deklaracje Func, result i entry pozostają takie same jak poprzednio:
// Func jest typem funkcji do memoizacji.
type Func func(key string) (interface{}, error)

// result jest wynikiem wywołania Func.


type result struct {
value interface{}
err error
}

type entry struct {


res result
ready chan struct{} // zamykany, gdy res jest gotowy
}
Jednak typ Memo składa się teraz z kanału requests, poprzez który podmiot wywołujący Get
komunikuje się z monitorującą funkcją goroutine. Typem elementów kanału jest request. Używając
tej struktury, podmiot wywołujący Get wysyła monitorującej funkcji goroutine zarówno klucz (czyli
argument dla zmemoizowanej funkcji), jak i kolejny kanał response, przez który powinien być
odesłany wynik, gdy tylko stanie się dostępny. Ten kanał będzie przenosił pojedynczą wartość.
code/r09/memo5
// Typ request jest komunikatem żądania, aby funkcja Func została zastosowana do klucza.
type request struct {
key string
response chan<- result // klient chce pojedynczy wynik
}

type Memo struct{ requests chan request }

// Funkcja New zwraca memoizację funkcji f. Klienty muszą kolejno wywoływać Close.
func New(f Func) *Memo {
memo := &Memo{requests: make(chan request)}
go memo.server(f)
return memo
}

func (memo *Memo) Get(key string) (interface{}, error) {


response := make(chan result)
memo.requests <- request{key, response}
res := <-response
9.7. PRZYKŁAD: WSPÓŁBIEŻNA NIEBLOKUJĄCA PAMIĘĆ PODRĘCZNA 273

return res.value, res.err


}

func (memo *Memo) Close() { close(memo.requests) }


Powyższa metoda Get tworzy kanał odpowiedzi, umieszcza go w żądaniu, wysyła go do monitorują-
cej funkcji goroutine i natychmiast odbiera od niej.
Zmienna cache jest zamknięta w pokazanej poniżej monitorującej funkcji goroutine (*Memo).server.
Monitor odczytuje żądania w pętli, dopóki kanał żądań nie zostanie zamknięty przez metodę
Close. Dla każdego żądania sprawdza pamięć podręczną, tworząc i wstawiając nowy wpis entry,
jeśli żaden nie został znaleziony.
func (memo *Memo) server(f Func) {
cache := make(map[string]*entry)
for req := range memo.requests {
e := cache[req.key]
if e == nil {
// To jest pierwsze żądanie dla tego klucza.
e = &entry{ready: make(chan struct{})}
cache[req.key] = e
go e.call(f, req.key) // wywołuje f(key)
}
go e.deliver(req.response)
}
}

func (e *entry) call(f Func, key string) {


// Ewaluuje funkcję.
e.res.value, e.res.err = f(key)
// Rozgłasza stan gotowości.
close(e.ready)
}

func (e *entry) deliver(response chan<- result) {


// Czeka na stan gotowości.
<-e.ready
// Wysyła wynik do klienta.
response <- e.res
}
W podobny sposób jak w wersji opartej na muteksie pierwsze żądanie dla danego klucza staje się
odpowiedzialne za wywołanie funkcji f na tym kluczu, zapisanie wyniku w entry oraz rozgłaszanie
gotowości entry poprzez zamknięcie kanału ready. Odbywa się to poprzez (*entry).call.
Kolejne żądanie dla tego samego klucza znajduje istniejący wpis entry w mapie, czeka, aż wynik
będzie gotowy, i wysyła go poprzez kanał odpowiedzi do klienckiej funkcji goroutine, która wywołała
metodę Get. Odbywa się to poprzez (*entry).deliver. Metody call i deliver muszą być
wywoływane we własnych funkcjach goroutine, aby zapewnić, że monitorująca funkcja goroutine
nie przestanie przetwarzać nowych żądań.
Ten przykład pokazuje, że można zbudować wiele współbieżnych struktur bez nadmiernej złożo-
ności, wykorzystując jedno z dwóch podejść: współdzielone zmienne i blokady lub komunikację
procesów sekwencyjnych. Nie zawsze jest oczywiste, które podejście będzie korzystne w danej sy-
tuacji, ale warto wiedzieć, jak ma się jedno do drugiego. Czasami przełączenie się z jednego podejścia
na drugie może uprościć Twój kod.
274 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

Ćwiczenie 9.3. Rozszerz typ Func i metodę (*Memo).Get w taki sposób, aby podmioty wywołujące mo-
gły dostarczyć opcjonalny kanał done, przez który będą mogły anulować operację (zob. podrozdział 8.9).
Wyniki anulowanego wywołania Func nie powinny być buforowane w pamięci podręcznej.

9.8. Funkcje goroutine i wątki


W poprzednim rozdziale stwierdziliśmy, że możemy na razie zignorować różnice między funkcjami
goroutine i wątkami systemu operacyjnego (OS). Chociaż różnice między nimi zasadniczo są ilo-
ściowe, to wystarczająco duża różnica ilościowa staje się jakościową, i tak samo jest z funkcjami go-
routine i wątkami. Nadszedł czas, aby zbadać te różnice.

9.8.1. Stosy o zmiennym rozmiarze


Każdy wątek systemu operacyjnego ma stałego rozmiaru blok pamięci (najczęściej o wielkości 2 MB)
przeznaczony dla stosu, czyli przestrzeni roboczej, w której zapisywane są lokalne zmienne wywołań
funkcji będących w trakcie wykonywania lub czasowo zawieszonych na czas wywołania innej funkcji.
Ten stos o stałym rozmiarze to jednocześnie zbyt wiele i zbyt mało. 2 MB stosu byłyby wielkim mar-
notrawstwem pamięci dla niewielkiej funkcji goroutine, np. takiej, która jedynie czeka, aż WaitGroup
zamknie kanał. Nie jest rzadkością, że program Go tworzy setki tysięcy funkcji goroutine w tym
samym czasie, co byłoby niemożliwe przy tak dużych stosach. Jednak mimo ich wielkości stosy
o stałym rozmiarze nie są wystarczająco duże dla większości złożonych i głęboko rekurencyjnych
funkcji. Zmiana ustalonego rozmiaru może poprawić wydajność przestrzeni i pozwolić na tworzenie
większej liczby wątków albo umożliwić tworzenie funkcji o głębszej rekurencji, ale nie jedno i drugie.
Natomiast funkcja goroutine zaczyna życie z małym stosem, zazwyczaj o wielkości 2 kB. Stos funkcji
goroutine, podobnie jak stos wątku systemu operacyjnego, przechowuje zmienne lokalne aktyw-
nych i zawieszonych wywołań funkcji, ale, w przeciwieństwie do wątku systemu operacyjnego, nie
ma stałego rozmiaru. Rośnie i maleje w miarę potrzeby. Limit rozmiaru dla stosu funkcji goroutine
może wynosić nawet 1 GB, więc jest o rzędy wielkości większy niż typowy stos wątkowy o stałym
rozmiarze, choć oczywiście niewiele funkcji goroutine używa aż tyle pamięci.
Ćwiczenie 9.4. Skonstruuj potok łączący dowolną liczbę funkcji goroutine za pomocą kanałów.
Jaka jest maksymalna liczba etapów potoku, którą można utworzyć bez wyczerpania pamięci?
Jak długo trwa przesłanie wartości przez cały potok?

9.8.2. Planowanie funkcji goroutine


Wątki systemu operacyjnego są rozplanowywane przez jądro systemu operacyjnego. Co kilka milise-
kund zegar sprzętowy przerywa działanie procesora, co powoduje wywołanie funkcji jądra zwa-
nej planistą (ang. scheduler). Ta funkcja zawiesza aktualnie wykonywany wątek i zapisuje jego
rejestry w pamięci, przegląda listę wątków i decyduje, który należy uruchomić jako następny,
przywraca z pamięci rejestry tego wątku, a następnie wznawia jego wykonywanie. Ponieważ wątki
systemu operacyjnego są rozplanowywane przez jądro, przekazanie sterowania z jednego wątku
do drugiego wymaga pełnego przełączania kontekstu, tzn. zapisania w pamięci stanu jednego
wątku użytkownika, przywrócenia stanu drugiego wątku oraz zaktualizowania struktury danych
planisty. Ta operacja jest powolna z powodu słabej lokalizacji i liczby wymaganych operacji dostępu
do pamięci, a w ujęciu historycznym sytuacja tylko się pogarszała wraz ze wzrostem liczby cykli
procesora wymaganych do uzyskania dostępu do pamięci.
9.8. FUNKCJE GOROUTINE I WĄTKI 275

Środowisko wykonawcze języka Go zawiera własnego planistę, który wykorzystuje technikę zwaną
planowaniem m:n, ponieważ multipleksuje (lub planuje) m funkcji goroutine na n wątkach systemu
operacyjnego. Zadanie planisty Go jest analogiczne do zadania planisty jądra systemu operacyjnego,
ale dotyczy tylko funkcji goroutine pojedynczego programu Go.
W przeciwieństwie do planisty wątków systemu operacyjnego, planista Go nie jest wywoływany
cyklicznie przez zegar sprzętowy, ale pośrednio przez określone konstrukcje języka Go. Przykładowo:
gdy funkcja goroutine wywołuje time.Sleep albo blokuje w operacji kanału lub muteksu, planista
usypia ją i uruchamia kolejną funkcję goroutine do momentu, aż nadejdzie czas wybudzenia tej
pierwszej. Ponieważ nie wymaga to przełączenia na kontekst jądra, zmiana rozplanowania funkcji
goroutine jest o wiele mniej kosztowna niż zmiana rozplanowania wątku.
Ćwiczenie 9.5. Napisz program z dwiema funkcjami goroutine, które wysyłają komunikaty tam
i z powrotem przez dwa niebuforowane kanały w sposób podobny do gry w tenisa stołowego. Ile
komunikacji na sekundę może utrzymać ten program?

9.8.3. Parametr GOMAXPROCS


Planista Go wykorzystuje parametr zwany GOMAXPROCS do ustalenia, jak wiele wątków systemu
operacyjnego może aktywnie wykonywać kod Go jednocześnie. Domyślną wartością tego parametru
jest liczba procesorów maszyny, więc na komputerze z ośmioma procesorami planista zaplanuje
wykonywanie kodu Go na maksymalnie ośmiu wątkach systemu operacyjnego na raz. (Parametr
GOMAXPROCS to zmienna n w planowaniu m:n). Funkcje goroutine, które są uśpione lub zablokowane
w komunikacji, w ogóle nie potrzebują wątku. Funkcje goroutine, które są zablokowane w operacji
we-wy lub innych wywołaniach systemowych albo wywołują funkcje inne niż funkcje Go, wyma-
gają wątku systemu operacyjnego, ale parametr GOMAXPROCS nie musi ich brać pod uwagę.
Można bezpośrednio kontrolować ten parametr, używając zmiennej środowiskowej GOMAXPROCS
lub funkcji runtime.GOMAXPROCS. Możemy zobaczyć efekt GOMAXPROCS w poniższym niewielkim
programie, który wyświetla niekończący się strumień zer i jedynek:
for {
go fmt.Print(0)
fmt.Print(1)
}

$ GOMAXPROCS=1 go run hacker-cliché.go


111111111111111111110000000000000000000011111...

$ GOMAXPROCS=2 go run hacker-cliché.go


010101010101010101011001100101011010010100110...
W pierwszym uruchomieniu wykonywana była co najwyżej jedna funkcja jednocześnie. Począt-
kowo była to główna funkcja goroutine, która wyświetla jedynki. Po pewnym czasie planista Go
uśpił ją i obudził funkcję goroutine, która wyświetla zera, dając teraz jej szansę na wykonywanie
w wątku systemu operacyjnego. W drugim uruchomieniu były dostępne dwa wątki systemu
operacyjnego, więc obie funkcje goroutine działały jednocześnie, wyświetlając cyfry mniej więcej
w tym samym tempie. Musimy podkreślić, że w planowanie funkcji goroutine zaangażowanych
jest wiele czynników, a środowisko wykonawcze stale ewoluuje, więc Twoje wyniki mogą się różnić
od tych przedstawionych powyżej.
276 ROZDZIAŁ 9. WSPÓŁBIEŻNOŚĆ ZE WSPÓŁDZIELENIEM ZMIENNYCH

Ćwiczenie 9.6. Zmierz, w jaki sposób wydajność programu równoległego ograniczonego mocą
obliczeniową procesora (zob. ćwiczenie 8.5) zmienia się wraz z parametrem GOMAXPROCS. Jaka jest
optymalna wartość na Twoim komputerze? Ile procesorów ma Twój komputer?

9.8.4. Funkcje goroutine nie mają identyfikatora


W większości systemów operacyjnych i języków programowania obsługujących wielowątkowość
aktualny wątek ma odrębny identyfikator, który można łatwo uzyskać w postaci zwykłej wartości,
zazwyczaj liczby całkowitej lub wskaźnika. Dzięki temu łatwo jest zbudować abstrakcję zwaną
pamięcią lokalną wątków (ang. thread-local storage), która jest w istocie globalną mapą z kluczami
w postaci identyfikatorów wątków, aby każdy wątek mógł zapisywać i pobierać wartości niezależne
od innych wątków.
Funkcje goroutine nie mają pojęcia identyfikatora, który jest dostępny dla programisty. Jest to
zamierzone, ponieważ pamięć lokalna wątków bywa nadużywana. Przykładowo: w serwerze
WWW zaimplementowanym w języku z pamięcią lokalną wątków dla wielu funkcji typowe jest
wyszukiwanie w tej pamięci informacji o żądaniach HTTP, na rzecz których obecnie pracują.
Jednak podobnie jak w przypadku programów, które opierają się nadmiernie na zmiennych glo-
balnych, może to prowadzić do niezdrowego „działania na odległość”, w którym zachowanie
funkcji nie jest określane tylko przez jej argumenty, ale przez identyfikator wątku, w którym
działa. W konsekwencji, jeśli zmieni się identyfikator wątku (np. zostaną zaangażowane do pomocy
jakieś wątki robocze), funkcja będzie zachowywać się w tajemniczy sposób.
Język Go promuje prostszy styl programowania, w którym parametry wpływające na zachowa-
nie funkcji są jednoznaczne. Dzięki temu nie tylko programy są czytelniejsze, ale można również
swobodnie przypisywać podzadania danej funkcji do wielu różnych funkcji goroutine bez przejmo-
wania się ich identyfikatorami.
Poznałeś już wszystkie funkcjonalności języka potrzebne do pisania programów Go. W kolejnych
dwóch rozdziałach zrobimy krok do tyłu, aby przyjrzeć się pewnym praktykom i narzędziom,
które wspierają programowanie na dużą skalę. Obejmuje to takie kwestie, jak organizowanie projektu
jako zbiór pakietów oraz sposoby pozyskiwania, kompilowania, testowania, benchmarkowania,
profilowania, dokumentowania i udostępniania tych pakietów.
Rozdział 10

Pakiety i narzędzie go

Obecnie skromnej wielkości program może zawierać 10 000 funkcji. Jednak jego twórca musi myśleć
o tylko kilku z nich, a jeszcze mniej zaprojektować, ponieważ większość funkcji została napisana
przez innych i udostępniona do ponownego wykorzystania poprzez pakiety.
Język Go ma ponad 100 standardowych pakietów, które zapewniają fundamenty dla większości apli-
kacji. Społeczność Go, będąca kwitnącym ekosystemem projektowania, udostępniania, ponowne-
go wykorzystywania i doskonalenia pakietów, opublikowała o wiele więcej. Przeszukiwalny indeks
tych pakietów można znaleźć na stronie: http://godoc.org. W tym rozdziale pokażemy, jak korzy-
stać z istniejących pakietów i jak tworzyć nowe.
Go jest również wyposażony w narzędzie go, które jest wyrafinowanym, ale prostym w użyciu
poleceniem do zarządzania obszarami roboczymi pakietów języka Go. Od początku książki poka-
zujemy, jak używać narzędzia go do pobierania, kompilowania i uruchamiania przykładowych
programów. W tym rozdziale przyjrzymy się bazowym koncepcjom tego narzędzia i zobaczymy
więcej jego możliwości, które obejmują wyświetlanie dokumentacji i odpytywanie metadanych na
temat pakietów w obszarze roboczym. W następnym rozdziale zbadamy jego możliwości dotyczące
testowania.

10.1. Wprowadzenie
Celem każdego systemu pakietów jest usprawnienie procesów projektowania i utrzymywania
dużych programów poprzez grupowanie powiązanych funkcji w jednostki, które można łatwo
zrozumieć i zmieniać niezależnie od innych pakietów programu. Ta modułowość pozwala współ-
dzielić i ponownie wykorzystywać pakiety w różnych projektach rozproszonych w ramach orga-
nizacji lub udostępniać je szerszemu gronu odbiorców.
Każdy pakiet definiuje odrębną przestrzeń nazw, do której ograniczone są jego identyfikatory.
Każda nazwa jest powiązana z konkretnym pakietem, co pozwala wybierać krótkie, jasne nazwy dla
najczęściej używanych typów, funkcji i innych elementów bez tworzenia konfliktów z innymi czę-
ściami programu.
Pakiety zapewniają również hermetyzację (ang. encapsulation) poprzez kontrolowanie tego,
które nazwy są widoczne lub eksportowane na zewnątrz pakietu. Ograniczenie widoczności ele-
mentów pakietu ukrywa funkcje pomocnicze oraz typy stojące za interfejsem API tego pakietu, dzię-
ki czemu jego opiekun może zmieniać implementację z przekonaniem, że nie będzie to miało
278 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO

wpływu na żaden kod spoza pakietu. Ograniczenie widoczności ukrywa również zmienne, aby
klienty mogły uzyskiwać do nich dostęp i aktualizować je tylko poprzez eksportowane funkcje, co
pozwala zachować wewnętrzne niezmienniki lub egzekwować wzajemne wykluczanie we współbież-
nym programie.
Gdy zmieniamy jakiś plik, musimy ponownie skompilować pakiet tego pliku oraz potencjalnie
wszystkie pakiety, które od niego zależą. Kompilacja w języku Go jest znacznie szybsza niż w więk-
szości innych języków kompilowanych, nawet przy budowaniu od podstaw. Istnieją trzy główne
kwestie wpływające na prędkość kompilatora. Po pierwsze, wszystkie importy muszą być bezpo-
średnio wymienione na początku każdego pliku źródłowego, aby kompilator nie musiał czytać
i przetwarzać całego pliku w celu określenia jego zależności. Po drugie, zależności pakietu tworzą
skierowany graf acykliczny, a ponieważ nie ma żadnych cykli, pakiety mogą zostać skompilowane
oddzielnie i być może równolegle. Po trzecie, plik obiektu dla skompilowanego pakietu Go reje-
struje informacje eksportowe nie tylko dla samego pakietu, ale również dla jego zależności. Podczas
kompilowania pakietu kompilator musi odczytać jeden plik obiektu dla każdego importu, ale nie
musi patrzeć poza te pliki.

10.2. Ścieżki importów


Każdy pakiet jest identyfikowany przez unikatowy łańcuch znaków nazywany jego ścieżką importu.
Ścieżki importów to łańcuchy znaków, które pojawiają się w deklaracjach import.
import (
"fmt"
"math/rand"
"encoding/json"

"golang.org/x/net/html"

"github.com/go-sql-driver/mysql"
)
Jak wspomniano w punkcie 2.6.1, specyfikacja języka Go nie definiuje znaczenia tych łańcuchów
znaków ani sposobu określania ścieżki importu pakietu, ale pozostawia te kwestie narzędziom.
W tym rozdziale przyjrzymy się szczegółowo temu, w jaki sposób ścieżki importów interpretuje na-
rzędzie go, ponieważ właśnie tego narzędzia większość programistów Go używa do kompilowania,
testowania itd. Oczywiście istnieją też inne narzędzia. Programiści Go używający np. wewnętrzne-
go wielojęzykowego systemu kompilacji Google stosują inne reguły nazywania i lokalizowania pa-
kietów, określania testów itd., które to reguły bardziej odpowiadają konwencjom tego systemu.
W przypadku pakietów, które zamierzasz udostępniać lub publikować, ścieżki importów powinny
być unikatowe w skali globalnej. Aby uniknąć konfliktów, ścieżki importów wszystkich pakietów
innych niż te ze standardowej biblioteki powinny zaczynać się nazwą domeny internetowej organizacji,
która jest właścicielem danego pakietu lub go hostuje. Umożliwia to również wyszukiwanie pakie-
tów. Powyższa deklaracja importuje np. parser HTML utrzymywany przez zespół Go oraz popu-
larny zewnętrzny sterownik bazy danych MySQL.
10.3. DEKLARACJA PACKAGE 279

10.3. Deklaracja package


Deklaracja package jest wymagana na początku każdego pliku źródłowego Go. Jej głównym celem
jest określenie domyślnego identyfikatora dla danego pakietu (tzw. nazwy pakietu), gdy jest on im-
portowany przez inny pakiet.
Przykładowo: każdy plik z pakietu math/rand rozpoczyna się deklaracją package rand, więc kiedy
zaimportujesz ten pakiet, możesz uzyskiwać dostęp do jego elementów jako rand.Int, rand.
Float64 itd.
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Int())
}
Zgodnie z konwencją nazwa pakietu jest ostatnim segmentem ścieżki importu, a w rezultacie dwa
pakiety mogą mieć taką samą nazwę, mimo że ich ścieżki importu różnią się z konieczności. Pa-
kiety, których ścieżkami importu są np. math/rand oraz crypto/rand, mają tę samą nazwę —
rand. Za chwilę zobaczymy, jak użyć obu tych pakietów w tym samym programie.
Istnieją trzy główne wyjątki od zasady „ostatniego segmentu”. Pierwszy jest taki, że pakiet de-
finiujący polecenie (wykonywalny program Go) zawsze ma nazwę main, niezależnie od ścieżki
importu tego pakietu. Jest to sygnał dla polecenia go build (zob. punkt 10.7.3), że musi wywołać
program konsolidujący (ang. linker), aby utworzyć plik wykonywalny.
Drugi wyjątek polega na tym, że niektóre pliki w katalogu mogą mieć przyrostek _test w nazwie pa-
kietu, jeśli nazwa pliku kończy się na _test.go. Taki katalog może definiować dwa pakiety: zwy-
czajowy pakiet oraz jeszcze jeden zwany zewnętrznym pakietem testowym. Przyrostek _test
sygnalizuje poleceniu go test, że musi skompilować oba pakiety, i wskazuje, które pliki należą do
każdego pakietu. Zewnętrzne pakiety testowe są stosowane w celu uniknięcia cykli w grafie impor-
tów, wynikających z zależności testu. Zostaną one omówione szczegółowo w punkcie 11.2.4.
Trzeci wyjątek jest taki, że niektóre narzędzia do zarządzania zależnościami dodają do ścieżek
importu pakietu przyrostek w postaci numeru wersji, np. "gopkg.in/yaml.v2". Nazwa pakietu
pomija ten przyrostek, więc w tym przypadku byłoby to po prostu yaml.

10.4. Deklaracje import


Plik źródłowy Go może zawierać zero lub więcej deklaracji import umieszczonych bezpośrednio
po deklaracji package i przed pierwszą deklaracją niebędącą importem. Każda deklaracja import
może określać ścieżkę importu pojedynczego pakietu lub wielu pakietów za pomocą listy umieszczo-
nej w nawiasach. Poniższe dwie formy są równoważne, ale druga forma jest bardziej powszechna.
import "fmt"
import "os"
import (
"fmt"
"os"
)
280 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO

Importowane pakiety mogą być grupowane poprzez wprowadzanie pustych linii. Takie pogru-
powanie zwykle wskazuje różne domeny. Kolejność nie jest istotna, ale zgodnie z konwencją linie
każdej grupy są posortowane alfabetycznie. (Narzędzia gofmt oraz goimports wykonają grupo-
wanie i sortowanie za Ciebie).
import (
"fmt"
"html/template"
"os"

"golang.org/x/net/html"
"golang.org/x/net/ipv4"
)
Jeśli musimy do jakiegoś pakietu zaimportować dwa pakiety, których nazwy są takie same, tak
jak math/rand i crypto/rand, deklaracja import musi określać alternatywną nazwę dla co najmniej
jednego z nich, aby uniknąć konfliktu. Nazywa się to importem ze zmianą nazwy.
import (
"crypto/rand"
mrand "math/rand" // alternatywna nazwa mrand pozwala uniknąć konfliktu
)
Taka alternatywna nazwa dotyczy tylko importującego pliku. Inne pliki, nawet znajdujące się w tym
samym pakiecie, mogą importować pakiet za pomocą jego domyślnej nazwy lub innej nazwy.
Import ze zmianą nazwy może być przydatny nawet, gdy nie ma żadnego konfliktu. Jeśli nazwa
importowanego pakietu jest nieporęczna, jak niekiedy ma to miejsce w przypadku kodu wyge-
nerowanego automatycznie, wygodniejsza może być nazwa skrócona. Ta sama nazwa skrócona
powinna być stosowana konsekwentnie, aby uniknąć zamieszania. Wybór alternatywnej nazwy
może pomóc w uniknięciu konfliktów z typowymi nazwami zmiennych lokalnych. W pliku
z wieloma zmiennymi lokalnymi nazwanymi path możemy np. zaimportować standardowy pakiet
"path" jako pathpkg.
Każda deklaracja import ustanawia zależność z bieżącego pakietu do importowanego pakietu.
Narzędzie go build zgłasza błąd, jeśli te zależności tworzą cykl.

10.5. Puste importy


Błędem jest importowanie pakietu do pliku i nieodwoływanie się do nazwy, którą on w tym pliku
definiuje. Czasami musimy jednak zaimportować pakiet tylko ze względu na efekty uboczne tej
czynności: ewaluację wyrażeń inicjatorów jego zmiennych na poziomie pakietu i wykonanie jego
funkcji init (zob. punkt 2.6.2). Aby powstrzymać błąd „niewykorzystywanego importu”, który
w przeciwnym razie by wystąpił, musimy użyć importu ze zmianą nazwy — z nazwą alternatywną
w postaci pustego identyfikatora (_). Jak zwykle do pustego identyfikatora nigdy nie można się
odwoływać.
import _ "image/png" // rejestrowanie dekodera PNG
Jest to znane jako pusty import. Najczęściej jest on używany do implementacji mechanizmu
czasu kompilacji, dzięki któremu program główny może włączyć opcjonalne funkcjonalności
poprzez pusty import dodatkowych pakietów. Najpierw zobaczymy, jak go używać, a potem zba-
damy, jak działa.
10.5. PUSTE IMPORTY 281

Pakiet image ze standardowej biblioteki eksportuje funkcję Decode, która odczytuje bajty z interfejsu
io.Reader, określa, jaki format obrazu został użyty do zakodowania danych, wywołuje odpowiedni
dekoder, a następnie zwraca wynikowy obiekt image.Image. Przy użyciu funkcji image.Decode
można łatwo zbudować prosty konwerter obrazów, który odczytuje obraz w jednym formacie i zapi-
suje go w innym:
code/r10/jpeg
// Polecenie jpeg odczytuje obraz PNG ze standardowego strumienia wejściowego
// i zapisuje go jako obraz JPEG do standardowego strumienia wyjściowego.
package main

import (
"fmt"
"image"
"image/jpeg"
_ "image/png" // rejestrowanie dekodera PNG
"io"
"os"
)

func main() {
if err := toJPEG(os.Stdin, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "jpeg: %v\n", err)
os.Exit(1)
}
}

func toJPEG(in io.Reader, out io.Writer) error {


img, kind, err := image.Decode(in)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Format wejściowy =", kind)
return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
}
Jeśli przekażemy dane wyjściowe z programu code/r03/mandelbrot (zob. podrozdział 3.3) do pro-
gramu konwertera, wykryje on format wejściowy PNG i zapisze wersję JPEG rysunku 3.3.
$ go build code/r03/mandelbrot
$ go build code/r10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
Format wejściowy = png
Należy zwrócić uwagę na pusty import image/png. Bez tej linii kodu program skompiluje i skon-
soliduje się jak zwykle, ale nie będzie mógł już rozpoznawać lub dekodować danych wejściowych
w formacie PNG:
$ go build code/r10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
jpeg: image: unknown format
A jak to działa? Standardowa biblioteka zapewnia dekodery dla formatów: GIF, PNG i JPEG,
a użytkownicy mogą zapewnić inne, jednak aby utrzymać niewielki rozmiar plików wykony-
walnych, dekodery nie są zawarte w aplikacji, dopóki nie zostaną bezpośrednio zażądane. Funkcja
image.Decode sprawdza tablicę obsługiwanych formatów. Każdy wpis w tablicy określa cztery
rzeczy: nazwę formatu, łańcuch znaków będący prefiksem wszystkich obrazów zakodowanych
w ten sposób, używany do wykrywania kodowania, funkcję Decode, która dekoduje zakodowany
282 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO

obraz, oraz funkcję DecodeConfig, która dekoduje tylko metadane obrazu, takie jak jego rozmiar
i przestrzeń barw. Wpis jest dodawany do tablicy poprzez wywołanie image.RegisterFormat,
zazwyczaj wykonywane z wewnątrz inicjatora pakietu dla pakietu obsługującego każdy format, ta-
kiego jak ten w image/png:
package png // image/png

func Decode(r io.Reader) (image.Image, error)


func DecodeConfig(r io.Reader) (image.Config, error)

func init() {
const pngHeader = "\x89PNG\r\n\x1a\n"
image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
W efekcie aplikacja potrzebuje jedynie pustego importu pakietu dla formatu, który ma być w stanie
dekodować funkcja image.Decode.
Pakiet database/sql używa podobnego mechanizmu, aby umożliwić użytkownikom instalowanie
tylko tych sterowników baz danych, których potrzebują, np.:
import (
"database/mysql"
_ "github.com/lib/pq" // włączenie obsługi dla Postgres
_ "github.com/go-sql-driver/mysql" // włączenie obsługi dla MySQL
)

db, err = sql.Open("postgres", dbname) // OK


db, err = sql.Open("mysql", dbname) // OK
db, err = sql.Open("sqlite3", dbname) // zwraca błąd: nieznany sterownik "sqlite3"
Ćwiczenie 10.1. Rozszerz program jpeg w taki sposób, aby konwertował dowolny obsługiwany for-
mat wejściowy na dowolny format wyjściowy, wykorzystując funkcję image.Decode do wykrywa-
nia formatu wejściowego oraz flagę do wybierania formatu wyjściowego.
Ćwiczenie 10.2. Zdefiniuj generyczną funkcję odczytującą pliki archiwum, zdolną do odczyty-
wania plików ZIP (archive/zip) oraz plików tar POSIX (archive/tar). Użyj mechanizmu reje-
strowania podobnego do opisanego powyżej, aby wsparcie dla każdego formatu plików mogło
być włączane za pomocą pustych importów.

10.6. Pakiety i nazewnictwo


W tym podrozdziale przedstawimy kilka porad dotyczących stosowania charakterystycznych
konwencji języka Go związanych z nazewnictwem pakietów i ich elementów.
Tworząc pakiety, stosuj krótkie nazwy, ale nie aż tak krótkie, aby stały się zagadkowe. Najczęściej
używane pakiety ze standardowej biblioteki mają nazwy: bufio, bytes, flag, fmt, http, io, json, os,
sort, sync i time.
Staraj się stosować nazwy opisowe i jednoznaczne, tam gdzie to możliwe. Nie nazywaj np. pakietu
narzędziowego util, gdy można zastosować bardziej szczegółowe i nadal zwięzłe nazwy, takie jak
imageutil lub ioutil. Unikaj wybierania takich nazw pakietów, które są powszechnie stosowane do
powiązanych zmiennych lokalnych, ponieważ w ten sposób możesz zmusić klienty tego pakietu
do używania importów ze zmianą nazwy, podobnie jak w przypadku pakietu path.
10.6. PAKIETY I NAZEWNICTWO 283

Nazwy pakietów zazwyczaj przyjmują formę liczby pojedynczej. Standardowe pakiety bytes, errors
i strings używają formy liczby mnogiej, aby uniknąć ukrywania odpowiadających im predekla-
rowanych typów oraz w przypadku go/types, aby uniknąć konfliktu ze słowem kluczowym.
Unikaj nazw pakietów, które mają już inne konotacje. Początkowo stosowaliśmy np. nazwę temp
dla pakietu konwersji temperatury w podrozdziale 2.5, ale nie trwało to długo. To był zły pomysł,
ponieważ „temp” jest niemal powszechnym synonimem dla słowa „temporary” (tymczasowy).
Przez krótki okres stosowaliśmy nazwę temperature, ale była zbyt długa i nie określała funkcji
pakietu. W końcu nazwaliśmy paczkę tempconv, co jest krótsze i analogiczne do strconv.
Przejdźmy teraz do nazewnictwa elementów pakietu. Ponieważ każda referencja do elementu
innego pakietu używa kwalifikowanego identyfikatora, takiego jak fmt.Println, ciężar opisania
elementu pakietu jest ponoszony w równych częściach przez nazwę pakietu oraz nazwę elementu.
Nie musimy wspominać o koncepcji formatowania w Println, ponieważ robi to już sama nazwa
pakietu fmt. Podczas projektowania pakietu należy się zastanowić, w jaki sposób współgrają ze sobą
te dwie części kwalifikowanego identyfikatora, a nie tylko rozważyć nazwę samego elementu. Oto
kilka charakterystycznych przykładów:
bytes.Equal flag.Int http.Get json.Marshal
Możemy wskazać kilka typowych wzorców nazewnictwa. Pakiet strings zapewnia szereg nieza-
leżnych funkcji do manipulowania łańcuchami znaków:
package strings

func Index(needle, haystack string) int

type Replacer struct{ /* ... */ }


func NewReplacer(oldnew ...string) *Replacer

type Reader struct{ /* ... */ }


func NewReader(s string) *Reader
Słowo string nie pojawia się w żadnej z nazw funkcji. Klienty odwołują się do nich jako
strings.Index, strings.Replacer itd.
Inne pakiety, które moglibyśmy opisać jako pakiety z pojedynczym typem, takie jak html/template
i math/rand, udostępniają jeden zasadniczy typ danych wraz z jego metodami oraz często funkcję
New do tworzenia instancji.
package rand // "math/rand"

type Rand struct{ /* ... */ }


func New(source Source) *Rand
Może to prowadzić do powtórzeń, tak jak w przypadku template.Template lub rand.Rand, dlatego
nazwy tego rodzaju pakietów są często szczególnie krótkie.
Na drugim biegunie znajdują się pakiety takie jak net/http, które mają wiele nazw pozbawionych
istotnej struktury, ponieważ wykonują skomplikowane zadania. Mimo ponad 20 typów i jeszcze
większej liczby funkcji, najważniejsze elementy tego pakietu mają najprostsze nazwy: Get, Post,
Handle, Error, Client czy Server.
284 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO

10.7. Narzędzie go
Pozostała część tego rozdziału została poświęcona narzędziu go, które jest używane do pobierania,
odpytywania, formatowania, kompilowania, testowania oraz instalowania pakietów kodu Go.
Narzędzie go łączy cechy zestawu różnorodnych narzędzi w jednym zestawie poleceń. Jest to
menedżer pakietów (analogiczny do apt lub rpm), który odpowiada na zapytania o swój inwen-
tarz pakietów, oblicza ich zależności i pobiera je ze zdalnych systemów kontroli wersji. Jest to też
system kompilacji, który oblicza zależności plików i wywołuje kompilatory, asemblery i konsoli-
datory, chociaż jest celowo mniej kompletny niż standardowe uniksowe polecenie make. Jest także
sterownikiem testów, jak zobaczymy w rozdziale 11.
Jego interfejs wiersza poleceń wykorzystuje styl „szwajcarskiego scyzoryka” z kilkunastoma podpole-
ceniami — niektóre z nich już poznaliśmy, np.: get, run, build i fmt. Można uruchomić polecenie
go help, aby wyświetlić indeks wbudowanej dokumentacji tego narzędzia, ale dla przykładu przed-
stawiliśmy poniżej listę najczęściej używanych poleceń:
$ go
...
build // kompilowanie pakietów i zależności
clean // usuwanie plików obiektów
doc // wyświetlanie dokumentacji dla pakietu lub symbolu
env // wyświetlanie informacji o zmiennych środowiskowych Go
fmt // uruchamianie gofmt na źródłach pakietu
get // pobieranie i instalowanie pakietów i zależności
install // kompilowanie i instalowanie pakietów i zależności
list // wyświetlanie listy pakietów
run // kompilowanie i uruchamianie programu Go
test // testowanie pakietów
version // wyświetlanie wersji Go
vet // uruchamianie narzędzia vet na pakietach

Use "go help [command]" for more information about a command.


// Aby uzyskać więcej informacji na temat polecenia, użyj "go help [polecenie]".
...
Aby ograniczyć potrzebę konfiguracji do minimum, narzędzie go opiera się w dużej mierze na kon-
wencjach. Mając np. daną nazwę pliku źródłowego Go, narzędzie może znaleźć zawierający go
pakiet, ponieważ każdy katalog zawiera pojedynczy pakiet, a ścieżka importu pakietu odpowiada
hierarchii katalogów w obszarze roboczym. Przy danej ścieżce importu pakietu narzędzie może
znaleźć odpowiedni katalog, w którym są przechowywane pliki obiektów. Może również znaleźć
adres URL serwera, na którym znajduje się repozytorium kodu źródłowego.

10.7.1. Organizacja obszaru roboczego


Jedyną konfiguracją, jakiej kiedykolwiek będzie potrzebować większość użytkowników, jest ustawie-
nie zmiennej środowiskowej GOPATH, która określa katalog główny obszaru roboczego. Przełączając
się do innego obszaru roboczego, użytkownicy aktualizują wartość GOPATH. Podczas pracy nad tą
książką możemy np. ustawić dla zmiennej GOPATH wartość $HOME/gobook:
$ export GOPATH=$HOME/gobook
Po pobraniu ze strony wydawnictwa wszystkich programów do tej książki i umieszczeniu ich
w odpowiednich katalogach, tak jak zostało to opisane we wstępie, oraz po pobraniu dodatkowych
10.7. NARZĘDZIE GO 285

pakietów z repozytorium za pomocą polecenia go get Twój obszar roboczy będzie zawierał hie-
rarchię katalogów podobną do poniższej:
GOPATH/
src/
code/
r01/
helloworld/
main.go
dup/
main.go
...
golang.org/x/net/
.git/
html/
parse.go
node.go
...
bin/
helloworld
dup
pkg/
darwin_amd64/
...
GOPATH ma trzy podkatalogi. Podkatalog src zawiera kod źródłowy. Każdy pakiet rezyduje w kata-
logu, którego nazwa względna w stosunku do $GOPATH/src jest ścieżką importu pakietu, np.
code/r01/helloworld. Należy zwrócić uwagę, że pojedynczy obszar roboczy GOPATH zawiera
w podkatalogu src wiele repozytoriów systemów kontroli wersji, takich jak golang.org lub github.com.
W podkatalogu pkg narzędzia kompilacji przechowują skompilowane pakiety, a podkatalog bin
przechowuje wykonywalne programy, takie jak helloworld.
Druga zmienna środowiskowa GOROOT określa katalog główny dystrybucji Go, który zawiera wszyst-
kie pakiety biblioteki standardowej. Struktura katalogów poniżej GOROOT przypomina tę dla GOPATH,
więc np. pliki źródłowe pakietu fmt znajdują się w katalogu $GOROOT/src/fmt. Użytkownicy nie muszą
nigdy ustawiać zmiennej środowiskowej GOROOT, ponieważ domyślnie narzędzie go będzie używać
lokalizacji, w której zostało zainstalowane.
Polecenie go env wyświetla faktyczne wartości zmiennych środowiskowych istotnych dla zestawu
narzędzi, w tym wartości domyślne dla brakujących zmiennych. Zmienna środowiskowa GOOS
określa docelowy system operacyjny (np.: android, linux, darwin lub windows), a GOARCH — archi-
tekturę docelowego procesora, np.: amd64, 386 lub arm. Chociaż GOPATH jest jedyną zmienną, jaką
musisz ustawić, od czasu do czasu w naszych objaśnieniach pojawiają się również inne zmienne.
$ go env
GOPATH="/home/gopher/gobook"
GOROOT="/usr/local/go"
GOARCH="amd64"
GOOS="darwin"
...

10.7.2. Pobieranie pakietów


Gdy korzystamy z narzędzia go, ścieżka importu pakietu wskazuje nie tylko jego lokalizację w lokal-
nym obszarze roboczym, ale również to, gdzie można ten pakiet znaleźć w internecie, aby polecenie
go get mogło go pobrać i zaktualizować.
286 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO

Polecenie go get może pobrać pojedynczy pakiet albo całe poddrzewo lub repozytorium, używa-
jąc notacji wielokropka (...). To narzędzie również oblicza i pobiera wszystkie zależności po-
czątkowych pakietów, dlatego w poprzednim przykładzie w obszarze roboczym pojawił się pakiet
golang.org/x/net/html.
Gdy narzędzie go get pobierze pakiety, kompiluje je, a następnie instaluje biblioteki i polecenia.
Szczegółom przyjrzymy się w kolejnym punkcie, ale poniższy przykład pokazuje, jak prosty jest
to proces. Pierwsze polecenie pobiera narzędzie golint, które sprawdza typowe problemy stylu
w kodzie źródłowym Go. Drugie polecenie uruchamia golint dla programu code/r02/popcount
z punktu 2.6.2. Narzędzie usłużnie informuje, że zapomnieliśmy napisać komentarz dokumentujący
dla tego pakietu:
$ go get github.com/golang/lint/golint
$ $GOPATH/bin/golint code/r02/popcount
src/code/r02/popcount/main.go:1:1:
package comment should be of the form "Package popcount ..."
Polecenie go get obsługuje popularne strony hostujące kody, takie jak GitHub, Bitbucket i Launchpad,
i może wysyłać odpowiednie żądania do ich systemów kontroli wersji. W przypadku mniej znanych
stron być może trzeba będzie wskazać w ścieżce importu, który protokół kontroli wersji powinien zo-
stać użyty, np. Git lub Mercurial. Aby uzyskać więcej informacji, uruchom polecenie go help
importpath.
Katalogi tworzone przez polecenie go get są prawdziwymi klientami zdalnego repozytorium,
a nie tylko kopiami plików, więc można korzystać z poleceń systemu kontroli wersji, aby zo-
baczyć różnice dokonanych lokalnych edycji lub zaktualizować folder do innej wersji. Katalog
golang.org/x/net jest np. klientem Git:
$ cd $GOPATH/src/golang.org/x/net
$ git remote -v
origin https://go.googlesource.com/net (fetch)
origin https://go.googlesource.com/net (push)
Należy zwrócić uwagę, że nazwa domeny widoczna w ścieżce importu pakietu, czyli golang.org,
różni się od rzeczywistej nazwy domeny serwera Git — go.googlesource.com. Jest to cecha narzędzia
go, które pozwala pakietom używać niestandardowej nazwy domeny w ich ścieżkach importu,
podczas gdy są one hostowane przez usługę generyczną, taką jak googlesource.com lub github.com.
Strony HTML dostępne pod adresem: https://golang.org/x/net/html zawierają poniższe meta-
dane, które przekierowują narzędzie go do repozytorium Git znajdującego się na rzeczywistej stronie
hostującej:
$ go build code/r01/fetch
$ ./fetch https://golang.org/x/net/html | grep go-import
<meta name="go-import"
content="golang.org/x/net git https://go.googlesource.com/net">
Jeśli określisz flagę -u, polecenie go get zapewni, że wszystkie odwiedzone pakiety, w tym zależ-
ności, zostaną zaktualizowane do ich najnowszej wersji przed skompilowaniem i zainstalowaniem.
Bez tej flagi istniejące już lokalnie pakiety nie zostaną zaktualizowane.
Polecenie go get -u zwykle pobiera najnowszą wersję każdego pakietu, co jest wygodne, gdy się
zaczyna pracę nad projektem, ale może być nieodpowiednie dla wdrożonych projektów, w których
precyzyjna kontrola zależności ma kluczowe znaczenie dla higieny publikowanej wersji. Typowym
rozwiązaniem tego problemu jest vendorowanie kodu, czyli wykonanie trwałej lokalnej kopii
wszystkich niezbędnych zależności oraz staranne i świadome aktualizowanie tej kopii. Przed wersją
10.7. NARZĘDZIE GO 287

Go 1.5 wymagało to zmiany ścieżek importów tych pakietów, więc nasza kopia golang.org/
x/net/html zmieniłaby ścieżkę na code/vendor/golang.org/x/net/html. Nowsze wersje narzę-
dzia go obsługują vendorowanie bezpośrednio, ale nie mamy miejsca, aby pokazać tutaj szczegóły.
Więcej informacji na ten temat znajdziesz w sekcji Vendor Directories w danych wyjściowych
z polecenia go help gopath.

10.7.3. Kompilowanie pakietów


Polecenie go build kompiluje każdy pakiet podany jako jego argument. Jeśli pakiet jest biblioteką,
wynik jest odrzucany — wtedy jest to tylko sprawdzenie, czy pakiet jest wolny od błędów kom-
pilacji. Jeśli pakiet ma nazwę main, polecenie go build wywołuje konsolidator, aby utworzyć plik
wykonywalny w bieżącym katalogu. Nazwa pliku wykonywalnego jest pobierana z ostatniego seg-
mentu ścieżki importu pakietu.
Ponieważ każdy katalog zawiera jeden pakiet, każdy program wykonywalny (czyli w terminologii
uniksowej polecenie) wymaga własnego katalogu. Te katalogi są czasami potomkami katalogu
o nazwie cmd, tak jak w przypadku polecenia golang.org/x/tools/cmd/godoc, które serwuje
dokumentację pakietu Go poprzez interfejs WWW (zob. punkt 10.7.4).
Pakiety mogą być określane poprzez ich ścieżki importu, jak widzieliśmy powyżej, lub za pomocą
względnej nazwy katalogu, która musi zaczynać się od segmentu . lub .., nawet jeśli normalnie
nie jest to wymagane. Jeżeli nie jest dostarczony żaden argument, przyjmowany jest bieżący katalog.
Tak więc poniższe polecenia kompilują ten sam pakiet, ale każde zapisuje plik wykonywalny do
katalogu, w którym zostało uruchomione polecenie go build:
$ cd $GOPATH/src/code/r01/helloworld
$ go build
Oraz:
$ cd gdziekolwiek
$ go build code/r01/helloworld
Lub:
$ cd $GOPATH
$ go build ./src/code/r01/helloworld
Ale nie:
$ cd $GOPATH
$ go build src/code/r01/helloworld
Error: cannot find package "src/code/r01/helloworld"
Pakiety mogą być również definiowane w formie listy nazw plików, chociaż z reguły jest to używane
tylko w przypadku małych programów i jednorazowych eksperymentów. Jeżeli nazwą pakietu
jest main, nazwa pliku wykonywalnego pochodzi od nazwy bazowej pierwszego pliku .go.
$ cat quoteargs.go
package main

import (
"fmt"
"os"
)

func main() {
fmt.Printf("%q\n", os.Args[1:])
288 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO

}
$ go build quoteargs.go
$ ./quoteargs jeden "dwa trzy" cztery\ pięć
["jeden" "dwa trzy" "cztery pięć"]
Szczególnie w przypadku jednorazowych programów, takich jak ten, chcemy uruchomić plik wy-
konywalny od razu po jego skompilowaniu. Te dwa kroki łączy w sobie polecenie go run:
$ go run quoteargs.go jeden "dwa trzy" cztery\ pięć
["jeden" "dwa trzy" "cztery pięć"]
Pierwszy argument, który nie kończy się na .go, przyjmuje się za początek listy argumentów dla
pliku wykonywalnego Go.
Domyślnie polecenie go build kompiluje żądany pakiet i wszystkie jego zależności, a następnie wy-
rzuca cały skompilowany kod, z wyjątkiem końcowego pliku wykonywalnego, jeśli taki powstanie.
Zarówno analiza zależności, jak i kompilacja są zaskakująco szybkie, ale wraz z rozrastaniem się pro-
jektu do kilkudziesięciu pakietów i setek tysięcy linii kodu czas rekompilacji zależności może stać
się odczuwalny i potencjalnie dojść do kilku sekund, nawet gdy te zależności wcale się nie zmieniły.
Polecenie go install jest bardzo podobne do go build, z tym wyjątkiem, że zapisuje skompilowany
kod dla każdego pakietu i polecenia, zamiast go wyrzucać. Skompilowane pakiety są zapisywane
pod katalogiem $GOPATH/pkg odpowiadającym katalogowi src przechowującemu kod źródłowy,
a polecenia wykonywalne są zapisywane w katalogu $GOPATH/bin. (Wielu użytkowników umiesz-
cza $GOPATH/bin w wykonywalnej ścieżce wyszukiwania). Polecenia go build i go install
nie uruchamiają potem kompilatora dla tych pakietów i poleceń, jeśli nie zostały one zmienione,
co powoduje, że kolejne kompilacje są znacznie szybsze. Dla wygody polecenie go build -i instaluje
pakiety, które są zależnościami celu kompilacji.
Ponieważ skompilowane pakiety różnią się w zależności od platformy i architektury, polecenie
go install zapisuje je pod podkatalogiem, którego nazwa zawiera wartości zmiennych środo-
wiskowych GOOS i GOARCH. Na maszynie Mac pakiet golang.org/x/net/html jest np. kompilowany
i instalowany w pliku golang.org/x/net/html.a w katalogu $GOPATH/pkg/darwin_amd64.
Łatwo jest skrośnie skompilować program Go, czyli skompilować plik wykonywalny przezna-
czony dla innego systemu operacyjnego lub procesora. Wystarczy ustawić zmienne GOOS lub
GOARCH podczas kompilacji. Program cross wypisuje system operacyjny i architekturę, dla których
został skompilowany:
code/r10/cross
func main() {
fmt.Println(runtime.GOOS, runtime.GOARCH)
}
Poniższe polecenia generują odpowiednio 64-bitowy i 32-bitowy plik wykonywalny:
$ go build code/r10/cross
$ ./cross
darwin amd64
$ GOARCH=386 go build code/r10/cross
$ ./cross
darwin 386
Niektóre pakiety mogą potrzebować skompilować różne wersje kodu dla określonych platform
lub procesorów, aby np. uporać się z niskopoziomowymi problemami przenośności lub dostarczyć
zoptymalizowane wersje ważnych procedur. Jeśli nazwa pliku zawiera nazwę systemu operacyjnego
lub architektury procesora, tak jak net_linux.go lub asm_amd64.s, wtedy narzędzie go skompiluje
10.7. NARZĘDZIE GO 289

ten plik tylko podczas kompilacji dla tego celu. Specjalne komentarze, zwane znacznikami kompilacji
(ang. build tags), zapewniają dokładniejszą kontrolę. Jeśli plik zawiera np. ten komentarz:
// +build linux darwin
przed deklaracją pakietu (i jego komentarzem dokumentującym), go build skompiluje go tylko
wtedy, gdy będzie przeprowadzać kompilację dla systemów Linux lub Mac OS X. Natomiast ten
komentarz mówi, aby nigdy nie kompilować pliku:
// +build ignore
Więcej informacji znajdziesz w sekcji Build Constraints dokumentacji pakietu go/build:
$ go doc go/build

10.7.4. Dokumentowanie pakietów


Styl języka Go mocno promuje dobre dokumentowanie interfejsów API pakietów. Każda deklaracja
eksportowanego elementu pakietu i sama deklaracja package powinny być bezpośrednio po-
przedzone komentarzem wyjaśniającym ich przeznaczenie i sposób użycia.
Komentarze dokumentujące są zawsze pełnymi zdaniami, a pierwsze zdanie jest zwykle podsu-
mowaniem, które rozpoczyna się od deklarowanej nazwy. Parametry funkcji i inne identyfikatory
są wymieniane bez cudzysłowu lub znaczników. Oto przykładowy komentarz dokumentujący dla
fmt.Fprintf:
// Fprintf formatuje zgodnie ze specyfikatorem formatu i zapisuje w zmiennej w.
// Zwraca liczbę zapisanych bajtów i każdy napotkany błąd zapisu.
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
Szczegóły dotyczące formatowania funkcji Fprintf są wyjaśnione w komentarzu dokumentują-
cym powiązanym z samym pakietem fmt. Komentarz bezpośrednio poprzedzający deklarację
package jest uważany za komentarz dokumentujący dla pakietu jako całości. Może być tylko je-
den, choć może pojawić się w jakimkolwiek pliku. Dłuższe komentarze pakietów mogą uzasadniać
własne pliki. Komentarz pakietu fmt ma ponad 300 linii. Taki plik nazywa się zwykle doc.go.
Dobra dokumentacja nie musi być obszerna i nie jest substytutem dla prostoty. Faktycznie kon-
wencje języka Go sprzyjają zwięzłości i prostocie w dokumentacji, jak we wszystkim, ponieważ
dokumentacja, tak jak kod, również wymaga utrzymywania. Wiele deklaracji można wyjaśnić
w jednym dobrze sformułowanym zdaniu, a jeśli dane zachowanie jest naprawdę oczywiste, komen-
tarz nie jest potrzebny.
W całej książce, na tyle, na ile pozwoliło miejsce, poprzedzaliśmy wiele deklaracji komentarzami
dokumentującymi, ale podczas przeglądania standardowej biblioteki znajdziesz lepsze przykłady.
Pomóc mogą w tym dwa narzędzia.
Narzędzie go doc wyświetla deklarację i komentarz dokumentujący dla encji określonej w wierszu
poleceń. Może to być pakiet:
$ go doc time
package time // importowany jako "time"

Package time provides functionality for measuring and displaying time.


// Pakiet time zapewnia funkcjonalności dla mierzenia i wyświetlania czasu.

const Nanosecond Duration = 1 ...


func After(d Duration) <-chan Time
func Sleep(d Duration)
290 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO

func Since(t Time) Duration


func Now() Time
type Duration int64
type Time struct { ... }
...listing skrócony...
Może to być element pakietu:
$ go doc time.Since
func Since(t Time) Duration

Since returns the time elapsed since t. // Since zwraca czas, który upłynął od momentu t
It is shorthand for time.Now().Sub(t). // jest to skrót dla time.Now().Sub(t)
Może to być metoda:
$ go doc time.Duration.Seconds
func (d Duration) Seconds() float64

Seconds returns the duration as a floating point number of seconds.


// Seconds zwraca czas trwania jako zmiennoprzecinkową liczbę sekund.
To narzędzie nie wymaga podawania pełnych ścieżek importów ani stosowania właściwej wielkości
liter. Poniższe polecenie wyświetla dokumentację funkcji (*json.Decoder).Decode z pakietu
encoding/json:
$ go doc json.decode
func (dec *Decoder) Decode(v interface{}) error

Decode reads the next JSON-encoded value from its input and stores
it in the value pointed to by v.
// Decode odczytuje ze swojego strumienia wejściowego kolejną zakodowaną w formacie JSON wartość
// i zapisuje ją w wartości wskazywanej przez zmienną v.
Drugie narzędzie, o łudząco podobnej nazwie godoc, serwuje zlinkowane strony HTML, które
dostarczają te same informacje co go doc i wiele więcej. Serwer godoc pod adresem: https://
golang.org/pkg obejmuje bibliotekę standardową. Rysunek 10.1 przedstawia dokumentację dla pa-
kietu time, a w podrozdziale 11.6 zobaczymy interaktywny wyświetlacz narzędzia godoc dla
przykładowych programów. Serwer godoc pod adresem: https://godoc.org ma przeszukiwalny in-
deks tysięcy pakietów open source.
Można również uruchomić instancję godoc w obszarze roboczym, jeśli chcesz przeglądać własne
pakiety. W tym celu wpisz w przeglądarce adres: http://localhost:8800/pkg po uruchomieniu tego
polecenia:
$ godoc -http :8000
Flagi -analysis=type i -analysis=pointer tego polecenia poszerzają dokumentację i kod źródłowy
o wyniki zaawansowanej analizy statycznej.

10.7.5. Pakiety wewnętrzne


Pakiet jest najważniejszym mechanizmem hermetyzacji w programach Go. Nieeksportowane
identyfikatory są widoczne tylko w ramach tego samego pakietu, a eksportowane pakiety są widoczne
dla świata.
10.7. NARZĘDZIE GO 291

Rysunek 10.1. Pakiet time w godoc

Czasem jednak pomocne byłoby jakieś pośrednie rozwiązanie, czyli sposób na definiowanie iden-
tyfikatorów, które są widoczne dla niewielkiego zestawu zaufanych pakietów, ale nie dla wszystkich.
Kiedy dzielimy np. duży pakiet na łatwiejsze w zarządzaniu części, możemy nie chcieć ujawniać
innym pakietom interfejsów pomiędzy tymi częściami. Możemy również chcieć współdzielić
funkcje narzędziowe między kilkoma pakietami projektu bez udostępniania ich na szerszą skalę.
A może po prostu chcemy poeksperymentować z nowym pakietem bez przedwczesnego zatwier-
dzania jego interfejsu API, poprzez umieszczenie go „na warunkowym” z ograniczonym zestawem
klientów.
Aby zaspokoić te potrzeby, narzędzie go build traktuje pakiet w szczególny sposób, jeśli jego
ścieżka importu zawiera segment o nazwie internal. Takie pakiety są nazywane pakietami we-
wnętrznymi. Pakiet wewnętrzny może być importowany wyłącznie przez inny pakiet, który znaj-
duje się wewnątrz drzewa zakorzenionego w katalogu nadrzędnym w stosunku do katalogu internal.
W przypadku poniższych pakietów pakiet net/http/internal/chunked może np. być importowany
z net/http/httputil lub net/http, ale nie z net/url. Pakiet net/url może jednak importować
net/http/httputil.
net/http
net/http/internal/chunked
net/http/httputil
net/url
292 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO

10.7.6. Odpytywanie pakietów


Narzędzie go list raportuje informacje o dostępnych pakietach. W najprostszej postaci go list
sprawdza, czy pakiet jest obecny w obszarze roboczym, a jeśli tak, to wyświetla jego ścieżkę importu:
$ go list github.com/go-sql-driver/mysql
github.com/go-sql-driver/mysql
Argument dla polecenia go list może zawierać znak wieloznaczny „...”, który dopasowuje dowol-
ny podłańcuch znaków ścieżki importu pakietu. Możemy użyć go do enumeracji wszystkich pa-
kietów w obszarze roboczym Go:
$ go list ...
archive/tar
archive/zip
bufio
bytes
cmd/addr2line
cmd/api
...listing skrócony...
Lub w obrębie określonego poddrzewa:
$ go list code/r03/...
code/r03/basename1
code/r03/basename2
code/r03/comma
code/r03/mandelbrot
code/r03/netflag
code/r03/printints
code/r03/surface
Albo w powiązaniu z konkretnym tematem:
$ go list ...xml...
encoding/xml
code/r07/xmlselect
Polecenie go list uzyskuje pełne metadane dla każdego pakietu, a nie tylko ścieżkę importu, i udo-
stępnia te informacje użytkownikom lub innym narzędziom w różnych formatach. Flaga -json
powoduje, że polecenie go list wyświetla cały rekord każdego pakietu w formacie JSON:
$ go list -json hash
{
"Dir": "/home/gopher/go/src/hash",
"ImportPath": "hash",
"Name": "hash",
"Doc": "Package hash provides interfaces for hash functions.",
"Target": "/home/gopher/go/pkg/darwin_amd64/hash.a",
"Goroot": true,
"Standard": true,
"Root": "/home/gopher/go",
"GoFiles": [
"hash.go"
],
"Imports": [
"io"
],
"Deps": [
"errors",
"io",
"runtime",
10.7. NARZĘDZIE GO 293

"sync",
"sync/atomic",
"unsafe"
]
}
Flaga -f pozwala użytkownikom dostosowywać format wyjściowy przy użyciu języka szablonów
pakietu text/template (zob. podrozdział 4.6). Poniższe polecenie wypisuje przechodnie zależności
pakietu strconv oddzielone spacjami:
$ go list -f '{{join .Deps " "}}' strconv
errors math runtime unicode/utf8 unsafe
Natomiast kolejne polecenie wyświetla bezpośrednie importy każdego pakietu w poddrzewie
compress ze standardowej biblioteki:
$ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/...
compress/bzip2 -> bufio io sort
compress/flate -> bufio fmt io math sort strconv
compress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time
compress/lzw -> bufio errors fmt io
compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io
Polecenie go list jest użyteczne zarówno dla jednorazowych interaktywnych zapytań, jak i dla
kompilowania i testowania skryptów automatyzacji. Użyjemy go ponownie w punkcie 11.2.4. Więcej
informacji, w tym zestaw dostępnych pól i ich znaczenie, można znaleźć w danych wyjściowych
z polecenia go help list.
W tym rozdziale objaśniliśmy wszystkie istotne podpolecenia narzędzia go, z wyjątkiem jednego.
W następnym rozdziale zobaczymy, w jaki sposób polecenie go test jest wykorzystywane do testo-
wania programów Go.
Ćwiczenie 10.3. Zbuduj narzędzie raportujące zbiór wszystkich pakietów w obszarze roboczym,
które przechodnio zależą od pakietów określonych przez argumenty. Podpowiedź: trzeba będzie
uruchomić polecenie go list dwa razy: pierwszy raz dla początkowych pakietów, a drugi raz dla
wszystkich pakietów. Możesz zechcieć parsować dane wyjściowe JSON, wykorzystując pakiet
encoding/json (zob. podrozdział 4.5).
294 ROZDZIAŁ 10. PAKIETY I NARZĘDZIE GO
Rozdział 11

Testowanie

Maurice Wilkes, konstruktor EDSAC-a, pierwszego komputera wykonującego zapisany program,


w 1949 r. dokonał zaskakującego spostrzeżenia podczas wspinaczki po schodach w swoim labo-
ratorium. W książce Memoirs of a Computer Pioneer wspomina: „Nagle uświadomiłem sobie, że
znaczną część reszty mojego życia będę musiał poświęcić na poszukiwanie błędów we własnych
programach”. Z pewnością od tego czasu każdy programista komputera wykonującego zapisane
programy może sympatyzować z Wilkesem, choć nie bez pewnego zdumienia naiwnością jego
podejścia w kwestii problemów związanych z tworzeniem oprogramowania.
Oczywiście dzisiejsze programy są znacznie większe i bardziej skomplikowane niż za czasów
Wilkesa i włożono wiele wysiłku w opracowanie technik służących opanowaniu tej złożoności.
Dwie z tych technik szczególnie wyróżniają się skutecznością. Pierwszą z nich jest rutynowe recen-
zowanie programów przed ich wdrożeniem. Drugą techniką, będącą tematem tego rozdziału,
jest testowanie.
Testowanie, przez które w domyśle rozumiemy testowanie zautomatyzowane, jest praktyką pi-
sania niewielkich programów, które sprawdzają, czy testowany kod (kod produkcyjny) zachowuje
się zgodnie z oczekiwaniami dla określonych danych wejściowych, zazwyczaj starannie dobranych
do przećwiczenia konkretnych funkcjonalności lub wybranych losowo, aby zapewnić szerokie po-
krycie testu.
Dziedzina testowania oprogramowania obejmuje bardzo szeroki zakres zagadnień. Proces te-
stowania zajmuje przez pewną część czasu wszystkich programistów, a niektórych przez cały
czas. Literatura poświęcona testom obejmuje tysiące książek drukowanych i miliony słów na blo-
gach. W każdym z języków programowania głównego nurtu istnieją dziesiątki pakietów oprogra-
mowania przeznaczonych do budowania testów, niektóre z pogłębioną teorią, a ta dziedzina wy-
daje się przyciągać więcej niż kilku proroków wraz z ich wyznawcami. To niemal wystarcza, by
przekonać programistów, że do pisania skutecznych testów wymagane jest nabycie zupełnie
nowego zestawu umiejętności.
Podejście do testowania w języku Go może się wydawać w porównaniu z innymi językami mało
zaawansowane technicznie. Opiera się na jednym poleceniu go test oraz zestawie konwencji
dotyczących pisania funkcji testowych, które mogą być uruchamiane za pomocą tego polecenia.
Ten stosunkowo lekki mechanizm jest efektywny dla czystego testowania i w naturalny sposób
rozciąga się na benchmarki i systematyczne przykłady dla celów dokumentacji.
296 ROZDZIAŁ 11. TESTOWANIE

W praktyce pisanie kodu testu nie różni się zbytnio od pisania samego pierwotnego programu.
Piszemy krótkie funkcje, które koncentrują się na jednej części zadania. Musimy uważać na wa-
runki brzegowe, myśleć o strukturach danych oraz zastanawiać się, jakie wyniki obliczeń powinni-
śmy otrzymać z odpowiednich danych wejściowych. Jest to jednak taki sam proces jak pisanie
zwykłego kodu Go. Nie wymaga nowych notacji, konwencji ani narzędzi.

11.1. Narzędzie go test


Podpolecenie go test jest sterownikiem testów dla pakietów Go, które są zorganizowane zgod-
nie z określonymi konwencjami. W katalogu pakietu pliki o nazwach kończących się na _test.go
nie są częścią pakietu skompilowanego w zwykły sposób za pomocą go build, ale są jego częścią,
gdy pakiet zostanie skompilowany za pomocą go test.
W plikach *_test.go szczególnie traktowane są trzy rodzaje funkcji: testy, benchmarki i przykłady.
Funkcja testująca, która jest funkcją o nazwie zaczynającej się od słowa Test, sprawdza logikę
programu pod kątem prawidłowego zachowania. Podpolecenie go test wywołuje funkcję testują-
cą i podaje wynik, którym jest PASS (wynik pozytywny) lub FAIL (wynik negatywny). Funkcja
benchmarkująca ma nazwę rozpoczynającą się od słowa Benchmark i mierzy wydajność pewnej
operacji. Podpolecenie go test raportuje średni czas wykonywania operacji. Natomiast funkcja
przykładu, której nazwa rozpoczyna się od słowa Example, zapewnia dokumentację sprawdzoną
maszynowo. Testy zostaną szczegółowo omówione w podrozdziale 11.2, benchmarki w podroz-
dziale 11.4, a przykłady w podrozdziale 11.6.
Narzędzie go test skanuje pliki *_test.go pod kątem tych specjalnych funkcji, generuje tym-
czasowy pakiet main, który wywołuje je wszystkie w odpowiedni sposób, kompiluje i uruchamia
go, podaje wyniki, a następnie czyści.

11.2. Funkcje testujące


Każdy plik testowy musi importować pakiet testing. Funkcje testowe mają następującą sygnaturę:
func TestNazwa(t *testing.T) {
// …
}
Nazwy funkcji testujących muszą rozpoczynać się od słowa Test. Opcjonalny przyrostek Nazwa
musi zaczynać się od wielkiej litery:
func TestSin(t *testing.T) { /* ... */ }
func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }
Parametr t dostarcza metody do raportowania niepowodzeń testów i rejestrowania dodatkowych
informacji. Zdefiniujmy przykładowy pakiet code/r11/word1, zawierający pojedynczą funkcję
IsPalindrome, która informuje, czy dany łańcuch znaków brzmi tak samo czytany od lewej do
prawej i od prawej do lewej. (Ta implementacja testuje każdy bajt dwa razy, jeśli łańcuch znaków jest
palindromem. Wkrótce do tego wrócimy).
code/r11/word1
// Package word zapewnia narzędzia do gier słownych.
package word
11.2. FUNKCJE TESTUJĄCE 297

// IsPalindrome raportuje, czy s czyta się tak samo od lewej do prawej i od prawej do lewej.
// (Nasza pierwsza próba).
func IsPalindrome(s string) bool {
for i := range s {
if s[i] != s[len(s)-1-i] {
return false
}
}
return true
}
Znajdujący się w tym samym katalogu plik word_test.go zawiera dwie funkcje testujące:
TestPalindrome i TestNonPalindrome. Każda z nich sprawdza, czy IsPalindrome daje właściwą
odpowiedź dla pojedynczej danej wejściowej, i zgłasza niepowodzenia, wykorzystując t.Error:
package word

import "testing"

func TestPalindrome(t *testing.T) {


if !IsPalindrome("owocowo") {
t.Error(`IsPalindrome("owocowo") = false` )
}
if !IsPalindrome("kajak") {
t.Error(`IsPalindrome("kajak") = false` )
}
}

func TestNonPalindrome(t *testing.T) {


if IsPalindrome("palindrom") {
t.Error(`IsPalindrome("palindrom") = true` )
}
}
Polecenie go test (lub go build) bez podania pakietów jako argumentów operuje na pakiecie
znajdującym się w bieżącym katalogu. Możemy skompilować i uruchomić testy za pomocą
następującego polecenia:
$ cd $GOPATH/src/code/r11/word1
$ go test
ok code/r11/word1 0.008s
Zadowoleni rezultatem wysyłamy program, ale zaraz po wyjściu ostatnich gości z inauguracyjnego
przyjęcia zaczynają napływać raporty o błędach. Francuska użytkowniczka Noelle Eve Elleon
skarży się, że IsPalindrome nie rozpoznaje „été”. Użytkownik z Ameryki Środkowej jest rozcza-
rowany, że program odrzuca frazę „A man, a plan, a canal: Panama”. Te konkretne i krótkie raporty
o błędach w naturalny sposób nadają się jako nowe przypadki testowe.
func TestFrenchPalindrome(t *testing.T) {
if !IsPalindrome("été") {
t.Error(`IsPalindrome("été") = false` )
}
}

func TestCanalPalindrome(t *testing.T) {


input := "A man, a plan, a canal: Panama"
if !IsPalindrome(input) {
t.Errorf(`IsPalindrome(%q) = false` , input)
}
}
298 ROZDZIAŁ 11. TESTOWANIE

Aby uniknąć dwukrotnego pisania długiego łańcucha znaków input, używamy funkcji Errorf,
która zapewnia formatowanie takie jak Printf.
Po dodaniu dwóch nowych testów wykonanie polecenia go test kończy się niepowodzeniem i wy-
świetleniem informacyjnych komunikatów o błędach.
$ go test
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
exit status 1
FAIL code/r11/word1 0.014s
Dobrą praktyką jest napisać test i obserwować, czy wygeneruje ten sam błąd opisany przez raport
błędów użytkownika. Tylko wtedy możemy być pewni, że jakakolwiek wymyślona przez nas po-
prawka będzie rozwiązywała właściwy problem.
Dodatkowo uruchomienie polecenia go test jest zwykle szybsze niż ręczne przechodzenie przez etapy
opisane w raporcie o błędach, co pozwala nam szybciej iterować. Jeśli zestaw testów zawiera wiele
powolnych testów, możemy zrobić jeszcze szybszy postęp, jeśli będziemy uruchamiać je wybiórczo.
Flaga -v wyświetla nazwę i czas wykonywania każdego testu w zestawie:
$ go test -v
=== RUN TestPalindrome
--- PASS: TestPalindrome (0.00s)
=== RUN TestNonPalindrome
--- PASS: TestNonPalindrome (0.00s)
=== RUN TestFrenchPalindrome
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
=== RUN TestCanalPalindrome
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
exit status 1
FAIL code/r11/word1 0.017s
Natomiast flaga -run, której argumentem jest wyrażenie regularne, powoduje, że go test uruchamia
tylko te testy, których nazwa funkcji odpowiada danemu wzorcowi:
$ go test -v -run="French|Canal"
=== RUN TestFrenchPalindrome
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
=== RUN TestCanalPalindrome
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
exit status 1
FAIL code/r11/word1 0.014s
Oczywiście gdy wybrane testy dadzą wynik pozytywny, należy wywołać polecenie go test bez
żadnych flag, aby uruchomić cały zestaw testów jeszcze raz przed zatwierdzeniem zmian.
Teraz naszym zadaniem jest poprawienie błędów. Szybkie dochodzenie ujawnia, że przyczyną
pierwszego błędu jest to, iż IsPalindrome używa sekwencji bajtów zamiast sekwencji run, więc
11.2. FUNKCJE TESTUJĄCE 299

znaki spoza ASCII, takie jak é w "été", dezorientują tę funkcję. Drugi błąd wynika z nieignorowania
spacji, znaków interpunkcyjnych i wielkości liter.
Przywołani do porządku przepisujemy tę funkcję bardziej uważnie:
code/r11/word2
// Package word zapewnia narzędzia do gier słownych.
package word

import "unicode"

// IsPalindrome raportuje, czy s czyta się tak samo od lewej do prawej i od prawej do lewej.
// Ignorowane są wielkości liter oraz znaki niebędące literami.
func IsPalindrome(s string) bool {
var letters []rune
for _, r := range s {
if unicode.IsLetter(r) {
letters = append(letters, unicode.ToLower(r))
}
}
for i := range letters {
if letters[i] != letters[len(letters)-1-i] {
return false
}
}
return true
}
Piszemy również bardziej wszechstronny zestaw przypadków testowych, który łączy w tablicy
wszystkie poprzednie i wiele nowych.
func TestIsPalindrome(t *testing.T) {
var tests = []struct {
input string
want bool
}{
{"", true},
{"a", true},
{"aa", true},
{"ab", false},
{"kajak", true},
{"owocowo", true},
{"A man, a plan, a canal: Panama", true},
{"Evil I did dwell; lewd did I live.", true},
{"Able was I ere I saw Elba", true},
{"été", true},
{"Et se resservir, ivresse reste.", true},
{"palindrom", false}, // to nie jest palindrom
{"żartem,", false}, // to jest półpalindrom
}
for _, test := range tests {
if got := IsPalindrome(test.input); got != test.want {
t.Errorf("IsPalindrome(%q) = %v", test.input, got)
}
}
}
Nasze nowe testy dają wynik pozytywny:
$ go test code/r11/word2
ok code/r11/word2 0.015s
300 ROZDZIAŁ 11. TESTOWANIE

Ten styl testów opartych na tablicach (ang. table-driven) jest bardzo powszechny w języku Go.
W razie potrzeby można łatwo dodawać nowe wpisy tablicy, a ponieważ logika asercji nie jest po-
wielana, możemy włożyć więcej wysiłku w generowanie dobrego komunikatu o błędzie.
Dane wyjściowe z testu o wyniku negatywnym nie obejmują całego śladu stosu z momentu wy-
wołania t.Errorf. Funkcja t.Errorf nie uruchamia również procedury panic ani nie przerywa
wykonywania testu, w przeciwieństwie do negatywnego wyniku asercji w wielu frameworkach
testowych dla innych języków. Testy są od siebie niezależne. Jeśli któryś z wczesnych wpisów
w tablicy powoduje negatywny wynik testu, późniejsze wpisy i tak zostaną sprawdzone, a w ten
sposób możemy się dowiedzieć o wielu negatywnych wynikach podczas jednego uruchomienia.
Gdy naprawdę musimy zatrzymać funkcję testującą (np. z powodu niepowodzenia jakiegoś kodu
inicjującego albo żeby zapobiec wywołaniu przez już zgłoszone niepowodzenie mylącej kaskady
innych niepowodzeń), używamy t.Fatal lub t.Fatalf. Te funkcje muszą być wywołane z tej
samej goroutine co funkcja Test, a nie z innej utworzonej podczas testu.
Komunikaty o negatywnych wynikach testów mają zwykle postać "f(x) = y, oczekiwane z",
gdzie f(x) opisuje operację, której próba wykonania jest podejmowana, oraz jej dane wejściowe,
y jest rzeczywistym wynikiem, a z jest wynikiem oczekiwanym. Gdy jest to wygodne, jak w tym
przykładzie palindromu, dla części f(x) wykorzystywana jest rzeczywista składnia języka Go.
Wyświetlanie x jest szczególnie ważne w testach opartych na tablicach, ponieważ dana asercja
jest wykonywana wiele razy z różnymi wartościami. Unikaj gotowców i zbędnych informacji. Pod-
czas testowania funkcji logicznej, takiej jak IsPalindrome, pomijaj fragment oczekiwane z, po-
nieważ nie dodaje on żadnych informacji. Jeśli x, y lub z są przydługie, wypisuj zamiast tego
zwięzłe podsumowanie istotnych części. Autor testu powinien starać się pomóc programiście,
który musi zdiagnozować negatywny wynik testu.
Ćwiczenie 11.1. Napisz testy dla programu charcount z podrozdziału 4.3.
Ćwiczenie 11.2. Napisz dla struktury IntSet (zob. podrozdział 6.5) zestaw testów, który spraw-
dza, czy jej zachowanie po każdej operacji jest równoważne z zachowaniem zbioru opartego na
wbudowanych mapach. Zapisz swoją implementację dla benchmarkowania, które będziemy
przeprowadzać w ćwiczeniu 11.7.

11.2.1. Testowanie zrandomizowane


Testy oparte na tablicach są wygodne do sprawdzania, czy funkcja działa dla danych wejścio-
wych starannie wybranych do przećwiczenia ciekawych przypadków w logice. Inne podejście,
jakim jest testowanie zrandomizowane, bada szerszy zakres danych wejściowych poprzez gene-
rowanie ich w sposób losowy.
Skąd mamy wiedzieć, jakich danych wyjściowych oczekiwać z naszej funkcji, jeśli dane wejściowe
są losowe? Istnieją dwie strategie. Pierwsza z nich polega na napisaniu alternatywnej implementacji
funkcji, która wykorzystuje mniej efektywny, ale prostszy i bardziej przejrzysty algorytm, oraz
sprawdzeniu, czy obie implementacje dają ten sam wynik. Drugą strategią jest tworzenie wartości
wejściowych zgodnie ze wzorcem, abyśmy wiedzieli, jakich danych wyjściowych się spodziewać.
Poniższy przykład wykorzystuje drugie podejście: funkcja randomPalindrome generuje słowa, po
których konstrukcji wiadomo, że są palindromami.
import "math/rand"
import "time”
11.2. FUNKCJE TESTUJĄCE 301

// randomPalindrome zwraca palindrom, którego długość i treść


// są uzyskiwane na podstawie wskazań pseudolosowego generatora liczb rng.
func randomPalindrome(rng *rand.Rand) string {
n := rng.Intn(25) // losowa długość do 24 znaków
runes := make([]rune, n)
for i := 0; i < (n+1)/2; i++ {
r := rune(rng.Intn(0x1000)) // losowa runa z zakresu do '\u0999'
runes[i] = r
runes[n-1-i] = r
}
return string(runes)
}

func TestRandomPalindromes(t *testing.T) {


// Inicjowanie pseudolosowego generatora liczb.
seed := time.Now().UTC().UnixNano()
t.Logf("Random seed: %d", seed)
rng := rand.New(rand.NewSource(seed))
for i := 0; i < 1000; i++ {
p := randomPalindrome(rng)
if !IsPalindrome(p) {
t.Errorf("IsPalindrome(%q) = false", p)
}
}
}
Ponieważ zrandomizowane testy są niedeterministyczne, bardzo ważne jest, aby dziennik testów
z negatywnym wynikiem rejestrował wystarczającą ilość informacji umożliwiającą odtworzenie
niepowodzenia. W naszym przykładzie dane wejściowe p dla funkcji IsPalindrome mówią nam
wszystko, co musimy wiedzieć. Jednak w przypadku funkcji, które przyjmują bardziej złożone
dane wejściowe, prostsze może być rejestrowanie informacji źródłowych generatora liczb pseu-
dolosowych (tak jak robimy powyżej) niż robienie zrzutu całej struktury danych wejściowych.
Uzbrojeni w tę wartość źródłową możemy łatwo zmodyfikować test, aby odtworzyć niepowo-
dzenie w sposób deterministyczny.
Dzięki użyciu aktualnego czasu jako źródła losowości przez cały czas swojego życia ten test będzie
przy każdym uruchomieniu badał nowe dane wejściowe. Jest to szczególnie cenne, jeśli Twój projekt
wykorzystuje zautomatyzowany system do periodycznego uruchamiania wszystkich testów.
Ćwiczenie 11.3. Funkcja TestRandomPalindromes testuje tylko palindromy. Napisz zrandomizo-
wany test, który generuje i weryfikuje frazy niebędące palindromami.
Ćwiczenie 11.4. Zmodyfikuj funkcję randomPalindrome w taki sposób, aby przećwiczyć obsługę
interpunkcji i spacji przez IsPalindrome.

11.2.2. Testowanie polecenia


Narzędzie go test jest przydatne do testowania pakietów biblioteki, ale przy odrobinie wysiłku
możemy użyć go również do testowania poleceń. Pakiet o nazwie main zazwyczaj generuje program
wykonywalny, ale może być także importowany jako biblioteka.
Napiszmy test dla programu echo z punktu 2.3.2. Podzieliliśmy ten program na dwie funkcje.
Funkcja echo wykonuje rzeczywistą pracę, podczas gdy funkcja main parsuje i odczytuje wartości
flag oraz raportuje wszelkie błędy zwracane przez echo.
302 ROZDZIAŁ 11. TESTOWANIE

code/r11/echo
// Echo wyświetla swoje argumenty wiersza poleceń.
package main

import (
"flag"
"fmt"
"io"
"os"
"strings"
)

var (
n = flag.Bool("n", false, "pominięcie na końcu znaku nowej linii")
s = flag.String("s", " ", "separator")
)

var out io.Writer = os.Stdout // modyfikowane podczas testowania

func main() {
flag.Parse()
if err := echo(!*n, *s, flag.Args()); err != nil {
fmt.Fprintf(os.Stderr, "echo: %v\n", err)
os.Exit(1)
}
}

func echo(newline bool, sep string, args []string) error {


fmt.Fprint(out, strings.Join(args, sep))
if newline {
fmt.Fprintln(out)
}
return nil
}
W teście będziemy wywoływać funkcję echo z różnymi argumentami i ustawieniami flag, a następnie
sprawdzać, czy w każdym przypadku wypisuje ona poprawny wynik, więc dodaliśmy do echo pa-
rametry, których zadaniem jest zmniejszenie zależności funkcji od zmiennych globalnych.
Mimo to wprowadziliśmy również kolejną zmienną globalną out, czyli interfejs io.Writer, do któ-
rego będziemy wypisywać wynik. Dzięki temu, że echo będzie zapisywać poprzez tę zmienną,
a nie bezpośrednio do os.Stdout, testy mogą podstawić inną implementację interfejsu Writer,
która rejestruje do późniejszego wglądu to, co zostało zapisane. Oto test w pliku echo_test.go:
package main

import (
"bytes"
"fmt"
"testing"
)

func TestEcho(t *testing.T) {


var tests = []struct {
newline bool
sep string
args []string
want string
}{
11.2. FUNKCJE TESTUJĄCE 303

{true, "", []string{}, "\n"},


{false, "", []string{}, ""},
{true, "\t", []string{"jeden", "dwa", "trzy"}, "jeden\tdwa\ttrzy\n"},
{true, ",", []string{"a", "b", "c"}, "a,b,c\n"},
{false, ":", []string{"1", "2", "3"}, "1:2:3"},
}

for _, test := range tests {


descr := fmt.Sprintf("echo(%v, %q, %q)",
test.newline, test.sep, test.args)
out = new(bytes.Buffer) // przechwycone dane wyjściowe
if err := echo(test.newline, test.sep, test.args); err != nil {
t.Errorf("%s nie powiódł się: %v", descr, err)
continue
}
got := out.(*bytes.Buffer).String()
if got != test.want {
t.Errorf("%s = %q, oczekiwane %q", descr, got, test.want)
}
}
}
Należy zwrócić uwagę, że kod testu znajduje się w tym samym pakiecie co kod produkcyjny.
Chociaż pakiet ma nazwę main i definiuje funkcję main, podczas testowania ten pakiet działa jak bi-
blioteka, która udostępnia funkcję TestEcho sterownikowi testów. Jego funkcja main jest ignorowana.
Gdy organizujemy test jako tablicę, możemy łatwo dodawać nowe przypadki testowe. Poprzez
dodanie do tablicy poniższej linii zobaczymy, co się stanie, gdy test się nie powiedzie:
{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // UWAGA: niewłaściwe oczekiwania!
Polecenie go test wyświetla następujący listing:
$ go test code/r11/echo
--- FAIL: TestEcho (0.00s)
echo_test.go:31: echo(true, ",", ["a" "b" "c"]) = "a,b,c", oczekiwane "a b c\n"
FAIL
FAIL code/r11/echo 0.006s
Komunikat o błędzie opisuje (przy użyciu składni takiej jak w Go) operację, której próba wykona-
nia jest podejmowana, rzeczywiste zachowanie oraz oczekiwane zachowanie (w tej kolejności).
Na podstawie takiego pouczającego komunikatu o błędzie można wyrobić sobie całkiem dobre
pojęcie na temat głównej przyczyny niepowodzenia, nawet nie znając jeszcze lokalizacji kodu
źródłowego testu.
Ważne jest, aby testowany kod nie wywoływał funkcji log.Fatal lub os.Exit, ponieważ zatrzyma
to wykonywanie procesu. Wywoływanie tych funkcji należy traktować jako wyłączne prawo
funkcji main. Jeśli dzieje się coś zupełnie nieoczekiwanego i funkcja panikuje, sterownik testów
odzyska sprawność, chociaż oczywiście test zostanie uznany za niepowodzenie. Oczekiwane
błędy, takie jak te, które wynikają z nieodpowiednich danych wprowadzanych przez użytkownika,
brakujących plików lub niewłaściwej konfiguracji, powinny być zgłaszane przez zwracanie innej
niż nil wartości error. Na szczęście (choć jest to niefortunne jako ilustracja) nasz przykład
echo jest tak prosty, że nigdy nie zwróci błędu innego niż nil.
304 ROZDZIAŁ 11. TESTOWANIE

11.2.3. Testy strukturalne


Jeden ze sposobów kategoryzacji testów opiera się na poziomie wymaganej wiedzy na temat
wewnętrznych mechanizmów testowanego pakietu. Test funkcjonalny, zwany też testem czarnej
skrzynki (ang. black-box), nie zakłada na temat pakietu żadnych innych informacji niż to, co
jest udostępniane przez jego interfejs API i określane przez jego dokumentację. Wewnętrzne me-
chanizmy działania tego pakietu są nieprzezroczyste. Natomiast test strukturalny, zwany testem
białej skrzynki (ang. white-box), ma uprzywilejowany dostęp do wewnętrznych funkcji i struktur
danych pakietu i może czynić spostrzeżenia oraz wprowadzać zmiany, których nie może wpro-
wadzić zwykły klient. Test strukturalny może np. sprawdzać, czy po każdej operacji utrzymywane
są niezmienniki typów danych pakietu. (Nazwa test białej skrzynki jest tradycyjna, ale bardziej
precyzyjna byłaby nazwa test szklanej skrzynki).
Oba podejścia są komplementarne. Testy funkcjonalne są zazwyczaj bardziej niezawodne i po-
trzebują mniej aktualizacji w trakcie ewoluowania oprogramowania. Pomagają również autoro-
wi bardziej utożsamić się z klientem pakietu i mogą ujawnić wady w projekcie interfejsu API.
Natomiast testy strukturalne mogą zapewnić bardziej szczegółowe pokrycie trudniejszych części
implementacji.
Widzieliśmy już przykłady obu rodzajów testów. Funkcja TestIsPalindrome wywołuje tylko eks-
portowaną funkcję IsPalindrome, więc jest testem funkcjonalnym. Funkcja TestEcho wywołuje
funkcję echo i aktualizuje globalną zmienną out, z których obie są nieeksportowane, więc jest
testem strukturalnym.
Podczas rozwijania funkcji TestEcho zmodyfikowaliśmy funkcję echo, aby używała zmiennej
out poziomu pakietu do zapisywania swoich danych wyjściowych, żeby test mógł zastąpić stan-
dardowy strumień wyjściowy alternatywną implementacją, która rejestruje dane do późniejszego
wglądu. Stosując tę samą technikę, możemy zastąpić inne części kodu produkcyjnego łatwymi do
przetestowania „atrapami” implementacji. Zaletą atrap implementacji jest to, że mogą być łatwiej-
sze do skonfigurowania, bardziej przewidywalne, bardziej niezawodne i łatwiejsze do obserwowania.
Mogą również pozwolić uniknąć niepożądanych skutków ubocznych, takich jak aktualizacja
produkcyjnej bazy danych lub obciążenie karty kredytowej.
Poniższy kod przedstawia logikę sprawdzającą wykorzystanie limitów w usłudze internetowej
dostarczającej użytkownikom sieciową przestrzeń dyskową. Gdy użytkownicy przekraczają 90%
swojego limitu wykorzystania tej przestrzeni, system wysyła im e-mail z ostrzeżeniem.
code/r11/storage1
package storage

import (
"fmt"
"log"
"net/smtp"
)

var usage = make(map[string]int64)

func bytesInUse(username string) int64 { return usage[username] }

// Konfiguracja nadawcy poczty e-mail.


// UWAGA: nigdy nie umieszczaj hasła w kodzie źródłowym!
const sender = "notifications@example.com"
11.2. FUNKCJE TESTUJĄCE 305

const password = "correcthorsebatterystaple"


const hostname = "smtp.example.com"

const template = `Uwaga: wykorzystujesz %d bajtów przestrzeni, %d%% Twojego limitu.`

func CheckQuota(username string) {


used := bytesInUse(username)
const quota = 1000000000 // 1 GB
percent := 100 * used / quota
if percent < 90 {
return // OK
}
msg := fmt.Sprintf(template, used, percent)
auth := smtp.PlainAuth("", sender, password, hostname)
err := smtp.SendMail(hostname+":587", auth, sender,
[]string{username}, []byte(msg))
if err != nil {
log.Printf("smtp.SendMail(%s) nie powiodło się: %s", username, err)
}
}
Chcielibyśmy to przetestować, ale nie chcemy, żeby test wysyłał prawdziwy e-mail. Przenosimy
więc logikę e-maila do osobnej funkcji i zapisujemy tę funkcję w nieeksportowanej zmiennej
poziomu pakietu — notifyUser.
code/r11/storage2
var notifyUser = func(username, msg string) {
auth := smtp.PlainAuth("", sender, password, hostname)
err := smtp.SendMail(hostname+":587", auth, sender,
[]string{username}, []byte(msg))
if err != nil {
log.Printf("smtp.SendEmail(%s) nie powiodło się: %s", username, err)
}
}

func CheckQuota(username string) {


used := bytesInUse(username)
const quota = 1000000000 // 1 GB
percent := 100 * used / quota
if percent < 90 {
return // OK
}
msg := fmt.Sprintf(template, used, percent)
notifyUser(username, msg)
}
Możemy teraz napisać test, który podstawia prostą atrapę mechanizmu powiadomień w miej-
sce wysyłania prawdziwego e-maila. Ten mechanizm rejestruje powiadomionego użytkownika
oraz treść wiadomości.
package storage

import (
"strings"
"testing"
)

func TestCheckQuotaNotifiesUser(t *testing.T) {


var notifiedUser, notifiedMsg string
306 ROZDZIAŁ 11. TESTOWANIE

notifyUser = func(user, msg string) {


notifiedUser, notifiedMsg = user, msg
}
const user = "joe@example.org"
usage[user] = 980000000 // symulowanie sytuacji wykorzystania 980 MB

CheckQuota(user)
if notifiedUser == "" && notifiedMsg == "" {
t.Fatalf("funkcja notifyUser nie została wywołana")
}
if notifiedUser != user {
t.Errorf("niewłaściwy użytkownik (%s) został powiadomiony, oczekiwany %s",
notifiedUser, user)
}
const wantSubstring = "98% Twojego limitu"
if !strings.Contains(notifiedMsg, wantSubstring) {
t.Errorf("nieoczekiwany komunikat powiadomienia <<%s>>, "+
"oczekiwany podłańcuch %q", notifiedMsg, wantSubstring)
}
}
Jest jeden problem. Po zakończeniu funkcji testującej CheckQuota już nie działa tak jak należy,
ponieważ nadal używa testowej atrapy implementacji notifyUser. (Podczas aktualizowania zmien-
nych globalnych zawsze istnieje tego rodzaju ryzyko). Musimy zmodyfikować test w celu przy-
wracania poprzedniej wartości, aby nie było żadnego wpływu na kolejne testy, i musimy to zrobić
na wszystkich ścieżkach wykonywania, w tym dla niepowodzeń testu i uruchamiania procedury
panic. W naturalny sposób sugeruje to użycie instrukcji defer.
func TestCheckQuotaNotifiesUser(t *testing.T) {
// Zapisanie i przywrócenie oryginalnej implementacji notifyUser.
saved := notifyUser
defer func() { notifyUser = saved }()

// Instalowanie testowej atrapy implementacji notifyUser.


var notifiedUser, notifiedMsg string
notifyUser = func(user, msg string) {
notifiedUser, notifiedMsg = user, msg
}
// …reszta testu…
}
Ten wzorzec może być stosowany do tymczasowego zapisywania, a następnie przywracania wszyst-
kich rodzajów zmiennych globalnych, w tym flag wiersza poleceń, opcji debugowania oraz para-
metrów wydajności. Może być również użyty do zainstalowania i usunięcia zaczepów, które po-
wodują, że kod produkcyjny wywołuje jakiś kod testowy, gdy dzieje się coś ciekawego, oraz do
wymuszenia na kodzie produkcyjnym rzadkich, ale ważnych stanów, takich jak limity czasu, błędy,
a nawet konkretne przeploty współbieżnych aktywności.
Używanie zmiennych globalnych w ten sposób jest bezpieczne tylko dlatego, że polecenie go test
normalnie nie uruchamia wielu testów równocześnie.

11.2.4. Zewnętrzne pakiety testowe


Rozważmy pakiet net/url, który dostarcza parser URL, oraz pakiet net/http, który zapewnia
serwer WWW i bibliotekę klienta HTTP. Jak można się było spodziewać, wyższego poziomu
pakiet net/http zależy od niższego poziomu pakietu net/url. Jednak jeden z testów w net/url
11.2. FUNKCJE TESTUJĄCE 307

jest przykładem demonstrującym interakcję pomiędzy adresami URL a biblioteką klienta HTTP.
Innymi słowy: test z pakietu niższego poziomu importuje pakiet wyższego poziomu.
Zadeklarowanie tej funkcji testującej w pakiecie net/url utworzyłoby cykl w grafie importów
pakietu, tak jak przedstawiono to za pomocą skierowanej w górę strzałki na rysunku 11.1, ale,
jak wyjaśniono w podrozdziale 10.1, specyfikacja Go zabrania tworzenia cykli podczas impor-
towania pakietów.

Rysunek 11.1. Test pakietu net/url zależy od pakietu net/http

Rozwiązujemy ten problem, deklarując funkcję testującą w zewnętrznym pakiecie testowym, czyli
w umieszczonym w katalogu net/url pliku, którego deklaracją pakietu jest package url_test.
Przyrostek _test jest sygnałem dla polecenia go test, że powinno skompilować dodatkowy pakiet
zawierający tylko te pliki i uruchomić jego testy. Pomocne może być potraktowanie tego zewnętrzne-
go pakietu testowego tak, jakby miał ścieżkę importu net/url_test, ale nie mógł być zaimpor-
towany ani pod tą, ani pod inną nazwą.
Ponieważ zewnętrzne testy znajdują się w osobnym pakiecie, mogą importować pakiety pomocni-
cze, które również zależą od pakietu testowanego. Testy należące do pakietu nie mogą tego robić.
W kategoriach warstw projektu zewnętrzny pakiet testowy znajduje się logicznie wyżej od obu
pakietów, od których zależy, tak jak pokazano na rysunku 11.2.

Rysunek 11.2. Zewnętrzne pakiety testowe przełamują cykle zależności

Dzięki unikaniu cykli importów zewnętrzne pakiety testowe pozwalają testom, a zwłaszcza testom
integracyjnym (które testują interakcję kilku komponentów), swobodnie importować inne pa-
kiety, dokładnie tak, jak zrobiłaby aplikacja.
Możemy użyć narzędzia go list do podsumowania, które pliki źródłowe Go w katalogu pakietu
są kodem produkcyjnym, testami należącymi do pakietu oraz testami zewnętrznymi. Jako przy-
kładu użyjemy pakietu fmt. GoFiles jest listą plików, które zawierają kod produkcyjny. Są to pliki,
które go build zawrze w aplikacji:
$ go list -f={{.GoFiles}} fmt
[doc.go format.go print.go scan.go]
308 ROZDZIAŁ 11. TESTOWANIE

TestGoFiles jest listą plików, które również należą do pakietu fmt, ale te pliki (o nazwach kończą-
cych się na _test.go) są uwzględniane tylko podczas kompilowania testów:
$ go list -f={{.TestGoFiles}} fmt
[export_test.go]
W tych plikach zazwyczaj znajdują się testy pakietu, choć wyjątkowo fmt nie ma żadnego. Prze-
znaczenie pliku export_test.go wyjaśnimy za chwilę.
XTestGoFiles jest listą plików, które stanowią zewnętrzny pakiet testowy, fmt_test, więc te pliki
muszą zaimportować pakiet fmt, aby go użyć. One również są uwzględniane jedynie podczas
testowania:
$ go list -f={{.XTestGoFiles}} fmt
[fmt_test.go norace_test.go scan_test.go stringer_test.go]
Czasami zewnętrzny pakiet testowy może potrzebować uprzywilejowanego dostępu do wewnętrz-
nych funkcjonalności testowanego pakietu, jeśli np. test strukturalny musi się mieścić w osobnym
pakiecie, aby uniknąć cykli importów. W takich przypadkach używamy pewnej sztuczki: dodajemy
deklaracje do należącego do pakietu pliku _test.go, aby udostępnić wewnętrzne funkcjonalności
zewnętrznemu testowi. Ten plik oferuje zatem testowi „tylne drzwi” do pakietu. Jeśli plik źródłowy
istnieje tylko w tym celu i sam nie zawiera żadnych testów, często jest nazywany export_test.go.
Implementacja pakietu fmt potrzebuje np. funkcjonalności unicode.IsSpace jako części fmt.Scanf.
Aby uniknąć tworzenia niepożądanej zależności, fmt nie importuje pakietu unicode i jego dużych
tablic danych. Zamiast tego zawiera prostszą implementację, którą nazywa isSpace.
Aby się upewnić, że zachowania fmt.isSpace i unicode.IsSpace nie będą się rozbiegać, fmt
roztropnie zawiera test. Jest to zewnętrzny test i przez to nie może uzyskiwać dostępu do
isSpace bezpośrednio, więc fmt otwiera mu tylne drzwi poprzez zadeklarowanie eksportowanej
zmiennej, która przechowuje wewnętrzną funkcję isSpace. Jest to cała zawartość pliku export_test.go
pakietu fmt.
package fmt

var IsSpace = isSpace


Ten plik testowy nie definiuje żadnych testów. Po prostu deklaruje eksportowany symbol
fmt.IsSpace do użycia przez zewnętrzny test. Ta sztuczka może być również używana za każdym
razem, gdy test zewnętrzny musi skorzystać z niektórych technik testów strukturalnych.

11.2.5. Pisanie efektywnych testów


Wielu początkujących programistów Go jest zaskoczonych minimalizmem frameworku testowego
tego języka. Frameworki innych języków zapewniają mechanizmy identyfikacji funkcji testujących
(często przy użyciu refleksji lub metadanych), zaczepów do wykonywania operacji „skonfigurowania”
i „zniszczenia” przed uruchomieniem testów i po ich uruchomieniu oraz bibliotek funkcji narzę-
dziowych dla asercji typowych predykatów, porównywania wartości, formatowania komunikatów
o błędach i przerywania nieudanego testu (często przy użyciu wyjątków). Chociaż te mechanizmy
mogą uczynić testy bardzo zwięzłymi, powstałe w ten sposób testy często wydają się być napisane
w obcym języku. Ponadto, chociaż mogą prawidłowo raportować PASS lub FAIL, sposób ich działania
może być nieprzyjazny dla nieszczęsnego opiekuna z powodu tajemniczych komunikatów o błędach
typu "assert: 0 == 1" lub wielostronicowych śladów stosów.
11.2. FUNKCJE TESTUJĄCE 309

Podejście języka Go do testowania stoi w ostrym kontraście do podejść stosowanych w innych


językach. Od autorów testów oczekuje się, że sami wykonają większość pracy, definiując funkcje,
aby uniknąć powtórzeń, tak jak w przypadku zwykłych programów. Proces testowania nie przy-
pomina wyuczonego na pamięć wypełniania formularza testowego. Test posiada również interfejs
użytkownika, aczkolwiek taki, którego jedynymi użytkownikami są jego opiekunowie. Dobry
test nie „wybucha” w przypadku niepowodzenia, ale wypisuje jasny i zwięzły opis symptomów
problemu i być może również inne istotne fakty na temat kontekstu. Najlepiej, gdy opiekun nie
musi czytać kodu źródłowego, aby rozszyfrować, że test miał wynik negatywny. Dobry test nie
powinien przerywać działania po jednym niepowodzeniu, ale powinien starać się zgłosić kilka
błędów w jednym uruchomieniu, ponieważ wzorzec awarii może sam w sobie być odkrywczy.
Poniższa funkcja asercji porównuje dwie wartości, konstruuje ogólny komunikat o błędzie i za-
trzymuje program. Jest łatwa w użyciu i poprawna, ale kiedy się nie powiedzie, komunikat o błędzie
jest prawie bezużyteczny. Nie rozwiązuje ona trudnego problemu zapewnienia dobrego interfej-
su użytkownika.
import (
"fmt"
"strings"
"testing"
)

// Słaba funkcja asercji.


func assertEqual(x, y int) {
if x != y {
panic(fmt.Sprintf("%d != %d", x, y))
}
}

func TestSplit(t *testing.T) {


words := strings.Split("a:b:c", ":")
assertEqual(len(words), 3)
// …
}
W tym sensie funkcje asercji cierpią z powodu przedwczesnej abstrakcji: poprzez potraktowanie
niepowodzenia tego testu jako zwykłej różnicy dwóch liczb całkowitych tracimy możliwość za-
pewnienia znaczącego kontekstu. Można zapewnić lepszy komunikat, zaczynając od konkretnych
szczegółów, tak jak w poniższym przykładzie. Dopiero gdy w danym zestawie testów pojawiają się
powtarzające się wzorce, nadchodzi czas, aby wprowadzić abstrakcje.
func TestSplit(t *testing.T) {
s, sep := "a:b:c", ":"
words := strings.Split(s, sep)
if got, want := len(words), 3; got != want {
t.Errorf("Split(%q, %q) zwróciła %d słów, oczekiwano %d",
s, sep, got, want)
}
// …
}
Teraz test raportuje wywołaną funkcję, jej dane wejściowe oraz znaczenie wyniku. Jednoznacz-
nie identyfikuje rzeczywistą wartość i oczekiwania. Ponadto kontynuuje wykonywanie, nawet
jeśli asercja się nie powiedzie. Gdy już napiszemy taki test, naturalnym kolejnym krokiem często
jest nie zdefiniowanie funkcji, która ma zastąpić całą instrukcję if, ale wykonanie testu w pętli,
w której s, sep i want zmieniają się, tak jak w opartym na tablicy teście IsPalindrome.
310 ROZDZIAŁ 11. TESTOWANIE

Poprzedni przykład nie potrzebował żadnych funkcji narzędziowych, ale nie powinno nas to
powstrzymać od wprowadzenia funkcji, gdy pomagają uczynić kod prostszym. (Przyjrzymy się
jednej z takich funkcji narzędziowych, reflect.DeepEqual, w podrozdziale 13.3). Kluczem do
dobrego testu jest rozpoczęcie od implementacji konkretnego wymaganego zachowania i dopiero
wtedy użycie funkcji w celu uproszczenia kodu i wyeliminowania powtórzeń. Najlepsze wyniki
rzadko uzyskuje się, zaczynając od biblioteki abstrakcyjnych, ogólnych funkcji testujących.
Ćwiczenie 11.5. Rozszerz funkcję TestSplit w taki sposób, aby wykorzystywała tablicę danych
wejściowych i oczekiwanych rezultatów.

11.2.6. Unikanie kruchych testów


Aplikacja, która często zawodzi, gdy napotyka nowe, ale prawidłowe dane wejściowe, jest nazywana
zapluskwioną (ang. buggy). Test, który fałszywie zawodzi, gdy w programie zostanie wprowadzo-
na jakaś rozsądna zmiana, nazywa się kruchym (ang. brittle). Podobnie jak zapluskwiony program
frustruje użytkowników, kruchy test drażni jego opiekunów. Najbardziej kruche testy, które zawodzą
dla niemal każdej zmiany w kodzie produkcyjnym (dobrej czy złej), są czasami nazywane testami
wykrywania zmian lub testami status quo, a poświęcony na nie czas może zniwelować wszelkie ko-
rzyści, jakie wydawały się zapewniać.
Gdy testowana funkcja generuje złożone dane wyjściowe, takie jak długi łańcuch znaków, skom-
plikowana struktura danych lub plik, aż korci, żeby sprawdzić, czy te dane wyjściowe równają się do-
kładnie jakiejś „złotej” wartości, która była oczekiwana, gdy test był pisany. Jednak wraz z ewo-
luowaniem programu fragmenty danych wyjściowych prawdopodobnie zmienią się, być może
na dobre, ale mimo wszystko się zmienią. I nie chodzi tylko o dane wyjściowe. Funkcje o złożo-
nych danych wejściowych często przestają działać, ponieważ dane wejściowe wykorzystane w teście
przestają być prawidłowe.
Najprostszym sposobem unikania kruchych testów jest sprawdzanie tylko tych właściwości, na
których nam zależy. Testuj prostsze i bardziej stabilne interfejsy programu zamiast ich funkcji
wewnętrznych. Bądź selektywny w swoich asercjach. Nie sprawdzaj np. dokładnych zgodności
łańcuchów znaków, ale szukaj istotnych podłańcuchów, które nie ulegną zmianie w miarę rozwoju
programu. Często warto napisać obszerną funkcję w celu wydobycia istoty złożonych danych
wyjściowych, aby asercje były niezawodne. Chociaż może się wydawać, że w tym celu trzeba poświę-
cić dużo wysiłku, szybko może się to zrekompensować w kategoriach czasu, który w przeciwnym
razie poświęcilibyśmy na naprawianie fałszywie zawodzących testów.

11.3. Pokrycie
Testowanie nigdy nie jest kompletne. Wpływowy informatyk Edsger Dijkstra ujął to następująco:
„Testowanie wykazuje obecność, a nie brak błędów”. Żadna liczba testów nigdy nie dowiedzie,
że pakiet jest wolny od błędów. W najlepszym wypadku testy zwiększają nasze przekonanie, że
dany pakiet działa prawidłowo dla szerokiego zakresu ważnych scenariuszy.
Stopień, w jakim zestaw testów jest w stanie przećwiczyć testowany pakiet, jest nazywany pokryciem
(ang. coverage) testu. Pokrycie nie może być kwantyfikowane bezpośrednio — nie da się precyzyj-
nie zmierzyć dynamiki żadnych programów, poza tymi najbardziej trywialnymi. Istnieją jednak
heurystyki, które mogą nam pomóc skierować nasze wysiłki związane z testowaniem na obszary,
gdzie najprawdopodobniej będą one użyteczne.
11.3. POKRYCIE 311

Pokrycie instrukcji (ang. statement coverage) to najprostsza i najczęściej stosowana z tych heurystyk.
Pokrycie instrukcji danego zestawu testów jest procentowym udziałem instrukcji źródłowych, któ-
re podczas testu są co najmniej raz wykonywane. W tym podrozdziale użyjemy narzędzia cover
języka Go (które jest zintegrowane z narzędziem go test), aby zmierzyć pokrycie instrukcji i pomóc
zidentyfikować oczywiste luki w testach.
Poniższy kod jest opartym na tablicy testem dla ewaluatora wyrażeń, który zbudowaliśmy
w rozdziale 7.:
code/r07/eval
func TestCoverage(t *testing.T) {
var tests = []struct {
input string
env Env
want string // oczekiwany błąd z Parse/Check lub wynik z Eval
}{
{"x % 2", nil, "nieoczekiwane '%'"},
{"!true", nil, "nieoczekiwane '!'"},
{"log(10)", nil, `nieznana funkcja "log"` },
{"sqrt(1, 2)", nil, "wywołanie sqrt ma argumentów 2, wymaga 1"},
{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
}

for _, test := range tests {


expr, err := Parse(test.input)
if err == nil {
err = expr.Check(map[Var]bool{})
}
if err != nil {
if err.Error() != test.want {
t.Errorf("%s: ma %q, oczekiwane %q", test.input, err, test.want)
}
continue
}
got := fmt.Sprintf("%.6g", expr.Eval(test.env))
if got != test.want {
t.Errorf("%s: %v => %s, oczekiwane %s",
test.input, test.env, got, test.want)
}
}
}
Najpierw sprawdźmy, czy test daje wynik pozytywny:
$ go test -v -run=Coverage code/r07/eval
=== RUN TestCoverage
--- PASS: TestCoverage (0.00s)
PASS
ok code/r07/eval 0.011s
To polecenie wyświetla informacje o sposobie używania narzędzia cover:
$ go tool cover
Usage of 'go tool cover':
Given a coverage profile produced by 'go test':
// Dla danego profilu pokrycia wygenerowanego przez 'go test':
go test -coverprofile=c.out
312 ROZDZIAŁ 11. TESTOWANIE

Open a web browser displaying annotated source code:


// Otwiera przeglądarkę, wyświetlając kod źródłowy z adnotacjami:
go tool cover -html=c.out
...
Polecenie go tool uruchamia jeden z plików wykonywalnych z zestawu narzędzi Go. Programy
te są przechowywane w katalogu $GOROOT/pkg/tool/${GOOS}_${GOARCH} i dzięki poleceniu
go build rzadko trzeba wywoływać je bezpośrednio.
Teraz uruchamiamy test z flagą -coverprofile:
$ go test -run=Coverage -coverprofile=c.out code/r07/eval
ok code/r07/eval 0.032s coverage: 67.5% of statements
Ta flaga umożliwia gromadzenie danych pokrycia poprzez instrumentację kodu produkcyjnego.
Oznacza to, iż modyfikuje kopię kodu źródłowego w taki sposób, że przed wykonaniem każdego
bloku instrukcji ustawiana jest zmienna logiczna (jedna zmienna na blok). Tuż przed zakończe-
niem zmodyfikowanego programu wartość każdej zmiennej jest zapisywana w określonym pliku
dziennika c.out i wyświetlane jest podsumowanie procentowego udziału instrukcji, które zostały
wykonane. (Jeśli potrzebujesz tylko tego podsumowania, użyj polecenia go test -cover).
Jeśli zostanie uruchomione polecenie go test z flagą -covermode=count, instrumentacja będzie
dla każdego bloku inkrementować licznik, zamiast ustawiać wartość logiczną. Powstały dziennik
liczb wykonań każdego bloku umożliwia dokonywanie ilościowych porównań pomiędzy blokami
„gorętszymi” (które są częściej wykonywane) a „chłodniejszymi”.
Po zebraniu danych możemy uruchomić narzędzie cover, które przetwarza dziennik, generuje
raport HTML i otwiera go w nowym oknie przeglądarki (rysunek 11.3).
$ go tool cover -html=c.out

Rysunek 11.3. Raport pokrycia


11.4. FUNKCJE BENCHMARKUJĄCE 313

Każda instrukcja jest zabarwiona na zielono, jeśli została pokryta, lub na czerwono, jeśli nie została
pokryta. Dla jasności zacieniowaliśmy tło z tekstem w kolorze czerwonym. Widać od razu, że żadna
z pozycji w tablicy danych wejściowych nie przećwiczyła metody Eval operatora jednoargumen-
towego. Jeśli dodamy do tablicy poniższy nowy przypadek testowy i ponownie uruchomimy po-
przednie dwa polecenia, kod wyrażenia jednoargumentowego zostanie podświetlony na zielono:
{"-x * -x", Env{"x": 2}, "4"}
Dwie instrukcje panic pozostają jednak w kolorze czerwonym. Nie powinno to być zaskoczeniem,
ponieważ te instrukcje mają być nieosiągalne.
Osiągnięcie 100% pokrycia instrukcji brzmi jak szlachetny cel, ale zwykle nie jest możliwe w prak-
tyce i raczej nie jest dobrym spożytkowaniem wysiłków. Sam fakt, że instrukcja jest wykonywana,
nie oznacza, że jest wolna od błędów. Instrukcje zawierające złożone wyrażenia muszą być wykony-
wane wiele razy z różnymi danymi wejściowymi, aby pokryć interesujące przypadki. Niektóre
instrukcje, takie jak powyższe instrukcje panic, nigdy nie mogą być osiągnięte. Inne instrukcje,
takie jak te obsługujące ezoteryczne błędy, są trudne do przećwiczenia, ale rzadko dociera się do
nich w praktyce. Testowanie jest zasadniczo dążeniem pragmatycznym, kompromisem pomiędzy
kosztami pisania testów a kosztami awarii, którym można by zapobiec poprzez testy. Narzędzia
pokrycia mogą pomóc określić najsłabsze punkty, ale opracowanie dobrych przypadków testowych
wymaga tak samo rygorystycznego sposobu myślenia jak programowanie w ogóle.

11.4. Funkcje benchmarkujące


Benchmarkowanie jest praktyką mierzenia wydajności programu dla ustalonego obciążenia roboczego.
W języku Go funkcja benchmarkująca wygląda jak funkcja testująca, ale z prefiksem Benchmark i pa-
rametrem *testing.B, który zapewnia większość tych samych metod co *testing.T plus kilka
dodatkowych związanych z pomiarem wydajności. Udostępnia również pole N wartości całkowitej,
która określa liczbę powtórzeń dla wykonywania mierzonej operacji.
Oto benchmark dla funkcji IsPalindrome, który wywołuje ją N razy w pętli.
import "testing"

func BenchmarkIsPalindrome(b *testing.B) {


for i := 0; i < b.N; i++ {
IsPalindrome("A man, a plan, a canal: Panama")
}
}
Uruchamiamy go za pomocą poniższego polecenia. W odróżnieniu od testów, domyślnie nie są uru-
chamiane żadne benchmarki. Argument flagi -bench określa, które benchmarki uruchomić. Jest
to wyrażenie regularne dopasowujące nazwy funkcji Benchmark, o wartości domyślnej, która nie
dopasowuje żadnej z nich. Wzorzec „.” powoduje dopasowanie wszystkich benchmarków w pakie-
cie word, ale ponieważ jest tylko jeden, równoważne byłoby wyrażenie -bench=IsPalindrome.
$ cd $GOPATH/src/code/r11/word2
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000 1035 ns/op
ok code/r11/word2 2.179s
Przyrostek numeryczny nazwy benchmarku (w tym przypadku 8) wskazuje wartość GOMAXPROCS,
która jest istotna dla współbieżnych benchmarków.
314 ROZDZIAŁ 11. TESTOWANIE

Ten raport mówi nam, że każde wywołanie funkcji IsPalindrome trwało ok. 1,035 mikrosekundy,
co jest wartością uśrednioną dla 1 000 000 uruchomień. Ponieważ narzędzie uruchamiające bench-
mark nie ma pojęcia, jak długo trwa dana operacja, wykonuje wstępne pomiary za pomocą małych
wartości N, a następnie ekstrapoluje do wartości wystarczająco dużej, aby dokonać stabilnego po-
miaru czasu.
Powodem implementowania pętli przez funkcję benchmarkującą, a nie przez kod wywołujący
w sterowniku testów, jest to, że funkcja benchmarkująca ma możliwość wykonania wszelkiego nie-
zbędnego jednorazowego kodu konfiguracyjnego na zewnątrz pętli bez dodawania tego do mierzo-
nego czasu każdej iteracji. Jeśli ten kod konfiguracyjny nadal zaburza wyniki, parametr testing.B
dostarcza metod do zatrzymania, wznowienia i wyzerowania stopera, ale rzadko są one potrzebne.
Gdy mamy już benchmark i testy, można łatwo wypróbować różne pomysły przyspieszenia pro-
gramu. Prawdopodobnie najbardziej oczywistą optymalizacją jest zatrzymanie w punkcie środko-
wym sprawdzania przez drugą pętlę funkcji IsPalindrome, aby uniknąć wykonywania każdego
porównania dwukrotnie:
n := len(letters)/2
for i := 0; i < n; i++ {
if letters[i] != letters[len(letters)-1-i] {
return false
}
}
return true
Oczywista optymalizacja nie zawsze jednak daje oczekiwaną korzyść. Ta optymalizacja zapewniła
zaledwie 4% poprawy w jednym eksperymencie.
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000 992 ns/op
ok code/r11/word2 2.093s
Innym pomysłem jest wstępne alokowanie wystarczająco dużej tablicy do użycia przez zmienną
letters, zamiast rozszerzać ją przez kolejne wywołania append. Zadeklarowanie letters jako
tablicy o odpowiedniej wielkości w taki sposób:
letters := make([]rune, 0, len(s))
for _, r := range s {
if unicode.IsLetter(r) {
letters = append(letters, unicode.ToLower(r))
}
}
daje poprawę o prawie 35%, a narzędzie uruchamiające benchmark raportuje teraz średnią dla
2 000 000 iteracji.
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 2000000 697 ns/op
ok code/r11/word2 1.468s
Jak pokazuje ten przykład, najszybszy program to często ten, który wykonuje najmniej alokacji
pamięci. Flaga -benchmem wiersza poleceń będzie załączała w swoim raporcie statystyki alokacji
pamięci. Porównajmy liczbę alokacji przed optymalizacją:
$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome-8 1000000 1026 ns/op 304 B/op 4 allocs/op
11.5. PROFILOWANIE 315

i po optymalizacji:
$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome-8 2000000 807 ns/op 128 B/op 1 allocs/op
Konsolidacja alokacji w pojedynczym wywołaniu make wyeliminowała 75% alokacji oraz zmniej-
szyła o połowę ilość przydzielonej pamięci.
Benchmarki takie jak ten wskazują nam bezwzględny czas wymagany dla danej operacji, ale w wielu
ustawieniach interesujące nas pytania o wydajność dotyczą względnych czasów dwóch różnych
operacji. Przykładowo: jeśli funkcja potrzebuje 1 mikrosekundy do przetworzenia 1000 elementów,
jak długo będzie trwało przetwarzanie 10 000 lub 1 000 000? Takie porównania ujawniają asympto-
tyczne tempo wzrostu czasu działania funkcji. Inny przykład: jaki jest najlepszy rozmiar bufora
we-wy? Benchmarkowanie przepustowości aplikacji dla różnych rozmiarów może pomóc nam wy-
brać najmniejszy bufor, który zapewnia zadowalającą wydajność. Trzeci przykład: który algorytm
sprawdza się najlepiej dla danej pracy? Benchmarki, które oceniają dwa różne algorytmy na tych
samych danych wejściowych, często mogą pokazać mocne i słabe strony każdego z nich dla
ważnych lub reprezentatywnych obciążeń roboczych.
Benchmarki porównawcze są po prostu regularnym kodem. Zwykle mają postać pojedynczej
sparametryzowanej funkcji wywoływanej z kilku funkcji Benchmark z różnymi wartościami, np.:
func benchmark(b *testing.B, size int) { /* ... */ }
func Benchmark10(b *testing.B) { benchmark(b, 10) }
func Benchmark100(b *testing.B) { benchmark(b, 100) }
func Benchmark1000(b *testing.B) { benchmark(b, 1000) }
Parametr size, który określa rozmiar danych wejściowych, różni się dla poszczególnych bench-
marków, ale jest stały w obrębie każdego z nich. Powinieneś oprzeć się pokusie użycia parametru b.N
jako rozmiaru danych wejściowych. Jeśli nie zinterpretujesz go jako liczby iteracji dla danych wej-
ściowych o stałym rozmiarze, wyniki Twojego benchmarku będą bez znaczenia.
Wzorce ujawnione przez benchmarki porównawcze są szczególnie przydatne podczas projekto-
wania programu, ale nie należy wyrzucać benchmarków, kiedy program już zadziała. Gdy pro-
gram ewoluuje, powiększają się jego dane wejściowe lub jest wdrażany na nowych systemach
operacyjnych albo procesorach z inną charakterystyką, możemy ponownie użyć tych benchmar-
ków do zrewidowania decyzji projektowych.
Ćwiczenie 11.6. Napisz benchmarki do porównania implementacji PopCount z punktu 2.6.2
z Twoimi rozwiązaniami opracowanymi dla ćwiczeń 2.4 i 2.5. W którym momencie opłacalne staje
się podejście oparte na tablicach?
Ćwiczenie 11.7. Napisz benchmarki dla Add, UnionWith i innych metod *IntSet (zob. podroz-
dział 6.5) przy użyciu dużych pseudolosowych danych wejściowych. Jak szybko mogą działać te
metody? W jaki sposób wybór rozmiaru słowa wpływa na wydajność? Jak szybki jest IntSet
w porównaniu do implementacji zbioru opartej na wbudowanym typie mapy?

11.5. Profilowanie
Benchmarki są przydatne do pomiaru wydajności konkretnych operacji, ale gdy próbujemy
przyspieszyć powolny program, często nie mamy pojęcia, od czego zacząć. Każdy programista
zna aforyzm Donalda Knutha na temat przedwczesnej optymalizacji, który znalazł się w książce
316 ROZDZIAŁ 11. TESTOWANIE

Structured Programming with go to Statements wydanej w 1974 r. Chociaż jest on często błędnie
interpretowany w taki sposób, że wydajność nie ma znaczenia, w oryginalnym kontekście możemy
dostrzec inne znaczenie:
Nie ma wątpliwości, że Święty Graal efektywności prowadzi do nadużyć. Programiści mar-
nują ogromne ilości czasu na myślenie o prędkości niekrytycznych części swoich programów
i przejmowanie się tymi kwestiami, a te próby zwiększenia efektywności mają w rzeczywisto-
ści silny negatywny wpływ, jeśli wziąć pod uwagę debugowanie i utrzymywanie oprogra-
mowania. Przez powiedzmy ok. 97% czasu powinniśmy zapomnieć o niewielkich zyskach
efektywności: przedwczesna optymalizacja jest przyczyną całego zła.
Jednak w tych krytycznych 3% czasu nie powinniśmy przepuszczać nadarzających się możli-
wości. Dobry programista nie pozwoli się ponieść samozadowoleniu poprzez takie rozumo-
wanie, ale rozsądnie przyjrzy się dokładnie krytycznym fragmentom kodu. Zrobi to jednak
dopiero po tym, gdy te fragmenty kodu zostaną zidentyfikowane. Często błędem jest doko-
nywanie apriorycznych osądów na temat tego, które części programu są naprawdę kluczowe,
ponieważ uniwersalne doświadczenie programistów używających narzędzi pomiarowych
jest takie, że ich intuicyjne przypuszczenia zawodzą.

Gdy chcemy dokładnie przyjrzeć się prędkości naszych programów, najlepszą techniką identyfi-
kowania krytycznego kodu jest profilowanie. Jest to zautomatyzowane podejście do mierzenia
wydajności, oparte na próbkowaniu szeregu zdarzeń profilu podczas wykonywania, a następnie
ekstrapolowaniu na ich podstawie podczas etapu następującego po przetwarzaniu. Uzyskane
podsumowanie statystyczne nazywa się profilem.
Język Go obsługuje wiele rodzajów profilowania, z których każde dotyczy innego aspektu wydajno-
ści, ale wszystkie obejmują rejestrowanie sekwencji zdarzeń będących przedmiotem zainteresowania.
Każdemu z tych zdarzeń towarzyszy ślad stosu, czyli stos wywołań funkcji aktywnych w momencie
zdarzenia. Narzędzie go test ma wbudowaną obsługę dla kilku rodzajów profilowania.
Profil CPU identyfikuje funkcje, których wykonywanie zabiera najwięcej czasu procesora. Wątki
uruchomione równolegle na każdym procesorze są cyklicznie przerywane przez system operacyjny
co kilka milisekund, a każde przerwanie rejestruje jedno zdarzenie profilowe przed wznowieniem
normalnego wykonywania.
Profil sterty identyfikuje instrukcje odpowiedzialne za alokowanie największej ilości pamięci.
Biblioteka profilująca próbkuje wywołania wewnętrznych procedur alokacji pamięci w taki sposób,
że średnio jedno zdarzenie profilowe jest rejestrowane dla każdych 512 kB przydzielonej pamięci.
Profil blokowania identyfikuje operacje odpowiedzialne za najdłuższe blokowanie funkcji goroutine,
takie jak wywołania systemowe, wysyłanie i odbieranie poprzez kanał oraz zakładanie blokad.
Biblioteka profilująca rejestruje zdarzenie za każdym razem, gdy jakaś funkcja goroutine zostaje
zablokowana przez jedną z tych operacji.
Gromadzenie profilu dla testowanego kodu jest tak proste jak włączenie jednej z poniższych flag.
Bądź jednak ostrożny, gdy będziesz używał więcej niż jednej flagi na raz: mechanizm odpowie-
dzialny za gromadzenie jednego rodzaju profilu może wypaczyć wyniki innych.
$ go test -cpuprofile=cpu.out
$ go test -blockprofile=block.out
$ go test -memprofile=mem.out
11.5. PROFILOWANIE 317

Łatwo można również dodać obsługę profilowania dla programów niebędących testami, choć różni
się to w szczegółach w zależności od tego, czy mamy do czynienia z krótko żyjącymi narzędziami
wiersza poleceń, czy długo działającymi aplikacjami serwerowymi. Profilowanie jest szczególnie
przydatne w długo działających aplikacjach, więc funkcje profilujące środowiska wykonawczego
Go można włączyć pod kontrolą programisty za pomocą interfejsu API pakietu runtime.
Gdy już zgromadzimy profil, musimy przeanalizować go przy użyciu narzędzia pprof. Jest ono stan-
dardową częścią dystrybucji Go, ale ponieważ nie jest codziennym narzędziem, dostęp do niego uzy-
skuje się pośrednio za pomocą polecenia go tool pprof. Posiada wiele funkcji i opcji, ale pod-
stawowe zastosowanie wymaga tylko dwóch argumentów: pliku wykonywalnego, który wygenerował
profil, oraz dziennika profilu.
Aby uczynić profilowanie efektywnym i zaoszczędzić przestrzeń, dziennik nie zawiera nazw funkcji.
Zamiast tego funkcje są identyfikowane poprzez ich adresy. Oznacza to, że pprof potrzebuje pliku
wykonywalnego, aby zrozumieć plik dziennika. Chociaż go test zwykle porzuca plik wykonywalny
po zakończeniu testu, gdy profilowanie jest włączone, zapisuje plik wykonywalny jako foo.test,
gdzie foo jest nazwą testowanego pakietu.
Poniższe polecenia pokazują, jak zgromadzić i wyświetlić prosty profil procesora. Wybraliśmy
jeden z benchmarków z pakietu net/http. Zazwyczaj lepiej jest profilować konkretne benchmarki,
które zostały skonstruowane w taki sposób, aby były reprezentatywne dla interesujących nas ob-
ciążeń roboczych. Benchmarkowanie przypadków testowych prawie nigdy nie jest reprezentatywne,
dlatego też wyłączyliśmy je, używając filtra -run=NONE.
$ go test -run=NONE -bench=ClientServerParallelTLS64 \
-cpuprofile=cpu.log net/http
PASS
BenchmarkClientServerParallelTLS64-8 1000
3141325 ns/op 143010 B/op 1747 allocs/op
ok net/http 3.395s

$ go tool pprof -text -nodecount=10 ./http.test cpu.log


2570ms of 3590ms total (71.59%)
Dropped 129 nodes (cum <= 17.95ms)
Showing top 10 nodes out of 166 (cum >= 60ms)
flat flat% sum% cum cum%
1730ms 48.19% 48.19% 1750ms 48.75% crypto/elliptic.p256ReduceDegree
230ms 6.41% 54.60% 250ms 6.96% crypto/elliptic.p256Diff
120ms 3.34% 57.94% 120ms 3.34% math/big.addMulVVW
110ms 3.06% 61.00% 110ms 3.06% syscall.Syscall
90ms 2.51% 63.51% 1130ms 31.48% crypto/elliptic.p256Square
70ms 1.95% 65.46% 120ms 3.34% runtime.scanobject
60ms 1.67% 67.13% 830ms 23.12% crypto/elliptic.p256Mul
60ms 1.67% 68.80% 190ms 5.29% math/big.nat.montgomery
50ms 1.39% 70.19% 50ms 1.39% crypto/elliptic.p256ReduceCarry
50ms 1.39% 71.59% 60ms 1.67% crypto/elliptic.p256Sum
Flaga -text określa format wyjściowy, w tym przypadku tabelę tekstową z jednym rzędem na
każdą funkcję, posortowaną w kolejności od „najgorętszych” (zużywających najwięcej cykli CPU)
do „najchłodniejszych”. Flaga -nodecount=10 ogranicza wynik do dziesięciu wierszy. W przypad-
ku rażących problemów z wydajnością taki format tekstowy może być wystarczający, aby określić
przyczynę.
318 ROZDZIAŁ 11. TESTOWANIE

Ten profil mówi nam, że kryptografia krzywych eliptycznych jest ważna dla wydajności tego kon-
kretnego benchmarku HTTPS. Natomiast jeśli profil jest zdominowany przez funkcje alokacji pa-
mięci z pakietu runtime, zmniejszenie zużycia pamięci może być opłacalną optymalizacją.
W przypadku bardziej subtelnych problemów lepsze może być użycie jednego z graficznych wy-
świetlaczy narzędzia pprof. Wymagają one oprogramowania Graphviz, które można pobrać ze
strony: http://www.graphviz.org. Wtedy flaga -web renderuje skierowany graf funkcji programu,
opatrzony adnotacjami wskazań profilu CPU i pokolorowany, aby wskazać najgorętsze funkcje.
W tym podrozdziale prześlizgnęliśmy się jedynie po temacie narzędzi profilowania języka Go.
Więcej informacji na ten temat znajdziesz w artykule Profilling Go Programs na stronie: http://
blog.golang.org/profiling-go-programs.

11.6. Funkcje przykładów


Trzecim rodzajem funkcji traktowanym wyjątkowo przez narzędzie go test jest funkcja przykładu,
której nazwa rozpoczyna się od słowa Example. Ta funkcja nie ma ani parametrów, ani wyników.
Oto funkcja przykładu dla IsPalindrome:
func ExampleIsPalindrome() {
fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
fmt.Println(IsPalindrome("palindrom"))
// Output:
// true
// false
}
Funkcje przykładów służą trzem celom. Podstawowym z nich jest dokumentacja: dobry przykład
może być bardziej zwięzłym lub intuicyjnym sposobem oddania zachowania funkcji biblioteki
niż jej opisanie, zwłaszcza gdy zostanie użyty jako przypomnienie lub skrócona instrukcja. Przy-
kład może również demonstrować interakcje pomiędzy kilkoma typami i funkcjami należącymi
do jednego interfejsu API, podczas gdy dokumentacja musi być zawsze dołączona do jednego
miejsca, takiego jak deklaracja typu lub funkcji albo pakietu jako całości. I, w przeciwieństwie do
przykładów w komentarzach, funkcje przykładów są rzeczywistym kodem Go, podlegającym kon-
troli w czasie kompilacji, więc nie stają się nieaktualne wraz z ewoluowaniem kodu.
Na podstawie przyrostka funkcji Example serwer dokumentacji WWW godoc kojarzy funkcje
przykładów z ilustrowaną funkcją lub ilustrowanym pakietem, więc funkcja ExampleIsPalindrome
byłaby pokazana wraz z dokumentacją dla funkcji IsPalindrome, a funkcja przykładu, zwana po
prostu Example, byłaby powiązana z pakietem word jako całością.
Drugim celem jest to, że przykłady są wykonywalnymi testami uruchamianymi za pomocą pole-
cenia go test. Jeśli funkcja przykładu zawiera końcowy komentarz //Output (tak jak ta powyżej),
sterownik testów wykona funkcję i sprawdzi, czy to, co wypisała do standardowego strumienia
wyjściowego, odpowiada tekstowi w komentarzu.
Trzecim celem przykładu są praktyczne eksperymenty. Serwer godoc na stronie golang.org używa
funkcjonalności Go Playground, aby umożliwić użytkownikowi uruchamianie każdej funkcji
przykładu z poziomu przeglądarki internetowej, tak jak pokazano na rysunku 11.4. Często jest to
najszybszy sposób, aby zobaczyć, jak zachowuje się dana funkcja lub cecha języka.
11.6. FUNKCJE PRZYKŁADÓW 319

Rysunek 11.4. Interaktywny przykład funkcji strings.Join w godoc

Ostatnie dwa rozdziały książki poświęcone są pakietom reflect i unsafe, których regularnie używa
niewielu programistów Go, a jeszcze mniejsza ich liczba w ogóle potrzebuje tych pakietów. Jeśli
jeszcze nie napisałeś żadnych istotnych programów w języku Go, teraz jest na to odpowiednia
chwila.
320 ROZDZIAŁ 11. TESTOWANIE
Rozdział 12

Refleksja

Język Go zapewnia mechanizm służący do aktualizacji zmiennych i sprawdzania ich wartości


w czasie wykonywania programu, wywoływania ich metod oraz stosowania operacji właściwych dla
ich reprezentacji, a wszystko to bez znajomości ich typów w czasie kompilacji. Mechanizm ten nazy-
wa się refleksją. Refleksja pozwala nam również traktować same typy jako typy pierwszoklasowe.
W tym rozdziale przyjrzymy się cechom mechanizmu refleksji Go, aby zobaczyć, w jaki sposób
zwiększają ekspresyjność języka, a w szczególności aby się dowiedzieć, dlaczego mają zasadnicze
znaczenie dla implementacji dwóch ważnych interfejsów API: formatowania łańcuchów znaków
zapewnianego przez fmt oraz kodowania protokołów zapewnianego przez pakiety takie jak
encoding/json i encoding/xml. Refleksja jest również istotna dla mechanizmu szablonów zapew-
nianego przez pakiety text/template i html/template, które poznaliśmy w podrozdziale 4.6. Re-
fleksja to jednak złożona kwestia i nie jest przeznaczona do przypadkowego użycia, więc chociaż
te pakiety zostały zaimplementowane z wykorzystaniem refleksji, nie udostępniają jej we własnych
interfejsach API.

12.1. Dlaczego refleksja?


Czasami musimy napisać funkcję zdolną do radzenia sobie w jednolity sposób z wartościami typów,
które nie spełniają warunków powszechnego interfejsu, nie mają znanej reprezentacji lub nie istnieją
w chwili projektowania funkcji, albo mamy do czynienia ze wszystkimi trzema przypadkami na raz.
Znanym przykładem jest logika formatowania w funkcji fmt.Fprintf, która to funkcja może
w użyteczny sposób wypisywać dowolną wartość dowolnego typu, nawet zdefiniowanego przez
użytkownika. Spróbujmy zaimplementować podobną funkcję, wykorzystując to, co już wiemy.
Dla uproszczenia nasza funkcja będzie przyjmować jeden argument i zwracać wynik jako łańcuch
znaków, tak jak robi to fmt.Sprint, więc nazwiemy ją Sprint.
Zaczniemy od przełącznika typów, który testuje, czy argument definiuje metodę String, a jeśli
tak, wywołuje ją. Następnie dodamy instancje przełącznika, które testują dynamiczny typ wartości
względem każdego z typów podstawowych (string, int, bool itd.) i w każdym z przypadków prze-
prowadzają odpowiednią operację formatowania.
func Sprint(x interface{}) string {
type stringer interface {
String() string
}
322 ROZDZIAŁ 12. REFLEKSJA

switch x := x.(type) {
case stringer:
return x.String()
case string:
return x
case int:
return strconv.Itoa(x)
// …podobne przypadki dla int16, uint32 itd.…
case bool:
if x {
return "true"
}
return "false"
default:
// tablica, kanał, funkcja, mapa, wskaźnik, wycinek, struktura
return "???"
}
}
Ale w jaki sposób mamy poradzić sobie z pozostałymi typami, takimi jak []float64, map
[string][]string itd.? Moglibyśmy dodać więcej przypadków, ale liczba takich typów jest
nieograniczona. A co z typami nazwanymi, takimi jak url.Values? Nawet jeśli przełącznik typów
obejmowałby przypadek jego typu bazowego map[string][]string, nie dopasowałby url.Values,
ponieważ te dwa typy nie są identyczne, a przełącznik typów nie może zawierać instancji każdego ty-
pu takiego jak url.Values, gdyż wymagałoby to uzależnienia tej biblioteki od jej klientów.
Bez sposobu sprawdzania reprezentacji wartości nieznanych typów szybko byśmy utknęli. Tym,
czego potrzebujemy, jest refleksja.

12.2. reflect.Type i reflect.Value


Refleksja jest zapewniana przez pakiet reflect. Definiuje on dwa ważne typy: Type i Value.
Type reprezentuje typ Go. Jest to interfejs z wieloma metodami do rozróżniania typów oraz spraw-
dzania ich komponentów, takich jak pola struktury lub parametry funkcji. Jedyną implementacją
reflect.Type jest deskryptor typu (zob. podrozdział 7.5), czyli ta sama encja, która identyfikuje
dynamiczny typ wartości interfejsu.
Funkcja reflect.TypeOf przyjmuje dowolny interface{} i zwraca jego typ dynamiczny jako
reflect.Type:
t := reflect.TypeOf(3) // reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"
Powyższe wywołanie TypeOf(3) przypisuje wartość 3 do parametru typu interface{}. Przypo-
mnijmy z podrozdziału 7.5, że przypisanie z konkretnej wartości do typu interfejsu przeprowa-
dza pośrednią konwersję interfejsu, która tworzy wartość interfejsu składającą się z dwóch kom-
ponentów: jego typem dynamicznym jest typ (int) operandu, a jego wartością dynamiczną jest
wartość (3) operandu.
Ponieważ reflect.TypeOf zwraca typ dynamiczny wartości interfejsu, to zawsze zwraca typ kon-
kretny. Tak więc poniższy kod wyświetla np. "*os.File", a nie "io.Writer". Później zobaczymy,
że reflect.Type jest w stanie reprezentować również typy interfejsowe.
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File"
12.2. REFLECT.TYPE I REFLECT.VALUE 323

Należy zwrócić uwagę, że reflect.Type spełnia warunki interfejsu fmt.Stringer. Ponieważ


wyświetlanie dynamicznego typu wartości interfejsu jest przydatne do debugowania i rejestrowania,
fmt.Printf zapewnia skrót %T, który używa reflect.TypeOf wewnętrznie:
fmt.Printf("%T\n", 3) // "int"
Drugim ważnym typem w pakiecie reflect jest Value. Typ reflect.Value może przechowywać
wartość dowolnego typu. Funkcja reflect.ValueOf przyjmuje dowolny interface{} i zwraca
reflect.Value zawierający wartość dynamiczną tego interfejsu. Tak jak w przypadku reflect.
TypeOf, wyniki funkcji reflect.ValueOf są zawsze konkretne, ale reflect.Value może przecho-
wywać również wartości interfejsów.
v := reflect.ValueOf(3) // reflect.Value
fmt.Println(v) // "3"
fmt.Printf("%v\n", v) // "3"
fmt.Println(v.String()) // UWAGA: "<int Value>"
Tak jak reflect.Type, reflect.Value również spełnia warunki interfejsu fmt.Stringer, ale jeśli
Value nie przechowuje łańcucha znaków, wynik metody String ujawnia tylko typ. W zamian
można użyć czasownika %v pakietu fmt, który traktuje reflect.Value w sposób wyjątkowy.
Wywołanie metody Type na Value zwraca jego typ jako reflect.Type:
t := v.Type() // reflect.Type
fmt.Println(t.String()) // "int"
Operacją odwrotną do reflect.ValueOf jest metoda reflect.Value.Interface. Zwraca ona
interface{} przechowujący taką samą wartość konkretną jak reflect.Value:
v := reflect.ValueOf(3) // reflect.Value
x := v.Interface() // interface{}
i := x.(int) // int
fmt.Printf("%d\n", i) // "3"
Typy reflect.Value oraz interface{} mogą przechowywać dowolne wartości. Różnica polega
na tym, że pusty interfejs ukrywa reprezentację i wewnętrzne operacje przechowywanej wartości
i nie udostępnia żadnej z jej metod, więc jeśli nie znamy typu dynamicznego i nie używamy asercji
typu, aby zajrzeć do środka (tak jak zrobiliśmy powyżej), niewiele możemy zrobić ze znajdującą
się wewnątrz wartością. W przeciwieństwie do tego Value ma wiele metod sprawdzania swojej
zawartości niezależnie od jej typu. Użyjemy ich w naszej drugiej próbie implementacji ogólnej
funkcji formatowania, którą nazwiemy format.Any.
Zamiast przełącznika typów użyjemy metody Kind typu reflect.Value, aby rozróżnić przypadki.
Chociaż istnieje nieskończenie wiele typów, to istnieje skończona liczba rodzajów typów. Są to
podstawowe typy Bool i String oraz wszystkie liczby; typy złożone, jak Array i Struct; typy refe-
rencyjne: Chan, Func, Ptr, Slice i Map; typy Interface i wreszcie Invalid, który oznacza brak
jakiejkolwiek wartości. (Wartość zerowa typu reflect.Value ma rodzaj Invalid).
code/r12/format
package format
import (
"reflect"
"strconv"
)
// Any formatuje dowolną wartość jako łańcuch znaków.
func Any(value interface{}) string {
324 ROZDZIAŁ 12. REFLEKSJA

return formatAtom(reflect.ValueOf(value))
}

// formatAtom formatuje wartość bez sprawdzania jej wewnętrznej struktury.


func formatAtom(v reflect.Value) string {
switch v.Kind() {
case reflect.Invalid:
return "invalid"
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(v.Uint(), 10)
// …dla zwięzłości pominięto przypadki dla liczb zmiennoprzecinkowych i zespolonych…
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.String:
return strconv.Quote(v.String())
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
return v.Type().String() + " 0x" +
strconv.FormatUint(uint64(v.Pointer()), 16)
default: // reflect.Array, reflect.Struct, reflect.Interface
return v.Type().String() + " value"
}
}
Na razie nasza funkcja traktuje każdą wartość jako niepodzielną, bez wewnętrznej struktury —
stąd funkcja formatAtom. Dla typów złożonych (struktur i tablic) i interfejsów wyświetla tylko typ
wartości, a dla typów referencyjnych (kanałów, funkcji, wskaźników, wycinków i map) wyświetla
typ i adres referencyjny w formacie szesnastkowym. Nie jest to rozwiązanie idealne, ale wciąż sta-
nowi znaczną poprawę, a ponieważ metoda Kind przejmuje się tylko bazową reprezentacją, funkcja
format.Any działa również dla typów nazwanych, np.:
var x int64 = 1
var d time.Duration = 1 * time.Nanosecond
fmt.Println(format.Any(x)) // "1"
fmt.Println(format.Any(d)) // "1"
fmt.Println(format.Any([]int64{x})) // "[]int64 0x8202b87b0"
fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0"

12.3. Display — rekurencyjny wyświetlacz wartości


Zobaczmy teraz, jak poprawić wyświetlanie typów złożonych. Zamiast próbować dokładnie sko-
piować funkcję fmt.Sprint, zbudujemy debugującą funkcję narzędziową o nazwie Display, któ-
ra mając daną dowolnie złożoną wartość x, wyświetla kompletną strukturę tej wartości. Etykietuje
też każdy element ścieżką, w której został znaleziony. Zacznijmy od przykładu.
e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)
W powyższym wywołaniu argumentem dla funkcji Display jest drzewo składniowe z ewaluatora
wyrażeń z podrozdziału 7.9. Dane wyjściowe z funkcji Display zostały przedstawione poniżej:
Display e (eval.call):
e.fn = "sqrt"
e.args[0].type = eval.binary
e.args[0].value.op = 47
e.args[0].value.x.type = eval.Var
12.3. DISPLAY — REKURENCYJNY WYŚWIETLACZ WARTOŚCI 325

e.args[0].value.x.value = "A"
e.args[0].value.y.type = eval.Var
e.args[0].value.y.value = "pi"
Tam, gdzie to możliwe, należy unikać udostępniania refleksji w interfejsie API pakietu. Zdefiniu-
jemy nieeksportowaną funkcję display do wykonywania rzeczywistej pracy rekurencji oraz
eksportowaną Display, która jest prostą funkcją opakowującą, akceptującą parametr typu
interface{}:
code/r12/display
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T):\n", name, x)
display(name, reflect.ValueOf(x))
}
W funkcji display użyjemy zdefiniowanej wcześniej funkcji formatAtom do wyświetlania elementar-
nych wartości (podstawowych typów, funkcji i kanałów) i skorzystamy z metod typu reflect.Value
do rekurencyjnego wyświetlania każdego komponentu bardziej złożonego typu. Wraz ze schodze-
niem rekurencyjnym łańcuch znaków path, który wstępnie opisuje wartość początkową (np. "e"),
będzie rozszerzany, aby wskazać sposób dotarcia do bieżącej wartości (np. "e.args[0].Value").
Ponieważ nie udajemy już, że implementujemy fmt.Sprint, użyjemy pakietu fmt, aby przykład
pozostał krótki.
func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Invalid:
fmt.Printf("%s = invalid\n", path)
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
display(fieldPath, v.Field(i))
}
case reflect.Map:
for _, key := range v.MapKeys() {
display(fmt.Sprintf("%s[%s]", path,
formatAtom(key)), v.MapIndex(key))
}
case reflect.Ptr:
if v.IsNil() {
fmt.Printf("%s = nil\n", path)
} else {
display(fmt.Sprintf("(*%s)", path), v.Elem())
}
case reflect.Interface:
if v.IsNil() {
fmt.Printf("%s = nil\n", path)
} else {
fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
display(path+".value", v.Elem())
}
default: // podstawowe typy, kanały, funkcje
fmt.Printf("%s = %s\n", path, formatAtom(v))
}
}
326 ROZDZIAŁ 12. REFLEKSJA

Omówmy te przypadki po kolei.


Wycinki i tablice. Logika jest taka sama dla obu tych typów. Metoda Len zwraca liczbę elementów
wycinka lub wartość tablicy, a metoda Index(i) pobiera element z indeksu i, również jako
reflect.Value. Uruchamia procedurę panic, jeśli i jest poza zakresem. Są to metody analogiczne
do wbudowanych operacji na sekwencjach len(a) oraz a[i]. Funkcja display rekurencyjnie wy-
wołuje samą siebie na każdym elemencie sekwencji, dołączając do ścieżki notację indeksową "[i]".
Chociaż typ reflect.Value ma wiele metod, tylko nieliczne można bezpiecznie wywoływać na
każdej wartości. Metoda Index może być np. wywoływana na wartościach rodzajów Slice,
Array lub String, ale panikuje dla każdego innego rodzaju.
Struktury. Metoda NumField raportuje liczbę pól w strukturze, a metoda Field(i) zwraca wartość
i-tego pola jako reflect.Value. Lista pól obejmuje te promowane z pól anonimowych. Aby
dołączyć do ścieżki notację selektora pola ".f", musimy uzyskać typ reflect.Type struktury oraz
dostęp do nazwy jej i-tego pola.
Mapy. Metoda MapKeys zwraca wycinek wartości reflect.Value po jednej na każdy klucz mapy. Jak
zwykle podczas iteracji przez mapę kolejność jest niezdefiniowana. Metoda MapIndex(key) zwraca
wartość odpowiadającą kluczowi (key). Do ścieżki dodajemy notację indeksową "[key]".
(Idziemy tutaj na skróty. Typ klucza mapy nie jest ograniczony do typów, które formatAtom obsłu-
guje najlepiej. Tablice, struktury i interfejsy mogą być również prawidłowymi kluczami mapy.
Rozszerzenie tego przypadku, aby wyświetlał klucz w całości, jest tematem ćwiczenia 12.1).
Wskaźniki. Metoda Elem zwraca zmienną wskazywaną przez wskaźnik jako reflect.Value. Ta
operacja byłaby bezpieczna, nawet jeśli wartością wskaźnika byłoby nil. W takim przypadku
wynik miałby rodzaj invalid, ale używamy funkcji IsNil, aby bezpośrednio wykrywać wskaź-
niki nil, więc możemy wyświetlać bardziej odpowiedni komunikat. Aby uniknąć dwuznaczności,
dodajemy do ścieżki prefiks "*" i umieszczamy ją w nawiasach.
Interfejsy. Ponownie używamy funkcji IsNil, aby sprawdzić, czy interfejs ma wartość nil, a jeśli
nie, pobieramy jego wartość dynamiczną, używając funkcji v.Elem(), i wyświetlamy jego typ
i wartość.
Nasza funkcja Display jest teraz kompletna, więc zobaczmy ją w działaniu. Poniższy typ Movie
(film) jest niewielką wariacją na temat typu z podrozdziału 4.5:
type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
Zadeklarujmy wartość tego typu i zobaczmy, co zrobi z nią funkcja Display:
strangelove := Movie{
Title: "Dr Strangelove",
Subtitle: "Czyli jak przestałem się martwić i pokochałem bombę",
Year: 1964,
Color: false,
Actor: map[string]string{
"Dr Strangelove": "Peter Sellers",
"Kapitan Lionel Mandrake": "Peter Sellers",
12.3. DISPLAY — REKURENCYJNY WYŚWIETLACZ WARTOŚCI 327

"Prezydent Merkin Muffley": "Peter Sellers",


"Generał Buck Turgidson": "George C. Scott",
"Generał brygady Jack D. Ripper": "Sterling Hayden",
`Major T.J. "King" Kong`: "Slim Pickens",
},
Oscars: []string{
"Najlepszy aktor pierwszoplanowy (nominacja)",
"Najlepszy scenariusz adaptowany (nominacja)",
"Najlepszy reżyser (nominacja)",
"Najlepszy film (nominacja)",
},
}
Wywołanie Display("strangelove", strangelove) wyświetla:
Display strangelove (display.Movie):
strangelove.Title = "Dr Strangelove"
strangelove.Subtitle = "Czyli jak przestałem się martwić i pokochałem bombę"
strangelove.Year = 1964
strangelove.Color = false
strangelove.Actor["Generał Buck Turgidson"] = "George C. Scott"
strangelove.Actor["Generał brygady Jack D. Ripper"] = "Sterling Hayden"
strangelove.Actor["Major T.J. \"King\" Kong"] = "Slim Pickens"
strangelove.Actor["Dr Strangelove"] = "Peter Sellers"
strangelove.Actor["Kapitan Lionel Mandrake"] = "Peter Sellers"
strangelove.Actor["Prezydent Merkin Muffley"] = "Peter Sellers"
strangelove.Oscars[0] = "Najlepszy aktor pierwszoplanowy (nominacja)"
strangelove.Oscars[1] = "Najlepszy scenariusz adaptowany (nominacja)"
strangelove.Oscars[2] = "Najlepszy reżyser (nominacja)"
strangelove.Oscars[3] = "Najlepszy film (nominacja)"
strangelove.Sequel = nil
Możemy użyć funkcji Display do wyświetlenia wewnętrznych mechanizmów typów biblioteki,
takich jak *os.File:
Display("os.Stderr", os.Stderr)
// Output:// Display os.Stderr (*os.File):
// (*(*os.Stderr).file).fd = 2
// (*(*os.Stderr).file).name = "/dev/stderr"
// (*(*os.Stderr).file).nepipe = 0
Należy zwrócić uwagę, że dla refleksji widoczne są nawet nieeksportowane pola. Trzeba pamiętać,
że dane wyjściowe z tego przykładu mogą być inne na różnych platformach i mogą zmieniać się
w czasie wraz z ewoluowaniem biblioteki. (Te pola są prywatne nie bez powodu!). Możemy nawet
zastosować funkcję Display do reflect.Value i przyjrzeć się jej trawersacji wewnętrznej reprezentacji
deskryptora typu dla *os.File. Dane wyjściowe z wywołania Display("rV", reflect.Value
Of(os.Stderr)) pokazano poniżej, chociaż oczywiście Twój przebieg może się różnić:
Display rV (reflect.Value):
(*rV.typ).size = 8
(*rV.typ).hash = 871609668
(*rV.typ).align = 8
(*rV.typ).fieldAlign = 8
(*rV.typ).kind = 22
(*(*rV.typ).string) = "*os.File"
(*(*(*rV.typ).uncommonType).methods[0].name) = "Chdir"
(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = "func() error"
(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) error"
...
328 ROZDZIAŁ 12. REFLEKSJA

Zaobserwuj różnicę między tymi dwoma przykładami:


var i interface{} = 3

Display("i", i)
// Output:
// Display i (int):
// i = 3

Display("&i", &i)
// Output:
// Display &i (*interface {}):
// (*&i).type = int
// (*&i).value = 3
W pierwszym przykładzie Display wywołuje funkcję reflect.ValueOf(i), która zwraca wartość
rodzaju Int. Jak wspomniano w podrozdziale 12.2, reflect.ValueOf zawsze zwraca Value kon-
kretnego typu, ponieważ wydobywa zawartość wartości interfejsu.
W drugim przykładzie Display wywołuje funkcję reflect.ValueOf(&i), która zwraca wskaź-
nik do i rodzaju Ptr. Przypadek przełącznika dla Ptr wywołuje na tej wartości metodę Elem,
która zwraca typ Value reprezentujący samą zmienną i rodzaju Interface. Typ Value uzyskany
w sposób pośredni, tak jak ten, może reprezentować całkowicie dowolną wartość, w tym interfejsy.
Funkcja display wywołuje samą siebie rekurencyjnie i tym razem wyświetla osobne komponenty
dla dynamicznego typu i dynamicznej wartości interfejsu.
Przy bieżącej implementacji funkcja Display nigdy nie zakończy działania, jeśli napotka w grafie
obiektów cykl taki jak ta powiązana lista, która zjada własny ogon:
// Struktura, która wskazuje na samą siebie.
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, &c}
Display("c", c)
Funkcja Display wyświetla tę stale rosnącą ekspansję:
Display c (display.Cycle):
c.Value = 42
(*c.Tail).Value = 42
(*(*c.Tail).Tail).Value = 42
(*(*(*c.Tail).Tail).Tail).Value = 42
...ad infinitum...
Wiele programów Go zawiera przynajmniej trochę cyklicznych danych. Uodpornienie funkcji
Display na takie cykle jest trudne i wymaga dodatkowej ewidencji do rejestrowania zbioru refe-
rencji, które zostały prześledzone do tej pory. Jest to zbyt kosztowne. Ogólne rozwiązanie wymaga
funkcjonalności unsafe tego języka, jak zobaczymy w podrozdziale 13.3.
Cykle stanowią mniejszy problem dla funkcji fmt.Sprint, ponieważ rzadko próbuje ona wyświe-
tlać pełną strukturę. Gdy napotka np. wskaźnik, przerywa rekurencję, wyświetlając wartość licz-
bową tego wskaźnika. Może utknąć, próbując wyświetlić wycinek lub mapę, które zawierają same
siebie jako element, ale takie rzadkie przypadki nie uzasadniają podejmowania znacznych do-
datkowych wysiłków w celu wprowadzenia obsługi cykli.
Ćwiczenie 12.1. Rozszerz funkcję Display w taki sposób, aby mogła wyświetlać mapy, których
klucze są strukturami lub tablicami.
12.4. PRZYKŁAD: KODOWANIE S-WYRAŻEŃ 329

Ćwiczenie 12.2. Uczyń funkcję display bezpieczną do używania na cyklicznych strukturach


danych poprzez ograniczenie liczby kroków podejmowanych przed porzuceniem rekurencji.
(W podrozdziale 13.3 zobaczymy kolejny sposób wykrywania cykli).

12.4. Przykład: kodowanie S-wyrażeń


Funkcja Display jest procedurą debugowania dla wyświetlania ustrukturyzowanych danych struktu-
ralnych, ale niewiele brakuje, aby mogła kodować lub marshalować dowolne obiekty Go jako ko-
munikaty w notacji przenośnej odpowiedniej dla komunikacji między procesami.
Jak widzieliśmy w podrozdziale 4.5, standardowa biblioteka Go obsługuje wiele formatów, w tym:
JSON, XML i ASN.1. Kolejną notacją, która jest nadal powszechnie stosowana, są S-wyrażenia (ang.
S-expressions), stanowiące składnię języka Lisp. W przeciwieństwie do innych notacji S-wyrażenia
nie są obsługiwane przez standardową bibliotekę języka Go, choćby dlatego, że nie mają powszechnie
akceptowanej definicji, pomimo kilku prób normalizacji oraz istnienia wielu implementacji.
W tym podrozdziale zdefiniujemy pakiet, który koduje dowolne obiekty Go, używając notacji
S-wyrażeń obsługującej następujące konstrukcje:
42 integer
"witaj" string (z cytowaniem w stylu Go)
foo symbol (niecytowana nazwa)
(1 2 3) list (zero lub więcej pozycji zamkniętych w nawiasach)
Wartości logiczne są tradycyjnie kodowane za pomocą symbolu t dla prawdy oraz przy użyciu
pustej listy () lub symbolu nil dla fałszu, ale dla uproszczenia nasza implementacja je ignoruje.
Ignoruje również kanały i funkcje, ponieważ ich stan jest nieprzezroczysty dla refleksji. Ignorowane
są także liczby rzeczywiste i zespolone liczby zmiennoprzecinkowe oraz interfejsy. Dodanie dla
nich wsparcia jest tematem ćwiczenia 12.3.
Będziemy kodować typy języka Go za pomocą S-wyrażeń następująco: liczby całkowite i łańcuchy
znaków będą kodowane w oczywisty sposób, wartości zerowe będą kodowane jako symbol nil,
tablice i wycinki będą kodowane za pomocą notacji listy.
Struktury będą kodowane jako lista wiązań pól, a każde wiązanie pola będzie dwuelementową listą,
której pierwszy element (symbol) będzie nazwą pola, a drugi wartością pola. Mapy również
będą kodowane jako lista par, a każdą parę będą stanowiły klucz i wartość jednego wpisu mapy.
Tradycyjnie S-wyrażenia reprezentują listę par klucz-wartość przy użyciu pojedynczej komórki
cons (klucz . wartość) dla każdej pary zamiast dwuelementowej listy, ale dla uproszczenia
dekodowania będziemy ignorować kropkową notację listy.
Kodowanie jest wykonywane poprzez pojedynczą funkcję rekurencyjną encode, pokazaną poniżej.
Jej struktura jest zasadniczo taka sama jak struktura funkcji Display z poprzedniego podrozdziału:
code/r12/sexpr
func encode(buf *bytes.Buffer, v reflect.Value) error {
switch v.Kind() {
case reflect.Invalid:
buf.WriteString("nil")

case reflect.Int, reflect.Int8, reflect.Int16,


reflect.Int32, reflect.Int64:
fmt.Fprintf(buf, "%d", v.Int())
330 ROZDZIAŁ 12. REFLEKSJA

case reflect.Uint, reflect.Uint8, reflect.Uint16,


reflect.Uint32, reflect.Uint64, reflect.Uintptr:
fmt.Fprintf(buf, "%d", v.Uint())

case reflect.String:
fmt.Fprintf(buf, "%q", v.String())

case reflect.Ptr:
return encode(buf, v.Elem())

case reflect.Array, reflect.Slice: // (wartość …)


buf.WriteByte('(')
for i := 0; i < v.Len(); i++ {
if i > 0 {
buf.WriteByte(' ')
}
if err := encode(buf, v.Index(i)); err != nil {
return err
}
}
buf.WriteByte(')')

case reflect.Struct: // ((nazwa wartość) …)


buf.WriteByte('(')
for i := 0; i < v.NumField(); i++ {
if i > 0 {
buf.WriteByte(' ')
}
fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
if err := encode(buf, v.Field(i)); err != nil {
return err
}
buf.WriteByte(')')
}
buf.WriteByte(')')

case reflect.Map: // ((klucz wartość) …)


buf.WriteByte('(')
for i, key := range v.MapKeys() {
if i > 0 {
buf.WriteByte(' ')
}
buf.WriteByte('(')
if err := encode(buf, key); err != nil {
return err
}
buf.WriteByte(' ')
if err := encode(buf, v.MapIndex(key)); err != nil {
return err
}
buf.WriteByte(')')
}
buf.WriteByte(')')

default: // liczba zmiennoprzecinkowa, liczba zespolona, wartość logiczna, kanał, funkcja, interfejs
return fmt.Errorf("nieobsługiwany typ: %s", v.Type())
}
return nil
}
12.4. PRZYKŁAD: KODOWANIE S-WYRAŻEŃ 331

Funkcja Marshal opakowuje koder w interfejs API podobny do interfejsów innych pakietów
encoding/...:
// Marshal koduje wartość Go w formie S-wyrażenia.
func Marshal(v interface{}) ([]byte, error) {
var buf bytes.Buffer
if err := encode(&buf, reflect.ValueOf(v)); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
Oto dane wyjściowe z funkcji Marshal zastosowanej do zmiennej strangelove z podrozdziału 12.3:
((Title "Dr Strangelove") (Subtitle "Czyli jak przestałem się martwić i po
kochałem bombę") (Year 1964) (Actor (("Kapitan Lionel Mandrake" "Peter Sel
lers") ("Prezydent Merkin Muffley" "Peter Sellers") ("Genenerał Buck Turgi
dson" "George C. Scott") ("Generał brygady Jack D. Ripper" "Sterling Hayde
n") ("Major T.J. \"King\" Kong" "Slim Pickens") ("Dr Strangelove" "Peter S
ellers"))) (Oscars ("Najlepszy aktor pierwszoplanowy (nominacja)" "Najlepsz
y scenariusz adaptowany (nominacja)" "Najlepszy reżyser (nominacja)" "Najle
pszy film (nominacja)")) (Sequel nil))
Cały blok danych wyjściowych wyświetlany jest w jednej długiej linii z minimalną ilością spacji,
przez co trudno je odczytać. Poniżej zostały zamieszczone te same dane wyjściowe sformatowane
ręcznie zgodnie z konwencjami S-wyrażeń. Napisanie programu typu pretty-printer dla S-wyrażeń
pozostawiamy jako (wymagające) ćwiczenie. Przykłady kodów dostarczone z książką zawierają
jego prostą wersję.
((Title "Dr Strangelove")
(Subtitle "Czyli jak przestałem się martwić i pokochałem bombę")
(Year 1964)
(Actor (("Kapitan Lionel Mandrake" "Peter Sellers")
("Prezydent Merkin Muffley" "Peter Sellers")
("Generał Buck Turgidson" "George C. Scott")
("Generał brygady Jack D. Ripper" "Sterling Hayden")
("Major T.J. \"King\" Kong" "Slim Pickens")
("Dr Strangelove" "Peter Sellers")))
(Oscars ("Najlepszy aktor pierwszoplanowy (nominacja)"
"Najlepszy scenariusz adaptowany (nominacja)"
"Najlepszy reżyser (nominacja)"
"Najlepszy film (nominacja)"))
(Sequel nil))
Podobnie jak funkcje: fmt.Print, json.Marshal i Display, funkcja sexpr.Marshal będzie zapętlać
w nieskończoność, jeśli zostanie wywołana z danymi cyklicznymi.
W podrozdziale 12.6 naszkicujemy implementację odpowiedniej funkcji dekodowania S-wyrażeń,
ale zanim do tego przejdziemy, najpierw musimy zrozumieć, w jaki sposób refleksja może być
stosowana do aktualizowania zmiennych programu.
Ćwiczenie 12.3. Zaimplementuj brakujące przypadki funkcji encode. Koduj wartości logiczne
jako t i nil, liczby zmiennoprzecinkowe za pomocą notacji Go, a liczby zespolone, takie jak 1+2i,
jako #C(1.0 2.0). Interfejsy mogą być kodowane jako para nazwy typu i wartości, np. ("[]int"
(1 2 3)), ale pamiętaj, że ta notacja jest niejednoznaczna: metoda reflect.Type.String może
zwracać ten sam łańcuch znaków dla różnych typów.
Ćwiczenie 12.4. Zmodyfikuj funkcję encode, aby ładnie formatowała S-wyrażenia w stylu przedsta-
wionym powyżej.
332 ROZDZIAŁ 12. REFLEKSJA

Ćwiczenie 12.5. Dostosuj funkcję encode, aby emitowała dane w formacie JSON zamiast w formie
S-wyrażeń. Przetestuj swój koder za pomocą standardowego dekodera json.Unmarshal.
Ćwiczenie 12.6. Dostosuj funkcję encode, aby w ramach optymalizacji nie kodowała pola, którego
wartość jest wartością zerową dla jego typu.
Ćwiczenie 12.7. Utwórz strumieniowy interfejs API dla dekodera S-wyrażeń, wykorzystując styl
dekodera json.Decoder (zob. podrozdział 4.5).

12.5. Ustawianie zmiennych za pomocą reflect.Value


Na razie w naszym programie refleksja tylko interpretowała wartości na różne sposoby. Celem
tego podrozdziału jest jednak ich zmiana.
Przypomnijmy, że niektóre wyrażenia Go, takie jak x, x.f[1] oraz *p, oznaczają zmienne, ale inne,
takie jak x + 1 i f(2), nie oznaczają zmiennych. Zmienna jest adresowalną lokalizacją pamięci,
która zawiera wartość, a jej wartość może być aktualizowana poprzez ten adres.
Podobne rozróżnienie dotyczy wartości reflect.Value. Niektóre z nich są adresowalne, a inne nie.
Rozważmy następujące deklaracje:
x := 2 // wartość typ zmienna?
a := reflect.ValueOf(2) // 2 int nie
b := reflect.ValueOf(x) // 2 int nie
c := reflect.ValueOf(&x) // &x *int nie
d := c.Elem() // 2 int tak (x)
Wartość w a nie jest adresowalna. Jest to jedynie kopia liczby całkowitej 2. To samo dotyczy b.
Wartość w c jest również nieadresowalna, będąc kopią wartości wskaźnika &x. W rzeczywistości
żadna wartość reflect.Value zwracana przez reflect.ValueOf(x) nie jest adresowalna. Ale d,
wywiedziona z c przez wyłuskanie znajdującego się w niej wskaźnika, odnosi się do zmiennej i jest
w ten sposób adresowalna. Możemy użyć tego podejścia, wywołując reflect.ValueOf(&x).Elem(),
aby uzyskać adresowalny typ Value dla każdej zmiennej x.
Możemy odpytać typ reflect.Value, czy jest adresowalny, za pośrednictwem jego metody CanAddr:
fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"
Uzyskujemy adresowalną zmienną reflect.Value za każdym razem, gdy robimy to pośrednio
poprzez wskaźnik, nawet jeśli zaczniemy od nieadresowalnego Value. Wszystkie standardowe za-
sady adresowalności mają odpowiedniki dla refleksji. Przykładowo: ponieważ wyrażenie e[i]
indeksowania wycinka pośrednio podąża za wskaźnikiem, to jest adresowalne, nawet jeśli wyraże-
nie e nie jest. Przez analogię: reflect.ValueOf(e).Index(i) odwołuje się do zmiennej, a zatem
jest adresowalne, nawet jeśli reflect.ValueOf(e) nie jest.
Aby odzyskać zmienną z adresowalnego typu reflect.Value, wymagane są trzy kroki. Najpierw
wywołujemy funkcję Addr(), która zwraca wartość Value przechowującą wskaźnik do zmiennej.
Następnie wywołujemy Interface() na tej wartości Value, co zwraca wartość interface{} zawie-
rającą wskaźnik. Na koniec, jeśli znamy typ zmiennej, możemy użyć asercji typu, aby pobrać zawar-
tość interfejsu jako zwykły wskaźnik. Możemy wtedy zaktualizować zmienną poprzez wskaźnik:
12.5. USTAWIANIE ZMIENNYCH ZA POMOCĄ REFLECT.VALUE 333

x := 2
d := reflect.ValueOf(&x).Elem() // d odwołuje się do zmiennej x
px := d.Addr().Interface().(*int) // px := &x
*px = 3 // x = 3
fmt.Println(x) // "3"
Możemy też zmienną, do której odnosi się adresowalny reflect.Value, zaktualizować bezpo-
średnio, bez użycia wskaźnika, poprzez wywołanie metody reflect.Value.Set:
d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"
Te same kontrole przypisywalności, które są zwykle wykonywane przez kompilator, są wykonywane
w czasie działania programu przez metody Set. Powyżej zmienna i wartość mają ten sam typ
int, ale jeśli zmienna miałaby typ int64, program uruchomiłby procedurę panic, więc istotne jest
upewnienie się, że wartość jest przypisywalna do typu zmiennej:
d.Set(reflect.ValueOf(int64(5))) // panic: int64 nie jest przypisywalne do int
Oczywiście wywołanie metody Set na nieadresowalnej wartości reflect.Value również uru-
chamia procedurę panic:
x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panic: Set używa nieadresowalnej wartości
Istnieją warianty metody Set wyspecjalizowane dla określonych grup podstawowych typów:
SetInt, SetUint, SetString, SetFloat itd.:
d := reflect.ValueOf(&x).Elem()
d.SetInt(3)
fmt.Println(x) // "3"
W pewnym sensie te metody są bardziej wyrozumiałe. Wykonanie metody SetInt będzie koń-
czyło się powodzeniem tak długo, jak długo typ zmiennej będzie jakąś liczbą całkowitą ze znakiem
lub nawet typem nazwanym, którego typem bazowym jest liczba całkowita ze znakiem, a jeśli war-
tość będzie zbyt duża, po cichu zostanie przycięta, by pasować. Postępuj jednak ostrożnie: wywołanie
metody SetInt na wartości reflect.Value, która odwołuje się do zmiennej typu interface{},
wywoła panikę, mimo że wywołanie metody Set powiodłoby się.
x := 1
rx := reflect.ValueOf(&x).Elem()
rx.SetInt(2) // OK, x = 2
rx.Set(reflect.ValueOf(3)) // OK, x = 3
rx.SetString("witaj") // panic: string nie jest przypisywalny do int
rx.Set(reflect.ValueOf("witaj")) // panic: string nie jest przypisywalny do int

var y interface{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2) // panic: SetInt wywołana na interfejsowej wartości Value
ry.Set(reflect.ValueOf(3)) // OK, y = int(3)
ry.SetString("witaj") // panic: SetString wywołana na interfejsowej wartości Value
ry.Set(reflect.ValueOf("witaj")) // OK, y = "witaj"
Gdy zastosowaliśmy funkcję Display do os.Stdout, okazało się, że refleksja może odczytywać
wartości z nieeksportowanych pól struktury, które są niedostępne zgodnie ze zwykłymi regułami
języka, tak jak pole fd int struktury os.File na platformie uniksowej. Refleksja nie może jednak
aktualizować takich wartości:
334 ROZDZIAŁ 12. REFLEKSJA

stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, zmienna os.File


fmt.Println(stdout.Type()) // "os.File"
fd := stdout.FieldByName("fd")
fmt.Println(fd.Int()) // "1"
fd.SetInt(2) // panic: nieeksportowane pole
Adresowalna zmienna reflect.Value rejestruje, czy została uzyskana poprzez trawersację nieeks-
portowanego pola struktury, a jeśli tak, uniemożliwia modyfikację. W konsekwencji metoda
CanAddr nie stanowi zazwyczaj właściwej kontroli do użycia przed ustawieniem zmiennej. Po-
wiązana z nią metoda CanSet informuje, czy zmienna reflect.Value jest adresowalna oraz
ustawialna:
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"

12.6. Przykład: dekodowanie S-wyrażeń


Dla każdej funkcji Marshal zapewnianej przez pakiety encoding/... standardowej biblioteki
istnieje odpowiadająca jej funkcja Unmarshal, która wykonuje dekodowanie. Jak widzieliśmy
w podrozdziale 4.5, mając np. dla naszego typu Movie (zob. podrozdział 12.3) dany wycinek bajtów
zawierający dane zakodowane w formacie JSON, możemy zdekodować je w następujący sposób:
data := []byte{/* ... */}
var movie Movie
err := json.Unmarshal(data, &movie)
Funkcja Unmarshal wykorzystuje refleksję do modyfikowania pól istniejącej zmiennej movie,
tworząc nowe mapy, struktury i wycinki, tak jak zostało to określone przez typ Movie i zawartość
przychodzących danych.
Zaimplementujmy teraz prostą funkcję Unmarshal dla S-wyrażeń, analogiczną do użytej powyżej stan-
dardowej funkcji json.Unmarshal i będącą odwrotnością naszej wcześniejszej funkcji sexpr.Marshal.
Musimy ostrzec, że solidna i ogólna implementacja wymaga znacznie więcej kodu niż można
wygodnie zmieścić w tym przykładzie (i tak już długim), więc zastosowaliśmy wiele skrótów. Ob-
służymy jedynie ograniczony podzbiór S-wyrażeń i nie obsłużymy elegancko błędów. Ten kod
ma na celu zilustrowanie refleksji, nie parsowania.
Typ lexer używa typu Scanner z pakietu text/scanner, aby rozbić strumień wejściowy na sekwencję
tokenów, takich jak: komentarze, identyfikatory, literały łańcuchów znaków i literały liczbowe.
Metoda Scan typu Scanner przesuwa skaner i zwraca rodzaj następnego tokena, który ma typ rune.
Większość tokenów, takich jak '(', składa się z pojedynczej runy, ale pakiet text/scanner re-
prezentuje rodzaje wieloznakowych tokenów Ident, String oraz Int, wykorzystując niewielkie
ujemne wartości typu rune. Po wywołaniu funkcji Scan, która zwraca jeden z tych rodzajów toke-
na, metoda skanera TokenText zwraca tekst tokena.
Ponieważ typowy parser może wymagać sprawdzenia bieżącego tokena kilka razy, a metoda Scan
przesuwa skaner, opakowujemy skaner w typ pomocniczy o nazwie lexer, który śledzi token ostat-
nio zwrócony przez metodę Scan.
code/r12/sexpr
type lexer struct {
scan scanner.Scanner
token rune // bieżący token
}
12.6. PRZYKŁAD: DEKODOWANIE S-WYRAŻEŃ 335

func (lex *lexer) next() { lex.token = lex.scan.Scan() }


func (lex *lexer) text() string { return lex.scan.TokenText() }

func (lex *lexer) consume(want rune) {


if lex.token != want { // UWAGA: nie jest to przykład dobrej obsługi błędów
panic(fmt.Sprintf("otrzymane %q, oczekiwane %q", lex.text(), want))
}
lex.next()
}
Teraz przejdźmy do parsera. Składa się on z dwóch głównych funkcji. Pierwsza z nich, read, odczy-
tuje S-wyrażenie, które rozpoczyna się od bieżącego tokena, i aktualizuje zmienną, do której od-
wołujemy się za pomocą adresowalnej zmiennej v typu reflect.Value.
func read(lex *lexer, v reflect.Value) {
switch lex.token {
case scanner.Ident:
// Jedynymi prawidłowymi identyfikatorami są "nil" oraz nazwy pól struktury.
if lex.text() == "nil" {
v.Set(reflect.Zero(v.Type()))
lex.next()
return
}
case scanner.String:
s, _ := strconv.Unquote(lex.text()) // UWAGA: ignorowanie błędów
v.SetString(s)
lex.next()
return
case scanner.Int:
i, _ := strconv.Atoi(lex.text()) // UWAGA: ignorowanie błędów
v.SetInt(int64(i))
lex.next()
return
case '(':
lex.next()
readList(lex, v)
lex.next() // konsumuje ')'
return
}
panic(fmt.Sprintf("nieoczekiwany token %q", lex.text()))
}
Nasze S-wyrażenia używają identyfikatorów w dwóch celach: dla nazw pól struktury i wartości
nil dla wskaźnika. Funkcja read obsługuje tylko ten drugi przypadek. Po napotkaniu identyfika-
tora scanner.Ident o wartości "nil" ustawia v na wartość zerową dla jej typu za pomocą funkcji
reflect.Zero. Dla każdego innego identyfikatora zgłasza błąd. Funkcja readList, którą zoba-
czymy za chwilę, obsługuje identyfikatory używane jako nazwy pól struktury.
Token '(' wskazuje początek listy. Druga funkcja readList dekoduje listę na zmienną typu
złożonego (mapę, strukturę, wycinek lub tablicę), w zależności od rodzaju zmiennej Go, którą
aktualnie zapełniamy. W każdym przypadku pętla parsuje pozycje, dopóki nie napotka pasującego
nawiasu zamykającego ')', wykrywanego przez funkcję endList.
Interesującą częścią jest rekurencja. Najprostszym przypadkiem jest tablica. Dopóki nie zostanie
napotkany nawias zamykający ')', używamy metody Index do uzyskania zmiennej dla każdego
elementu tablicy i wykonujemy rekurencyjne wywołanie funkcji read, aby ją zapełnić. Tak jak
w wielu innych przypadkach błędów, jeśli dane wejściowe powodują, że dekoder indeksuje poza
koniec tablicy, dekoder uruchamia procedurę panic. Podobne podejście stosuje się do wycinków,
336 ROZDZIAŁ 12. REFLEKSJA

z tym wyjątkiem, że musimy utworzyć nową zmienną dla każdego elementu, zapełnić ją, a następnie
dołączyć do wycinka.
Pętle dla struktur i map muszą parsować podlistę (klucz wartość) w każdej iteracji. W przypadku
struktur klucz jest symbolem identyfikującym pole. Analogicznie do przypadku dla tablic, uzy-
skujemy istniejącą zmienną dla pola struktury, używając funkcji FieldByName i wywołując ją reku-
rencyjnie, aby zapełnić tę zmienną. W przypadku map klucz może być dowolnego typu i, analo-
gicznie do przypadku dla wycinków, tworzymy nową zmienną, zapełniamy ją rekurencyjnie, a na
koniec wstawiamy do mapy nową parę klucz-wartość.
func readList(lex *lexer, v reflect.Value) {
switch v.Kind() {
case reflect.Array: // (pozycja …)
for i := 0; !endList(lex); i++ {
read(lex, v.Index(i))
}

case reflect.Slice: // (pozycja …)


for !endList(lex) {
item := reflect.New(v.Type().Elem()).Elem()
read(lex, item)
v.Set(reflect.Append(v, item))
}

case reflect.Struct: // ((nazwa wartość) …)


for !endList(lex) {
lex.consume('(')
if lex.token != scanner.Ident {
panic(fmt.Sprintf("otrzymano token %q, oczekiwana nazwa pola",
lex.text()))
}
name := lex.text()
lex.next()
read(lex, v.FieldByName(name))
lex.consume(')')
}

case reflect.Map: // ((klucz wartość) …)


v.Set(reflect.MakeMap(v.Type()))
for !endList(lex) {
lex.consume('(')
key := reflect.New(v.Type().Key()).Elem()
read(lex, key)
value := reflect.New(v.Type().Elem()).Elem()
read(lex, value)
v.SetMapIndex(key, value)
lex.consume(')')
}

default:
panic(fmt.Sprintf("nie można zdekodować listy na %v", v.Type()))
}
}

func endList(lex *lexer) bool {


switch lex.token {
case scanner.EOF:
panic("koniec pliku")
12.7. UZYSKIWANIE DOSTĘPU DO ZNACZNIKÓW PÓL STRUKTURY 337

case ')':
return true
}
return false
}
Wreszcie opakowujemy parser w pokazaną poniżej eksportowaną funkcję Unmarshal, ukrywają-
cą niektóre niedoskonałości implementacji. Błędy napotkane podczas parsowania skutkują wywoła-
niem paniki, więc funkcja Unmarshal używa odroczonego wywołania w celu odzyskania spraw-
ności po panice (zob. podrozdział 5.10) i zwraca komunikat o błędzie.
// Unmarshal parsuje dane S-wyrażenia i zapełnia zmienną,
// której adres znajduje się w różnym od nil wskaźniku out.
func Unmarshal(data []byte, out interface{}) (err error) {
lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
lex.scan.Init(bytes.NewReader(data))
lex.next() // pobiera pierwszy token
defer func() {
// UWAGA: to nie jest przykład idealnej obsługi błędów.
if x := recover(); x != nil {
err = fmt.Errorf("błąd w %s: %v", lex.scan.Position, x)
}
}()
read(lex, reflect.ValueOf(out).Elem())
return nil
}
Implementacja o jakości produkcyjnej nie powinna nigdy panikować dla żadnych danych wej-
ściowych i powinna zgłaszać informacyjny błąd dla każdego nieszczęśliwego wypadku, być może
z numerem linii lub offsetem. Niemniej jednak mamy nadzieję, że ten przykład daje pewne wyobra-
żenie o tym, co się dzieje pod maską pakietów takich jak encoding/json i jak można użyć reflek-
sji do zapełniania struktur danych.
Ćwiczenie 12.8. Funkcja sexpr.Unmarshal, tak jak funkcja json.Marshal, wymaga pełnych da-
nych wejściowych w wycinku bajtów, zanim będzie mogła rozpocząć dekodowanie. Zdefiniuj
typ sexpr.Decoder, który podobnie jak json.Decoder umożliwia dekodowanie sekwencji war-
tości z interfejsu io.Reader. Zmień funkcję sexpr.Unmarshal, aby używała tego nowego typu.
Ćwiczenie 12.9. Napisz oparty na tokenach interfejs API do dekodowania S-wyrażeń, naśladujący
styl xml.Decoder (zob. podrozdział 7.14). Będziesz potrzebować pięciu typów tokenów: Symbol,
String, Int, StartList i EndList.
Ćwiczenie 12.10. Rozszerz funkcję sexpr.Unmarshal, aby obsługiwała wartości logiczne, liczby
zmiennoprzecinkowe oraz interfejsy kodowane przez Twoje rozwiązanie dla ćwiczenia 12.3.
(Podpowiedź: do dekodowania interfejsów będziesz potrzebować mapowania z nazwy każdego
obsługiwanego typu na jego reflect.Type).

12.7. Uzyskiwanie dostępu do znaczników pól struktury


W podrozdziale 4.5 używaliśmy znaczników pól struktury do modyfikowania kodowania JSON
wartości struktury Go. Znacznik pola json pozwala wybrać alternatywną nazwę pola i ograniczyć
puste pola w danych wyjściowych. W tym podrozdziale zobaczymy, jak uzyskać dostęp do znaczni-
ków pól przy użyciu refleksji.
338 ROZDZIAŁ 12. REFLEKSJA

W serwerze WWW większość funkcji procedur obsługi HTTP najpierw wyodrębnia parametry żą-
dania do zmiennych lokalnych. Zdefiniujemy funkcję narzędziową params.Unpack, która używa
znaczników pól struktury, aby pisanie procedur obsługi HTTP (zob. podrozdział 7.7) było wygod-
niejsze.
Najpierw pokażemy, jak jest ona wykorzystywana. Poniższa funkcja search jest procedurą obsługi
HTTP. Definiuje zmienną o nazwie data anonimowego typu struct, którego pola odpowiadają
parametrom żądania HTTP. Znaczniki pól tej struktury określają nazwy parametrów, które często są
krótkie i tajemnicze, ponieważ w adresie URL przestrzeń jest cenna. Funkcja Unpack zapełnia
strukturę na podstawie żądania, aby do tych parametrów można było uzyskać dostęp wygodnie
i z odpowiednim typem.
code/r12/search
import "code/r12/params"
// search implementuje punkt końcowy /search adresu URL.
func search(resp http.ResponseWriter, req *http.Request) {
var data struct {
Labels []string `http:"l"`
MaxResults int `http:"max"`
Exact bool `http:"x"`
}
data.MaxResults = 10 // ustawia wartość domyślną
if err := params.Unpack(req, &data); err != nil {
http.Error(resp, err.Error(), http.StatusBadRequest) // 400
return
}
// …reszta procedury obsługi…
fmt.Fprintf(resp, "Wyszukiwanie: %+v\n", data)
}
Poniższa funkcja Unpack robi trzy rzeczy. Najpierw wywołuje funkcję req.ParseForm() do parsowa-
nia żądania. Od tego momentu req.Form zawiera wszystkie parametry, niezależnie od tego, czy klient
HTTP użył metody żądania GET, czy POST.
Następnie funkcja Unpack tworzy mapowanie z efektywnej nazwy każdego pola na zmienną dla te-
go pola. Efektywna nazwa może się różnić od rzeczywistej nazwy, jeśli pole posiada znacznik.
Metoda Field typu reflect.Type zwraca typ reflect.StructField, który zapewnia informacje
na temat typu każdego pola, takie jak jego nazwa, typ i opcjonalny znacznik. Pole Tag ma typ
reflect.StructTag, który jest typem łańcucha znaków zapewniającym metodę Get do parsowania
i wydobywania podłańcucha znaków dla konkretnego klucza, w tym przypadku http:"...".
code/r12/params
// Unpack zapełnia pola struktury wskazane przez ptr z parametrów żądania HTTP w req.
func Unpack(req *http.Request, ptr interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
// Tworzy mapę pól z kluczami w postaci efektywnych nazw.
fields := make(map[string]reflect.Value)
v := reflect.ValueOf(ptr).Elem() // zmienna struktury
for i := 0; i < v.NumField(); i++ {
fieldInfo := v.Type().Field(i) // reflect.StructField
tag := fieldInfo.Tag // reflect.StructTag
name := tag.Get("http")
12.7. UZYSKIWANIE DOSTĘPU DO ZNACZNIKÓW PÓL STRUKTURY 339

if name == "" {
name = strings.ToLower(fieldInfo.Name)
}
fields[name] = v.Field(i)
}

// Aktualizuje pole struktury dla każdego parametru w żądaniu.


for name, values := range req.Form {
f := fields[name]
if !f.IsValid() {
continue // ignorowanie nierozpoznanych parametrów HTTP
}
for _, value := range values {
if f.Kind() == reflect.Slice {
elem := reflect.New(f.Type().Elem()).Elem()
if err := populate(elem, value); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
f.Set(reflect.Append(f, elem))
} else {
if err := populate(f, value); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
}
}
}
return nil
}
Na koniec funkcja Unpack iteruje przez pary nazwa-wartość parametrów HTTP i aktualizuje
odpowiadające im pola struktury. Przypomnijmy, że ta sama nazwa parametru może pojawić się
więcej niż raz. Jeśli tak się stanie, a pole jest wycinkiem, wtedy wszystkie wartości tego parametru
są gromadzone w tym wycinku. W przeciwnym razie pole jest wielokrotnie nadpisywane, więc tylko
ostatnia wartość ma jakiś skutek.
Funkcja populate zajmuje się ustawianiem pojedynczego pola v (lub pojedynczego elementu
pola wycinka) według wartości parametru. Na razie ta funkcja obsługuje tylko łańcuchy znaków,
liczby całkowite ze znakiem i wartości logiczne. Obsługa pozostałych typów została pozostawiona
jako ćwiczenie.
func populate(v reflect.Value, value string) error {
switch v.Kind() {
case reflect.String:
v.SetString(value)

case reflect.Int:
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
v.SetInt(i)

case reflect.Bool:
b, err := strconv.ParseBool(value)
if err != nil {
return err
}
v.SetBool(b)
340 ROZDZIAŁ 12. REFLEKSJA

default:
return fmt.Errorf("nieobsługiwany rodzaj %s", v.Type())
}
return nil
}
Jeśli do serwera WWW dodamy procedurę obsługi server, to może być typowa sesja:
$ go build code/r12/search
$ ./search &
$ ./fetch 'http://localhost:12345/search'
Wyszukiwanie: {Labels:[] MaxResults:10 Exact:false}
$ ./fetch 'http://localhost:12345/search?l=golang&l=programming'
Wyszukiwanie: {Labels:[golang programming] MaxResults:10 Exact:false}
$ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100'
Wyszukiwanie: {Labels:[golang programming] MaxResults:100 Exact:false}
$ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming'
Wyszukiwanie: {Labels:[golang programming] MaxResults:10 Exact:true}
$ ./fetch 'http://localhost:12345/search?q=hello&x=123'
x: strconv.ParseBool: parsing "123": invalid syntax
$ ./fetch 'http://localhost:12345/search?q=hello&max=lots'
max: strconv.ParseInt: parsing "lots": invalid syntax
Ćwiczenie 12.11. Napisz funkcję Pack odpowiadającą funkcji Unpack. Mając daną wartość struktury,
funkcja Pack powinna zwracać adres URL zawierający wartości parametrów z danej struktury.
Ćwiczenie 12.12. Rozszerz notację znacznika pola, aby wyrazić wymagania dotyczące poprawności
parametrów. Możemy np. chcieć, aby łańcuch znaków był prawidłowym adresem e-mail lub nume-
rem karty kredytowej, a liczba całkowita była prawidłowym kodem pocztowym. Zmodyfikuj funk-
cję Unpack, aby sprawdzała te wymagania.
Ćwiczenie 12.13. Zmodyfikuj koder (zob. podrozdział 12.4) i dekoder (zob. podrozdział 12.6)
S-wyrażeń, aby honorowały znacznik pola sexpr: "..." w podobny sposób, jak robi to encoding/
json (zob. podrozdział 4.5).

12.8. Wyświetlanie metod typu


Nasz ostatni przykład refleksji wykorzystuje reflect.Type do wyświetlenia typu dowolnej wartości
oraz enumeracji jego metod:
code/r12/methods
// Print wyświetla zestaw metod wartości x.
func Print(x interface{}) {
v := reflect.ValueOf(x)
t := v.Type()
fmt.Printf("typ %s\n", t)

for i := 0; i < v.NumMethod(); i++ {


methType := v.Method(i).Type()
fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
strings.TrimPrefix(methType.String(), "func"))
}
}
Oba typy: reflect.Type i reflect.Value mają metodę o nazwie Method. Każde wywołanie
t.Method(i) zwraca instancję reflect.Method, czyli typ struct, który opisuje nazwę i typ pojedyn-
czej metody. Każde wywołanie v.Method(i) zwraca typ reflect.Value reprezentujący wartość
12.9. SŁOWO OSTRZEŻENIA 341

metody (zob. podrozdział 6.4), tzn. metodę powiązaną z jej odbiornikiem. Gdy korzystamy z metody
reflect.Value.Call (której nie możemy tutaj pokazać z braku miejsca), możemy wywoływać typy
Value rodzaju Func, takie jak ten, ale nasz program potrzebuje tylko swojego typu Type.
Oto metody należące do dwóch typów: time.Duration i *strings.Replacer:
methods.Print(time.Hour)
// Output:
// typ time.Duration
// func (time.Duration) Hours() float64
// func (time.Duration) Minutes() float64
// func (time.Duration) Nanoseconds() int64
// func (time.Duration) Seconds() float64
// func (time.Duration) String() string

methods.Print(new(strings.Replacer))
// Output:
// typ *strings.Replacer
// func (*strings.Replacer) Replace(string) string
// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)

12.9. Słowo ostrzeżenia


Istnieje znacznie więcej kwestii związanych z interfejsem API refleksji, niż jesteśmy w stanie tutaj
pokazać, ale przedstawione przykłady dają wyobrażenie o tym, co jest możliwe. Refleksja jest wszech-
stronnym i ekspresyjnym narzędziem, ale powinna być stosowana ostrożnie z trzech powodów.
Pierwszym powodem jest to, że kod oparty na refleksji może być kruchy. Każdej pomyłce, która
spowodowałaby zgłoszenie przez kompilator typu error, odpowiada pewien sposób niewłaściwego
użycia refleksji, ale podczas gdy kompilator zgłasza błąd w czasie kompilacji, błąd refleksji jest
raportowany w trakcie wykonywania jako panika prawdopodobnie długo po napisaniu progra-
mu lub nawet długo po tym, jak został uruchomiony.
Jeśli np. funkcja readList (zob. podrozdział 12.6) odczytywałaby łańcuch znaków z danych wej-
ściowych podczas zapełniania zmiennej typu int, wywołanie reflect.Value.SetString uru-
chomiłoby procedurę panic. W większości programów używających refleksji występują podobne
zagrożenia i trzeba dołożyć znacznej staranności, aby śledzić typ, adresowalność i ustawialność
każdej zmiennej reflect.Value.
Najlepszym sposobem na uniknięcie tej kruchości jest zapewnienie, że użycie refleksji będzie
zhermetyzowane w Twoim pakiecie, i w miarę możliwości unikanie reflect.Value na rzecz kon-
kretnych typów z interfejsu API pakietu, aby ograniczyć dane wejściowe do poprawnych wartości.
Jeśli nie jest to możliwe, należy wykonywać dodatkowe dynamiczne kontrole przed każdą ryzy-
kowną operacją. Przykład możemy znaleźć w standardowej bibliotece: gdy funkcja fmt.Printf
stosuje czasownik do niewłaściwego operandu, nie panikuje w tajemniczy sposób, ale wypisuje
informacyjny komunikat o błędzie. Program nadal zawiera błąd, ale jest on łatwiejszy do zdiagno-
zowania.
fmt.Printf("%d %s\n", "witaj", 42) // "%!d(string=witaj) %!s(int=42)"
Refleksja zmniejsza również bezpieczeństwo i dokładność automatycznej refaktoryzacji i narzędzi
do analizy, ponieważ nie mogą one ustalić informacji o typach i opierać się na tych informacjach.
342 ROZDZIAŁ 12. REFLEKSJA

Drugim powodem do unikania refleksji jest to, że ponieważ typy służą jako forma dokumentacji,
a operacje refleksji nie mogą podlegać statycznej kontroli typów, silnie refleksyjny kod jest czę-
sto trudny do zrozumienia. Należy zawsze starannie dokumentować oczekiwane typy i inne nie-
zmienniki funkcji, które przyjmują typy interface{} lub reflect.Value.
Trzecim powodem jest to, że funkcje oparte na refleksji mogą być o rząd lub dwa rzędy wielkości
wolniejsze niż kod wyspecjalizowany dla określonego typu. W typowym programie większość funkcji
nie jest istotna dla ogólnej wydajności, więc można używać refleksji, gdy dzięki niej program staje się
bardziej przejrzysty. Testowanie szczególnie dobrze nadaje się do refleksji, ponieważ większość
testów używa małych zbiorów danych. Jednak w przypadku funkcji na kluczowej ścieżce najlepiej
jest refleksji unikać.
Rozdział 13

Programowanie
niskiego poziomu

Konstrukcja języka Go gwarantuje szereg mechanizmów bezpieczeństwa, które ograniczają licz-


bę sposobów, na jakie program Go może się „nie udać”. Podczas kompilacji kontrole typów wy-
krywają większość prób zastosowania do wartości operacji, która nie jest dla tego typu odpowied-
nia, np. odejmowanie jednego łańcucha znaków od drugiego. Surowe reguły dla konwersji typów
uniemożliwiają bezpośredni dostęp do wewnętrznych funkcjonalności wbudowanych typów,
takich jak łańcuchy znaków, mapy, wycinki i kanały.
W przypadku błędów, które nie mogą być wykryte statycznie, takich jak próba uzyskania dostępu
poza zakresem tablicy lub wyłuskanie wskaźnika nil, kontrole dynamiczne powodują, że gdy
wystąpi zabroniona operacja, program zostanie natychmiast przerwany z wyświetleniem informa-
cyjnego błędu. Automatyczne zarządzanie pamięcią (odzyskiwanie pamięci, ang. garbage collection)
eliminuje błędy typu use after free (próby uzyskania dostępu do pamięci po jej odzyskaniu) oraz
większość wycieków pamięci.
Wiele szczegółów implementacji jest niedostępnych dla programów Go. Nie ma sposobu odkrycia
układu pamięci typu złożonego, takiego jak struktura, kodu maszynowego dla funkcji lub tożsamości
wątku systemu operacyjnego, w którym uruchomiona jest bieżąca funkcja goroutine. W rzeczywisto-
ści planista Go swobodnie przesuwa funkcje goroutine z jednego wątku do drugiego. Wskaźnik
identyfikuje zmienną bez ujawniania adresu numerycznego tej zmiennej. Adresy mogą ulec zmianie
wraz z przenoszeniem zmiennych przez mechanizm odzyskiwania pamięci. Wskaźniki są przejrzy-
ście aktualizowane.
Wszystkie te cechy sprawiają, że programy Go, szczególnie te wadliwe, stają się bardziej przewi-
dywalne i mniej tajemnicze niż programy napisane w C, czyli podstawowym języku niskiego po-
ziomu. Poprzez ukrywanie bazowych szczegółów czynią również programy Go wysoce przenośnymi,
ponieważ semantyka języka jest w znacznym stopniu niezależna od jakiegokolwiek konkretnego
kompilatora, systemu operacyjnego czy od jakiejkolwiek architektury procesora. (Nie jest jednak
całkowicie niezależna — niektóre szczegóły przeciekają, np.: rozmiar słowa procesora, kolejność ewalu-
acji niektórych wyrażeń czy zbiór ograniczeń implementacji nałożonych przez kompilator).
Od czasu do czasu możemy zrezygnować z niektórych z tych pomocnych gwarancji, aby osiągnąć
najwyższą możliwą wydajność, zapewnić współdziałanie z bibliotekami napisanymi w innych
językach lub zaimplementować funkcję, która nie może być wyrażona w czystym języku Go.
344 ROZDZIAŁ 13. PROGRAMOWANIE NISKIEGO POZIOMU

W tym rozdziale zobaczymy, w jaki sposób pakiet unsafe pozwala nam wyjść poza zwyczajowe re-
guły, i dowiemy się, jak używać narzędzia cgo do tworzenia powiązań Go dla bibliotek C i wywołań
systemu operacyjnego.
Podejścia opisane w tym rozdziale nie powinny być stosowane lekkomyślnie. Bez starannej dbało-
ści o szczegóły mogą powodować różnego rodzaju nieprzewidywalne, tajemnicze, nielokalne
awarie, z którymi na nieszczęście zaznajomieni są programiści C. Użycie pakietu unsafe unieważnia
również gwarancję języka Go dotyczącą kompatybilności z przyszłymi wersjami, ponieważ (w sposób
zamierzony lub nie) łatwo jest uzależnić się od niesprecyzowanych szczegółów implementacji,
które mogą się nieoczekiwanie zmienić.
Pakiet unsafe jest dość magiczny. Chociaż wydaje się regularny i jest importowany w zwykły
sposób, jest w rzeczywistości implementowany przez kompilator. Zapewnia dostęp do wielu wbu-
dowanych funkcjonalności języka, które nie są normalnie dostępne, ponieważ udostępniają szcze-
góły układu pamięci Go. Przedstawienie tych funkcjonalności w postaci osobnego pakietu zwraca
uwagę na rzadkie przypadki, w których są one potrzebne. Ponadto niektóre środowiska mogą
ograniczać stosowanie pakietu unsafe ze względów bezpieczeństwa.
Pakiet unsafe jest szeroko stosowany w ramach niskopoziomowych pakietów, takich jak: runtime,
os, syscall i net, które współdziałają z systemem operacyjnym, ale prawie nigdy nie jest wymagany
przez zwykłe programy.

13.1. Funkcje unsafe.Sizeof, Alignof i Offsetof


Funkcja unsafe.Sizeof raportuje w bajtach rozmiar reprezentacji swojego operandu, który może
być wyrażeniem dowolnego typu. Samo wyrażenie nie jest ewaluowane. Wywołanie Sizeof jest
wyrażeniem stałej typu uintptr, dzięki czemu wynik może być używany jako rozmiar typu tabli-
cowego lub do obliczania innych stałych.
import "unsafe"
fmt.Println(unsafe.Sizeof(float64(0))) // "8"
Funkcja Sizeof raportuje tylko rozmiar stałej części każdej struktury danych, takiej jak wskaźnik
i długość łańcucha znaków, ale nie części pośrednich, takich jak zawartość łańcucha znaków. Ty-
powe rozmiary dla wszystkich typów Go niebędących typami złożonymi przedstawiono poniżej,
choć dokładne rozmiary mogą się różnić w zależności od zestawu narzędzi. W celu zapewnienia
przenośności podaliśmy rozmiary typów referencyjnych (lub typów zawierających referencje)
w kategoriach słów, w których słowo ma 4 bajty na platformie 32-bitowej i 8 bajtów na platformie
64-bitowej.

Komputery najbardziej efektywnie ładują i przechowują wartości z pamięci, gdy te wartości są pra-
widłowo wyrównane. Adres wartości typu dwubajtowego, takiego jak int16, powinien być np. liczbą
parzystą, adres wartości czterobajtowej, takiej jak rune, powinien być wielokrotnością czterech,
a adres wartości ośmiobajtowej, takiej jak float64, uint64 lub 64-bitowy wskaźnik, powinien być
wielokrotnością ośmiu. Wymagania wyrównań o wyższych wielokrotnościach są niespotykane,
nawet w przypadku większych typów danych, takich jak complex128.
13.1. FUNKCJE UNSAFE.SIZEOF, ALIGNOF I OFFSETOF 345

Typ Rozmiar

bool 1 bajt
intN, uintN, floatN, complexN N/8 bajtów (np. float64 ma 8 bajtów)
int, uint, uintptr 1 słowo
*T 1 słowo
string 2 słowa (dane, długość)
[]T 3 słowa (dane, długość, pojemność)
map 1 słowo
func 1 słowo
chan 1 słowo
interface 2 słowa (typ, wartość)

Z tego powodu rozmiar wartości typu złożonego (struktury lub tablicy) jest co najmniej sumą
rozmiarów jego pól lub elementów, ale może być większy z powodu obecności „dziur”. Dziury są
niewykorzystywanymi spacjami dodawanymi przez kompilator, aby zapewnić, że następujące po
nich pole lub element będą prawidłowo wyrównane względem początku struktury lub tablicy.
Specyfikacja języka nie gwarantuje, że kolejność zadeklarowania pól stanie się kolejnością, w jakiej
będą one ułożone w pamięci, więc teoretycznie kompilator może je swobodnie reorganizować,
chociaż w chwili pisania tego tekstu żaden tego nie robi. Jeśli typy pól struktury są różnych rozmia-
rów, bardziej efektywne pod względem przestrzeni może być zadeklarowanie tych pól w kolejności,
która upakowuje je tak ściśle jak to możliwe. Poniższe trzy struktury mają takie same pola, ale
pierwsza z nich wymaga do 50% więcej pamięci niż dwie pozostałe:
// 64-bitowe 32-bitowe
struct{ bool; float64; int16 } // 3 słowa 4 słowa
struct{ float64; int16; bool } // 2 słowa 3 słowa
struct{ bool; int16; float64 } // 2 słowa 3 słowa
Szczegóły dotyczące algorytmu wyrównania leżą poza zakresem tej książki. Na pewno nie warto
martwić się każdą strukturą, ale efektywne upakowywanie może uczynić często alokowane struk-
tury danych bardziej kompaktowymi, a zatem szybszymi.
Funkcja unsafe.Alignof raportuje wymagane wyrównanie typu jej argumentu. Podobnie jak
Sizeof może być stosowana do wyrażenia dowolnego typu i daje stałą. Zazwyczaj logiczne i nume-
ryczne typy są wyrównywane do ich rozmiaru (maksymalnie do 8 bajtów), a wszystkie pozostałe
typy są wyrównywane do słów.
Funkcja unsafe.Offsetof, której operand musi być selektorem pola x.f, oblicza offset pola f
względem początku zawierającej go struktury x, z uwzględnieniem dziur, jeśli są jakieś.
Rysunek 13.1 pokazuje zmienną x struktury i jej układ pamięci w typowych 32- i 64-bitowych im-
plementacjach Go. Szare obszary to dziury.
var x struct {
a bool
b int16
c [ ]int
}
346 ROZDZIAŁ 13. PROGRAMOWANIE NISKIEGO POZIOMU

Rysunek 13.1. Dziury w strukturze

Poniższa tabela przedstawia wyniki zastosowania trzech funkcji unsafe do samej struktury x i do
każdego z jej trzech pól:
Typowa platforma 32-bitowa:
Sizeof(x) = 16 Alignof(x) = 4
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4

Typowa platforma 64-bitowa:


Sizeof(x) = 32 Alignof(x) = 8
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
Pomimo ich nazw te funkcje nie są w rzeczywistości niebezpieczne (ang. unsafe) i mogą być po-
mocne dla zrozumienia surowego układu pamięci w programie przy optymalizacji przestrzeni.

13.2. Typ unsafe.Pointer


Wiele typów wskaźnika jest zapisywanych jako *T, co oznacza „wskaźnik do zmiennej typu T”. Typ
unsafe.Pointer to specjalny rodzaj wskaźnika, który może przechowywać adres dowolnej zmiennej.
Oczywiście nie możemy pośrednio korzystać z unsafe.Pointer, używając *p, ponieważ nie wiemy,
jaki typ powinno mieć to wyrażenie. Podobnie jak zwykłe wskaźniki, wskaźniki unsafe.Pointer są
porównywalne i mogą być porównywane z nil, która jest wartością zerową tego typu.
Zwykły wskaźnik *T może być konwertowany na unsafe.Pointer, a unsafe.Pointer może być
z powrotem konwertowany na zwykły wskaźnik, niekoniecznie tego samego typu *T. Poprzez
konwersję wskaźnika *float64 na wskaźnik *uint64 możemy np. sprawdzić wzorzec bitowy
zmiennej liczby zmiennoprzecinkowej:
package math

func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }

fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"


Poprzez uzyskany wskaźnik możemy również zaktualizować wzorzec bitowy. Jest to nieszkodliwe
w przypadku zmiennej liczby zmiennoprzecinkowej, ponieważ każdy wzorzec bitowy jest prawidło-
wy, ale ogólnie rzecz biorąc, konwersje wskaźnika unsafe.Pointer pozwalają zapisywać w pamięci
dowolne wartości, a tym samym obalić system typów.
13.2. TYP UNSAFE.POINTER 347

Wskaźnik unsafe.Pointer można także przekonwertować na uintptr, który przechowuje liczbową


wartość wskaźnika, umożliwiając wykonywanie operacji arytmetycznych na adresach. (Przypo-
mnijmy z rozdziału 3., że uintptr jest liczbą całkowitą bez znaku, wystarczająco szeroką, aby repre-
zentować adres). Ta konwersja może być również stosowana w odwrotnym kierunku, ale tu rów-
nież konwersja z uintptr na unsafe.Pointer może obalić system typów, ponieważ nie wszystkie
numery są prawidłowymi adresami.
Wiele wartości unsafe.Pointer jest więc pośrednikami dla konwersji zwykłych wskaźników na
surowe adresy numeryczne i z powrotem. Poniższy przykład pobiera adres zmiennej x, dodaje
offset jej pola b, konwertuje wynikowy adres na *int16 i przez ten wskaźnik aktualizuje x.b:
code/r13/unsafeptr
var x struct {
a bool
b int16
c [ ]int
}

// Równoważne z pb := &x.b.
pb := (*int16)(unsafe.Pointer(
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42

fmt.Println(x.b) // "42"
Chociaż składnia jest kłopotliwa (co nie jest raczej niczym złym, ponieważ te funkcjonalności
powinny być stosowane z umiarem), nie należy ulegać pokusie wprowadzania tymczasowych
zmiennych typu uintptr, aby uczynić kod bardziej czytelnym. Ten kod jest nieprawidłowy:
// UWAGA: subtelnie nieprawidłowe!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
Powód jest bardzo subtelny. Niektóre mechanizmy odzyskiwania pamięci przenoszą zmienne
w inne obszary pamięci, aby redukować fragmentację lub ewidencjonowanie. Tego rodzaju me-
chanizm odzyskiwania pamięci jest znany jako przenoszący GC (ang. moving garbage collector).
Gdy zmienna jest przenoszona, wszystkie wskaźniki, które przechowują adres starej lokalizacji,
muszą być zaktualizowane, aby wskazywały nowy adres. Z punktu widzenia mechanizmu odzy-
skiwania pamięci unsafe.Pointer jest wskaźnikiem, a tym samym jego wartość trzeba zmienić
przy przenoszeniu zmiennej, ale uintptr jest tylko liczbą, więc jej wartość nie może ulec zmianie.
Powyższy nieprawidłowy kod ukrywa wskaźnik przed mechanizmem odzyskiwania pamięci
w niebędącej wskaźnikiem zmiennej tmp. Zanim zostanie wykonana druga instrukcja, zmienna x
może zostać przeniesiona, a liczba w zmiennej tmp przestanie już być adresem &x.b. Trzecia in-
strukcja nadpisuje dowolną lokalizację pamięci wartością 42.
Istnieją niezliczone patologiczne wariacje na ten temat. Po wykonaniu tej instrukcji:
pT := uintptr(unsafe.Pointer(new(T))) // UWAGA: źle!
nie ma żadnych wskaźników odwołujących się do zmiennej utworzonej przez new, więc mechanizm
odzyskiwania pamięci jest uprawniony do recyklingu jej pamięci, gdy ta instrukcja się zakończy,
po czym pT będzie zawierać adres, pod którym ta zmienna była, ale już nie jest.
Żadna z bieżących implementacji Go nie wykorzystuje przenoszącego mechanizmu odzyskiwania
pamięci (choć przyszłe implementacje mogą to robić), ale to nie powód do samozadowolenia:
348 ROZDZIAŁ 13. PROGRAMOWANIE NISKIEGO POZIOMU

aktualne wersje Go przenoszą w pamięci niektóre zmienne. Przypomnijmy z podrozdziału 5.2, że


stosy funkcji goroutine rosną w miarę potrzeb. Gdy tak się dzieje, wszystkie zmienne ze starego
stosu mogą zostać przeniesione do nowego, większego stosu, więc nie można polegać na tym, że
wartość numeryczna adresu zmiennej będzie pozostawała niezmieniona przez cały czas jej życia.
W chwili pisania tego tekstu nie ma jasnych wskazówek co do tego, na czym mogą polegać pro-
gramiści Go po konwersji z unsafe.Pointer na uintptr (zob. temat Go 7192), więc zdecydowanie
zalecamy, aby zakładać jedynie absolutne minimum. Traktuj wszystkie wartości uintptr, jakby za-
wierały poprzedni adres zmiennej, i minimalizuj liczbę operacji między konwersją z unsafe.Pointer
na uintptr i użyciem uintptr. W naszym pierwszym z powyższych przykładów wszystkie te trzy
operacje — konwersja na uintptr, dodanie offsetu pola, konwersja z powrotem — pojawiają się
w ramach pojedynczego wyrażenia.
Gdy wywołujemy funkcję biblioteczną, która zwraca uintptr (taką jak poniższe funkcje z pakietu
reflect), wynik powinien być natychmiast konwertowany na unsafe.Pointer, aby zapewnić,
że w dalszym ciągu będzie wskazywał tę samą zmienną.
package reflect

func (Value) Pointer() uintptr


func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (indeks 1)

13.3. Przykład: głęboka równoważność


Funkcja DeepEqual z pakietu reflect wskazuje, czy dwie wartości są „głęboko” równoważne.
DeepEqual porównuje podstawowe wartości w taki sposób, jakby używała wbudowanego ope-
ratora ==. W przypadku wartości złożonych trawersuje je rekurencyjnie, porównując odpowiada-
jące sobie elementy. Ponieważ działa dla każdej pary wartości, nawet tych, które nie są porówny-
walne za pomocą operatora ==, ma szerokie zastosowanie w testach. Poniższy test wykorzystuje
funkcję DeepEqual do porównania dwóch wartości []string:
func TestSplit(t *testing.T) {
got := strings.Split("a:b:c", ":")
want := []string{"a", "b", "c"};
if !reflect.DeepEqual(got, want) { /* ... */ }
}
Chociaż funkcja DeepEqual jest przydatna, jej rozróżnienia mogą się wydawać przesadnie sztywne.
Nie uznaje np. mapy nil za równą pustej mapie różnej od nil ani wycinka nil za równego puste-
mu wycinkowi różnemu od nil:
var a, b []string = nil, []string{}
fmt.Println(reflect.DeepEqual(a, b)) // "false"

var c, d map[string]int = nil, make(map[string]int)


fmt.Println(reflect.DeepEqual(c, d)) // "false"
W tym podrozdziale zdefiniujemy funkcję Equal, która porównuje dowolne wartości. Tak jak
DeepEqual, funkcja Equal porównuje wycinki i mapy na podstawie ich elementów. Jednak w prze-
ciwieństwie do DeepEqual, uznaje wycinek (lub mapę) nil za równy pustemu wycinkowi różnemu od
nil. Podstawowa rekurencja przez argumenty może być wykonywana za pomocą refleksji, przy
użyciu podejścia podobnego do podejścia programu Display z podrozdziału 12.3. Jak zwykle
zdefiniujemy nieeksportowaną funkcję equal dla rekurencji. Na razie nie przejmuj się parametrem
13.3. PRZYKŁAD: GŁĘBOKA RÓWNOWAŻNOŚĆ 349

seen. Dla każdej pary wartości x i y, które mają być porównywane, equal sprawdza, czy obie są
prawidłowe (lub żadna nie jest) oraz czy mają ten sam typ. Wynik funkcji jest zdefiniowany jako
zestaw przypadków przełącznika, które porównują dwie wartości tego samego typu. Aby zaoszczę-
dzić miejsce, pominęliśmy kilka przypadków, ponieważ ten wzorzec powinien już być znajomy.
code/r13/equal
func equal(x, y reflect.Value, seen map[comparison]bool) bool {
if !x.IsValid() || !y.IsValid() {
return x.IsValid() == y.IsValid()
}
if x.Type() != y.Type() {
return false
}

// …pominięta kontrola cykli (zostanie pokazana później)…

switch x.Kind() {
case reflect.Bool:
return x.Bool() == y.Bool()

case reflect.String:
return x.String() == y.String()

// …dla zwięzłości pominięte przypadki liczbowe…

case reflect.Chan, reflect.UnsafePointer, reflect.Func:


return x.Pointer() == y.Pointer()

case reflect.Ptr, reflect.Interface:


return equal(x.Elem(), y.Elem(), seen)

case reflect.Array, reflect.Slice:


if x.Len() != y.Len() {
return false
}
for i := 0; i < x.Len(); i++ {
if !equal(x.Index(i), y.Index(i), seen) {
return false
}
}
return true

// …dla zwięzłości pominięte przypadki struktury i mapy…


}
panic("nieosiągalne")
}
Jak zwykle nie udostępniamy użycia refleksji w interfejsie API, więc eksportowana funkcja Equal
musi wywoływać funkcję reflect.ValueOf na swoich argumentach:
// Equal raportuje, czy x i y są głęboko równoważne.
func Equal(x, y interface{}) bool {
seen := make(map[comparison]bool)
return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}

type comparison struct {


x, y unsafe.Pointer
t reflect.Type
}
350 ROZDZIAŁ 13. PROGRAMOWANIE NISKIEGO POZIOMU

Aby można było mieć pewność, że algorytm zakończy działanie nawet w przypadku cyklicznych
struktur danych, musi on rejestrować, które pary zmiennych zostały już porównane, i unikać
powtórnego ich porównywania. Funkcja Equal alokuje zestaw struktur comparison, z których
każda przechowuje adres dwóch zmiennych (reprezentowanych jako wartości unsafe.Pointer)
oraz typ porównania. Poza adresami musimy rejestrować również typ, ponieważ różne zmienne
mogą mieć ten sam adres. Jeśli obie zmienne, x i y, są np. tablicami, wtedy x i x[0] mają ten sam
adres, tak samo jak y i y[0], ważne jest więc rozróżnianie, czy porównaliśmy x i y, czy też
x[0] i y[0].
Gdy funkcja equal ustali, że jej argumenty są tego samego typu, ale zanim wykona instrukcje
przełącznika, sprawdza, czy nie porównuje dwóch już wcześniej widzianych zmiennych, a jeśli tak,
przerywa rekurencję.
// Kontrola cykli.
if x.CanAddr() && y.CanAddr() {
xptr := unsafe.Pointer(x.UnsafeAddr())
yptr := unsafe.Pointer(y.UnsafeAddr())
if xptr == yptr {
return true // identyczne referencje
}
c := comparison{xptr, yptr, x.Type()}
if seen[c] {
return true // już widziane
}
seen[c] = true
}
Oto nasza funkcja Equal w akcji:
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
fmt.Println(Equal([]string{"foo"}, []string{"bar"})) // "false"
fmt.Println(Equal([]string(nil), []string{})) // "true"
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
Działa nawet na cyklicznych danych wejściowych podobnych do tych, które spowodowały, że
funkcja Display z podrozdziału 12.3 utknęła w pętli:
// Cyrkularne listy powiązane a -> b -> oraz c -> c.
type link struct {
value string
tail *link
}
a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
a.tail, b.tail, c.tail = b, a, c
fmt.Println(Equal(a, a)) // "true"
fmt.Println(Equal(b, b)) // "true"
fmt.Println(Equal(c, c)) // "true"
fmt.Println(Equal(a, b)) // "false"
fmt.Println(Equal(a, c)) // "false"
Ćwiczenie 13.1. Zdefiniuj funkcję głębokiego porównywania, która uznaje liczby (dowolnego
typu) za równe, jeżeli różnią się mniej niż jedną częścią na miliard.
Ćwiczenie 13.2. Napisz funkcję, która raportuje, czy jej argument jest cykliczną strukturą danych.
13.4. WYWOŁYWANIE KODU C ZA POMOCĄ NARZĘDZIA CGO 351

13.4. Wywoływanie kodu C za pomocą narzędzia cgo


Program Go może potrzebować użyć sterownika sprzętowego zaimplementowanego w języku C,
kwerendy osadzonej bazy danych zaimplementowanej w języku C++ lub skorzystać z niektórych
procedur algebry liniowej zaimplementowanych w języku Fortran. Język C od dawna jest lingua
franca programowania, więc wiele pakietów przeznaczonych do powszechnego użycia eksportuje
interfejsy API kompatybilne z C, niezależnie od języka ich implementacji.
W tym podrozdziale zbudujemy prosty program do kompresji danych, wykorzystujący narzędzie
cgo, które tworzy dowiązania Go dla funkcji C. Takie narzędzia są nazywane interfejsami funk-
cji obcych (ang. foreign-function interfaces — FFI), a cgo nie jest jedynym tego typu interfejsem
dla programów Go. Kolejnym jest SWIG (swig.org), który zapewnia bardziej złożone funkcje
integracji z klasami C++, ale nie pokażemy go tutaj.
Poddrzewo compress/... standardowej biblioteki zapewnia kompresory i dekompresory dla popu-
larnych algorytmów kompresji, w tym LZW (używanego przez uniksowe polecenie compress)
oraz DEFLATE (używanego przez polecenie gzip GNU). Interfejsy API tych pakietów różnią się
w szczegółach, ale wszystkie zapewniają funkcję opakowującą dla interfejsu io.Writer (która
kompresuje zapisywane w nim dane) oraz funkcję opakowującą dla interfejsu io.Reader (która
kompresuje odczytywane z niego dane), np.:
package gzip // compress/gzip

func NewWriter(w io.Writer) io.WriteCloser


func NewReader(r io.Reader) (io.ReadCloser, error)
Algorytm bzip2, oparty na eleganckiej transformacie Burrowsa-Wheelera, działa wolniej niż gzip,
ale zapewnia o wiele lepszą kompresję. Pakiet compress/bzip2 zapewnia dekompresor dla bzip2,
ale obecnie ten pakiet nie dostarcza kompresora. Budowanie kompresora od podstaw jest zna-
czącym przedsięwzięciem, ale istnieje dobrze udokumentowana implementacja C open source
o wysokiej wydajności, czyli pakiet libbzip2, który można pobrać ze strony bzip.org.
Jeśli biblioteka C byłaby mała, to po prostu podłączylibyśmy ją do czystego Go, a jeśli jej wydajność
nie byłaby kluczowa dla naszych celów, lepiej byłoby wywoływać program C jako podproces pomoc-
niczy przy użyciu pakietu os/exec. Dopiero gdy potrzebujemy użyć złożonej, kluczowej pod
względem wydajności biblioteki z wąskim interfejsem API języka C, sensowne może być opakowanie
jej za pomocą narzędzia cgo. W pozostałej części tego rozdziału zajmiemy się tym przykładem.
Z pakietu libbzip2 języka C potrzebujemy typu struktury bz_stream, który przechowuje bu-
fory wejściowe i wyjściowe, oraz trzech funkcji C: BZ2_bzCompressInit (alokuje bufory strumienia),
BZ2_bzCompress (kompresuje dane z bufora wejściowego do bufora wyjściowego) oraz
BZ2_bzCompressEnd (zwalnia bufory). (Nie przejmuj się mechanizmami pakietu libbzip2. Celem
tego przykładu jest pokazanie, w jaki sposób te części ze sobą współgrają).
Funkcje C BZ2_bzCompessInit i BZ2_bzCompressEnd będziemy wywoływać bezpośrednio z Go,
ale dla BZ2_bzCompress zdefiniujemy funkcję opakowującą w języku C, aby pokazać, jak to się robi.
Poniższy plik źródłowy C jest umieszczony obok kodu Go w naszym pakiecie:
code/r13/bzip
/* Ten plik to code/r13/bzip/bzip2.c, */
/* czyli prosta funkcja opakowująca dla libbzip2 odpowiednia dla narzędzia cgo. */
#include <bzlib.h>
352 ROZDZIAŁ 13. PROGRAMOWANIE NISKIEGO POZIOMU

int bz2compress(bz_stream *s, int action,


char *in, unsigned *inlen, char *out, unsigned *outlen) {
s->next_in = in;
s->avail_in = *inlen;
s->next_out = out;
s->avail_out = *outlen;
int r = BZ2_bzCompress(s, action);
*inlen -= s->avail_in;
*outlen -= s->avail_out;
return r;
}
Teraz wróćmy do kodu Go, którego pierwsza część znajduje się poniżej. Deklaracja import "C"
jest wyjątkowa. Nie ma pakietu C, ale ten import powoduje, że go build wstępnie przetwarza ten
plik za pomocą narzędzia cgo, zanim zobaczy go kompilator Go.
// Package bzip zapewnia interfejs zapisujący, który używa kompresji bzip2 (bzip.org).
package bzip

/*
#cgo CFLAGS: -I/usr/include
#cgo LDFLAGS: -L/usr/lib -lbz2
#include <bzlib.h>
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen);
*/
import "C"
import (
"io"
"unsafe"
)

type writer struct {


w io.Writer // bazowy strumień wyjściowy
stream *C.bz_stream
outbuf [64 * 1024]byte
}

// NewWriter zwraca interfejs zapisujący dla strumieni skompresowanych za pomocą bzip2.


func NewWriter(out io.Writer) io.WriteCloser {
const (
blockSize = 9
verbosity = 0
workFactor = 30
)
w := &writer{w: out, stream: new(C.bz_stream)}
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
return w
}
Podczas wstępnego przetwarzania cgo generuje tymczasowy pakiet, który zawiera deklaracje
Go odpowiadające wszystkim funkcjom i typom C wykorzystywanym przez ten plik, takim jak
C.bz_stream i C.BZ2_bzCompressInit. Narzędzie cgo odkrywa te typy poprzez wywołanie kom-
pilatora C w specjalny sposób na zawartości komentarza poprzedzającego tę deklarację importu.
Ten komentarz może również zawierać dyrektywy #cgo, które określają dodatkowe opcje dla zestawu
narzędzi C. Wartości CFLAGS i LDFLAGS wnoszą dodatkowe argumenty do poleceń kompilatora
i konsolidatora, aby mogły zlokalizować plik nagłówka bzlib.h i bibliotekę archiwum libbz2.a.
13.4. WYWOŁYWANIE KODU C ZA POMOCĄ NARZĘDZIA CGO 353

Ten przykład zakłada, że są one zainstalowane w katalogu /usr w Twoim systemie. Być może
będziesz musiał zmienić lub usunąć te flagi w swojej instalacji.
NewWriter wywołuje funkcję BZ2_bzCompressInit języka C, aby zainicjować bufory dla strumienia.
Typ writer zawiera kolejny bufor, który będzie używany do osuszenia bufora wyjściowego de-
kompresora.
Pokazana poniżej metoda Write dostarcza do dekompresora nieskompresowane dane (data), wy-
wołując funkcję bz2compress w pętli, dopóki wszystkie dane nie zostaną skonsumowane. Należy
zwrócić uwagę, że program Go może uzyskiwać za pomocą notacji C.x dostęp do typów C takich jak
bz_stream, char i uint, funkcji C takich jak bz2compress, a nawet do przypominających obiekty
makr preprocesora C takich jak BZ_RUN. Typ C.uint różni się od typu uint języka Go, nawet jeśli oba
mają taką samą szerokość.
func (w *writer) Write(data []byte) (int, error) {
if w.stream == nil {
panic("zamknięty")
}
var total int // ilość zapisanych nieskompresowanych danych
for len(data) > 0 {
inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
C.bz2compress(w.stream, C.BZ_RUN,
(*C.char)(unsafe.Pointer(&data[0])), &inlen,
(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
total += int(inlen)
data = data[inlen:]
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
return total, err
}
}
return total, nil
}
Każda iteracja pętli przekazuje funkcji bz2compress adres i długość pozostałej części danych (da-
ta) oraz adres i pojemność w.outbuf. Obie zmienne długości są przekazywane przez ich adresy,
a nie wartości, aby funkcja C mogła aktualizować je w celu wskazania, ile nieskompresowanych
danych zostało skonsumowanych i ile skompresowanych danych zostało wyprodukowanych.
Każda porcja skompresowanych danych jest zapisywana do bazowego interfejsu io.Writer.
Metoda Close ma podobną strukturę do metody Write i wykorzystuje pętlę do spłukania wszelkich
pozostałych skompresowanych danych z bufora wyjściowego strumienia.
// Close spłukuje skompresowane dane i zamyka strumień.
// Nie zamyka bazowego io.Writer.
func (w *writer) Close() error {
if w.stream == nil {
panic("zamknięty")
}
defer func() {
C.BZ2_bzCompressEnd(w.stream)
w.stream = nil
}()
for {
inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen,
(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
return err
}
354 ROZDZIAŁ 13. PROGRAMOWANIE NISKIEGO POZIOMU

if r == C.BZ_STREAM_END {
return nil
}
}
}
Po zakończeniu metoda Close wywołuje funkcję C.BZ2_bzCompressEnd, aby zwolnić bufory
strumienia, używając instrukcji defer w celu upewnienia się, że stanie się to na wszystkich ścież-
kach zwracania. Na tym etapie nie jest już bezpieczne wyłuskiwanie wskaźnika w.stream.
Przyjmując strategię obronną, ustawiamy go na wartość nil i dodajemy do każdej metody bezpo-
średnie kontrole nil, żeby program uruchamiał procedurę panic, jeśli użytkownik błędnie wywoła
jakąś metodę po Close.
Nie tylko writer nie jest współbieżnie bezpieczny, ale również współbieżne wywołania metod
Close i Write mogą spowodować, że program ulegnie awarii w kodzie C. Naprawienie tego jest
tematem ćwiczenia 13.3.
Poniższy program bzipper jest poleceniem kompresora bzip2, które korzysta z naszego nowego pa-
kietu. Zachowuje się jak polecenie bzip2 obecne w wielu systemach uniksowych.
code/r13/bzipper
// Bzipper odczytuje dane wejściowe, kompresuje je za pomocą bzip2 i wypisuje je.
package main

import (
"io"
"log"
"os"

"code/r13/bzip"
)

func main() {
w := bzip.NewWriter(os.Stdout)
if _, err := io.Copy(w, os.Stdin); err != nil {
log.Fatalf("bzipper: %v\n", err)
}
if err := w.Close(); err != nil {
log.Fatalf("bzipper: zamykanie: %v\n", err)
}
}
W poniższej sesji używamy polecenia bzipper do skompresowania słownika systemowego
/usr/share/dict/words z 938 848 bajtów do 335 405 bajtów (do ok. jednej trzeciej jego oryginalnego
rozmiaru), a następnie do rozpakowania go za pomocą polecenia systemowego bunzip2. Skrót
SHA256 jest taki sam przed skompresowaniem i po rozpakowaniu, co daje nam pewność, że kom-
presor działa poprawnie. (Jeśli nie masz sha256sum w systemie, użyj swojego rozwiązania ćwi-
czenia 4.2).
$ go build code/r13/bzipper
$ wc -c < /usr/share/dict/words
938848
$ sha256sum < /usr/share/dict/words
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
$ ./bzipper < /usr/share/dict/words | wc -c
335405
$ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
13.5. KOLEJNE SŁOWO OSTRZEŻENIA 355

Powyżej przedstawiliśmy sposób linkowania biblioteki C z programem Go. Idąc w drugą stronę,
można również skompilować program Go jako statyczne archiwum, które można zlinkować z pro-
gramem C, lub jako udostępnianą bibliotekę, która może być dynamicznie ładowana przez pro-
gram C. Prześlizgnęliśmy się jedynie po powierzchni narzędzia cgo i pozostało jeszcze wiele do
powiedzenia na temat zarządzania pamięcią, wskaźników, wywołań zwrotnych, obsługi sygnału,
łańcuchów znaków, errno, finalizatorów oraz relacji między funkcjami goroutine i wątkami syste-
mu operacyjnego, a wiele z tych kwestii jest bardzo subtelnych. Złożone są w szczególności za-
sady prawidłowego przekazywania wskaźników z Go do C lub odwrotnie, z powodów podobnych
do tych, które zostały omówione w podrozdziale 13.2 i nie zostały jeszcze autorytatywnie spre-
cyzowane. Jeśli chcesz poszukać więcej informacji na ten temat, zacznij od lektury dokumentacji
ze strony: https://golang.org/cmd/cgo.
Ćwiczenie 13.3. Wykorzystaj sync.Mutex, aby uczynić bzip2.writer bezpiecznym dla równole-
głego używania przez wiele funkcji goroutine.
Ćwiczenie 13.4. Uzależnienie od bibliotek C ma swoje wady. Zapewnij alternatywną, opierającą się
na czystym Go implementację bzip.NewWriter, która wykorzystuje pakiet os/exec do uru-
chamiania /bin/bzip2 w postaci podprocesu.

13.5. Kolejne słowo ostrzeżenia


Poprzedni rozdział zakończyliśmy ostrzeżeniem o negatywnych stronach interfejsu refleksji.
Ostrzeżenie to z jeszcze większą mocą dotyczy pakietu unsafe opisanego w tym rozdziale.
Języki wysokiego poziomu izolują programy i programistów nie tylko od tajemniczych specyfikacji
zestawów instrukcji poszczególnych komputerów, ale również od uzależnienia od nieistotnych
kwestii, takich jak: lokalizacja zmiennej w pamięci, rozmiar typu danych, szczegóły układu struk-
tury oraz mnóstwo innych szczegółów implementacji. Dzięki tej warstwie izolującej można pisać
programy, które są bezpieczne i niezawodne oraz będą działać w każdym systemie operacyjnym
bez żadnych zmian.
Pakiet unsafe pozwala programistom przebić się przez tę izolację, aby mogli skorzystać z niektó-
rych kluczowych, ale w inny sposób niedostępnych funkcji lub osiągnąć wyższą wydajność. Kosztami
są zwykle przenośność i bezpieczeństwo, więc każdy programista używa pakietu unsafe na
własne ryzyko. Nasze porady dotyczące tego, jak i kiedy używać pakietu unsafe, przypominają
komentarze Donalda Knutha na temat przedwczesnej optymalizacji, które przytoczyliśmy w pod-
rozdziale 11.5. Większość programistów raczej nie będzie nigdy musiała korzystać z pakietu
unsafe. Niemniej jednak czasem wystąpią sytuacje, w których pewne krytyczne kawałki kodu naj-
lepiej jest napisać przy użyciu unsafe. Jeśli wnikliwa analiza i staranne pomiary wskazują, że pakiet
unsafe jest naprawdę najlepszym rozwiązaniem, ogranicz go do możliwie najmniejszego regio-
nu kodu, aby większość programu była obojętna na jego stosowanie.
Na razie postaraj się nie myśleć o dwóch ostatnich rozdziałach tej książki. Napisz kilka znaczą-
cych programów w języku Go. Unikaj pakietów reflect i unsafe. Wróć do tych rozdziałów tylko
wtedy, kiedy będziesz musiał.
Tymczasem życzymy Ci udanego programowania w Go. Mamy nadzieję, że pisanie w języku Go
dostarczy Ci tyle samo radości co nam.
Skorowidz

A C E
adres URL, 30, 32 CSP, communicating sequential encja, 42
zmiennej, 45 processes, 11, 215 enumeracja, 87
akcje, 120 czarna skrzynka, 304 EOF, End-of-file, 136
algorytmy kompresji, 351 czas życia zmiennych, 48 ewaluator, 199
aliasy, 46 czasowniki, verbs, 25 wyrażeń, 197
animowane
figury Lissajous, 37 D F
GIF-y, 28 deklaracja, 42
anonimowe pola, 112 import, 279 FFI, foreign-function interfaces,
anulowanie, 246 package, 279 351
argumenty, 125 deklaracje figury Lissajous, 28
przekazywane przez wartość, funkcji, 125 flagi, 181
126 lokalne, 59 funkcja
wiersza poleceń, 19 metod, 157 Alignof, 344
ASCII, 78 typów, 52 append, 97
asercja typów, 178, 203, 205, 207 zewnętrzne, 59 copy, 98
atak wstrzyknięcia, 122 zmiennych, 21, 44 Display, 324
dekoder strumieniowy, 118 forEachNode, 139
B dekodowanie goroutine, 33
S-wyrażeń, 334 handler, 34
bazowa tablica wycinka, 94 XML, 211 main, 18
benchmarki, 315 deskryptor typów, 183 make, 27
biała skrzynka, 304 detektor wyścigów, 266 new, 48
blok dokumentowanie pakietów, 289 Offsetof, 344
składniowy, syntatic block, 58 domknięcie, 140 populate, 339
uniwersum, universe block, 58 dostęp do znaczników pól
Printf, 25
blokada, 258 struktury, 338
ReadFile, 27
muteksu, 260 drzewo węzłów HTML, 128
sin(r)/r, 70
błąd io.EOF, 136 dynamiczna wartość interfejsu,
Unpack, 339
błędy, 132, 196 183
unsafe.Sizeof, 344
statyczne, 200 dynamiczne rozdzielanie, 184
dynamiczny typ interfejsu, 183
SKOROWIDZ 357

funkcje, 125 J struktur, 110


anonimowe, 139 tablicy, 92
benchmarkujące, 296, 313 jednokierunkowe typy kanałów, urojony, 72
goroutine, 215, 274, 276 227 wycinka, 96
o zmiennej liczbie język Go, 10 złożony, 29
argumentów, 146 JSON, JavaScript Object
Notation, 114
obce, 351 Ł
odroczone, 150
przykładu, 296, 318 K łańcuchy znaków, 75, 82
testujące, 296
wariadyczne, 146
kanały, channels, 222
buforowane, 223, 228
M
niebuforowane, 223 mapa, 24, 27, 102, 326
G synchroniczne, 223 marshaling, 115
klatka animacji, 30 marshalowanie, 329
GC, garbage collector, 347
kodowanie mechanizm odzyskiwania
generator stałych iota, 87
ASCII, 78 pamięci, 9
głęboka równoważność, 348
S-wyrażeń, 329 memoizacja funkcji, 267
Unicode, 78 metaznaki HTML, 123
H UTF-8, 79 metody, 39, 157
hermetyzacja, 169, 277 komentarze, 20, 40 pobierające, getters, 170
HTML, 120 dokumentujące, 55, 289 typu, 53
kompilowanie pakietów, 287 ustawiające, setters, 170
komponowanie typów, 162 z odbiornikiem
I kompozycja, 157, 163 wskaźnikowym, 159
identyfikator kwalifikowany, 56 komunikacja, 222 modułowość, 277
import, 55 nieblokująca, 242 monitor, 259
pusty, 280 procesów sekwencyjnych, monitorująca funkcja goroutine,
ze zmianą nazwy, 280 CSP, 11 257, 272
inicjowanie pakietu, 56 komunikat o błędzie, 196 multimapa, 162
instrukcja konwersja typów, 52 multiplekser żądań, 193
if, 36 konwersje, 85 multipleksowanie, 239
select, 239 krotka, 45 muteksy odczytu/zapisu, 261
instrukcje krótka deklaracja zmiennej, 44
niezadeklarowane, 38 kwalifikacja identyfikatora, 54 N
proste, 21 kwerendowanie zachowań, 207
przypisania, 21 największy wspólny dzielnik, 50
instrumentacja kodu narzędzie
produkcyjnego, 312
L cgo, 351
interfejs, 13, 37, 173, 326 leniwe inicjowanie, 264 dedup, 105
API, 117 dup, 105
liczby
API refleksji, 341 go, 284
całkowite, 63
error, 196 go doc, 289
zespolone, 72
flag.Value, 180 go test, 296
zmiennoprzecinkowe, 68
funkcji obcych, FFI, 351 godoc, 290
liczebność populacji, 57
http.Handler, 191 nazewnictwo, 41, 282
literał
sort.Interface, 187 nazwa pakietu, 55
funkcji, 37, 139
Token, 212 nienazwane typy struktury, 164
łańcuchów znaków, 77
interfejsy jako kontrakty, 173 niezdefiniowane zachowanie, 255
mapy, 102
358 SKOROWIDZ

O planowanie funkcji goroutine, pusty


274 identyfikator, 22
obiekt, 157 plik, file, 54, 174 typ interfejsowy, 178
obrót, 96 pobieranie
obsługa
błędów, 134
pakietów, 286 R
zawartości adresu URL, 30
trawersacji, 142 zawartości adresu URL raport pokrycia, 312
odbiornik równolegle, 32 referencja, 93, 222
metody, 158 podstawialność, 174 do struktury, 27
wskaźnikowy, 159 pokrycie refleksja, 116, 321, 332, 342
odpytywanie instrukcji, 311 rekurencja, 127
kanału, 242 testu, 310 rekurencyjny wyświetlacz
pakietów, 292 pola, 29 wartości, 324
odroczone wywołania, 147 anonimowe, 113 robot internetowy, 235
odzyskiwanie struktury, 338 rozgłaszanie, broadcast, 246
pamięci, 347 polimorfizm rozróżnianie błędów, 205
sprawności, 154 ad hoc, 209
ograniczenie duplikatów, 270 podtypowy, 209 S
OOP, object-oriented porównywanie struktur, 111
programming, 157 potok trzyetapowy, 224 sekcja krytyczna, 259
operacja zamknięcia, 222 potoki, 224 sekwencja zdarzeń, 235
operator procedura panic, 152, 154 sekwencje ucieczki, 25, 77
+, 21 profil selektor, 158
adresu, &, 46 blokowania, 316 semafor zliczający, 237, 258
przypisania, 21, 50, 64 CPU, 316 semaforbinarny, 258
wycinka, 95 sterty, 316 serwer
organizacja obszaru roboczego, profilowanie, 315 czatu, 248
284 program, Patrz narzędzie echo, 220
osadzanie struktur, 112, 162 programowanie WWW, 34
niskiego poziomu, 343 zegara, 217
skrót, 354
P obiektowe, OOP, 157
kryptograficzny, 92
projekt Go, 11
pakiet, package, 18, 39, 54, 277, propagacja błędu, 134 słowa, words, 63
282 przechwytywanie zmiennych słowo kluczowe, 41
reflect, 322 iteracji, 145 sortowanie, 187
unsafe, 355 przedwczesna abstrakcja, 309 szybkie, quicksort, 187
pakiety przedział jednostronnie otwarty, spełnianie warunków interfejsu,
testowe, 306 20 177
wewnętrzne, 291 przełącznik typów, 209 stała, 43, 86
z pojedynczym typem, 283 przepełnienie stosu, 130 stałe nietypowane, 88
pamięć przepełnienie, overflow, 64 stosy o zmiennym rozmiarze,
lokalna wątków, 276 przepływ sterowania, 38 274
podręczna, 267 przesłanianie deklaracji, 59 struktura programu, 41
para klucz-wartość, 24 przestrzeń nazw, 54 struktury, 29, 108, 326
parametr GOMAXPROCS, 275 przypisania, 49, 51 surowy literał łańcucha znaków,
parametry funkcji, 125 przypisanie krotki, 45, 50 78
parsowanie flag, 180 pusta struktura, 110 S-wyrażenia, 329, 334
pisanie testów, 308 puste importy, 280 sygnatura, 126
SKOROWIDZ 359

symbol $, 18 pierwszoklasowe, 137 nieblokująca pamięć


synchronizacja podstawowe, 63 podręczna, 267
funkcji goroutine, 223 referencyjne, 63 trawersacja katalogów, 242
pamięci, 262 wektora bitowego, 166 współbieżność, 253
sytuacja wyścigu, race condition, złożone, 63, 91 współbieżny
36, 253 robot internetowy, 235
szablony tekstowe, 120 U serwer echo, 220
serwer zegara, 217
układy współrzędnych, 71
Ś ukrywanie informacji, 169
współdzielenie zmiennych, 253
wyciek funkcji goroutine, 230,
ścieżka importu, 55, 278 unia, 209 241
ślad stosu, 152 Unicode, 78 wycinek, slice, 20, 94, 326
środowisko REPL, 14 unie rozróżnialne, 209 wycinki bajtów, 82
unikanie kruchych testów, 310 wyjątek, 133
T unmarshaling, 117 wykrywanie zmian, 310
ustawianie zmiennych, 332 wymagania behawioralne, 186
tabela HTML, 123 UTF-8, 79 wyrażenia metod, 165
tablice, 91, 326 wyszukiwanie zduplikowanych
techniki in situ wycinka, 100
test
V linii, 23
wyścig, 36, 253
białej skrzynki, 304 vendorowanie kodu, 287 danych, 255
integracyjny, 307 wyświetlacz wartości, 324
funkcjonalny, 304 W wyświetlanie metod typu, 340
kruchy, 310 wywołanie
oparty na tablicach, 300 wartość
kodu C, 351
strukturalny, 304 adresowalna, 46
odroczone funkcji, 147
szklanej skrzynki, 304 dynamiczna, 322
wzajemne wykluczanie, mutual
testowanie, 295 funkcji, 137
exclusion, 254
polecenia, 301 interfejsu, 183–185
sync.mutex, 258
zrandomizowane, 300 logiczna, 75
token, 211 metody, 165
tożsamość referencji, 96 nil, 183 Z
trawersacja, 142 nil interfejsu, 185 zakleszczenie, 236
katalogów, 242 odbiornika, 161 zakres, scope, 22, 58
typ paniki, 152 zamykanie szeregowe, serial
T, 203 zerowa, 43 confinement, 257
unsafe.Pointer, 346 warunek końca pliku, EOF, 136 zapętlenie równoległe, 231
typy warunki interfejsu, 177 zbiór Mandelbrota, 74
abstrakcyjne, 173 wątki, 274 zdarzenia, 224
bazowe, 52 wektor bitowy, 166 zewnętrzny pakiet testowy, 279,
danych, 63 węzły elementów, 127 306
dynamiczne, 322 wielowątkowość pamięci zmienna, 43
interfejsowe, 63, 173, 176 współdzielonej, 215 środowiskowa GOARCH,
kanałów, 222 wskaźnik, 39, 45, 326 288
kanałów jednokierunkowe, do typu nazwanego, 163 środowiskowa GOOS, 288
227 nil, 161 środowiskowa GOPATH,
konkretne, 173 współbieżna 284
nazwane, 39, 52
360 SKOROWIDZ

zmienne znak Ż
liczby argumentów, 100 lewego ukośnika, 77
nienazwane, 48 ucieczki ósemkowy, 77 żeton, 258
typu T, 48 ucieczki szesnastkowy, 77
znaczniki zastępczy Unicode, 81
kompilacji, 289 zwracanie
pól, 115, 116 nagie, 132
pól struktury, 338 wielu wartości, 130

You might also like