变量、常量与基本类型
需要预先了解:
1. 变量、常量与基本类型
上一篇把环境搭好并跑通了第一个程序,这篇开始进入语法。Go 的变量声明有几种固定写法,const 的规则比大多数语言严格,而基本类型里 rune、byte 和 int 的宽度是最容易搞混的几个点。
1.1 变量声明:var
var 是最基础的声明关键字,有下面几种形式:
var a int = 10 // 完整写法:类型 + 初值
var b = 20 // 省略类型,由右边的值推断
var c, d = 1, 2 // 一次声明多个,类型分别推断
var e int // 只声明不赋值,取零值
类型推断只在有初值的时候生效,var e 这种没有初值的写法必须写类型,否则编译器无法知道 e 是什么。
变量也可以成组声明,适合把相关的变量放在一起:
var (
name string = "一撮灰"
age int = 3
)
1.2 零值
**零值(zero value)**是变量声明后未显式赋值时自动获得的值。Go 没有"未初始化变量"这种状态——任何变量一诞生就有确定的值,因此不存在读取未初始化内存的问题。
| 类型 | 零值 |
|---|---|
| 数值类型 | 0 |
bool |
false |
string |
""(空字符串) |
| 指针、切片、map、channel、函数、接口 | nil |
用 fmt.Printf 把各类型的零值打印出来:
var (
i int
f float64
b bool
s string
p *int
sl []int
m map[string]int
ch chan int
fn func()
)
func main() {
fmt.Printf("int 的零值: %d\n", i)
fmt.Printf("float64 的零值: %v\n", f)
fmt.Printf("bool 的零值: %v\n", b)
fmt.Printf("string 的零值: %q\n", s)
fmt.Printf("指针、切片、map、channel、函数、接口的零值: %v %v %v %v %v %v\n", p, sl, m, ch, fn, nil)
fmt.Println(p == nil, sl == nil, m == nil, ch == nil, fn == nil)
}
输出:
int 的零值: 0
float64 的零值: 0
bool 的零值: false
string 的零值: ""
指针、切片、map、channel、函数、接口的零值: <nil> [] map[] <nil> <nil> <nil>
true true true true true
有两点值得留意:
- nil 切片打印出来是
[],nil map 打印出来是map[],看起来像"空的",但它们和nil相等,说明确实是零值状态。 - 零值设计让很多类型可以直接使用,不需要额外初始化:
var buf bytes.Buffer拿到的就是一个可用的空缓冲区,var wg sync.WaitGroup拿到的就是一个可用的等待组。
1.3 短变量声明 :=
:= 是函数内部最常用的声明方式,它同时完成"声明 + 赋值 + 类型推断":
x := 100
y, z := 1, "hello"
它的规则有三条,每条都容易踩坑。
1. 只能在函数内部使用。
name := "一撮灰" // 编译错误:non-declaration statement outside function body
func main() {
name := "一撮灰" // 正确
}
包级别的变量必须用 var(或 const)。原因是 := 是语句而不是声明,包级别的位置只接受声明。
2. 左侧至少要有一个新变量。
x := 1
x, y := 2, 3 // 合法:y 是新的
如果左侧全是已存在的变量,编译器会报错:
x := 1
x := 2
no new variables on left side of :=
注意这个报错有个重要前提:"新变量"是相对于同一个作用域而言的。如果左侧的变量属于外层作用域,那么 := 会新建一个内层变量,而不是赋值给外层变量。这就是下一节的变量遮蔽。
3. 类型一旦确定就不能改变。
x := 1
x = "hello" // 编译错误:cannot use "hello" (untyped string constant) as int value
:= 和 var 的区别只是写法,类型规则完全一样:Go 的变量类型是固定的,不存在"变量可以指向任何类型"的情况(那要用接口,见 接口)。
1.4 变量遮蔽(shadow)
在内层作用域用 := 声明一个与外层同名的变量,就叫变量遮蔽。内层变量会"挡住"外层变量,离开作用域后外层变量依旧存在:
v := 1
fmt.Println("外层 v =", v)
{
v := 2 // 新变量,遮蔽外层 v
fmt.Println("内层 v =", v)
}
fmt.Println("回到外层 v =", v)
输出:
外层 v = 1
内层 v = 2
回到外层 v = 1
单看这个例子,遮蔽只是让人有点晕。真正危险的是它与 err 的组合——错误处理时最常见的陷阱之一:
func do() error { return errors.New("出错了") }
func main() {
var err error
if true {
err := do() // 这里的 := 新建了一个内层 err
fmt.Println("内层:", err)
}
fmt.Println("外层:", err) // 外层 err 仍然是 nil
}
输出:
内层: 出错了
外层: <nil>
if、for、switch 都会引入新的作用域,在它们的花括号内用 := 处理错误,很容易把错误"关"在内层。规避办法是:当你想给外层变量赋值时,用 = 而不是 :=;或者用 go vet 的 shadow 分析(需要额外开启)来发现问题。
1.5 常量 const
const 声明的值必须在编译期就能确定,因此右边只能是字面量、其他常量,或由它们构成的常量表达式:
const Pi = 3.14159
const (
A = 1
B = "hello"
C = true
)
const Big = 1 << 62 // 常量表达式在编译期求值
编译期求值意味着下面这种写法不合法:
const Now = time.Now() // 编译错误:time.Now() 的值要到运行时才知道
无类型常量
Go 的常量可以没有类型,这叫无类型常量。它有一个很实用的性质:在赋值或参与运算时,会按上下文自动"适配"到目标类型,只要目标类型能容纳它的值:
const Pi = 3.14159
var f float64 = Pi // 适配到 float64
var i8 int8 = 100 // 适配到 int8,100 在 int8 范围内
一旦超出范围,编译期就报错:
var i2 int8 = 200 // 编译错误:constant 200 overflows int8
constant 200 overflows int8
无类型常量的精度可以非常高,const n = 1.0 / 3.0 在参与运算前不会因为某个具体类型而损失精度:
const n = 1.0 / 3.0
fmt.Println(n) // 0.3333333333333333
fmt.Printf("%.10f\n", n) // 0.3333333333
iota
iota 是常量声明块里的行号计数器,每换一行就加 1,从 0 开始。最常见的用法是做枚举:
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
)
省略类型和值的行会复用上一行的表达式,因此 Monday 等价于 Monday Weekday = iota。
几个常见变体:
// 用 _ 跳过某个值
const (
_ = iota
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
)
// 位掩码枚举
const (
ReadFlag = 1 << iota // 1
WriteFlag // 2
ExecFlag // 4
)
输出:
0 1 2 3
1024 1048576 1073741824
1 2 4
注意:
iota只在const块内有效,且计数的是"行"而不是"有名字的常量"。如果某一行是注释或空行,计数不受影响;如果一行里声明了多个常量,它们的iota值相同。
1.6 基本类型
| 类型 | 说明 |
|---|---|
int |
有符号整数,宽度与平台相关(32 位平台 4 字节,64 位平台 8 字节) |
uint |
无符号整数,宽度与平台相关 |
int8 ~ int64 |
固定宽度的有符号整数,分别是 8/16/32/64 位 |
uint8 ~ uint64 |
固定宽度的无符号整数 |
uintptr |
无符号整数,宽度足以容纳一个指针值,供 unsafe 相关场景使用 |
float32 / float64 |
单精度 / 双精度浮点数,遵循 IEEE-754 |
complex64 / complex128 |
复数,实部与虚部分别是 float32 / float64 |
bool |
true 或 false |
string |
UTF-8 编码的只读字节序列(详见 map 与字符串) |
byte |
uint8 的类型别名 |
rune |
int32 的类型别名,表示一个 Unicode 码点 |
int 的宽度由平台决定,用 strconv.IntSize 可以在运行时查到:
fmt.Println("int 位数:", strconv.IntSize)
在 64 位平台上输出 64,在 32 位平台上输出 32。所以需要精确控制宽度时,应该显式使用 int32、int64 这类固定宽度的类型——比如处理二进制协议、文件格式时。
byte 和 rune 是别名,不是新类型,它们和 uint8、int32 完全等价:
var b byte = 'A'
var r rune = '中'
fmt.Printf("%T %T\n", b, r)
uint8 int32
1.7 类型转换
Go 里不存在隐式类型转换,不同类型之间的赋值必须显式写转换:
var i int = 1
var f float64 = i // 编译错误
var j int32 = 10
var k int64 = j // 编译错误
cannot use i (variable of type int) as float64 value in variable declaration
cannot use j (variable of type int32) as int64 value in variable declaration
这条规则看起来啰嗦,但它让"哪里发生了类型变化"在代码里一目了然,也避免了大整数悄悄变成小整数这类问题。正确的写法是:
var i int = 42
var f float64 = float64(i)
var u uint = uint(i)
数值转换可能丢失信息
转换是"按位截断",不检查范围:
var big int32 = 300
var small int8 = int8(big)
fmt.Println(big, small)
300 44
300 的二进制是 100101100,截断成 8 位后剩下 00101100,也就是 44。浮点转整数则直接丢弃小数部分(向零取整):
pi := 3.99
fmt.Println(int(pi), int(-pi))
3 -3
string(整数) 的陷阱
把整数转成字符串,很多人会以为得到的是数字的十进制写法,实际得到的是"编码为该整数的字符":
fmt.Println(string(65)) // A
fmt.Println(string(97)) // a
fmt.Println(string(rune(0x4e2d))) // 中
想要数字的十进制文本,应该用 strconv:
fmt.Println(strconv.Itoa(65)) // "65"
go vet 会主动报出这类可疑写法:
main.go:6:14: conversion from untyped int to string yields a string of one rune, not a string of digits
字符串与数值互转:strconv
strconv 包负责字符串与基本类型之间的转换:
n, err := strconv.Atoi("123") // 字符串转 int
fmt.Println(n, err) // 123 <nil>
bad, err := strconv.Atoi("12x")
fmt.Println(bad, err) // 0 strconv.Atoi: parsing "12x": invalid syntax
注意转换失败时返回的是零值加错误,而不是 panic,这是 Go 处理"可能失败的操作"的统一风格(详见 错误处理)。
1.8 rune 与 byte 的初步区分
字符串 "Go语言" 里有 4 个字符,但 len 返回的不是 4:
s := "Go语言"
fmt.Println(len(s), utf8.RuneCountInString(s))
8 4
原因是 string 的底层是 UTF-8 字节序列,len 统计的是字节数。英文字母占 1 个字节,中文在 UTF-8 里占 3 个字节,所以 "Go语言" 是 2 + 3 + 3 = 8 个字节。
byte(uint8)用来表示单个字节;rune(int32)用来表示一个 Unicode 码点,也就是"一个字符"。
用下标访问字符串得到的是 byte,用 for range 遍历得到的是 rune:
fmt.Println(s[0], s[1], string(s[0])) // 71 111 G
for i, r := range s {
fmt.Printf("字节下标 %d 处是 rune %q,码点 %U\n", i, r, r)
}
字节下标 0 处是 rune 'G',码点 U+0047
字节下标 1 处是 rune 'o',码点 U+006F
字节下标 2 处是 rune '语',码点 U+8BED
字节下标 5 处是 rune '言',码点 U+8A00
range 的下标是字节下标,所以第二个中文的起点是 5 而不是 3。字符串的遍历细节与 strings 包的常用操作放在 map 与字符串 展开。
1.9 练习
- 声明一个
int8变量并赋值为 127,再让它自增 1,观察结果;然后用const写同样的代码,看编译器在什么时候报错。 - 用
iota定义一个枚举,要求值从 100 开始、步长为 5。 - 写一段代码,分别用下标和
for range遍历"héllo",比较两种方式得到的元素个数,并解释差异。



