本文基于 Go 1.26。
上一篇我们聊了 Go 的设计理念和第一个 Hello World 程序。这一篇直接上手语法——变量、类型、控制流、函数、复合类型和指针。核心目标只有一个:让你能读懂和写出基本的 Go 代码。
Go 的语法设计哲学是简洁胜过繁复——去掉不必要的语法糖,保持清晰直观。如果你从其他语言转过来,会发现 Go 的学习曲线相对平缓,因为它的语法特性不多,但每一个都很实用。
变量与常量
Go 的变量声明非常灵活,提供了多种方式,核心原则是明确意图,避免歧义。
var 声明
var 关键字可以声明变量,支持多种形式:
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() {
// 标准声明(显式类型)
var name string = "Alice"
var age int = 25
// 类型推断
var city = "Beijing" // 推断为 string
var score = 95.5 // 推断为 float64
// 批量声明(推荐用于相关变量)
var (
username string = "user123"
password string = "secret"
isActive bool = true
)
fmt.Println(name, age, city, score)
fmt.Println(username, password, isActive)
}
|
短声明 :=
Go 最常用的变量声明方式是短声明 :=,它只能在函数内部使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| func main() {
// 短声明(自动类型推断)
name := "Bob"
count := 42
pi := 3.14159
// 多变量同时声明
x, y := 10, 20
// 在 if/for 等控制结构中声明临时变量(非常常见)
if err := doSomething(); err != nil {
fmt.Println("Error:", err)
}
fmt.Println(name, count, pi, x, y)
}
func doSomething() error {
return nil
}
|
重要细节:
:= 只能用于函数内,不能用于包级变量- 同一个作用域内,
:= 不能重复声明已有变量(除非至少有一个是新变量) := 会自动推断类型,你不需要写类型
常量 const
常量用 const 声明,编译期确定:
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
)
|
零值初始化:Go 中每个类型都有零值,声明变量但未初始化时自动获得零值:
- 数值类型:
0 - 布尔类型:
false - 字符串:
""(空字符串) - 指针、slice、map、channel、interface、function:
nil
基本数据类型
Go 的类型系统清晰直观,没有复杂的类型层级。
基础类型
| 类型类别 | Go 类型 | 说明 |
|---|
| 有符号整数 | int8, int16, int32, int64 | 8/16/32/64 位有符号整数 |
| 无符号整数 | uint8, uint16, uint32, uint64 | 8/16/32/64 位无符号整数 |
| 平台相关 | int, uint | 32 位系统上为 32 位,64 位系统上为 64 位 |
| 指针大小 | uintptr | 无符号整数,足够存储指针 |
| 浮点数 | float32, float64 | IEEE 754 标准(推荐用 float64) |
| 复数 | complex64, complex128 | 实部和虚部都是 float32/float64 |
| 布尔值 | bool | true / false |
| 字符串 | string | UTF-8 编码的不可变字节序列 |
类型转换
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) // 显式转换
var u uint = uint(f)
// 字符串转换(小心陷阱)
var s string = string(i) // 不是 "42",而是对应 Unicode 码点(42 是 '*')
var s2 string = string(65) // "A"(ASCII 65)
// 正确的整数到字符串转换
var s3 string = fmt.Sprintf("%d", i) // "42"
// 字符串到整数
var num int
fmt.Sscanf("42", "%d", &num) // num = 42
fmt.Println(f, u, s, s2, s3, num)
}
|
常见陷阱:
string(65) 不是 "65",而是 "A"(ASCII 65)string([]byte{'H','i'}) 才是字符串字面量转换的正确方式- 整数到字符串用
fmt.Sprintf("%d", num) 或 strconv.Itoa(num)
控制流
Go 的控制流简洁直接——只有 if、for、switch 三种,没有 while。
if/else
Go 的 if 语句支持前置语句(常用于错误处理):
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() {
// 标准 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("优秀")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
// 带前置语句的 if(非常常见)
if err := doWork(); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Work completed successfully")
}
func doWork() error {
return nil
}
|
前置语句的作用域:前置语句声明的变量只在 if 语句块内可见,这避免了命名污染。
for 循环(唯一循环)
Go 只有 for 一种循环,但它可以扮演 C 语言中 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. 标准 C 风格 for 循环
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 2. 类似 while 的条件循环(只有条件)
i := 0
for i < 10 {
fmt.Println(i)
i++
}
// 3. 无限循环(死循环)
count := 0
for {
fmt.Println("Tick")
count++
if count >= 5 {
break
}
}
// 4. for range(遍历集合)
numbers := []int{1, 2, 3, 4, 5}
for index, value := range numbers {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
// 只要值,不要索引
for _, value := range numbers {
fmt.Println("Value:", value)
}
// 只要索引,不要值
for index := range numbers {
fmt.Println("Index:", index)
}
// 遍历 map
m := map[string]int{"Alice": 25, "Bob": 30}
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// 遍历字符串(按 rune 遍历,不是按字节)
for index, runeValue := range "Hello, 世界" {
fmt.Printf("Index: %d, Rune: %c\n", index, runeValue)
}
}
|
for range 的坑:
- 遍历字符串时,
index 是字节位置(不是字符位置),runeValue 是 Unicode 字符 - 遍历 slice/array 时,元素是拷贝,不是引用
- 遍历 map 时,顺序不确定(map 是无序的)
switch
Go 的 switch 不需要 break(不会自动 fallthrough),也可以省略条件(相当于 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() {
// 标准 switch
grade := 'B'
switch grade {
case 'A':
fmt.Println("优秀")
case 'B':
fmt.Println("良好")
case 'C':
fmt.Println("及格")
default:
fmt.Println("其他")
}
// 多条件匹配
num := 3
switch num {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8, 10:
fmt.Println("偶数")
default:
fmt.Println("其他")
}
// 无条件的 switch(替代 if-else if-else)
score := 75
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
// 类型 switch(判断接口类型)
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")
}
}
|
关键点:
- 不需要
break(除非显式 fallthrough) - 可以匹配多个值(
case 1, 3, 5:) - 无条件的
switch 是 if-else if-else 的清晰替代品 - 类型
switch 判断接口的实际类型(接口在后面系列会详细讲)
函数
Go 的函数设计简洁但强大,支持多返回值、命名返回值、可变参数等特性。
基本函数
1
2
3
4
5
6
7
8
9
| // 基本函数(单返回值)
func add(a int, b int) int {
return a + b
}
// 多个参数类型相同时可以简写
func add2(a, b int) int {
return a + b
}
|
多返回值
Go 原生支持多返回值,这是错误处理的基础:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 多返回值
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// 调用多返回值函数
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
// 忽略某个返回值
result2, _ := divide(10, 5) // 忽略错误(不推荐)
fmt.Println("Result2:", result2)
}
|
命名返回值
返回值可以命名,函数内可以直接使用这些变量:
1
2
3
4
5
6
7
8
9
| // 命名返回值
func rectArea(width, height float64) (area float64) {
area = width * height
return // 相当于 return area
}
func main() {
fmt.Println(rectArea(3, 4)) // 12
}
|
命名返回值的优势:
- 自动文档化返回值的含义
- 在复杂函数中减少重复代码
- 隐式
return 返回所有命名返回值
可变参数
Go 使用 ... 声明可变参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 可变参数(类型为 []int)
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
// 调用可变参数函数
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// 展开切片
nums := []int{1, 2, 3}
fmt.Println(sum(nums...)) // 6
}
|
关键点:
- 可变参数本质上是一个切片(
[]int) - 调用函数时用
... 展开切片 - 可变参数必须是参数列表的最后一个
复合类型
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() {
// 数组(固定长度)
var arr1 [5]int // 零值数组 [0 0 0 0 0]
arr2 := [5]int{1, 2, 3, 4, 5} // 显式初始化
arr3 := [...]int{1, 2, 3} // 自动推断长度([3]int)
// 切片(动态长度,更常用)
var s1 []int // nil 切片
s2 := []int{1, 2, 3, 4, 5} // 直接初始化
s3 := make([]int, 5) // make 创建(全 0)
s4 := make([]int, 5, 10) // length=5, capacity=10
// 切片操作
fmt.Println(s2[0]) // 访问元素:1
fmt.Println(s2[1:3]) // 切片:[2 3]
fmt.Println(len(s2)) // 长度:5
fmt.Println(cap(s2)) // 容量:5
// 添加元素(可能触发扩容)
s5 := []int{}
s5 = append(s5, 1)
s5 = append(s5, 2, 3, 4)
fmt.Println(s5) // [1 2 3 4]
// 复制切片
s6 := make([]int, len(s5))
copy(s6, s5)
fmt.Println(s6) // [1 2 3 4]
}
|
数组和切片的关键区别:
- 数组:固定长度,长度是类型的一部分(
[3]int 和 [4]int 是不同类型) - 切片:动态长度,是对数组的引用(引用类型)
- 切片的底层:切片包含三个信息:指针(指向数组)、长度、容量
map(映射)
Go 的 map 是无序的键值对集合:
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() {
// 创建 map
m1 := make(map[string]int) // 空 map
m2 := map[string]int{"Alice": 25, "Bob": 30} // 直接初始化
// 添加/修改
m1["Charlie"] = 35
m1["David"] = 40
// 读取
fmt.Println(m1["Charlie"]) // 35
fmt.Println(m2["Alice"]) // 25
// 读取并判断是否存在
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(零值)
}
// 删除
delete(m2, "Alice")
// 遍历 map(顺序不确定)
for key, value := range m1 {
fmt.Printf("%s: %d\n", key, value)
}
}
|
map 的关键点:
- map 是无序的(每次遍历顺序可能不同)
- 读取不存在的键返回类型的零值
- 读取时用
value, exists := m[key] 判断键是否存在 - map 是引用类型(函数内修改会影响原 map)
struct(结构体)
Go 没有类,用 struct 和方法组合实现面向对象:
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
| // 定义结构体
type Person struct {
Name string
Age int
}
// 结构体方法(在后面系列会详细讲)
func (p Person) Introduce() string {
return fmt.Sprintf("My name is %s, I'm %d years old", p.Name, p.Age)
}
func main() {
// 创建结构体
p1 := Person{Name: "Alice", Age: 25}
p2 := Person{"Bob", 30} // 按顺序初始化
p3 := Person{} // 零值
// 访问字段
fmt.Println(p1.Name) // Alice
fmt.Println(p2.Age) // 30
// 修改字段
p1.Age = 26
// 结构体指针
p4 := &Person{Name: "Charlie", Age: 35}
p4.Age = 36 // 自动解引用
// 调用方法
fmt.Println(p1.Introduce())
}
|
指针基础
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() {
// 声明指针(零值为 nil)
var p *int
// 取地址
x := 42
p = &x // p 指向 x 的地址
// 解引用
fmt.Println(*p) // 42
// 通过指针修改值
*p = 100
fmt.Println(x) // 100
// 函数参数(值传递 vs 指针传递)
num := 10
modifyByValue(num)
fmt.Println(num) // 10(不变)
modifyByPointer(&num)
fmt.Println(num) // 20(改变)
}
func modifyByValue(n int) {
n = 20 // 不影响原变量
}
func modifyByPointer(n *int) {
*n = 20 // 修改原变量
}
|
Go 指针 vs C 指针:
- Go 没有:指针算术(
p++)、void*、函数指针(用函数值替代) - Go 有:
& 取地址、* 解引用、nil 指针 - 安全性:Go 的指针不能指向无效地址,降低了内存安全风险
本篇小结
到此我们已经覆盖了 Go 基础语法的核心——变量与常量、基本数据类型、控制流、函数、复合类型和指针。你现在已经能读懂大部分 Go 代码了。
如果记住一句话就够了:Go 的语法简洁直观,变量声明灵活(var/:=),类型转换必须显式,控制流只有 for 循环一种,函数支持多返回值,切片和 map 是最常用的复合类型,指针安全但无算术。
下一篇我们深入 Go 的面向对象特性——struct、方法、接口,以及 Go 最独特的错误处理模式(error 接口、panic/recover)。