闭包的概念
Go最重要的特性之一,就是它支持头等函数。它能作为变量传递给其他函数,也可以作为其他函数的返回。也因此,我们可以把它作为闭包来使用。
闭包的作用很大,它有助于复用代码,以及隔离数据。而良好的数据隔离,是很有助于代码性能的。
闭包保持一个局部的作用域,并且可以访问外部函数的作用域和参数,以及全局变量。闭包指的是在其主体之外引用变量的函数。这些函数能够访问与赋值被引用的变量,因此可以在函数之间传递闭包。
匿名函数
从匿名函数开始说起,顾名思义,匿名函数是没有名称和标志符的函数。如果之前有学习过其他语言的话,会更好理解点,比如Java中的Lambda表达式。
这是一个普通的函数:
func helloWorld(){
fmt.Println("Hello World")
}
执行的结果,就是在控制台输出“Hello world”
如果我们要把它转成一个匿名函数呢?如下:
func() {
fmt.Println("Hello World")
}()
可以把下面这个方法作为变量传入:
fmt.Println("Hello World From Anonymous Function Variable")
完整的测试代码如下:
package main
import "fmt"
func main() {
helloWorld()
func() { fmt.Println("Hello World From Anonymous Function.") }()
var helloFunction func() = func() {
fmt.Println("Hello World From Anonymous Function Variable.")
}
helloFunction()
}
func helloWorld() {
fmt.Println("Hello World from normal function.")
}
执行代码后,会看到如下结果:
Hello World from normal function.
Hello World From Anonymous Function.
Hello World From Anonymous Function Variable.
闭包的匿名函数
虽然闭包很好用,但是往往开发者在学习过程当中都被告知要谨慎使用闭包。那么这是为什么呢?下面给出一个例子,我们在定义了匿名函数之后,就可以利用闭包来引用外部变量,代码如下:
package main
import "fmt"
func add() func() int {
var i = 0
return func() int {
i++ //每次调用自增
return i
}
}
func main() {
n1 := add()
fmt.Println("n1 increment counter #1: ", n1()) // 第一次调用n1
fmt.Println("n1 increment counter #2: ", n1()) // 第二次调用n1
n2 := add() // 创建新实例
fmt.Println("n2 increment counter #1: ", n2()) // 调用一次n2
fmt.Println("n1 increment counter #3: ", n1()) // 第三次调用n1
}
输出结果如下:
n1 increment counter #1: 1
n1 increment counter #2: 2
n2 increment counter #1: 1
n1 increment counter #3: 3
通过上面的例子,可以看到闭包很好的起到了隔离数据的作用,n2的调用并没有影响到n1中的计数。能够在函数调用之间持久化数据,同时还能将数据与其他调用隔离开,这就是匿名函数强大之处。
使用闭包来嵌套任务
闭包也常用于实现嵌套任务,如下例,对一个字符串切片进行了两次append操作,最后排序:
func main() {
input := []string{"a", "d", "e"}
var result []string
func() {
result = append(input, "b")
result = append(result, "z")
sort.Sort(sort.StringSlice(result))
}()
fmt.Print(result)
}
结合goroutine使用,对于性能提升很有帮助。
带闭包的HTTP处理器
闭包在Go的HTTP调用中也常被用作中间件,我们可以把普通的HTTP函数调用包装在一个闭包里,按需来给这个调用添加附加信息,并且可被其他函数重用。
下面是一个示例,准备4个路由:
- "/" :返回中的Header中有:CustomHeader:RandomValue——由addHeader添加;状态码200——由setStatusCode添加;返回内容:Hellow World——由writeResponse添加。
- "/headerOnly":只添加一个Header
- "/statusOnly":只设置状态码
- "/admin":检查用户是否存在,根据Header中是否有user:admin键值对判断,存在的话就打印用户信息,不存在的话就返回一个401未授权的响应。
使用这个例子是因为比较容易理解,在HTTP处理中使用闭包也很方便,它们可以做到以下几点:
- 从数据库访问中隔离出数据
- 处理授权请求
- 用隔离的数据(比如时序数据)来包装其他函数
- 以在容忍范围内的超时机制与第三方服务进行透明的交互
代码如下:
package main
import (
"fmt"
"net/http"
)
func adminCheck(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("user") != "admin" {
http.Error(w, "Not Authorized", 401)
return
}
_, _ = fmt.Fprintln(w, "Bob")
h.ServeHTTP(w, r)
}
}
func setStatusCode(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
h.ServeHTTP(w, r)
}
}
func addHeader(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("CustomHeader", "RandomValue")
h.ServeHTTP(w, r)
}
}
func writeResponse(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintln(w, "Hello World!")
}
func main() {
handler := http.HandlerFunc(writeResponse)
http.Handle("/", addHeader(setStatusCode(handler)))
http.Handle("/headerOnly", addHeader(handler))
http.Handle("/statusOnly", setStatusCode(handler))
http.Handle("/admin", adminCheck(handler))
_ = http.ListenAndServe(":8080", nil)
}
请求"/":
C:\Users\Limbo>curl -D - localhost:8080
HTTP/1.1 200 OK
Customheader: RandomValue
Date: Sat, 10 Oct 2020 08:51:47 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Hello World!
请求“/headerOnly”:
C:\Users\Limbo>curl -D - localhost:8080/headerOnly
HTTP/1.1 200 OK
Customheader: RandomValue
Date: Sat, 10 Oct 2020 08:53:21 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Hello World!
请求“/statusOnly”:
C:\Users\Limbo>curl -D - localhost:8080/statusOnly
HTTP/1.1 200 OK
Date: Sat, 10 Oct 2020 08:54:38 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Hello World!
以未授权用户的身份去请求"/admin":
C:\Users\Limbo>curl -D - localhost:8080/admin
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sat, 10 Oct 2020 08:55:43 GMT
Content-Length: 15
Not Authorized
以授权用户的身份去请求"/admin":
C:\Users\Limbo>curl -D - localhost:8080/admin -H user:admin
HTTP/1.1 200 OK
Date: Sat, 10 Oct 2020 08:56:21 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8
Bob
Hello World!
通过这个例子可以了解到,用匿名函数用作中间件,有助于快速迭代,同时维护代码复杂度。
Goroutines
Go是一门以并发为设计理念的语言,并发指执行独立进程的能力。Goroutines是Go中的一种结构,可以用以实现并发,它也被称作是轻量级线程。其他的编程语言中,比如Java,是通过线程来实现并发的,而线程是由操作系统处理的,这反过来需求更大的堆栈空间,通常在开发者限制的堆栈大小下,用以处理比较有限的并发场景,而Goroutines并不涉及到操作系统底层,其生命周期交由Go语言的调度器来管理,可以很好的避免系统调度器带来的巨大开销。
Go的调度器
Go的运行时调度器对goroutine生命周期的管理分为几个不同的部分。Go调度器设计理念来源于论文《Scheduling Multithreaded Computations by Work Stealing》,论文的基本概念是确保动态的多线程计算,以此在维持内存占用的同时有效的利用CPU资源。
Goroutines在初始化时的堆栈大小只有2KB,这也是Goroutines在并发编程当中最吸引人的特点之一。因为在其他使用线程的语言中,如果创建成千上万线程的话很可能会占用几十上百Mb的空间,而在Go中却可以轻松实现。如果Goroutines需要更多的空间时,Go函数可以另寻空闲内存区域并且分配一个更大的值。默认情况下,会为新划分的区域分配两倍的空间。
Goroutines会在系统调用的时候阻塞一个正在运行的线程,当这种情况发生的时候,Go会从调度器中抽取另一个线程,用以其他等待被执行的Goroutines。
工作共享是调度器将新线程迁移到其他处理器上进行工作分配的过程,工作窃取也执行类似的操作,但是其中未充分利用的处理器会从其他处理器中窃取线程。在Go中遵循工作窃取模式,有助于提高调度器的执行效率,进而给运行在操作系统内核调度器上的Goroutines提供更高的吞吐量。Go的调度器还能够旋转线程,旋转线程相比抢占线程会占用额外的CPU周期。线程的旋转有三种不同的方式:
- 当一个线程没有连接到一个处理器上时
- 当一个goroutine准备好的时候,会解除系统线程对一个空闲处理器的阻塞
- 当一个线程正在运行,但是没有goroutine连接到这个线程上的时候,这个空闲的线程会继续寻找可运行的goroutine来执行。
Scheduler和Goroutine的底层
Go语言有3个关键结构体来处理Goroutines的工作量:M,P,G。三者协作来高效处理groutines。
- M
M是Machine(机器)的缩写,M代表一个系统线程,它包含一个指向goroutines全局队列(由P定义)的指针,M从P中检索任务。M里包含了待执行的空闲和等待的goroutines。源码链接:点我。一些值得注意的M当中的参数如下:
-
g0——一个包含调度堆栈的goroutine
-
tls——线程的本地存储
-
p——指向执行Go代码的P的链接,如果没有需要执行的代码,就是nil
-
P
P是Processor(处理器)的缩写,P代表一个逻辑处理器,这是由gomaxprocs参数设置的。P负责维护所有groutines的队列(由G定义)。当使用Go的执行器调用一个新的goroutine时,这个新的goroutine会被插入到P的队列当中,如果P没有相关的M,则分配一个新的M。源码链接:点我。P的主要参数如下:
-
id——P的ID
-
m——相关M的反向链接(如果M适用)
-
deferpool——延迟结构池
-
runq——可运行的goroutines队列
-
gFree——可用的G's(status == Gdead)结构
-
G
G是Goroutine的缩写,表示goroutine的堆栈参数。这些参数对于goroutine非常重要,每一个新的和运行时的goroutine都会创建G结构。源码链接:点我。一些关键参数如下:
- stack——实际堆栈空间
- stackguard0——Go的堆栈成长序幕的当前值
- stackguard1——C的堆栈成长序幕的当前值
- m——当前的M结构
Goroutines的使用
在对goroutines的原理有基本了解后,就可以开始尝试简单使用了,代码如下例:
func printSleep(s string) {
for index, stringVal := range s {
fmt.Printf("%#U at index %d\n", stringVal, index)
time.Sleep(1 * time.Millisecond)
}
}
func main() {
const t time.Duration = 9
go printSleep("Hello World")
time.Sleep(t * time.Millisecond)
fmt.Println("sleep complete")
}
在该例中,printSleep函数每间隔1ms输出给定字符串中的每一位,主函数中使用go执行printSleep函数,之后主函数休眠9ms,再输出休眠结束的提示语。结果如下:
U+0048 'H' at index 0
U+0065 'e' at index 1
U+006C 'l' at index 2
U+006C 'l' at index 3
U+006F 'o' at index 4
sleep complete
可以发现,只输出了前5个字符,程序就终止了,这跟其他使用线程来实现并发的语言一样,因为main()函数终止,导致其他的goroutines也被终止。如果我们把const t的值改到20,就能执行完了。
Channel
Go中的channel可以用来发送和接收值,通常与goroutine一起使用,以便在goroutine之间传递对象。Channel主要有两种类型:非缓冲通道(unbuffered)和缓冲通道(buffered)。
- Channel的结构
Channel由Golang内置的make()函数调用,,在这里会创建一个hchan结构,hchan中包含了队列中数据的计数,队列的大小,缓冲区的数组指针,发送和接收的索引,发送和接收的等待者集合,以及一个互斥锁。如下,更多详情可参见源码:点我。
type hchan struct {
33 qcount uint // total data in the queue
34 dataqsiz uint // size of the circular queue
35 buf unsafe.Pointer // points to an array of dataqsiz elements
36 elemsize uint16
37 closed uint32
38 elemtype *_type // element type
39 sendx uint // send index
40 recvx uint // receive index
41 recvq waitq // list of recv waiters
42 sendq waitq // list of send waiters
43
44 // lock protects all fields in hchan, as well as several
45 // fields in sudogs blocked on this channel.
46 //
47 // Do not change another G's status while holding this lock
48 // (in particular, do not ready a G), as this can deadlock
49 // with stack shrinking.
50 lock mutex
51 }
- 缓冲通道(Buffered Channel)
缓冲通道是有界的,通常性能比无界通道要高。用它从当前已启动的一定数量的goroutine中检索数据很有效,因为它是一个FIFO(先进先出)的队列,也因此,我们可以按照请求进来的顺序进行处理。Channel在被使用之前,会先通过调用make()函数来创建。当一个channel被创建好之后,就可以立即投入使用。只要channel中还有足够空间,就不会阻塞写入。下面给出一个例子:
1.把foo和bar写入到缓冲通道中
2.校验channel的长度
3.把foo和bar从channel中弹出
4.再次校验channel长度
5.把baz添加到channel中
6.弹出baz到一个变量上
7.输出baz
8.关闭channel
func main() {
buffered_channel := make(chan string, 2)
buffered_channel <- "foo"
buffered_channel <- "bar"
fmt.Println("Channel Length After Add ", len(buffered_channel))
fmt.Println(<-buffered_channel)
fmt.Println(<-buffered_channel)
fmt.Println("Channel Length After Pop: ", len(buffered_channel))
buffered_channel <- "baz"
out := <-buffered_channel
fmt.Println(out)
close(buffered_channel)
}
控制台的输出如下:
Channel Length After Add 2
foo
bar
Channel Length After Pop: 0
baz
如上例,我们能够把数据推送到channel中,也能把数据从channel中弹出。len()函数能够返回缓冲通道中未读或者说是位于队列中的元素数量。除了len()之外,还可以使用cap()内置函数来推断缓冲区的总容量。将这两个函数结合起来使用,可以用来了解channel当前的状态。当执行close()函数的时候,是在通知Scheduler不会再有数据发送到这个channel上了。如果往一个已经关闭的channel或者没有空闲空间的channel中继续写入数据,就会引发panic,就像下面这个示例:
func main() {
ch := make(chan string, 1)
close(ch)
ch <- "foo"
}
panic信息:
panic: send on closed channel
- 非缓冲通道
非缓冲通道是Go中的默认通道。非缓冲通道更灵活,因为它是无界的。当消费者的消费速度比生产者慢的时候,非缓冲通道通常是更好的选择。它的读写也都是阻塞的,生产者会在消费者接收到数据之前保持阻塞。这类通道往往与goroutine一起使用,以确保对数据按照预期的顺序进行消费。下面给出一个例子:
1.创建一个Boolean类型的通道来存储状态
2.创建一个未经过排序的slice
3.用sortInts()函数对上面创建的slice进行排序
4.把true发送到channel中代表排序操作已经完成
5.在slice中搜索一个给定的整数
6.把true发送到channel中代表搜索操作已经完成
7.返回channel中的值
func sortInts(intArray []int, done chan bool) {
sort.Ints(intArray)
fmt.Printf("Sorted Array: %v\n", intArray)
done <- true
}
func searchInts(intArray []int, searchNumber int, done chan bool) {
sorted := sort.SearchInts(intArray, searchNumber)
if sorted < len(intArray) {
fmt.Printf("Found element %d at array position %d\n", searchNumber, sorted)
} else {
fmt.Printf("Element %d not found in array %v\n", searchNumber, intArray)
}
done <- true
}
func main() {
ch := make(chan bool)
go func() {
s := []int{2, 11, 3, 34, 5, 0, 16}
fmt.Println("Unsorted array : ", s)
searchNUmber := 16
sortInts(s, ch)
searchInts(s, searchNUmber, ch)
}()
<-ch
}
输出结果:
Unsorted array : [2 11 3 34 5 0 16]
Sorted Array: [0 2 3 5 11 16 34]
Found element 16 at array position 5
- Select
Select是Go中的一个控制结构,它可以与goroutines和channel结合使用。下面给出一个例子,创建三个channel,分别是string类型,bool类型,rune类型,执行匿名函数来给这些通道添加数据,并使用select从channel中返回值。
func main() {
ch1 := make(chan string)
ch2 := make(chan bool)
ch3 := make(chan rune)
go func() {
ch1 <- "im string channel"
}()
go func() {
ch2 <- true
}()
go func() {
time.Sleep(1 * time.Second) //此处调用Sleep是为了让其在最后输出
ch3 <- 'r'
}()
for i := 0; i < 3; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Channel1 message : ", msg1)
case msg2 := <-ch2:
fmt.Println("Channel2 message : ", msg2)
case msg3 := <-ch3:
fmt.Println("Channel3 message : ", msg3)
}
}
}
输出如下:
Channel1 message : im string channel
Channel2 message : true
Channel3 message : 114
Semaphores
Semaphores是另一种控制goroutines执行流程的途径,它提供了Worker Pool模式的能力,但是我们不需要在任务完成后关闭Worker,Worker可以是空闲的。Go中加权Semaphores的特性是比较新颖的,semaphores的sync包是在2017年实现的,是最新的并行任务构造之一。
下面给出一个例子,在一个循环中为数组元素赋值,每次间隔一定时间,这时候所有任务都是串行执行,耗时较长。
func main() {
now := time.Now()
defer func() {
excLoop()
fmt.Println(time.Since(now))
}()
}
func excLoop() {
var out = make([]string, 5)
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
out[i] = "Slow loop.\n"
}
fmt.Println(out)
}
下面是使用Semaphores的实现:
func main() {
now := time.Now()
defer func() {
excLoop()
fmt.Println(time.Since(now))
}()
}
func excLoop() {
ctx := context.Background()
var (
sem = semaphore.NewWeighted(int64(runtime.GOMAXPROCS(0)))
result = make([]string, 5)
)
for i := range result {
if err := sem.Acquire(ctx, 1); err != nil {
break
}
go func(i int) {
defer sem.Release(1)
time.Sleep(100 * time.Millisecond)
result[i] = "Semaphores are Cool \n"
}(i)
}
if err := sem.Acquire(ctx, int64(runtime.GOMAXPROCS(0))); err != nil {
fmt.Println("Error acquiring semaphore")
}
fmt.Println(result)
}
对比一下两种实现的运行效率:
[Slow loop.
Slow loop.
Slow loop.
Slow loop.
Slow loop.
]
503.6531ms
[Semaphores are Cool
Semaphores are Cool
Semaphores are Cool
Semaphores are Cool
Semaphores are Cool
]
100.7296ms
可以看到相差达到5倍,当然这个提升是取决于使用场景的,如果实际生产中阻塞时间更长,性能提升也会随之提升。
WaitGroups
WaitGroups通常用于验证多个goroutines是否已经完成,以确保我们已经完成了所有期望完成的任务。
下面给出一个例子,用一个WaitGroup向4个站点发出请求。这个WaitGroup将等待所有请求完成,并且只有在所有的值返回后才会结束主函数。
func retrieve(url string, wg *sync.WaitGroup) {
defer wg.Done()
start := time.Now()
res, err := http.Get(url)
end := time.Since(start)
if err != nil {
panic(err)
}
fmt.Println(url, res.StatusCode, end)
}
func main() {
var wg sync.WaitGroup
var urls = []string{"https://godoc.org", "https://www.packtpub.com", "https://kubernetes.io/"}
for i := range urls {
wg.Add(1)
go retrieve(urls[i], &wg)
}
wg.Wait()
}
下面是执行结果:
https://godoc.org 200 1.5568622s
https://kubernetes.io/ 200 2.2041597s
https://www.packtpub.com 200 7.2238459s
Process finished with exit code 0
Iterators
迭代器(Iterators )又称为光标(cursor),是软件设计中的一种设计模式,可在容器对象,通常是一个列表上访问的接口,Go中有多种不同用途的迭代器。
| 迭代器 | 使用复杂度 | 缺陷 |
| for循环 | 低 | 没有自动并行 |
| callback iterator | 低 | 可读性较差 |
| channel | 低 | 并行,但是成本较高 |
| stateful iterator | 高 | 较为复杂 |
下面给出各类迭代器的示例代码,并且做一个基准测试来对比:
for
var sumLoops int
func simpleLoop(n int) int {
for i := 0; i < n; i++ {
sumLoops += i
}
return sumLoops
}
callback
func callbackLoop(top int) {
err := callbackLoopIterator(top, func(n int) error {
sumCallBack += n
return nil
})
if err != nil {
panic(err)
}
}
func callbackLoopIterator(top int, callback func(n int) error) error {
for i := 0; i < top; i++ {
err := callback(i)
if err != nil {
return err
}
}
return nil
}
next
var sumNext int
type CounterStruct struct {
err error
max int
cur int
}
func NewCounterIterator(top int) *CounterStruct {
var err error
return &CounterStruct{
err: err,
max: top,
cur: 0,
}
}
func (i *CounterStruct) Next() bool {
if i.err != nil {
return false
}
i.cur++
return i.cur <= i.max
}
func (i *CounterStruct) Value() int {
if i.err != nil || i.cur > i.max {
panic("Value is not valid after iterator finished.")
}
return i.cur
}
func NextLoop(top int) {
nextIterator := NewCounterIterator(top)
for nextIterator.Next() {
//fmt.Print(nextIterator.Value())
}
}
buffered channel(缓冲通道)
var sumBufferedChan int
func bufferedChanLoop(n int) int {
ch := make(chan int, n)
go func() {
defer close(ch)
for i := 0; i < n; i++ {
ch <- i
}
}()
for j := range ch {
sumBufferedChan += j
}
return sumBufferedChan
}
unbuffered channel(非缓冲通道)
var sumUnbufferedChan int
func UnbufferedChanLoop(n int) int {
ch := make(chan int, n)
go func() {
defer close(ch)
for i := 0; i < n; i++ {
ch <- i
}
}()
for j := range ch {
sumUnbufferedChan += j
}
return sumUnbufferedChan
}
测试代码
func benchmarkLoop(i int, b *testing.B) {
for n := 0; n < b.N; n++ {
simpleLoop(i)
}
}
func benchmarkCallback(i int, b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
callbackLoop(i)
}
}
func benchmarkNext(i int, b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
NextLoop(i)
}
}
func benchmarkBufferedChan(i int, b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
bufferedChanLoop(i)
}
}
func benchmarkUnbufferedChan(i int, b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
UnbufferedChanLoop(i)
}
}
func BenchmarkLoop10000000(b *testing.B) { benchmarkLoop(1000000, b) }
func BenchmarkCallback10000000(b *testing.B) { benchmarkCallback(1000000, b) }
func BenchmarkNext10000000(b *testing.B) { benchmarkNext(1000000, b) }
func BenchmarkBufferedChan10000000(b *testing.B) { benchmarkBufferedChan(1000000, b) }
func BenchmarkUnbufferedChan10000000(b *testing.B) { benchmarkUnbufferedChan(1000000, b) }
测试结果:
\main\iterators>go test -bench=.
goos: windows
goarch: amd64
pkg: iterators
BenchmarkLoop10000000-8 751 1594954 ns/op
BenchmarkCallback10000000-8 580 2070349 ns/op
BenchmarkNext10000000-8 897 1338152 ns/op
BenchmarkBufferedChan10000000-8 14 71559200 ns/op
BenchmarkUnbufferedChan10000000-8 15 72009293 ns/op
PASS
ok iterators 6.513s
测试场景是简单的累加,因此越简单的迭代器效果越好,但是根据场景不同,结果就会出现或大或小的差异,比如在阻塞较多的场景下,使用channel的性能明显要好得多。
Generators简述
Go中的Generator会在每次调用的时候按顺序返回下一个值,可用于并行化循环。Go中的Generator是由Goroutines实现的。通常用来实现生产者消费者模型,其本身也能并行化。
- 参考资料
《High Performance with Go》