简单聊一聊Golang(一)

最近老大说有个活需要快速撸一个web原型出来,后端随意选择,我就想这么自由的话那我就玩玩golang吧,就简单入坑了一下。其实之前做MIT6.828的时候接触过Golang,不过当时其实写的并没有很爽,加上但是是跟着lab来做,很多golang的特性什么都其实都没有涉及到。这次正好就用golang写写server,正好还涉及到了一些分布式和并发的部分,就想试试golang的并发。

Beego

入坑的server框架我随手选了一个Beego,其实也不算随手,因为是看Beego有完整的中文文档,而且很多大公司都在用,在github上的star也相当之高,而且评价都挺好,属于比较稳定比较全的框架,就先用它入手玩玩。花了2天看了一下Golang的语法,一周时间撸了一个server出来。觉得Golang写得有时候还是挺爽,写并发确实方便。

模块

根据官网的叙述,beego是个HTTP框架,可以快速开发API层,后端逻辑层和数据接口层。和flask比较相似。结构高度模块化,基础的模块比如log,orm,config,cache,httplibs等都是高度解耦,你可以很容易的组织项目的model层,controller层,还有统一的路由模块。很小巧很灵活,比SSH那种大而全的是简单多了。个人感觉controller层封装的还挺方便的。orm层的话,对表的匹配竟然是通过大小写匹配的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type DruidHistorySearch struct {
PressureTestName string
PressureTestVersion string
Author string
CreateTime string
SpendTime int
IsMoreCity bool
IsHaveInterval bool
Qps int
Half int `orm:"column(50p)"`
NinetyFive int `orm:"column(95p)"`
NinetyNine int `orm:"column(99p)"`
Avg int
Max int
Min int
ErrorPercent float64
JsonStr string
Id int
}

默认会把驼峰的命名转成带下划线的,比如这样就会转成这样一张表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE `druid_history_search` (
`pressure_test_name` varchar(255) DEFAULT NULL,
`pressure_test_version` varchar(10) DEFAULT NULL,
`author` varchar(10) DEFAULT NULL,
`signal` varchar(50) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`spend_time` int(11) DEFAULT NULL,
`is_more_city` tinyint(1) DEFAULT NULL,
`is_have_interval` tinyint(1) DEFAULT NULL,
`qps` int(11) DEFAULT NULL,
`fifty_percent` int(11) DEFAULT NULL,
`ninety_five_percent` int(11) DEFAULT NULL,
`ninety_nine_percent` int(11) DEFAULT NULL,
`avg` int(11) DEFAULT NULL,
`max_qps` int(11) DEFAULT NULL,
`min_qps` int(11) DEFAULT NULL,
`error_percent` float DEFAULT NULL,
`json_str` blob,
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
)

因为我数据库用的是mysql,设置了大小写不敏感,如果设置了大小写敏感,就需要用这样的代码:

1
Half int `orm:"column(50p)"`

来特别指定这个变量在表中代表的字段了。

打包部署

beego有一个好处是提供了一套的tool来帮助开发部署。

1
go get github.com/beego/bee

用这个指令可以获取beego工具。比如bee指令可以有很多功能:
bee工具

用new可以新建beego项目,run可以本地热部署的运行项目,很适合debug。还可以编译到指定平台,比如你在mac上开发,想编译成linux上的可执行文件,就执行:

1
bee -be GOOS=linux GOARCH=amd64

然后把打包编译出来的文件上传的服务器上,就可以直接运行了。

Golang的面向对象

我们用面向对象的语言写了很久的工程,工业界大型系统还是以Java,C++为主。在写Golang的过程中,我发现他很多地方确实很符合软件工程的设计,很适合写工程,有着很多工程经验的约定俗成,比如变量名,代码格式化等等。这里我就讲几个特性。

golang的并发编程

这个是我最喜欢的特性,当然我最开始接触的时候一直用不好。goroutine就是轻量级的线程,基本就是协程,但又有一点不一样。

我曾经一直理解的协程是类似线程的一种。但是又有人理解的是callback的一种。我研究了一下,发现和我一样感觉的都是Java,C++出身,理解为callback的都是写node,lua等出身的。这里正好把这些概念理清楚下然后再和Golang的Goroutine联系起来。

首先我们理清一个概念,并发和并行,就是Concurrency and Parallelism,用《Computer Systems: A Programmer’s Perspective》里面的一张图来解释:

并发和并行

这里可以明确,并发是逻辑架构,非并发的程序就是一根竹竿捅到底,只有一个逻辑控制流,也就是顺序执行的(Sequential)程序,在任何时刻,程序只会处在这个逻辑控制流的某个位置。而如果某个程序有多个独立的逻辑控制流,也就是可以同时处理(deal)多件事情,我们就说这个程序是并发的。这里的“同时”,并不一定要是真正在时钟的某一时刻(那是运行状态而不是逻辑结构),而是指:如果把这些逻辑控制流画成时序流程图,它们在时间线上是可以重叠的。

并行是指程序的运行状态。如果一个程序在某一时刻被多个CPU流水线同时进行处理,那么我们就说这个程序是以并行的形式在运行。(严格意义上讲,我们不能说某程序是“并行”的,因为“并行”不是描述程序本身,而是描述程序的运行状态,但这篇小文里就不那么咬文嚼字,以下说到“并行”的时候,就是指代“以并行的形式运行”)显然,并行一定是需要硬件支持的。

所以我们就知道,进程是有自己的隔离空间的,在CPU中多个进程运行是靠时钟终端来抢占CPU时间的,线程的切换也是如此,而且比进程灵活的是进程的切换调度我们只能依赖操作系统,线程的话我们可以自己控制调度,我们可以通过系统调用来切换到内核进程,然后对整个系统的线程进行调度,所以其实线程调度和并发的逻辑和原理并不复杂,哪个线程抢占CPU全部由内核线程来调度,然后线程通过时钟中断或者系统调用来让内核获得控制权,从而进行调度。

所以从上面的逻辑中,我们可以自己设计一个并发编程的框架,不依赖系统的线程和系统调度,为了描述方便,我们接下来把“代码片段”称为“任务”。
下面通过摘取一篇blog中的片段来解释如何自己设计一个并发编程框架:
http://www.sizeofvoid.net/goroutine-under-the-hood/


>
和内核的实现类似,只是我们不需要考虑中断和系统调用,那么,我们的程序本质上就是一个循环,这个循环本身就是调度程序schedule(),我们需要维护一个任务的列表,根据我们定义的策略,先进先出或是有优先级等等,每次从列表里挑选出一个任务,然后恢复各个寄存器的值,并且JMP到该任务上次被暂停的地方,所有这些需要保存的信息都可以作为该任务的属性,存放在任务列表里。
>
看起来很简单啊,可是我们还需要解决几个问题:
>
(1) 我们运行在用户态,是没有中断或系统调用这样的机制来打断代码执行的,那么,一旦我们的schedule()代码把控制权交给了任务的代码,我们下次的调度在什么时候发生?答案是,不会发生,只有靠任务主动调用schedule(),我们才有机会进行调度,所以,这里的任务不能像线程一样依赖内核调度从而毫无顾忌的执行,我们的任务里一定要显式的调用schedule(),这就是所谓的协作式(cooperative)调度。(虽然我们可以通过注册信号处理函数来模拟内核里的时钟中断并取得控制权,可问题在于,信号处理函数是由内核调用的,在其结束的时候,内核重新获得控制权,随后返回用户态并继续沿着信号发生时被中断的代码路径执行,从而我们无法在信号处理函数内进行任务切换)
>
(2) 堆栈。和内核调度线程的原理一样,我们也需要为每个任务单独分配堆栈,并且把其堆栈信息保存在任务属性里,在任务切换时也保存或恢复当前的SS:ESP。任务堆栈的空间可以是在当前线程的堆栈上分配,也可以是在堆上分配,但通常是在堆上分配比较好:几乎没有大小或任务总数的限制、堆栈大小可以动态扩展(gcc有split stack,但太复杂了)、便于把任务切换到其他线程。
>
到这里,我们大概知道了如何构造一个并发的编程框架,可如何让任务可以并行的在多个逻辑处理器上执行呢?只有内核才有调度CPU的权限,所以,我们还是必须通过系统调用创建线程,才可以实现并行。在多线程处理多任务的时候,我们还需要考虑几个问题:
>
(1) 如果某个任务发起了一个系统调用,譬如长时间等待IO,那当前线程就被内核放入了等待调度的队列,岂不是让其他任务都没有机会执行?
>
在单线程的情况下,我们只有一个解决办法,就是使用非阻塞的IO系统调用,并让出CPU,然后在schedule()里统一进行轮询,有数据时切换回该fd对应的任务;效率略低的做法是不进行统一轮询,让各个任务在轮到自己执行时再次用非阻塞方式进行IO,直到有数据可用。
>
如果我们采用多线程来构造我们整个的程序,那么我们可以封装系统调用的接口,当某个任务进入系统调用时,我们就把当前线程留给它(暂时)独享,并开启新的线程来处理其他任务。
>
(2) 任务同步。譬如我们上节提到的生产者和消费者的例子,如何让消费者在数据还没有被生产出来的时候进入等待,并且在数据可用时触发消费者继续执行呢?
>
在单线程的情况下,我们可以定义一个结构,其中有变量用于存放交互数据本身,以及数据的当前可用状态,以及负责读写此数据的两个任务的编号。然后我们的并发编程框架再提供read和write方法供任务调用,在read方法里,我们循环检查数据是否可用,如果数据还不可用,我们就调用schedule()让出CPU进入等待;在write方法里,我们往结构里写入数据,更改数据可用状态,然后返回;在schedule()里,我们检查数据可用状态,如果可用,则激活需要读取此数据的任务,该任务继续循环检测数据是否可用,发现可用,读取,更改状态为不可用,返回。


所以golang的goroutine就是golang自己实现的类似协程的一个并发编程的框架,他就是一个函数的入口,可以在堆上为这个函数分配堆栈,所以很廉价,可以很轻松的创建上万个goroutine,因为他不涉及到操作系统的调度。他们之间通过channel来通信。