一、Go 代码基本元素
1. 基本代码元素
读代码时,可以先抓住两件事:数据保存在哪里,程序对这些数据做了什么。 变量和常量承载数据,类型约束数据,函数、运算符以及条件和循环组织操作。原文
go
var price int = 12
quantity := 3
const Discount = 2
这里,price 和 quantity 是变量,int 是类型,12、3、2 是直接写出的字面量,Discount 是具名常量。值是具体内容,类型决定变量能保存哪类值。
| 元素 | 例子 | 作用 |
|---|---|---|
| 关键字 | var、const、func |
构成 Go 语法,不能用作自定义名称 |
| 标识符 | price、Discount |
为变量、常量、类型、函数等命名 |
| 类型 | int、string |
约束数据及其操作 |
| 字面量 | 12、"你好" |
直接表示一个值 |
| 运算符 | +、*、< |
计算或比较 |
| 函数 | countBuckets(20) 中的 countBuckets |
组织可调用的操作 |
容易混淆的是:int 虽然由 Go 预先提供,却是预声明的类型名,属于标识符,并不是关键字。Go 语言规范
2. 示例与执行过程
下面的程序生成 20 个随机整数,统计其中小于 5 和大于等于 5 的数量。
go
package main
import (
"fmt"
"math/rand"
)
const Limit = 10
func countBuckets(samples int) (int, int) {
var low, high int
for step := 0; step < samples; step++ {
number := rand.Intn(Limit)
if number < Limit/2 {
low++
} else {
high++
}
}
return low, high
}
func main() {
samples := 20
low, high := countBuckets(samples)
fmt.Println("较小组:", low, "较大组:", high)
fmt.Println("数量核对:", low+high == samples)
}
入口与包
可执行程序从 main 包中的 main() 函数开始执行。import 引入需要使用的包;fmt.Println 是调用 fmt 包的输出函数,rand.Intn 是调用 rand 包的随机数函数。注意:"math/rand" 是导入路径,默认使用的包名是 rand。包命名说明
函数的输入与输出
func countBuckets(samples int) (int, int) 表示:接收一个整数参数,返回两个整数。return low, high 按顺序返回结果,调用处的 low, high := countBuckets(samples) 按相同顺序接收。多返回值说明
循环与分支如何配合
low 和 high 没有显式初始值,使用整数零值 0。零值规则 循环让 step 从 0 走到 19,共执行 20 次。每次抽取一个数,再由 if-else 决定哪个计数加一;++ 表示自增 1。
根据这段代码,每次抽样恰好进入一组,所以两组计数的和一定是 20。各组数量可以不同,并不保证各占一半。完整示例已编译并运行验证。
3. 常见易错点
一、随机数范围和条件边界
rand.Intn(10) 生成的是 0~9,不包含 10。判断 number < 5 时,0~4 进入第一组,5~9 进入第二组,数字 5 归入 else。Intn 官方文档
网页原例使用 rand.Intn(16) 和 < 8,因此实际分组是 0~7 与 8~15。读代码时,尤其要分清"小于"和"小于等于"。
二、声明、赋值和比较
go
count := 4 // 声明并初始化变量
count = 6 // 给已有变量赋值
fmt.Println(count == 6) // 比较,输出 true
:= 用于简短变量声明,= 用于赋值,== 用于判断是否相等。比较是产生布尔结果的表达式,赋值是改变变量内容的语句,二者用途不同。Go 语句规则
三、换行不只是格式问题
go
if count > 0 {
fmt.Println(count)
} else {
fmt.Println("没有数据")
}
函数声明以及这里的 if、三段式 for,左大括号应与前面的内容放在同一行;else 也应跟在前一个右大括号后。Go 会在特定行尾自动插入分号,随意换行可能导致语法错误。写代码时用 gofmt 统一格式即可,不必一开始就记住全部断行规则。分号规则、格式化说明
4. 读代码的思路
先找到程序入口,再看变量保存什么、函数接收和返回什么,最后跟着循环与条件走一遍执行过程。能说明这段程序的数据如何变化,就比单纯记住每个符号的名字更接近真正理解代码。
二、关键字与标识符
1. 关键字与标识符的区别
go
var score int = 90
var 告诉编译器要声明变量,是关键字;score 给变量命名,是标识符;int 表示整数类型,也属于标识符。标识符不只用于变量,还用于类型、常量、函数、包等。原文
Go 的 25 个关键字可以按作用来理解,不必一开始全部背下来:
| 作用 | 关键字 |
|---|---|
| 声明和代码组织 | const、func、import、package、type、var |
| 构造类型 | chan、interface、map、struct |
| 循环与分支等流程控制 | break、case、continue、default、else、fallthrough、for、goto、if、range、return、select、switch |
| 延迟调用与并发执行 | defer、go |
关键字不能拿来给变量或函数起名字。 var type int 是错误的,可以改成 var kind int。关键字规范
预声明标识符不等于关键字。 int、string、true、nil、len 等名称由 Go 预先提供,却不是保留关键字。语法允许在适当作用域中用同名声明遮蔽它们,但这容易妨碍阅读和调用,实际命名应尽量避免。预声明标识符
2. 命名规则与惯例
标识符由 Unicode 字母、数字和下划线组成,首字符只能是字母或下划线,不能是数字。Go 区分大小写,所以 score 与 Score 是不同的名字。标识符规则
| 名字 | 是否合法 | 原因 |
|---|---|---|
userName、user2、_temp |
合法 | 字符和开头都符合规则 |
年龄 |
合法 | 汉字可以属于 Unicode 字母 |
2users |
不合法 | 数字开头 |
user-name、$user |
不合法 | 包含不允许的字符 |
for |
不合法 | 是关键字 |
user.name 不能作为一个完整的变量名,但点号可以在代码中表达成员访问;fmt.Println 同样可以合法地访问包中的函数。
语法合法不代表适合项目协作。 通常用 userName 这样的驼峰式名称;包名简短、小写;缩写尽量保持一致,例如 userID、HTTPClient。Go 命名惯例
3. 导出与访问范围
对于包级别的函数、类型、变量、常量,以及结构体字段和方法,首字母是 Unicode 大写字母时,名称可以导出供其他包访问。导出规则
go
package calculator
func Add(a, b int) int {
return a + b
}
func subtract(a, b int) int {
return a - b
}
其他包导入这个包后,可以调用 calculator.Add,不能直接调用 calculator.subtract。但同一个包里的其他文件也可以使用 subtract。未导出的边界是包,不是单个文件。
名字开头才决定是否满足大写条件:User 可以满足,_User 不满足,中文开头也不满足。函数里的局部变量即使叫 Total,也不会因为大写而成为供其他包访问的变量。
4. 空标识符
单独的 _ 叫空标识符。接收多个返回值时,它可以占住不需要的那个位置:空标识符说明
go
func limits() (int, int) {
return 10, 50
}
// 以下代码放在调用函数中
low, _ := limits()
fmt.Println(low) // 10
这里保留第一个返回值 10,忽略第二个返回值 50;函数仍然会正常执行。_ 不保存一个可以再读出的变量,不能写 fmt.Println(_)。
_temp 则是普通名字,没有忽略值的特殊作用。理解关键字、普通名字、导出名字和空标识符各自的角色,就能避免大多数入门命名错误。
三、基本类型与字面量
1. 基本类型与数值范围
类型规定变量能保存哪些值;字面量是值在代码里的直接写法,例如 42、3.5、'好' 和 "Go"。Go 的基本类型分为五类:原文
| 分类 | 内置类型 | 用途 |
|---|---|---|
| 布尔 | bool |
真假判断 |
| 整数 | int、int8、int16、int32、int64;uint、uint8、uint16、uint32、uint64;uintptr |
整数值 |
| 浮点数 | float32、float64 |
近似表示实数 |
| 复数 | complex64、complex128 |
带实部和虚部的数 |
| 字符串 | string |
字节序列,常用于文本 |
整数名称中的数字表示位数,而不是十进制位数。 8 位共有 256 种组合:uint8 的范围是 0~255,int8 的范围是 -128~127。一个字节等于 8 位,所以 uint32 占 4 字节。
int 和 uint 的大小为 32 或 64 位,由实现决定;uintptr 是足以保存指针位模式的无符号整数,入门阶段只需认识它。complex64 的实部和虚部各为 float32,complex128 则各为 float64。数值类型规范
2. 新类型与类型别名
go
// 创建新类型
type UserID uint64
// 给原类型起别名
type ID = uint64
UserID 与 uint64 是不同类型,只是底层类型相同;ID 与 uint64 则是同一个类型的两个名字。
go
var raw uint64 = 100
var user UserID = UserID(raw) // 新类型需要显式转换
var alias ID = raw // 别名可以直接使用
同理,type Status bool 创建新布尔类型,而 type Boolean = bool 只是别名。定义新类型可以区分用户 ID、订单 ID 等含义,降低误用的机会。类型声明规则
基本类型中还常见两个预声明别名:byte 就是 uint8,rune 就是 int32。别名不会额外创造一种新类型。
3. 零值与布尔值
没有显式初始化时,基本类型变量使用零值:布尔为 false,数值为 0,字符串为 ""。零值规则
go
var enabled bool // false
var count int // 0
var title string // ""
true 和 false 用来表达真假。严格来说,它们是预声明的布尔常量,Go 规范并没有单独定义布尔字面量。布尔类型规范
4. 数值的不同写法
整数:进制不同,值可以相同
| 进制 | 写法 | 十进制值 |
|---|---|---|
| 十进制 | 26 |
26 |
| 二进制 | 0b11010 |
26 |
| 八进制 | 0o32 |
26 |
| 十六进制 | 0x1A |
26 |
旧式八进制也可以用前导 0 表示,所以 032 是十进制的 26,不是 32。新代码用 0o 更容易辨认。整数字面量规范
浮点数与复数:先认识常用形式
go
3.25 // 小数
3.25e2 // 3.25 × 10²,即 325
3.25e-2 // 3.25 × 10⁻²,即 0.0325
2 + 3i // 实部为 2,虚部为 3 的复数表达式
十六进制浮点数较少使用,只需记住指数用 p,底数为 2;例如 0x1p-3 等于 1/8。3i 是虚部字面量,而 2 + 3i 整体是表达式。浮点数、虚部字面量
浮点变量不一定精确保存十进制小数,float64 比 float32 精度更高,也不意味着所有小数都能精确表示。
下划线:只增加可读性
1_000_000 与 1000000 相同。下划线通常放在数字之间,不能连续或放在末尾;_1000 则是标识符,不是数值字面量。分段规则
5. rune 与字符串
单引号表示码点,双引号表示字符串
go
ch := '好' // 默认类型为 rune,即 int32
text := "好" // string
rune 保存的是 Unicode 码点的整数编号;string 保存字节序列。'好' 与 "好" 不是同一种值,'你好' 也不是合法的 rune 字面量。
go
fmt.Println(len("好")) // 3 个 UTF-8 字节
fmt.Println(len([]rune("好"))) // 1 个码点
字节数、码点数和人眼看到的字符数需要区分。 一些显示字符由多个码点组成,因此码点数也不总等于显示字符数。Go 字符串常用于 UTF-8 文本,但也允许保存任意字节,不能断言所有字符串都是合法 UTF-8。Go 官方字符串讲解
双引号解释转义,反引号保留原始内容
go
message := "第一行\n第二行" // \n 变成换行
path := `C:\notes\go` // 反斜杠不需要转义
双引号中的 \n、\t、\\、\" 分别表示换行、制表符、反斜杠和双引号;反引号不处理这些转义,可以直接书写多行内容,但不能在内部包含反引号。字符串字面量规范
6. 字面量与目标类型
一个数值字面量能否赋给变量,要看其值能否由目标类型表示,不能只看有没有小数点。
go
var whole int = 5.0 // 可以:值恰好是整数 5
var fraction int = 5.2 // 编译错误:不是整数
var small uint8 = 300 // 编译错误:超过 255
上面的错误行用于说明规则,实际运行时应删除或注释。整数目标不允许把常量的小数部分舍掉;浮点目标允许按精度舍入,但不能溢出。常量可表示性规则
字面量常量与已经定型的变量,赋值规则不同。
go
value := 5.0 // 推断为 float64
// var n int = value // 错误:不能直接把 float64 变量赋给 int
n := int(value) // 显式转换
转换非整数的浮点变量时,int(value) 会截掉小数部分,而不是四舍五入。数值转换规则
理解这一页的关键,是把"类型身份""实际数值"和"代码写法"分别看清:不同进制可以表达同一个值,同一常量可以适用于多种目标类型,但已经定型的变量并不会自动变成另一种类型。
四、常量与变量
1. 常量与变量的区别
变量保存程序运行中可以修改的值,常量表示编译阶段就能确定、不能重新赋值的值。两者都可以用名字表达含义,减少没有解释的数字或字符串。原文
go
const MaxRetries = 3
var retries = 0
retries++
// MaxRetries = 5 // 错误:不能修改常量
Go 的常量只涉及布尔、数值和字符串值,不能用 const 声明切片或映射。普通函数调用即使总返回相同结果,也不能用于常量初始化;常量表达式和满足特定条件的内置函数可以,例如 const Size = len("Go")。常量规范
2. 常量类型与类型推断
未定类型常量可以适配上下文,变量必须有确定类型。
go
const Flexible = 12
const Fixed int = 12
var a int32 = Flexible // 可以:值在 int32 范围内
var b float64 = Flexible
// var c int32 = Fixed // 错误:Fixed 已经是 int 类型
var c int32 = int32(Fixed)
"未定类型"不代表任意值都可以转换,只是尚未固定到具体类型;数值范围及可表示性仍然限制它。常量声明没有显式类型,也不保证未定类型,例如 const X = int64(12) 就是已定类型常量。Go 官方常量讲解
变量声明省略类型时:初始化值已经定型,就沿用该类型;否则使用未定类型值的默认类型。
| 初始化值 | 推断出的变量类型 |
|---|---|
12 |
int |
12.0 |
float64 |
'好' |
rune,即 int32 |
2i |
complex128 |
true |
bool |
"Go" |
string |
int64(12) |
int64 |
因此 const N = 12 没有固定具体类型,var n = N 却创建了一个 int 变量。nil 没有默认类型,不能单独用 x := nil 推断变量类型。变量声明规则
常量计算与变量计算也有区别。 常量表达式在编译时求值;例如 const Sum = 0.1 + 0.2 保留精确的常量结果,赋给浮点变量时才按目标精度舍入。未定类型数值常量也可以大于默认类型的范围,但放进具体类型的变量时仍必须能被表示。常量表达式规则
3. 变量声明与赋值
go
var count int = 3 // 类型、初始值都指定
var score = 90 // 从初始值推断类型
var total int // 使用零值 0
name := "Go" // 简短声明,只能在函数内
使用 var 时,类型和初始值至少要提供一个。变量可以修改值,但类型不会因为重新赋值而改变。变量声明规范
go
left, right := 3, 8
left, right = right, left // 交换:得到 8、3
多值赋值先取得右侧的值,再赋给左侧,所以能直接交换变量。Go 不支持 left = right = 8 这样的连等赋值。赋值规则
局部变量声明后需要被使用,只给它反复赋值不算读取;包级变量没有相同的未使用限制。调试时可以用 _ = count 显式忽略值,正式代码中没有用途的变量应删除。
4. 短声明与作用域
同一代码块中,:= 至少要声明一个新的非空变量。
go
count := 1
count, label := 2, "Go" // count 沿用原变量,label 是新变量
count = 3 // 只修改已有变量,用 =
// count := 4 // 错误:没有新变量
// count, _ := 4, 0 // 错误:_ 不算新变量
沿用的变量不会改变类型。:= 左侧是变量名,不能把字段访问或索引表达式当作要声明的名字。短声明规则
内层代码块的同名声明,会遮蔽外层变量。
go
count := 1
{
count := 2
fmt.Println(count) // 2:内层的新变量
}
fmt.Println(count) // 1:外层变量没有改变
内层若写 count = 2,则是修改能访问到的外层变量。判断一行 := 在做什么,必须结合它所在的代码块,而不能只看名字是否曾经出现过。重声明与赋值
5. 常量组与 iota
常量可以成组声明。第一项之后省略类型与表达式时,会重复前一个完整声明项的类型和表达式:
go
const (
First = 7
Second // 同样是 7,不会自动变成 8
)
iota 则表示当前 const 声明中的常量声明项序号,从 0 开始;配合表达式重复,可以生成状态编号:
go
const (
Pending = iota // 0
Running // 1
Finished // 2
)
这里重复的是表达式 iota,不是上一次计算出的数值。每个新的 const 声明重新从 0 计数;没有使用 iota 的声明项以及 _ 占位项也占序号,同一声明项内出现多次 iota 时取值相同。它按声明项计数,不按空行或调用次数计数。常量组与 iota
也可以重复其他表达式,例如 1 << iota 依次生成 1、2、4,适合表示不同的权限位,不必手工列出每个值。
6. 显式转换的边界
类型转换写成 T(value)。常量转换要求结果能被目标类型表示;变量转换可能丢失精度或截掉高位。
go
// int(3.8) // 编译错误:常量不能舍掉小数部分
value := 3.8
whole := int(value) // 3:浮点变量转整数,向零截断
var wide uint16 = 300
small := uint8(wide) // 44:只保留低 8 位
// uint8(300) // 编译错误:常量超出 uint8 范围
因此,"显式转换能编译"不等于"转换后数值保持不变"。不同的基本数值类型也不能仅因为数值一样就直接相互赋值。数值转换规则
整数转字符串不是数字格式化。 string(65) 得到 Unicode 码点 65 对应的 "A";要得到十进制文本 "65",使用 strconv.Itoa(65),并导入 strconv 包。整数到字符串转换、Itoa 文档
五、运算符
1. 算术运算与类型
运算符对值进行计算或判断:a + b 需要两个操作数,是二元运算;-a、!ok 只需要一个操作数,是一元运算。本页关注基本类型上的算术、位运算、比较、逻辑和字符串拼接。原文
| 运算符 | 含义 | 示例 |
|---|---|---|
+、-、* |
加、减、乘 | 3 + 2 得到 5 |
/ |
除法 | 7 / 2 得到 3 |
% |
余数,只用于整数 | 7 % 2 得到 1 |
整数除法向零截断,非零余数的符号跟随被除数。 -7 / 2 是 -3,-7 % 2 是 -1;需要小数结果时,应在除法发生前转为浮点数。
go
numerator, denominator := 7, 2
fmt.Println(float64(numerator) / float64(denominator)) // 3.5
fmt.Println(float64(numerator / denominator)) // 3:小数早已丢失
常量除数为 0 会导致编译错误,整数变量除数在运行时为 0 会引发 panic。整数运算规则
普通数值运算不会自动混合不同类型的变量。 int 与 int64 相加需要先统一类型;未定类型常量则可以在能够被表示时适配变量类型,例如 int64 变量加常量 1。操作数类型规则
2. 位运算与移位
位运算按整数的二进制位处理数据。以 a = 0b1100、b = 0b1010 为例:
| 运算符 | 规则 | 结果 |
|---|---|---|
a & b |
两位都为 1,才保留 1 | 0b1000,即 8 |
| `a | b` | 至少一位为 1,就得到 1 |
a ^ b |
两位不同,才得到 1 | 0b0110,即 6 |
a &^ b |
清除 a 中与 b 的 1 对应的位 | 0b0100,即 4 |
a << 1 |
左移一位,低位补 0 | 0b11000,即 24 |
a >> 1 |
右移一位,丢掉最低位 | 0b0110,即 6 |
^ 有两个用途,都不是乘方。 a ^ b 是异或,^a 是按位取反。取反结果与位宽、是否有符号有关,例如 ^uint8(0) 是 255,而 ^0 是 -1。Go 没有乘方运算符,需要时使用 math.Pow 等函数。算术运算规范
&^ 常用于清除标志位:若权限的低三位是 111,清除 010 后得到 101。它等价于 a & (^b)。
移位次数必须非负;常量负数不能编译,运行时出现负数会 panic。移位左右操作数的整数类型可以不同。无符号数右移高位补 0,有符号数右移保留符号,因此不能把负数右移简单当成整数除法:-7 >> 1 是 -4,而 -7 / 2 是 -3。
3. 比较与逻辑判断
比较使用 ==、!=、<、<=、>、>=,结果表示真假。布尔和复数支持相等、不等比较,但不支持大小排序;整数、浮点数和字符串可以比较大小。比较规则
字符串按字节顺序比较,不按数值大小或自然语言习惯排序。 因此 "10" < "2" 为 true。浮点数的相等比较有确定语义,但计算误差可能使结果不符合直觉;需要近似相等时,应按业务精度设置误差标准。
| 逻辑运算 | 含义 |
|---|---|
a && b |
两个条件都成立 |
| `a | |
!a |
取反 |
&& 和 || 会短路。 && 左边为 false 时不计算右边;|| 左边为 true 时不计算右边。
go
numerator, denominator := 12, 0
ok := denominator != 0 && numerator/denominator > 2
fmt.Println(ok) // false;右侧除法没有执行
把保护条件写在左边,可以避免右边进行非法操作,但右边的代码仍然必须通过编译时检查。逻辑运算规范
4. 拼接与更新变量
字符串使用 + 拼接,也支持 +=:
go
message := "Hello"
message += ", Go" // 得到 "Hello, Go"
它不会自动把数字转成文本,"age=" + 18 不合法,数字格式化应另行处理。
count += 2 表示将 count 加 2 后写回;*=、/=、%=、<<= 等同样用于更新变量。复合赋值的左侧只求值一次。赋值规则
go
count := 1
count++ // 变成 2
count-- // 回到 1
++、-- 是语句,不产生可供其他表达式使用的结果。 只能后置,不能写 ++count,也不能写 value := count++。自增与自减
5. 优先级与括号
二元运算符按下表从高到低结合,同级从左向右结合;一元运算符优先于二元运算符。优先级规范
| 优先级 | 运算符 |
|---|---|
| 5,最高 | * / % << >> & &^ |
| 4 | `+ - |
| 3 | == != < <= > >= |
| 2 | && |
| 1,最低 | ` |
移位比加减法优先,这是与一些语言不同的地方。
go
1 << 2 + 1 // (1 << 2) + 1,结果为 5
1 << (2 + 1) // 结果为 8
括号还可以强调逻辑分组,例如 isAdmin || (isMember && paid)。混合多种运算时,直接写清分组通常比依赖读者记住优先级更容易理解。
6. 常量与变量运算的差别
常量表达式先按自己的规则计算,并不会因为最终需要浮点数就跳过整数除法。
go
const A = 3 / 2 * 0.1 // 先做整数除法,结果为 0.1
const B = 0.1 * 3 / 2 // 先得到浮点常量 0.3,结果为 0.15
窄整数变量的运算可能回绕,已定类型常量溢出则不能编译。
go
var top uint8 = 255
fmt.Println(top + 1) // 0,结果仍为 uint8
// const Bad = uint8(255) + 1 // 编译错误
移位时也要看左操作数最终采用的类型:
go
const fixed = 8
var dynamic = 8
var a uint8 = 1 << fixed / 128 // 2:先完成常量计算
var b uint8 = 1 << dynamic / 128 // 0:uint8 的 1 左移时已丢失高位
若需要先在较宽的类型中计算,再转换到窄类型,应明确写出较宽类型,例如 uint64(1) << dynamic。理解复杂表达式时,除了运算符,还要一起检查操作数类型、是否为常量以及中间结果。移位规则、整数溢出规则