《Go 语言编程入门》2.1 变量、常量与基本类型

本节为 TaskAPI 定义第一个数据结构:先用 var 与 := 的四种写法引出 Go 的零值哲学,再对照列出 bool、string、各类整数、float、byte 与 rune 的取值与默认值,讲清「没有隐式转换」意味着什么;随后用 const 与 iota 给任务状态建模,最后落定 Task 三个字段的类型选择。

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 没有「未初始化变量」这个概念。每个变量声明后立刻拥有一个确定的零值:

类型零值
boolfalse
string""(空串,不是 nil)
所有整数类型0
float32 / float640
byte0
rune0
指针、切片、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 int64int 随平台,64 位机上为 64 位
无符号整数uint uint8 uint16 uint32 uint64 uintptruint8 即 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 控制流与循环 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练