Blog

Golang dla Hobbystów. Pętle

Czwarty tekst o GO dla hobbystów poświęcony został pętlom.

Golang dla Hobbystów. Pętle

Zacznijmy od truizmy, czyli przypomnienia do czego służy pętla w programowaniu komputerowym. Otóż jest to struktura, która ma na celu wielokrotnie wykonanie jakiegoś fragment kodu, zazwyczaj do osiągnięcia czy też spełnienia jakiegoś z góry zdefiniowanego warunku. Tym samym wykorzystanie pętli w programowaniu komputerowym pozwala na automatyzację i wielokrotne powtarzanie jakiegoś zadania.

W Go pętla for implementuje wielokrotne wykonanie kodu na podstawie licznika pętli lub zmiennej pętli. Jednak w przeciwieństwie do innych języków programowania (a przynajmniej tych, które mi są znane) Go ma tylko jeden jej rodzaj, a dokładniej pętlę for. Takie podejście w założeniu twórców tego języka miało uczynić kod bardziej przejrzystym i czytelnym, ponieważ nie musimy się wówczas  martwić uczeniem i zapamiętywaniem różnych strategii tworzenia pętli, co pośrednio powinno sprawić, iż tworzony kod będzie mniej podatny na błędy.

W przypadku GO pętla zawsze rozpoczyna się słowem kluczowym for, po którym mogą pojawić się trzy elementy, które sterują działaniem pętli:

• może pojawić się nstrukcja inicjalizacji, która służy - jak sama nazwa wskazuje - do inicjalizowania w tym wypadku zmiennej,

• musi pojawić się wyrażenie określające określające warunek, którego wystąpienie spowodować ma zakończenie pracy pętli,

• opcjonalnie trzeci element, czyli instrukcja wykonywana po każdej iteracji pętli.

Ogólny schemat wygląda zatem w ten sposób:

for zmienna z wartością początkową ; warunek zakończenia pętli; operacja na zmiennej początkowej {

blok kodu do wykonania

}

Dla rozjaśnienia tego schematu posłużymy się niniejszym przykładem:

package main

import "fmt"

func main() {
 for x:= 0; i < 5; x++ {
  fmt.Println(x)
 }
}

Na początku deklarujemy zmienną o nazwie "x" i ustawiamy jej wartość początkową na 0. Do tego celu używamy krótkiej deklaracji zmiennych. Dalej pojawia się warunek, w którym stwierdzamy, że pętla powinna kontynuować swoje działanie do momentu, gdy wartość naszej zmiennej "x" jest mniejsza od 5. Po tym następuję instrukcja, która wykonywana jest po zakończeniu każdej iteracji pętli. W naszym wypadku po prostu zwiększamy zmienną "x" o jeden przy użyciu operatora inkrementacji x++. Zaś pomiędzy nawiasami mamy kod, który wykonywany jest dla każdego przypadku, gdy pętla jest wykonywana (tutaj proste wyświetlanie wartości naszej zmiennej "x").

W efekcie po uruchomieniu takiego fragmentu kodu wynik będzie wyglądał następująco:

0
1
2
3
4

Jak już wspomniałem wcześniej, pierwszy i ostatni element definicji pętli jest opcjonalny, czyli tym samym możemy wykluczyć początkową inicjalizację zmiennej oraz operacje na niej, pozostawiając tylko warunku mówiący o momencie zakończenia działania pętli. Wówczas konstrukcja takiej pętli mogłaby prezentować się w następujący sposób:

package main

import "fmt"

func main() {
 x:= 0
 for x < 5 {
  fmt.Println(x)
  x++
 }
}

Tym razem zmienną "x" zadeklarowaliśmy poza samą pętlą, w której zostawiliśmy tylko klauzulę warunku mającego za zadanie sprawdzać, czy "x" jest mniejsze niż 5. Oczywiście dopóki warunek ma wartość true pętla będzie kontynuowała iterację. By jednak nie była to pętla nieskończona oraz zwracała dokładnie to samo, co wcześniejsza konstrukcja, sama inkrementacja o 1 wartości przypisanej do zmiennej "x", zawarta jest w bloku wykonywanego kodu.

Pętla for z klauzulą range

Oczywiście w Go można używać pętli for do iteracji dla różnego rodzaju kolekcji typów danych, chociażby takich jak wycinki czy tablice. Niestety tutaj wchodzimy na obszar, który nie został dotąd omówiony (typy złożone), ale myślę, że ze zrozumieniem samej zasady rządzącej takimi scenariuszami dla pętlami oraz  przykładów użytych dalej nie powinno być problemu dla wszystkich osób mających nawet najmniejsze doświadczenie z innych języków programowania, gdzie posługiwali się podobnymi konstrukcjami.

Oczywiście nic nie stoi na przeszkodzie, by iterować po kolekcjach czy sekwencyjnych typach danych przy użyciu wcześniej zaprezentowanej składni i wykorzystaniu chociażby funkcji len. Niżej przykład, który udowadnia tę tezę.

package main
import "fmt"
func main() {
 systemy := []string{"linux", "windows", "bsd", "darwin", "android"}
 for i := 0; i < len(systemy); i++ {
	fmt.Println(os[i])
 }
}

W rezultacie wykonania tego kodu zobaczymy na wyjściu następującą listę wartości:

linux
windows
bsd
darwin
android

Teraz użyjmy klauzuli range, aby wykonać iteracje przez zbiór systemów operacyjnych:

package main

import "fmt"

func main() {
 systemy := []string{"linux", "windows", "bsd", "darwin", "android"}
 for i, system := range systemy {
	fmt.Println(i, system)
 }
}

W efekcie zobaczymy coś takiego:

0 linux
1 windows
2 bsd
3 darwin
4 android

Skąd ta różnica między pierwszym i drugim przypadkiem? Otóż używając klauzuli range na tablicy/wycinku (slice), funkcja zawsze zwróci dwie wartości. Pierwsza wartość będzie indeksem (tutaj od 0 do 4), odpowiadającym bieżącej iteracji pętli, a druga będzie odpowiadała interesującej nas wartości dla każdego z tych indeksów.

Tym niemniej czasami potrzebujemy tylko tej ostatniej wartości. Usunięcie zmiennej “i” z funkcji Println nie wystarczy, ponieważ każda zadeklarowana zmienna musi być następnie użyta w kodzie (o czym była już mowa przy okazji wpisu poświęconego zmiennym). A ponieważ zmienna “i” zadeklarowana została jako część pętli for, w związku z tym musi zostać następnie użyta. Bez tego kompilator zareaguje stosownym błędem.

Tym niemniej jest dość banalny sposób, by tę “niedogodność” obejść. Chodzi mianowicie o wykorzystanie tzw. pustego identyfikatora (ang. blank identifier). Ma on postać podkreślenia dolnego (“_”) i jest używany wszędzie tam, gdzie składnia wymaga posłużenia się nazwą zmiennej, ale logika programu już niekoniecznie, właśnie chociażby po to  by pozbyć niechcianego indeksu pętli. Tym niemniej chyba najwięcej miejsca poświęcimy temu rozwiązaniu przy omawianiu funkcji - tutaj mamy dopiero coś w rodzaju zajawki.

Przykładowy kod będzie wyglądał tak:

package main

import "fmt"

func main() {
    systemy := []string{"linux", "windows", "bsd", "darwin", "android"}
    for _, system := range systemy {
        fmt.Println(system)
    }
}

Używanie instrukcji break i continue podczas pracy z pętlami w Go

Czasami możemy rzecz jasna nie znać liczby iteracji potrzebnych do wykonania określonego zadania, a jednocześnie chcielibyśmy uniknąć wykonywania pętli w nieskończoność. W takim przypadku możemy użyć słowa kluczowego break, aby zakończyć wykonywanie pętli w jakimś momencie. Zazwyczaj ma to postać jakiegoś warunku w bloku wykonywanych instrukcji dla naszej pętli.

for {

if pewienWarunek {

break

}

// kolejne instrukcje}

Poniżej przykład użycia tej instrukcji:

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        if i == 5 {
            fmt.Println("Przerwanie wykonywania pętli")
            break
        }
        fmt.Println("Wartość dla i wynosi:", i)
    }
    fmt.Println("Wyjście z programu")
}

Ten mały program tworzy pętlę for, która powinna być wykonywana do momentu, gdy wartość zmienne “i” będzie mniejsze niż 10. Tym niemniej w kodzie pętli znajduje się instrukcja warunkowa if, która sprawdza czy wartość naszej zmiennej jest różna od 5. Jeśli wartość “i” spełnia ten warunek to pętla kontynuuje swoje działanie, tym samym wyświetlając bieżącą wartość zmiennej. W przeciwny razie (dla “i” równego 5), nastąpi komunikat o przerwaniu działania pętli, a następnie wykonana zostanie instrukcja break. Na końcu programu pojawi się zaś komunikat o zakończeniu całości ("Wyjście z programu"). Tym samym w efekcie tego krótkiego programy ujrzymy następujący efekt:

Wartość dla i wynosi: 0
Wartość dla i wynosi: 1
Wartość dla i wynosi: 2
Wartość dla i wynosi: 3
Wartość dla i wynosi: 4
Przerwanie wykonywania pętli
Wyjście z programu

Skoro mamy instrukcję break, to rzecz jasna występuje jej “antonim” w postaci instrukcji continue. Używana jest ona wtedy, gdy chcemy pominąć pozostałą część dla konkretnej iteracji pętli, ale przy okazji wrócić na jej początek pętli i kontynuować jej działanie, a nie przerywać jak w poprzednim przykładzie.

Podobnie jak w przypadku instrukcji break, instrukcja continue jest powszechnie używana z instrukcją warunkową if. Używając tego samego programu pętli podmienimy instrukcję break na continue:

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        if i == 5 {
            fmt.Println("Pominięta iteracja")
            continue
        }
        fmt.Println("Wartość dla i wynosi:", i)
    }
    fmt.Println("Wyjście z programu")
}

Różnica w używaniu instrukcji continue zamiast instrukcji break polega na tym, że nasz kod będzie kontynuowany pomimo zakłóceń, gdy zmienna i zostanie oceniona jako równoważna 5. Przyjrzyjmy się z resztą naszemu wynikowi:

Wartość dla i wynosi: 0
Wartość dla i wynosi: 1
Wartość dla i wynosi: 2
Wartość dla i wynosi: 3
Wartość dla i wynosi: 4
Pominięta iteracja
Wartość dla i wynosi: 6
Wartość dla i wynosi: 7
Wartość dla i wynosi: 8
Wartość dla i wynosi: 9
Wyjście z programu

I to chyba wszystko, co na początku naszej zabawy z GO powinniśmy wiedzieć o działaniu pętli, a przynajmniej na ten moment niczym ciekawym w tym zakresie nie jestem się podzielić. Tym samym pora zakończyć dzisiejszy wpis. W kolejnym tekście będzie mowa o typach złożonych, czyli o tablicach oraz wycinkach i być może również o mapach.