blog.michalt.pl

Go - język do wszystkiego?

13.06.2021 19:40 Tech
„Przychodzi student informatyki do profesora i mówi: Panie profesorze, wymyśliłem nowy język programowania, który zastąpi 100 pozostałych. Na co odpowiada profesor: Ty idioto! Teraz będziesz miał 101 języków programowania”

Taki żarcik usłyszałem kiedyś podczas studiów informatycznych. Czy celem stworzenia Go było zastąpienie pozostałych języków, nie wiem, ale jeśli tak, to jest on moim zdaniem dobrym do tego kandydatem. Bardzo podobają mi się niektóre koncepcje w nim zawarte - są niezwykle logiczne i rozwiązują wiele niepotrzebnych problemów, które możemy znaleźć w innych językach programowania.

Shellowo czy binarnie – jak wolisz

Kompilator Go pozwala budować źródła do kodu maszynowego, a także uruchamiać je wsadowo. Sam korzystam z obu rozwiązań. Niekompilowany wariant wykorzystuję w obszarach administracyjnych – bardzo nie lubię basha i długo stawiałem na Pythona, jednak Go jest jego godnym następcą.


Pętle i uproszczenia składni – Keep It Simple Stupid

Odnoszę wrażenie, że KISS był istotną zasadą w tworzeniu tego języka. Po co uczyć się kilku słów kluczowych dla różnych wariantów pętli? Po co tworzyć jakieś niepotrzebne sprawdzanie wartości w pętlach nieskończonych? Go podchodzi do tego bardzo rozsądnie – wszystkie z nich tworzy się przy użycia słówka for.


Pętla nieskończona:


for {
   fmt.Println("Neverending stoooory")
}

foreach:


for i, arg := range os.Args {
  fmt.Println( "Argument: "i, ", wartość: "+arg)
}

tradycyjny for:


for i := 1; i<=10; i++{
  fmt.Println(i)
}

while:


for i > 0 {
  fmt.Println(i)
  i-=1
}

Jak zauważyliście, twórcy języka Go zrezygnowali również z nawiasów okrągłych w pętlach – ale i nie tylko, nie uświadczymy ich również w instrukcjach warunkowych. Ponadto, Go jest kolejnym językiem w którym nie musimy kończyć instrukcji średnikiem.


Wbudowany linter w kompilator

Może to być zaskakujące i niektórych programistów zniechęcić. Go dosyć subtelnie, ale jednak pilnuje stylu składni. Bardzo dużym plusem jest fakt, że nie skompiluje ani nie wykona się kod z dociągniętymi nieużywanymi modułami – to bardzo ważne. Oczywiście lintery też raczej tego pilnują.


Statyczne typowanie i autotype

Język Go wykorzystuje statyczne typowanie, a także dedukcje typów. Składnia definiowania zmiennych nieco się różni w przypadku jawnego deklarowania typu i dedukcji:


var number int = 19
var number2 := 44

Warto przypomnieć, że dedukcja typu to nie to samo co typowanie dynamiczne – tu zasada działania jest analogiczna do auto type w C++: zmienna number2 otrzymuje typ integer i nie da się go zmienić.


Pakiety

Jak na każdy nowoczesny język programowania przystało, Go korzysta z pakietowania źródeł. Tu jednak pojawia się pewne fajne udogodnienie. Importować pakiety możemy również bezpośrednio z repozytoriów git. Jak już wcześniej wspomniałem, kod z zaimportowanymi nieużywanymi pakietami się nie uruchomi i nie zbuduje.


OOP i hermetyzacja – tu też jest, ale trochę inaczej i prościej

Zacznijmy od hermetyzacji. W Go hermetyzowane są zarówno interfejsy jak i zmienne, metody oraz funkcje. Tu nie stosujemy kwalifikatorów dostępu, zamiast tego zastosowano konstrukcję nazewniczą: elementy publiczne posiadają nazwę zaczynającą się od wielkiej litery, prywatne zaś z małej. W Go mamy typy podstawowe, aliasy na typy, slice, mapy, interfejsy i struktury. Interfejsy działają podobnie do tych znanych m.in. z języka Java. Istnieje jednak sporo różnic. Możemy na przykład zainicjalizować instancję interfejsu, a także tworzyć puste interfejsy.


Typ interfejsowy tworzy się prosto:


type NaszPublicznyTyp struct {
	przykładowyPrywatnyAtrybut String 
	PrzykładowyPublicznyAtrybut int
}

Wystraszyć nas może tworzenie metod bo pojawia się wskaźnik. Programiści jak słyszą słowo wskaźnik dostają alergii, grypy jelitowej i paranoi - jest to zupełnie niepotrzebne.


func (naszTyp* NaszPublicznyTyp) SumujZAtrybutem(argument int) int {
	return naszTyp.PrzykładowyPublicznyAtrybut + argument
}

Dziedziczenie wygląda identycznie jak implementacja interfejsu:


type Interfejs interface {
	UstawNapis(napis string)
	ZwrocNapis() string
}

type TypNadrzedny struct {
	atrybutPrywatny string
}

type TypKonkretny struct {
	TypNadrzedny
	Interfejs
}

func (naszTyp* TypKonkretny) UstawNapis(argument string)  {
	naszTyp.atrybutPrywatny = argument
}

func (naszTyp* TypKonkretny) ZwrocNapis()  string {
	return naszTyp.atrybutPrywatny
}

Współbieżność – można zrobić to prosto i nowocześnie – goroutines i kanały

Każdą wybraną przez nas funkcję można uruchomić w oddzielnym wątku w tle. Aby móc tego dokonać, należy przed jej wywołaniem użyć słówka go:


go SortujZbiorDanych(dane)

Istotnym elementem programowania współbieżnego jest komunikacja i obsługa zdarzeń. Do tego celu przydatnymi narzędziami są kanały – służą one do komunikacji międzywątkowej. Istnieją 2 albo jak kto woli 3 warianty kanałów – zależnie od sposobu ich przekazania, tj: do odczytu, do zapisu i dwukierunkowe.
Deklarujemy je w następujący sposób:


var wynik chan int = make(chan int)

Tak stworzony kanał możemy przekazać do funkcji działającej w tle, która będzie czekała na przekazanie wartości. Gdy wartość zostanie wysłana do kanału, wykona się pozostała część kodu funkcji.


Oto przykład:


func wyswietlKwadratLiczby(liczba<-chan int) {
  var wynik int <- liczba 
  fmt.Println(wynik*wynik)
}

func main() {
  var liczba chan int = make(chan int)
  go wyswietlKwadratLiczby(liczba)
  liczba <- 9
}

W powyższym przykładzie w funkcji main przekazujemy kanał do zdefiniowanej wcześniej funkcji wyswietlKwadratLiczby, ta w pierwszej linii swojej implementacji oczekuje na przekazanie do kanału wartości, gdy tak się stanie, zostanie wykonana reszta instrukcji, tj: przypisanie wartości do zmiennej wynik i wyświetlenie wartości przypisanej do zmiennej wynik podniesionej do kwadratu. Przekazanie wartości do kanału znajduje się w 3 linii funkcji main.


Tu przekazaliśmy do funkcji wyswietlKwadratLiczby kanał do odczytu, można również przekazać kanał do zapisu: kanal chan <- int.

Dosyć ciekawym narzędziem jest instrukcja select, która działa podobnie do switch – case, operuje jednak na kanałach:


func warianty(wariant1<-chan bool, wariant2 <- bool) {
  select {
    case <- wariant1:
      fmt.Println("Wybrałeś wariant 1")
    case <- wariant2:
      fmt.Println("Wybrałeś wariant 2")
  }
}

W tym przypadku funkcja czeka, aż któryś z kanałów otrzyma wartość, gdy tak się stanie, zostanie wykonany przypisany mu case i nastąpi wyjście z instrukcji select.


Funkcje odroczone

Funkcje odroczone to niesamowite udogodnienie. Wykonywane są na końcu funkcji w której pojawia się ich wywołanie. Co istotne, funkcja taka wywoła się nawet w przypadku wystąpienia awarii pod warunkiem, że jej wywołanie pojawi się przed miejscem wystąpienia błędu. Zaleca się wywoływanie funkcji odroczonych na samym początku bloku kodu. Do ich wywołania wykorzystujemy słówko kluczowe defer.


func zrobCos() {
  defer funkcjaOdroczona()
  /* dalsze instrukcje (...)*/
}

Dlaczego rozwiązanie to jest tak fajne? Między innymi w sytuacji pojawienia się awarii daje nam możliwość zamknięcia pootwieranych połączeń, pousuwania plików tymczasowych lub cofnięcia różnego rodzaju zmian.


Podsumowanie

Przedstawiłem tu zaledwie wierzchołki różnych rozwiązań z języka Go. Oferuje on bardzo wiele. Oprócz wymienionych tu jego cech, moim zdaniem na uwagę zasługują również refleksje i programowanie niskopoziomowe. Go również pozwala obsługiwać wyjątki – choć robi to dosyć niekonwencjonalnie, bo przy użyciu instrukcji warunkowych. Nie zabrakło w nim również funkcji anonimowych i wielu innych ciekawostek do których zgłębienia bardzo Was zachęcam. Jest to jeden z niewielu języków programowania, który ma swoją filozofię i nie próbuje na siłę czerpać z innych jak np. C++, PHP czy Java i nie próbuje być nimi wszystkimi na raz. Twórcy języka stworzyli proste narzędzie, które pozwala skupić się na rozwiązaniach logicznych w pisaniu kodu, a nie na zastanawianiu się nad jego syntaktyką i niepotrzebnie zawiłymi i mnogimi rozwiązaniami. Moim zdaniem Go to język przyszłości i zdecydowanie jest kandydatem mogącym zastąpić pozostałe 100 innych.