Professional Documents
Culture Documents
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
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.
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.
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.
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
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).
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)
}
}
}
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
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"
)
const (
whiteIndex = 0 // pierwszy kolor w zmiennej palette
blackIndex = 1 // następny kolor w zmiennej palette
)
func main() {
lissajous(os.Stdout)
}
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.
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">
Ć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.
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())
}
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).
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))
}
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))
}
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
Ć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.
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
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
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()
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"
)
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 " ")
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
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
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.
import "fmt"
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
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
import "fmt"
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
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
func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}
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
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
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.
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
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
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'"
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
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.
import (
"image"
"image/color"
"image/png"
"math/cmplx"
"os"
)
func main() {
const (
xmin, ymin, xmax, ymax = -2, -2, +2, +2
width, height = 1024, 1024
)
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
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
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.
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
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)) // "�"
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
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.
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
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
)
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.
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
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]
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
)
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ń"}
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
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.
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.
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.
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.
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 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
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)
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)
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
}
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
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…
}
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}
}
p := Point{1, 2}
q := Point{2, 1}
112 ROZDZIAŁ 4. TYPY ZŁOŻONE
hits := make(map[address]int)
hits[address{"golang.org", 443}]++
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
}
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
}
[{"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
import "time"
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ń.
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"
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 "<" 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.
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.
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 }
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
const (
ErrorNode NodeType = iota
TextNode
DocumentNode
ElementNode
CommentNode
DoctypeNode
)
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)
}
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.
}
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))
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
}
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
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)
}
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.
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 }
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
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").
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",
},
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"
)
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
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.
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)
}
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
_ = 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
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.
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.
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ę.
// 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 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.
// 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 ""
}
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.
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)
}
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.
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} }
s.words = append(s.words, 0)
}
s.words[word] |= 1 << bit
}
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}"
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
/* ... */
}
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.
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
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
Ć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
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
}
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.
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
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.
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).
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).
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).
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.
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.
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
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
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
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))
}
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
import "errors"
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.
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
}
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"
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.
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…
}
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
}
import (
"errors"
"syscall"
)
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
"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.
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)
}
}
}
}
Ć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"
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.
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)
}
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
}
}
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)
}
$ 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
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
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
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.
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.
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
}
}()
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)
}()
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.
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ć.
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ź
}
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
// 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
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
}
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)
}()
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ł.
Ć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ć?
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)
func main() {
worklist := make(chan []string)
var n int // liczba oczekujących operacji wysłania do worklist
// 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
func main() {
// …tworzenie kanału abort…
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.
func main() {
// …uruchamianie funkcji goroutine działającej w tle…
Ć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{})
// …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.
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
}
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()
}
Ć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.
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
// 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
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)
// 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
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 }
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.
var (
mu sync.Mutex // strzeże zmiennej balance
balance int
)
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
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
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
// 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()
// 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).
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
}
// 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
}
// 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
}
Ć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.
Ś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?
Ć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?
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.
"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
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.
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)
}
}
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 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
)
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
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
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"
...
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.
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
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
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.
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
"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
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.
// 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"
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.
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")
)
func main() {
flag.Parse()
if err := echo(!*n, *s, flag.Args()); err != nil {
fmt.Fprintf(os.Stderr, "echo: %v\n", err)
os.Exit(1)
}
}
import (
"bytes"
"fmt"
"testing"
)
import (
"fmt"
"log"
"net/smtp"
)
import (
"strings"
"testing"
)
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 }()
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.
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.
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
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.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"},
}
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.
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
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.
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
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.
return formatAtom(reflect.ValueOf(value))
}
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
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
case reflect.String:
fmt.Fprintf(buf, "%q", v.String())
case reflect.Ptr:
return encode(buf, v.Elem())
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).
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
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))
}
default:
panic(fmt.Sprintf("nie można zdekodować listy na %v", v.Type()))
}
}
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).
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)
}
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).
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)
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
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.
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
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
// 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
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
}
switch x.Kind() {
case reflect.Bool:
return x.Bool() == y.Bool()
case reflect.String:
return x.String() == y.String()
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
/*
#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"
)
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.
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
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