现如今Golang在后端大火,介绍Golang的文章层出不穷,然而,很少有能跳出职能划分,从新的角度看待Golang的文章,既然如此,小的可以不自量力,来试一试了
首先,为什么要站在前端的角度考虑Golang呢?
因为Golang的原生异步并发能力
这和前端有什么关系?
先不急,我们来看一个例子:
const [data,setData] = useState<string>("") useEffect(()=>{ console.log(data) },[data])复制代码
这是一段 React 代码,那么我们再来看一段:
var data = make(chan string)for range data{ println(<-data) }复制代码
这是一段Golang代码,有没有感觉?
我们先抛开为什么这两段代码在逻辑上神似的问题(顺便忽略未close的deadlock),先来了解一下后端异步的问题
传统单机后端,并不需要异步,甚至并不存在异步
为何如此说?
以 Nodejs 为例,什么时候会用到异步呢?
ReadFile?WriteFile?createReadFileStream?
现实是,很多时候大家直接写作:
const content = ReadFileSync("xxxx")复制代码
而不会去写这样的代码:
const pipeline = util.promisify(stream.pipeline);const fs = require('fs');async function run() { await pipeline( fs.createReadStream('lowercase.txt'),async function* (source) { source.setEncoding('utf8'); // Work with strings rather than `Buffer`s. for await (const chunk of source) {yield chunk.toUpperCase(); } }, fs.createWriteStream('uppercase.txt') ); console.log('Pipeline 成功'); } run().catch(console.error);复制代码
就这还是 Promise 化流的结果
当年Nodejs在被创作出来的时候,作者即表示,js是很适合用来做ev的语言
话说?为什么是js?或者说,为什么是在前端使用的语言,适合用来做并发和异步?
因为并发,异步,在前端无处不在
低头看看你的无冲键盘,看看你的高刷新率鼠标和屏幕,再打开调试工具,看看请求的等待时间
是不是到处都是异步?
传统后端呢?
并没有大量异步的存在,长期以来,后端都是以会话为单位编程,以flask为例:
def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': # check if the post request has the file part if 'file' not in request.files: flash('No file part') return redirect(request.url) file = request.files['file'] # if user does not select file, browser also # submit an empty part without filename if file.filename == '': flash('No selected file') return redirect(request.url) if file and allowed_file(file.filename): filename = secure_filename(file.filename) file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) return redirect(url_for('uploaded_file', filename=filename)) return ''' <!doctype html> <title>Upload new File</title> <h1>Upload new File</h1> <form method=post enctype=multipart/form-data> <input type=file name=file> <input type=submit value=Upload> </form> '''复制代码
编程过程只存在一次请求中
那么问题来了:
- 针对会话分发,请求分发的异步并发处理
- 针对文件操作,请求操作的异步并发处理
哪一个效率更高呢?
500个请求来了,我是用nginx之类的工具做负载好呢?还是要精细到每一个文件,请求,等可能造成堵塞的方法去做优化呢?
即便是支持异步的框架,比如tornado和nodejs,很多时候你也用不到异步,比如Tornado,只在全局声明一次 eventloop 就给了你每个请求的高并发能力
局限在单次请求过程中编程,异步很多时候是困扰,并不会给你带来效率上的提升
即便你写出了:await readFile() 之类的代码,这实际上也是同步的,它与 readFileSync() 这样的代码并没有太大的区别
Nodejs 作者反对 Promise 也是这个原因,在事件驱动模式下,流才是正解
话说,异步为什么会是问题呢?
首先要知道什么是异步?
异步就是不同步,asynchronous 就是 not synchronous,同步指的是,你需要的数据就是当前的版本,异步指的是,你需要的数据不是当前版本,甚至早版本
就拿请求来说,数据库有个数据是:a = 100,前端的用户将a用键盘修改为 200,那么,输入框中的数据和后台接收到的数据就不同
不同就需要处理为相同,异步需要处理为同步
例如 python future await,或者 js async await 的处理方式就很自然出现了,然而这还不够,因为类似鼠标移动,键盘输入等事件,并不是只有一次返回,这种只有开始和结束状态的逻辑,无法处理所有异步的情况
很多人很自然想到:
在内存中开辟一个空间,所有线程,进程,网络,使用这个空间不就可以了?
这种思想可以称为单一数据源思想 —— 当我们只有一份数据可以修改,就不存在数据版本异步的问题
当然,普通人想得到的,基础设施(例如linux)的建设者们早就想到了,这种方案被称为——
共享内存
而使用共享内存作为异步通讯机制,称为
共享内存异步通讯
然而,共享内存通讯是非常复杂的,很多人会说,不就是开辟单独的内存空间么,有什么难的?
但是类似文件操作,网络操作等,都是操作系统的工作,如果应用和操作系统内核共享同一段内存,那这个系统的安全性,可靠性就基本爆炸了
所以,共享内存会存在用户态和内核态两种状态,内核态时,只有内核可以操作,用户态时,应用可以进行读写
除此之外,写入读写的并发问题,哲学家吃饭问题,都是处理异步过程中的烦恼
为了解决这些问题,有很多异步机制被提出来,比如poll,epoll模型等
而linux从内核层次支撑了这一部分:
以一个4g的linux系统的内存为例,除了头尾的部分,中间出现了堆区和栈区
而二者的中间,出现了一段“内存映射片段”:
内存映射,就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。
而网关,文件之类资源,可以用文件描述符(fs)进行访问
因此,可以实现内核与应用,应用与应用,线程间,进程间甚至是计算机与计算机之间的高效异步
相信前端程序员们已经发现了自己熟悉的堆栈,类似复杂数据存在堆中,简单数据存在栈中之类的描述
正常来说,这种异步方案人们会很自然地用事件驱动模式来思考,即,先后逻辑顺序,毕竟中间内核处理的时间,其他计算单元是无法介入的
然而,利用一些方法,是可以使用 响应式数据驱动 的模式思考同样一个问题的
如果只是简单的将内存划分为堆栈+mmap,确实只能机械地按照一个个方法处理异步逻辑,这种方法的集合就是 “流”
然而,类似TCMalloc等工具,通过更加精细的内存管理,彻底改变了这一点:
通过精细化的封装,实现这样的数据驱动高并发异步是可能的:
React 通过 FiberNode 精细化浏览器异步调度
同样 Golang 也是利用类似这样的思想
因此,留给大家的就是 Go程 + 通道:
// 一个通道channel_1 := make(chan string)// 向通道传值channel_1 <- 333// 关闭这个通道close(channel_1)// 订阅这个通道for range channel_1{ println(<-channel_1) }// 打开一个 go 程go func(){ time.Sleep(time.Second * 1) channel_1 <- "test"}()复制代码
不难找到这些方案和前端在 React 等框架中大量使用的 hooks,service 等思想
但是,以 React 为例,useState() 的状态是单一的,而 Golang 的 chan 状态并非是单一的:
var data = make(chan string, 2) go func() { time.Sleep(time.Second * 1) data <- "test" time.Sleep(time.Second * 1) data <- "test2" }() println(<-data) println(<-data)复制代码
这是有两个缓存的 channel,你答应第几个,它就是第几个数据,这种模式有点类似与 generator 方案,然而控制数据异步的仅仅是数据源,并非 xxx.next
Golang的三位作者之一,同时也是Javascript V8引擎的作者,相信前端从业者会从很多api的使用上找到亲切感
Golang现在市场比较火热,我之前有篇文章提到过前端应该如何反内卷,有一条解决方案便是去更好的市场,更有议价权的市场中试试看,Golang也是一个很好的方案
千万不要提全栈,全栈是内卷得不能再内卷的自轻自贱
有疑问加站长微信联系(非本文作者)