Basic Syntax: Variables, Types, and Control Flow

🔊

This article is based on Go 1.26.

In the previous article, we discussed Go’s design philosophy and your first Hello World program. This time, let’s dive directly into syntax—variables, types, control flow, functions, composite types, and pointers. The core goal is simple: enable you to read and write basic Go code.

Go’s syntax design philosophy is simplicity over complexity—eliminating unnecessary syntactic sugar while keeping things clear and intuitive. If you’re coming from another language, you’ll find Go’s learning curve relatively gentle because it has few syntactic features, but each one is practical.

Variables and Constants

Go’s variable declarations are very flexible, providing multiple approaches. The core principle is clear intent, avoid ambiguity.

var Declaration

The var keyword can declare variables and supports multiple forms:

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
    // Standard declaration (explicit type)
    var name string = "Alice"
    var age int = 25

    // Type inference
    var city = "Beijing"  // inferred as string
    var score = 95.5      // inferred as float64

    // Batch declaration (recommended for related variables)
    var (
        username string = "user123"
        password string = "secret"
        isActive bool   = true
    )

    fmt.Println(name, age, city, score)
    fmt.Println(username, password, isActive)
}

Short Declaration :=

The most commonly used variable declaration in Go is the short declaration :=, which can only be used inside functions:

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
    // Short declaration (automatic type inference)
    name := "Bob"
    count := 42
    pi := 3.14159

    // Declare multiple variables simultaneously
    x, y := 10, 20

    // Declare temporary variables in control structures (very common)
    if err := doSomething(); err != nil {
        fmt.Println("Error:", err)
    }

    fmt.Println(name, count, pi, x, y)
}

func doSomething() error {
    return nil
}

Important details:

  • := can only be used inside functions, not for package-level variables
  • In the same scope, := cannot redeclare existing variables (unless at least one is new)
  • := automatically infers types, you don’t need to write the type

Constants const

Constants are declared with const and are determined at compile time:

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const (
    Pi      = 3.141592653589793
    MaxSize = 100
    Version = "1.0.0"
)

const (
    StatusOK    = 200
    StatusError = 500
)

Zero value initialization: Every type in Go has a zero value. When declaring a variable without initialization, it automatically gets the zero value:

  • Numeric types: 0
  • Boolean type: false
  • String: "" (empty string)
  • Pointer, slice, map, channel, interface, function: nil

Basic Data Types

Go’s type system is clear and intuitive, without complex type hierarchies.

Fundamental Types

Type CategoryGo TypesDescription
Signed integersint8, int16, int32, int648/16/32/64-bit signed integers
Unsigned integersuint8, uint16, uint32, uint648/16/32/64-bit unsigned integers
Platform-dependentint, uint32 bits on 32-bit systems, 64 bits on 64-bit systems
Pointer-sizeduintptrUnsigned integer large enough to store a pointer
Floating-pointfloat32, float64IEEE 754 standard (float64 recommended)
Complex numberscomplex64, complex128Real and imaginary parts are both float32/float64
Booleanbooltrue / false
StringstringUTF-8 encoded immutable byte sequence

Type Conversion

Go does not support implicit type conversion, explicit conversion is required:

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func main() {
    var i int = 42
    var f float64 = float64(i)  // explicit conversion
    var u uint = uint(f)

    // String conversion (watch out for pitfalls)
    var s string = string(i)    // not "42", but corresponding Unicode code point (42 is '*')
    var s2 string = string(65)  // "A" (ASCII 65)

    // Correct integer to string conversion
    var s3 string = fmt.Sprintf("%d", i)  // "42"

    // String to integer
    var num int
    fmt.Sscanf("42", "%d", &num)  // num = 42

    fmt.Println(f, u, s, s2, s3, num)
}

Common pitfalls:

  • string(65) is not "65", but "A" (ASCII 65)
  • string([]byte{'H','i'}) is the correct way for string literal conversion
  • Integer to string conversion uses fmt.Sprintf("%d", num) or strconv.Itoa(num)

Control Flow

Go’s control flow is simple and direct—only if, for, switch three forms, no while.

if/else

Go’s if statement supports a pre-statement (commonly used for error handling):

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
func main() {
    // Standard if/else
    x := 10
    if x > 5 {
        fmt.Println("x is greater than 5")
    } else {
        fmt.Println("x is not greater than 5")
    }

    // if/else if/else
    score := 85
    if score >= 90 {
        fmt.Println("Excellent")
    } else if score >= 60 {
        fmt.Println("Passed")
    } else {
        fmt.Println("Failed")
    }

    // if with pre-statement (very common)
    if err := doWork(); err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Work completed successfully")
}

func doWork() error {
    return nil
}

Scope of pre-statement: Variables declared in the pre-statement are only visible within the if statement block, avoiding namespace pollution.

for Loop (The Only Loop)

Go has only one type of loop: for, but it can play all the roles of C’s for, while, do-while:

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
func main() {
    // 1. Standard C-style for loop
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }

    // 2. while-like conditional loop (only condition)
    i := 0
    for i < 10 {
        fmt.Println(i)
        i++
    }

    // 3. Infinite loop
    count := 0
    for {
        fmt.Println("Tick")
        count++
        if count >= 5 {
            break
        }
    }

    // 4. for range (iterate collections)
    numbers := []int{1, 2, 3, 4, 5}
    for index, value := range numbers {
        fmt.Printf("Index: %d, Value: %d\n", index, value)
    }

    // Only value, ignore index
    for _, value := range numbers {
        fmt.Println("Value:", value)
    }

    // Only index, ignore value
    for index := range numbers {
        fmt.Println("Index:", index)
    }

    // Iterate map
    m := map[string]int{"Alice": 25, "Bob": 30}
    for key, value := range m {
        fmt.Printf("%s: %d\n", key, value)
    }

    // Iterate string (by rune, not byte)
    for index, runeValue := range "Hello, 世界" {
        fmt.Printf("Index: %d, Rune: %c\n", index, runeValue)
    }
}

for range pitfalls:

  • When iterating strings, index is byte position (not character position), runeValue is Unicode character
  • When iterating slice/array, elements are copied, not referenced
  • When iterating map, order is undefined (map is unordered)

switch

Go’s switch doesn’t need break (won’t automatically fallthrough), and can omit the condition (equivalent to if-else if-else):

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
func main() {
    // Standard switch
    grade := 'B'
    switch grade {
    case 'A':
        fmt.Println("Excellent")
    case 'B':
        fmt.Println("Good")
    case 'C':
        fmt.Println("Passed")
    default:
        fmt.Println("Other")
    }

    // Multiple condition matching
    num := 3
    switch num {
    case 1, 3, 5, 7, 9:
        fmt.Println("Odd number")
    case 2, 4, 6, 8, 10:
        fmt.Println("Even number")
    default:
        fmt.Println("Other")
    }

    // Switch without condition (replace if-else if-else)
    score := 75
    switch {
    case score >= 90:
        fmt.Println("Excellent")
    case score >= 60:
        fmt.Println("Passed")
    default:
        fmt.Println("Failed")
    }

    // Type switch (check interface type)
    var i interface{} = "hello"
    switch v := i.(type) {
    case string:
        fmt.Println("String:", v)
    case int:
        fmt.Println("Int:", v)
    default:
        fmt.Println("Unknown type")
    }
}

Key points:

  • No break needed (unless explicit fallthrough)
  • Can match multiple values (case 1, 3, 5:)
  • Switch without condition is a clear alternative to if-else if-else
  • Type switch checks the actual type of an interface (will be covered in detail later in the series)

Functions

Go’s function design is simple but powerful, supporting multiple return values, named return values, variadic parameters, etc.

Basic Functions

go
1
2
3
4
5
6
7
8
9
// Basic function (single return value)
func add(a int, b int) int {
    return a + b
}

// Multiple parameters of same type can be shortened
func add2(a, b int) int {
    return a + b
}

Multiple Return Values

Go natively supports multiple return values, which is the foundation of error handling:

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

// Call function with multiple return values
func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)

    // Ignore certain return values
    result2, _ := divide(10, 5)  // ignore error (not recommended)
    fmt.Println("Result2:", result2)
}

Named Return Values

Return values can be named, and these variables can be used directly within the function:

go
1
2
3
4
5
6
7
8
9
// Named return values
func rectArea(width, height float64) (area float64) {
    area = width * height
    return  // equivalent to return area
}

func main() {
    fmt.Println(rectArea(3, 4))  // 12
}

Advantages of named return values:

  • Automatically documents the meaning of return values
  • Reduces duplicate code in complex functions
  • Implicit return returns all named return values

Variadic Parameters

Go uses ... to declare variadic parameters:

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Variadic parameters (type is []int)
func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

// Call variadic function
func main() {
    fmt.Println(sum(1, 2, 3))        // 6
    fmt.Println(sum(1, 2, 3, 4, 5))  // 15

    // Unfold slice
    nums := []int{1, 2, 3}
    fmt.Println(sum(nums...))        // 6
}

Key points:

  • Variadic parameters are essentially a slice ([]int)
  • When calling function, use ... to unfold slice
  • Variadic parameters must be the last in the parameter list

Composite Types

Go provides composite types like arrays, slices, maps, and structs.

Arrays and Slices

Arrays have fixed length, slices have dynamic length (more commonly used):

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
func main() {
    // Arrays (fixed length)
    var arr1 [5]int                    // zero-value array [0 0 0 0 0]
    arr2 := [5]int{1, 2, 3, 4, 5}      // explicit initialization
    arr3 := [...]int{1, 2, 3}          // auto-infer length ([3]int)

    // Slices (dynamic length, more common)
    var s1 []int                       // nil slice
    s2 := []int{1, 2, 3, 4, 5}         // direct initialization
    s3 := make([]int, 5)               // make create (all 0)
    s4 := make([]int, 5, 10)           // length=5, capacity=10

    // Slice operations
    fmt.Println(s2[0])                 // access element: 1
    fmt.Println(s2[1:3])               // slice: [2 3]
    fmt.Println(len(s2))               // length: 5
    fmt.Println(cap(s2))               // capacity: 5

    // Append elements (may trigger expansion)
    s5 := []int{}
    s5 = append(s5, 1)
    s5 = append(s5, 2, 3, 4)
    fmt.Println(s5)                    // [1 2 3 4]

    // Copy slice
    s6 := make([]int, len(s5))
    copy(s6, s5)
    fmt.Println(s6)                    // [1 2 3 4]
}

Key differences between arrays and slices:

  • Arrays: Fixed length, length is part of the type ([3]int and [4]int are different types)
  • Slices: Dynamic length, are references to arrays (reference type)
  • Slice internals: A slice contains three pieces of information: pointer (to array), length, capacity

map (Mapping)

Go’s map is an unordered collection of key-value pairs:

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
func main() {
    // Create map
    m1 := make(map[string]int)           // empty map
    m2 := map[string]int{"Alice": 25, "Bob": 30}  // direct initialization

    // Add/modify
    m1["Charlie"] = 35
    m1["David"] = 40

    // Read
    fmt.Println(m1["Charlie"])           // 35
    fmt.Println(m2["Alice"])             // 25

    // Read and check existence
    value, exists := m2["Bob"]
    if exists {
        fmt.Println("Bob's age:", value)
    }

    value2, exists2 := m2["NonExistent"]
    if !exists2 {
        fmt.Println("NonExistent not found, value:", value2)  // 0 (zero value)
    }

    // Delete
    delete(m2, "Alice")

    // Iterate map (order undefined)
    for key, value := range m1 {
        fmt.Printf("%s: %d\n", key, value)
    }
}

Key points about map:

  • map is unordered (iteration order may vary each time)
  • Reading non-existent key returns the type’s zero value
  • Use value, exists := m[key] to check if key exists when reading
  • map is a reference type (modifying inside function affects original map)

struct (Structures)

Go has no classes, it uses struct and method combination to implement object-oriented:

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
// Define struct
type Person struct {
    Name string
    Age  int
}

// Struct method (will be covered in detail later in the series)
func (p Person) Introduce() string {
    return fmt.Sprintf("My name is %s, I'm %d years old", p.Name, p.Age)
}

func main() {
    // Create struct
    p1 := Person{Name: "Alice", Age: 25}
    p2 := Person{"Bob", 30}             // initialize by order
    p3 := Person{}                      // zero value

    // Access fields
    fmt.Println(p1.Name)                // Alice
    fmt.Println(p2.Age)                 // 30

    // Modify fields
    p1.Age = 26

    // Struct pointer
    p4 := &Person{Name: "Charlie", Age: 35}
    p4.Age = 36                         // auto dereference

    // Call method
    fmt.Println(p1.Introduce())
}

Pointer Basics

Go has pointers, but no pointer arithmetic (for safety):

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
func main() {
    // Declare pointer (zero value is nil)
    var p *int

    // Take address
    x := 42
    p = &x  // p points to x's address

    // Dereference
    fmt.Println(*p)  // 42

    // Modify value through pointer
    *p = 100
    fmt.Println(x)   // 100

    // Function parameters (pass by value vs pass by pointer)
    num := 10
    modifyByValue(num)
    fmt.Println(num)          // 10 (unchanged)

    modifyByPointer(&num)
    fmt.Println(num)          // 20 (changed)
}

func modifyByValue(n int) {
    n = 20  // doesn't affect original variable
}

func modifyByPointer(n *int) {
    *n = 20  // modifies original variable
}

Go pointers vs C pointers:

  • Go doesn’t have: Pointer arithmetic (p++), void*, function pointers (use function values instead)
  • Go has: & for taking address, * for dereferencing, nil pointers
  • Safety: Go’s pointers cannot point to invalid addresses, reducing memory safety risks

Summary

We have now covered the core of Go’s basic syntax—variables and constants, basic data types, control flow, functions, composite types, and pointers. You can now read most Go code.

If one sentence is enough: Go’s syntax is simple and intuitive, variable declarations are flexible (var/:=), type conversion must be explicit, control flow has only one form (for loop), functions support multiple return values, slices and maps are the most commonly used composite types, pointers are safe but have no arithmetic.

The next article will dive into Go’s object-oriented features—struct, methods, interfaces, and Go’s unique error handling pattern (error interface, panic/recover).