Basic Types
Go's Type Systemβ
Go is a statically typed language. Every variable's type is determined at compile time, and there are no implicit type conversions. To operate on values of different types, you must explicitly convert them.
This philosophy catches type-related bugs at compile time, dramatically reducing runtime errors.
Integer Typesβ
Signed Integersβ
| Type | Size | Range |
|---|---|---|
| int8 | 1 byte | -128 to 127 |
| int16 | 2 bytes | -32,768 to 32,767 |
| int32 | 4 bytes | -2,147,483,648 to 2,147,483,647 |
| int64 | 8 bytes | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
| int | Platform dependent | 32-bit system: int32, 64-bit system: int64 |
Unsigned Integersβ
| Type | Size | Range |
|---|---|---|
| uint8 | 1 byte | 0 to 255 |
| uint16 | 2 bytes | 0 to 65,535 |
| uint32 | 4 bytes | 0 to 4,294,967,295 |
| uint64 | 8 bytes | 0 to 18,446,744,073,709,551,615 |
| uint | Platform dependent | 32-bit system: uint32, 64-bit system: uint64 |
| uintptr | Platform dependent | Large enough to hold a pointer |
package main
import (
"fmt"
"math"
)
func main() {
// Print the maximum value of each type
fmt.Println("int8 max:", math.MaxInt8) // 127
fmt.Println("int16 max:", math.MaxInt16) // 32767
fmt.Println("int32 max:", math.MaxInt32) // 2147483647
fmt.Println("int64 max:", math.MaxInt64) // 9223372036854775807
fmt.Println("uint8 max:", math.MaxUint8) // 255
fmt.Println("uint16 max:", math.MaxUint16) // 65535
fmt.Println("uint32 max:", math.MaxUint32) // 4294967295
// Practical usage
var counter int = 0
var fileSize int64 = 1_234_567_890
var flags uint8 = 0b11001010
var port uint16 = 8080
fmt.Println(counter, fileSize, flags, port)
}
int vs int64 β Platform-Dependent Sizeβ
The size of int depends on the platform (OS and CPU architecture). On modern 64-bit systems it is 8 bytes, but if you need a guaranteed size for cross-platform code, use int64.
package main
import (
"fmt"
"unsafe"
)
func main() {
var i int
var i64 int64
fmt.Printf("int size: %d bytes\n", unsafe.Sizeof(i)) // 64-bit: 8
fmt.Printf("int64 size: %d bytes\n", unsafe.Sizeof(i64)) // always 8
// General counters and indices β use int (convention)
for i := 0; i < 10; i++ {
_ = i
}
// File sizes, timestamps β use int64 (guaranteed size)
var timestamp int64 = 1_700_000_000
var fileOffset int64 = 4_294_967_296 // over 4 GB
fmt.Println(timestamp, fileOffset)
}
Floating-Point Typesβ
| Type | Size | Precision |
|---|---|---|
| float32 | 4 bytes | ~7 decimal digits |
| float64 | 8 bytes | ~15β16 decimal digits |
The default type for floating-point literals in Go is float64.
package main
import (
"fmt"
"math"
)
func main() {
var f32 float32 = 3.14159265358979
var f64 float64 = 3.14159265358979
// float32 loses precision
fmt.Printf("float32: %.10f\n", f32) // 3.1415927410
fmt.Printf("float64: %.10f\n", f64) // 3.1415926536
// Mathematical constants
fmt.Println("Ο:", math.Pi)
fmt.Println("e:", math.E)
fmt.Println("max float64:", math.MaxFloat64)
// Special values
inf := math.Inf(1) // positive infinity
nan := math.NaN() // NaN (Not a Number)
fmt.Println("inf:", inf)
fmt.Println("nan:", nan)
fmt.Println("IsNaN:", math.IsNaN(nan))
fmt.Println("IsInf:", math.IsInf(inf, 1))
}
Complex Typesβ
| Type | Size | Composition |
|---|---|---|
| complex64 | 8 bytes | float32 real + float32 imaginary |
| complex128 | 16 bytes | float64 real + float64 imaginary |
package main
import (
"fmt"
"math/cmplx"
)
func main() {
// Complex number literals
c1 := 3 + 4i // complex128 (default)
c2 := complex(1, 2) // complex(real, imaginary)
fmt.Println("c1:", c1)
fmt.Println("c2:", c2)
// Extract real and imaginary parts
fmt.Println("real:", real(c1))
fmt.Println("imag:", imag(c1))
// Complex number operations
fmt.Println("magnitude:", cmplx.Abs(c1)) // 5 (3-4-5 Pythagorean triple)
fmt.Println("sqrt(-1):", cmplx.Sqrt(-1)) // (0+1i)
}
bool Typeβ
bool has only two possible values: true or false.
package main
import "fmt"
func main() {
var isReady bool = true
var hasError = false
// Logical operators
fmt.Println(true && false) // AND: false
fmt.Println(true || false) // OR: true
fmt.Println(!true) // NOT: false
// Comparison operators return bool
x, y := 10, 20
fmt.Println(x < y) // true
fmt.Println(x == y) // false
fmt.Println(x != y) // true
// Conditional expression
if isReady && !hasError {
fmt.Println("System ready")
}
// In Go, bool and int are different types β no implicit conversion
// var n int = true // compile error!
var n int
if isReady {
n = 1
}
fmt.Println("n:", n)
}
string Typeβ
A Go string is an immutable sequence of bytes, encoded in UTF-8. It can be treated as a byte array ([]byte).
package main
import "fmt"
func main() {
// Double-quoted string β escape sequences are processed
s1 := "Hello, World!\n"
fmt.Print(s1) // newline is processed
// Backtick string (raw string) β no escape processing
s2 := `First line
Second line
Path: C:\Users\Go`
fmt.Println(s2)
// Strings are immutable β you can read individual bytes by index
s3 := "Hello"
fmt.Printf("s3[0] = %d (%c)\n", s3[0], s3[0]) // 72 (H)
// s3[0] = 'h' // compile error! strings are immutable
// String concatenation
hello := "Hello"
world := "World"
greeting := hello + ", " + world + "!"
fmt.Println(greeting)
// String length β number of bytes
emoji := "Hello π"
fmt.Printf("len(%q) = %d bytes\n", emoji, len(emoji)) // more than 7 due to UTF-8 encoding
}
byte and runeβ
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// byte = uint8 (a single ASCII character)
var b byte = 'A'
fmt.Printf("byte: %d (%c)\n", b, b) // 65 (A)
// rune = int32 (a Unicode code point)
var r rune = 'δΈ'
fmt.Printf("rune: %d (%c)\n", r, r) // 19990 (δΈ)
fmt.Printf("rune hex: U+%04X\n", r) // U+4E16
// Byte count vs character count
s := "Hello, δΈη"
fmt.Printf("byte count: %d\n", len(s)) // 13
fmt.Printf("rune count: %d\n", utf8.RuneCountInString(s)) // 9
// Convert to rune slice to access individual characters
runes := []rune(s)
fmt.Printf("runes[7]: %c\n", runes[7]) // δΈ
fmt.Printf("runes[8]: %c\n", runes[8]) // η
// Iterating with range yields runes
for i, r := range s {
fmt.Printf("index %2d: %c (U+%04X)\n", i, r, r)
}
}
Output (partial):
byte count: 13
rune count: 9
runes[7]: δΈ
index 0: H (U+0048)
index 1: e (U+0065)
...
index 7: δΈ (U+4E16)
index 10: η (U+754C)
Numeric Literal Syntaxβ
Since Go 1.13, various base prefixes and readability separators are supported.
package main
import "fmt"
func main() {
// Decimal (default)
decimal := 1_000_000 // underscores improve readability
fmt.Println("decimal:", decimal) // 1000000
// Binary (0b or 0B prefix)
binary := 0b1010_1100
fmt.Printf("binary: %b = %d\n", binary, binary) // 10101100 = 172
// Octal (0o or 0O prefix, or leading 0)
octal := 0o755
fmt.Printf("octal: %o = %d\n", octal, octal) // 755 = 493
// Hexadecimal (0x or 0X prefix)
hex := 0xFF_EC_D0_12
fmt.Printf("hex: %X = %d\n", hex, hex)
// Floating-point literals
f1 := 1_234.567_89
f2 := 1.5e10 // 1.5 Γ 10^10
f3 := 0x1p-2 // hexadecimal floating-point (0.25)
fmt.Println(f1, f2, f3)
}
Explicit Type Conversionβ
Go has no implicit type conversions. Operations between different types require an explicit conversion using the T(v) syntax.
package main
import "fmt"
func main() {
var i int = 42
var f float64 = 3.14
// int β float64
result := float64(i) + f
fmt.Printf("%.2f\n", result) // 45.14
// float64 β int (truncates the fractional part)
truncated := int(f)
fmt.Println(truncated) // 3
// int β string (warning: this converts the code point, not the number!)
var code int = 65
wrongWay := string(code) // "A" (code point 65 = 'A')
fmt.Println(wrongWay) // A (not the string "65"!)
// To convert a number to its string representation, use fmt.Sprintf or strconv
// rightWay := fmt.Sprintf("%d", code) // "65"
// string β []byte conversion (causes a memory copy)
s := "Hello, Go"
b := []byte(s) // string β []byte
b[0] = 'h' // []byte is mutable!
s2 := string(b) // []byte β string
fmt.Println(s2) // hello, Go
// string β []rune conversion
text := "Hello"
r := []rune(text)
fmt.Printf("first char: %c\n", r[0]) // H
fmt.Printf("char count: %d\n", len(r)) // 5
}
Type Conversion in Practiceβ
package main
import (
"fmt"
"unsafe"
)
func main() {
// Conversion between integer types
var small int8 = 100
var large int64 = int64(small) // int8 β int64
fmt.Println(large)
// Beware of overflow: large type β small type
var big int = 300
var tiny int8 = int8(big) // 300 exceeds int8 range!
fmt.Println(tiny) // 44 (300 mod 256 = 44, unexpected result!)
// Check type sizes with unsafe.Sizeof
fmt.Println("int8 size:", unsafe.Sizeof(int8(0))) // 1
fmt.Println("int16 size:", unsafe.Sizeof(int16(0))) // 2
fmt.Println("int32 size:", unsafe.Sizeof(int32(0))) // 4
fmt.Println("int64 size:", unsafe.Sizeof(int64(0))) // 8
fmt.Println("float32 size:", unsafe.Sizeof(float32(0))) // 4
fmt.Println("float64 size:", unsafe.Sizeof(float64(0))) // 8
}
Type Alias vs Type Definitionβ
Go provides two ways to create new type names.
Type Definitionβ
package main
import "fmt"
// Type definition: creates a completely new type
type Celsius float64
type Fahrenheit float64
func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func main() {
temp := Celsius(100.0)
fmt.Printf("%.1fΒ°C = %.1fΒ°F\n", temp, temp.ToFahrenheit())
// Type safety: cannot directly assign between different types
var c Celsius = 100
// var f Fahrenheit = c // compile error! different types
var f Fahrenheit = Fahrenheit(c) // explicit conversion is OK
fmt.Println(f)
// Can also convert back to the underlying type
raw := float64(c)
fmt.Println(raw)
}
Type Aliasβ
package main
import "fmt"
// Type alias: gives a different name to the same type (Go 1.9+)
type MyString = string // MyString is exactly the same as string
func main() {
var s MyString = "Hello"
var t string = s // no conversion needed since it's an alias
fmt.Println(s, t)
// byte and rune are actually type aliases
// type byte = uint8
// type rune = int32
var b byte = 255
var u uint8 = b // assignable directly since they are aliases
fmt.Println(b, u)
}
Type Definition vs Type Aliasβ
| Aspect | Type Definition (type T U) | Type Alias (type T = U) |
|---|---|---|
| Creates new type | Yes | No (same type) |
| Can add methods | Yes | No |
| Implicit assignment | No (explicit conversion needed) | Yes (same type) |
| Common use | Domain types, safe APIs | Compatibility, refactoring |
Real-World Example: Type Conversionsβ
package main
import (
"fmt"
"math"
"strconv"
)
// Temperature converter
type Celsius float64
type Fahrenheit float64
type Kelvin float64
func celsiusToFahrenheit(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func celsiusToKelvin(c Celsius) Kelvin {
return Kelvin(c + 273.15)
}
func main() {
// string β number conversion (using strconv)
input := "36.6"
tempF64, err := strconv.ParseFloat(input, 64)
if err != nil {
fmt.Println("conversion error:", err)
return
}
temp := Celsius(tempF64)
fmt.Printf("Body temp: %.1fΒ°C = %.1fΒ°F = %.2fK\n",
temp,
celsiusToFahrenheit(temp),
celsiusToKelvin(temp))
// number β string conversion
score := 98
scoreStr := strconv.Itoa(score)
fmt.Println("score string:", scoreStr + " points")
// Type conversions in numeric calculations
total := 7
parts := 3
// Integer division truncates
intDivision := total / parts
// Convert to float64 before dividing
floatDivision := float64(total) / float64(parts)
fmt.Printf("integer division: %d\n", intDivision) // 2
fmt.Printf("float division: %.4f\n", floatDivision) // 2.3333
fmt.Printf("rounded: %.0f\n", math.Round(floatDivision)) // 2
}
Pro Tipsβ
Tip 1: Use int for general purposes, int64 when size must be guaranteed
By convention the standard library and Go idioms use int for indices and counters. Use int64 for file sizes, Unix timestamps, and network protocols where a specific size must be guaranteed.
Tip 2: Prefer float64 over float32 in most cases
Go's math package is built around float64. Reserve float32 for cases where memory savings matter, such as large numeric arrays or GPU computation.
Tip 3: Use type definitions to prevent unit mistakes
type UserID int64
type OrderID int64
func processOrder(uid UserID, oid OrderID) { ... }
// Accidentally swapping uid and oid is caught at compile time
Tip 4: Check for overflow when converting from a larger type to a smaller type
func safeInt64ToInt32(n int64) (int32, error) {
if n > math.MaxInt32 || n < math.MinInt32 {
return 0, fmt.Errorf("overflow: %d does not fit in int32", n)
}
return int32(n), nil
}