Instrukcje warunkowe i pętle
W Go mamy kilka instrukcji warunkowych: if, switch, select i tylko jedną
pętle - for. Każda z nich ma dodatkowe właściwości, które nie są oczywiste,
dla kogoś kto programował już wcześniej. Przyjrzyjmy się im z bliska.
If, składnia
if to instrukcja warunkująca wykonanie bloku kodu. Zwyczaj każe by po słowie
kluczowym if pojawiło się wyrażenie zwracające wartość logiczną, a następnie
blok kodu, tak też jest i w Go:
if a > b { c := a a = b b = c }
Powyższy fragment wykona zamianę wartości a i b jeśli a jest większe od
b. Użyliśmy tam zmiennej pomocniczej c, która ponieważ została zadeklarowana
wewnątrz bloku warunkowego nie będzie widoczna poza nim. Takie zachowanie
zasięgów zmiennych może prowadzić do ciężkich do debugowania błędów, szczególnie
w sytuacjach gdy będziemy re-deklarować istniejące zmienne:
a := 100 if a > 0 { a := 1 //1 a++ fmt.Println(a)//2 } fmt.Println(a)//3
Można by się spodziewać, że gdy program osiągnie linię //3 to wyprowadzi na wyjście liczbę 2, a jednak wyprowadza 100. Spowodowane jest to tym, że w linii //1 zadeklarowaliśmy nową zmienną w nowym zasięgu, która jest nieosiągalna poza blokiem warunkowym. Niestety też kompilator w //1 nie uprzedzi nas o błędzie bo to całkiem legalna i czasem pożądana operacja.
Możemy zadeklarować także zmienną w samej instrukcji warunkowej, jeśli to
potrzebne. Między if a warunkiem można dodać dowolną instrukcję, którą należy
zakończyć średnikiem:
if v1, ok := execute(cmd); ok { fmt.Println(cmd, "successful, returned:", v1) }
To całkiem typowa dla Go konstrukcja, często używana np. przy asercji typów.
Oczywiście, opcjonalnie możemy dodać klauzule else, i blok kodu który będzie
wykonywany tylko w przypadku nie spełnienia warunku w instrucji if.
if a > 0 { doSmth() } else { doSmthElse() }
Za else możemy wstawić kolejny if:
if a % 2 == 0 { fmt.Println("a jest parzyste") } else if a % 5 == 0 { fmt.Println("a jest nieparzyste podzielne przez 5") } else { fmt.Println("a jest nie parzyste i nie podzielne przez 5") }
Rodzaje pętli
W Go zapętlania służy tylko jedną instrukcja: for. Nie mniej przybiera kilka
form:
For warunkowy
Po for możemy wstawić dowolne wyrażenie, które zwraca wartość logiczną, np:
for true { }
lub
for a < b { }
W pierwszym przykładzie pętla będzie przechodziła dopóki wykonywanie programu się nie zakończy. W drugim, dopóki wartość a będzie mniejsza od b.
For klasyczny
for w postaci klasycznej posiada 3 rozdzielone średnikami instrukcje. Pierwsza
to inicjalizacja, która odbywa się tylko raz. Druga instrukcja to wyrażenie
zwracające wartość logiczną, pętla będzie wykonywać się tak długo jak owo
wyrażenie będzie zwracało true, wyrażenie będzie wywoływane co przejście
pętli. Trzecia instrukcja to inkrementacja, odbywa się po każdym przejściu pętli
przed sprawdzeniem warunku.
Jak działa for w klasycznej formie polecam sprawdzić samemu kompilując i uruchamiając następujący program:
For iteracyjny
W Go iterowalnymi kolekcjami są wartości typów: slice, array, map,
string i chan. Do iterowania po kolekcjach używamy instrukcji range.
Iterowanie po slicesach i arrayach
W przypadku tablic i wycinków iteracja wygląda tak samo.
for i := range(it) { fmt.Println(it[i]) }
lub też:
for i, v := range(it) { fmt.Print(v) }
W obu przypadkach zmienna i to numer indeksu tablicy czy wycinka. Iteracja
zaczyna się od i == 0 a kończy na len(it). W drugim przypadku dodatkowa
zmienna v będzie wartością elementu o indeksie i w skrócie v := it[i].
Iterowanie po elementach mapy
Tak jak dla wycinków i tablic iteracja map może przybrać jedną z dwóch form
for k := range(m) { fmt.Println(m[k]) }
oraz
for k, v := range(m) { fmt.Println(v) }
Analogicznie, k to klucz a v to przypisana mu wartość w mapie. Mapy nie
gwarantują żadnego porządku w iteracji.
Iterowanie po stringach
Iterowanie po stringach jest oczywiste... dopóki nie próbujemy iterować po
łańcuchach encodowanych w UTF-8. Co do zasady w i będzie pozycja bajta
rozpoczynającego znak a w c wartość znaku. Niestety pozycja bajta nie zmienia
się liniowo, bo w UTFie znaki są zapisane na jednym lub większej ilości bajtów.
Przykładowo:
for i, c := range("zażółć") { fmt.Println(i, c) }
Wyprowadzi na ekran:
0 122 1 97 2 380 4 243 6 322 8 263
ostatnie trzy przejścia przez pętle i zwiększało się o 2.
W UTFach, można też zapisać znak niepoprawnie, wtedy range zamiast poprawnej wartości znaku zwróci wartość 0xFFFD i od następnego bajta będzie próbował znaleźć prawidłowy znak.
Iterowanie kanału
Można także iterować po zmiennej reprezentującej kanał, przy czym wyrażenie range() zwraca tylko jedną wartość na raz i jest to wartość przekazana do kanału. Iteracja kończy się gdy kanał zostaje zamknięty.
for v := range(ch) { fmt.Println(v) }
Switch (wyrażeniowy)
Zamiast pisać łańcuchy if-else można użyć instrukcji warunkowej switch.
Składa się ona ze słowa kluczowego switch po którym następuje wyrażenie i
blok przypadków (case).
Każdy case składa się z listy wyrażeń rozdzielonych przecinkami która kończy
się dwukropkiem. Po dwukropku rozpoczyna się blok kodu, który zostanie wykonany
gdy wynik wyrażenia z instrukcji switch będzie tożsamy z wynikiem jednego
z wylistowywanych wyrażeń. Blok kodu case kończy się wraz z rozpoczęciem
definicji kolejnego przypadku.
Wygląda na skomplikowane, więc posłużę się przykładem:
switch a { case 1, 2, 3: //1 fmt.Println("a ma wartość 1, 2 lub 3") case 4: //2 fmt.Println("a ma wartość 4") }
W linii //1 porównujemy wartość zmiennej a z jedną z wartości: 1, 2, 3. Jeśli a spełnia ten przypadek to na ekran zostanie wyprowadzony napis "a ma wartość 1, 2 lub 3". W linii //2 mamy tylko jeden element listy wyrażeń przypadku.
Gdyby interesował nas przypadek nie spełniający wszystkich w/w caseów możemy
użyć instrukcji default np.:
switch a { case 1, 2, 3: fmt.Println("a ma wartość 1, 2 lub 3") case 4: fmt.Println("a ma wartość 4") default: fmt.Println("a ma inną wartość niż 1,2,3,4") }
Co ciekawe można pominąć wyrażenie w switch, wtedy wyrażenia w case będą
porównywane z prawdą true. Moglibyśmy powyższy przykład przepisać tak:
switch { case a == 1, a == 2, a == 3: fmt.Println("a ma wartość 1, 2 lub 3") case a == 4: fmt.Println("a ma wartość 4") default: fmt.Println("a ma inną wartość niż 1,2,3,4") }
I podobnie jak w ifach, między switch a wyrażeniem możemy wstawić dodatkową
instrukcję, którą należy zakończyć średnikiem:
switch a := produceA(); a { case 1, 2, 3: fmt.Println("a ma wartość 1, 2 lub 3") case 4: fmt.Println("a ma wartość 4") default: fmt.Println("a ma inną wartość niż 1,2,3,4") }
Zasady zasięgu deklarowanych zmiennych w blokach są takie jak w if. Tu
jeszcze raz przypominam: brak wyrażenia po średniku sprawi, że wyrażenia w
przypadkach będą przyrównywane do true:
switch a := produceA(); { case a == 1, a == 2, a == 3: fmt.Println("a ma wartość 1, 2 lub 3") case a == 4: fmt.Println("a ma wartość 4") default: fmt.Println("a ma inną wartość niż 1,2,3,4") }
Switch (typów)
To bardzo użyteczna konstrukcja, szczególnie, gdy w kodzie mocno polegamy na interfejsach, pomaga nam wyciągnąć typ bazowy wartości zmiennej, lub sprawdzić czy spełnia ona jakiś inny interfejs. (To takie połączenie asercji typów ze switchem)
Składniowo przypomina zwykłego switcha, nie mniej zamiast wyrażenia używamy
specjalnego operatora .(type), a zamiast wyrażeń w case'ach wylistowujemy
typy:
switch a := unknown.(type) { case int: fmt.Println("unknown daje się przedstawić jako int o wartości", a) case string: fmt.Println("unknown daje się przedstawić jako string o wartości", a) case float, float64: fmt.Println("unknown jest jednym z floatów, ale a będzie typu interface{}", a) default: fmt.Println("unknown nie jest intem, stringiem, floatem a zmienna a jest typu interface{}", a) }
Jak w przykładzie dałem do zrozumienia, jeśli wyrażenie switcha poprzedzimy przypisaniem do jakiejś zmiennej to ta zmienna w bloku case będzie miała już taki typ jaki przewiduje warunek wejścia do bloku. Jeśli więcej niż jeden typ wpuszcza do bloku zmienna a przyjmuje typ pustego interfejsu.
Select
To specjalny typ przełącznika podobnego w konstrukcji do switch. Służy jednak
do wybierania kanału (wartości typów chan) z którego informacja zostanie
odebrana lub wysłana i obsłużona. Wybór odbywa się w zależności od dostępności
kanałów, a w przypadku gdy więcej niż jedna operacja jest możliwa wybór następuje
losowo. Przełącznik select blokuje wykonanie gorutyny do momentu wykonania
operacji.
select { case e := <-ch : fmt.Println("Z kanału ch otrzymaliśmy wartość", e) case f := <-ch1: fmt.Println("Z kanału ch1 otrzymaliśmy wartość", f) case ch <- g: fmt.Println("Do kanału ch wysłaliśmy wartość", g) }
Selecta używamy gdy chcemy by program zaczekał na jedno ze zdarzeń, lub jeśli select będzie w pętli nieskończonej, zarządzał danymi przychodzącymi z gorutyn.
Wszystko o języku programowania Go