Blog

Golang dla Hobbystów. Struktura aplikacji

Pierwsza odsłona kursu Golang dla amatorów w świecie kodowania. Zaczynamy od omówienia ogólnej struktury aplikacji pisanych w GO.

Golang dla Hobbystów. Struktura aplikacji

Język GO, którego nadal w bólach się uczę, zdążył już mnie zaskoczyć w paru miejscach. Warto jednak zaznaczyć i zarazem przypomnieć, iż moje dotychczasowe bardzo skromne doświadczenie z kodowaniem sprowadzały się w całości do niezbyt ambitnych wprawek w Pythonie (częściej) oraz JavaScripcie (rzadziej). W związku z tym przyzwyczajony byłem, że kolejność pisania kodu jest kluczowa, ponieważ interpreter leci linijka po linijce po naszym skrypcie, czyli na ten przykład z punktu widzenia kolejności wierszy deklaracja danej funkcji musi poprzedzać jej wywołanie i to samo dotyczy pozostałych obiektów. W przypadku GO jest trochę inaczej, ponieważ mamy tutaj do czynienia z językiem kompilowanym, więc organizacja kodu w pewne logiczne całości ma drugorzędne znaczenie.

Oczywiście to ostatnie stwierdzenie jest prawdziwe tylko do pewnego stopnia, poniewaz taka taza nie oznacza automatycznie, że w przypadku tworzenie aplikacji przy pomocy GO nie uświadczymy żadnej struktury i w gruncie rzeczy wszystko w tym zakresie wolno - jest to rzecz jasna fałszywe przekonanie i gdyby okazało się prawdą to takie podejście byłoby skrajnie nierozsądne. Jednak co więcej: na pewnym elementarnym poziomie struktura każdej aplikacji napisanej w GO jest bardzo podobna czy wręcz identyczna. Jest to szczególnie widoczne w przypadku mini-aplikacji wykonujących jakieś bardzo podstawowe działania w rodzaju programu doskonale znanego wszystkim adeptom kodowania, który ma za zadanie przywitanie się ze światem. Oto przykład kodu popełnionego w GO:

package main

import (
"fmt"
)

func main() {

fmt.Println("Witaj Świecie!")

}

Widoczny wyżej kawałek kodu jest najprostszym, ale zarazem podstawowym sposobem tworzenia kodu w GO. Widoczne są w nim 3 kluczowe słowa, czyli package, import oraz func, z którymi będziemy mieć okazję zetknąć się w niemal każdym pliku GO. W tym tekście chyba najwięcej miejsca poświęcimy pierwszemu z nich, ponieważ nie będziemy już raczej mieć sposobności, by wrócić w przyszłości do tego zagadnienia, zaś w przypadku dwóch pozostałych do tematu jeszcze wrócimy przy okazji omawiania chociażby zmiennych czy funkcji.

PAKIETY (package)

Każdy plik zawierający nawet najmniejszą część część kodu, który finalnie składał się będzie na naszą aplikację napisaną w GO, musimy należeć do jakiegoś pakietu. Stąd zawsze na początku takiego pliku musimy zadeklarować ową przynależność właśnie przy pomocy słowa kluczowego package (w przykładzie wyżej nasz pakiet nazywa się main). Oczywiście w przypadku naszego kursu, gdzie zamieszczone wprawki w programowaniu nie będą dużo bardziej złożone niż powyższy przykład, nie ma większego sensu dzielenie kodu nie będzie miało większego sensu, tym samym obowiązek deklarowania przynależności jakiegoś fragmentu kodu do konkretnego pakietu będzie wyglądało trochę na sztukę dla sztuki. Tym niemniej trzeba pamiętać i od razu należy to podkreślić, iż taka deklaracja na początku każdego pliku jest wymagana. W przeciwnym razie kompilator zawsze zwróci nam błąd. Jednakże w przypadku bardziej ambitnych projektów możemy chcieć z różnych powodów rozpisać naszą aplikację na większą liczbę plików, a te ostatnie dodatkowo podzielić na odrębne pakiety. Dlatego mimo wszystko już teraz warto wiedzieć jaka z grubsza rządzi tym logika.

Po pierwsze pakiety tworzą wydzieloną przestrzeń nazw dla naszych deklaracji, tak zmiennych jak i funkcji. Rzecz bardziej po ludzku stawiając: jeśli ten sam pakiet rozpisany został na większą ilość plików, to w każdy z nich mamy bezpośredni dostęp do funkcji czy zmiennych, które zostały zadeklarowane w pozostałych plikach z tej grupy. Pisząc “bezpośrednim dostępie” idzie o to, że nie musimy w jakiś specjalny sposób ich przywoływać czy też importować: po prostu w ramach tego samego pakietu używamy tych obiektów, tak jakbyśmy cały czas pracowali na jednym pliku.

Kolejną rzeczą, o której należy pamiętać, to konieczność umieszczania plików z jednego pakietu w tym samym, dedykowanym dla tego pakietu folderze. Tym samym dla każdego pakietu tworzymy osobny katalog i tam dopiero tworzymy pliki z kodem przynależnym do danej paczki. Oczywiście te różne pliki będą miały siłą rzeczy inne nazwy, ale wspólny folder oraz deklaracja package na początku każdego z nich mówi kompilatorowi, jak powinien z nimi wszystkimi postępować.

Kończąc ten wątek warto jeszcze wspomnieć o jednej rzeczy, choć trochę więcej będzie na ten temat nieco dalej przy okazji omawiania funkcji main. Otóż pakiet main jest pod pewnymi względami wyjątkowy i tej nazwy bynajmniej nie wybraliśmy ze względu na pewną konwencję. Znajdziemy go w przypadku kodu źródłowego dla każdego programu w GO popełnionego, ponieważ tak naprawdę to od niego zaczyna się kompilacja każdej aplikacji, a dokładniej od pewnego szczególnego elementu, który w nim się znajduje.

IMPORTOWANIE

Jeśli nasza aplikacja składa się z większej liczby pakietów (naszego autorstwa lub pochodzących z zewnętrznych źródeł), wówczas musimy je zaimportować w obręb naszego projektu, jeśli chcemy korzystać z dostępnych tam funkcji czy zmiennych. Do tego właśnie służy słowo kluczowe import. W przykładzie przywołanym wyżej skorzystaliśmy akurat z biblioteki standardowej, a konkretnie pakietu fmt (stałego bywalca przyszłych wpisów), który dostarcza funkcje sformatowanego wejścia i wyjścia. W przypadku innych źródeł rzecz jasna musimy wskazać bardziej szczegółowo, gdzie należy szukać interesującej nas zawartości.

Warto jednak podkreślić fakt, że dokonanie importu danego pakietu nie powoduje, że mamy dostęp do wszystkiego, co w jego ramach zostało napisane. Tak naprawdę to autor tej importowanej paczki określa - w dość prosty sposób, o czym będzie przy okazji wpisów o zmiennych i funkcjach - które elementy umieszczone w ramach zawartego tam kodu są “publicznie” dostępne dla innych pakietów.

Kolejną rzeczą, na którą musimy zwrócić uwagę, to wstrzemięźliwość w importowaniu zewnętrznych pakietów. W przypadku Pythona nieraz spotkałem się z sytuacją, gdy autor ładował do swojego kodu jakąś bibliotekę, choć później w skrypcie nie wykorzystał jej w żaden sposób. Często robione jest to w myśl zasady, że być może kiedyś przyda ona się przyda jak wreszcie twórca dołoży jakąś opcję, której nie miał czasu teraz zaimplementować lub część autorów po prostu kopiuje bezmyślnie gotowy kawałek kodu z wszystkim zależnościami, z których w praktyce wykorzysta tylko pewną część. W przypadku GO taka praktyka nie przejdzie. Jeśli importujemy jakiś pakiet, a później z jego zawartości w żaden sposób nie skorzystamy, wówczas kompilator zamiast pliku wynikowego zaserwuje nam błąd w miejsce spodziewanego pliku wynikowego. W tym miejscu twórcom GO chodzi o optymalizację procesu kompilacji bez sztucznego nadmuchiwania aplikacji zależnościami, które do niczego następnie nie są wykorzystywane, co tylko zajmuje czas i zasoby.

Tym niemniej jeśli już zaimportowaliśmy jakiś zewnętrzny wobec naszego pakiet, wówczas wywołując na ten przykład interesującą nas funkcję musimy przed jej nazwą dodać również nazwą importowanego pakietu i kropkę - oj niepocieszeni będą wszyscy fani Pythona, którzy mają - dla mnie osobiście wyjątkowo nieznośną - manierę importowania wprost jakiejś funkcji czy klasy z innego modułu. W naszym przykładzie takie podejście widać w przypadku wywołania funkcji Println, która została poprzedzona nazwą pakietu, czyli mamy zapisane to jako fmt.Println(). Tym sposobem możemy bezpiecznie korzystać z funkcji o tej samej nazwie bez obawy, że jedna nadpisze drugą, pod warunkiem, że pochodzą one z różnych pakietów.

Kończąc już wątek importowania pakietów chciałbym zwrócić jeszcze uwagę na jedną kwestię. Chodzi konkretnie o sposób zapisu instrukcji odpowiedzialnej za import pakietów. Zwracam na to uwagę, ponieważ nierzadko można spotkać się z nieco innym sposobem notacji takiego importu, zwłaszcza w sytuacji, gdy pobierany jest - jak ma to miejsce w naszym przypadku - tylko jeden pakiet zewnętrzny. Nic nie stoi na przeszkodzie, by pominąć wówczas nawiasy klamrowe i zapisać całość jako import “fmt”. Tym niemniej przy takim podejściu chcąc zaimportować większą ilość bibliotek - co od pewnego momentu staje się chlebem powszednim każdego programisty GO - trzeba będzie wywołać słowo kluczowo import tyle razy, ile bibliotek czy pakietów będziemy potrzebować. W przypadku notacji z nawiasami klamrowymi wystarczy po prostu dodawać kolejne pakiety pomiędzy nawiasami, rozdzielone enterem, czyli jeden pod drugim. Jest to moim zdaniem nie tylko wygodniejsze rozwiązanie, ale sprawia ono, że kod robi się nieco czytelniejszy.

W tym miejscu pokusimy się o krótkie podsumowanie dwóch pierwszych paragrafów niniejszego tekstu. Z tego co zostało wyżej napisane zapamiętać wystarczy:

FUNKCJA MAIN

To wyjątkowa funkcja w ramach GO. Znajdziemy ją w przypadku kodu źródłowego dla jakiegokolwiek programu napisanego w tym języku niezależnie od wielkości i złożoności aplikacji. Co więcej: mimo że natkniemy się na nią niechybnie, to nigdy nie zobaczymy jej wywołania. Ta dziwna sytuacja staje się klarowna w momencie, gdy uświadomimy sobie rolę jaką ta funkcja spełnia w przypadku programowania w GO.

Jak już wspomniałem wcześniej, GO to język, którego kod źródłowy kompilowany jest na język maszynowy, czyli popularne i lubiane zera oraz jedynki. Kompilator musi jednak wiedzieć, od którego miejsca musi zacząć wykonywać swoją pracę i to właśnie funkcja main spełnia rolę takiej rozbiegówki, czyli tam bez wyjątku zaczyna się wykonywanie naszego programu. Może nawet bardziej trafne byłoby stwierdzenie, że program realizuje dokładnie to, co wykonuje funkcja main. Rzecz jasna w przypadku złożonych projektów funkcja main będzie wywoływać inne funkcje (również z innych pakietów), które same zależą od działania innych elementów całości kodu. Należy pamiętać przy tym - co już wcześniej zostało zasygnalizowane - że funkcja main musi być zadeklarowana w pakiecie o takiej samej nazwie, czyli main.

OSTATNIE SŁOWO

Biorąc zakres praktycznych ćwiczeń, jakie zostaną przedstawione w kolejnych odsłonach niniejszego cyklu, będącego wprowadzeniem do GO, wydaje się, że zagadnieniom dzisiaj omawianym zostało poświęcone przesadnie dużo miejsca. Tym niemniej nie chciałbym w kolejnych wpisach wracać już do tego zagadnienia, chyba że w przyszłości zajmę się ekstra tematem modułów, które to - jako mechanizm zarządzania projektem - pojawiły się w języku GO stosunkowo niedawno. Prawda jest taka, że w przypadku hobbystów w rodzaju piszącego te słowa zapewne rzadko pojawiać się będzie potrzeba dzielania kodu źródłowego na kilka pakietów czy plików w ramach jednej paczki.

Skoro już jednak mamy za sobą ten wycinek tworzenie oprogramowania w GO, wspomnę na sam koniec, czym zajmiemy się w kolejnym tekście. Będzie ona dotyczyć zmiennych, tj. ich sposobu deklarowania, zasięgu i praktycznych wskazówek. Chciałbym przy tej okazji poruszyć kwestie związane z wbudowanymi typami danych, ale tylko tymi, które zwyczajowo nazywa się prostymi (w odróżnieniu od bardziej złożonych struktur jak chociażby tablice). Zobaczymy jak jednak wyjdzie, bo choć nie chciałbym poświęcać tym ostatnim osobnego tekstu, to jednocześnie zależy mi, by kolejne wpisy nie były dłuższe niż ten dzisiejszy, ale też z grubsza wyczerpywały temat. Jednak przekonanym się o tym za kilka tygodni i tymczasem do zobaczenia.