变量与表达式(Variables & Expressions)

开篇故事

想象你有一个工具箱,里面装着各种工具:螺丝刀、锤子、尺子。你给每个工具贴上标签,下一次需要时就知道去哪里找。Go 中的变量就像这些贴标签的工具箱——它们帮你存储和管理程序中的数据。常量则是那些你钉在墙上的工具——一旦放好,就不会再移动。


本章适合谁

如果你是 Go 初学者,想理解如何存储数据、声明常量和进行基本计算,本章适合你。这是所有编程的基础,即使你是第一次接触编程也能理解。


你会学到什么

完成本章后,你可以:

  1. 使用 var 关键字声明变量,理解何时需要显式写类型
  2. 使用 := 短变量声明,理解类型推断(type inference)
  3. 使用 const 声明常量,理解不可变值的意义
  4. 使用 iota 生成连续的常量编号
  5. 区分"应该用变量"和"应该用常量"的场景

前置要求

本章是 Go 的第一章,不需要前置知识。如果你有任意编程基础(Python、JavaScript、Java 等)会更容易理解。


第一个例子

让我们从最简单的变量声明开始:

var language string = "Go"
var lessonCount int = 12
var ready bool = true

关键概念

  • var - 声明变量的关键字
  • 类型写在变量名后面(这是 Go 和其他语言的重要区别)
  • 每个变量声明后都有初始值

原理解析

1. 变量声明(var)

在 Go 中,var 是最基础的变量声明方式:

var language string = "Go"
var lessonCount int = 12

为什么 Go 要把类型写在后面?

  • 其他语言(C/Java):string language = "Go";
  • Go:var language string = "Go"

Go 的设计者认为,当类型显而易见时,你根本不需要写它。这让 var:= 的视觉模式更一致。

类比

就像你填写表格——先写名字,再写类型("姓名:张三"),而不是先写类型再写名字。

2. 短变量声明(:=)

当类型显而易见时,Go 允许你省略 var 和类型:

name := "Alice"
age := 30

var vs := 的选择指南

场景推荐写法原因
函数内部,类型明显:=简洁,最常见
想突出类型var让读者注意到类型
包级别(函数外部)var:= 只能在函数内部使用
需要零值(zero value)varvar x int 会得到 0

3. 类型推断(Type Inference)

Go 会根据右侧的值自动推断变量类型:

total := 3          // int
progress := 75.5    // float64
note := "go"        // string
ready := true       // bool

重要:虽然 Go 帮你推断类型,但你依然需要知道最终推断出的是什么类型。因为类型会影响运算、函数调用和接口匹配。

4. 常量(Constants)

常量是永远不变的值:

const courseName = "hello-go"
const maxRetries = 3

常量 vs 变量

特征变量 (var/:=)常量 (const)
可修改✅ 是(除非用 const)❌ 否
运行时确定✅ 是❌ 否(编译期已知)
可以使用函数值✅ 是❌ 否(只能用字面量)
生命周期作用域内整个程序运行期间

何时使用常量

  • 配置值(最大重试次数、超时时间)
  • 状态名("draft", "review", "published")
  • 数学常数

5. iota 生成连续常量

iota 是 Go 用来生成连续常量值的内建标识符:

const (
    stageDraft = iota    // 0
    stageReview          // 1
    stagePublished       // 2
)

类比

就像自动编号的发票——你不需要手动写 1、2、3,机器帮你递增。

为什么用 iota 而不是手写数字?

  • 不容易出错(不会漏掉某个编号)
  • 更容易维护(中间插入新状态时,后面的自动调整)
  • 意图更清晰(读者一眼就知道这是连续编号)

常见错误

错误 1: 在函数外部使用 :=

package main

x := 5  // ❌ 编译错误!

func main() {}

编译器输出:

syntax error: non-declaration statement outside function body

修复方法

在包级别使用 var

package main

var x = 5  // ✅

func main() {}

错误 2: 常量使用运行时值

const now = time.Now()  // ❌ 编译错误!

编译器输出:

const initializer time.Now() is not a constant

修复方法

改用 var

var now = time.Now()  // ✅

错误 3: 未使用变量的警告

func main() {
    unused := 5  // ⚠️ 编译错误!Go 不允许未使用的变量
}

编译器输出:

unused declared and not used

修复方法

使用前缀下划线或真正使用它:

func main() {
    _ = 5  // ✅ 编译器知道你是故意的
}

动手练习

练习 1: 预测输出

不运行代码,预测下面代码的输出:

x := 5
x = x + 1
{
    x := x * 2
    fmt.Println("内部:", x)
}
fmt.Println("外部:", x)
点击查看答案

输出:

内部: 12
外部: 6

解析

  1. x = 5 - 初始值
  2. x = 5 + 1 = 6 - 修改 x
  3. 内部作用域:x := 6 * 2 = 12 - 新变量遮蔽了外部 x
  4. 内部作用域结束,内部 x 失效
  5. 外部 x 仍然是 6

练习 2: 修复错误

下面的代码有编译错误,请修复:

const maxUsers = 100
maxUsers = 200  // ❌ 错误
点击查看修复方法

修复

var maxUsers = 100  // 改为 var
maxUsers = 200      // ✅ 现在可以修改了

或者,如果你确实需要常量,就不要修改它:

const maxUsers = 100
// maxUsers = 200  // ❌ 常量不能修改
newMax := 200       // ✅ 创建新变量

练习 3: 使用 iota

改写下面的代码,使用 iota 代替手动编号:

const (
    statusPending = 0
    statusActive = 1
    statusCompleted = 2
    statusArchived = 3
)
点击查看参考实现
const (
    statusPending = iota  // 0
    statusActive          // 1
    statusCompleted       // 2
    statusArchived        // 3
)

好处

  • 不需要手动写数字
  • 中间插入新状态时,后面的自动调整
  • 意图更清晰

故障排查 (FAQ)

Q: 什么时候应该用 var,什么时候用 :=

A: 遵循这个原则:

  • 函数内部,类型明显 → 用 :=(90% 的情况)
  • 想突出类型 → 用 var
  • 包级别(函数外部) → 只能用 var
  • 需要零值语义 → 用 var(如 var count int 得到 0

示例:

// ✅ 好的实践
var config *Config  // 包级别,突出类型

func main() {
    name := "hello"  // 类型明显是 string
    var count int    // 需要零值 0
}

Q: 为什么 Go 不允许未使用的变量?

A: 这是 Go 的设计哲学——未使用的变量通常是 bug 的信号。

  • C/Java/Python:未使用变量只是警告
  • Go:未使用变量是编译错误

好处

  1. 减少代码噪音(没有"死代码")
  2. 避免拼写错误(userName vs userNmae
  3. 强制你清理不需要的代码

Q: constvar 的性能有区别吗?

A: 有,但通常可以忽略。

  • const 在编译期求值,零运行时开销
  • var 在运行期初始化

实际影响:对于简单类型(int, string),差异在纳秒级别,不需要担心。


知识扩展 (选学)

零值(Zero Value)

Go 中每个类型都有一个"零值"——当你声明变量但不赋值时的默认值:

var i int      // 0
var f float64  // 0.0
var s string   // ""
var b bool     // false
var p *int     // nil

为什么 Go 要设计零值?

  • 避免未初始化变量的 bug(其他语言中常见)
  • 简化代码(不需要处处检查 null)
  • var 声明更简洁

变量遮蔽(Shadowing)

Go 允许在内部作用域用 := 创建同名变量——新变量会"遮蔽"旧变量:

x := 5
{
    x := 10  // 新 x 遮蔽了外部 x
    fmt.Println(x)  // 10
}
fmt.Println(x)  // 5

遮蔽的优势

  • 可以改变类型
  • 可以复用名称(代码更简洁)
  • 在不同作用域有不同含义

遮蔽的风险

  • 如果遮蔽让代码更难理解,使用不同的名称

工业界应用:配置管理

场景:Web 服务器配置

const (
    defaultPort = 8080
    defaultHost = "127.0.0.1"
    maxConnections = 1000
)

func main() {
    // 配置在初始化后不应该改变
    port := defaultPort
    host := defaultHost

    fmt.Printf("服务器启动在 %s:%d\n", host, port)
}

为什么常量很重要

  • 防止运行中意外修改配置
  • 集中定义,易于修改
  • 编译器保证配置不会被篡改

小结

核心要点

  1. var 是最基础的声明方式 - 可以在任何地方使用
  2. := 是最常见的写法 - 只能在函数内部,依赖类型推断
  3. const 表达不变的值 - 编译期已知,运行时不可修改
  4. iota 生成连续常量 - 适合状态编号、枚举风格常量
  5. Go 不允许未使用的变量 - 这是编译错误,不是警告

关键术语

  • Type Inference (类型推断): 编译器根据右侧值自动推断变量类型
  • Zero Value (零值): 变量声明但未赋值时的默认值
  • Shadowing (遮蔽): 在内部作用域用同名变量覆盖外部变量
  • iota: Go 内建的连续常量生成器

下一步


术语表

English中文
Variable变量
Constant常量
Type Inference类型推断
Zero Value零值
Short Declaration短变量声明
Shadowing遮蔽

源码