Golang dla Hobbystów. Zmienne i typy proste

Nie będę ukrywał, że do dzisiejszego wpisu zabierałem się jak przysłowiowy królik do jeża, ponieważ dotyczy on materii niespecjalnie ekscytującej, by nie napisać nudnej. Tym niemniej nie da się pójść dalej, jeśli nie przerobi się tak podstawowych zagadnień jak zmienne i typy proste. Jednak zanim przejdziemy do właściwej części dzisiejszego wpisu chciałbym na początek przybliżyć jedną z najpopularniejszych funkcji wśród osób zaczynających swoją przygodę z GO. Konkretnie chodzi o dostępną w ramach biblioteki standardowej funkcję Printf, którą możemy zaimportować do własnego kodu  z pakietu FMT. Będzie ona potrzebne do zaprezentowania materiału, który chciałbym omówić  w ramach dzisiejszego tekstu.

Funkcja Printf

Funkcja ta jest jedną z kilku dostępnych funkcji w ramach pakietu FMT, które służą do wyświetlania danych wyjściowych, w naszym wypadku będą to komunikaty widoczne w terminalu. W przypadku Printf pierwszym argumentem przekazywanym do tej funkcji jest zawsze jakiś tekst, który zawiera tak zwany kod formatujący w postaci pewnych “znaczników”. W naszym wypadku będzie to najczęściej %v oraz rzadziej %t. W ich miejsce zostanie podstawiona wartość wyrażenia przekazanego jako drugi argument. Natomiast jeśli w tekście pojawi się kilka kodów formatujących, funkcja Printf podmieni je na wartości kolejnych argumentów.

Załóżmy, że mamy trzy zmienne o nazwach a, b i c, którym odpowiednio odpowiadają wartości “jeden”, “dwa” i “trzy” i chcielibyśmy wyświetlić je w postaci jakiegoś ciągu tekstowego, wówczas nas kod mógłby wyglądać następująco:

fmt.PrintF(“Oto przykład listy trzech zmiennych, gdzie pierwsza to %v, druga to %v, zaś trzecia to %v”, a, c, b)

W wyniku wykonania tego polecenia w naszym terminalu powinniśmy zobaczyć następujący komunikat: “Oto przykład listy trzech zmiennych, gdzie pierwsza to jeden, druga to trzy, zaś trzecia to dwa” (zwracam uwagę, że zmienne były podane w kolejności a, c, b).

Jeśli w pierwszym argumencie zamiast znacznika %v użylibyśmy wspomnianego wcześniej znacznika %t, wówczas w miejsce wartości przypisanej do zmiennej zobaczylibyśmy typ danych, które w tej zmiennej są przechowywane. Na ten moment sądzę, że tyle wiedzy wystarczy i uzbrojeni w nią możemy spokojnie przejść do omówienia zmiennych jako takich.

Deklarowanie zmiennych

Do deklarowania zmiennych w GO służy dobrze znane słowo kluczowe var, choć nieco uprzedzając fakty od razu napiszę, że istnieje również alternatywny sposób (tzw. skrócona deklaracja), ale o tym nieco później. Po tym słowie należy rzecz jasna podać nazwę naszej zmiennej, a ponieważ w przypadku GO mamy do czynienia z językiem statycznie typowanym, musimy czy raczej “możemy” również wskazać jakiego rodzaju dane można do naszej zmiennej przypisać.Niżej widać ogólną postać deklaracji oraz przykład utworzenia zmiennej “a”, która ma przechowywać wartości w postaci liczb całkowitych.

var nazwaZmiennej typ danych

var a int

Wyżej napisałem, iż nie musimy wskazać na typ danych, ponieważ twórcy GO nie są w tym zakresie nadmiernymi formalistami i dopuszczają pominięcie jawnego określenia typu danych dla zmiennej pod warunkiem, że deklarując ją od razu przypisujemy do niej odpowiednią wartość. W takiej sytuacji kompilator sobie poradzi z właściwym rozpoznaniem i przypisaniem do zmiennej odpowiedniego typu na podstawie przypisanej wartości. Tym niemniej w jednym i drugim przypadku należy pamiętać, że później do raz utworzonej zmiennej nie możemy przypisać innego typu wartości niż ten, który wprost lub niejawnie został na początku ustanowiony. To może być jeden z największych problemów dla osób, które wcześniej miały doświadczenia wyłącznie z językami dynamicznie typowanymi.

To na co zwróciłbym jeszcze uwagę, to możliwość utworzenia kilku zmiennych w ramach pojedynczej deklaracji, co jest szczególnie wygodnie w sytuacji, gdy od razu podstawiamy pod nie jakieś wartości. Wówczas wystarczy zacząć od słowa kluczowego var, po którym podajemy listę nazw zmiennych, by następnie przypisać do każdej z nich wartości niekoniecznie tego samego typu.

var a, b, c = true, 2.3, “cztery”

By zademonstrować w praktyce działanie kompilatora w tym zakresie po raz pierwszy będziemy mogli posłużyć się wcześniej przywołaną funkcją Printf.

W powyższym kodzie zadeklarowane zostały 4 zmienne o niezbyt wyszukanych nazwach a, b, c i d. Do każdej z nich od razu przepisaliśmy wartości będące odpowiednio literałem, liczbą całkowitą, liczbą zmiennoprzecinkową oraz wartością logiczną odpowiadającą “prawdzie”. Jeśli uruchomimy kod wówczas przekonamy się, że kompilator dokonał właściwego przypisania wartości (string, int, float64, bool).

package main

import "fmt"

func main() {

	var a = "słowo"
	var b = 1
	var c = 1.5
	var d = true

	fmt.Printf("Wartość %v jest typu %T \n", a, a)
	fmt.Printf("Wartość %v jest typu %T \n", b, b)
	fmt.Printf("Wartość %v jest typu %T \n", c, c)
	fmt.Printf("Wartość %v jest typu %T \n", d, d)
}

Co by jednak było ciekawej proponowałbym usunąć którąkolwiek instrukcję wykorzystującą funkcję Printf i ponownie spróbować uruchomić nasz kod. W takim przypadku kompilator zamiast wyświetlić nam pozostałe 3 komunikaty przywita nas informacją o błędzie, że zmienna, dla której usunęliśmy polecenie z Printf, nie została wykorzystana. Otóż kompilator pilnuje – podobnie jak w przypadku importu pakietów – by nie tworzyć nadmiarowych bytów. Tym samym, jeśli zadeklarowałeś zmienną, to musisz ją potem użyć w kodzie, przy czym użycie jak widać nie oznacza przypisania wartości do zmiennej.

Wartości zerowe

Z resztą w GO nie ma czegoś takiego jak zmienna bez przypisanej wartości.  Otóż deklaracja var nie tylko tworzy zmienną konkretnego typu, do której następnie dołączana jest jej nazwa. Każda taka deklaracja ustawia z automatu wartość początkową dla naszej zmiennej, o ile od razu nie przypisaliśmy do niej jawnie jakiejś konkretnej wartości. Ta wartość domyślna dla danego typu danych nazywana jest “wartością zerową” i jest ona znana z góry. I tak dla przykładu: dla wartości liczbowych będzie to zero, zaś dla wartości logicznych “false”. Zresztą zobaczmy to na przykładzie:

package main

import "fmt"

func main() {
	var a string
	var b int
	var c float64
	var d bool

	fmt.Printf("Zmiana a jest typu %T a jej wartość to %v \n", a, a)
	fmt.Printf("Zmiana b jest typu %T a jej wartość to %v \n", b, b)
	fmt.Printf("Zmiana c jest typu %T a jej wartość to %v \n", c, c)
	fmt.Printf("Zmiana d jest typu %T a jej wartość to %v \n", d, d)

}

Dla bardziej złożonych typów danych, takich jak jak tablica, wartość zerowa to nic jednego jak poszczególne wartości zerowe dla wszystkich elementów lub pól występujących w danej strukturze.

Typy proste oraz operatory

Skoro przed momentem wjechał temat typów danych, to warto może chociaż spróbować je wymienić. Z grubsza w GO mamy do czynienia z dość klasycznym zestawem, choć dla amatorów Javascript czy Pythona w paru kwestiach będzie to pewne novum:

  • Strings (string): dane tekstowe, czy jak kto woli “literały łańcuchowe”. Jeśli coś ujmiemy w cudzysłowy to wówczas otrzymamy ten typ wartości.
  • bool: wartości logiczne przyjmujące wartość prawda (true) oraz fałsz (false),
  • Integers (int): liczby całkowite, przy czym Go rozróżnia aż 10 różnych typów liczb całkowitych, a dokładniej: jest pięć typów liczb całkowitych “ze znakiem” (int), czyli takich, które mogą przedstawiać zarówno liczby dodatnie, jak i ujemne, oraz 5 typów, które reprezentują wyłącznie liczby dodatnie (uint). W obu wypadkach dalszy podział wynika z rozmiarów danego zakresu liczby, ponieważ mamy po cztery typy, gdzie jawnie wskazujemy liczbę bitów (int8, int16, int32, int64 oraz uint8, uint16, uint32, uint64) oraz dwa (int, uint), gdzie reprezentacja w pamięci jest dobrana optymalnie dla urządzenia, na którym działa program ;
  • Floating-Point Numbers (float): liczby zmiennoprzecinkowe, podobnie jak w wyżej mamy do czynienia z podziałem wynikającym z adresacji pamięci, ale tutaj mamy do wyboru “tylko” dwa typy, czyli float32 oraz float64 (ten ostatnie typ to domyślna wartość przypisane do zmiennej w przypadku liczby zmiennoprzecinkowej, o ile nie wskażemy inaczej).

Oprócz tych dość oczywistych typów prostych są też takie, które niekoniecznie znane są w przypadku skryptowych języków. I tak mamy jeszcze runy, czyli literały, które zapisywane są przy pomocy znaków apostrofów, choć przyznam szczerze, że na ten moment nie do końca czuję ich przeznaczenie. W przypadku danych numerycznych są jeszcze liczby zespolone (complex64, complex128), ale w tym zakresie czuję się jeszcze bardziej zagubiony niż w przypadku run.

Oczywiście na wszystkich tych danych możemy dokonywać różnych działań przy użyciu podstawowych operatorów. W przypadku operatorów arytmetycznych  GO udostępnia raczej dość typowe rozwiązania co inne języki programowania, czyli +, –, *, / i % (odpowiednio: dodawanie, odejmowanie, mnożenie, dzielenie i modulo). Ten ostatni operator (modulo) oblicza resztę z dzielenia dwóch liczb całkowitych (np. dla operacji 23 % 10 otrzymamy w wyniku 3).

Jeśli chodzi o operatory porównania to na szczęście twórcy GO nie silili się na oryginalność i całość wygląda dość typowo:

“==” równa się

“!=” nie równa się

“<” mniejsze niż

“<=” mniejsze niż lub równe

“>” większe niż

“>=” większe niż lub równe

Nie inaczej jest w przypadku inkrementacji oraz dekrementacji. Ta pierwsza ma postać i++ i w ten sposób dodaje 1 do wartości naszej zmiennej “i”. Jest to równoważne z instrukcją i += 1, która z kolei jest równoważna z instrukcją i = i + 1. Instrukcja dekrementacji wygląda w sposób dość oczywisty, czyli i– i rzecz jasna odejmuje 1 od wartości zmiennej.

Zasięg zmiennych oraz skrócona deklaracja zmiennych

Zasady rządzące tworzeniem nazw zmiennych są równie proste i można je sprowadzić do następujących reguł:

  • nazwa zmiennej musi zaczynać się od litery lub znaku podkreślenia (“_”),
  • potem już można używać dowolnej konfiguracji liter, cyfr oraz znaków podkreślenia,
  • wielkość znaków – jak zapewne się domyślacie – ma znaczenie i to nawet w podwójnym sensie, czyli po pierwsze ten sam zestaw liter będzie tworzył osobne zmienne, jeśli poszczególne znaki różnią się wielkością, dodatkowo użycie dużej litery na początku nazwy sygnalizuje, że w tym wypadku mamy do czynienia ze zmienną, która ma być widoczna z poziomu innych pakietów (można ją importować do plików przypisanych do innych pakietów).

Skoro już o zasięgu zmiennych mowa, to jest on najczęściej wyznaczany za pomocą nawiasów klamrowych. Tym samym wszystkie zmienne globalne, do których chcemy mieć dostęp w każdym momencie, musimy zadeklarować poza zakresem jakiejkolwiek funkcji, czyli na przykład zaraz po sekcji, w której wskazujemy pakiety do zaimportowania. Oczywiście – jak już to przed momentem wspomniałem – jak rozpoczniemy ich nazwę od dużej litery, wówczas taką zmienną będziemy mogli używać w ramach innych pakietów.

Deklaracja skrócona ma oczywiście inną składnię, czyli przede wszystkim pomija słowo kluczowe var oraz konieczność wskazywania na typ danych. W efekcie przyjmuje ona następującą postać:

nazwaZmiennej := wyrażenie

Ze względu na ich zwięzłość i elastyczność krótkie deklaracje zmiennych są używane do deklarowania i inicjowania zdecydowanej większości zmiennych lokalnych. Kluczowe jest jednak to ostatnie stwierdzenie, ponieważ tej formy deklaracji można używać wyłącznie w ramach poszczególnych funkcji, czyli nie ma możliwości powołania do życia w ten sposób zmiennych globalnych. W tym ostatnim przypadku zawsze musi być użyta pełna forma deklaracji z użyciem słowa kluczowego var.

Analogicznie jak w przypadku deklaracji var istnieje możliwość deklarowania i inicjowania wielu zmiennych w jednej i  tej samej krótkiej deklaracji zmiennych:

a, b := 0, 1

Co ciekawa, jeśli któraś z tych dwóch zmiennych została wcześniej zadeklarowana, wówczas użycie zapisu := spowoduje po prostu zmianę wcześniej przypisanej wartości. Tym niemniej krótka deklaracja zmiennych musi jednak zadeklarować co najmniej jedną nową zmienną, więc gdyby w przykładzie wyżej obie zmienne a i b były już wcześniej zadeklarowane, wówczas kompilator zwróci nam błąd.

Wydaj mi się, że w tym momencie wyczerpany został temat zmiennych, przynajmniej jeśli chodzi o podstawowe zastosowania. Niestety tekst poświęcony tak banalnej – wydawałoby się – kwestii rozrósł się do sporych rozmiarów wpisu, dlatego muszę przemyśleć strategię podejścia do kolejnych odsłon tego cyklu i być może podzielić materiał na mniejsze porcje. Natomiast z całą pewnością kolejna odsłona dotyczyła będzie sterowania przepływem działania programu, czyli przyjrzymy się wspólnie instrukcjom warunkowym oraz pętlom.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *