Go dla początkujących - Część 6: Współbierzność i goroutines
Mateusz Kędziora

Witaj w kolejnej części naszego kursu Go! Dzisiaj zajmiemy się jednym z najbardziej ekscytujących i potężnych aspektów języka Go – współbieżnością. Współbieżność pozwala nam na wykonywanie wielu zadań “jednocześnie”, co może znacząco przyspieszyć działanie naszych programów i sprawić, że będą bardziej responsywne.
W Go, kluczowymi elementami współbieżności są goroutines i kanały. W tym artykule wyjaśnimy, czym są te pojęcia, jak ich używać i jak mogą one pomóc w tworzeniu efektywnych aplikacji. Zaczynajmy!
Czym jest Współbieżność?
Zacznijmy od podstaw. Współbieżność (ang. concurrency) to zdolność programu do wykonywania wielu zadań w tym samym czasie. Warto zauważyć, że nie jest to to samo co równoległość (ang. parallelism). Równoległość oznacza faktyczne wykonywanie zadań jednocześnie na różnych rdzeniach procesora, natomiast współbieżność to sposób strukturyzacji programu, który pozwala na przełączanie się między zadaniami, dając iluzję równoczesności.
Go świetnie radzi sobie z współbieżnością, dzięki czemu programy napisane w tym języku mogą efektywnie wykorzystywać zasoby procesora.
Goroutines: Lekkie Wątki Go
Goroutine to funkcja, która jest uruchamiana współbieżnie z innymi funkcjami. Możesz myśleć o goroutines jako o lekkich wątkach. W przeciwieństwie do tradycyjnych wątków, goroutines są znacznie tańsze w utworzeniu i zarządzaniu, co pozwala na uruchomienie tysięcy, a nawet milionów goroutines w jednym programie.
Aby uruchomić funkcję jako goroutine, wystarczy dodać słowo kluczowe go
przed wywołaniem funkcji. Zobaczmy przykład:
package main
import (
"fmt"
"time"
)
func wypiszLiczby(max int) {
for i := 0; i < max; i++ {
fmt.Println("Goroutine:", i)
time.Sleep(100 * time.Millisecond) // Dodajemy małe opóźnienie, aby było widoczne przełączanie
}
}
func main() {
go wypiszLiczby(5) // Uruchomienie funkcji wypiszLiczby jako goroutine
fmt.Println("Main Goroutine: Start")
time.Sleep(1 * time.Second) // Czekamy, aż goroutine wykona swoje zadanie
fmt.Println("Main Goroutine: Koniec")
}
Co robi ten kod?
package main
: Definiuje, że kod znajduje się w głównym pakiecie (czyli jest wykonywalnym programem).import
: Importuje pakietyfmt
(do drukowania) itime
(do zarządzania czasem).func wypiszLiczby(max int)
: Definiuje funkcjęwypiszLiczby
, która przyjmuje argumentmax
(maksymalna liczba) i wypisuje kolejne liczby, zatrzymując się na chwilę pomiędzy każdym wypisaniem.func main()
: Główna funkcja programu.go wypiszLiczby(5)
: Uruchamia funkcjęwypiszLiczby
jako goroutine. Oznacza to, że funkcja ta będzie wykonywana w tle, niezależnie od głównego wątku programu.fmt.Println("Main Goroutine: Start")
: Wyświetla informację o rozpoczęciu głównej goroutine.time.Sleep(1 * time.Second)
: Wstrzymuje wykonanie głównej goroutine na 1 sekundę.fmt.Println("Main Goroutine: Koniec")
: Wyświetla informację o zakończeniu głównej goroutine.
Wynik działania:
Uruchomienie tego programu da wynik podobny do poniższego:
Main Goroutine: Start
Goroutine: 0
Goroutine: 1
Goroutine: 2
Goroutine: 3
Goroutine: 4
Main Goroutine: Koniec
Widzimy, że main goroutine najpierw rozpoczyna wykonywanie, po czym wypisują się wartości w goroutine, a na końcu main goroutine kończy swoje działanie. Należy jednak pamiętać, że kolejność wypisywania wartości z goroutine nie jest deterministyczna. Oznacza to, że za każdym uruchomieniem programu kolejność wykonywania może być inna, ponieważ goroutines wykonują się współbieżnie.
Ważne: Gdy główna funkcja (main goroutine) kończy swoje działanie, program również się kończy, nawet jeśli inne goroutines wciąż działają. Dlatego w powyższym przykładzie musieliśmy dodać time.Sleep
w main
, aby dać goroutine czas na dokończenie zadania.
Kanały: Bezpieczna Komunikacja Między Goroutines
Sama możliwość uruchamiania funkcji jako goroutines to tylko połowa sukcesu. Często potrzebujemy, aby goroutines komunikowały się ze sobą i przesyłały dane. W Go, do tego celu służą kanały (channels).
Kanały to mechanizm, który umożliwia bezpieczne przesyłanie danych między goroutines. Kanał może przesyłać dane określonego typu. Działają one na zasadzie kolejki FIFO (First In, First Out), czyli dane wysłane do kanału są odbierane w tej samej kolejności.
Tworzenie Kanału
Aby utworzyć kanał, używamy słowa kluczowego make
wraz ze słowem kluczowym chan
, oraz typem danych, jaki będzie przechowywany w kanale:
// Kanał przesyłający liczby całkowite
myChannel := make(chan int)
// Kanał przesyłający ciągi znaków
stringChannel := make(chan string)
Wysyłanie i Odbieranie Danych
Do kanału wysyłamy dane za pomocą operatora <-
. Dane odbieramy również za pomocą tego samego operatora:
// Wysyłanie danych do kanału
myChannel <- 10
// Odbieranie danych z kanału
value := <-myChannel
Przykładowy kod z kanałem:
Zobaczmy przykład, który demonstruje wykorzystanie kanałów do komunikacji między goroutines:
package main
import (
"fmt"
"time"
)
func kwadrat(liczba int, channel chan int) {
wynik := liczba * liczba
channel <- wynik // Wysyłamy wynik do kanału
}
func main() {
channel := make(chan int) // Tworzymy kanał liczb całkowitych
liczba := 5
go kwadrat(liczba, channel) // Uruchamiamy goroutine z obliczeniem kwadratu
wynik := <-channel // Odbieramy wynik z kanału
fmt.Printf("Kwadrat liczby %d to %d\n", liczba, wynik)
}
Co robi ten kod?
package main
,import
: Tak samo jak w poprzednim przykładzie, definiuje pakiet główny i importuje niezbędne biblioteki.func kwadrat(liczba int, channel chan int)
: Definiuje funkcjękwadrat
, która przyjmuje argumentliczba
(liczba całkowita) ichannel
(kanał do przesyłania liczb całkowitych). Oblicza kwadratliczby
i wysyła wynik do kanału.func main()
: Główna funkcja programu.channel := make(chan int)
: Tworzy kanałchannel
, który będzie przesyłał dane typuint
.liczba := 5
: Definiuje zmiennąliczba
i przypisuje jej wartość 5.go kwadrat(liczba, channel)
: Uruchamia funkcjękwadrat
jako goroutine, przekazując do niej liczbę i kanał.wynik := <-channel
: Odbiera wynik z kanału i przypisuje go do zmiennejwynik
. W tym momencie program czeka, aż do kanału wpłynie jakaś wartość.fmt.Printf(...)
: Wyświetla wynik.
Wynik działania:
Kwadrat liczby 5 to 25
W tym przykładzie, goroutine kwadrat
oblicza kwadrat liczby i wysyła go do kanału, a main goroutine odbiera ten wynik z kanału. Kanały synchronizują goroutines i zapewniają, że wynik zostanie odebrany dopiero po obliczeniu.
Buforowane Kanały
W poprzednim przykładzie użyliśmy kanału bez bufora. Oznacza to, że wysłanie danych do kanału (np. channel <- wynik
) zatrzymuje goroutine wysyłającą, dopóki inna goroutine nie odbierze danych z kanału.
Możemy jednak tworzyć kanały buforowane, które pozwalają na wysłanie określonej liczby danych do kanału bez natychmiastowego odbierania. Aby utworzyć buforowany kanał, musimy podać pojemność bufora jako drugi argument funkcji make
:
// Buforowany kanał o pojemności 3
bufferedChannel := make(chan int, 3)
Oto przykład wykorzystania buforowanego kanału:
package main
import (
"fmt"
"time"
)
func wysylaj(channel chan int) {
for i := 0; i < 3; i++ {
fmt.Println("Wysyłam:", i)
channel <- i // Wysyłamy dane do kanału
}
close(channel) // Zamykamy kanał
}
func odbieraj(channel chan int) {
for liczba := range channel { // Odbieramy dane z kanału aż do jego zamknięcia
fmt.Println("Odebrano:", liczba)
time.Sleep(500 * time.Millisecond) // Symulujemy prace
}
}
func main() {
channel := make(chan int, 3) // Tworzymy buforowany kanał o pojemności 3
go wysylaj(channel) // Uruchamiamy goroutine wysyłającą dane
go odbieraj(channel) // Uruchamiamy goroutine odbierającą dane
time.Sleep(2 * time.Second) // Pozwalamy goroutines na dokończenie pracy
fmt.Println("Koniec")
}
Co robi ten kod?
func wysylaj(channel chan int)
: Funkcja wysyła trzy liczby do kanału.close(channel)
: Zamyka kanał, informując odbiorcę, że nie będą już przesyłane żadne dane.func odbieraj(channel chan int)
: Funkcja odbiera dane z kanału za pomocą pętlirange
. Pętla ta działa, dopóki kanał nie zostanie zamknięty.channel := make(chan int, 3)
: Tworzy buforowany kanał o pojemności 3.time.Sleep(2 * time.Second)
: Pozwala na dokończenie pracy obu goroutines.
Wynik działania:
Wysyłam: 0
Wysyłam: 1
Wysyłam: 2
Odebrano: 0
Odebrano: 1
Odebrano: 2
Koniec
Buforowanie kanału pozwala goroutine wysyłającej na wysłanie danych, dopóki bufor nie zostanie zapełniony, bez konieczności natychmiastowego odbioru tych danych. Jest to szczególnie przydatne, gdy procesy wysyłania i odbierania działają z różnymi prędkościami.
Praca Domowa
Aby utrwalić zdobytą wiedzę, wykonaj następujące zadanie:
- Napisz program, który uruchomi dwie goroutines.
- Pierwsza goroutine ma za zadanie generować losowe liczby i wysyłać je do kanału.
- Druga goroutine ma za zadanie odbierać te liczby z kanału i obliczać ich pierwiastek kwadratowy.
- Główna goroutine powinna na koniec wypisać, ile liczb zostało wygenerowanych, obliczonych i wyniki obliczeń.
- Użyj buforowanego kanału.
- Przetestuj program z różnymi ilościami liczb generowanych, różnymi pojemnościami kanału i sprawdź, jak to wpływa na szybkość działania.
Podsumowanie
W tym artykule omówiliśmy podstawy współbieżności w Go, a w szczególności goroutines i kanały.
- Goroutines umożliwiają wykonywanie wielu zadań jednocześnie, co zwiększa efektywność programów.
- Kanały umożliwiają bezpieczną komunikację i synchronizację między goroutines, co jest kluczowe w programowaniu współbieżnym.
Zachęcamy do dalszego eksperymentowania z goroutines i kanałami. Im więcej będziesz praktykować, tym lepiej zrozumiesz, jak wykorzystać te potężne narzędzia w swoich programach.
Pamiętaj, aby śledzić kolejne posty z naszego kursu Go. W kolejnych artykułach będziemy omawiać bardziej zaawansowane zagadnienia związane ze współbieżnością i innymi aspektami języka Go.
Przydatne Linki
Aby pomóc Ci w dalszej nauce, oto lista przydatnych linków:
- Oficjalna Dokumentacja Go:
- A Tour of Go - Interaktywny kurs wprowadzający do Go, z sekcją o współbieżności.
- Effective Go - Dokument zawierający wskazówki dotyczące pisania dobrego kodu Go, w tym rozdział o współbieżności.
- Go Documentation - Pełna dokumentacja języka Go.
- The Go Memory Model - Szczegółowe informacje o modelu pamięci w Go, przydatne przy zaawansowanej pracy ze współbieżnością.
Dziękuję za poświęcony czas i zapraszam do kolejnych artykułów!
Polecane artykuły
Docker vs Kubernetes: Który dla Ciebie w 2025?
Docker i Kubernetes objaśnione! Która technologia lepsza dla początkujących w 2025? Porównanie, przykłady i przyszłość.
Mateusz Kędziora
DevOps: Automatyzacja zadań sysadmina dla programistów
Zautomatyzuj pracę sysadmina w środowisku DevOps! Praktyczne przykłady, skrypty, Ansible, Terraform, Prometheus i Grafana.
Mateusz Kędziora
Automatyzacja Linux/macOS z Bash: Praktyczny Przewodnik
Zacznij automatyzować system Linux/macOS z Bash! Dowiedz się, czym jest Bash, jak pisać skrypty i używać podstawowych komend.
Mateusz Kędziora