一、goroutine 基础
定义
- 使用者分配足够多的任务,系统能自动帮助使用者把任务分配到 CPU 上,让这些任务尽量并发运作,此机制在Go中称作 goroutine
- goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
- Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。
语法
// 普通函数创建 goroutine 语法 go 函数名称(参数列表) // 匿名函数创建 goroutine 语法 go func(形参列表) {函数体...}(实参列表) // 使用 go 创建 goroutine 后,函数的返回值会被忽略
普通函数创建 goroutine
package main import ( "fmt" "math/rand" "time" ) func listElem(n int) { for i:=0; i<n; i++ { fmt.Println(i) } } func main() { go listElem(10) // 若 main 函数中仅有这一句代码,控制台将没有任何输出,因为 main 创建协程后立即推出了,还未来得及执行协程 //添加以下代码 var input string fmt.Scanln(&input) // 需要用户输入任意字符 main 才会结束 }
匿名函数创建 goroutine
func main() { go func() { var times int for { times++ fmt.Println("tick", times) time.Sleep(time.Second) } } () }
二、channel
channel 基础
- 一个 channel 是一个通信机制,可让一个 goroutine 通过 channel 给另一个 goroutine 发送消息。
- 每个 channel 都有一个特殊的类型,也就是 goroutine 之间进行通信时能够传递的数据类型;eg:若要通过 channel 发送 int 类型的数据,那么 chan int 即可。
- 通道之间能够共享的数据类型有:内置类型、命名类型、结构类型、引用类型的值、指针。
- 通道遵循 “先进先出” 原则,保证收发数据的顺序,并且在同一时间只能有一个 goroutine 访问通道进行发送消息或者读取消息。
语法
// 声明之后必须配合 make 才能使用 var 通道变量 chan 通道类型 // 声明通道;通道类型,通道内的数据类型 通道实例 = make(chan 数据类型) // 创建通道;数据类型,通道内传输的元素类型 // eg: var ch1 chan string ch1 = make(chan string) ch2 := make(chan int) // 创建整型类型的通道 ch3 := make(chan interface{}) // 创建空接口类型的通道,可存放任意格式 ch4 := make(chan *Equip) // 创建 Equip 指针类型的通道,可存放 *Equip // 关闭通道 close(ch1)
使用通道发送数据
// 通道发送数据的语法: "<-" 操作符 通道变量 <- 值 // 值可以是:变量、常量、表达式、函数返回值、但值的类型必须与通道声明的元素类型一致 // eg ch := make(chan interface{}) // 创建空接口通道 ch <- 0 // 向通道内放入 0 ch <- "Hello World" // 向通道内放入 字符串 // 附:向通道内发送数据时,若没有对应的接收方读取数据,那么发送数据的操作将会一直堵塞。
使用通道接收数据
- 通道数据的收发操作在两个不同的 goroutine 中进行
- 接收方会一直阻塞到发送方发送数据为止,同样的,发送方也会一直阻塞到接收方读取数据为止。
- 通道每次只能接收一个数据元素
// 接收数据同样使用 "<-" 操作符 data := <-ch // 阻塞接收数据 data, ok := <-ch // 非阻塞接收数据,方未接收到数据时,data为通道类型的零值,ok为false;此方式会造成CPU高占用,实现接收超时检测会配合 select 和 计时器channel进行,一般较少使用 <-ch // 接收任意数据,并忽略接收到的数据 // 循环接收 for data := range ch { // do something }
单向通道
var 通道变量 chan<- 通道类型 // 只写 channel var 通道变量 <-chan 通道类型 // 只读 channel var 通道变量 chan 通道类型 // 可读可写 channel // eg var wc chan<- string var rc <-chan string var ch chan string // 关闭 channel close(ch)
无缓冲通道
在接收数据前,没有能力,保存任何值,的通道。它要求 发送通道和接收通道 必须同时准备好,才能完成发送和接收操作;如果有一方通道a没有准备好,那么另一方通道b会阻塞等待着通道a。
package main import ( "fmt" "math/rand" "sync" "time" ) var wg sync.WaitGroup func init() { rand.Seed(time.Now().UnixNano()) } func player(name string, court chan int) { /** 击球比赛 大致流程: 创建一个数据类型是 int的channel 创建两个 goroutine(选手),互相进行击球,由于 player 中第一行代码是在阻塞等待接球(从channel中获取数据),所以在main当中需要发出第一个球 player: 启动两个 goroutine 后,总有一方会抢先从 channel 中取出数据,那么另一方处于阻塞等待的状态。 然后判断 通道channel 是否关闭(若已关闭,ok=false ) 若已经关闭,代表是对方关闭的通道对方输球,则当前选手获胜,退出游戏。 若上一个判断不成立,则接下来选择一个随机数 并 判断是否可以被 22 整除,以此来决定是否丢球(向通道写入数据,让对方获取【接球】) 若无法被整除,当前选手输球,直接关闭通道并退出,此时执行权在另一方选手,从通道取出数据时,发现通道已关闭,那么打印胜利信息。 若可以整除,显现当前的击球数,并将其写入到通道,让对方接球。 以下代表表示主 goroutine 需要等待另两个 goroutine 结束之后才能结束 main wg.Add(2) defer wg.Done() wg.Wait() **/ defer wg.Done() // 函数退出时调用 Done 函数,以此通知 main 函数,当前函数的工作已完成 for { ball, ok := <-court // 等待接球 if !ok { // 若通道关闭,则胜利 fmt.Printf("Player %s Won\n", name) return } // 选择随机数,使用此数判断是否丢球 n := rand.Intn(100) if n % 22 == 0 { fmt.Printf("Player %s Missed\n", name) // 关闭通道,表输球 close(court) return } // 显示击球数,且击球数 +1 fmt.Printf("Player %s Hit %d\n", name, ball) ball++ // 击球给对方 court <- ball } } func main() { // 击球比赛 court := make(chan int) wg.Add(2) // 表:需等待两个 goroutine go player("Juan", court) // 启动两个选手 go player("Xiao", court) court <- 1 // 发球 wg.Wait() // 等待结束 }