CodeGym /Kursy /Go SELF /Typy całkowite — int, int64, uint

Typy całkowite — int, int64, uint

Go SELF
Poziom 7, Lekcja 1
Dostępny

1. Wprowadzenie

Kiedy dopiero zaczynasz programować, chcesz, żeby „liczba była liczbą”: wpisałeś 10 — więc dziesięć, jakie jeszcze mogą być opcje? Rozumiem. Ale komputer nie żyje w świecie abstrakcyjnej matematyki, tylko w świecie bitów i skończonych zakresów. Każdy typ całkowity ma granice: minimalną i maksymalną wartość, jaką może przechować. Wybór typu to sposób, by z góry powiedzieć kompilatorowi: „tu mam licznik, on nie będzie ogromny”, albo „tu są pieniądze w groszach, nie chcę przepełnień”, albo „tu nie występują wartości ujemne”.

W Go typ to nie ozdoba dla urody. To część poprawności. I dobry wybór typu zwykle czyni kod bardziej zrozumiałym niż komentarz w rodzaju // tutaj na pewno nie będzie minusa, serio-serio.

Liczby ze znakiem i bez znaku: int i uint

Upraszczając, int to „liczba całkowita, która może być ujemna”, a uint to „liczba całkowita, która nie może być ujemna”. Ale za tą prostotą kryje się ważny szczegół: typy ze znakiem i bez znaku inaczej wykorzystują te same bity. Dlatego to samo „miejsce w pamięci” można interpretować albo jako zakres z minusami, albo jako zakres tylko od zera wzwyż.

Żeby było łatwiej to ogarnąć, trzymaj w głowie taki obraz:

int (ze znakiem):   ... -3  -2  -1   0   1   2   3 ...
uint (bez znaku):             0   1   2   3   4 ...

I od razu pojawia się przydatna życiowa logika.

Jeśli wartość może być ujemna (temperatura, saldo konta, różnica między dwiema liczbami, „błąd” jako -1 — chociaż tak robić nie trzeba), to uint po prostu nie pasuje znaczeniowo.

Jeśli wartość nie może być ujemna (liczba czegoś, rozmiar czegoś), to jeszcze nie znaczy, że uint automatycznie jest najlepszym wyborem. Dlaczego? Bo w Go bardzo wiele „rozmiarowych” rzeczy (na przykład len(...)) używa int, i łatwo się do tego dostosować.

2. int: domyślny typ roboczy

Gdy piszesz zadania edukacyjne, liczniki pętli, indeksy, liczbę znaków, liczbę prób, wiek użytkownika — najczęściej twoim najlepszym przyjacielem będzie int. To typ „natywny” dla platformy: Go wybiera taki rozmiar int, który jest naturalny dla procesora i systemu operacyjnego.

I tutaj pojawia się niuans, który warto zapamiętać raz, a potem po prostu z tym żyć: int bywa 32-bitowy albo 64-bitowy, zależnie od platformy. Dlatego int to nie „zawsze ogromny” i nie „zawsze mały”, tylko „dopasowany do platformy”.

W Go jest gotowa podpowiedź: strconv.IntSize to liczba bitów w typie int na bieżącej platformie.

Krótki przykład diagnostyczny:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	fmt.Println("int size:", strconv.IntSize, "bits") // np.: int size: 64 bits
}

Dlaczego int jest tak popularny, mimo że ma „pływający” rozmiar?

Bo idealnie pasuje do zwykłej logiki programu: pętle, indeksowanie, zliczanie, większość szkolnej arytmetyki. I, co ważne, standardowa biblioteka często oczekuje właśnie int w „miejscach związanych z rozmiarem” (na przykład długość łańcuchów zwracana przez len to int).

Mini-przykład, który dobrze „wbija” int do głowy jako typ liczników:

package main

import "fmt"

func main() {
	s := "hello"
	n := len(s)                    // n ma typ int
	fmt.Printf("%v (%T)\n", n, n)  // 5 (int)
}

4. int64: gdy potrzebny jest stały 64-bitowy zakres

Jeśli int to wygodny typ „codzienny”, to int64 jest narzędziem wtedy, gdy chcesz stały rozmiar i duży zakres. Zawsze ma 64 bity, niezależnie od tego, gdzie uruchomiona jest program. To przydatne w sytuacjach, gdy przechowujesz potencjalnie duże liczby albo gdy format danych ma być identyczny na różnych maszynach.

Ważne: nie musisz „wszędzie pisać int64, bo jest większy”. To automatycznie nie czyni programu lepszym. Ale istnieje klasa zadań, gdzie int64 naprawdę jest „trafniejszy znaczeniowo”.

Dobre kandydaty na int64:

1) Pieniądze w najmniejszych jednostkach, na przykład w groszach/cents. Tu kluczowa idea brzmi: pieniądze często lepiej przechowywać jako liczby całkowite (na przykład „12345 groszy”), żeby nie mieć problemów z dokładnością float. A suma pieniędzy w groszach może dość szybko urosnąć.

2) Duże liczniki i sumy: łączna liczba wyświetleń, łączna liczba bajtów, duże wartości „od początku istnienia”.

3) Liczby przychodzące ze świata zewnętrznego (umownie: „ID użytkownika”, „numer transakcji”), jeśli wiadomo z góry, że są 64-bitowe. Nawet bez znajomości przyszłych tematów: spotkanie „ID na 18 cyfr” jest całkiem normalne.

Mini-przykład: zdefiniujmy „saldo w groszach” jako int64, żeby nie martwić się o zakres:

package main

import "fmt"

func main() {
	var balanceCents int64 = 12500
	balanceCents = balanceCents - 1999
	fmt.Println(balanceCents) // 10501
}

5. uint: kiedy naprawdę ma sens

uint to bezznakowy typ całkowity „pod platformę” (jak int, tylko bez minusa). Czyli też może mieć 32 albo 64 bity. Jego sens: przechowywać wartości nieujemne z maksymalnie dużym zakresem dla danego rozmiaru.

I tutaj jest pułapka: „skoro nie ma liczb ujemnych, to bierzemy uint”. W praktyce często prowadzi to do bardziej złożonego kodu. Główna przyczyna jest prosta: w realnej logice programów liczby ujemne czasem pojawiają się „po drodze”. Na przykład liczysz różnicę między dwiema wielkościami, zmniejszasz licznik, robisz x - 1 — i nagle odejmowanie staje się źródłem niespodzianek, jeśli x to uint.

Dlatego dla początkującego i dla zwykłej „logiki aplikacyjnej” zasada jest taka: domyślnie bierz int, a uint używaj wtedy, gdy masz wyraźny sens albo wymaganie.

Typowe sytuacje, w których uint pojawia się naturalnie:

  • praca z maskami bitowymi i rzeczami niskopoziomowymi (nie rozkładamy tego dziś, ale będziesz się z tym spotykać);
  • gdy API (twoje albo biblioteki) już wymaga uint, a ty się do tego dostosowujesz;
  • gdy wyraźnie modelujesz „nieujemny identyfikator/rozmiar” i jesteś pewien, że w obliczeniach nie pojawią się ujemne wartości pośrednie.

Jest też jeden niuans, który wygląda jak „magia kompilatora”, ale w rzeczywistości to po prostu ścisłe reguły. Domyślnie stała numeryczna 17 ma typ int, a nie uint. Dla stałych całkowitych „domyślny typ” to int, a stałe nie mają „domyślnego typu = unsigned”.

Dlatego jeśli potrzebujesz uint, musisz jawnie powiedzieć to w kodzie:

package main

import "fmt"

func main() {
	u := uint(17)
	fmt.Printf("%v (%T)\n", u, u) // 17 (uint)
}

Taka jawna deklaracja typu to normalna praktyka; sprawia, że kod jest bardziej uczciwy. To dokładnie ten przypadek, kiedy Go mówi: „nie będę zgadywać za ciebie”. I to dobrze.

6. Rozmiar i zakres: jak oceniać typ

Gdy wybierasz typ całkowity, warto myśleć nie „na pewno wystarczy”, tylko „jaki to ma zakres”. Każdy typ całkowity to określona liczba bitów, a więc określona liczba możliwych wartości.

Pomocna ściąga wygląda tak:

Typ Znak Rozmiar Idea zakresu
int
ze znakiem 32 lub 64 bity „wygodny typ roboczy”
uint
bez znaku 32 lub 64 bity „wygodny typ roboczy bez minusa”
int64
ze znakiem 64 bity „stały szeroki”

Węższych typów, takich jak int8 i uint8, dziś nie omawiamy, ale jako ilustracja zakresów są bardzo przydatne. Na przykład int8 ma zakres od -128 do 127, a uint8 — od 0 do 255. To pomaga poczuć ideę „typ ogranicza możliwe wartości”, tylko na małych liczbach, gdzie granice łatwo zapamiętać.

Jeśli teraz pomyślałeś: „to czemu nie pisać wszędzie int64, skoro ma duży zakres?” — to normalne pytanie. Ale w Go ważne jest nie tylko „czy liczba się mieści”, lecz także czytelność, spójność typów w wyrażeniach i to, jakich typów oczekują funkcje standardowej biblioteki. Jeśli masz licznik pętli, int będzie wyglądał naturalnie. Jeśli masz pieniądze w groszach albo duży licznik narastający — int64 będzie bardziej logiczny.

7. Przykład: FinTrack i wybór typów

Żeby to nie pozostało teorią, kontynuujmy naszą edukacyjną linię: mały program konsolowy FinTrack (umowny „budżetowy rejestr”). Na razie potrafi odczytać przychód i wydatek oraz wypisać saldo. Naszym celem nie jest dziś zrobienie „idealnej księgowości”, tylko dobranie typów tak, aby program był przewidywalny.

Uzgodnimy prostą zasadę danych: pieniądze wpisujemy w groszach (albo centach), żeby pozostać przy liczbach całkowitych i nie kłócić się z float. To od razu popycha nas w stronę int64: saldo może być ujemne (czyli jesteśmy „na minusie”), a zakres chcemy szeroki.

Przykład minimalnego kodu:

package main

import "fmt"

func main() {
	var incomeCents int64
	var expenseCents int64

	fmt.Scan(&incomeCents, &expenseCents)

	balanceCents := incomeCents - expenseCents
	fmt.Println(balanceCents) // np.: 10501
}

Zwróć uwagę, że int też może się tu pojawić, ale w innym miejscu: jeśli później zechcemy na przykład „powtórzyć wprowadzanie N razy” albo policzyć liczbę operacji, naturalny będzie licznik int w pętli. Natomiast same pieniądze jako wielkość, która może rosnąć i być ujemna, żyją w int64.

I jeszcze jeden mały nawyk, który ratuje przed pomyłkami: czasem warto tymczasowo wypisać typ zmiennej przez %T, żeby upewnić się, że przypadkiem nie dostałeś int tam, gdzie oczekiwałeś int64.

package main

import "fmt"

func main() {
	var incomeCents int64 = 10000
	fmt.Printf("%v (%T)\n", incomeCents, incomeCents) // 10000 (int64)
}

8. Typowe błędy przy wyborze int, int64, uint

Błąd nr 1: „Dajmy wszędzie uint, bo liczb ujemnych nie ma”.

W realnej logice programu wartości ujemne często pojawiają się jako wynik pośredni: różnica, zmniejszenie, korekty, „dodaj/odejmij”. Jeśli na siłę zrobiłeś wszystko jako uint, albo zaczniesz ciągle konwertować typy (i denerwować się na kompilator), albo pewnego dnia dostaniesz bardzo dziwne zachowanie przy odejmowaniu blisko zera. uint jest dobry tam, gdzie naprawdę oddaje sens, a nie tam, gdzie „chcemy zakazać minusa”.

Błąd nr 2: oczekiwać, że literał 17 sam stanie się uint.

Dla stałych całkowitych typ domyślny to int, a stałe nie mają „automatycznie unsigned-owej wersji”. Dlatego u := 17 to int, a nie uint. Jeśli potrzebujesz uint, musisz jawnie napisać uint(17) albo zadeklarować zmienną jako var u uint = 17.

Błąd nr 3: „int zawsze ma 64 bity, więc wszystko gra”.

Na wielu współczesnych maszynach tak właśnie będzie, ale z definicji int zależy od platformy: 32 albo 64. Jeśli piszesz kod, w którym ważny jest duży zakres albo stały format, lepiej użyć int64. A żeby po prostu sprawdzić, jaki int masz teraz, służy strconv.IntSize.

Błąd nr 4: używać int64 wszędzie „na wszelki wypadek” i dostać bałagan typów.

int64 sam w sobie nie jest zły. Ale gdy w kodzie wymieszają się int, int64 i uint, zaczynają się ciągłe niezgodności typów w wyrażeniach i zbędne konwersje. Zasada „domyślnie int, a int64 tam, gdzie potrzebny” zwykle daje czystszy kod niż „wszystko sprowadzimy do int64 i zapomnimy”.

Błąd nr 5: wybierać typ „pod kątem pamięci”, a nie sensu.

Początkujący czasem myślą: „wezmę int8, bo oszczędza pamięć”. W praktyce oszczędzasz dosłownie grosze, a bardzo zwiększasz szansę, że uderzysz w granice zakresu. Wąskie typy (int8, uint8) są przydatne w specjalnych zadaniach i jako demonstracja zakresów (na przykład int8 od -128 do 127, uint8 od 0 do 255). W zwykłej logice częściej przeszkadzają, niż pomagają.

1
Zadanie
Go SELF, poziom 7, lekcja 1
Niedostępne
Paszport liczb
Paszport liczb
1
Zadanie
Go SELF, poziom 7, lekcja 1
Niedostępne
Licznik nagród
Licznik nagród
1
Zadanie
Go SELF, poziom 7, lekcja 1
Niedostępne
Saldo portfela
Saldo portfela
1
Zadanie
Go SELF, poziom 7, lekcja 1
Niedostępne
Wolne sloty
Wolne sloty
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION