Go语法与基础编程思想 | 青训营笔记

120 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 1 天

Go语法与编程思想

思想:协程与goroutine

并发:[单核CPU情况],指多个线程在单核CPU上运行。

image.png

(图片来源:青训营课件资料文档)

并行:[多核CPU情况],指多个线程在多核CPU上运行。

image.png

(图片来源:青训营课件资料文档)

开启一个协程的习惯语句:

go func(){
    //your code here...
}()

通过使用关键字 go 来完成开启协程目的,通过匿名函数func(){}()的形式进行协程语句包装。

思想:channel在协程(goroutine)之间进行通信

image.png

(图片来源:青训营课件资料文档)

Go鼓励通过通道/管道的方式,在协程之间进行通信。

来看下面一段代码:

func main(){
    src := make(chan int)
    dest := make(chan int, 5)
    go func() {
        defer close(src)
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
    go func() {
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()
    for i := range dest { 
        fmt.Println(i)
    }
}

1. 上述代码中语法讲解

  1. channel

    Go语言中使用chan表示channel对象,通过make分配内存

    chan的声明方案通常有两种:

    ch := make(chan T)          //声明一个channel,无缓冲区
    ch := make(chan T, [int])   //声明一个channel,有大小为[int]的缓冲区
    

    管道的send/receive方案:

    //表示往管道内传入1
    ch <- 1  
    
    //表示从管道接收数据(此处会接收到1)
    var i int
    i <- ch  
    

    Go使用"<-"进行channel的数据收放,在声明channel的时候也是如此

    m := make(chan T)      //声明一个可以用来接收和发送类型为T的数据 的管道
    m := make(chan<- T)    //只用来接收T类型数据 的管道
    m := make(<-chan T)    //只用来发送T类型数据 的管道
    

    channel在使用完之后,通常通过close(ch)来进行关闭

  2. defer

    defer表示延迟执行,类似于C++中的析构函数。它通常会在执行完一个代码块中的语句后,再执行。

    举两个例子,一看便知:

    i:=0
    defer func(a int){
        fmt.Println(a)
    }(i)    
    i++
    //这里会打印0,因为defer入栈的时候,记录的i的值为0,所以即便是i++之后再执行defer中的内容,
    //依然是打印入栈时候的值
    
    i:=0
    defer func(a *int){
        fmt.Println(*a)
    }(&i)    
    i++
    //这里会打印1,因为入栈的时候记录的i的地址,所以在读地址内容的时候,会读到1

    在讲完基本内容之后,我们回到最上面的代码段,讲解遗漏的内容。

2. 代码讲解图示

流程图.png

main函数单开两个routine,这两个routine通过src(channel)进行通信。最后通过dest(channel)将数据传出给main函数,从而对管道内的数据进行后续处理。

其中对于代码中range进行额外讲解:

range可以用来遍历数组,但是在此处是用来判断管道是否关闭,如果管道没有关闭则持续接收数据,否则退出循环。

思想:锁与协程

锁,在并发编程开发中用于对多个线程对同一变量进行访问时的管理操作,例如两个协程同时对一个变量进行争抢,在这种情况下,就会用到锁。

锁的编程思想虽然不仅仅是在Go中存在(Java同样有),但是针对Go语言的goroutine与channel的语言特性,此处仍然介绍锁在Go中的相关应用

此处仅讲解互斥锁 (Mutex)

Go对于互斥锁的声明:

type Mutex struct {
   state int32
   sema  uint32
}


type Locker interface {
   Lock()
   Unlock()
}

func (m *Mutex) Lock() 

func (m *Mutex) Unlock()

看下面一段笔者略作改动的代码:

import (
    "fmt"
    "sync"
    "time"
)

var (
    x    int64
    lock sync.Mutex //注意  锁是在外面的  不是在程序里面的,这个锁叫互斥锁
)

func addWithLock() {
    for i := 0; i < 5000; i++ {
        lock.Lock() //如果锁已被使用(lock),则阻塞,直到锁可用(unlock)
        x++
        lock.Unlock()
    }
}

func addWithoutLock() {
    for i := 0; i < 5000; i++ {
        x++
    }
}

func main() {
    a := time.Now()
    x = 0
    for i := 0; i < 5; i++ { //这句话表示同时开五个协程 for循环开启的时间可以忽略不计
        go func() {
            addWithoutLock()
        }()
    }
    time.Sleep(time.Second)
    fmt.Println("Without Lock: ", x)
    b := time.Now()
    fmt.Println("The time is: ", b.Sub(a))

    a = time.Now()
    x = 0
    for i := 0; i < 5; i++ {
        go func() {
            addWithLock()
        }()
    }
    time.Sleep(time.Second)
    fmt.Println("With Lock: ", x)
    b = time.Now()
    fmt.Println("The time is: ", b.Sub(a)) //实际上,加了锁,耗时并不是特别大
}

1. 程序讲解

这个程序是用来比较两种计数的差别,分别比较上锁与不上锁,针对同一变量x进行多个协程进行自加的情况。

2. 程序时间差别

观察到程序中有笔者自己加上的两个时间差值输出,

image.png

实际上,加锁操作Lock()与Unlock()对于时间的影响并不是特别大。

3.锁了什么?

观察到两种不同的计数方式,五个goroutines同时进行,每个自加5000次,加出来的结果,上了锁的程序部分是对的,而未上锁的程序加出来的结果与目标值相差甚远。

那么lock.Lock()lock.Unlock()都锁了什么呢?

这个锁并不是用来锁变量的,而是用来锁goroutine

程序执行到lock.Lock()的时候,会检测lock状态,如果lock已经是上锁状态,那么该锁定操作被阻塞,直到该互斥锁回到被解锁状态(Unlock状态)。

所以在使用goroutine时,利用好互斥锁管理同一资源。

4.反思与思考:如果使用defer lock.Unlock()会发生什么?

来看这段代码:

func addWithLock() {
    for i := 0; i < 5000; i++ {
        lock.Lock() 
        defer lock.Unlock()	
        x++
    }
}

笔者在搜寻defer与lock时,看到其他人的工程中有这样的写法,于是想照搬试试,因为defer确实是最后执行,通过defer lock.Unlock()可以很好的进行解锁操作,以免代码写到后面忘记解锁了。

但是实际运行效果如何?

image.png

实际上,并没有把结果自加到25000,甚至在1就停止了。

而且我们观察时间,发现依然执行了1s。

这是因为defer在循环中执行,会依次入栈。单从这个循环进行分析,lock.Unlock()语句会依次入栈5000次,每一个循环都是遇到Lock的情形,直到循环到第5000次时,才会依次stack.pop() (也就是挨个unlock)

所以,从第二次循环开始,每一次都会遇到Lock的状态,难怪无法完成自加。

在这里我们认识到:defer与unlock的操作在实际开发过程中确实并不少见,但是一定要处理好运用场景,注意是否在循环中或者其他情况,避免程序异常执行。