Go 语言之三驾马车

导语:Go语言的三个核心设计: interface 、goroutine 、 channel

less is more —— Wikipedia

interface

Go是一门面向接口编程的语言,interface的设计自然是重中之重。Go中对于interface设计的巧妙之处就在于空的interface可以被当作“Duck”类型使用,它使得Go这样的静态语言拥有了一定的动态性,却又不损失静态语言在类型安全方面拥有的编译时检查的优势。

source code

从底层实现来看,interface实际上是一个结构体,包含两个成员。其中一个成员指针指向了包含类型信息的区域,可以理解为虚表指针,而另一个则指向具体数据,也就是该interface实际引用的数据。

其中 interfacetype 包含了一些关于interface本身的信息,_type表示具体实现类型,在下文eface中会有详细描述,bad 是一个状态变量,fun是一个长度为1的指针数组,在 fun0 的地址后面依次保存method对应的函数指针。go runtime 包里面有一个hash表,通过这个hash表可以取得 itab,link跟inhash则是为了保存hash表中对应的位置并设置标识。主要代码如下:

Itab的结构如下:

其中 interfacetype 包含了一些关于interface本身的信息,_type表示具体实现类型,在下文eface中会有详细描述,bad 是一个状态变量,fun是一个长度为1的指针数组,在 fun0 的地址后面依次保存method对应的函数指针。go runtime 包里面有一个hash表,通过这个hash表可以取得 itab,link跟inhash则是为了保存hash表中对应的位置并设置标识。主要代码如下:

空接口的实现略有不同。Go中任何对象都可以表示为interface{},类似于C中的 void*,而且interface{}中存有类型信息。

Type的结构如下:

i_example

关于interface的应用,下面举个简单的例子,是关于Go与Mysql数据库交互的。

首先在mysql test库中创建一张任务信息表:

数据库交互最基本的四个操作:增删改查, 这里以查询为例:

Go来实现查询这张表里面的所有数据

其中:

这段代码可以实现查表这个简单的逻辑,但是有一个小小的问题就是,我们这张表结构比较简单只有4个字段,如果换一张有20+个字段甚至更多的表来查询的话,这段代码就显得太过于低效,这个时候我们便可以引入interface{}来进行优化。

优化后的代码如下:

由于interface{}可以保存任何类型的数据,所以通过构造args、values两个数组,其中args的每个值指向values相应值的地址,来对数据进行批量的读取及后续操作,值得注意的是Go是一门强类型的语言,而且不同的interface{}是存有不同的类型信息的,在进行赋值等相关操作时需要进行类型转换。

Go对于Mysql事务处理也提供了比较好的支持。一般的操作使用的是db对象的方法,事务则是使用sql.Tx对象。使用db的Begin方法可以创建tx对象。tx对象也有数据库交互的Query,Exec和Prepare方法,与db的操作类似。查询或修改的操作完毕之后,需要调用tx对象的Commit()提交或者Rollback()回滚。

例如,现在需要利用事务对之前创建的user表进行update操作,代码如下

注意: “ := “ 跟 “ = “两个操作符不要弄混淆

如果不需要进行事务处理的话,update对应的代码如下:

可以与上面增加事务操作的代码进行对比,因为操作比较简单所以也就增加了几行代码,以及将db对象换成了tx对象。

goroutine

并发:同一时间内处理(dealing with)不同的事情

并行:同一时间内做(doing)不同的事情

Go从语言层面就支持了并行,而goroutine则是Go并行设计的核心。本质上,goroutine就是协程,拥有独立的可以自行管理的调用栈,可以把goroutine理解为轻量级的thread。但是thread是操作系统调度的,抢占式的。goroutine是通过自己的调度器来调度的。

scheduler

Go的调度器实现了G-P-M调度模型,其中有三个重要的结构:M,P,G

M : Machine (OS thread)

P : Context (Go Scheduler)

G : Goroutine

底层的数据结构长这样:

M、P 和 G 之间的交互可以通过下面这几张来自go runtime scheduler的图来展现

上图中看,有2个物理线程M,每一个M都拥有一个上下文P,也都有一个正在运行的goroutine G。图中灰色的那些G并没有运行,而是出于ready的就绪态,正在等待被调度。由P来维护着这个runqueue队列。

图中的M1可能是被新建出来的,也可能是从线程缓存中取出来的。当M0返回时,它必须尝试获取P来运行G,通常情况下,它会尝试从其他的thread那里”steal”一个P过来,失败的话,它就把G放在一个global runqueue里,然后自己会被放入线程缓存里。所有的P会周期性的检查global runqueue,否则global runqueue上的G永远无法执行。

另一种情况是P所分配的任务G很快就执行完了(因为分配不均),这就导致了某些P处于空闲状态而系统却依然在运行态。但如果global runqueue没有任务G了,那么P就不得不从其他的P那里拿一些G来执行。通常情况下,如果P从其他的P那里要偷一个任务的话,一般就‘steal’ runqueue的一半,这就确保了每个thread都能充分的使用。

P如何从其他P维护的队列中”steal”到G呢?这就涉及到work-stealing算法,关于该算法的更多信息可以参考这篇文章。

g_example

举个简单的例子来演示下goroutine是如何运行的

这段代码非常简单,两个不同的goroutine异步运行

运行结果如下:

然后做个小小的改动,只是将main()中的两个函数的位置互换,其余代码变:

会出现一件有意思的事情:

原因也很简单,因为main()返回时, 并不会等待其他goroutine(非主goroutine)结束。对上面的例子, 主函数执行完第一个say()后,创建了一个新的goroutine没来得及执行程序就结束了,所以会出现上面的运行结果。

channel

goroutine在相同的地址空间中运行,因此必须同步对共享内存的访问。Go语言提供了一个很好的通信机制channel,来满足goroutine之间数据的通信。channel与Unix shell 中的双向管道有些类似:可以通过它发送或者接收值。

source code

其中waitq的结构如下

可以看到channel其实就是一个队列加一个锁。其中sendx和recvx可以看做生产者跟消费者队列,分别保存的是等待在channel上进行读操作的goroutine和等待在channel上进行写操作的goroutine,如下图所示。

写channel (ch <- x)的具体实现如下(只选取了核心代码):

具体可以分为三种情况:

  • 有goroutine阻塞在channel上,而且chanbuf为空,直接将数据发送给该goroutine上。
  • chanbuf有空间可用:将数据放到chanbuf里面。
  • chanbuf没有空间可用:阻塞当前goroutine。

读channel( <-ch)和发送的操作类似,就不帖代码展示了。

c_example

关于goroutine跟channel进行通信的一个简单的例子,逻辑很简单:

这里我们定义了两个带缓存的channel jobs 和 results,如果把这两个channel都换成不带缓存的,就会报错,不过可以这样进行处理就可以了:

比较常见的channel操作还有select , 存在多个channel的时候,可以通过select可以监听channel上的数据流动。

因为 ch1 和 ch2 都为空,所以 case1 和 case2 都不会读取成功。 则 select 执行 default 语句。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

唐郑望的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IMWeb前端团队

也谈 setTimeout

也谈 setTimeout setTimeout ,延迟一段事件执行代码,当然这是最基本的用法,这里不说基本用法。 jQuery 中的轮询 轮询,可能是 set...

20210
来自专栏水击三千

浅谈JavaScript的字符串的replace方法

  JavaScript字符串提供了一个replace方法。replace方法可以接受两个参数:第一个参数可以使RegExp对象或者一个字符串,第二个参数可以是...

22610
来自专栏软件开发

C语言 第八章 函数、指针与宏

一、函数 函数是一个包含完成一定功能的执行代码段。我们可以把函数看成一个"黑盒子", 你只要将数据送进去就能得到结果, 而函数内部究竟是如何工作的的, 外部程序...

1695
来自专栏Linux驱动

指针学习(详解)

在指针中*是取内容,&是取地址 (在结构体中时:变量结构体用".",指针结构体用"->") 通常有两种的表示: 1. 通过指针向指向的地址内容赋值 *p=a;...

1625
来自专栏静默虚空的博客

jQuery Ajax

AJAX简介 AJAX 是与服务器交换数据的技术,它在不重载全部页面的情况下,实现了对部分网页的更新。 AJAX = 异步 JavaScript 和 XML(...

17610
来自专栏锦小年的博客

python学习笔记7.5-内建模块struct

Python中变量的类型只有列表、元祖、字典、集合等高级抽象类型,并没有像c中定义了位、字节、整型等底层初级类型。因为Python本来就是高级解释性语言,运行的...

1728
来自专栏深度学习思考者

Python学习(二) 正则表达式

Python正则表达式 正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配。re 模块使 Python 语言拥有全部的正则表达式功...

1899
来自专栏静默虚空的博客

剖析 HTTP 协议

HTTP 概述 HTTP 是什么? HTTP(HyperText Transfer Protocol,超文本传输协议)是WWW (World Wide Web)...

1937
来自专栏乐百川的学习频道

C++学习笔记 类型声明

类型别名 typedef关键字 typedef关键字是继承自C语言的特性,利用它我们可以为一个类型起别名,一般用于将复杂类型简化。举个简单的例子,将int类型定...

1849
来自专栏深度学习思考者

Python学习(三) 八大排序算法的实现(下)

本文Python实现了插入排序、基数排序、希尔排序、冒泡排序、快速排序、直接选择排序、堆排序、归并排序的后面四种。 上篇:Python学习(三) 八大排序算...

23710

扫码关注云+社区