On this page

Variables and Types

14 min read TextCh. 1 — Go Fundamentals

Go's Type System

Go is a strongly statically typed language. This means two things: first, every variable has a fixed type that the compiler knows at compile time; second, conversions between types never happen implicitly — you must always be explicit. This combination eliminates an entire category of bugs common in JavaScript or Python.

Understanding Go's type system is fundamental because everything else in the language is built on top of it.

Declaring Variables

Go offers several ways to declare variables, each with its ideal context:

1. Explicit declaration with `var`

var name string = "Go"
var age int = 13       // Go turned 13 in 2022
var pi float64 = 3.14159
var active bool = true

2. Declaration with type inference

If you provide an initial value, Go can infer the type:

var name = "Go"     // string inferred
var pi = 3.14159    // float64 inferred
var active = true   // bool inferred

3. Short declaration with `:=`

The most common form inside functions. Only works inside functions (not at package level):

func main() {
    name := "Gopher"   // string
    version := 1.26    // float64
    active := true     // bool
    b := byte('A')     // byte (uint8)
}

4. `var` block for multiple variables

When declaring several related variables, a var block improves readability:

var (
    host    string  = "localhost"
    port    int     = 8080
    debug   bool    = false
    timeout float64 = 30.0
)

Numeric Types

Go has a specific set of numeric types. The right choice matters for performance and semantics:

Signed integers

Type Size Range
int8 8 bits -128 to 127
int16 16 bits -32,768 to 32,767
int32 32 bits -2,147,483,648 to 2,147,483,647
int64 64 bits ±9.2 × 10¹⁸
int 32 or 64 bits* platform-dependent

*int is 64 bits on 64-bit systems (virtually all modern systems). It is the default integer type.

Unsigned integers

var b uint8  = 255     // byte — alias for uint8
var s uint16 = 65535
var u uint32 = 4294967295
var g uint64 = 18446744073709551615
var n uint   = 42      // unsigned, platform size

Floating point

var f32 float32 = 3.14           // single precision (~7 decimal digits)
var f64 float64 = 3.14159265358979  // double precision (~15 decimal digits)
// float64 is the default type for floating-point literals
pi := 3.14159  // float64

The `string` Type

In Go, a string is an immutable sequence of bytes encoded in UTF-8. You cannot modify a character in a string directly — you must create a new one:

greeting := "Hello, Go!"

// Length in bytes (not in Unicode characters)
fmt.Println(len(greeting))

// Concatenation
full := greeting + " How are you?"

// Multi-line string with backticks (raw string literal)
json := `{
    "name": "Go",
    "version": 1.26
}`

// Interpolation (requires fmt.Sprintf)
message := fmt.Sprintf("Version: %.2f", 1.26)

`byte` and `rune`

Here is a crucial distinction in Go:

  • byte (alias for uint8): represents a single byte. Useful for working with binary data.
  • rune (alias for int32): represents a Unicode code point. A single character such as ñ, é, or may occupy multiple bytes in UTF-8, but is always a single rune.
// Iterate over bytes
s := "hello"
for i := 0; i < len(s); i++ {
    fmt.Printf("byte[%d] = %d\n", i, s[i])
}

// Iterate over runes (Unicode characters correctly)
for i, r := range "Hello!" {
    fmt.Printf("rune[%d] = %c (%d)\n", i, r, r)
}

// Convert string to rune slice for index access
runes := []rune("Hello!")
fmt.Println(len(runes))  // 6 (characters), not bytes

The `bool` Type

active := true
inactive := false

// Logical operators
result := active && !inactive  // true
either := active || inactive   // true

Zero Values

This is one of Go's most elegant features: every variable declared without an initial value automatically receives its zero value. There are no uninitialized variables, no undefined behavior:

var i int        // 0
var f float64    // 0.0
var b bool       // false
var s string     // "" (empty string)
var p *int       // nil
var slice []int  // nil
var m map[string]int  // nil

Zero values make code predictable. In other languages, reading an uninitialized variable can give random results or throw exceptions. In Go, you always get the well-defined zero value for the type.

Constants and `const`

Constants in Go are evaluated at compile time and do not occupy memory at runtime:

const Pi = 3.14159265358979323846
const Company = "Bemorex"
const MaxConnections = 100

// Typed constant
const Timeout time.Duration = 30 * time.Second

// Constant block
const (
    KB = 1024
    MB = 1024 * KB
    GB = 1024 * MB
)

`iota`: Idiomatic Enumerations

iota is a special identifier used within const blocks. It starts at 0 and increments by 1 for each constant in the block:

type Level int

const (
    Beginner     Level = iota  // 0
    Intermediate               // 1
    Advanced                   // 2
    Expert                     // 3
)

// With expressions
type ByteSize float64

const (
    _           = iota // Ignore the first value with _
    KB ByteSize = 1 << (10 * iota)  // 1024
    MB                               // 1048576
    GB                               // 1073741824
    TB                               // 1099511627776
)

Explicit Type Conversion

In Go, there is never implicit conversion between numeric types. You must be explicit:

var i int = 42
var f float64 = float64(i)   // int → float64
var u uint = uint(f)          // float64 → uint

// String to number
import "strconv"

n, err := strconv.Atoi("42")           // string → int
if err != nil {
    fmt.Println("Conversion error")
}

f, err := strconv.ParseFloat("3.14", 64)  // string → float64

// Number to string
s := strconv.Itoa(42)                   // int → string (more efficient than fmt.Sprintf)
s2 := fmt.Sprintf("%.2f", 3.14)        // float64 → formatted string

Multiple Assignment

Go allows assigning multiple variables in a single line:

x, y := 10, 20
a, b := "hello", true

// Swap values without a temporary variable
x, y = y, x
fmt.Println(x, y)  // 20 10

The Blank Identifier `_`

The underscore _ discards values. It is Go's "black hole":

// Ignore the index in a range
for _, value := range []int{1, 2, 3} {
    fmt.Println(value)
}

// Ignore a return value
result, _ := strconv.Atoi("42")  // ignore the error (not recommended in production)

With this solid understanding of the type system, in the next lesson we will explore how to control execution flow with if, switch, and Go's only loop: for.

Go has no implicit type conversion
Unlike JavaScript or Python, Go never converts types automatically. If you have an int and need a float64, you must write float64(myInt) explicitly. This prevents subtle precision errors and makes every type conversion visible in the code.
iota resets in each const block
iota is a counter that starts at 0 and increments by 1 for each line in a const block. It is perfect for creating enumerations without manually assigning values. You can combine it with expressions: iota * 2, 1 << iota, and so on.