1、概述
最近在倒腾GO语言,用来做了一段时间研发后,发现一些特点,在此记录一下。
首先学习了下他的语言语法,发现规则和其他语言规则有点类似,函数是通过大括号来进行规范,条件语句也是通过大括号在规范,然后就是else语句必须放在if的结束大括号后面,否则会报错;语法简单,不需要像C/C++语言那样需要分号来结束每条语句,直接换行即可,也不需要像python语言那样需要强要求的换行来标识语句和函数;最后就是协程,协程可以算是go语言的最大的特点,也是go语言诞生的初衷。
很多文章有这么一句话叫做:一核有难多核围观,意思是针对多核CPU一个核忙得要死要活,剩下的核确实闲置的。出现上面的原因就是写的程序是单核处理器,不是针对多核CPU的高并发程序。在这里也记录下并发和并行的区别,借用看到资料的解释,比较形象,并发就是使用同一个锅炒不同的菜,菜品在锅中随时切换;并行就是有多个锅,每个锅同时炒不同的菜。写完这个比喻我突然想到了另一更加形象的比喻:并发就是你拿着一把刀切菜,一会切白菜,一会切萝卜,一会切茄子,你的这把刀就是CPU一个核,然后并发的去切很多菜;并行就是你用多把刀同时切不同的菜,哈哈,感觉更形象了,是不是?
(补充一下,可能有点乱:并发不是并行,并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。)
2、协程实现和协程交互
协程我们在代码里面实现直接在函数调用前面直接加go就可以实现协程调用,如下所示:
func testfun(){
//do something
}
go testfun()
这种用法还是相当方便的,只需要加一个关键字就可以实现协程功能。但是这种一般都是独立的协程,如果需要协程之间相互通信,go语言也提供多种方法,第一种就是跟C/C++一样的加锁,方法如下:
lock.Lock()
testname = "newname" //testname为全局变量,多线程操作的时候需要锁住变量
//解锁
lock.Unlock()
但是go语言虽然支持这种锁的方式进行线程之间的通信,但是go一般不用这种方式,go一般都用通道(channel)的形式来进行协程之间数据交互。下面将简述使用通道channel来实现协程之间的交互。
3、创建、使用 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就类似枷锁操作的变量,通道也是一种数据结构,跟使用枷锁方式操作变量一样,通道定义的时候也需要定下来通道的类型,定义好后不能修改。
4、数据协程、通道channel使用实例
这个例子是我在搜索资料看到的,感觉还行,放在记录一下,例子中主要展示的例子原意是有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里面去取出一个数据,如果里面没有了,循环就会结束。今天先写到这,后面再补充。