Beginner

Variables and Types in Go

Learn about variables, data types, type inference, and constants in Go with practical Docker-ready examples

Go takes a pragmatic approach to variables and types. It is statically and strongly typed, meaning every variable has a fixed type determined at compile time, but Go’s type inference often lets you skip writing types explicitly. The result is code that feels nearly as concise as a dynamically typed language while retaining the safety guarantees of a static type system.

Understanding Go’s type system is essential because the compiler catches type mismatches before your code ever runs. Go has no implicit type conversions between numeric types — you must be explicit, which eliminates an entire class of subtle bugs common in C and C++.

In this tutorial you will learn how to declare variables using both explicit types and short declaration syntax, work with Go’s basic types, convert between types, and define constants.

Variable Declaration

Go offers several ways to declare variables. The most common in practice is the short declaration operator :=, which infers the type from the value on the right-hand side. You can also use the var keyword for explicit declarations, which is required at the package level or when you want a variable initialized to its zero value.

Create a file named variables.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"

// Package-level variables must use var
var packageLevel = "I'm accessible throughout the package"

func main() {
    // Explicit type declaration
    var age int = 30
    var name string = "Go Developer"
    var pi float64 = 3.14159
    var active bool = true

    fmt.Println("=== Explicit Declarations ===")
    fmt.Println("age:", age)
    fmt.Println("name:", name)
    fmt.Println("pi:", pi)
    fmt.Println("active:", active)

    // Short declaration with type inference
    city := "Chicago"
    year := 2009
    ratio := 0.618
    verbose := false

    fmt.Println("\n=== Short Declarations ===")
    fmt.Println("city:", city)
    fmt.Println("year:", year)
    fmt.Println("ratio:", ratio)
    fmt.Println("verbose:", verbose)

    // Zero values - uninitialized variables get a default
    var zeroInt int
    var zeroFloat float64
    var zeroString string
    var zeroBool bool

    fmt.Println("\n=== Zero Values ===")
    fmt.Printf("int: %d, float64: %g, string: %q, bool: %t\n",
        zeroInt, zeroFloat, zeroString, zeroBool)

    // Multiple assignment
    var a, b, c int = 1, 2, 3
    x, y := "hello", 42

    fmt.Println("\n=== Multiple Assignment ===")
    fmt.Println("a, b, c:", a, b, c)
    fmt.Println("x, y:", x, y)

    fmt.Println("\n=== Package Level ===")
    fmt.Println(packageLevel)
}

Every type in Go has a zero value — the default when no initial value is provided. Numeric types default to 0, booleans to false, and strings to the empty string "". This means variables in Go are always initialized, eliminating the undefined-variable bugs found in some other languages.

Basic Types and Conversions

Go has a rich set of numeric types with explicit sizes. Unlike C, where int varies by platform, Go’s sized types (int8, int32, int64) have guaranteed widths. The plain int type is platform-dependent (32 or 64 bits) but is the idiomatic choice for most integer work.

Go requires explicit type conversions — there is no automatic promotion between types. This is a deliberate design choice that prevents subtle data loss.

Create a file named variables_types.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import "fmt"

func main() {
    // Integer types
    var small int8 = 127        // -128 to 127
    var medium int32 = 2147483647
    var large int64 = 9223372036854775807
    var unsigned uint8 = 255    // 0 to 255

    fmt.Println("=== Integer Types ===")
    fmt.Println("int8:", small)
    fmt.Println("int32:", medium)
    fmt.Println("int64:", large)
    fmt.Println("uint8:", unsigned)

    // Floating point
    var single float32 = 3.14
    var double float64 = 3.141592653589793

    fmt.Println("\n=== Float Types ===")
    fmt.Println("float32:", single)
    fmt.Println("float64:", double)

    // Strings and runes
    greeting := "Hello, 世界"
    var letter rune = 'A'       // rune is an alias for int32 (Unicode code point)
    var b byte = 65             // byte is an alias for uint8

    fmt.Println("\n=== Strings and Runes ===")
    fmt.Println("greeting:", greeting)
    fmt.Printf("letter: %c (code point: %d)\n", letter, letter)
    fmt.Printf("byte: %c (value: %d)\n", b, b)
    fmt.Println("greeting length in bytes:", len(greeting))

    // Explicit type conversions (no implicit conversions in Go)
    intVal := 42
    floatVal := float64(intVal)
    backToInt := int(floatVal + 0.9) // Truncates, does not round

    fmt.Println("\n=== Type Conversions ===")
    fmt.Println("int to float64:", floatVal)
    fmt.Println("float64 to int (42.9):", backToInt)

    // String conversions
    num := 72
    char := string(rune(num))
    fmt.Println("int 72 as rune:", char)

    fmt.Printf("\n=== Type Inspection ===\n")
    fmt.Printf("intVal: %T, floatVal: %T, greeting: %T\n",
        intVal, floatVal, greeting)
}

Constants

Go distinguishes between variables and constants with the const keyword. Constants are determined at compile time and cannot be changed. Go also supports untyped constants, which have higher precision and can be used more flexibly across expressions involving different numeric types.

The iota enumerator is a powerful feature for creating sequences of related constants, commonly used to define enumerations.

Create a file named variables_constants.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import "fmt"

// Constants
const Pi = 3.14159265358979323846
const AppName = "CodeArchaeology"

// Grouped constants with iota
const (
    Sunday = iota // 0
    Monday        // 1
    Tuesday       // 2
    Wednesday     // 3
    Thursday      // 4
    Friday        // 5
    Saturday      // 6
)

// Iota with bit shifting for flags
const (
    ReadPermission   = 1 << iota // 1
    WritePermission              // 2
    ExecutePermission            // 4
)

func main() {
    fmt.Println("=== Constants ===")
    fmt.Println("Pi:", Pi)
    fmt.Println("App:", AppName)

    fmt.Println("\n=== Days (iota) ===")
    fmt.Println("Sunday:", Sunday)
    fmt.Println("Wednesday:", Wednesday)
    fmt.Println("Saturday:", Saturday)

    fmt.Println("\n=== Permissions (bit flags) ===")
    fmt.Println("Read:", ReadPermission)
    fmt.Println("Write:", WritePermission)
    fmt.Println("Execute:", ExecutePermission)

    // Combining flags
    readWrite := ReadPermission | WritePermission
    fmt.Println("Read + Write:", readWrite)

    // Untyped constants have flexible precision
    const huge = 1e100
    const tiny = huge / 1e99
    fmt.Println("\n=== Untyped Constants ===")
    fmt.Printf("tiny: %g\n", tiny)
}

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the official Go image
docker pull golang:1.23

# Run the variables example
docker run --rm -v $(pwd):/app -w /app golang:1.23 go run variables.go

# Run the types and conversions example
docker run --rm -v $(pwd):/app -w /app golang:1.23 go run variables_types.go

# Run the constants example
docker run --rm -v $(pwd):/app -w /app golang:1.23 go run variables_constants.go

Expected Output

Output from variables.go:

=== Explicit Declarations ===
age: 30
name: Go Developer
pi: 3.14159
active: true

=== Short Declarations ===
city: Chicago
year: 2009
ratio: 0.618
verbose: false

=== Zero Values ===
int: 0, float64: 0, string: "", bool: false

=== Multiple Assignment ===
a, b, c: 1 2 3
x, y: hello 42

=== Package Level ===
I'm accessible throughout the package

Output from variables_types.go:

=== Integer Types ===
int8: 127
int32: 2147483647
int64: 9223372036854775807
uint8: 255

=== Float Types ===
float32: 3.14
float64: 3.141592653589793

=== Strings and Runes ===
greeting: Hello, 世界
letter: A (code point: 65)
byte: A (value: 65)
greeting length in bytes: 13

=== Type Conversions ===
int to float64: 42
float64 to int (42.9): 42

int 72 as rune: H

=== Type Inspection ===
intVal: int, floatVal: float64, greeting: string

Output from variables_constants.go:

=== Constants ===
Pi: 3.141592653589793
App: CodeArchaeology

=== Days (iota) ===
Sunday: 0
Wednesday: 3
Saturday: 6

=== Permissions (bit flags) ===
Read: 1
Write: 2
Execute: 4
Read + Write: 3

=== Untyped Constants ===
tiny: 10

Key Concepts

  • Short declarations (:=) infer the type from the value and are the most common way to declare variables inside functions
  • Zero values guarantee every variable is initialized — 0 for numbers, false for bools, "" for strings
  • No implicit type conversions — Go requires explicit casts like float64(x), preventing silent data loss
  • rune is an alias for int32 and represents a Unicode code point; byte is an alias for uint8
  • Sized integer types (int8, int16, int32, int64) have guaranteed widths, unlike C’s platform-dependent sizes
  • const with iota provides a clean way to define enumerations and bit flags without magic numbers
  • Untyped constants have higher precision than any concrete type and adapt to their usage context
  • var is required at the package level and when you want zero-value initialization without an explicit value

Running Today

All examples can be run using Docker:

docker pull golang:1.23
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining