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:
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:
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:
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 Category | Go Types | Description |
|---|
| Signed integers | int8, int16, int32, int64 | 8/16/32/64-bit signed integers |
| Unsigned integers | uint8, uint16, uint32, uint64 | 8/16/32/64-bit unsigned integers |
| Platform-dependent | int, uint | 32 bits on 32-bit systems, 64 bits on 64-bit systems |
| Pointer-sized | uintptr | Unsigned integer large enough to store a pointer |
| Floating-point | float32, float64 | IEEE 754 standard (float64 recommended) |
| Complex numbers | complex64, complex128 | Real and imaginary parts are both float32/float64 |
| Boolean | bool | true / false |
| String | string | UTF-8 encoded immutable byte sequence |
Type Conversion
Go does not support implicit type conversion, explicit conversion is required:
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):
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:
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):
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
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:
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:
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:
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):
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:
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:
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):
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).