Go:类型、控制流、数组值复制与集合 · 变量、常量与作用域至range 元素副本与循环变量版本
前置:能在模块目录运行 main.go。本章每个完整程序分别替换上一章 main.go,再 go run .;不要在同一目录保留多个 main 函数。Go 1.27.2,Windows/Linux 运行核对属于历史记录,本轮示例未执行;macOS 仅作平台说明,本项目不执行平台运行验证。
Go:类型、控制流与集合
前置:能在模块目录运行 main.go。本章每个完整程序分别替换上一章 main.go,再 go run .;不要在同一目录保留多个 main 函数。Go 1.27.2,Windows/Linux 运行核对属于历史记录,本轮示例未执行;macOS 仅作平台说明,本项目不执行平台运行验证。
变量、常量与作用域
变量是有名字的存储位置,赋值会替换其中的值;常量不能重新赋值。将下面的完整程序保存为 main.go,执行 go run .。
package main
import "fmt"
func main() {
var count int
title := "Go"
const goal = 3
count = count + 1
fmt.Println(title, count, goal)
{
count := 9
fmt.Println(count)
}
fmt.Println(count)
}预期输出:
Go 1 3
9
1var count int 声明整数,未提供初值时为零值;bool 的零值是 false,string 是空字符串。函数内 := 声明并推断类型,= 给已有变量赋值。同一块里使用 := 至少要有一个新变量;包级声明使用 var 或 const。
花括号形成作用域。内层 count := 9 创建同名新变量,暂时遮蔽外层 count,退出后外层仍为 1。局部变量声明后必须使用,删掉打印可能产生 declared and not used 编译错误。
练习:把内层 := 改为 =,预测最后一行,再运行。
参考解法
最后为 9,因为没有创建内层变量,而是修改外层 count。作用域决定名字指向哪个变量。
常量与 iota 枚举
前置:变量与常量。iota 是 const 声明组中当前声明项的序号,从 0 开始;显式写其他值不会重置序号。同一项中的两个 iota 相等。重新排序会改变值,不能直接当作永久保存的协议编号。
package main
import "fmt"
func main() {
const (
draft = iota
fixed = 100
done = iota
archived
)
const left, right = iota, iota
fmt.Println(draft, fixed, done, archived)
fmt.Println(left, right)
}预期输出:
0 100 2 3
0 0常量可以是布尔、数值和字符串,切片和结构体不能声明成 const。无类型常量赋值时仍须能用目标类型表示。练习:在 draft 前加一项,观察哪些编号变化,再解释为什么外部状态码宜显式指定。
参考解法
fixed 仍为 100,依赖 iota 的项增加 1。需要保持兼容的外部值应显式约定。
基本类型、转换与运算
整数用于计数,小数计算通常用 float64,条件用 bool,文本用 string。转换应在需要保留小数的运算前做。
package main
import "fmt"
func main() {
completed, total := 2, 3
fmt.Println(completed / total)
fmt.Printf("%.2f\n", float64(completed) / float64(total))
fmt.Println(completed < total && total > 0)
fmt.Println(7 % 3)
}预期输出:
0
0.67
true
1整数除法丢弃小数部分;把已经算出的 completed / total 转成 float64 不能找回它。% 求余,== 比较相等,= 赋值。浮点数是近似表示,金额通常使用有明确单位的整数。
有符号整数除法向零截断,例如 -7/3 == -2、-7%3 == -1。int 的宽度由实现决定,固定格式可选 int64;byte、rune 分别是 uint8、int32 的别名。complex64/complex128 表示复数。切片、map 和函数只能与 nil 比较,不能直接比较两个值。整数位操作有 &、|、^、&^、<<、>>。i++ 是语句,不能写成 x = i++。Go 没有三元条件表达式。
&&、|| 从左到右短路:右侧只有在需要时才求值。total != 0 && completed/total > 0 可避免除零。类型转换不会普遍检查溢出,输入范围需先检查。
练习:将 completed 改成 3、total 改成 2,预期前两行为 1 和 1.50。解释为什么 float64(completed / total) 只得到 1。
参考解法
整数除法先得 1,再转换只改变表示类型;应分别转换两个操作数后再除。
类型、条件与循环
类型决定可做哪些操作:int 用于整数计数,float64 用于近似小数,bool 表示真假,string 保存字节串。var total int 的初值为 0;const limit = 3 是不可重新赋值的常量。整数 5/2 是 2,5.0/2.0 才能保留小数。int 和 int64 是不同类型,需要显式转换;转换不会替你检查溢出,输入范围应在转换前确认。
下面统计完成的章节数。[]bool 是布尔切片;range 的第一个值是下标、第二个是元素副本,_ 表示不用下标。if 只接受布尔条件,不能把整数当真假。
package main
import "fmt"
func main() {
completed := []bool{true, false, true}
total := 0
for _, done := range completed {
if done {
total++
}
}
switch total {
case 0:
fmt.Println("尚未开始")
case len(completed):
fmt.Println("全部完成")
default:
fmt.Printf("完成 %d/%d\n", total, len(completed))
}
}预期输出:
完成 2/3还有 for i := 0; i < 3; i++:初始化一次,每轮先判断条件,末尾递增。break 退出循环,continue 跳过本轮剩余语句。switch 匹配后默认结束,不需要像某些语言那样每个 case 都加 break。&&/|| 从左到右短路求值,例如先检查长度,再访问下标。
for condition 只保留条件;for {} 是无限循环,必须设计退出条件。switch { case condition: ... } 按条件依次判断;一个 case 可列多个值。fallthrough 会无条件进入下一分支,普通分支通常不需要。break 默认只退出最近的循环、switch 或 select,多层查找可用 Outer: 和 break Outer;return 退出整个函数。
range 元素副本与循环变量版本
前置:循环、数组;闭包部分在学完函数后回看。range 数组或切片给出索引和元素副本,修改副本不会替换元素;字符串给字节偏移和 rune;map 给键与值副本,顺序不固定;channel 每次给一个接收值,关闭且耗尽才结束。
package main
import "fmt"
func main() {
values := []int{1, 2}
for _, value := range values { value *= 2 }
fmt.Println(values)
for i := range values { values[i] *= 2 }
fmt.Println(values)
var saved []func() int
for i := 0; i < 3; i++ { saved = append(saved, func() int { return i }) }
fmt.Println(saved[0](), saved[1](), saved[2]())
}预期输出(本课程模块 go 1.27.0):
[1 2]
[2 4]
0 1 2Go 1.22 起,采用新语言语义的循环中,:= 声明的迭代变量每轮独立;循环外已有变量用 = 赋值仍会复用。阅读旧闭包题必须核对 go.mod 的 go 行,不能只看工具链。这里按顺序调用函数,保证输出可预测;并发输出没有固定顺序。
练习:把 i 移到循环外声明并在循环中用 =,最后一行应变成 3 3 3。这解释变量绑定,不代表取消了共享状态的同步要求。
继续阅读
- 开发实践
- 开发实践
- 开发实践