Darmowy fragment publikacji:
• Kup książkę
• Poleć książkę
• Oceń książkę
• Księgarnia internetowa
• Lubię to! » Nasza społeczność
Spis tre(cid:264)ci
Przedmowa ................................................................................................ 9
Pochodzenie języka Go ....................................................................................................................10
Projekt Go ..........................................................................................................................................11
Struktura książki ...............................................................................................................................13
Gdzie można znaleźć więcej informacji .........................................................................................14
Podziękowania ..................................................................................................................................15
Rozdzia(cid:228) 1. Przewodnik ............................................................................. 17
1.1. Witaj, świecie ..............................................................................................................................17
1.2. Argumenty wiersza poleceń .....................................................................................................19
1.3. Wyszukiwanie zduplikowanych linii ......................................................................................23
1.4. Animowane GIF-y .....................................................................................................................28
1.5. Pobieranie zawartości adresu URL .........................................................................................30
1.6. Pobieranie zawartości kilku adresów URL równolegle ........................................................32
1.7. Serwer WWW ............................................................................................................................33
1.8. Kilka pominiętych kwestii ........................................................................................................37
Rozdzia(cid:228) 2. Struktura programu ................................................................ 41
2.1. Nazwy ..........................................................................................................................................41
2.2. Deklaracje ...................................................................................................................................42
2.3. Zmienne ......................................................................................................................................43
2.4. Przypisania .................................................................................................................................50
2.5. Deklaracje typów .......................................................................................................................52
2.6. Pakiety i pliki ..............................................................................................................................54
2.7. Zakres ..........................................................................................................................................58
Poleć książkęKup książkę6
SPIS TRE(cid:263)CI
Rozdzia(cid:228) 3. Podstawowe typy danych ........................................................ 63
3.1. Liczby całkowite .........................................................................................................................63
3.2. Liczby zmiennoprzecinkowe ....................................................................................................68
3.3. Liczby zespolone ........................................................................................................................72
3.4. Wartości logiczne ......................................................................................................................75
3.5. Łańcuchy znaków ......................................................................................................................75
3.6. Stałe ..............................................................................................................................................86
Rozdzia(cid:228) 4. Typy z(cid:228)o(cid:276)one ........................................................................... 91
4.1. Tablice .........................................................................................................................................91
4.2. Wycinki .......................................................................................................................................94
4.3. Mapy ......................................................................................................................................... 102
4.4. Struktury .................................................................................................................................. 108
4.5. JSON ......................................................................................................................................... 114
4.6. Szablony tekstowe i HTML ................................................................................................... 120
Rozdzia(cid:228) 5. Funkcje ................................................................................ 125
5.1. Deklaracje funkcji ................................................................................................................... 125
5.2. Rekurencja ............................................................................................................................... 127
5.3. Zwracanie wielu wartości ...................................................................................................... 130
5.4. Błędy ......................................................................................................................................... 132
5.5. Wartości funkcji ...................................................................................................................... 137
5.6. Funkcje anonimowe ............................................................................................................... 139
5.7. Funkcje o zmiennej liczbie argumentów ............................................................................. 146
5.8. Odroczone wywołania funkcji .............................................................................................. 147
5.9. Procedura panic ...................................................................................................................... 152
5.10. Odzyskiwanie sprawności ................................................................................................... 154
Rozdzia(cid:228) 6. Metody ................................................................................ 157
6.1. Deklaracje metod .................................................................................................................... 157
6.2. Metody z odbiornikiem wskaźnikowym ............................................................................. 159
6.3. Komponowanie typów poprzez osadzanie struktur .......................................................... 162
6.4. Wartości i wyrażenia metod .................................................................................................. 165
6.5. Przykład: typ wektora bitowego ........................................................................................... 166
6.6. Hermetyzacja ........................................................................................................................... 169
Rozdzia(cid:228) 7. Interfejsy .............................................................................. 173
7.1. Interfejsy jako kontrakty ........................................................................................................ 173
7.2. Typy interfejsowe ................................................................................................................... 176
Poleć książkęKup książkęSPIS TRE(cid:263)CI
7
7.3. Spełnianie warunków interfejsu ........................................................................................... 177
7.4. Parsowanie flag za pomocą interfejsu flag.Value ............................................................... 180
7.5. Wartości interfejsów .............................................................................................................. 182
7.6. Sortowanie za pomocą interfejsu sort.Interface ................................................................. 187
7.7. Interfejs http.Handler ............................................................................................................ 191
7.8. Interfejs error .......................................................................................................................... 196
7.9. Przykład: ewaluator wyrażeń ................................................................................................ 197
7.10. Asercje typów ........................................................................................................................ 203
7.11. Rozróżnianie błędów za pomocą asercji typów ................................................................ 205
7.12. Kwerendowanie zachowań za pomocą interfejsowych asercji typów ........................... 207
7.13. Przełączniki typów ............................................................................................................... 209
7.14. Przykład: dekodowanie XML oparte na tokenach ........................................................... 211
7.15. Kilka porad ............................................................................................................................ 214
Rozdzia(cid:228) 8. Funkcje goroutine i kana(cid:228)y .................................................... 215
8.1. Funkcje goroutine ................................................................................................................... 215
8.2. Przykład: współbieżny serwer zegara ................................................................................... 217
8.3. Przykład: współbieżny serwer echo ...................................................................................... 220
8.4. Kanały ....................................................................................................................................... 222
8.5. Zapętlenie równoległe ............................................................................................................ 231
8.6. Przykład: współbieżny robot internetowy ........................................................................... 235
8.7. Multipleksowanie za pomocą instrukcji select ................................................................... 239
8.8. Przykład: współbieżna trawersacja katalogów .................................................................... 242
8.9. Anulowanie ............................................................................................................................. 246
8.10. Przykład: serwer czatu ......................................................................................................... 248
Rozdzia(cid:228) 9. Wspó(cid:228)bie(cid:276)no(cid:264)(cid:232) ze wspó(cid:228)dzieleniem zmiennych ....................... 253
9.1. Sytuacje wyścigu ..................................................................................................................... 253
9.2. Wzajemne wykluczanie: sync.mutex ................................................................................... 258
9.3. Muteksy odczytu/zapisu: sync.RWMutex ........................................................................... 261
9.4. Synchronizacja pamięci ......................................................................................................... 262
9.5. Leniwe inicjowanie: sync.Once ............................................................................................. 264
9.6. Detektor wyścigów ................................................................................................................. 266
9.7. Przykład: współbieżna nieblokująca pamięć podręczna ................................................... 267
9.8. Funkcje goroutine i wątki ...................................................................................................... 274
Poleć książkęKup książkę8
SPIS TRE(cid:263)CI
Rozdzia(cid:228) 10. Pakiety i narz(cid:246)dzie go ......................................................... 277
10.1. Wprowadzenie ...................................................................................................................... 277
10.2. Ścieżki importów .................................................................................................................. 278
10.3. Deklaracja package ............................................................................................................... 279
10.4. Deklaracje import ................................................................................................................. 279
10.5. Puste importy ........................................................................................................................ 280
10.6. Pakiety i nazewnictwo .......................................................................................................... 282
10.7. Narzędzie go .......................................................................................................................... 284
Rozdzia(cid:228) 11. Testowanie ......................................................................... 295
11.1. Narzędzie go test ................................................................................................................... 296
11.2. Funkcje testujące ................................................................................................................... 296
11.3. Pokrycie ................................................................................................................................. 310
11.4. Funkcje benchmarkujące ..................................................................................................... 313
11.5. Profilowanie .......................................................................................................................... 315
11.6. Funkcje przykładów ............................................................................................................. 318
Rozdzia(cid:228) 12. Refleksja ............................................................................ 321
12.1. Dlaczego refleksja? ............................................................................................................... 321
12.2. reflect.Type i reflect.Value ................................................................................................... 322
12.3. Display — rekurencyjny wyświetlacz wartości ................................................................. 324
12.4. Przykład: kodowanie S-wyrażeń ......................................................................................... 329
12.5. Ustawianie zmiennych za pomocą reflect.Value .............................................................. 332
12.6. Przykład: dekodowanie S-wyrażeń .................................................................................... 334
12.7. Uzyskiwanie dostępu do znaczników pól struktury ........................................................ 337
12.8. Wyświetlanie metod typu .................................................................................................... 340
12.9. Słowo ostrzeżenia ................................................................................................................. 341
Rozdzia(cid:228) 13. Programowanie niskiego poziomu ........................................ 343
13.1. Funkcje unsafe.Sizeof, Alignof i Offsetof .......................................................................... 344
13.2. Typ unsafe.Pointer ............................................................................................................... 346
13.3. Przykład: głęboka równoważność ...................................................................................... 348
13.4. Wywoływanie kodu C za pomocą narzędzia cgo ............................................................. 351
13.5. Kolejne słowo ostrzeżenia ................................................................................................... 355
Skorowidz .............................................................................................. 356
Poleć książkęKup książkęRozdzia(cid:228) 7
Interfejsy
Typy interfejsowe wyrażają uogólnienia lub abstrakcje dotyczące zachowań innych typów. Dzięki
uogólnianiu interfejsy pozwalają pisać funkcje, które są bardziej elastyczne i adaptowalne, ponieważ
nie są związane ze szczegółami jednej konkretnej implementacji.
Wiele języków obiektowych ma jakąś koncepcję interfejsów, ale interfejsy języka Go wyróżnia to, że
ich warunki są spełniane pośrednio. Innymi słowy: nie ma potrzeby deklarowania wszystkich in-
terfejsów, których warunki spełnia konkretny typ. Wystarczy po prostu posiadanie niezbędnych
metod. Taka konstrukcja pozwala na tworzenie nowych interfejsów, których warunki są spełniane
przez istniejące konkretne typy bez ich zmieniania, co jest szczególnie przydatne dla typów zde-
finiowanych w niekontrolowanych przez Ciebie pakietach.
Ten rozdział rozpoczniemy od przyjrzenia się podstawowym mechanizmom typów interfejso-
wych i ich wartościom. Potem przestudiujemy kilka ważnych interfejsów ze standardowej bi-
blioteki — wielu programistów języka Go korzysta ze standardowych interfejsów w równym stopniu
jak ze swoich własnych. Na koniec przyjrzymy się asercjom typów (zob. podrozdział 7.10) oraz
przełącznikom typów (zob. podrozdział 7.13) i zobaczymy, w jaki sposób umożliwiają stoso-
wanie innego rodzaju ogólności.
7.1. Interfejsy jako kontrakty
Wszystkie omawiane do tej pory typy były typami konkretnymi. Typ konkretny określa dokładną
reprezentację swoich wartości i udostępnia wewnętrzne operacje tej reprezentacji, takie jak
arytmetyka dla liczb albo indeksowanie, append i range dla wycinków. Konkretny typ może
również zapewniać dodatkowe zachowania poprzez swoje metody. Gdy masz wartość konkretnego
typu, wiesz dokładnie, czym ona jest i co możesz z nią zrobić.
W języku Go istnieje jeszcze inny rodzaj typu, zwany typem interfejsowym. Interfejs jest typem
abstrakcyjnym. Nie udostępnia reprezentacji czy wewnętrznej struktury swoich wartości ani też
zbioru podstawowych obsługiwanych operacji. Ujawnia tylko niektóre swoje metody. Gdy masz
wartość typu interfejsowego, nie wiesz nic na temat tego, czym ona jest. Wiesz tylko, co może robić,
lub, mówiąc bardziej precyzyjnie, jakie zachowania są dostarczane przez jej metody.
Dotychczas używaliśmy dwóch podobnych funkcji do formatowania łańcuchów znaków: fmt.Printf,
która zapisuje wynik do standardowego strumienia wyjściowego (pliku), oraz fmt.Sprintf, która
zwraca wynik jako string. Byłoby niedobrze, gdyby ta trudna część, jaką jest formatowanie wyniku,
Poleć książkęKup książkę174
ROZDZIA(cid:227) 7. INTERFEJSY
musiała być duplikowana ze względu na te drobne różnice w sposobie wykorzystywania wyniku.
Dzięki interfejsom tak nie jest. Obie te funkcje są w istocie funkcjami opakowującymi trzecią funkcję,
fmt.Fprintf, która jest neutralna w kwestii tego, co się dzieje z obliczanym przez nią wynikiem:
package fmt
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
func Printf(format string, args ...interface{}) (int, error) {
return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string {
var buf bytes.Buffer
Fprintf( buf, format, args...)
return buf.String()
}
Prefiks F w nazwie Fprintf oznacza plik (ang. file) i wskazuje, że sformatowane dane wyjściowe
powinny zostać zapisane w pliku dostarczonym jako pierwszy argument. W przypadku Printf
tym argumentem (os.Stdout) jest *os.File. Jednak w przypadku Sprintf tym argumentem nie
jest plik, chociaż pozornie go przypomina: buf jest wskaźnikiem do bufora pamięci, w którym
mogą być zapisywane bajty.
Pierwszym parametrem funkcji Fprintf również nie jest plik. Jest nim io.Writer, czyli typ interfej-
sowy z następującą deklaracją:
package io
// Writer jest interfejsem, który opakowuje podstawow(cid:261) metod(cid:266) Write.
type Writer interface {
// Write zapisuje len(p) bajtów ze zmiennej p do bazowego strumienia danych.
// Zwraca liczb(cid:266) bajtów zapisanych z p (0 = n = len(p))
// oraz ka(cid:298)dy napotkany b(cid:225)(cid:261)d, który spowodowa(cid:225) przedwczesne zatrzymanie zapisywania.
// Metoda Write musi zwraca(cid:252) b(cid:225)(cid:261)d nieb(cid:266)d(cid:261)cy nil, je(cid:286)li zwraca n len(p).
// Metoda Write nie mo(cid:298)e modyfikowa(cid:252) danych wycinka, nawet tymczasowo.
//
// Implementacje nie mog(cid:261) przechowywa(cid:252) w pami(cid:266)ci zmiennej p.
Write(p []byte) (n int, err error)
}
Interfejs io.Writer definiuje kontrakt między funkcją Fprintf a wywołującymi ją podmiotami.
Z jednej strony, kontrakt wymaga, aby podmiot wywołujący zapewnił wartość konkretnego typu,
takiego jak *os.File lub *bytes.Buffer, który ma metodę o nazwie Write z odpowiednimi sy-
gnaturą i zachowaniem. Z drugiej strony, kontrakt gwarantuje, że funkcja Fprintf będzie wyko-
nywać swoje zadania, mając daną dowolną wartość, która spełnia warunki interfejsu io.Writer.
Funkcja Fprintf nie może zakładać, że zapisuje w pliku lub pamięci, tylko że może wywołać
metodę Write.
Ponieważ funkcja fmt.Fprintf nie zakłada niczego w kwestii reprezentacji wartości i opiera się
tylko na zachowaniach gwarantowanych przez kontrakt io.Writer, możemy do tej funkcji bez-
piecznie przekazywać jako pierwszy argument wartość dowolnego typu konkretnego, który spełnia
warunki interfejsu io.Writer. Ta swoboda zastępowania jednego typu innym typem, który
spełnia warunki tego samego interfejsu, nazywa się podstawialnością i jest charakterystyczną
cechą programowania obiektowego.
Poleć książkęKup książkę7.1. INTERFEJSY JAKO KONTRAKTY
175
Sprawdźmy to przy użyciu nowego typu. Przedstawiona poniżej metoda Write typu *ByteCounter
jedynie zlicza zapisane w nim bajty przed ich porzuceniem. (Wymagana jest konwersja, aby za-
pewnić dopasowanie typów dla len(p) i *c w instrukcji przypisania +=).
code/r07/bytecounter
type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
*c += ByteCounter(len(p)) // konwersja int na ByteCounter
return len(p), nil
}
Ponieważ *ByteCounter spełnia warunki kontraktu io.Writer, możemy przekazać go do funkcji
Fprintf, która przeprowadza formatowanie swojego łańcucha znaków bez świadomości tej zmiany.
ByteCounter poprawnie akumuluje długość wyniku.
var c ByteCounter
c.Write([]byte( witaj ))
fmt.Println(c) // 5 , = len( witaj )
c = 0 // resetowanie licznika
var name = Marta
fmt.Fprintf( c, witaj, s , name)
fmt.Println(c) // 12 , = len( witaj, Marta )
Poza io.Writer istnieje jeszcze inny bardzo ważny interfejs dla pakietu fmt. Funkcje Fprintf
i Fprintln zapewniają typom możliwość kontrolowania sposobu, w jaki wyświetlane są ich wartości.
W podrozdziale 2.5 zdefiniowaliśmy metodę String dla typu Celsius, aby temperatury były wy-
świetlane jako 100 °C , a w podrozdziale 6.5 wyposażyliśmy typ *IntSet w metodę String,
aby zbiory były przedstawiane z wykorzystaniem tradycyjnej notacji, np. {1 2 3} . Zadeklaro-
wanie metody String sprawia, że dany typ spełnia warunki jednego z najszerzej stosowanych in-
terfejsów, czyli interfejsu fmt.Stringer:
package fmt
// String jest wykorzystywana do wy(cid:286)wietlania warto(cid:286)ci przekazywanych
// jako operand do dowolnego formatu, który akceptuje (cid:225)a(cid:276)cuch znaków,
// lub do funkcji wypisywania bez ustawionego formatu, takiej jak Print.
type Stringer interface {
String() string
}
W podrozdziale 7.10 wyjaśnimy, w jaki sposób pakiet fmt wykrywa, które wartości spełniają warunki
tego interfejsu.
Ćwiczenie 7.1. Wykorzystując pomysły z ByteCounter, zaimplementuj liczniki dla słów i linii. Przy-
dać Ci się może funkcja bufio.ScanWords.
Ćwiczenie 7.2. Napisz funkcję CountingWriter (z przedstawioną poniżej sygnaturą), która mając
dany interfejs io.Writer, zwraca nowy Writer, opakowujący oryginalny, oraz wskaźnik do zmiennej
int64, która w każdym momencie zawiera liczbę bajtów aktualnie zapisanych do nowego interfejsu
Writer.
func CountingWriter(w io.Writer) (io.Writer, *int64)
Ćwiczenie 7.3. Napisz metodę String dla typu *tree z programu code/r04/treesort (zob.
podrozdział 4.4), która ujawnia sekwencję wartości w drzewie.
Poleć książkęKup książkę176
ROZDZIA(cid:227) 7. INTERFEJSY
7.2. Typy interfejsowe
Typ interfejsowy określa zestaw metod, które musi posiadać typ konkretny, aby został uznany
za instancję danego interfejsu.
Typ io.Writer jest jednym z najszerzej stosowanych interfejsów, ponieważ zapewnia abstrakcję
wszystkich typów, do których mogą być zapisywane bajty, co obejmuje: pliki, bufory pamięci,
połączenia sieciowe, klienty HTTP, programy archiwizujące, programy szyfrujące itd. Wiele innych
użytecznych interfejsów definiuje pakiet io. Interfejs Reader reprezentuje dowolny typ, z którego
można odczytywać bajty, a Closer jest dowolną wartością, którą można zamknąć, taką jak plik
lub połączenie sieciowe. (Prawdopodobnie dostrzegłeś już konwencję nazewnictwa stosowaną
dla wielu posiadających jedną metodę interfejsów języka Go).
package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
Jeśli poszukamy dalej, znajdziemy deklaracje nowych typów interfejsów jako kombinacje istnieją-
cych. Oto dwa przykłady:
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Użyta powyżej składnia, przypominająca osadzanie struktur, pozwala nam określać kolejny inter-
fejs jako skrót służący do wypisywania wszystkich jego metod. Nazywa się to osadzaniem inter-
fejsu. Moglibyśmy zapisać interfejs io.ReadWriter bez osadzania (chociaż mniej zwięźle) w nastę-
pujący sposób:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
Moglibyśmy nawet pomieszać te dwa style:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
Wszystkie trzy deklaracje mają ten sam efekt. Kolejność, w jakiej pojawiają się metody, jest nie-
istotna. Znaczenie ma jedynie zestaw metod.
Poleć książkęKup książkę7.3. SPE(cid:227)NIANIE WARUNKÓW INTERFEJSU
177
Ćwiczenie 7.4. Funkcja strings.NewReader zwraca wartość, która spełnia warunki interfejsu
io.Reader (i innych), odczytując z jego argumentu, czyli łańcucha znaków. Zaimplementuj prostą
wersję funkcji NewReader i użyj jej, aby umożliwić przyjmowanie przez parser HTML (zob. podroz-
dział 5.2) danych wejściowych z łańcucha znaków.
Ćwiczenie 7.5. Funkcja LimitReader z pakietu io akceptuje argument r interfejsu io.Reader
oraz liczbę bajtów n, a następnie zwraca kolejny Reader, który odczytuje z argumentu r, ale zgłasza stan
końca pliku po n bajtach. Zaimplementuj ją.
func LimitReader(r io.Reader, n int64) io.Reader
7.3. Spe(cid:228)nianie warunków interfejsu
Typ spełnia warunki interfejsu, jeśli ma wszystkie metody, których wymaga dany interfejs. Przy-
kładowo: typ *os.File spełnia warunki interfejsów io.Reader, Writer, Closer i ReadWriter.
Typ *bytes.Buffer spełnia warunki interfejsów Reader, Write i ReadWriter, ale nie spełnia wa-
runków interfejsu Closer, ponieważ nie ma metody Close. Programiści języka Go często stosują
skrót myślowy, mówiąc, że konkretny typ „jest” określonym typem interfejsowym, co oznacza, że
spełnia warunki tego interfejsu, np. *bytes.Buffer „jest” typem io.Writer, a *os.File „jest” typem
io.ReadWriter.
Reguła przypisywalności (zob. punkt 2.4.2) dla interfejsów jest bardzo prosta: wyrażenie może być
przypisane do interfejsu tylko wtedy, gdy spełnia warunki tego interfejsu. Więc:
var w io.Writer
w = os.Stdout // OK: *os.File ma metod(cid:266) Write
w = new(bytes.Buffer) // OK: *bytes.Buffer ma metod(cid:266) Write
w = time.Second // b(cid:225)(cid:261)d kompilacji: time.Duration nie ma metody Write
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File ma metody Read, Write i Close
rwc = new(bytes.Buffer) // b(cid:225)(cid:261)d kompilacji: *bytes.Buffer nie ma metody Close
Ta reguła ma zastosowanie nawet wtedy, gdy prawa strona sama jest interfejsem:
w = rwc // OK: io.ReadWriteCloser ma metod(cid:266) Write
rwc = w // b(cid:225)(cid:261)d kompilacji: io.Writer nie ma metody Close
Ponieważ ReadWriter i ReadWriteCloser obejmują wszystkie metody interfejsu Writer, każdy
typ spełniający warunki interfejsu ReadWriter lub ReadWriteCloser siłą rzeczy spełnia warunki
interfejsu Writer.
Zanim przejdziemy dalej, powinniśmy wyjaśnić kwestię tego, co znaczy, że jakiś typ ma metodę.
Przypomnijmy z punktu 6.2, że dla każdego nazwanego typu konkretnego T niektóre jego metody
same posiadają odbiornik typu T, podczas gdy inne wymagają wskaźnika *T. Przypomnijmy rów-
nież, że prawidłowe jest wywołanie metody *T na argumencie typu T, pod warunkiem że ten ar-
gument jest zmienną. Kompilator pośrednio pobiera jej adres. Ale to jest zwykły lukier skła-
dniowy: wartość typu T nie posiada wszystkich metod, które ma wskaźnik *T, w wyniku czego
może spełniać wymagania mniejszej liczby interfejsów.
Wyjaśnimy to na przykładzie. Metoda String typu IntSet z podrozdziału 6.5 wymaga odbiornika
wskaźnikowego, więc nie możemy wywołać tej metody na nieadresowalnej wartości IntSet:
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // b(cid:225)(cid:261)d kompilacji: String wymaga odbiornika *IntSet
Poleć książkęKup książkę178
ROZDZIA(cid:227) 7. INTERFEJSY
Możemy jednak wywołać ją na zmiennej IntSet:
var s IntSet
var _ = s.String() // OK: s jest zmienn(cid:261), a s ma metod(cid:266) String
Ponieważ jednak tylko *IntSet ma metodę String, tylko *IntSet spełnia warunki interfejsu
fmt.Stringer:
var _ fmt.Stringer = s // OK
var _ fmt.Stringer = s // b(cid:225)(cid:261)d kompilacji: IntSet nie ma metody String
Podrozdział 12.8 zawiera program wyświetlający metody dowolnej wartości, a narzędzie godoc
-analysis=type (zob. punkt 10.7.4) wyświetla metody każdego typu i relacje pomiędzy interfejsami
i typami konkretnymi.
Tak jak koperta opakowuje i ukrywa przechowywany list, tak interfejs opakowuje i ukrywa typ
konkretny i przechowywaną przez niego wartość. Wywoływane mogą być tylko metody ujawnione
przez typ interfejsowy, nawet jeśli dany typ konkretny ma jeszcze inne:
os.Stdout.Write([]byte( witaj )) // OK: *os.File ma metod(cid:266) Write
os.Stdout.Close() // OK: *os.File ma metod(cid:266) Close
var w io.Writer
w = os.Stdout
w.Write([]byte( witaj )) // OK: io.Writer ma metod(cid:266) Write
w.Close() // b(cid:225)(cid:261)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
Poleć książkęKup książkę7.3. SPE(cid:227)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(cid:225)nia(cid:252) 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(cid:225)nia(cid:252) warunki interfejsu io.Writer.
var _ io.Writer = (*bytes.Buffer)(nil)
Warunki niepustych typów interfejsowych, takich jak io.Writer, są najczęściej spełniane przez
typ wskaźnika, w szczególności gdy jedna z metod tego interfejsu lub kilka z nich zakłada pewien
rodzaj mutacji odbiornika, tak jak metoda Write. Wskaźnik do struktury jest szczególnie popu-
larnym typem przenoszącym metody.
Jednak typy wskaźników wcale nie są jedynymi typami, które spełniają warunki interfejsów, i nawet
warunki interfejsów z metodami modyfikującymi mogą być spełnione przez jeden z innych typów
referencyjnych języka Go. Widzieliśmy przykłady typów wycinka z metodami (geometry.Path,
podrozdział 6.1) i typów map z metodami (url.Values, punkt 6.2.1), a później zobaczymy typ
funkcji z metodami (http.HandlerFunc, podrozdział 7.7). Nawet podstawowe typy mogą
spełniać warunki interfejsów. Jak zobaczymy w podrozdziale 7.4, typ time.Duration spełnia
warunki interfejsu fmt.Stringer.
Typ konkretny może spełniać warunki wielu niepowiązanych interfejsów. Rozważmy program,
który organizuje lub sprzedaje cyfrowe artefakty kulturowe, takie jak muzyka, filmy i książki.
Może on definiować następujący zestaw typów konkretnych:
Album
Book
Movie
Magazine
Podcast
TVEpisode
Track
Każdą interesującą nas abstrakcję możemy wyrazić jako interfejs. Niektóre właściwości są wspólne
dla wszystkich artefaktów, np.: tytuł, data utworzenia oraz lista twórców (autorów lub artystów).
type Artifact interface {
Title() string
Creators() []string
Created() time.Time
}
Inne właściwości są ograniczone do określonych typów artefaktów. Właściwości słowa drukowanego
dotyczą tylko książek i czasopism, podczas gdy rozdzielczość mają tylko filmy i seriale telewizyjne.
type Text interface {
Pages() int
Words() int
PageSize() int
}
type Audio interface {
Stream() (io.ReadCloser, error)
Poleć książkęKup książkę180
ROZDZIA(cid:227) 7. INTERFEJSY
RunningTime() time.Duration
Format() string // np. MP3 , WAV
}
type Video interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // np. MP4 , WMV
Resolution() (x, y int)
}
Interfejsy są tylko jednym z wielu sposobów grupowania powiązanych typów konkretnych i wyra-
żania ich wspólnych aspektów. Inne sposoby grupowania poznamy później. Jeśli okazałoby się
np., że potrzebujemy obsługiwać elementy Audio i Video w taki sam sposób, moglibyśmy zdefinio-
wać interfejs Streamer służący do reprezentowania ich wspólnych aspektów bez zmiany istniejących
deklaracji typów.
type Streamer interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string
}
Każde grupowanie typów konkretnych oparte na ich wspólnych zachowaniach może być wyrażone
jako typ interfejsowy. W przeciwieństwie do języków opartych na klasach, w których zestaw interfej-
sów o warunkach spełnianych przez jakąś klasę jest wyraźnie określony, w języku Go możemy
definiować nowe abstrakcje lub grupy interesów w razie potrzeby, bez modyfikacji deklaracji typu
konkretnego. Jest to szczególnie przydatne, gdy dany typ konkretny pochodzi z pakietu napisanego
przez innego autora. Oczywiście muszą istnieć bazowe podobieństwa w tych typach konkretnych.
7.4. Parsowanie flag za pomoc(cid:241) interfejsu flag.Value
W tym podrozdziale zobaczymy, jak inny standardowy interfejs, flag.Value, pomaga nam defi-
niować nowe notacje dla flag wiersza poleceń. Rozważmy poniższy program, który zostaje uśpiony
na określony czas.
code/r07/sleep
var period = flag.Duration( period , 1*time.Second, sleep period )
func main() {
flag.Parse()
fmt.Printf( (cid:165)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
(cid:165)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
Poleć książkęKup książkę7.4. PARSOWANIE FLAG ZA POMOC(cid:240) 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
(cid:165)pi przez 50ms...
$ ./sleep -period 2m30s
(cid:165)pi przez 2m30s...
$ ./sleep -period 1.5h
(cid:165)pi przez 1h30m0s...
$ ./sleep -period 1 dzie(cid:241)
invalid value 1 dzie(cid:241) for flag -period: time: unknown unit dzie(cid:241) in duration 1 dzie(cid:241)
Ponieważ flagi czasu trwania są tak przydatne, ta funkcja jest wbudowana w pakiet flag, ale łatwo
jest zdefiniować nową notację flag dla własnych typów danych. Trzeba tylko zdefiniować typ
spełniający warunki interfejsu flag.Value, którego deklaracja została przedstawiona poniżej:
package flag
// Value jest interfejsem dla warto(cid:286)ci przechowywanej we fladze.
type Value interface {
String() string
Set(string) error
}
Metoda String formatuje wartość flagi do wykorzystywania w komunikatach pomocy wiersza
poleceń. Zatem każdy interfejs flag.Value jest również interfejsem fmt.Stringer. Metoda Set
parsuje swój argument w postaci łańcucha znaków i aktualizuje wartość flagi. W efekcie metoda Set
stanowi odwrotność metody String i jest dobrą praktyką, aby obie te metody korzystały z tej
samej notacji.
Zdefiniujmy typ celsiusFlag, który pozwala określać temperaturę w stopniach Celsjusza lub Fahren-
heita z odpowiednią konwersją. Należy zwrócić uwagę, że celsiusFlag osadza typ Celsius (zob.
podrozdział 2.5), a tym samym uzyskuje za darmo metodę String. Aby spełnić warunki inter-
fejsu flag.Value, trzeba tylko zadeklarować metodę Set:
code/r07/tempconv
// Typ *celsiusFlag spe(cid:225)nia warunki interfejsu flag.Value.
type celsiusFlag struct{ Celsius }
func (f *celsiusFlag) Set(s string) error {
var unit string
var value float64
fmt.Sscanf(s, f s , value, unit) // nie potrzeba kontroli b(cid:225)(cid:266)dów
switch unit {
case C , °C :
f.Celsius = Celsius(value)
return nil
case F , °F :
f.Celsius = FToC(Fahrenheit(value))
return nil
}
return fmt.Errorf( nieprawid(cid:239)owa temperatura q , s)
}
Wywołanie funkcji fmt.Sscanf parsuje liczbę zmiennoprzecinkową (value) i łańcuch znaków
(unit) z danych wejściowych s. Chociaż zwykle trzeba sprawdzać wynik błędu funkcji Sscanf,
w tym przypadku nie musimy tego robić, ponieważ jeśli wystąpiłby problem, nie zostałby dopasowa-
ny żaden przypadek instrukcji switch.
Poleć książkęKup książkę182
ROZDZIA(cid:227) 7. INTERFEJSY
To wszystko opakowuje przedstawiona poniżej funkcja CelsiusFlag. Zwraca ona podmiotowi
wywołującemu wskaźnik do pola Celsius osadzonego w zmiennej f typu celsiusFlag. Pole
Celsius jest zmienną, która będzie aktualizowana przez metodę Set w trakcie przetwarzania flag.
Wywołanie Var dodaje daną flagę do zestawu flag wiersza poleceń aplikacji, czyli zmiennej globalnej
flag.CommandLine. Programy z niezwykle skomplikowanymi interfejsami wiersza poleceń mogą
mieć kilka zmiennych tego typu. Wywołanie Var przypisuje argument *celsiusFlag do parametru
flag.Value, powodując, że kompilator sprawdza, czy *celsiusFlag posiada niezbędne metody.
// CelsiusFlag definiuje flag(cid:266) Celsius z okre(cid:286)lon(cid:261) nazw(cid:261), domy(cid:286)ln(cid:261) warto(cid:286)ci(cid:261)
// oraz informacj(cid:261) o sposobie wykorzystania i zwraca adres zmiennej flagi.
// Argument flagi musi podawa(cid:252) ilo(cid:286)(cid:252) i jednostk(cid:266), np. 100C .
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
f := celsiusFlag{value}
flag.CommandLine.Var( f, name, usage)
return f.Celsius
}
Teraz możemy zacząć używać tej nowej flagi w naszych programach:
code/r07/tempflag
var temp = tempconv.CelsiusFlag( temp , 20.0, temperatura )
func main() {
flag.Parse()
fmt.Println(*temp)
}
Oto typowa sesja:
$ go build code/r07/tempflag
$ ./tempflag
20°C
$ ./tempflag -temp -18C
-18°C
$ ./tempflag -temp 212°F
100°C
$ ./tempflag -temp 273.15K
invalid value 273.15K for flag -temp: nieprawid(cid:239)owa temperatura 273.15K
Usage of ./tempflag:
-temp value
temperatura (default 20°C)
$ ./tempflag -help
Usage of ./tempflag:
-temp value
temperatura (default 20°C)
Ćwiczenie 7.6. Dodaj do programu tempflag wsparcie dla temperatury w skali Kelvina.
Ćwiczenie 7.7. Wyjaśnij, dlaczego komunikat pomocy zawiera symbol °C, podczas gdy domyślna
wartość 20.0 go nie zawiera.
7.5. Warto(cid:264)ci interfejsów
Koncepcyjnie wartość typu interfejsowego, czyli inaczej wartość interfejsu, obejmuje dwa kompo-
nenty: typ konkretny i wartość tego typu. Są one nazywane dynamicznym typem i dynamiczną
wartością interfejsu.
Poleć książkęKup książkę7.5. WARTO(cid:263)CI INTERFEJSÓW
183
W przypadku języka typowanego statycznie, takiego jak Go, typy są pojęciem czasu kompilacji,
więc typ nie jest wartością. W naszym modelu koncepcyjnym zestaw wartości, zwanych deskryp-
torami typów, dostarcza na temat każdego typu informacji takich jak jego nazwa i metody. W warto-
ści interfejsu komponent typu jest reprezentowany przez odpowiedni deskryptor typu.
W czterech poniższych instrukcjach zmienna w przyjmuje trzy różne wartości. (Wartość początkowa
jest taka sama jak końcowa).
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
Przyjrzyjmy się bliżej wartości i dynamicznemu zachowaniu zmiennej w po każdej instrukcji.
Pierwsza instrukcja deklaruje w:
var w io.Writer
W języku Go zmienne są zawsze inicjowane do wyraźnie określonej wartości, a interfejsy nie są wy-
jątkiem. Wartość zerowa dla interfejsu ma ustawione na nil oba jego komponenty: typ i wartość
(rysunek 7.1).
Rysunek 7.1. Wartość nil interfejsu
Wartość interfejsu jest opisywana jako nil lub różna od nil na podstawie jego dynamicznego
typu, więc to jest wartość nil interfejsu. Możesz sprawdzić, czy wartością interfejsu jest nil, za
pomocą w == nil lub w != nil. Wywołanie jakiejkolwiek metody wartości nil interfejsu wywołuje
procedurę panic:
w.Write([]byte( witaj )) // panic: wy(cid:225)uskanie wska(cid:296)nika nil
Druga instrukcja przypisuje do zmiennej w wartość typu *os.File:
w = os.Stdout
To przypisanie obejmuje pośrednią konwersję z typu konkretnego na typ interfejsowy i jest równo-
ważne z bezpośrednią konwersją io.Writer(os.Stdout). Konwersja tego rodzaju, pośrednia lub
bezpośrednia, przechwytuje typ i wartość jego operandu. Typ dynamiczny interfejsu jest ustawiany
na deskryptor typu dla typu wskaźnika *os.File, a jego wartość dynamiczna przechowuje kopię
os.Stdout, która jest wskaźnikiem do zmiennej os.File reprezentującej standardowy strumień
wyjściowy procesu (rysunek 7.2).
Rysunek 7.2. Wartość interfejsu zawierająca wskaźnik *os.File
Poleć książkęKup książkę184
ROZDZIA(cid:227) 7. INTERFEJSY
Wywołanie metody Write na wartości interfejsu zawierającej wskaźnik *os.File powoduje wywoła-
nie metody (*os.File).Write. To wywołanie wyświetla witaj .
w.Write([]byte( witaj )) // witaj
Zasadniczo podczas kompilacji nie możemy wiedzieć, jaki będzie dynamiczny typ wartości inter-
fejsu, więc wywołanie poprzez interfejs musi używać dynamicznego rozdzielania (ang. dynamic
dispatch). Zamiast bezpośredniego wywołania kompilator musi wygenerować kod, aby uzyskać adres
metody o nazwie Write z deskryptora typu, a następnie wykonać pośrednie wywołanie tego adre-
su. Argumentem odbiornika dla tego wywołania jest kopia wartości dynamicznej interfejsu —
os.Stdout. Efekt jest taki, jakbyśmy wykonali to wywołanie bezpośrednio:
os.Stdout.Write([]byte( witaj )) // witaj
Trzecia instrukcja przypisuje wartość typu *bytes.Buffer do wartości interfejsu:
w = new(bytes.Buffer)
Typem dynamicznym jest teraz *bytes.Buffer, a wartością dynamiczną jest wskaźnik do nowo alo-
kowanego bufora (rysunek 7.3).
Rysunek 7.3. Wartość interfejsu zawierająca wskaźnik *bytes.Buffer
Wywołanie metody Write wykorzystuje ten sam mechanizm co poprzednio:
w.Write([]byte( witaj )) // zapisuje witaj w bytes.Buffer
Tym razem deskryptorem typu jest *bytes.Buffer, więc wywoływana jest metoda (*bytes.
Buffer).Write z adresem bufora jako wartością parametru odbiornika. To wywołanie dołącza
witaj do bufora.
Na koniec czwarta instrukcja przypisuje nil do wartości interfejsu:
w = nil
To przypisanie resetuje oba jego komponenty do wartości nil, przywracając zmienną w do tego same-
go stanu, w jakim była po zadeklarowaniu, czyli do stanu, który został przedstawiony na rysunku 7.1.
Wartość interfejsu może przechowywać dowolnie duże wartości dynamiczne. Przykładowo: typ
time.Time, który reprezentuje moment w czasie, jest typem struct z kilkoma niewyeksporto-
wanymi polami. Jeśli utworzymy z niego wartość interfejsu
var x interface{} = time.Now()
wynik może wyglądać tak, jak pokazano na rysunku 7.4. Koncepcyjnie wartość dynamiczna zawsze
mieści się wewnątrz wartości interfejsu, bez względu na to, jak duży jest jego typ. (To tylko model
koncepcyjny. Rzeczywista implementacja jest zupełnie inna)
Poleć książkęKup książkę7.5. WARTO(cid:263)CI INTERFEJSÓW
185
Rysunek 7.4. Wartość interfejsu przechowuje strukturę time.Time
Wartości interfejsów mogą być porównywane za pomocą operatorów == i !=. Dwie wartości in-
terfejsów są równe, jeśli obie są nil lub jeśli ich typy dynamiczne są identyczne, a ich wartości
dynamiczne są równe zgodnie ze standardowym zachowaniem operatora == dla danego typu.
Ponieważ wartości interfejsów są porównywalne, mogą być stosowane jako klucze map lub jako
operandy instrukcji switch.
Jeśli jednak porównywane są dwie wartości interfejsów i mają one ten sam typ dynamiczny, który
jednak nie jest porównywalny (np. wycinek), porównanie nie powiedzie się i wywoła panikę:
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: porównywanie nieporównywalnego typu []int
Pod tym względem typy interfejsów są ewenementem. Inne typy są bezpiecznie porównywalne
(typy podstawowe i wskaźniki) albo w ogóle nieporównywalne (wycinki, mapy i funkcje), ale kie-
dy porównujemy wartości interfejsów lub typy złożone, które zawierają wartości interfejsów,
musimy być świadomi możliwości uruchomienia procedury panic. Podobne ryzyko istnieje, gdy
używamy interfejsów jako kluczy map lub operandów instrukcji switch. Wartości interfejsów na-
leży porównywać tylko wtedy, kiedy ma się pewność, że zawierają one dynamiczne wartości po-
równywalnych typów.
Podczas obsługi błędów lub debugowania często pomocne jest raportowanie dynamicznego typu
wartości interfejsu. W tym celu używamy czasownika T pakietu fmt:
var w io.Writer
fmt.Printf( T\n , w) // nil
w = os.Stdout
fmt.Printf( T\n , w) // *os.File
w = new(bytes.Buffer)
fmt.Printf( T\n , w) // *bytes.Buffer
Wewnętrznie pakiet fmt do uzyskania nazwy dynamicznego typu interfejsu wykorzystuje refleksję.
Refleksji przyjrzymy się w rozdziale 12.
7.5.1. Zastrzeżenie: interfejs zawierający wskaźnik nil jest różny od nil
Wartość nil interfejsu, który nie zawiera w ogóle żadnej wartości, nie jest tym samym co wartość
interfejsu zawierającego wskaźnik, który akurat jest nil. Ta subtelna różnica tworzy pułapkę, w którą
wpadł chyba każdy programista Go.
Rozważmy poniższy program. Przy stałej debug ustawionej na wartość true funkcja main gromadzi
dane wyjściowe z funkcji f w typie bytes.Buffer.
Poleć książkęKup książkę186
ROZDZIA(cid:227) 7. INTERFEJSY
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // w(cid:225)(cid:261)czenie gromadzenia danych wyj(cid:286)ciowych
}
f(buf) // Uwaga: subtelnie nieprawid(cid:225)owe!
if debug {
// …u(cid:298)ycie buf…
}
}
// Je(cid:286)li parametr out jest ró(cid:298)ny od nil, dane wyj(cid:286)ciowe b(cid:266)d(cid:261) w nim zapisywane.
func f(out io.Writer) {
// …co(cid:286) 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(cid:225)uskanie wska(cid:296)nika nil
}
Gdy funkcja main wywołuje funkcję f, przypisuje do parametru out wskaźnik nil typu *bytes.
(cid:180)Buffer, więc wartością dynamiczną parametru out jest nil. Jego typem dynamicznym jest jednak
*bytes.Buffer, co oznacza, że out jest interfejsem różnym od nil, zawierającym wartość wskaź-
nika nil (rysunek 7.5), więc sprawdzenie defensywne out != nil jest nadal prawdziwe.
Rysunek 7.5. Różny od nil interfejs zawierający wskaźnik nil
Tak jak wcześniej dynamiczny mechanizm rozdzielania określa, że wywołana musi być metoda
(*bytes.Buffer).Write, ale tym razem z wartością odbiornika, którą jest nil. Dla niektórych typów,
takich jak *os.File, nil jest prawidłowym odbiornikiem (zob. punkt 6.2.1), ale *bytes.Buffer
do nich nie należy. Ta metoda jest wywoływana, ale uruchamia procedurę panic, ponieważ próbuje
uzyskać dostęp do bufora.
Problem polega na tym, że chociaż wskaźnik nil typu *bytes.Buffer ma metody niezbędne do
spełnienia warunków tego interfejsu, to nie spełnia jego wymagań behawioralnych. Wywołanie
to narusza w szczególności dorozumiany warunek wstępny metody (*bytes.Buffer).Write,
który zakłada, że jej odbiornik nie będzie nil, więc przypisanie wskaźnika nil do tego interfejsu
było błędem. Rozwiązaniem jest zmiana typu zmiennej buf w funkcji main na io.Writer, co po-
zwala przede wszystkim uniknąć przypisania do interfejsu dysfunkcyjnej wartości:
Poleć książkęKup książkę7.6. SORTOWANIE ZA POMOC(cid:240) INTERFEJSU SORT.INTERFACE
187
var buf io.Writer
if debug {
buf = new(bytes.Buffer) // w(cid:225)(cid:261)czenie gromadzenia danych wyj(cid:286)ciowych
}
f(buf) // OK
Omówiliśmy mechanizmy wartości interfejsu, więc przyjrzyjmy się teraz kilku ważniejszym inter-
fejsom ze standardowej biblioteki języka Go. W trzech kolejnych podrozdziałach zobaczymy, w jaki
sposób interfejsy są używane do sortowania, serwowania zawartości WWW i obsługi błędów.
7.6. Sortowanie za pomoc(cid:241) interfejsu sort.Interface
Tak jak formatowanie łańcuchów znaków sortowanie jest często używaną operacją w wielu pro-
gramach. Chociaż minimalny algorytm sortowania szybkiego (ang. quicksort) można zmieścić w ja-
kichś 15 liniach kodu, solidna implementacja jest znacznie dłuższa i nie jest to rodzaj kodu, który
za każdym razem chcielibyśmy pisać od nowa lub kopiować, gdy jest on potrzebny.
Na szczęście pakiet sort zapewnia sortowanie in situ dowolnej sekwencji według którejkolwiek
funkcji porządkującej. Jego konstrukcja jest dość niezwykła. W wielu językach algorytm sortowa-
nia jest powiązany z sekwencyjnym typem danych, podczas gdy funkcja porządkująca jest powią-
zana z typem elementów. Natomiast funkcja sort.Sort języka Go nie zakłada niczego na temat
reprezentacji sekwencji lub jej elementów. Zamiast tego wykorzystuje interfejs sort.Interface,
służący do określania kontraktu pomiędzy ogólnym algorytmem sortowania i każdym typem se-
kwencyjnym, który może być sortowany. Implementacja tego interfejsu określa zarówno konkretną
reprezentację sekwencji (którą często jest wycinek), jak i wymagany porządek jej elementów.
Algorytm sortowania in situ potrzebuje trzech rzeczy (długości sekwencji, zasad porównywania
dwóch elementów i sposobu zamieniania dwóch elementów), więc interfejs sort.Interface ma
trzy metody:
package sort
type Interface interface {
Len() int
Less(i, j int) bool // i, j to indeksy elementów sekwencji
Swap(i, j int)
}
Aby posortować dowolną sekwencję, musimy zdefiniować typ implementujący te trzy metody, a na-
stępnie zastosować funkcję sort.Sort do instancji tego typu. Rozważmy sortowanie wycinka łań-
cuchów znaków jako być może najprostszy przykład. Poniżej pokazano nowy typ StringSlice
i jego metody Len, Less i Swap.
type StringSlice []string
func (p StringSlice) Len() int { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] p[j] }
func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
Teraz możemy posortować wycinek łańcuchów znaków (names) poprzez przekonwertowanie go
na typ StringSlice w taki sposób:
sort.Sort(StringSlice(names))
Ta konwersja daje wartość wycinka o tej samej długości, pojemności i tablicy bazowej co names,
ale z typem, który ma trzy metody wymagane do sortowania.
Poleć książkęKup książkę188
ROZDZIA(cid:227) 7. INTERFEJSY
Sortowanie wycinka łańcuchów znaków jest tak powszechne, że pakiet sort zapewnia typ
StringSlice oraz funkcję o nazwie Strings, więc powyższe wywołanie można uprościć do postaci
sort.Strings(names).
Przedstawioną tu technikę można łatwo dostosować do innych porządków sortowania, np. do
ignorowania wielkich liter lub znaków specjalnych. (Program Go sortujący tematy skorowidza
i numery stron dla tej książki robi to przy dodatkowej logice dla cyfr rzymskich). Do skompliko-
wanego sortowania używamy tej samej koncepcji, ale z bardziej skomplikowanymi strukturami
danych lub z bardziej skomplikowanymi implementacjami metod interfejsu sort.Interface.
Naszym działającym przykładem sortowania będzie lista odtwarzania utworów muzycznych wy-
świetlona w postaci tabeli. Każdy utwór będzie pojedynczym wierszem, a każda kolumna będzie
atrybutem tego utworu, takim jak: artysta, tytuł i czas odtwarzania. Wyobraź sobie, że graficzny
interfejs użytkownika prezentuje tabelę, a kliknięcie nagłówka wybranej kolumny powoduje po-
sortowanie listy odtwarzania według tego atrybutu. Ponowne kliknięcie nagłówka tej samej ko-
lumny odwraca kolejność. Zobaczmy, co się może zdarzyć w reakcji na każde kliknięcie.
Użyta poniżej zmienna tracks zawiera listę odtwarzania. (Jeden z autorów przeprasza za gust mu-
zyczny drugiego). Każdy element jest pośredni — jest wskaźnikiem do typu Track. Chociaż poniższy
kod działałby, gdybyśmy przechowywali elementy Track bezpośrednio, funkcja sortowania za-
mieni wiele par elementów, więc będzie działać szybciej, jeżeli każdy element będzie wskaźnikiem,
czyli pojedynczym słowem maszynowym, a nie całym typem Track, który może mieć osiem
słów lub więcej.
code/r07/sorting
type Track struct {
Title string
Artist string
Album string
Year int
Length time.Duration
}
var tracks = []*Track{
{ Go , Delilah , From the Roots Up , 2012, length( 3m38s )},
{ Go , Moby , Moby , 1992, length( 3m37s )},
{ Go Ahead , Alicia Keys , As I Am , 2007, length( 4m36s )},
{ Ready 2 Go , Martin Solveig , Smash , 2011, length( 4m24s )},
}
func length(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
panic(s)
}
return d
}
Funkcja printTracks wyświetla listę odtwarzania w postaci tabeli. Wyświetlenie graficzne było-
by ładniejsze, ale ta prosta procedura wykorzystuje pakiet text/tabwriter do wygenerowania tabeli,
której kolumny są starannie wyrównane i dopełnione, tak jak pokazano nieco niżej. Zauważmy,
że *tabwriter.Writer spełnia warunki interfejsu io.Writer. Gromadzi każdy zapisany w nim
fragment danych. Jego metoda Flush formatuje całą tabelę i zapisuje ją do os.Stdout.
func printTracks(tracks []*Track) {
const format = v\t v\t v\t v\t v\t\n
Poleć książkęKup książkę7.6. SORTOWANIE ZA POMOC(cid:240) INTERFEJSU SORT.INTERFACE
189
tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, , 0)
fmt.Fprintf(tw, format, Tytu(cid:239) , Artysta , Album , Rok , D(cid:239)ugo(cid:258)(cid:202) )
fmt.Fprintf(tw, format, ----- , ------- , ----- , --- , ------- )
for _, t := range tracks {
fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
}
tw.Flush() // oblicza szeroko(cid:286)ci kolumn i wy(cid:286)wietla tabel(cid:266)
}
Aby posortować listę utworów według pola Artist, definiujemy nowy typ wycinka z niezbędnymi
metodami Len, Less i Swap, analogicznie do tego, co zrobiliśmy dla StringSlice.
type byArtist []*Track
func (x byArtist) Len() int { return len(x) }
func (x byArtist) Less(i, j int) bool { return x[i].Artist x[j].Artist }
func (x byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
Aby wywołać ogólną procedurę sortowania, musimy najpierw przekonwertować tracks na no-
wy typ byArtist, który definiuje porządek:
sort.Sort(byArtist(tracks))
Po posortowaniu wycinka według artysty dane wyjściowe z funkcji printTracks są następujące:
Tytu(cid:239) Artysta Album Rok D(cid:239)ugo(cid:258)(cid:202)
----- ------- ----- --- -------
Go Ahead Alicia Keys As I Am 2007 4m36s
Go Delilah From the Roots Up 2012 3m38s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Moby Moby 1992 3m37s
Jeśli użytkownik po raz drugi zażąda „sortuj według artysty”, utwory zostaną posortowane w od-
wrotnej kolejności. Nie musimy jednak definiować nowego typu byReverseArtist z odwróconą
metodą Less, ponieważ pakiet sort zapewnia funkcję Reverse, która przekształca dowolny porzą-
dek sortowania na jego odwrotność:
sort.Sort(sort.Reverse(byArtist(tracks)))
Po odwrotnym posortowaniu wycinka według artysty dane wyjściowe z funkcji printTracks są
następujące:
Tytu(cid:239) Artysta Album Rok D(cid:239)ugo(cid:258)(cid:202)
----- ------- ----- --- -------
Go Moby Moby 1992 3m37s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
Funkcja sort.Reverse zasługuje na uwagę, ponieważ wykorzystuje kompozycję (zob. podrozdział
6.3), która jest ważną koncepcją. Pakiet sort definiuje niewyeksportowany typ reverse, który jest
strukturą osadzającą sort.Interface. Metoda Less dla typu reverse wywołuje metodę Less osa-
dzonej wartości sort.Interface, ale z odwróconymi indeksami, co odwraca kolejność wyników
sortowania.
package sort
type reverse struct{ Interface } // czyli sort.Interface
func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }
Poleć książkęKup książkę190
ROZDZIA(cid:227) 7. INTERFEJSY
Pozostałe dwie metody typu reverse, czyli Len i Swap, są pośrednio dostarczane przez oryginal-
ną wartość sort.Interface, ponieważ jest to osadzone pole. Wyeksportowana funkcja Reverse
zwraca instancję typu reverse, która zawiera oryginalną wartość sort.Interface.
Aby posortować według innej kolumny, musimy zdefiniować nowy typ, np. byYear (według roku):
type byYear []*Track
func (x byYear) Len() int { return len(x) }
func (x byYear) Less(i, j int) bool { return x[i].Year x[j].Year }
func (x byYear) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
Po posortowaniu trakcs według roku za pomocą sort.Sort(byYear(tracks)) funkcja printTracks
wyświetla chronologiczny wykaz:
Tytu(cid:239) Artysta Album Rok D(cid:239)ugo(cid:258)(cid:202)
----- ------- ----- --- -------
Go Moby Moby 1992 3m37s
Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Delilah From the Roots Up 2012 3m38s
Dla każdego typu elementu wycinka i każdej funkcji porządkowania, której potrzebujemy, dekla-
rujemy nową implementację sort.Interface. Jak widać, metody Len i Swap mają identyczne
definicje dla wszystkich typów wycinka. W następnym przykładzie typ konkretny customSort
łączy wycinek z funkcją, pozwalając nam zdefiniować nowy porządek poprzez napisanie jedynie
funkcji porównania. Nawiasem mówiąc, konkretne typy implementujące sort.Interface nie
zawsze są wycinkami. Typ customSort jest typem struct.
type customSort struct {
t [ ]*Track
less func(x, y *Track) bool
}
func (x customSort) Len() int { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }
Zdefiniujmy wielopoziomową funkcję porządkowania, której głównym kluczem sortowania jest
Title (tytuł), kluczem wtórnym jest Year (rok), a kluczem trzeciorzędowym jest Length (długość
utworu). Oto wywołanie funkcji Sort wykorzystujące anonimową funkcję porządkowania:
sort.Sort(customSort{tracks, func(x, y *Track) bool {
if x.Title != y.Title {
return x.Title y.Title
}
if x.Year != y.Year {
return x.Year y.Year
}
if x.Length != y.Length {
return x.Length y.Length
}
return false
}})
A oto wynik. Należy zwrócić uwagę, że powiązanie między dwoma utworami zatytułowanymi Go
zostało rozerwane na korzyść utworu starszego
Pobierz darmowy fragment (pdf)