我们常常需要在后面一个时刻运行 Go 代码,或者在某段时间间隔内重复运行。Go 的内置 定时器 和 打点器 特性让这些很容易实现。
package main
import (
"fmt"
"time"
)
func main() {
// 定时器表示在未来某一时刻的独立事件。
// 你告诉定时器需要等待的时间,然后它将提供一个用于通知的通道。
// 这里的定时器将等待 2 秒。
timer1 := time.NewTimer(time.Second * 2)
//<-timer1.C 直到这个定时器的通道 C 明确的发送了定时器失效的值之前,将一直阻塞。
<-timer1.C
fmt.Println("Timer 1 expired")
//如果你需要的仅仅是单纯的等待,你需要使用 time.Sleep。
// 定时器是有用原因之一就是你可以在定时器失效之前,取消这个定时器。
timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}
}
执行结果如下图所示:
第一个定时器将在程序开始后 ~2s 失效,但是第二个在它没失效之前就停止了。
定时器 是当你想要在未来某一刻执行一次时使用的 - 打点器 则是当你想要在固定的时间间隔重复执行准备的。这里是一个打点器的例子,它将定时的执行,直到我们将它停止。
package main
import (
"fmt"
"time"
)
func main() {
//打点器和定时器的机制有点相似:一个通道用来发送数据。
// 这里我们在这个通道上使用内置的 range 来迭代值每隔500ms 发送一次的值。
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
//打点器可以和定时器一样被停止。一旦一个打点停止了,将不能再从它的通道中接收到值。
// 我们将在运行后 1600ms停止这个打点器。
time.Sleep(time.Millisecond * 1600)
ticker.Stop()
fmt.Println("Ticker stopped")
}
执行结果如下图所示:
当我们运行这个程序时,这个打点器会在我们停止它前打点3次。
在这个例子中,我们将看到如何使用 Go 协程和通道实现一个工作池 。
package main
import (
"fmt"
"time"
)
//这是我们将要在多个并发实例中支持的任务了。
// 这些执行者将从 jobs 通道接收任务,并且通过 results 发送对应的结果。
// 我们将让每个任务间隔 1s 来模仿一个耗时的任务。
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "processing job", j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
//为了使用 worker 工作池并且收集他们的结果,我们需要2 个通道。
jobs := make(chan int, 100)
results := make(chan int, 100)
//这里启动了 3 个 worker,初始是阻塞的,因为还没有传递任务。
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
//这里我们发送 9 个 jobs,然后 close 这些通道来表示这些就是所有的任务了。
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
//最后,我们收集所有这些任务的返回值。
for a := 1; a <= 9; a++ {
<-results
}
}
执行结果如下图所示:
执行这个程序,显示 9 个任务被多个 worker 执行。整个程序处理所有的任务仅执行了 3s 而不是 9s,是因为 3 个 worker是并行的。
速率限制(英) 是一个重要的控制服务资源利用和质量的途径。Go 通过 Go 协程、通道和打点器优美的支持了速率限制。
package main
import (
"time"
"fmt"
)
func main() {
//首先我们将看一下基本的速率限制。
// 假设我们想限制我们接收请求的处理,我们将这些请求发送给一个相同的通道。
requests := make(chan int, 5)
for i := 1; i <= 5; i++ {
requests <- i
}
close(requests)
//这个 limiter 通道将每 200ms 接收一个值。这个是速率限制任务中的管理器。
limiter := time.Tick(time.Millisecond * 200)
//通过在每次请求前阻塞 limiter 通道的一个接收,我们限制自己每 200ms 执行一次请求。
for req := range requests {
<-limiter
fmt.Println("request", req, time.Now())
}
//有时候我们想临时进行速率限制,并且不影响整体的速率控制我们可以通过通道缓冲来实现。
// 这个 burstyLimiter 通道用来进行 3 次临时的脉冲型速率限制。
burstyLimiter := make(chan time.Time, 3)
//想将通道填充需要临时改变3次的值,做好准备。
for i := 0; i < 3; i++ {
burstyLimiter <- time.Now()
}
//每 200 ms 我们将添加一个新的值到 burstyLimiter中,直到达到 3 个的限制。
go func() {
for t := range time.Tick(time.Millisecond * 200) {
burstyLimiter <- t
}
}()
//现在模拟超过 5 个的接入请求。
//它们中 刚开始的 3 个将由于受 burstyLimiter 的“脉冲”影响。
burstyRequests := make(chan int, 5)
for i := 1; i <= 5; i++ {
burstyRequests <- i
}
close(burstyRequests)
for req := range burstyRequests {
<-burstyLimiter
fmt.Println("request", req, time.Now())
}
}
运行程序,我们看到第一批请求意料之中的大约每 200ms 处理一次。第二批请求,我们直接连续处理了 3 次,这是由于这个“脉冲”速率控制,然后大约每 200ms 处理其余的 2 个。
Go 中最主要的状态管理方式是通过通道间的沟通来完成的,我们在工作池的例子中碰到过,但是还是有一些其他的方法来管理状态的。这里我们将看看如何使用 sync/atomic包在多个 Go 协程中进行 原子计数 。
package main
import (
"time"
"fmt"
"sync/atomic"
"runtime"
)
func main() {
//使用一个无符号整型数来表示(永远是正整数)这个计数器。
var ops uint64 = 0
//为了模拟并发更新,我们启动 50 个 Go 协程,对计数器每隔 1ms进行一次加一操作。
for i := 0; i < 50; i++ {
go func() {
for {
//使用 AddUint64 来让计数器自动增加,使用& 语法来给出 ops 的内存地址。
atomic.AddUint64(&ops, 1)
//允许其它 Go 协程的执行
runtime.Gosched()
}
}()
}
//等待一秒,让 ops 的自加操作执行一会。
time.Sleep(time.Second)
//为了在计数器还在被其它 Go 协程更新时,安全的使用它,
// 我们通过 LoadUint64 将当前值的拷贝提取到 opsFinal中。
// 和上面一样,我们需要给这个函数所取值的内存地址 &ops
opsFinal := atomic.LoadUint64(&ops)
fmt.Println("ops:", opsFinal)
}
执行结果如下图所示:
执行这个程序,显示我们执行了大约 40,000 次操作
在前面的例子中,我们看到了如何使用原子操作来管理简单的计数器。对于更加复杂的情况,我们可以使用一个互斥锁来在 Go 协程间安全的访问数据。
package main
import (
"time"
"fmt"
"sync/atomic"
"runtime"
"sync"
"math/rand"
)
func main() {
//在此例子中,state 是一个 map。
var state = make(map[int]int)
//这里的 mutex 将同步对 state 的访问。
var mutex = &sync.Mutex{}
//we'll see later, ops will count how many operations we perform against the state.
// 为了比较基于互斥锁的处理方式和我们后面将要看到的其他方式,ops 将记录我们对 state 的操作次数。
var ops int64 = 0
//这里我们运行 100 个 Go 协程来重复读取 state。
for r := 0; r < 100; r++ {
go func() {
total := 0
for {
//每次循环读取,我们使用一个键来进行访问,
// Lock() 这个 mutex 来确保对 state 的独占访问,读取选定的键的值,
// Unlock() 这个mutex,并且 ops 值加 1。
key := rand.Intn(5)
mutex.Lock()
total += state[key]
mutex.Unlock()
atomic.AddInt64(&ops, 1)
//为了确保这个 Go 协程不会在调度中饿死,
// 我们在每次操作后明确的使用 runtime.Gosched()进行释放。
// 这个释放一般是自动处理的,像例如每个通道操作后或者time.Sleep的阻塞调用后相似,
// 但是在这个例子中我们需要手动的处理。
runtime.Gosched()
}
}()
}
//同样的,我们运行 10 个 Go 协程来模拟写入操作,使用和读取相同的模式。
for w := 0; w < 10; w++ {
go func() {
for {
key := rand.Intn(5)
val := rand.Intn(100)
mutex.Lock()
state[key] = val
mutex.Unlock()
atomic.AddInt64(&ops, 1)
runtime.Gosched()
}
}()
}
//让这 10 个 Go 协程对 state 和 mutex 的操作运行 1 s。
time.Sleep(time.Second)
//获取并输出最终的操作计数。
opsFinal := atomic.LoadInt64(&ops)
fmt.Println("ops:", opsFinal)
//对 state 使用一个最终的锁,显示它是如何结束的。
mutex.Lock()
fmt.Println("state:", state)
mutex.Unlock()
}
运行这个程序,显示我们对已进行了同步的 state 执行了3,500,000 次操作。接下来我们将看一下只使用 Go 协程和通道是如何实现相同的状态控制任务的。
在前面的例子中,我们用互斥锁进行了明确的锁定来让共享的state 跨多个 Go 协程同步访问。另一个选择是使用内置的 Go协程和通道的的同步特性来达到同样的效果。这个基于通道的方法和 Go 通过通信以及 每个 Go 协程间通过通讯来共享内存,确保每块数据有单独的 Go 协程所有的思路是一致的。
package main
import (
"time"
"fmt"
"sync/atomic"
"math/rand"
)
//在这个例子中,state 将被一个单独的 Go 协程拥有。
// 这就能够保证数据在并行读取时不会混乱。为了对 state 进行读取或者写入,
// 其他的 Go 协程将发送一条数据到拥有的 Go协程中,然后接收对应的回复。
// 结构体 readOp 和 writeOp封装这些请求,并且是拥有 Go 协程响应的一个方式。
type readOp struct {
key int
resp chan int
}
type writeOp struct {
key int
val int
resp chan bool
}
func main() {
//计算执行操作的次数。
var ops int64
//reads 和 writes 通道分别将被其他 Go 协程用来发布读和写请求。
reads := make(chan *readOp)
writes := make(chan *writeOp)
//这个就是拥有 state 的那个 Go 协程,和前面例子中的map一样,不过这里是被这个状态协程私有的。
//这个 Go 协程反复响应到达的请求。
//先响应到达的请求,然后返回一个值到响应通道 resp 来表示操作成功(或者是 reads 中请求的值)
go func() {
var state = make(map[int]int)
for {
select {
case read := <-reads:
read.resp <- state[read.key]
case write := <-writes:
state[write.key] = write.val
write.resp <- true
}
}
}()
//启动 100 个 Go 协程通过 reads 通道发起对 state 所有者Go 协程的读取请求。
// 每个读取请求需要构造一个 readOp,发送它到 reads 通道中,并通过给定的 resp 通道接收结果。
for r := 0; r < 100; r++ {
go func() {
for {
read := &readOp{
key: rand.Intn(5),
resp: make(chan int)}
reads <- read
<-read.resp
atomic.AddInt64(&ops, 1)
}
}()
}
//用相同的方法启动 10 个写操作。
for w := 0; w < 10; w++ {
go func() {
for {
write := &writeOp{
key: rand.Intn(5),
val: rand.Intn(100),
resp: make(chan bool)}
writes <- write
<-write.resp
atomic.AddInt64(&ops, 1)
}
}()
}
//让 Go 协程们跑 1s。
time.Sleep(time.Second)
//最后,获取并报告 ops 值。
opsFinal := atomic.LoadInt64(&ops)
fmt.Println("ops:", opsFinal)
}
运行这个程序显示这个基于 Go 协程的转台管理的例子达到了每秒大约 800,000 次操作。在这个特殊的例子中,基于 Go 协程的比基于互斥锁的稍复杂。这在某些例子中会有用,例如,在你有其他通道包含其中或者当你管理多个这样的互斥锁容易出错的时候。你应该使用最自然的方法,特别是关于程序正确性的时候。
Go 的 sort 包实现了内置和用户自定义数据类型的排序功能。我们首先关注内置数据类型的排序。
package main
import (
"fmt"
"sort"
)
func main() {
//排序方法是正对内置数据类型的;这里是一个字符串的例子。
// 注意排序是原地更新的,所以他会改变给定的序列并且不返回一个新值。
strs := []string{"c", "a", "d", "b"}
sort.Strings(strs)
fmt.Println("Strings:", strs)
//一个 int 排序的例子。
ints := []int{7, 2, 4}
sort.Ints(ints)
fmt.Println("Ints: ", ints)
//我们也可以使用 sort 来检查一个序列是不是已经是排好序的。
s := sort.IntsAreSorted(ints)
fmt.Println("Sorted: ", s)
}
执行结果如下图所示:
运行程序,打印排序好的字符串和整形序列以及我们 AreSorted测试的结构 true。
有时候我们想使用和集合的自然排序不同的方法对集合进行排序。例如,我们想按照字母的长度而不是首字母顺序对字符串排序。这里是一个 Go 自定义排序的例子。
package main
import (
"fmt"
"sort"
)
//为了在 Go 中使用自定义函数进行排序,我们需要一个对应的类型。
// 这里我们创建一个为内置 []string 类型的别名的ByLength 类型,
type ByLength []string
//我们在类型中实现了 sort.Interface 的 Len,Less和 Swap 方法,
// 这样我们就可以使用 sort 包的通用Sort 方法了,Len 和 Swap 通常在各个类型中都差不多,
// Less 将控制实际的自定义排序逻辑。
// 在我们的例子中,我们想按字符串长度增加的顺序来排序,所以这里使用了 len(s[i]) 和 len(s[j])。
func (s ByLength) Len() int {
return len(s)
}
func (s ByLength) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s ByLength) Less(i, j int) bool {
return len(s[i]) < len(s[j])
}
func main() {
//通过将原始的 fruits 切片转型成 ByLength 来实现我们的自定排序了。
// 然后对这个转型的切片使用 sort.Sort 方法。
fruits := []string{"peach", "banana", "kiwi"}
sort.Sort(ByLength(fruits))
fmt.Println(fruits)
}
运行这个程序,和预期的一样,显示了一个按照字符串长度排序的列表。类似的,参照这个创建一个自定义类型的方法,实现这个类型的这三个接口方法,然后在一个这个自定义类型的集合上调用 sort.Sort 方法,我们就可以使用任意的函数来排序 Go 切片了。
panic 意味着有些出乎意料的错误发生。通常我们用它来表示程序正常运行中不应该出现的,或者我们没有处理好的错误。
package main
import (
"os"
)
func main() {
//我们将在这个网站中使用 panic 来检查预期外的错误。这个是唯一一个为 panic 准备的例子。
panic("a problem")
//panic 的一个基本用法就是在一个函数返回了错误值但是我们并不知道(或者不想)处理时终止运行。
// 这里是一个在创建一个新文件时返回异常错误时的panic 用法。
_, err := os.Create("/tmp/file")
if err != nil {
panic(err)
}
}
运行程序将会引起 panic,输出一个错误消息和 Go 运行时栈信息,并且返回一个非零的状态码。
注意,不像在有些语言中使用异常处理错误,在 Go 中则习惯通过返回值来标示错误。
上一篇:学习Go语言必备案例 (3)
下一篇:学习Go语言必备案例 (5)