Blog

Golang dla Hobbystów. Mapy

Kolejny wpis w ramach wprowadzenia do GO i kolejny tekst o wbudowanych typach złożonych - tym razem na tapet bierzemy mapy.

Golang dla Hobbystów. Mapy

Mapy to nieuporządkowany zbiór par klucz-wartość, w którym wszystkie klucze są od siebie różne (nie mogą się powtarzać w odróżnieniu od powiązanych z nimi wartości). Mapy są szczególnie przydatne w odniesieniu do danych nieustrukturyzowanych, czyli takich gdzie klucze nie są znane w momencie pisania kodu i dostępne będą dopiero podczas działania programu. Choć rzecz jasna nie tylko wówczas można i należy z nich korzystać.

Klucze map w GO mogą być praktycznie dowolnego typu, tym niemniej jego rodzaj musi być określony już na samym początku i każdy klucz musi reprezentować wartości dozwolone z punktu widzenia zadeklarowanego typu. To samo dotyczy wartości, które są z poszczególnymi kluczami powiązane tj. musimy z góry określić ich typ, a następnie trzymać się ściśle tego “zobowiązania” przy dodawaniu kolejnych elementów kolekcji.

W GO zadeklarowanie zmiennej reprezentującej mapę nie powoduje automatycznego utworzenia mapy. Do tego ostatniego konieczne jest wywołanie funkcji make, z którą mieliśmy już okazję zapoznać przy okazji omawiania tablic i wycinków. Spójrzmy na przykład:

mojaMapa := map [string] int

mojaMapa = make(map[string]int)

W pierwszym wierszu mamy do czynienia z zadeklarowaniem zmiennej “mojaMapa”, której typ to ma od ściśle określonym typie zarówno klucz jak i wartość (w naszym przykładzie to odpowiednio: literał łańcuchowy oraz liczba całkowita). Sama deklaracja zaczyna się od słowa kluczowego map, po czym w nawiasach kwadratowych wskazujemy typ dla klucza, który będziemy w tej kolekcji wykorzystywać, by następnie określić jakie wartości możemy do poszczególnych kluczy przypisać.

Po zadeklarowaniu i przypisaniu do zmiennej “mojaMapa” tego rodzaju kolekcji przy pomocy funkcji make, możemy przystąpić do faktycznego przypisania naszej zmiennej odpowiedniej mapy. Oczywiście nie ma potrzeby rozbijania tego na dwa wiersze (jak zostało to pokazane wyżej) i można to wykonać w jednym kroku:

mojaMapa := make(map[string]int)

Gdy już dysponujemy tak spreparowanym “szkieletem” mapy pora najwyższa wypełnić go treścią. W naszym przykładzie utworzymy trzy pary klucz-wartość, które będą odpowiadały lokatorom mieszkania, z którym przyszło pomieszkiwać piszącemu te słowa. Jako klucza użyjemy imion tych mieszkańców, zaś wartością będzie ich obecny wiek:

mojaMapa[“Borciugner”] = 44

mojaMapa[“Szanowna Małżonka”] = 18

mojaMapa[“Nadobny potomek”]=12

To co chyba od razu rzuca się w oczy to fakt, że taki sposób tworzenie map trudno nazwać specjalnie ergonomicznym, a w mojej opinii jest on średnio czytelny. Natomiast jak łatwo się domyślić da się ten zabieg przeprowadzić nieco sprytniej, ale pod jednym warunkiem, który w przypadku mojej familii jest akurat spełniony: musimy od samego początku znać klucze i wartości, jakie tworzona właśnie mapa ma zawierać (oczywiście nic nie stoi na przeszkodzie, by później dokonać stosownych modyfikacji w tym zakresie, ale o tym za moment). W tym celu można użyć tzw. literału mapy, co zostało zaprezentowane niżej:

package main

import "fmt"

func main() {
	lepszaMapa := map[string]int{
		"Borciugner": 44,
		"Szanowna Małżonka": 18,
	"Nadobny Potomek": 12,
	}
	fmt.Println(lepszaMapa)
}

Na dwie rzeczy należy zwrócić w tym miejscu uwagę. Po pierwsze, między kluczem a wartością występuje tutaj znak dwukropkach. Po drugie, przy takim sposobie zapisu, gdzie każda para klucz oraz wartość umieszczona jest w osobnym wierszu, po wypisaniu ostatnie pary należy wstawić przecinek albo zamiast niego od razu zamknąć nawias klamrowy (jak kto woli - mnie bardziej podoba się wersja zaprezentowana w przykładzie).

Dalsze uzupełnianie mapy o kolejne par klucz-wartość będzie już wymagało posługiwania się zapoznaną już notyfikacją z nawiasami klamrowymi, gdzie podajemy nazwę zmiennej, później w nawiasie kwadratowym wpisujemy nazwę nowego klucza i przy pomocy znaku równości wiążemy z nim odpowiednią wartość. Jeśli jako nazwy klucza użyjemy już istniejącej to po prostu nadpiszemy obecną wartość (jak już było wspomniane wcześniej nie może być dwóch identycznych kluczy).

Tej notyfikacji używamy również wówczas, gdy chcemy dotrzeć do wartości ukrytej pod interesującym nas kluczem, czyli chcą wyświetlić wiek autora tego wpisu wystarczy uruchomić następujący kod:

package main

import "fmt"

func main() {
	lepszaMapa := map[string]int{
		"Borciugner":        44,
		"Szanowna Małżonka": 18,
		"Nadobny Potomek":   12}
	fmt.Println(lepszaMapa["Borciugner"])
	}
}

Jako ciekawostkę warto zwrócić uwagę na to, co stanie się w momencie, gdy spróbujemy się odwołać do klucza, który w naszej kolekcji nie istnieje (ot chociażby poprzez zwykłą literówkę). Wówczas bynajmniej się nie dojdzie do “kolapsu” naszej aplikacji, ponieważ kompilator w odpowiedzi przekaże wartość zerową właściwą dla zadeklarowanego typu danych (w naszym przykładzie byłoby to zero, ponieważ mamy do czynienia z typem integer). W ramach testów proponowałbym w wywołaniu funkcji Println zmienić wielkość litery w przekazanej nazwie klucza ("Borciugner") z dużej na małą .

Oczywiście może zdarzyć się i tak, iż w pewnym momencie już po przypisaniu wartości do jakiegoś klucza możemy stwierdzić, że jest on zbędny i tym samym chcieć usunąć te dane z naszej mapy. Wówczas możemy skorzystać - inaczej niż w przypadku tablic - z wbudowanej funkcji delete. Wystarczy przekazać do niej jako argumenty dwie informacje tj. nazwę modyfikowanej mapy, na której taki zabieg chcemy przeprowadzić, oraz kluczy, który planujemy usunąć. Rzecz jasna usunięcia klucza powoduje automatycznie pozbycie się powiązanej z nią wartości. Dla przykładu eksmitujemy z mojego mieszkania jedyną reprezentantkę płci pięknej:

package main

import "fmt"

func main() {
	lepszaMapa := map[string]int{
	"Borciugner":        44,
	"Szanowna Małżonka": 18,
	"Nadobny Potomek":   12,
	}
	fmt.Println(lepszaMapa)
	delete(lepszaMapa, "Szanowna Małżonka")
	fmt.Println(lepszaMapa)
}

Gdyby w pewnym momencie strzeliłby nam do głowy pomysł wyświetlenia każdej pary klucz i wartość w odrębnym wierszu, trzeba byłoby sięgnąć po pętlę, dzięki której będziemy mogli pobrać każdy element takiej mapy. Do tego celu posłuży nam dobrze znana pętla for...range, której używaliśmy do również do przetwarzania elementów tablic i wycinków. Oczywiście tutaj zamiast indeksu jako pierwszą zmienną ustawiamy klucz, zaś druga zmiana odpowiadać będzie wartości z tym kluczem powiązanej.

package main
	import "fmt"
	func main() {
	lepszaMapa := map[string]int{
		"Borciugner":        44,
		"Szanowna Małżonka": 18,
		"Nadobny Potomek":   12,
	}
	for klucz, wartosc := range lepszaMapa {
		fmt.Printf("Lokator %s ma lat: %d\n", klucz, wartosc)
		}
}

Już na sam koniec dzisiejszego wpisu chciałbym zwrócić uwagę na pewien szczegół, który odróżnia mapę od tablic i wycinków, a być może wszystkich innych wbudowanych typów w GO. Warto mieć świadomość występowania tej “osobliwości”, by działania naszego własnego kodu nie zaskoczyło nas w pewnym momencie. Otóż mapy nie podlegają kopiowaniu tak jak ma to miejsce w przypadku tablic czy typów prostych. Najlepiej zobrazować to na przykładzie:

package main

import "fmt"

func main() {
	staryDom := map[string]int{
		"Borciugner":        44,
		"Szanowna Małżonka": 18,
		"Nadobny Potomek":   12,
	}
	nowyDom := staryDom
	fmt.Println(staryDom)
	fmt.Println(nowyDom)
	delete(staryDom, "Szanowna Małżonka")
	fmt.Println(nowyDom)
}

Na początku tworzymy zmienną “staryDom", by następnie przypisać ją do zmiennej “nowyDom”. Tym samym każda z tych zmiennych użyta w ramach funkcji fmt.Println rzecz jasna wyświetli dokładnie tą samą zawartość. Jednak, żeby było ciekawej użyjemy funkcji delete na zmiennej “staryDom” i tym samym ponownie wyprosimy panią domu za drzwi. W efekcie okazuje się jednak, że ta akcja będzie miała dokładnie taki sam skutek w przypadku zmiennej “nowyDom”, czyli wywołując tę zmienną do tablicy naszego terminala przy pomocy fmt.Println, okaże się, że nie ujrzymy bynajmniej informacji o "Szanownej Małżonce".

Dzieje się tak, ponieważ obie zmienne odwołują się tak naprawdę do tej samej przestrzeni w pamięci komputera i dlatego jakakolwiek modyfikacja w tym obszarze będzie skutkowała zmianą w przypadku obu zmiennych, gdyż sięgają one do tego samego zasobu. Jeśli ktoś kiedyś trochę się bawił Pythonem nie będzie tym specjalnie zaszokowany, ale w przypadku GO jest to faktycznie coś nieoczywistego, co stanie się jeszcze bardziej zaskakujące, gdy będziemy starać się opanować zagadnienie wskaźników. Ale nie uprzedzajmy faktów, bo na to przyjdzie jeszcze pora.

Tymczasem dzisiaj to już wszystko. Od siebie dodałbym, że tym sposobem udało się nam zamknąć wprowadzenie do - jak dla mnie przynajmniej - najnudniejszej część wiedzy o GO, czyli o wbudowanych typach danych. Wprawdzie zostają nam jeszcze do omówienia struktury, ale te są akurat wyjątkowo interesujące pod wieloma względami. Zanim jednak do nich przejdziemy trzeba będzie omówić co najmniej tak samo ciekawe zagadnienia funkcji, czym zajmiemy się w kolejnym wpisie (albo wpisach, bo to dość obszerny temat).