变量与常量
变量的声明方式
Go 语言提供了多种变量声明方式,每种方式适用于不同的场景。理解它们之间的区别是写好 Go 代码的基础。
变量是程序运行过程中其值可以改变的量。在 Go 中,每个变量都有明确的类型,编译器会在编译期进行类型检查。
var 关键字声明
var 是 Go 中最基础的变量声明方式,可以出现在包级别或函数体内:
// 标准声明:指定类型
var name string = "Go"
var age int = 10
// 类型推导:省略类型,编译器根据初始值推导
var score = 95.5
var active = true
// 批量声明
var (
width int = 800
height int = 600
title string = "Hello"
)变量声明规则
var 声明的变量如果没有显式初始化,会被赋予该类型的零值(Zero Value)。var 声明可以出现在函数外部(包级变量),也可以出现在函数内部。
:= 短变量声明
:= 是 Go 中最常用的声明方式,它只能用于函数体内,并且会自动进行类型推导:
package main
import "fmt"
func main() {
name := "Alice" // 推导为 string
age := 25 // 推导为 int
pi := 3.14 // 推导为 float64
isStudent := true // 推导为 bool
fmt.Println(name, age, pi, isStudent)
}短变量声明的限制
:=只能在函数内部使用,不能用于包级别声明。:=左侧至少要有一个新变量,否则编译报错。- 多变量声明时,即使某些变量已经存在,只要有一个是新的就可以使用
:=。
// 正确:num 是新变量,err 已经存在
num, err := strconv.Atoi("42")
// 错误:name 已经声明过,且没有新变量
// name := "Bob" // 编译报错:no new variables on left side of :=
// 正确:如果需要在同一作用域重新赋值,使用 = 而非 :=
name = "Bob"var 与 := 的对比
| 特性 | var 声明 | := 短声明 |
|---|---|---|
| 使用位置 | 函数内外均可 | 仅函数内部 |
| 类型推导 | 支持 | 支持 |
| 零值初始化 | 支持(不赋值时为零值) | 必须赋初始值 |
| 批量声明 | 支持 var () | 不支持 |
| 适用场景 | 包级变量、明确指定类型 | 函数内的局部变量 |
常量
常量是在程序编译期就确定的值,运行期间不可修改。常量只能是基本类型(布尔、数值、字符串)。
const 声明
常量使用 const 关键字声明,声明时必须赋值:
const Pi = 3.14159265
const AppName = "MyApp"
const MaxRetries = 3
// 批量声明
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)
// 显式指定类型
const TimeoutSeconds int = 30iota 枚举器
iota 是 Go 语言的常量计数器,只能在 const 块中使用。在每一个 const 块中,iota 从 0 开始,每新增一行(无论是否使用 iota)自动递增 1。
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)iota 的递增规则
iota 在每个 const 块中重置为 0。每一行都会使 iota 递增,即使该行没有直接使用 iota。未赋值的常量会继承上一行的表达式。
iota 的高级用法:
const (
_ = iota // 0(忽略)
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20 = 1048576
GB // 1 << 30 = 1073741824
TB // 1 << 40
)
const (
Readable = 1 << iota // 1 << 0 = 1(二进制: 0001)
Writable // 1 << 1 = 2(二进制: 0010)
Executable // 1 << 2 = 4(二进制: 0100)
)const (
A = iota // 0
B // 1
C = 100 // 100
D // 100(继承上一行的值,iota 仍然递增但未使用)
E = iota // 4(iota 恢复显式引用)
)零值(Zero Value)
Go 中变量声明但不显式初始化时,编译器会自动赋予该类型的零值。这是 Go 的一个重要设计哲学——变量总有明确的初始值,避免了未初始化变量带来的隐患。
| 类型 | 零值 | 说明 |
|---|---|---|
int / int8 / int16 / int32 / int64 | 0 | 所有整数类型 |
uint / uint8 / … | 0 | 所有无符号整数类型 |
float32 / float64 | 0.0 | 浮点类型 |
bool | false | 布尔类型 |
string | "" | 空字符串 |
| 指针 | nil | 空指针 |
| 切片 | nil | 空切片 |
| 映射 | nil | 空映射 |
| 接口 | nil | 空接口 |
| 通道 | nil | 空通道 |
| 结构体 | 各字段为零值 | 字段为零值的结构体 |
func demonstrateZeroValues() {
var i int
var f float64
var b bool
var s string
var p *int
var slice []int
fmt.Println(i) // 0
fmt.Println(f) // 0
fmt.Println(b) // false
fmt.Println(s) // ""
fmt.Println(p) // <nil>
fmt.Println(slice) // []
}零值不是"未定义"
与 C/Java 不同,Go 中不存在”未定义”的变量。即使你只写了 var x int,x 也有确定的值 0。这避免了因使用未初始化变量而产生的各种 bug。
命名规范与可见性
Go 语言的命名规则直接影响变量的可见性(作用范围):
命名规则
- 变量名由字母、数字、下划线组成
- 必须以字母或下划线开头
- 区分大小写
- 不能使用 Go 语言关键字(
var、const、func、if等)
可见性规则
Go 通过首字母大小写来控制访问权限:
| 首字母 | 可见性 | 示例 |
|---|---|---|
| 大写 | 公开(Exported)——可被其他包访问 | UserName、MaxSize、DB |
| 小写 | 私有(Unexported)——仅当前包内可访问 | userName、maxSize、db |
package mypackage
// 公开变量,其他包可以通过 mypackage.AppName 访问
var AppName string = "MyApp"
// 私有变量,仅 mypackage 包内可访问
var config map[string]string
// 公开常量
const Version = "1.0.0"
// 私有常量
const debugMode = true命名风格建议
Go 命名惯例
- 使用驼峰命名法(camelCase),不使用下划线命名法(snake_case)
- 缩写应保持全大写或全小写:
URL、HTTP、userID(而不是uid) - 局部变量名尽量简短:
i、j、s、ok、err - 包级变量名应更具描述性:
maxRetries、defaultTimeout - 布尔变量通常以
Is、Has、Can、Should开头:isEnabled、hasPermission
作用域
作用域是指变量在程序中可以被访问的范围。Go 语言中的作用域分为:包级作用域、函数级作用域和块级作用域。
包级变量
在函数外部使用 var 或 const 声明的变量,作用域为整个包:
package main
var globalCount int = 0 // 包级变量,整个包内可见
func increment() {
globalCount++ // 可以访问包级变量
}
func getCount() int {
return globalCount
}局部变量
在函数内部声明的变量,作用域仅限于该函数:
func calculateSum(a, b int) int {
sum := a + b // sum 只在 calculateSum 函数内可见
return sum
}
// 错误:sum 在此处不可访问
// fmt.Println(sum)块级作用域
if、for、switch 等控制结构中声明的变量,作用域仅限于该块:
func example() {
x := 10
if x > 5 {
y := 20 // y 仅在 if 块内可见
fmt.Println(x + y) // 正确
}
// fmt.Println(y) // 编译错误:y 未定义
}短变量声明与作用域
if、for 等语句的初始化部分声明的变量,其作用域包含整个语句块。这是 Go 中非常常见的模式:
// if 语句初始化部分声明的 err,作用域覆盖整个 if-else 块
if err := doSomething(); err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("success")
}
// err 在此处不可访问
// for 语句同理
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// i 在此处不可访问变量遮蔽(Shadowing)
内部作用域可以声明与外部同名变量,这会”遮蔽”外层变量:
func shadowing() {
x := 10
fmt.Println(x) // 10
{
x := 20 // 遮蔽了外层的 x
fmt.Println(x) // 20
}
fmt.Println(x) // 10(外层 x 不受影响)
}避免变量遮蔽
变量遮蔽虽然合法,但容易引起混淆,应尽量避免。如果需要修改外层变量,直接使用 = 赋值即可,不要用 := 重新声明。
练习题
练习 1:变量声明与零值
声明以下变量但不赋初始值,然后打印它们的值:一个 int、一个 float64、一个 bool、一个 string、一个 *int 指针。请写出代码并说明输出结果。
解题思路:使用 var 声明变量但不赋初始值,Go 会自动赋予零值。
代码:
package main
import "fmt"
func main() {
var i int
var f float64
var b bool
var s string
var p *int
fmt.Printf("int: %d\n", i) // 0
fmt.Printf("float64: %f\n", f) // 0.000000
fmt.Printf("bool: %t\n", b) // false
fmt.Printf("string: %q\n", s) // ""
fmt.Printf("pointer: %v\n", p) // <nil>
}输出:
int: 0
float64: 0.000000
bool: false
string: ""
pointer: <nil>练习 2:iota 枚举练习
使用 iota 定义一个表示文件权限的常量组:Read = 1、Write = 2、Execute = 4。然后再定义一组表示 HTTP 状态码的常量:Continue = 100、SwitchingProtocols = 101、OK = 200。
解题思路:使用 iota 和位运算定义权限常量;使用 iota + 偏移量 定义状态码常量。
代码:
package main
import "fmt"
const (
Read = 1 << iota // 1 << 0 = 1
Write // 1 << 1 = 2
Execute // 1 << 2 = 4
)
const (
Continue = 100 + iota // 100
SwitchingProtocols // 101
OK // 102 —— 注意这里 iota 是 2,值为 102
)注意
由于 iota 逐行递增,OK 的实际值是 100 + 2 = 102 而非 200。如果需要定义 OK = 200,需要使用另一个 const 块或在中间插入空白行。
修正版本:
const (
Continue = 100 + iota // 100
SwitchingProtocols // 101
)
const (
OK = 200 + iota // 200(新 const 块中 iota 重置为 0)
)练习 3:作用域与变量遮蔽
以下代码的输出是什么?请解释原因。
package main
import "fmt"
func main() {
x := 10
if x > 5 {
x := x + 5
fmt.Println("inside:", x)
}
fmt.Println("outside:", x)
}解题思路:if 块内使用 := 声明了一个新变量 x,遮蔽了外层的 x。
输出:
inside: 15
outside: 10解释:
if块内的x := x + 5创建了一个新的局部变量x,它遮蔽了外层的x- 内层的
x初始化为外层x的值10加上5,等于15 if块结束后,内层x销毁,外层x保持不变,仍为10
