0%

Go 语言学习笔记(1):初识 Go,类型与表达式

Go 语言自 C 一脉相承,又吸收了些时髦的东西。最重要的是,它依旧简单。本系列文章是《Go 语言学习笔记》一书的读书笔记。

Go 特征简介

  • 语法简单:Go 语言与 C99、C11 相似之处颇多,有时候被称为 NextC。Go 语言从 0 开始,没有历史包袱,在汲取众多经验教训后,可以从头规划一个规则严谨、条例简单的世界
  • 并发模型:Goruntime 是 Go 最为显著的特征,它用类似于协程的方式来处理并发单元,却又在运行时层面做了更深度的优化处理。这使得语法上的并发编程变得极为容易,无需处理回调,无须关注运行时切换,仅一个关键字,简单而自然。搭配 channel,实现 CSP 模型,将并发单元间的数据耦合解开,各司其职
  • 内存分配:Go 使用了 tcmalloc(为并发而设计的高性能内存分配组件)。除偶尔因性能问题而被迫采用对象池和自主内存管理外,基本无需参与内存管理操作
  • 垃圾回收:垃圾回收一直是一个难题,每次升级,垃圾回收器必然是核心组件里修改最多的部分。Go Team 一直在为好用的垃圾回收算法而努力
  • 静态链接:Go 刚发布时,静态链接被当做优点宣传,只需编译后的一个可执行文件,无需附加任何东西就能部署。将运行时、依赖库直接打包到可执行文件内部,简化了部署和发布操作。这种简单方式对于编写系统软件有着极大的好处,因为库依赖一直都是一个麻烦事
  • 标准库:功能完善、质量可靠的标准库为编程语言提供了充足动力。优秀的第三方资源也是语言生态圈的重要组成部分
  • 工具链:完整的工具链对日常开发极为重要,无论是编译、格式化、错误检查、帮助文档、还是第三方包下载、更新,都有对应的工具。内置完整的测试框架,包括单元测试、性能测试、代码覆盖率、数据竞争,以及用来调用的 pprof。除此之外,还可以通过环境变量输出运行时监控信息,尤其是垃圾回收和并发调度跟踪,可以帮助我们改进算法,获得更佳的运行器表现

初识 Go

源文件

  • 源文件使用 utf-8 编码,对 unicode 支持良好
  • 每个源文件都属于包的一部分,在文件头部用 package 声明所属包的名称
  • .go 作为文件扩展名,语句结束分号会被默认省略
  • 支持 C 样式注释
  • 入口函数 main 没有参数,且必须放在 main 包中
  • 用 import 导入标准库或第三方包。如果导入某个包,但是没有使用,编译器会报错
  • 可以直接运行,或将源文件编译为可执行文件后运行

以下是 Go 语言的 hello, world

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("hello, world")
}
1
2
3
4
5
6
7
$ ls
test.go
$ go run test.go
hello, world
$ go build test.go
$ ./test
hello, world

变量

  • 使用 var 定义变量,支持类型推断
  • 基础数据类型划分清晰明确,有助于编写跨平台应用
  • 编译器确保变量总是被初始化为零值,避免出现意外情况
  • 在函数内部,还可以省略 var 关键字,使用更简单的定义模式
  • 编译器将未使用的局部变量定义为错误
1
2
3
4
5
6
7
8
package main

func main() {
var x int32
var s = "hello, world"
y := 100
println(x, y, s)
}

表达式

Go 仅有三种流控制语句:

  • if 语句
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

func main() {
x := 100

if x > 0 {
println(x)
} else if x < 0 {
println(-x)
} else {
println(0)
}
}
  • switch 语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

func main() {
x := 100

switch {
case x > 0:
println(x)
case x < 0:
println(-x)
default:
println(0)
}
}
  • for 语句
1
2
3
4
5
6
7
package main

func main() {
for i := 0; i < 5; i++ {
println(i)
}
}

在迭代遍历时,for…range 除元素外,还可以返回索引

1
2
3
4
5
6
7
8
9
package main

func main() {
x := []int{100, 101, 102}

for i, n := range x {
println(i, ":", n)
}
}

函数

  • 函数可以定义多个返回值,甚至对其命名
1
2
3
4
5
6
7
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}

return a / b, nil
}
  • 函数是第一类型,可以将其作为参数或者返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

func test(x int) func() {
return func() {
println(x)
}
}

func main() {
x := 100

f := test(x)
f()
}
  • 用 defer 定义延迟调用,无论函数是否出错,它都确保结束前被调用
1
2
3
4
5
6
7
8
9
10
package main

func test(a, b int) {
defer println("dispose...")
println(a / b)

}
func main() {
test(10, 0)
}

数据

  • 切片实现类似动态数组的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
x := make([]int, 0, 5)

for i := 0; i < 8; i++ {
x = append(x, i)
}

fmt.Println(x)
}
  • 将字典类型内置,可从运行时层面获得性能优化
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
m := make(map[string]int)

m["a"] = 1
x, ok := m["b"]
fmt.Println(x, ok)

delete(m, "a")
}
  • 上一个例子中使用了 ok-idiom 模式,在多个返回返回值中使用一个名为 ok 的布尔值来表示操作是否成功。因为很多操作默认都返回零值,所以需要额外说明

  • 结构体可以匿名嵌入其他类型,然后直接访问匿名字段的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type user struct {
name string
age byte
}

type manager struct {
user
title string
}

func main() {
var m manager

m.name = "Tom"
m.age = 29
m.title = "CTO"

fmt.Println(m)
}

方法

  • 可以为当前包内的任意类型定义方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

type X int

func (x *X) inc() {
*x++
}

func main() {
var x X
x.inc()
fmt.Println(x)
}
  • 可以直接调用匿名字段的方法,这种方式可实现与继承类似的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

type user struct {
name string
age byte
}

func (u user) ToString() string {
return fmt.Sprintf("%+v", u)
}

type manager struct {
user
title string
}

func main() {
var m manager

m.name = "Tom"
m.age = 29
m.title = "CTO"

fmt.Println(m.ToString())
}

接口

  • 接口采用了 duck type,也就是说无须在实现类型上添加显式声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

type user struct {
name string
age byte
}

func (u user) Print() {
fmt.Printf("%+v\n", u)
}

type Printer interface {
Print()
}

func main() {
var m user

m.name = "Tom"
m.age = 29

var p Printer = m
p.Print()
}
  • 空接口类型 interface{} 类似于 OOP 里的 Object 类型,可以接收任意类型对象

并发

  • 整个运行时完全并发设计,凡是你能看到的,几乎都在以 goroutine 方式运行
  • 这是一种比普通线协程或线程更加高效的设计,能够轻松创建和运行成千上万的并发任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"time"
)

func task(id int) {
for i := 0; i < 5; i++ {
fmt.Printf("%d: %d\n", id, i)
time.Sleep(time.Second)
}
}

func main() {
go task(1)
go task(2)

time.Sleep(time.Second * 6)
}
  • 通道(channel)与 goroutine 搭配,实现用通信代替内存共享的 CSP 模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

func consumer(data chan int, done chan bool) {
for x := range data {
println("recv:", x)
}

done <- true
}

func producer(data chan int) {
for i := 0; i < 4; i++ {
data <- i
}

close(data)
}

func main() {
done := make(chan bool)
data := make(chan int)

go consumer(data, done)
go producer(data)

<-done
}

类型

变量

从计算机的角度来看,变量是一段或多段用来存储数据的内存。作为静态数据类型语言,Go 变量总是有固定的数据类型,类型决定了变量内存的长度和存储格式,只能修改变量的值,无法改变类型。实际上,编译后的机器码从不使用变量名,而是直接通过内存地址来访问目标数据。保存在符号表中的变量名等信息可以被删除,或用于输出更详细的错误信息。

变量的定义

  • 关键字 var 用于定义变量,类型被放置于变量名之后。另外运行时内存分配操作会确保变量自动初始化为该类型的零值,避免出现不可预测的行为
1
var x int
  • 如果显式提供初始值,可省略变量类型,由编译器推断
1
var y = false
  • 可一次定义多个变量,包括用不同初始值定义不同类型
1
2
var x, y int
var a, s = 100, "abc"
  • 依照惯例,建议以多组方式整理多行变量定义
1
2
3
4
var (
x, y int
a, s = 100, "abc"
)
  • 除 var 关键字外,还可以使用更加简短的变量定义和初始化语法。简短定义在函数多返回值,以及 if/for/switch 等语句中定义局部变量非常方便
1
2
x := 100
a, s := 1, "abc"

简短模式有些限制:

1: 定义变量,同时显式初始化
2:不能提供数据类型
3:只能用在函数内部

需要注意,简短模式并不总是重新定义变量,也可能是部分退化的赋值操作。

1
2
3
4
5
6
7
8
9
10
package main

func main() {
x := 100
println(&x, x)

x, y := 200, "abc"
println(&x, x)
println(&y, y)
}
1
2
3
4
$ ./short_var
0xc00003a760 100
0xc00003a760 200
0xc00003a768 abc

退化赋值的前提条件是:最少有一个新变量被定义,且必须是同一作用域。以下两种情况都不是退化赋值:

1
2
3
4
5
6
7
8
9
package main

func main() {
x := 100
println(&x, x)

x := 200
println(&x, x)
}
1
2
3
$ go build short_var_err1.go
# command-line-arguments
./short_var_err1.go:7:4: no new variables on left side of :=
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

func main() {
x := 100
println(&x, x)

{
x, y := 200, "abc"
println(&x, x)
println(&y, y)
}

}
1
2
3
4
$ ./short_var_err2
0xc00003a760 100
0xc00003a758 200
0xc00003a768 abc

在处理函数错误返回值时,退化赋值允许我们重复使用 err 变量,这简化了代码的写法

多变量赋值

  • 在进行多变量赋值操作时,首先计算出所有右值,然后再依次完成赋值操作
  • 赋值操作,必须确保左右值类型相同

未使用错误

  • 编译器将未使用的局部变量当做错误,这有助于培养良好的编码习惯

命名

对变量、常量、函数、自定义类型进行命名,通常优先选用有实际含义,易于阅读和理解的字母或单词组合。命名建议:

  • 以字母或下划线开始,由字母、数字、下划线构成
  • 区分大小写
  • 使用驼峰拼写格式
  • 局部变量优先使用短名
  • **不要使用保留关键字
  • 不建议使用与预定义常量、类型、内置函数相同的名字
  • 专有名词通常会全部大写

符号名字首字母大小写决定了其作用域,首字母大写的为导出成员,可以被外部包引用,而小写则仅能在包内使用。c

Go 提供了一个名为 _ 的特殊成员,通常用作忽略占位符使用,可用作表达式左值,无法读取内容。空标识符可用来临时规避编译器对未使用变量和导入包的错误检查。空标识符是预置成员,不能重新定义。

常量

常量表示运行时恒定不可变的值,通常是一些字面量。常量值必须是编译器可确定的字符、字符串、数字和布尔值。可指定常量类型,或由编译器通过初始化值推断,不支持 C/C++ 数字类型后缀。未曾使用的常量不会引发编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

const x, y int = 123, 0x22
const s = "hello, world"

const (
i, f = 1, 0.123
b = false
)

const (
m, n int = 99, -99
p byte = byte(m)
q = uint8(n)
)

func main() {
const x = 123
}

如果显式指定类型,必须确保常量的左右值类型一致,需要时可以做显式转换。右值不能超过常量类型取值范围,否则会引发溢出错误。

1
2
3
4
5
const (
m, n int = 99, -99
p byte = byte(m)
q = uint8(n) // constant -99 overflows uint8
)

常量值也可以是某些编译器能计算出结果的表达式,如 unsafe.Sizeof, len, cap 等。

在常量组中,如果不指定类型和初始化值,则与上一行非空常量右值相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

const (
x uint16 = 120
y
s = "abc"
z
)

func main() {
fmt.Println(x, y, s, z)
}

枚举

Go 并没有明确意义上的 enum 定义,但是通过 itoa 标识符可以实现一组自增常量值,从而实现枚举类型。iota 的值是当前常量表达式在常量组中的索引,从 0 开始计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

const (
x = iota
y
z
)

const (
_ = iota
KB = 1 << (10 * iota)
MB
GB
)

func main() {
fmt.Println(x, y, z)
fmt.Println(KB, MB, GB)
}

可以在多常量定义中使用多个 iota,它们各自单独计数,只需要保证每行常量的列数量相同即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

const (
_, _ = iota, iota * 10
a, b = iota, iota * 10
c, d = iota, iota * 10
)

func main() {
fmt.Println(a, b)
fmt.Println(c, d)
}

如果中断 iota,必须显式恢复。且恢复后的枚举值仍按行序递增(而不是从上一个值开始递增):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

const (
a = iota
b
c = 100
d
e = iota
f
)

func main() {
fmt.Println(a, b, c, d, e, f)
}

在实际编码中,建议使用自定义类型实现用途明确的枚举类型。但这并不能将取值范围限定在预定义的枚举值内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

type color byte

const (
black color = iota
red
blue
)

func test(c color) {
println(c)
}

func main() {
test(red)
// compile error
test(100)

// compile error
x := 2
test(x)
}

常量小结

  • 不同于变量在运行期分配存储内存(非优化状态),常量通常会被编译器在预处理阶段直接展开,作为指令数据使用
  • 数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址
  • 另外定义常量时,是否指定类型也会对编译器产生影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

func main() {
const x = 100
// ok
const y byte = x

println(x, y)

const a int = 100
// compile error
const b byte = a
println(a, b)
}
1
2
3
$ go build const3.go
# command-line-arguments
./const3.go:10:8: cannot use a (type int) as type byte in const initializer

基本类型

清晰完备的预定义基础类型,使得开发跨平台应用时无需过多考虑符号和长度差异。

类型 长度 默认值
bool 1 false
byte 1 0
int, uint 4 / 8 0
int8, uint8 1 0
int16, uint16 2 0
int32, uint32 4 0
int64, uint64 8 0
float32 4 0.0
float64 8 0.0
complex64 8
complex128 16
rune 4 false
uintptr 4 / 8 0
string “”
array
struct
function nil
interface nil
map nil
slice nil
channel nil
  • 支持八进制、十六进制以及科学计数法,标准库 math 定义了各数字类型的取值范围
  • 标准库 strconv 可以在不同进制(字符串)间转换
  • 使用浮点数时,需要注意小数位的有效精度问题

别名

byte 其实是 uint8 的别名,而 rune 则是 uint32 的别名。别名类型无须转换,可直接赋值。但是这并不表示,拥有相同底层结构的就属于别名。例如在 64 位体系结构中,int 和 int64 底层结构完全相同,但是分属于不同类型,需显式转换。

引用类型

所谓引用类型,特指 slice、map、channel 这三种预定义类型。相比数字、数组等类型,引用类型拥有更复杂的存储结构,除分配内存外,它们还需要初始化一系列属性,例如指针、长度等。

内置函数 new 按照指定类型长度分配内存(内存中填充该类型的零值),返回指针,它并不关心类型内部构造和初始化方式。而引用类型则必须使用 make 函数创建,编译器会将 make 转换为目标类型专用的创建函数,以确保完成全部内存分配和相关属性初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

func mkslice() []int {
s := make([]int, 0, 10)
s = append(s, 100)
return s
}

func mkmap() map[string]int {
m := make(map[string]int)
m["a"] = 1
return m
}

func main() {
m := mkmap()
println(m["a"])

s := mkslice()
println(s[0])
}

类型转换

隐式转换造成的问题远远大于它所带来的好处。除常量、别名类型以及未命名类型外,Go 强制要求使用显式转换。加上不支持操作符重载,所以总是能确定语句及表达式的明确含义。同样,不能把非 bool 类型结果当做 true/false 使用。

如果转换的目标是指针、单向通道或者没有返回值的函数类型,必须使用括号,以避免造成语法分解错误。

1
2
3
x := 100
p := *int(&x) // error
p := (*int)(&x) // ok

自定义类型

使用关键字 type 定义用户自定义类型,包括基于现有基础类型创建,或者是结构体、函数类型等。和 var、const 一样,多个 type 定义可以合并成组,可以在函数或代码块内定义局部类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

type flags byte

const (
read flags = 1 << iota
write
exec
)

func main() {
f := read | exec
fmt.Printf("%b", f)
}

即便指定了基础类型,也只表明它们拥有相同的底层数据结构,两者之间不存在任何关系,属于两种完全不同的类型。除操作符外,自定义类型不会继承基础类型的其他信息(包括方法)。不能视作别名,不能隐式转换,不能直接用于比较表达式

1
2
3
4
5
6
7
8
9
10
package main

func main() {
type data int
var d data = 10

// compile error
var x int = d
println(d)
}
1
./data.go:7:6: cannot use d (type data) as type int in assignment

与有明确标识符的 bool、int、string 等类型相比,数组、切片、字典、通道等类型与具体元素类型或长度等属性有关,因此称为未命名类型。可以用 type 为其提供具体名称,将其改变为命名类型。具有相同声明的未命名类型被视为同一类型:

  • 具有相同基类型的指针
  • 具有相同元素类型和长度的数组
  • 具有相同元素类型的切片
  • 具有相同键值类型的字典
  • 具有相同数据类型及操作方向的通道
  • 具有相同字段序列(字段名、字段类型、标签以及字段顺序)的结构体
  • 具有相同签名(参数和返回值列表,不包括参数名)的函数
  • 具有相同方法集(方法名、方法签名,不包括顺序)的接口

未命名类型的转换规则如下:

  • 所属类型相同
  • 基础类型相同,且其中一个是未命名类型
  • 数据类型相同,将双向通道赋值给单向通道,且其中一个为未命名类型
  • 将默认值 nil 赋值给切片、字典、通道、指针、函数或接口
  • 对象实现了目标接口

表达式

保留字

Go 语言仅包含 25 个保留关键字,这体现了 Go 语法规则的简洁性。

运算符

运算符和表达式用来串联数据和指令,算是最基础的算法。一元运算符的优先级最高,二元则分成 5 个级别,从高到低分别是:

1:* / % << >> & &^
2:+ - | ^
3:== != < <= > >=
4:&&
5:||

相同优先级的二元运算符,从左往右依次计算。对于二元运算符而言,操作数类型必须相同,如果其中一个是无显式类型声明的常量,那么该常量操作数会自动转型。

  • 位移右操作数必须是无符号整数,或者可以转换的无显式类型常量。如果是非常量位移表达式,那么会优先将无显式类型的常量左操作数转型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
a := 1.0 << 3
fmt.Printf("%T, %v\n", a, a)

var s uint = 3
b := 1.0 << s

var c int32 = 1.0 << s
fmt.Printf("%T, %v\n", c, c)
}
1
2
# command-line-arguments
./shift.go:10:4: invalid operation: 1 << s (shift of type float64)
  • 二进制位运算符比较特别的是 bit clear,即 &^,例如 1011 &^ 1101 = 0010

  • 自增、自减不再是运算符,只能作为独立语句,不能用于表达式(表达式通常是求值代码,可作为右值或参数使用,而语句完成一个行为。表达式可以作为语句使用,而语句却不能作为表达式使用)。

  • 不能将内存地址和指针混淆。内存地址是内存中每个字节单元的唯一编号,而指针则是一个实体。指针会分配内存空间,相当于一个专门用来保存地址的整型变量。取地址运算符 & 用于获取对象的地址,而指针运算符 * 用于间接引用目标对象。

  • 并非所有对象都能进行取地址操作,变量总是可寻址的。指针类型支持相等运算符,但不能做加减运算和类型转换。如果两个指针指向同一个地址,或者都为 nil,那么它们相等。

  • 可通过 unsafe.Pointer 将指针转换为 uintptr 后进行加减运算,但可能会造成非法访问。unsafe.Pointer 类似于 C 语言中的 void*,可用来转换指针类型,它能安全地持有对象或对象成员,但 uintptr 不行。后者仅仅是一种特殊整型,并不引用目标对象,无法阻止垃圾回收器回收对象内存

  • 指针没有专门指向成员的 -> 运算符,统一使用 . 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

func main() {
type user struct {
name string
age int
}

u := user{
name: "Jack",
age: 10,
}

p := &u
println(p.name, p.age)
}
  • 零长度对象的地址是否相等和具体的实现相关,但是肯定不等于 nil。

初始化

对复合类型(数组、切片、字典、结构体)变量初始化时,有一些语法限制:

  • 初始化表达式必须包含类型标签
  • 左花括号必须在类型尾部,不能另起一行
  • 多个成员初始值以逗号分隔
  • 允许多行,但每行以逗号或者右花括号结束

流控制

if…else 中的条件表达式必须是布尔类型,可省略括号,且左花括号不能另起一行。比较特别的是对初始化语句的支持,可定义块局部变量(有效范围包含整个 if/else 块)或执行初始化函数。

与 if 类似,switch 语句也用于选择执行,但具体使用的场景会有所不同。条件表达式支持非常量,这要比 C 更加灵活。switch 同样支持初始化语句,按照从上到下,从左到右的顺序匹配 case 执行。只有全部匹配失败时,才会执行 default 块。无须显式执行 break 语句,case 执行完毕后自动中断。如须贯通后续 case,须执行 fallthrough,但不再匹配后续条件表达式。注意,fallthrough 必须放在 case 块结尾,可使用 break 语句阻止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

func main() {
a, b, c, x := 1, 2, 3, 2

switch x {
case a, b:
println("a | b")
case c:
println("c")
case 4:
println("d")
default:
println("z")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

func main() {
switch x := 5; x {
default:
println(x)
case 5:
x += 10
println(x)
fallthrough
case 6:
x += 20
println(x)
}
}

需要注意,相邻的空 case 不构成多条件匹配,此时仅表示相应的 case 内容为空。另外,不能出现重复的 case 常量值。

有些时候,switch 还被用来替换 if 语句,被省略的 switch 条件表达式默认为 true,继而与 case 表达式结果匹配

1
2
3
4
5
6
7
8
9
10
11
12
package main

func main() {
switch x := 5; {
case x > 5:
println("a")
case x > 0 && x <= 5:
println("b")
default:
println("z")
}
}

仅有 for 一种循环语句,但是常用方式都支持。初始化语句仅被执行一次。

1
2
3
4
5
6
7
8
9
10
11
for i := 0; i < 3; i++ {

}

for x < 10 {
x++
}

for {
break
}

可用 for...range 完成数据迭代,支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据:

1
2
3
4
5
6
7
8
9
package main

func main() {
data := [3]string{"a", "b", "c"}

for i, s := range data {
println(i, s)
}
}

不同于 Python,在 Go 中没有相关接口实现自定义迭代类型,除非基础类型就是以上类型之一。

无论普通 for 循环,还是 range 迭代,其定义的局部变量都会重复使用。另外需要注意,range 会复制目标数据,受直接影响的是数组,可改用数组指针或切片类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

func main() {
data := [3]int{10, 20, 30}

for i, x := range data {
if i == 0 {
data[0] += 100
data[1] += 200
data[2] += 300
}

fmt.Printf("x: %d data: %d\n", x, data[i])
}

for i, x := range data[:] {
if i == 0 {
data[0] += 100
data[1] += 200
data[2] += 300
}
fmt.Printf("x: %d data: %d\n", x, data[i])
}
}

运行结果:

1
2
3
4
5
6
7
 ./range
x: 10 data: 110
x: 20 data: 220
x: 30 data: 330
x: 110 data: 210
x: 420 data: 420
x: 630 data: 630

如果 range 目标表达式是函数调用,也仅被执行一次。

使用 goto 前,必须先定义标签,标签区分大小写,且未使用的标签会引发编译错误。且不能跳转到其他函数或者内层代码块内。和 goto 定点跳转不同,break、continue 用于中断代码块执行:

  • break:用于 swtich、for、select 语句,终止整个语句块执行
  • continue:仅用于 for 循环,终止后续逻辑,立即进入下一轮循环

配合标签,break 和 continue 可在多层嵌套中指定目标层级:

1
2
3
4
5
6
7
8
9
10
11
12
package main

func main() {
outer:
for x := 0; x < 5; x++ {
println("loop exec", x)

if x > 2 {
break outer
}
}
}
1
2
3
4
5
$ ./break_label
loop exec 0
loop exec 1
loop exec 2
loop exec 3