Go learn the language necessary cases (4)

31. Timer

We frequently need a time code after the Go or repeated operation within a certain time interval. Go's built- timers and RBI characteristics make these easy to implement.

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")
	}
}

Execution results as shown below:
Here Insert Picture Description
The first timer would expire in ~ 2s program is started, but the second did not fail before it stopped.

32. RBI

Timer when you want to use in the future when a certain moment of execution - RBI is ready to perform when you want to repeat in a fixed time interval. Here is an example of a RBI, it will perform timing until we stop it.

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")
}

The results as shown below:
Here Insert Picture Description
When we run this program, the RBI will we stop it before RBI three times.

33. Work Pool

In this example, we will see how to use Go Coroutine channel and implement a work pool.

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
	}
}

Execution results as shown below:
Here Insert Picture Description
the implementation of this procedure, a plurality of display 9 worker tasks are executed. The whole process only handles all the tasks performed 3s instead of 9s, because three worker parallel.

34. The rate limit

Rate limiting (UK) is an important control resource utilization and quality of service approach. Go Go by coroutine, and RBI channel supports graceful rate limiting.

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())
	}
}

Execution results as shown below:
Here Insert Picture Description

Run the program, we have seen about once every 200ms processing the first request to be expected. The second batch of requests, we treated 3 times in direct succession, because this "pulse" rate control, and then approximately every 200ms process the remaining two.

35. atomic counter

Go in the main state management is through communication between channels is accomplished, we come across examples of work in the pool, but there are some other ways to manage the condition. Here we will look at how to use the sync / atomic packets in a plurality of atoms counted Go coroutine.

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)
}

The results as shown below:
Here Insert Picture Description
execute this program, we show performed about 40,000 operations

36. mutex

在前面的例子中,我们看到了如何使用原子操作来管理简单的计数器。对于更加复杂的情况,我们可以使用一个互斥锁来在 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()
}

执行结果如下图所示:
Here Insert Picture Description

运行这个程序,显示我们对已进行了同步的 state 执行了3,500,000 次操作。接下来我们将看一下只使用 Go 协程和通道是如何实现相同的状态控制任务的。

37. 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)
}

执行结果如下图所示:
Here Insert Picture Description

运行这个程序显示这个基于 Go 协程的转台管理的例子达到了每秒大约 800,000 次操作。在这个特殊的例子中,基于 Go 协程的比基于互斥锁的稍复杂。这在某些例子中会有用,例如,在你有其他通道包含其中或者当你管理多个这样的互斥锁容易出错的时候。你应该使用最自然的方法,特别是关于程序正确性的时候。

38. 排序

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)
}

执行结果如下图所示:
Here Insert Picture Description
运行程序,打印排序好的字符串和整形序列以及我们 AreSorted测试的结构 true。

39. 使用函数自定义排序

有时候我们想使用和集合的自然排序不同的方法对集合进行排序。例如,我们想按照字母的长度而不是首字母顺序对字符串排序。这里是一个 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)
}

执行结果如下图所示:
Here Insert Picture Description

Run the program, and as expected, showing a list sorted according to the length of the string. Similarly, referring to the creation of a custom type of method to achieve this type of three interface methods, then this one from the set of defined types of calls sort.Sort method, we can use any sort of function to slice Go a.

40. Panic

panic means that some unexpected error has occurred. Usually we use it to indicate normal operation, the program should not be there, or we do not have a good handle errors.

package main

import (
	"os"
)

func main() {
	//我们将在这个网站中使用 panic 来检查预期外的错误。这个是唯一一个为 panic 准备的例子。
	panic("a problem")

	//panic 的一个基本用法就是在一个函数返回了错误值但是我们并不知道(或者不想)处理时终止运行。
	// 这里是一个在创建一个新文件时返回异常错误时的panic 用法。
	_, err := os.Create("/tmp/file")
	if err != nil {
		panic(err)
	}
}

Execution results as shown below:
Here Insert Picture Description

Run the program will cause panic, stack information and output an error message when the Go, and returns a non-zero status code.
Note that, unlike the use of exception handling errors in some languages, in Go the habit to mark error by returning a value.

Previous: Learn the essential language Go Case (3)

Next: Learn the essential language Go Case (5)

Guess you like

Origin blog.csdn.net/cui_yonghua/article/details/91859564