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:

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:

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.

lub też:

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].

Jeśli nie potrzebujemy indeksu w pętli możemy go pominąć, za pomocą pustej zmiennej:

Iterowanie po elementach mapy

Tak jak dla wycinków i tablic iteracja map może przybrać jedną z dwóch form

oraz

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:

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.

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:

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.:

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:

I podobnie jak w ifach, między switch a wyrażeniem możemy wstawić dodatkową instrukcję, którą należy zakończyć średnikiem:

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 (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:

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.

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.