本节摘要:Go 用 var 显式声明、短声明符冒号等号、const 常量三种方式引入名字;所有类型都有确定的零值,未初始化不会得到随机垃圾;整型按位宽家族化命名,字符串是不可变字节序列。本节讲清每种声明方式的适用边界、iota 枚举的玩法,以及 string 与 rune 的字节陷阱。
阅读完本节,你应当能够:
先看一个把三种声明一网打尽的例子:
package main import "fmt" const Pi = 3.14 var globalCount = 100 // 包级变量,用var func main() { var a int = 10 // 显式类型加显式初值 var b = 20 // 类型由右侧推导,b是int c := 30 // 短声明,只能在函数内用 fmt.Println(a, b, c, Pi, globalCount) }
三种方式各有领地。var 是正式声明,包级只能用它;类型可写可不写,不写就推导。短声明(冒号等号)是局部变量的日常写法,冒号左侧必须至少有一个新名字——a, err := f() 里 err 已存在时是赋值,a 是新声明,这个特性在"循环里反复接收错误"时极其顺手。const 在编译期定死,且必须是编译器能算出来的表达式;它没有固定内存地址(除非取地址强制触发分配),本质更接近文本替换加类型检查。
⚠️ 常见坑:短声明写在外层 if 之外却在代码块里又想新建同名变量时,内层的短声明会遮蔽外层——变量遮蔽是 Go 代码评审里最高频的缺陷来源,值得每次怀疑。
Go 不允许"未初始化的脏内存"。任何类型声明后不赋初值,就得到一个确定的零值:
| 类型族 | 零值 | 记忆点 |
|---|---|---|
| 数值(int、float 等) | 0 / 0.0 | 全零 |
| 布尔 | false | 全零的自然延伸 |
| 字符串 | 空串,长度为0 | 不是 nil |
| 指针、切片、map、channel、函数、接口 | nil | 引用类统一 nil |
这个设计直接消灭了一整类"C 里读未初始化变量"的事故。工程上的推论是:很多结构体只需要声明,不需要构造函数——零值即可用。标准库里 bytes.Buffer、sync.Mutex 都是"零值有用"的典范,后面章节会反复遇到。
整型按位宽成套:int8/16/32/64 加平台相关的 int 与 uint(64 位平台上 int 是 64 位),另有 byte(uint8 的别名)与 rune(int32 的别名,专表 Unicode 码点)。常规业务代码无脑用 int 即可;与协议、文件格式打交道时才精确到位宽——这时溢出行为必须心里有数:int8 最大 127,再加 1 会回绕成 -128。
浮点只有 float32 与 float64,遵循 IEEE 754。钱的计算别用浮点,用整数分或者专门的定点方案,这是所有语言的通病不是 Go 的锅。
布尔就是 bool,且 Go 强制 if 的条件必须是布尔表达式——if 1 {} 直接编译错,杜绝了"把赋值当判断"的经典笔误。
字符串是不可变的字节序列。注意两点:下标访问拿到的是字节(byte)不是字符;len 返回字节数不是字符数。一个汉字 UTF-8 编码占 3 字节,所以对中文串直接下标切分,切出来的是乱码字节。按字符遍历要用 for range,它按 UTF-8 解码产出 rune:
s := "Go语言" fmt.Println(len(s)) // 8:两个字母加两个汉字各3字节 for i, r := range s { fmt.Printf("%d:%c ", i, r) // 下标是字节偏移,值是解码后的rune }
const 块里每出现一行,iota 从 0 递增一次。最朴素的枚举:
type Weekday int const ( Sunday Weekday = iota // 0 Monday // 1,重复上一行表达式 Tuesday // 2 )
进阶玩法是位标志,一份表达式贯穿整个块:
const ( Read = 1 << iota // 1 Write // 2 Execute // 4 ) perm := Read | Write // 3:按位或组合权限
跳过某个值用下划线占位(比如让枚举从 1 开始时先放一个 _)。iota 的价值在于"单一事实源":改一处,全序列自动重排,不会出现手写枚举忘了同步的老 bug。
Go 不做任何隐式数值转换,int 与 int64 混算必须手动转:int64(a) + b。显式转换啰嗦,但每一处宽度变化都看得见——排障时这是恩人。转换语法就是"目标类型加括号":float64(x)、string(65)(得到字母 A,注意这是码点转串,不是数字转字符串;数字转字符串要用 strconv 包的格式化函数)。
💡 关键直觉:把 Go 的类型系统当成海关——任何东西跨境(跨类型)都要填表申报,麻烦,但走私(隐式截断、符号扩展错误)绝迹。
再答几个新手高频疑问。:= 和 var 到底用哪个? 包级只能 var;局部默认 :=,需要零值语义(暂时不想给初值)时用 var 声明。为什么编译器报"声明了但未使用"? Go 把未使用变量当编译错误而不是警告——死变量是 bug 的温床,直接拒编译,下划线是唯一的"我知道我没用它"。string 能修改吗? 不能,任何"修改"都产生新串;需要可变字节缓冲就用字节切片,或标准库的字符串构造器。iota 从几开始? 零。要跳过值就插下划线占位。不同整型能直接比较吗? 类型不同就不能比较,先转换;比较本身没问题,溢出回绕才是陷阱来源。
下一节把变量放进流程里:for、if 与 switch。