2.1 变量、常量与基本类型
上一章我们把项目骨架搭好了,internal/task/task.go 里已经有一个空壳 Task 结构体,但还没解释它为什么长那样。本节就补上这一课:先讲 Go 怎么声明变量,再讲它有哪些基本类型、零值是什么,最后回到 Task,解释 ID 为什么用 int64、Title 为什么是 string、Done 为什么是 bool。
本节把 TaskAPI 推进到「有第一个数据结构」:为
Task定下ID int64、Title string、Done bool三个字段,并用一组常量描述任务状态。到本节结束,项目就有了可承载数据的类型,下一节的控制流才有东西可操作。
2.1.1 var 的四种写法
Go 的变量声明比多数语言啰嗦,但每一种写法都有明确用途:
var count int // 1. 声明并取零值
var title string = "写第一章" // 2. 声明并初始化(类型可省略)
var rate = 0.75 // 3. 类型由右值推断
count = 1 // 4. 仅赋值
第二种写法里的类型其实可以省——var title = "写第一章" 与 var title string = "写第一章" 等价,编译器会推断出 string。那为什么还要写类型?当右值的类型不够明确时(比如 var x float64 = 1 想强调是浮点),显式写出更有可读性。
批量声明用括号包起来:
var (
id int64
title string
done bool
)
这种形式常见于包级变量的集中声明,比逐个 var 更整齐。
2.1.2 := 短变量声明
函数内部最常用的是 :=:
title := "写第一章"
count := 0
rate := 0.75
它等价于「声明 + 推断类型 + 赋值」三合一。但有四条限制必须记住:
| 限制 | 说明 |
|---|---|
| 只能在函数内使用 | 包级作用域不能用 := |
| 至少要有一个新变量 | a, b := 1, 2 后写 a, b := 3, 4 会编译错误 |
| 不能指定类型 | x int := 1 非法 |
| 可部分复用 | a, c := 1, 2 中若 a 已存在,只新增 c,a 被赋值 |
第三条限制有个例外值得说明:x := int64(1) 是合法的——那不是「指定类型」,而是把右值转换成 int64 再推断。写法不同,含义也不同。
2.1.3 零值:Go 的默认可用哲学
Go 没有「未初始化变量」这个概念。每个变量声明后立刻拥有一个确定的零值:
| 类型 | 零值 |
|---|---|
bool | false |
string | ""(空串,不是 nil) |
| 所有整数类型 | 0 |
float32 / float64 | 0 |
byte | 0 |
rune | 0 |
| 指针、切片、map、channel、函数、接口 | nil |
实测一下:
package main
import "fmt"
func main() {
var count int
var title string
var rate float64
var ok bool
fmt.Printf("count=%d title=%q rate=%v ok=%v\n", count, title, rate, ok)
}
count=0 title="" rate=0 ok=false
零值哲学的价值在于:结构体的零值通常就是可用的。var t Task 立刻是一个 ID 为 0、标题为空、未完成的任务,不需要构造函数,也不会因为「字段没赋值」而崩溃。这个特性深刻影响了 Go 的 API 设计——标准库里大量类型(sync.Mutex、bytes.Buffer、strings.Builder)的零值开箱即用。
2.1.4 基本类型清单
Go 的基本类型不多,一次列全:
| 类别 | 类型 | 说明 |
|---|---|---|
| 布尔 | bool | 只有 true / false,不能与整数互转 |
| 字符串 | string | 不可变的 UTF-8 字节序列 |
| 有符号整数 | int int8 int16 int32 int64 | int 随平台,64 位机上为 64 位 |
| 无符号整数 | uint uint8 uint16 uint32 uint64 uintptr | uint8 即 byte |
| 浮点 | float32 float64 | 无 float,默认字面量是 float64 |
| 复数 | complex64 complex128 | 工程中极少用 |
| 别名 | byte = uint8,rune = int32 | 纯粹是可读性别名 |
几个容易混淆的点:
int不是「32 位」。它随平台字长变化,64 位机上就是 64 位。需要跨平台一致时用int64。rune是int32的别名,用来表示一个 Unicode 码点;byte是uint8的别名,用来表示一个原始字节。二者的区别是 3.3 字符串、rune 与字节 的主角。float32精度有限。同样写0.1,float32实际存的是0.10000000149011611938,float64是0.10000000000000000555,两者不相等。涉及金额等精确计算时不要用浮点。
实测一下宽度与默认字面量类型:
package main
import (
"fmt"
"strconv"
)
func main() {
fmt.Println("int 位数:", strconv.IntSize)
fmt.Println("默认浮点字面量是 float64:", fmt.Sprintf("%T", 0.75))
fmt.Println("默认整数字面量是 int:", fmt.Sprintf("%T", 42))
}
int 位数: 64
默认浮点字面量是 float64: float64
默认整数字面量是 int: int
2.1.5 类型转换:没有隐式转换
Go 最让新手别扭的一条规则是:不同类型之间不存在隐式转换,哪怕两者都是整数:
var i int = 3
var n int64 = 1
// i + n // 编译错误:mismatched types int and int64
var sum int64 = int64(i) + n // 必须显式转换
这条规则看似麻烦,实则消灭了一整类 bug。C 语言里 int 与 unsigned 混算导致的诡异结果、JS 里 "1" + 1 得到 "11",在 Go 里都不会发生——编译器逼你把意图写清楚。
唯一「宽松」的地方是无类型常量:字面量 1、0.75、1 << 62 在赋值时能自动适配目标类型,只要不溢出:
const big = 1 << 62 // 无类型常量
var i64 int64 = big // OK,能装下
// var i32 int32 = big // 编译错误:溢出
溢出在运行时也会发生,且 Go 不会报错,而是静默回绕:
var u uint8 = 255
u++ // 256 装不下,回绕成 0
fmt.Println(u) // 0
所以做计数、索引时优先用 int 或 int64,不要为了省内存选小类型,除非你确实需要它的回绕语义。
2.1.6 常量与 iota:给任务状态建模
常量用 const 声明,它必须在编译期就能确定值,因此不能是函数调用的结果:
const maxTitleLen = 80
const appName = "TaskAPI"
一组相关的常量可以用 iota 自动编号,这是 Go 表达枚举的惯用法:
type Status int
const (
StatusTodo Status = iota // 0
StatusDoing // 1
StatusDone // 2
)
iota 从 0 开始,在每个 const 块内逐行递增;不写值的行自动沿用上一行的表达式,所以 StatusDoing 等于 1、StatusDone 等于 2。三个值都带上 Status 类型,是因为第一行显式写了 Status = iota,后续行继承了这个类型——如果第一行省掉类型,它们就会是无类型的整数常量。
实测确认取值:
package main
import "fmt"
type Status int
const (
StatusTodo Status = iota
StatusDoing
StatusDone
)
func main() {
fmt.Println(StatusTodo, StatusDoing, StatusDone)
fmt.Println(int(StatusDone))
}
0 1 2
2
用 iota 而不用裸的 0/1/2,好处是意图明确、可读性强;坏处是它只是 int 的别名,编译期不会阻止你把一个任意的 3 当成 Status。想要更强的约束,要么写校验函数,要么在章节 4 之后用自定义类型加方法把合法取值收口。
2.1.7 落定 Task 的字段类型
回到项目。现在可以解释 internal/task/task.go 里那三个字段的选择了:
package task
type Task struct {
ID int64 // 用 int64 而非 int,避免 32 位平台上截断
Title string // 字符串零值是空串,天然可判空
Done bool // 布尔零值是 false,新任务默认未完成
}
三个字段各有理由:
ID int64:int在不同平台上宽度不同,一旦 ID 要落库(第 14 章接 PostgreSQL 的bigint),int64是唯一安全的选择。提前统一,省去日后改类型。Title string:字符串零值是"",于是「标题为空」这个非法状态可以用t.Title == ""直接判断,不需要额外的nil检查。Done bool:布尔零值是false,于是var t Task天然就是一个「未完成」的任务,符合直觉。
试一下零值结构与显式初始化:
package main
import "fmt"
type Task struct {
ID int64
Title string
Done bool
}
func main() {
var zero Task
fmt.Printf("零值: %+v\n", zero)
t := Task{ID: 1, Title: "写第一章", Done: false}
fmt.Printf("显式: %+v 标题字符数=%d\n", t, len([]rune(t.Title)))
}
零值: {ID:0 Title: Done:false}
显式: {ID:1 Title:写第一章 Done:false} 标题字符数=4
%+v 会连字段名一起打印,是调试结构体最顺手的格式。注意「写第一章」的 len() 返回的是字节数(3 个汉字各 3 字节,共 12),所以这里用 len([]rune(...)) 取字符数得到 4——这个坑在 3.3 字符串、rune 与字节
会专门拆解。
小结
var有「声明取零值 / 声明并初始化 / 推断类型 / 仅赋值」四种用法;:=是函数内的三合一简写,且至少要有一个新变量。- Go 没有未初始化变量:每种类型都有确定零值,结构体的零值通常可直接使用,这是标准库 API 设计的基石。
- 基本类型分 bool、string、各类定长整数、
float32/64、复数;int随平台(本机 64 位),需要一致宽度时用int64。 byte是uint8别名,rune是int32别名;float32存0.1会有精度损失。- 不存在隐式类型转换,必须显式
int64(x);无类型常量可在不溢出的前提下自动适配。 - 整数溢出静默回绕,计数与索引优先用
int/int64。 const编译期求值;iota在const块内逐行递增,是 Go 的枚举惯用法。Task三个字段的类型选择都有理由:int64防截断、string零值可判空、bool零值为未完成。
类型定了,接下来要让程序根据这些值做判断。下一节 2.2 控制流与循环
会用 if、for、switch 写出 list 命令的骨架,并实现一个能把 os.Args 分发到 add/list/toggle 的命令派发器。想回顾类型背后的项目结构,见 1.3 Go Modules 与项目骨架
。
阅读导航:上一节:1.3 Go Modules 与项目骨架 · 下一节:2.2 控制流与循环 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。