最近在倒腾GO语言,用来做了一段时间研发后,发现一些特点,在此记录一下。
首先学习了下他的语言语法,发现规则和其他语言规则有点类似,函数是通过大括号来进行规范,条件语句也是通过大括号在规范,然后就是else语句必须放在if的结束大括号后面,否则会报错;语法简单,不需要像C/C++语言那样需要分号来结束每条语句,直接换行即可,也不需要像python语言那样需要强要求的换行来标识语句和函数;最后就是协程,协程可以算是go语言的最大的特点,也是go语言诞生的初衷。
很多文章有这么一句话叫做:一核有难多核围观,意思是针对多核CPU一个核忙得要死要活,剩下的核确实闲置的。出现上面的原因就是写的程序是单核处理器,不是针对多核CPU的高并发程序。在这里也记录下并发和并行的区别,借用看到资料的解释,比较形象,并发就是使用同一个锅炒不同的菜,菜品在锅中随时切换;并行就是有多个锅,每个锅同时炒不同的菜。写完这个比喻我突然想到了另一更加形象的比喻:并发就是你拿着一把刀切菜,一会切白菜,一会切萝卜,一会切茄子,你的这把刀就是CPU一个核,然后并发的去切很多菜;并行就是你用多把刀同时切不同的菜,哈哈,感觉更形象了,是不是?
(补充一下,可能有点乱:并发不是并行,并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。)
协程我们在代码里面实现直接在函数调用前面直接加go就可以实现协程调用,如下所示:
- func testfun(){
-
- //do something
- }
-
- go testfun()
这种用法还是相当方便的,只需要加一个关键字就可以实现协程功能。但是这种一般都是独立的协程,如果需要协程之间相互通信,go语言也提供多种方法,第一种就是跟C/C++一样的加锁,方法如下:
- lock.Lock()
- testname = "newname" //testname为全局变量,多线程操作的时候需要锁住变量
- //解锁
- lock.Unlock()
但是go语言虽然支持这种锁的方式进行线程之间的通信,但是go一般不用这种方式,go一般都用通道(channel)的形式来进行协程之间数据交互。下面将简述使用通道channel来实现协程之间的交互。
channel 是一个通道、队列,那么我们关心的应该就是如何创建这个通道、将数据装到通道中、从通道中提取数据。 golang 为它设计了一个操作符:
left <- right,当 left 为 channel 时,表示向通道中写入数据(发送),并且如果通道存在数据,写入会被阻塞,所以可以建立一个大小为n的通道,当写入数量大于n时,通道才会堵塞;当 right 为通道时,表示从通道提取数据(接收)。
- package main
-
- import "fmt"
-
- func main() {
- simpleChan()
- }
-
- func simpleChan() {
- // 声明一 chan 类型的字符串的变量
- var ch chan string
- ch = make(chan string)
- // 向 channel 中发送 string 类型数据
- go func() { //前面括号里传形式参数
- ch <- "ping"
- }() //前面括号里传实参参数
- // 创建一个 string 类型的变量,用来保存从 channel 队列提取的数据
- var v string
- v = <-ch
- fmt.Println(v)
- }
上面的例子中创建了一个通道ch,然后并make了一块内存,这个通道实现了一个类似队列的功能,有数据写入,里面如果没有被读走,就排队等候。上面代码里面的两个操作语句就可以完成了数据入队列(ch <- "ping"),数据出队列(v = <-ch)的动作。这里有个问题需要注意,channel 的接收与发送需要分别在两个 goroutine 中,如果你是直接看英文的文档、或者其他介绍的文章,可能没有指出这个要求。它是跨协程的。如果ch <- "ping"不用协程调用,跑起来会报错。
从上面的例子可以看到协程通过通道ch来实现数据传递,这个通道ch就类似枷锁操作的变量,通道也是一种数据结构,跟使用枷锁方式操作变量一样,通道定义的时候也需要定下来通道的类型,定义好后不能修改。
这个例子是我在搜索资料看到的,感觉还行,放在记录一下,例子中主要展示的例子原意是有2个干活的worker,然后有五个工作job,需要这两个人来完成这五个工作,功能实现里面还定义了五个工作完成的结果。如下所示:
- // channel.go
- package main
-
- import (
- "fmt"
- "time"
- )
-
- func main() {
- workpools()
- }
-
- func workpools() {
- const number_of_jobs = 5
- const number_of_workers = 2
- jobs := make(chan int, number_of_jobs)
- results := make(chan string, number_of_jobs)
-
- // 向 任务队列写入任务
- for i := 1; i <= number_of_jobs; i++ {
- jobs <- i
- }
- fmt.Println("布置 job 后,关闭 jobs channel")
- close(jobs)
-
- // 控制并行度,每个 worker 函数都运行在单独的 goroutine 中
- for w := 1; w <= number_of_workers; w++ {
- go worker(w, jobs, results)
- }
-
- // 监听 results channel,只要有内容就会被取走
- for i := 1; i <= number_of_jobs; i++ {
- fmt.Printf("结果: %s\n", <-results)
- }
- }
-
- // worker 逻辑:一个不断从 jobs chan 中取任务的循环
- // 并将结果放在 out channel 中待取
- func worker(id int, jobs <-chan int, out chan<- string) {
- fmt.Printf("worker #%d 启动\n", id)
- for job := range jobs {
- fmt.Printf("worker #%d 开始 工作%d\n", id, job)
- // sleep 模拟 『正在处理任务』
- time.Sleep(time.Millisecond * 500)
- fmt.Printf("worker #%d 结束 工作%d\n", id, job)
-
- out <- fmt.Sprintf("worker #%d 工作%d", id, job)
- }
- fmt.Printf("worker #%d 退出\n", id)
- }
从例子中可以看到,逻辑是首先jobs是缓冲区为5的通道,所以先给工作通道布置了五个工作分别是工作1,2,3,4,5,然后就使用两个worker工作者1,2协程来完成这五个工作。运行代码可以看到如下打印信息:
- 布置 job 后,关闭 jobs channel
- worker #2 启动
- worker #2 开始 工作1
- worker #1 启动
- worker #1 开始 工作2
- worker #1 结束 工作2
- worker #2 结束 工作1
- worker #2 开始 工作4
- 结果: worker #1 工作2
- 结果: worker #2 工作1
- worker #1 开始 工作3
- worker #2 结束 工作4
- worker #2 开始 工作5
- worker #1 结束 工作3
- worker #1 退出
- 结果: worker #2 工作4
- 结果: worker #1 工作3
- worker #2 结束 工作5
- worker #2 退出
- 结果: worker #2 工作5
从上面的打印信息可以看到,两个工作者worker1,2,同时完成布置的jobs,里面的这句语句有点魔性,
for job := range jobs
在协程里面是都会使用这句语句的,根据资料说的是这个range是不管jobs里面的个数,只是从jobs里面去取出一个数据,如果里面没有了,循环就会结束。今天先写到这,后面再补充。