Typy

Dzięki typom rozróżniamy właściwości wartości składowanych pod zmiennymi. Np. gdy zmienna jest typu int8 to wiemy, że może zawierać każdą liczbę całkowitą z zakresu od -128 do 127.

Typy pre-deklarowane

W Go jest 12 typów numerycznych, poniżej ich rozpiska:

uint8       liczby całkowite nieujemne zapisywane na 8 bitach (od 0 do 255)
uint16      liczby całkowite nieujemne zapisywane na 16 bitach (od 0 do 65535)
uint32      liczby całkowite nieujemne zapisywane na 32 bitach (od 0 do 4294967295)
uint64      liczby całkowite nieujemne zapisywane na 64 bitach (od 0 do 18446744073709551615)

int8        liczby całkowite zapisywane na 8 bitach (od -128 do 127)
int16       liczby całkowite zapisywane na 16 bitach (od -32768 do 32767)
int32       liczby całkowite zapisywane na 32 bitach (od -2147483648 do 2147483647)
int64       liczby całkowite zapisywane na 64 bitach (od -9223372036854775808 do 9223372036854775807)

float32     liczby zmiennoprzecinkowe zapisane na 32 bitach (8 bitów na wykładnik 23 na mantysę i 1 na znak)
float64     liczby zmiennoprzecinkowe zapisane na 64 bitach (11 bitów na wykładnik 52 na mantysę i 1 na znak)

complex64   liczby zespolone z 32bitową zmiennoprzecinkową częścią rzeczywistą i taką samą częścią urojoną
complex128  liczby zespolone z 64bitową zmiennoprzecinkową częścią rzeczywistą i taką samą częścią urojoną

byte        alias typu uint8

Mamy także typ bool, który przyjmuje wartości true (prawda) i false (fałsz) oraz string.

Stringi, czyli łańcuchy znaków, są niezmienialne, oznacza to, że nie możemy zmienić części stringa, jeśli chcemy zmienić choć jeden znak musimy wygenerować nową wartość.

I na tym się kończy lista typów pre-deklarowanych (czyli takich, które są dostępne od ręki) pozostałe typy takie jak array, struct, slice, chan i interface musimy zadeklarować jako własne, lub posługiwać się ich literałami (tj. wpisywać ich definicję w kod programu) nazywamy je typami złożonymi.

Tworzenie własnych typów i nadawanie im nazw

Typy nazwane tworzymy przez deklarację zaczynającą się od słowa type po nim musi nastąpić identyfikator typu a na końcu sama definicja typu:

type myInt int8

Od teraz możemy używać myInt zamiast int8 w kodzie programu, choć musimy się liczyć z konsekwencjami omówionymi w paragrafie o własnościach typów.

Typy złożone

Typ struct

Struty to typy zawierające sekwencje elementów określonych typów dostępnych za pośrednictwem nazw:

struct {
    a int8
    b int32
}

Wartości struktur o tego typu będą miały dwa pola a (typu int8) i b (typu int32), do których można się odwoływać po kropce. Na przykład:

type point struct {
    x int
    y int
}
p := new(point)
p.x = 1
p.y = 2
fmt.Println(p.x + p.y)//wypisze na ekran 3

Typy funkcyjne

Funkcje też mogą mieć swoje typy. Definicja typu funkcji składa się z słowa kluczowego func i następującej po nim sygnaturze funkcji (czyli liście parametrów i wartości zwracanych). Np. funkcje które przyjmują dwa parametry typu int i zwracają jedną wartość typu int mogą być określone typem:

type intfun func (a, b int) int

O tym czym dokładnie jest sygnatura funkcji wyczytasz więcej w rozdziale o funkcjach.

Typy tablicowe

Tablice to ponumerowane sekwencje elementów określonego typu, tablice mają z góry określoną pojemność, której nie da się zmienić. Typ tablic zawierających 10 bajtów zapiszemy tak:

[10]byte

Czyli każdą deklarację zaczynamy od ustalonej liczby elementów ujętej w nawiasy kwadratowe po której następuje deklaracja typów elementów jakie się w niej znajdują. Napisałem deklaracja typów elementów, ponieważ elementem tablicy może być np. inna tablica:

[10][2]int8

Taki zapis informuje nas o tym, że mamy poczynienia z 10 elementową tablicą 2 elementowych tablic z wartościami typu int8.

Połączmy wiedzę z rozdziału o zmiennych z tym czego dowiedzieliśmy się do tej pory. Zadeklarujemy zmienną typu tablicowego zawierającego 10 elementów typu byte:

var byteArr [10]byte

Jak pamiętamy zmienna byteArr została zainicjowana tzw. wartością "zero-typu", w tym przypadku będzie to tablica wypełniona zerami (gdyż elementy są typu liczbowego a zero typu liczbowego to 0).

Żeby zainicjować zmienną własnymi wartościami musimy się uciec do tzw. literałów (słowo literał będzie się często powtarzać na tych stronach, ciężko od niego uciec, więc polecam się z nim oswoić):

var byteArr [10]byte = [...]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}

Typy wycinków

Wycinki (slice) do złudzenia przypominają tablicę, ale tak na prawdę są indeksowaną sekwencją referencji do elementów tablicy. O samych slicesach więcej przeczytasz w osobnym artykule, tutaj tylko napiszę jak się deklaruje typ slice. A deklaruje się podobnie jak tablice z taką różnicą, że nie określamy jej rozmiaru, czyli nawiasy kwadratowe przed typem elementów pozostają puste. Np. wycinek intów zadeklarujemy tak:

[]int

Typy mapowań

Mapy to typy przyporządkowujące jednym wartościom inne. Definicję rozpoczynamy od słowa map po nim następuje typ kluczy ujęty w nawiasy kwadratowe a na końcu wskazujemy typ wartości. Np. możemy zmapować liczby z ich słownymi odpowiednikami, czyli będziemy chcieli zmapować typ int na string. Taki typ definiujemy następująco:

map[int]string

Typy kanałów

Kanały to typ którego wartości służą do przekazywania informacji tj. można je wysyłać przez kanał i z niego otrzymywać. O szczegółach działania kanałów napiszę w innym artykule, tu się skupię na deklaracji typu kanału.

Kanał może zostać zadeklarowany na 3 sposoby:

  • uniwersalny, można z niego wyciągać i wkładać dane

    chan TYP

  • do którego komunikaty są wysyłane

    chan <- TYP

  • z którego tylko odbieramy komunikaty

    <- chan TYP

Dla ścisłości: zmienna typu chan TYP może być użyta gdy wymagany jest typ <- chan TYP lub chan <- typ, zmienia się tylko możliwość jej wykorzystania - tylko do odbierania lub tylko do wysyłania komunikatów.

Typ interface

Interfejsy to zbiory metod, mówimy że typ spełnia dany interface gdy implementuje wszystkie zebrane w nim metody. Więcej o metodach i interfejsach w artykule o obiektowości. Typ interfejsu deklarujemy następująco: zaczynamy od słowa interface a pomiędzy klamerkami wypisujemy deklaracje metod. Deklaracja metody składa się z nazwy metody i sygnatury funkcji. Np:

interface Stringer {
    String() string
}

Typy wskaźnikowe

Każdy typ ma odpowiadający mu typ wskaźnikowy. Typ wskaźnikowy oznaczamy * przed resztą deklaracji typu. Np. wskaźnik do wartości typu int będzie zadeklarowany następująco:

*int

Warto wiedzieć, że wskaźniki w Go są bezpieczne - nie można ich przesuwać jak w c/c++. Dodatkowo, wartości wskaźnikowych można używać prawie tak samo jak zwykłych bo dereferencja jest robiona w locie i nie trzeba przeprowadzać jej jawnie. Bardzo to wygodne :-)

Własności typów i ich wartości

Dwa typy mogą być w dwóch rodzajach relacji między sobą: są identyczne lub ich wartości są przypisywalne.

Identyczność typów

Nazwane typy mogą są identyczne tylko same ze sobą. Tj. mimo iż deklaracje różnią się tylko nadaną nazwą typu nie spełniają relacji identyczności. Np.

type T0 int
type T1 int

T0 i T1 są nieidentyczne, tak samo jak int jest nieidentyczny z T0

Zadeklarowane w różnych miejscach nienazwane typy mogą być identyczne tylko wtedy gdy odpowiadające im literały są identyczne, tj. mają taką samą strukturę a ich elementy są identycznych typów. Np.:

  • *int nie jest identyczny z int, to zupełnie dwa różne typy
  • [2]int będzie identyczne z innym typem [2]int.
  • [2]int nie jest identyczne z [3]int, bo ma więcej elementów.
  • [2]int nie jest identyczne z type T [2]int, bo literał wymaga od tego drugiego podania typu.
  • [2]*int nie będzie identyczny z [2]int bo *int nie jest identyczny z int
  • typ struct{a, b int} jest identyczny z struct{a int, b int}
  • typ struct{b, a int} nie jest identyczny z struct{a int, b int} różnią się kolejnością pól
  • dwa typy funkcji są identyczne gdy ich typy i kolejność parametrów i wartości zwracanych są identyczne (nazwa nie jest brana pod uwagę przy porównaniu)
  • typy mapowe są identyczne gdy ich typy kluczy i wartości są identyczne
  • dwa typy kanałów są identyczne gdy ich wartości są identycznych typów i mają zgodne kierunki

Przypisywalność

O przypisywalności mówimy wtedy gdy wartość x jednego typu X może być przypisana do zmiennej v typu T. Zachodzi to gdy spełniony jest jeden z warunków:

  • typ x jest identyczny z T
  • gdy X określa taki sam typ jak T i przynajmniej jeden (T lub X) jest nienazwany
  • gdy T jest interfejsem a x spełnia ten interface
  • gdy x jest kanałem typu V oraz T jest typem kanału z identycznymi typami elementów dodatkowo przynajmniej jeden z nich jest typem nienazwanym
  • gdy x jest wartością nil a T jest wskaźnikiem, funkcją, slice, mapą, kanałem lub interfejsem
  • gdy x jest stałą reprezentowalną przez typ T