快速入门Go(6)

Go·语法 2019-03-02 6701 字 1173 浏览 点赞

前言

这是我学习Go语法的笔记。由于有C和Python的基础,上手Go很快。笔记很粗糙,好在自己够用。

此篇包括了Go相关的:并发编程,信号通道,定时器等。涉及关键字go select

goroutine

Go天生优秀,利用关键字go可以很容易的实现并发编程。如一个并发示例:

func print_a() {
    for {
        fmt.Println("a")
        time.Sleep(time.Second)  // 睡1s
    }
}

func print_b() {
    for {
        fmt.Println("b")
        time.Sleep(time.Second)  // // 睡1s
    }
}

func main() {
    go print_a()  // 打印 a
    go  print_b()  // 打印 b

    fmt.Println("等待线程执行")
    for {  // 死循环
    }
}

运行上述代码,可看到程序交替打印ab。如果不出意外,程序会先打印“等待线程执行”,然后打印ab。所需注意两点:

  • go方式调用的函数都是异步执行的。也就是说,不会等到被调函数返回结果才往下执行;
  • 主协程退出,它的子协程跟着退出。因此上述demo中设置死循环,是为避免程序退出,导致两个协程跟着退出。

runtime

以下简单介绍runtime包的使用,其中包括了GoschedGoexitGOMAXPROCS

Gosched

Gosched表示让出时间片,可以让本应该执行的协程稍后执行。

func main() {
    go func() {
        fmt.Println("this is a")
    }()

    fmt.Println("this is b")
}
// 输出:
this is b

上述代码中匿名函数的功能是打印语句“this is a”,以协程的方式执行。然而,在子协程开始之前,主协程已经往下执行并执行结束,所以demo的运行结果是:只打印了“this is b”。马上来试试Gosched的效果:

func main() {
    go func() {
        fmt.Println("this is a")
    }()
    
    runtime.Gosched()
    fmt.Println("this is b")
}
// 输出:
this is a
this is b

现在demo的执行结果是先打印a然后打印b。这是因为程序遇到runtime.Gosched()后会主动让出执行权,此时子协程也排队等着了,一看到“有空位”就立马钻进去执行,才有了我们看到的结果。

Goexit

Goexit能够终止一个正在执行的协程,其作用效果跟return类似。有一点需要额外注意:Goexit不能在主函数中使用,否则程序panic。子协程退出时,依然能够执行defer修饰的语句。

func test() {
    defer fmt.Println("three")
    runtime.Goexit()  // 等同return
    fmt.Println("four")  // 这句话不会被执行
}

func main() {
    fmt.Println("one")
    go test()
    fmt.Println("two")

    for{
    }
}
// 输出:
one
two
three

GOMAXPROCS

Go语言默认单核运行,通过runtime.GOMAXPROCS()设置参与运行的CPU个数。runtime.NumCPU()用来获取计算机CPU数。查阅网上部分资料,发现有人说设置最大CPU数最好不要超过实际CPU数,但也有人说在一定范围内超过效果最佳。这里存疑。

func main() {
    ...
    runtime.GOMAXPROCS(2)  // 设置最大2核处理,函数返回计算机实际最大核数
    ...
}

channel

goroutine运行在相同的地址空间上,因此访问内存必须做好同步。goroutine通过通信共享内存,而不是共享内存来通信。在函数传递中,channel引用传递。不同角度可对channel做不同分类:

  • 第一种可以是:有缓存channel和无缓存channel;
  • 第二种可以是:单向channel和双向channel。

无缓存与有缓存

创建channel:

make(chan Type)  // 无缓存,等价于make(chan Type, 0)
make(chan Type, capacity)  // 有缓存

当capacity=0时,channel无缓冲阻塞读写;当capacity>0时,channel有缓冲非阻塞,直到写满capacity个元素才阻塞写入。channel通过操作符<-接收、发送数据。如ch<-表示发出数据到ch中,<-ch表示从ch中接收数据。常用操作如下:

channel<- value  // 发送value到channel
<-channel  // 接收并丢弃
x := <-channel  // 从channel中接收数据,并赋值给x
x, ok := <-channel  // 功能同上,同时检查通道是否已关闭或者是否为空

无缓存与有缓存的示例对比

var ch = make(chan int)

func producer() {
    for {
        randomNum := rand.Intn(100)  // 生产随机数
        ch<- randomNum  // 发送channel中
        fmt.Println("生产一个数据为:", randomNum)
    }
}

func comsumer() {
    for {
        data := <-ch  // 从channel中接收数据
        fmt.Println("处理一个数据为:",  data)
    }
}

func main() {
    go producer()
    go comsumer()
    for{
    }
}

运行上述代码,总能看到生产者与消费者成对出现。这正是由于无缓冲channel在接收一个数据后会立马阻塞协程,等到消费者从channel中拿走数据再继续向下执行。

// 输出:
...
处理一个数据为: 14
生产一个数据为: 14
生产一个数据为: 16
处理一个数据为: 16
处理一个数据为: 23
生产一个数据为: 23
生产一个数据为: 57
处理一个数据为: 57
...

现将channel修改为有缓冲,缓冲为10。即:var ch = make(chan int, 10),观察运行结果:

...
处理一个数据为: 45
处理一个数据为: 23
处理一个数据为: 44
生产一个数据为: 5
生产一个数据为: 30
生产一个数据为: 34
处理一个数据为: 86
...

生产者与消费者没有成对出现。因为channel“满载”前,程序不会被阻塞


len(ch) 缓冲区数据个数; cap(ch)缓冲区大小。

单向channel

前面提到的channel都是双向的,既可写也可读。Go允许做一个单向channel,其职责是:要么写、要么读。这有利于程序员对程序的控制,以及程序出错时方便排查。

单向channel是由双向channel隐式转换来的:

ch := make(chan int)

var onlyWrite chan<- int = ch  // 只写不读
var onlyRead <-chan int = ch  // 只读不写

然而单向不能转双向。这种用法更多是在函数传参时使用,场景为消费者与生产者:

func provider(ch chan<-int) {
    ...
}

func comsumer(ch <-chan int) {
    ...
}

func main() {
    ch := make(chan int)
    go provider(ch)
    go comsumer(ch)
    ...
}

关闭channel

可利用close()关闭channel,关闭后,不可以再向channel发送数据(会引发panic),但可以从channel中读取之前已有的数据。

检查channel是否关闭可以:

if num, ok := <-ch; ok == true {
    ...
}

可利用range对channel遍历:

func get_data(onlyRead <-chan int) {
    for value := range onlyRead {
        fmt.Println(value)
    }
}

func main() {
    ch := make(chan int, 0)
    
    go get_data(ch)
    for i:=0; i<10; i++ {
        ch<- i
    }

定时器

定时器分Timer与Ticker,前者会在指定的时间上作用一下,后者则是循环作用。

Timer

简单示例:

func main() {
    fmt.Println("当前时间", time.Now())  // 打印当前时间
    timer := time.NewTimer(2 * time.Second)  // 创建一个timer,设置为2s后往timer.C管道中写数据
                                             // 写的数据是,这一刻的时间
    fmt.Println("程序继续向下执行")

    fmt.Println("程序被阻塞...")
    fmt.Println("管道中的内容:", <-timer.C)  // 程序在此阻塞,直到管道中有数据了,取出数据
    fmt.Println("当前时间", time.Now())  // 打印当前时间
}
// 输出:
当前时间 2019-03-02 10:44:46.4775098 +0800 CST m=+0.013954001
程序继续向下执行
程序被阻塞...
管道中的内容: 2019-03-02 10:44:48.5120856 +0800 CST m=+2.048529801
当前时间 2019-03-02 10:44:48.5120856 +0800 CST m=+2.048529801

time包的一些其他用法:

  • After:
time.After(2* time.Second)  // 定时2s,2s后向管道写入内容
// 由于该函数会返回一个管道类型的数据,因此可以直接对它做读操作
// eg: `<-time.After(2* time.Second)`  这种用法会把程序阻塞,类似time.Sleep()
  • Stop:
timer := time.NewTimer(2 * time.Second)
timer.Stop()  // 停止定时器,若在停止前定时器没有写入数据,以后也不会写入了
  • Reset:
fmt.Println("当前时间:", time.Now())
timer := time.NewTimer(10 * time.Second)  // 10s后写入数据
timer.Reset(time.Second)  // 重置时间,指定1s后写入数据
fmt.Println("输出内容:", <-timer.C)

// 输出:
当前时间: 2019-03-02 11:10:26.3631141 +0800 CST m=+0.014000901
输出内容: 2019-03-02 11:10:27.3977497 +0800 CST m=+1.048636501

Ticker

func main() {
    ticker := time.NewTicker(1 * time.Second)
    for {
        fmt.Println(<-ticker.C)
    }
}
// 输出:
2019-03-02 12:50:32.005107 +0800 CST m=+1.011311801
2019-03-02 12:50:33.0049539 +0800 CST m=+2.011158701
2019-03-02 12:50:34.0057679 +0800 CST m=+3.011972701
2019-03-02 12:50:35.0052581 +0800 CST m=+4.011462901
2019-03-02 12:50:36.0057503 +0800 CST m=+5.011955101
...

select

select是Go中的关键字,用于监听IO操作,当IO操作发生时,触发对应的动作。select的使用与switch相似,区别在于,select中的case必须是IO操作

...
func main() {
    var (
        chApha =  make(chan string)
        chNum = make(chan int)
    )
    ...
    select {
    case <-chApha:  // 如果`<-chApha`可以先执行,就打印"处理一个字母"
        fmt.Println("处理一个字母")
    case <-chNum:  // 如果`<-chNum`可以先执行,就打印"处理一个数字"
        fmt.Println("处理一个数字")
    default:  // 如果前面都不能执行,就打印"等不到"
        fmt.Println("等不到了")
    }
}

说明:在select中使用default,属于少数情况。



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论