In the previous tutorial, you installed Go, set up VS Code, and wrote your first program. Now it is time to learn about variables, types, and constants. These are the building blocks of every Go program.

Declaring Variables

Go has two ways to declare variables: the var keyword and the short declaration :=.

The var Keyword

The var keyword declares a variable with an explicit type:

package main

import "fmt"

func main() {
    var name string = "Alex"
    var age int = 25
    var height float64 = 1.75
    var isStudent bool = true

    fmt.Println(name, age, height, isStudent)
}

Output:

Alex 25 1.75 true

You can also let Go figure out the type from the value:

var name = "Alex"    // Go knows this is a string
var age = 25         // Go knows this is an int
var height = 1.75    // Go knows this is a float64
var isStudent = true // Go knows this is a bool

You can declare multiple variables at once:

var (
    firstName string = "Alex"
    lastName  string = "Smith"
    age       int    = 25
)

Short Declaration :=

Inside functions, you can use := to declare and assign a variable in one step:

package main

import "fmt"

func main() {
    name := "Alex"       // Same as: var name string = "Alex"
    age := 25            // Same as: var age int = 25
    height := 1.75       // Same as: var height float64 = 1.75
    isStudent := true    // Same as: var isStudent bool = true

    fmt.Println(name, age, height, isStudent)
}

The := operator is the most common way to declare variables in Go. Most Go code uses := inside functions.

Important: You cannot use := outside of functions. For package-level variables, you must use var:

package main

// Package-level variable — must use var
var appName = "MyApp"

func main() {
    // Inside a function — can use :=
    version := "1.0"
    fmt.Println(appName, version)
}

var vs :=

When should you use var and when should you use :=?

Use var when…Use := when…
Declaring package-level variablesDeclaring variables inside functions
You want to declare without a valueYou want to declare and assign at the same time
You want to be explicit about the typeYou want Go to figure out the type

In practice, most Go developers use := inside functions and var only when needed.

Basic Types

Go has several built-in types. Here are the ones you will use most often:

Integers

package main

import "fmt"

func main() {
    var a int = 42          // Platform-dependent (32 or 64 bit)
    var b int8 = 127        // -128 to 127
    var c int16 = 32767     // -32768 to 32767
    var d int32 = 2147483647
    var e int64 = 9223372036854775807

    fmt.Println(a, b, c, d, e)

    // Unsigned integers (no negative numbers)
    var f uint = 42
    var g uint8 = 255       // 0 to 255 (same as byte)
    var h uint16 = 65535
    var i uint32 = 4294967295
    var j uint64 = 18446744073709551615

    fmt.Println(f, g, h, i, j)
}

For most cases, just use int. Go will make it 64-bit on modern systems. Use specific sizes only when you need them.

Floats

package main

import "fmt"

func main() {
    var price float64 = 19.99   // 64-bit float (most common)
    var score float32 = 95.5    // 32-bit float (less precision)

    fmt.Printf("Price: %.2f\n", price)
    fmt.Printf("Score: %.1f\n", score)
}

Output:

Price: 19.99
Score: 95.5

Use float64 by default. It has more precision and is the default type for floating-point numbers in Go.

Strings

package main

import "fmt"

func main() {
    // Regular string (double quotes)
    greeting := "Hello, World!"

    // Raw string (backticks) — no escape sequences
    path := `C:\Users\Alex\Documents`
    multiline := `This is
a multi-line
string`

    fmt.Println(greeting)
    fmt.Println(path)
    fmt.Println(multiline)

    // String length
    fmt.Println("Length:", len(greeting))

    // Access individual bytes
    fmt.Printf("First byte: %c\n", greeting[0])
}

Output:

Hello, World!
C:\Users\Alex\Documents
This is
a multi-line
string
Length: 13
First byte: H

Strings in Go are immutable. You cannot change a character in a string. You need to create a new string instead.

Booleans

package main

import "fmt"

func main() {
    isReady := true
    isComplete := false

    fmt.Println("Ready:", isReady)
    fmt.Println("Complete:", isComplete)
    fmt.Println("Both:", isReady && isComplete)   // AND
    fmt.Println("Either:", isReady || isComplete)  // OR
    fmt.Println("Not ready:", !isReady)            // NOT
}

Output:

Ready: true
Complete: false
Both: false
Either: true
Not ready: false

Byte and Rune

package main

import "fmt"

func main() {
    // byte is an alias for uint8 — represents ASCII characters
    var b byte = 'A'
    fmt.Printf("Byte: %c (value: %d)\n", b, b)

    // rune is an alias for int32 — represents Unicode characters
    var r rune = '日'
    fmt.Printf("Rune: %c (value: %d)\n", r, r)

    // Iterating over a string gives you runes, not bytes
    word := "Hello"
    for i, ch := range word {
        fmt.Printf("Index %d: %c\n", i, ch)
    }
}

Output:

Byte: A (value: 65)
Rune: 日 (value: 26085)
Index 0: H
Index 1: e
Index 2: l
Index 3: l
Index 4: o

Use byte for ASCII text. Use rune when working with Unicode characters.

Zero Values

This is an important concept in Go. Every type has a default value called the zero value. When you declare a variable without assigning it, it gets the zero value automatically.

package main

import "fmt"

func main() {
    var i int        // 0
    var f float64    // 0.0
    var b bool       // false
    var s string     // "" (empty string)

    fmt.Printf("int: %d\n", i)
    fmt.Printf("float64: %f\n", f)
    fmt.Printf("bool: %t\n", b)
    fmt.Printf("string: %q\n", s) // %q shows quotes around strings
}

Output:

int: 0
float64: 0.000000
bool: false
string: ""

Here is the full list of zero values:

TypeZero Value
int, int8, int16, int32, int640
uint, uint8, uint16, uint32, uint640
float32, float640.0
boolfalse
string"" (empty string)
Pointersnil
Slices, maps, channelsnil

Zero values are useful. You don’t need to initialize variables to a default value. Go does it for you. This is different from some languages where uninitialized variables contain garbage data.

Constants

Constants are values that never change. Use the const keyword:

package main

import "fmt"

func main() {
    const pi = 3.14159
    const greeting = "Hello"
    const maxRetries = 3

    fmt.Println(pi)
    fmt.Println(greeting)
    fmt.Println(maxRetries)

    // This would be an error:
    // pi = 3.14 // Error: cannot assign to pi
}

You can declare multiple constants in a block:

const (
    appName    = "MyApp"
    appVersion = "1.0.0"
    maxUsers   = 1000
)

Constants must be known at compile time. You cannot assign a function result to a constant:

const name = "Alex"        // OK — string literal
const age = 25             // OK — number literal
// const now = time.Now()  // Error — function call is not a constant

Iota — Auto-Incrementing Constants

iota is a special constant generator. It starts at 0 and increments by 1 for each constant in a block:

package main

import "fmt"

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

func main() {
    fmt.Println("Sunday:", Sunday)
    fmt.Println("Monday:", Monday)
    fmt.Println("Saturday:", Saturday)
}

Output:

Sunday: 0
Monday: 1
Saturday: 6

You can use expressions with iota:

package main

import "fmt"

// File sizes using iota with bit shifting
const (
    _  = iota             // Skip 0
    KB = 1 << (10 * iota) // 1 << 10 = 1024
    MB                    // 1 << 20 = 1048576
    GB                    // 1 << 30 = 1073741824
    TB                    // 1 << 40
)

func main() {
    fmt.Printf("KB: %d\n", KB)
    fmt.Printf("MB: %d\n", MB)
    fmt.Printf("GB: %d\n", GB)
}

Output:

KB: 1024
MB: 1048576
GB: 1073741824

iota is commonly used for enumerations and bit flags. It replaces the need for an enum keyword.

Type Conversions

Go does not convert types automatically. You must convert them explicitly:

package main

import "fmt"

func main() {
    // int to float64
    age := 25
    ageFloat := float64(age)
    fmt.Printf("Age as float: %.1f\n", ageFloat)

    // float64 to int (truncates, does not round)
    price := 19.99
    priceInt := int(price)
    fmt.Printf("Price as int: %d\n", priceInt) // 19, not 20

    // int to string — does NOT do what you think!
    number := 65
    letter := string(number) // Converts to Unicode character, not "65"
    fmt.Println(letter)      // Prints: A (Unicode 65)

    // To convert a number to a string, use fmt.Sprintf
    numberStr := fmt.Sprintf("%d", number)
    fmt.Println(numberStr) // Prints: 65

    // string to int — use strconv package
    // (we'll show this below)
}

Output:

Age as float: 25.0
Price as int: 19
A
65

Converting Strings to Numbers

Use the strconv package to convert between strings and numbers:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // String to int
    numStr := "42"
    num, err := strconv.Atoi(numStr) // Atoi = ASCII to Integer
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Number:", num)

    // Int to string
    str := strconv.Itoa(100) // Itoa = Integer to ASCII
    fmt.Println("String:", str)

    // String to float
    floatStr := "3.14"
    f, err := strconv.ParseFloat(floatStr, 64)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Float:", f)

    // String to bool
    boolStr := "true"
    b, err := strconv.ParseBool(boolStr)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Bool:", b)
}

Output:

Number: 42
String: 100
Float: 3.14
Bool: true

Notice that strconv.Atoi returns two values: the number and an error. If the string is not a valid number, the error will not be nil. This is Go’s error handling pattern. We will cover it in detail in the next tutorial.

String Formatting

Go’s fmt.Sprintf is the main way to build formatted strings:

package main

import "fmt"

func main() {
    name := "Alex"
    age := 25
    score := 95.5

    // Build a formatted string
    message := fmt.Sprintf("Name: %s, Age: %d, Score: %.1f", name, age, score)
    fmt.Println(message)

    // Padding and alignment
    fmt.Printf("%-10s %5d\n", "Alex", 100)    // Left-align name, right-align number
    fmt.Printf("%-10s %5d\n", "Sam", 95)
    fmt.Printf("%-10s %5d\n", "Jordan", 87)

    // Leading zeros
    fmt.Printf("ID: %05d\n", 42) // 00042
}

Output:

Name: Alex, Age: 25, Score: 95.5
Alex         100
Sam           95
Jordan        87
ID: 00042

A Complete Example

Here is a program that uses everything from this tutorial:

package main

import (
    "fmt"
    "strconv"
)

// Constants for our simple grading system
const (
    gradeA = iota + 1 // 1
    gradeB             // 2
    gradeC             // 3
    gradeD             // 4
    gradeF             // 5
)

const passingGrade = gradeC

func main() {
    fmt.Println("=== GO-3: Variables, Types, and Constants ===")
    fmt.Println()

    // Variables with different types
    studentName := "Alex"
    studentAge := 20
    gpa := 3.75
    isEnrolled := true

    // Print student info
    fmt.Printf("Student: %s\n", studentName)
    fmt.Printf("Age: %d\n", studentAge)
    fmt.Printf("GPA: %.2f\n", gpa)
    fmt.Printf("Enrolled: %t\n", isEnrolled)
    fmt.Println()

    // Zero values
    var unsetInt int
    var unsetString string
    var unsetBool bool
    fmt.Printf("Zero int: %d\n", unsetInt)
    fmt.Printf("Zero string: %q\n", unsetString)
    fmt.Printf("Zero bool: %t\n", unsetBool)
    fmt.Println()

    // Type conversions
    ageStr := strconv.Itoa(studentAge)
    fmt.Printf("Age as string: %s\n", ageStr)

    scoreStr := "95"
    score, err := strconv.Atoi(scoreStr)
    if err != nil {
        fmt.Println("Error converting score:", err)
    } else {
        fmt.Printf("Score as int: %d\n", score)
    }
    fmt.Println()

    // Constants with iota
    fmt.Println("Grade A:", gradeA)
    fmt.Println("Grade B:", gradeB)
    fmt.Println("Passing grade:", passingGrade)
}

Output:

=== GO-3: Variables, Types, and Constants ===

Student: Alex
Age: 20
GPA: 3.75
Enrolled: true

Zero int: 0
Zero string: ""
Zero bool: false

Age as string: 20
Score as int: 95

Grade A: 1
Grade B: 2
Passing grade: 3

Common Mistakes

1. Using := to reassign an existing variable.

name := "Alex"
name := "Sam" // Error: no new variables on left side of :=
name = "Sam"  // Correct: use = to reassign

Use := only for the first declaration. Use = for reassignment.

2. Forgetting that string() converts to a Unicode character.

num := 65
s := string(num) // "A", not "65"
s = fmt.Sprintf("%d", num) // "65" — this is what you want

3. Mixing types without conversion.

a := 10      // int
b := 3.5     // float64
// c := a + b // Error: mismatched types int and float64
c := float64(a) + b // Correct

Go never converts types implicitly. You must always be explicit.

Source Code

You can find the complete source code for this tutorial on GitHub:

GO-3 Source Code on GitHub

What’s Next?

In the next tutorial, Go Tutorial #4: Functions and Error Handling, you will learn:

  • How to write functions in Go
  • Multiple return values
  • The error type and if err != nil pattern
  • Variadic functions
  • The defer keyword

This is part 3 of the Go Tutorial series. Follow along to learn Go from scratch.