# 一、Go基础
## 1. 编译运行
``` go
// 构建二进制文件
go build hello.go
./hello.go
go run hello.go
```
## 2. 数据类型
- 布尔型
- 数字类型
- 字符串
- 派生类型
- 指针 pointer
- 数组
- 结构化类型 struct
- channel
- 函数
- 切片
- 接口 interface
- map
## 3. 字符串
``` go
// 字符串拼接
fmt.Println("Google" + "Runoob")
// 字符串格式化
var stockcode=123
var enddate="2020-12-31"
var url="Code=%d&endDate=%s"
var target_url=fmt.Sprintf(url,stockcode,enddate)
```
## 4. 常量
``` go
// 常量的定义格式:
const identifier [type] = value
/*
iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前)
const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
*/
// 第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
```
## 5. 变量
### a. 变量声明
``` go
// 变量声明
1. var identifier1, identifier2 type
2. identifier1, identifier2 := value1, value2
/* 指定变量类型,如果没有初始化,则变量默认为零值。
- 数值类型(包括complex64/128)为 0
- 布尔类型为 false
- 字符串为 ""(空字符串)
- 以下几种类型为 nil
*/
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口
// go可以根据值自行判定变量类型。
var v_name = value
// 如果变量已经使用 var 声明过了,再使用 := 声明变量,就产生*编译错误*
var intVal int
intVal :=1
```
``` go
/*
如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a,两个变量的类型必须是相同。
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。
*/
```
### b. 变量作用域
Go 语言中变量可以在三个地方声明:
- 函数内定义的变量称为局部变量
- 函数外定义的变量称为全局变量
- 函数定义中的变量称为形式参数
## 6. 数组
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。
### a. 基础
``` go
// 声明数组
var arrayName [size]dataType
var balance [10]float32
// 初始化数组
// 初始化数组中 {} 中的元素个数不能大于 [] 中的数字
var numbers [5]int
var numbers = [5]int{1, 2, 3, 4, 5}
// 数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}
```
### b. 二维数组
Go 语言支持多维数组,以下为常用的多维数组声明方式:
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
``` go
// Step 1: 创建数组
values := [][]int{}
// Step 2: 使用 append() 函数向空的二维数组添加两行一维数组
row1 := []int{1, 2, 3}
row2 := []int{4, 5, 6}
values = append(values, row1)
values = append(values, row2)
```
### c. 向函数传递数组
Go 语言中的数组是值类型,因此在将数组传递给函数时,实际上是传递数组的副本。
``` go
func myFunction(param []int) {
....
}
/** 在例子中,
* modifyArray 函数接受一个数组,并尝试修改数组的值,但在主函数中调用后,原始数组并未被修改。
* modifyArrayWithPointer 函数接受一个数组的指针,并通过指针修改了原始数组的值。
**/
func main() {
// 创建一个包含5个元素的整数数组
myArray := [5]int{1, 2, 3, 4, 5}
fmt.Println("Original Array:", myArray)
// 传递数组给函数,但不会修改原始数组的值
modifyArray(myArray)
fmt.Println("Array after modifyArray:", myArray)
// 传递数组的指针给函数,可以修改原始数组的值
modifyArrayWithPointer(&myArray)
fmt.Println("Array after modifyArrayWithPointer:", myArray)
}
```
## 7. 指针
一个指针变量指向了一个值的内存地址。
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
``` go
// 指针声明格式
var var_name *var-type
// var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针
var ip *int /* 指向整型*/
var fp *string /* 指向字符型 */
// 如何使用指针
func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}
```
### a. Go 指针数组
可以定义一个指针数组来存储地址
``` go
const MAX int = 3
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
```
### b. Go 指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址:
``` go
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
ptr = &a
pptr = &ptr
}
```
### c. Go 向函数传递指针参数
通过引用或地址传参,在函数调用时可以改变其值
Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。
``` go
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 将 y 赋值给 x */
*y = temp /* 将 temp 赋值给 y */
}
a, b := 1000, 2000
swap(&a, &b);
```
## 8. 语言结构体
``` go
type struct_variable_type struct {
member definition
member definition
...
member definition
}
// 应用
type Books struct {
title string
author string
subject string
book_id int
}
var struct_pointer *Books
var Book1 Books
struct_pointer = &Book1
```
## 9. 切片
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
一个切片在未初始化之前默认为 nil,长度为 0
### a. 定义切片
``` go
var identifier []type
var slice1 []type = make([]type, len)
s := arr[:]
s := arr[startIndex:endIndex]
s := arr[startIndex:]
s := arr[:endIndex]
s :=make([]int,len,cap)
```
- len(): 切片是可索引的,并且可以由 len() 方法获取长度。
- cap(): 切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
- copy(): 从原分片拷贝切片的 copy 方法
- 拷贝 numbers 的内容到 numbers1: copy(numbers1,numbers)
- append(): 向切片追加新元素的 append 方法。
- 同时添加多个元素: numbers = append(numbers, 2,3,4)
## 10. 集合Map
Map 是一种无序的键值对的集合。
Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 ""。
Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。
``` go
// 使用内建函数 make 或使用 map 关键字来定义 Map:
map_variable := make(map[KeyType]ValueType, initialCapacity)
m := make(map[string]int)
// 创建一个初始容量为 10 的 Map
m := make(map[string]int, 10)
// 使用字面量创建 Map
m := map[string]int{
"apple": 1,
"banana": 2,
"orange": 3,
}
```
### a. delete
delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key
``` go
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
delete(countryCapitalMap, "France")
```
## 11. 参数转换
type_name(expression)
### a. 数值类型转换
var a int = 10
var b float64 = float64(a)
### b. 字符串类型转换
var str string = "10"
var num int
num, _ = strconv.Atoi(str)
### c. 接口类型转换
接口类型转换有两种情况:类型断言和类型转换。
``` go
// 类型断言用于将接口类型转换为指定类型,其语法为:
value.(type)
或者
value.(T)
if str, ok := i.(string); ok {
fmt.Printf("'%s' is a string\n", str)
}
// 类型转换用于将一个接口类型的值转换为另一个接口类型
T(value)
// 定义一个接口 Writer
type Writer interface {
Write([]byte) (int, error)
}
// 实现 Writer 接口的结构体 StringWriter
type StringWriter struct {
str string
}
// 实现 Write 方法
func (sw *StringWriter) Write(data []byte) (int, error) {
sw.str += string(data)
return len(data), nil
}
func main() {
// 创建一个 StringWriter 实例并赋值给 Writer 接口变量
var w Writer = &StringWriter{}
// 将 Writer 接口类型转换为 StringWriter 类型
sw := w.(*StringWriter)
// 修改 StringWriter 的字段
sw.str = "Hello, World"
// 打印 StringWriter 的字段值
fmt.Println(sw.str)
}
```
### d. 空接口类型
空接口 interface{} 可以持有任何类型的值。在实际应用中,空接口经常被用来处理多种类型的值。
``` go
func printValue(v interface{}) {
switch v := v.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
}
```
# 二、条件和循环
## 1. 条件
``` go
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}
```
``` go
// 每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止
// 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case
switch var1 {
case val1:
...
case val2:
...
default:
...
}
// 如果我们需要执行后面的 case,可以使用 fallthrough
// 使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}
// 输出结果
2、case 条件语句为 true
3、case 条件语句为 false
4、case 条件语句为 true
```
``` go
/* select 是 Go 中的一个控制结构,类似于 switch 语句
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
*/
select {
case <- channel1:
// 执行的代码
case value := <- channel2:
// 执行的代码
case channel3 <- value:
// 执行的代码
// 你可以定义任意数量的 case
default:
// 所有通道都没有准备好,执行的代码
}
```
## 2. 循环
``` go
/*
for init; condition; post { }
init: 一般为赋值表达式,给控制变量赋初值;
condition: 关系表达式或逻辑表达式,循环控制条件;
post: 一般为赋值表达式,给控制变量增量或减量。
*/
// 读取 slice map 数组 字符串
for key, value := range oldMap {
newMap[key] = value
}
for i, x:= range value { // value 可以为string slice
fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}
// break continue 可以附带标记
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
break re
}
}
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue re
}
}
// goto 语法格式如下:
goto label;
..
.
label: statement;
example:
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
```
# 三、函数
``` go
func function_name( [parameter list] ) [return_types] {
函数体
}
```
## 1. 参数
调用函数,可以通过两种方式来传递参数:
- 值传递
- 值传递是指在调用函数时将*实际参数*复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
- **默认情况下,Go 语言使用的是值传递**
- 引用传递
- 引用传递是指在调用函数时将*实际参数的地址*传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
``` go
func swap(x *int, y *int) {
var temp int
temp = *x /* 保持 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
swap(&a, &b)
```
## 2. 闭包
Go 语言支持匿名函数,可作为闭包。
匿名函数是一种没有函数名的函数,通常用于在函数内部定义函数,或者作为函数参数进行传递。
``` go
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber()) // 1
fmt.Println(nextNumber()) // 2
fmt.Println(nextNumber()) // 3
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1()) // 1
fmt.Println(nextNumber1()) // 2
```
``` go
// 定义一个匿名函数并将其赋值给变量add
add := func(a, b int) int {
return a + b
}
// 将匿名函数作为参数传递给其他函数
calculate := func(operation func(int, int) int, x, y int) int {
return operation(x, y)
}
sum := calculate(add, 2, 8)
fmt.Println("2 + 8 =", sum) // 10
// 也可以直接在函数调用中定义匿名函数
difference := calculate(func(a, b int) int {
return a - b
}, 10, 4)
fmt.Println("10 - 4 =", difference) // 6
```
## 3. 函数方法
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:
```go
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
ex:
type Circle struct {
radius float64
}
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
c := Circle{5.0}
c.getArea() // 返回 78.5
```
## 4. 递归
递归函数通常包含两个部分:
基准条件(Base Case):这是递归的终止条件,防止函数无限调用自身。
递归条件(Recursive Case):这是函数调用自身的部分,用于将问题分解为更小的子问题。
``` go
func recursion() {
recursion() /* 函数调用自身 */
}
func main() {
recursion()
}
```
# 四、错误处理
error 接口:标准的错误表示。
显式返回值:通过函数的返回值返回错误。
自定义错误:可以通过标准库或自定义的方式创建错误。
panic 和 recover:处理不可恢复的严重错误。
## 1. error 接口
``` go
// 实现 error 接口:任何实现了 Error() 方法的类型都可以作为错误。
// Error() 方法返回一个描述错误的字符串。
type error interface {
Error() string
}
// 我们可以在编码中通过实现 error 接口类型来生成错误信息。
err := errors.New("this is an error")
```
## 2. 显式返回值
Go 中,错误通常作为函数的返回值返回,开发者需要显式检查并处理。
``` go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
```
## 3. 自定义错误
通过定义自定义类型,可以扩展 error 接口。
``` go
type DivideError struct {
Dividend int
Divisor int
}
func (e *DivideError) Error() string {
return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}
func divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivideError{Dividend: a, Divisor: b}
}
return a / b, nil
}
func main() {
_, err := divide(10, 0)
if err != nil {
fmt.Println(err) // 输出:cannot divide 10 by 0
}
}
```
fmt 包提供了对错误的格式化输出支持:
- %v:默认格式。
- %+v:如果支持,显示详细的错误信息。
- %s:作为字符串输出。
``` go
type DivideError struct {
dividee int
divider int
}
// 实现 `error` 接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
```
errors.Is
检查某个错误是否是特定错误或由该错误包装而成。
``` go
var ErrNotFound = errors.New("not found")
func findItem(id int) error {
return fmt.Errorf("database error: %w", ErrNotFound)
}
func main() {
err := findItem(1)
if errors.Is(err, ErrNotFound) {
fmt.Println("Item not found")
} else {
fmt.Println("Other error:", err)
}
}
```
errors.As
将错误转换为特定类型以便进一步处理。
``` go
type MyError struct {
Code int
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("Code: %d, Msg: %s", e.Code, e.Msg)
}
func getError() error {
return &MyError{Code: 404, Msg: "Not Found"}
}
func main() {
err := getError()
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Printf("Custom error - Code: %d, Msg: %s\n", myErr.Code, myErr.Msg)
}
}
```
## 4. panic 和 recover
Go 的 panic 用于处理不可恢复的错误,recover 用于从 panic 中恢复。
- panic:
- 导致程序崩溃并输出堆栈信息。
- 常用于程序无法继续运行的情况。
- recover
- 捕获 panic,避免程序崩溃。
``` go
func safeFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
panic("something went wrong")
}
func main() {
fmt.Println("Starting program...")
safeFunction()
fmt.Println("Program continued after panic")
}
```
# 五、并发
Go 语言支持并发,通过 goroutines 和 channels 提供了一种简洁且高效的方式来实现并发。
- Goroutines:
- Go 中的并发执行单位,类似于轻量级的线程。
- Goroutine 的调度由 Go 运行时管理,用户无需手动分配线程。
- 使用 go 关键字启动 Goroutine。
- Goroutine 是非阻塞的,可以高效地运行成千上万个 Goroutine。
- Channel:
- Go 中用于在 Goroutine 之间通信的机制。
- 支持同步和数据共享,避免了显式的锁机制。
- 使用 chan 关键字创建,通过 <- 操作符发送和接收数据。
- Scheduler(调度器):
- Go 的调度器基于 GMP 模型,调度器会将 Goroutine 分配到系统线程中执行,并通过 M 和 P 的配合高效管理并发。
- G:Goroutine。
- M:系统线程(Machine)。
- P:逻辑处理器(Processor)。
## 1. Goroutine
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
``` go
func sayHello() {
for i := 0; i < 5; i++ {
fmt.Println("Hello")
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go sayHello() // 启动 Goroutine
for i := 0; i < 5; i++ {
fmt.Println("Main")
time.Sleep(100 * time.Millisecond)
}
}
```
## 2. Channel
通道(Channel)是用于 Goroutine 之间的数据传递。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。
使用 make 函数创建一个 channel,使用 <- 操作符发送和接收数据。如果未指定方向,则为双向通道。
``` go
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
```
``` go
// 声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建
ch := make(chan int)
// 以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y)
}
```
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
``` go
func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
```
Go 遍历通道与关闭通道
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
v, ok := <-ch
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
``` go
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}
```
select 语句使得一个 goroutine 可以等待多个通信操作。select 会阻塞,直到其中的某个 case 可以继续执行:
``` go
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
```
sync.WaitGroup 用于等待多个 Goroutine 完成。
同步多个 Goroutine:
``` go
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Goroutine 完成时调用 Done()
fmt.Printf("Worker %d started\n", id)
fmt.Printf("Worker %d finished\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加计数器
go worker(i, &wg)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("All workers done")
}
```