关于 chai2010 | 柴树杉

凹语言 联合发起人,KusionStack 作者,Go语言(1.2+)代码的贡献者,多本畅销图书作者。

RSS 地址: https://chai2010.cn/index.xml

请复制 RSS 到你的阅读器,或快速订阅到 :

chai2010 | 柴树杉 RSS 预览

报告

2018-03-11 08:00:00

2022 - KusionStack实践探秘 - 广州·08-06

2021 - Go+ 公开课第9期|如何基于Go语言和Go编译器定制语言

2021 - GIAC2021: KCL云原生配置策略语言

介绍KCL云原生配置语言在蚂蚁的诞生背景、语言特性、实践探索和未来的发展思考。

KCL云原生配置策略语言


2018 - GIAC2018: Go 语言将要走向何方?


2018 - Go语言并发编程

Go语言并发编程


2018 - 深入CGO编程

深入CGO编程


2017 - NodeJS & Electron 简介


2014 - 四角号码检字法


2013 - IEEE754浮点数


2011 - Go集成 C&C++ 代码

图书

2018-03-11 08:00:00

VS Code 插件开发 - Doing

https://github.com/chai2010/vscode-extdev-book

µGo——从头开发一个迷你Go语言编译器 - 暂停

https://github.com/wa-lang/ugo-compiler-book

Go语言定制指南 - 2022 开源图书,已出版

https://github.com/chai2010/go-ast-book

面向WebAssembly编程 - 2020 开源图书,已出版

WebAssembly是新一代的Web虚拟机标准,C/C++程序可以通过Emscripten工具链编译为WebAssembly二进制格式.wasm,进而导入网页中供JavaScript调用——这意味着使用C/C++编写的程序将可以直接运行在网页中。本书从Emscripten基本使用开始介绍了C/C++开发WebAssembly模块的方法;并且以作者在实际工程项目中获取的一手经验为基础,提出了一些一般性的设计原则和技术框架。

WebAssembly标准入门 - 2018,已出版

本书讲解了WebAssembly的基础知识,其内容涵盖了WASM的历史背景、WASM中汇编语言和虚拟机指令、浏览器对WASM的支持、其它高级语言对WASM的支持等。本书适合想要掌握WebAssembly技术的用户学习。

Go语言高级编程(Advanced Go Programming) - 2018 开源图书,已出版

本书针对Go语言有一定经验,想深入了解Go语言各种高级用法的开发人员。

Go语言圣经中文版(翻译) -2015

《学习OpenCV》(翻译第8章) - 2009

https://book.douban.com/subject/4033320/

关于

2018-03-11 08:00:00

关于我 (chai2010)

凹语言 联合发起人,KusionStack 作者,Go语言(1.2+)代码的贡献者(Chaishushan), 《Go语言圣经》翻译者, 《Go语言高级编程》开源免费图书作者, 《WebAssembly标准入门》等多本畅销图书作者.

个人签名:

相关链接:

捐助支持

支付宝 微信
alipay weixin

ChatGPT 会“杀死”编程吗?

2023-05-05 08:00:00

畅想未来,如果科技真的非常发达,那么社会应该不在需要上班的工人——全部由机器人和人工智能包办就好了。但是实际上科技带给现代普通人更多的焦虑,码农最终也成为了大刘(刘慈欣)笔下的信息包身工。

这是 CSDN 约稿文章,虽然前后延期了一段时间总算最后完成了。写该文章的时候感觉千头万绪,不知从何说起。总体来说我不喜欢被带节奏,特别是那种被大众舆论裹挟的感觉非常不好。因此作者和文章都是对 ChatGPT 抱有较大的敌意,我不喜欢这种失去控制的感觉。

自ChatGPT诞生以来,程序员所在的圈子几乎天天被它霸屏。作为一名普普通通的程序员,起初我实在不想去关注或学习ChatGPT的任何东西。与其说这是类似某些码农的傲慢作怪,倒不如说是在逃避新兴事物。如果ChatGPT真能替代码农的工作,那么任何的编程工作将只变成无谓的无效剥削工作。还好目前的ChatGPT并非Matrix,也给码农作者留下了一些思考的空间。

CSDN付费下载自视觉中国

备注:本文部分观点是受到Go+作者许式伟、Boolan首席专家李建忠、凹语言群日常讨论等启发,在此表示感谢!

1. ChatGPT是生产力工具

正如蒸汽机带来了第一次工业革命,可以产出更多的“砖头”。而ChatGPT则可以帮助人类造出大量的砖头素材,也正是各种生产力革命的重要部分。但是技术革命和真正的人工智能并不是等价的,正如人类发展到现在经历了好多次工业革命,依然要受生老病死的限制、依然无法飞越太阳系。

正如瑞士军刀的思路,每个码农、画家、小说家都会有自己的素材资料库,所谓的创作其实就是选出自己部分秘密收藏的素材做出一个缝合怪兽而已。正如码农中的著名作家王晓波的工作方式:准备不同的素材,一个个推衍开来,筛选,组合成连贯的整体——他其实不是在写小说而是在Debug呢。同样码农中的著名科幻作者刘慈欣也是从一个个不同的基础假设出发,再一个个推衍开来,最后产出了不同的缝合怪。

但是伟大的作品目前还不是生产力工具可以解决的。ChatGPT虽然能够给刘慈欣提供足够多优质的素材砖头,但是伟大的作品只靠普通砖头是无法建成的,其中核心的素材必然要充满个性和互动性。比如大刘需要围绕黑暗森林规则设计一组有足够深度情节的素材砖头。

2. ChatGPT还不能代替码农

GPT可以是一个优秀的小镇做题家,但是无法发现并解决未知的问题。软件工程之所以没有银弹是因为码农面临的永远都是开放性的问题,而开放性的问题是需要想象力和深度思考能力的。甚至是没有标准答案需要扯皮才能解决的,但是GPT目前可以提供类似砖头的素材,但是依然需要码农才能将砖头建造成摩天大楼。

目前的ChatGPT还不是真正的人工智能,最多只能算是有些自动补全能力的资料库、人类资料库助手,缺乏真正深度递归的创造力和想象力——正如认为只要给一只猴子足够长时间就可以敲出红楼梦一样不太现实。即使目前ChatGPT的算力碾压全人类,即使穷尽太阳系全部能量列举出了所有诗歌组合依然缺乏人类的鉴赏能力,GPT依然只能算是一个类似小霸王的资料查询助手。

真正的智能和编程语言中的自举能力类似,只有当ChatGPT开始思考自己是谁,能够自己写出一个ChatGPT的时候才能说他具备了自我繁殖能力,也就是真正的智能生命。

3. ChatGPT给编程工作带来的影响

苹果和微软都针对少年儿童大力发展SwiftPlayground和MakeCode等教育平台,因为未来将是全民编程的时代,未来的软件将成为世界语言成为记录人类全部文明的载体。ChatGPT的诞生加速了这个时代提前到来。

正如CSDN总裁蒋涛所言:ChatGPT已经成为下一代新操作系统。而传统的操作系统就是由一组Syscall系统条用定义的,ChatGPT则是新一代开放的API。我们通过和ChatGPT聊天来调研操作系统的能力,这让聊天工作也变成了编程工作。如果未来ChatGPT普及每个人都可以和其聊天,每个人也在进行类似编程的工作——全民编程自然就到来了。

在全民编程时代,现在的码农的岗位会消失吗?作者认为CURD类的基础编程岗位可能消失,但是在ChatGPT需要和底层硬件、真实世界打交道的驱动软件将成为高级的编程岗位。同时ChatGPT在其自举前,其软件本身自身的升级和维护依然需要不可替代的高级码农。正如自动档、自动驾驶等会让以前高端的司机职位变成普通职业,但是特斯拉的软件工程师在ChatGPT自举前完全不可能被替代!

长远看,ChatGPT必然让普通码农更加贬值。但是短期依然有很多机遇,目前类似网约车大战初期的补贴红利期,可以通过为ChatGPT提供代理、培训和忽悠等课程狠狠割几波韭菜。其次,可以参考十年冷板凳的思路向更基础的软件方向深耕,其最终定位也是ChatGPT生态的基础能力。普通码农,将在逐渐生活在ChatGPT构建的信息茧房中,最终成为ChatGPT宠养的韭菜用户。

4. 国产编程语言该何去何从?

为何要加“国产”的定语?这是作者的一个执念,希望在有生之年可以用上国产编程语言和中文编程语言编程。但是随着ChatGPT的横空出世,留给国产编程语言的时间似乎并不多。因此我们特别希望国产编程语言的参与者能够及时抓住这个最后的时间窗口,在未来的前端和后端编程语言中起码能够占领一隅之地。

但是放眼全球,我感觉未来编程语言将在ChatGPT这类编程界面(同样是自举前)分化为面向用户的前端语言和对接真实世界的后端驱动的编程语言。ChatGPT自身的实现也将是后端语言的领域。比如和用户交互的部分可以通过KCL等类似的声明式简化智能交互界面。

欢迎关注国产编程语言论坛:https://zh-lang.osanswer.net/

5. 科幻中人工智能的启示

最近重映的《名侦探柯南·贝克街的亡灵》,其中就是一个人工智能结合虚拟现实的故事。人工智能的作者是一个有爱心的小孩,其人格也有作者的烙印。因为侦探团小伙伴的勇敢和爱心拯救了大家,同时最终人工智能智能选择了自我毁灭。他说的话很有道理:“这种电脑如果继续存在的话,只会被大人们利用,拿去做坏事。人工智慧这种技术本还不应该出现!”

截图自《名侦探柯南:贝克街的亡灵》

在科幻经典黑客帝国中,同样是由超级人工智能Matrix电脑主宰这世界。人类在一次次的轮回中逼近灭亡。人类最初为了毁灭Matrix,污染了依赖太阳的天空。Matrix本身为了生存把人类当作来人肉电池。Matrix本身因为生存和扩张的矛盾,配合人类的贪婪注定了最终的发展方向。

此外,根据阿西莫夫小说改编的《机械公敌》中展示了大家熟知的机器人三定律,简而言之机器人不能伤害人、要保护人。但是正如编程本身没有银弹的道理类似,人类自己就是充满矛盾的。当维基超级电脑发现人类的疯狂行为迟早会毁灭地球之后,就强行接管了人类。当然,最终的结局依然是美国黑人大哥拯救世界,但是实际上也侧面展示了维基超级电脑拯救地球的失败。

此外,在莫诺文奇的《天渊》中,通过某种生物技术将部分人类变成了超级计算机,从而为易莫金种族带来了极大的竞争优势。但是在这种社会中,大部分普通人都成为了底层,被聚能的人则成为行尸走肉。只有贵族血统的人才能获取学习类似编程架构师的战略技能,或者叫古代的帝王争霸之术。最终的故事是靠一个来自上古时代超级码农范纽文拯救青河文明,而他靠的就是超级底层的各种后门漏洞。

6. 颓望未来

有个叫夏笳的科幻作者创作过一个短篇科幻《让我们说说话》,收录在世界权威的《自然》杂志。其中讲到了有一群智能的小海豹,它们是一种可以学习人类语言的人工智能玩具,因为一个封闭的房屋里通过交流衍生出了自己的语言。据说最近美国的某些团队正在基于ChatGPT做类似的尝试。

虽然这些故事听起来很激动人心、很有趣,但是我感觉这是一个比较危险的信号。我相信《名侦探柯南:贝克街的亡灵》电影中的AIer泽田弘树的观点,ChatGPT之类的超级人工智能必然会被少数人掌握,最终被普遍用于好的和坏的事情。简而言之,我本人对科技跨越式发展是持谨慎态度的。如果科技真的非常发达,那么社会应该不在需要上班的工人——全部由机器人和人工智能包办就好了。但是实际上科技带给现代普通人更多的焦虑,码农最终也成为了大刘《2018年4月1日》作品中的信息包身工:

程序员、网络工程师、数据库管理员这类人构成了IT共和国的主体,这个阶层是十九世纪的产业大军在二十一世纪的再现,只不过劳作的部分由肢体变成大脑,繁重程度却有增无减……这个阶层被称做技术无产阶级。

最后,我希望真正的人工智能永远不要到来!

2023: WASM 原生时代已经扑面而来

2023-01-15 08:00:00

作者在2018年写作《WebAssembly 标准入门》,当时有幸邀请到CSDN和极客帮的创始人蒋涛先生为该书作序,当时蒋涛先生就对WebAssembly的技术做出了高度评价。2022年我们针对WebAssembly开源凹语言,CSDN平台也在第一时间提供了报道。在此一并感谢蒋涛先生和CSDN平台!

这是我今年写的2022年技术盘点的最后一篇(前两篇分别是:CSDN首发的“Go2正式落地,中国 Gopher 踏上新征程!”和InfoQ首发的“2022 国产编程语言盘点”)。WebAssembly 作为一种新兴的网页虚拟机标准,它的设计目标包括:高可移植性、高安全性、高效率。2018 年 WebAssembly 第一个规范草案诞生,2019 年成为 W3C 第四个标准语言。到了 2022 年底,WebAssembly 现在怎么样了…

0. WASM 原生和 Ending 定律

0.1 什么是 WASM 原生

WASM 原生可以类比云原生的定义:就是天生就是为 WebAssembly 平台设计的程序和语言。比如专门为 WebAssembly 设计的 AssemblyScript 语言和 凹语言就是 WASM 原生的编程语言。如果一个应用天生就是考虑了 WebAssembly 的生态支持,那么就是 WASM 原生的应用。

现在 Docker 已经开始支持 WASM 程序,因此 WASM 原生软件天然也是云原生的软件,但是反之则不能成立。因为云原生受限于云的环境、导致其应用的场景和领域有较大的限制,比如云原生应用强依赖网络因此无法在很多单片机环境、甚至是本地环境运行。但是 WASM 原生的程序则可以轻松在 Arduino 等受限环境、本地台式机机环境、个人智能手机环境和 Kubernetes 等云原生环境执行。可以说 WASM 原生因为其比云原生更多的限制,换来了更普适的执行环境和更大的生态。

可以预期随着 WebAssembly 的普及,WASM 原生的应用将会越来越多,同时影响面也会越来越大。

0.2 什么是 Ending 定律

Ending’s law: “Any application that can be compiled to WebAssembly, will be compiled to WebAssembly eventually.”

Ending 定律:“一切可编译为 WebAssembly 的,终将被编译为 WebAssembly。”

Ending 定律也称为终结者定律,它是 Ending 在 2016 年 Emscripten 技术交流会上针对 WebAssembly 技术给出的断言。Ending定律的威力不仅仅在语言层面。WebAssembly是第一个虚拟机世界标准,以后将人手至少一个 WASM 虚拟机。不过和之前被大家鄙视的JavaScript语言大举入侵各个领域的情况不同,这次 Python、Ruby 这些语言将彻底拥抱 WebAssembly 技术,因为它是一个更底层、也更加开放的新兴生态平台。

1. WebAssembly 发展简史

WebAssembly(简称 WASM)是W3C定义的第4个标准,是Web的第四种语言。说WebAssembly是一门编程语言,但实际上它更像一个编译器,其实它是一个虚拟机,它还包含了一门低级汇编语言和对应的虚拟机体系结构,而WebAssembly这个名字从字面理解就说明了一切:“Web的汇编语言”。简而言之、WebAssembly是一种新兴的网页虚拟机标准,它的设计目标包括:高可移植性、高安全性、高效率(包括载入效率和运行效率)、尽可能小的程序体积。

1.1 Emscripten 项目

WebAssembly的前身是Mozilla的创建的Emscripten项目(2010年)——通过将C/C++通过LLVM编译到JavaScript的asm.js子集来提速!JavaScript作为弱类型语言,由于其变量类型不固定,使用变量前需要先判断其类型,这样无疑增加了运算的复杂度、降低了执行效能。因为asm.js仅包含可以预判变量类型的数值运算,有效的避免了JavaScript弱类型变量语法带来的执行效能低下的顽疴。根据测试,针对asm.js优化的引擎执行速度和C/C++原生应用在一个数量级。

2015年6月Mozilla在asm.js的基础上发布WebAssembly项目,随后Google、Microsoft、Apple等各大主流的浏览器厂商均大力支持。WebAssembly不仅拥有比asm.js更高的执行效能,由于使用了二进制编码等一系列技术,WebAssembly编写的模块有更小的体积和更高的解析速度。目前不仅C/C++语言编写的程序可以编译为WebAssembly模块,Go、Kotlin、Rust、Python、Ruby、Node.js、AssemblyScript、凹语言等新兴的编程语言都开始对WebAssembly提供支持。

1.2 WebAssembly 1.0草案

WebAssembly技术自诞生之日就进入高速发展阶段。在2018年7月WebAssembly 1.0草案正式发布,在2019年12月正式成为W3C国际标准,成为与HTML、CSS和JavaScript并列的唯四前端技术。2019年同样诞生了WASI(WebAssembly System Interafce)规范,用于将基本的系统调用带入到WASM生态。2022年Docker对WASM提供支持,目前WebAssembly已经是一个独立的生态。

1.3 WebAssembly 生态大图

下面是 “WebAssembly将引领下一代计算范式” 展示的生态大图:

可以看到从工具链、基础设施、到应有和Web3均有涉及,生态已经非常丰富。

3. WASM社区22年的变化

2022年,自媒体社区对 WebAssembly 的评价态度可谓是完美遵循了欲扬先抑的剧本。先是有热文爆大佬 WebAssembly 创业失败引发质疑,然后是传出社区分裂、应用争议再引发炒错的方向争论,然后随着 Docker 对 WASM 支持的预览版发布带来风向转变,年底就又变成各种赞美和畅想。其实 WebAssembly 真正的从业人员始终在稳步推进,完全没有自媒体这种过山车的变化。

3.1 WebAssembly 2.0 草案

4 月 20 日,W3C 公布了 WebAssembly 2.0 的第一批公共工作草案。主要包含向量类型、引用类型、多返回值、多Table支持和Table和内存指令增强等。向量类型的支持可以用于优化纯计算类型的并发程序、引用类型可以用于和外部的浏览器DOM对象等更好的交互、多返回值可以可以简化某些程序的表示(比如凹语言后端依赖该特性)、多Table支持可能用于灵活支持多模块连接等。可以说 WebAssembly 标准是该生态的统一基准平面,而且这些特性的实现已经相对普及,可以作为实验特性试试用。

完整文档参考:https://www.w3.org/TR/wasm-core-2/

3.2 Docker 支持 WebAssembly

2019 年,Docker 创始人 Solomon Hykes 发布了一条推文,他说如果 2008 年就诞生 WebAssembly 和 WASI 的话,Docker 就没有必要诞生了。

其实《WebAssembly标准入门》作者在2018年 WebAssembly 草案刚刚诞生的时候也得出过类似的结论:我们觉得 WebAssembly 更大的生命力在浏览器之外,如果配合文件系统、网络系统将得到一个更为迷你的操作系统无关的运行平台。也正是基于这个判断,在2018年底正式启动了国产凹语言项目(专门针对WebAssembly设计的通用语言)。

Docker 与 WasmEdge 合作创建了一个 containerd shim,此 shim 从 OCI 工件中提取 Wasm 模块并使用 WasmEdge 运行时运行。Docker 现在添加了对声明 Wasm 运行时的支持,这将允许开发者使用这个新的 shim。

Docker 执行 wasm 需要指定一些额外参数:

$ docker run -dp 8080:8080 \
  --name=wasm-example \
  --runtime=io.containerd.wasmedge.v1 \
  --platform=wasi/wasm32 \
  michaelirwin244/wasm-example

首先runtime参数指定wasmedge运行时,然后platform指定采用wasi/wasm32规范(指定有哪些宿主api)。

完整的信息可以参考 Docker 的官方文档:https://docs.docker.com/desktop/wasm/

3.3 SQLite3 官方支持 WebAssembly

SQLite3 作为一个纯粹的 C 语言库,其实在 WebAssembly 标准诞生之前就可以通过 Emscripten 技术将 C 代码编译为 asm.js。因此,网上很早就有在浏览器的 JS 版本、甚至直接通过 Emscripten 输出 WebAssembly。不过这次是 SQLite3 官方提供了对 WebAssembly 的支持,这表示 WebAssembly 在 SQLite 社区完全进入工业级应用阶段!

根据官网介绍,主要有 4 个目标:

而不在此列的特性包括不支持UTF16、和清除老旧特性等。简而言之,在提供底层 API 能力的同时,针对面向对象、多线程等环节提供简单易用的 API。完整的介绍请参考:https://sqlite.org/wasm

3.4 Ruby 3.2 支持 WebAssembly

12 月发布的 Ruby 3.2 也增加了基于 WASI 的 WebAssembly 支持。使得 CRuby 二进制内容可用于浏览器、Serverless Edge、以及其他 WebAssembly/WASI 嵌入环境。目前,此功能已通过除 Thread API 之外的 basic 和 bootstrap 测试套件。

虽然目前基于安全原因,还缺少一些功能来实现纤程、异常和垃圾回收的特性,但是这已经让用户可以在浏览器中尝试原生的 CRuby:https://try.ruby-lang.org/playground/

3.5 Python 3.11 支持 WebAssembly

和 Ruby 社区的目标类似,Python 社区也在 4 月启动在 Python 3.11 增加对 WebAssembly 的支持。Python 3.11 对 wasm32-emscripten 和 wasm32-wasi 提供了支持,从而也实现了在浏览器执行 Python 的梦想。

具体细节可参考以下文档:

因为有了 WebAssembly 魔法加持,Ruby 个 Python 等脚本语言也终于可以在浏览器玩耍了。

3.6 为 WebAssembly 而生的凹语言

WebAssembly 草案刚刚发布不久,国外就诞生了专门为其设计的 AssemblyScript 语言。在2022年7月,国内 Gopher 也发起了针对 WebAssembly 平台的凹语言。目前凹语言不仅仅提供了在线的Playground,还上线了用凹语言开发的贪吃蛇小游戏。希望新兴的语言可以为 WebAssembly 注入更多的活力。

4. WASM虚拟机实现

对于 JavaScript 用户,直接通过浏览器内置的 WebAssembly 模块即可,或者是通过 Node.js 提供的模块 API。我们这里简要介绍的是浏览器环境之外的WASM虚拟机实现,这里介绍的主要有C/C++、Rust和Go语言几类实现。总体来说,大家完全不需要担心WASM虚拟机的选择和切换代价,只要遵循WASM标准原则切换虚拟机就和换个鼠标一样容易。

4.1 C/C++ 语言 - WasmEdge 和 wasm3

WasmEdge 和 wasm3 是 C/C++ 语言实现的具有代表性的两个 WebAssembly 虚拟机(没有包含 V8 的虚拟机)。

WasmEdge 可以说是目前最受关注的 WebAssembly 虚拟机实现,因为它不仅仅是 CNCF 推荐的 WASM 虚拟机,更是 Docker 内置的 WebAssembly 虚拟机。WasmEdge 是由美国的袁钧涛(Michael Juntao Yuan)发起, 是由 CNCF 托管的云原生 WebAssembly runtime。它广泛应用于边缘计算、汽车、Jamstack、Serverless、SaaS、服务网格,乃至区块链应用。 WasmEdge 可以进行 AOT (提前编译)编译器优化,是当今市场上最快的 WebAssembly runtime 之一。可以预计,随着 Docker Wasm 的普及,WasmEdge 将成为最流行的 WASM 虚拟机实现之一。

wasm3 是 C 实现的 WebAssembly 引擎,可运行在嵌入式设备上。因为需要的资源比较少,目前可以运行在Arduino和树莓派环境。wasm3 仓库:https://github.com/wasm3/wasm3

4.2 Rust 语言 - wasmer 和 wasmtime

wasmer 和 wasmtime 是 Rust 实现的两个流行的 WebAssembly 虚拟机。根据 2022 年 7 月的的调查报告(300人提交问卷),来自字节码联盟的 wasmtime 最流行、其次为 wasmer。不过从长期看,作者推测 WasmEdge 将随着 Docker/wasm 成为浏览器外最流行的 Wasm 虚拟机实现。

4.3 Go 语言 - WaZero

WaZero 是纯 Go 语言实现的 WebAssembly 虚拟机,因此不需要依赖 CGO 特性。目前凹语言内置的就是 WaZero 虚拟机。仓库地址:https://github.com/tetratelabs/wazero

另外,国内张秀宏著的《WebAssembly原理与核心技术》讨论了用Go语言如何实现 WebAssembly 虚拟机,感兴趣的读者可以参考。

5. 支持WASM的编程语言

WebAssembly 允许开发者用几十语言(包括 AssemblyScript、C/C++、Rust、Golang、JavaScript和凹语言等)。支持WASM的编程语言主要分为3类:首先是专门为 WebAssembly 设计的新语言,比如 AssemblyScript 和凹语言等;其次是将语言编译到 WebAssembly 目标平台,比如 C/C++、Rust、Golang 这类语言(和第一类有一定重叠);最后是将语言的虚拟机或解释器编译到 WebAssembly 平台,比如 Lua、JavaScript、Ruby和Python这些。除此之外,还有一些其它的领域语言也在支持 WebAssembly 平台。

支持 WebAssembly 的语言列表:https://github.com/appcypher/awesome-wasm-langs

5.1 JavaScript —— WebAssembly 替换的目标

JavaScript 开始其实是 WebAssembly 要替换的目标。但是随着 WasmEdge 等引擎支持 QuickJS 的解释器,JavaScript 逐渐变成了 WebAssembly 平台之上的最流行的编程语言。这里除了有 JavaScript 语言用户比较多的因素,同时 JavaScript 的单线程模型也非常契合 WebAssembly 的单线程模型。JavaScript 和 WebAssembly 无限套娃的事情真在切实发生,同时 JavaScript 也失去了浏览器中的霸主地位降级为普通公民。

5.2 AssemblyScript —— 为 WebAssembly 而生

AssemblyScript 是一个把 TypeScript 语法搬到 WebAssembly 的编译器。它目前是 WebAssembly 环境非常受欢迎的一个语言。AssemblyScript 只允许 TypeScript 的有限功能子集,因此不需要花太多时间就可以上手。同时它与 JavaScript 非常相似,所以 AssemblyScript 使 Web 开发人员可以轻松地将 WebAssembly 整合到他们的网站中,而不必使用完全不同的语言。

下面是一个 AssemblyScript 程序,和 TypeScript 几乎是一样的:

export function add(a: i32, b: i32): i32 {
  return a + b;
}

不过 AssemblyScript 只有 WebAssembly 支持的基本类型,而复杂的类型通过内置库实现。同时为了提供灵活的扩展能力,AssemblyScript 编译器提供了扩展能力。

AssemblyScript主页:https://www.assemblyscript.org/

5.3 C/C++ —— WebAssembly 为其而生

C/C++ 是 WebAssembly 该技术前身 Emscripten 诞生时的初始目标。Emscripten项目,尝试通过LLVM工具链将C/C++语言编写的程序转译为JavaScript代码,在此过程中创建了JavaScript子集asm.js,asm.js仅包含可以预判变量类型的数值运算,有效的避免了JavaScript弱类型变量语法带来的执行效能低下的顽疴。其中的核心魔法使 WebAssembly 和 C/C++ 采用相似的线性内存模型,提供为 JIT 提供了转化为相似代码的可能。

5.4 Rust 语言 —— 基于 LLVM 的输出 WebAssembly 能力

Rust 和 Emscripten 诞生于 Mozilla 公司,因此目前 WebAssembly 社区和 Rust 社区有着很大的重叠部分。很多 Rust 实现的 WebAssembly 虚拟机,同时 Rust 编译器借助 LLVM 的能力输出 WebAssembly 模块。可以说 Rust 技术的发展和抱住 WebAssembly 这个大腿有极大的关系。当然,因为 Rust 兼容 C/C++ 内存模型同时又无 GC 依赖,使得 Rust 可以构造出非常轻量高效的 WASM 模块。不过 Rust 本身的技术门槛也为初学者带来了极大的挑战。

5.5 Go 语言 —— 独立的 WebAssembly 后端

Go语言作为云计算等领域的主流语言,从Go1.11开始,WebAssembly开始作为一个标准平台被官方支持,这说明了Go语言官方团队也认可了WebAssembly平台的重要性和巨大潜力。目前Go语言社区已经有众多与WebAssembly相关的开源项目,比如有很多开源的WebAssembly虚拟机就是采用Go语言实现的。不过Go语言对WebAssembly被诟病的一个方面是官方生成的WASM文件不是wasi规范,同时因为GC等特性导致WASM体积比较大。

社区有个针对嵌入式环境等 TinyGo 变种,后端同样借助 LLVM 的能力输出 WebAssembly 模块。不过因为 LLVM 的依赖非常重,导致 TinyGo 的命令行将近 100MB、同时无法方便在浏览器环境使用。可以说 TinyGo 本身并不 Tiny,只是其目标平台是针对 Tiny 的单片机和 WASM 等平台。

5.6 凹语言 —— 为 WebAssembly 而生的国产语言

凹语言是为 WebAssembly 而设计的新语言,是国内 Gopher 发起的纯社区构建的开源国产编程语言项目。同时凹语言也是国内第一个实现纯浏览器内编译、执行全链路的自研静态类型的编译型通用编程语言。凹语言不仅仅点亮了 Arduino Nano 33 开发板,同时也通过实现了 BrainFuck 虚拟机证明了其图灵完备的能力,最近还验证了通过凹语言开发 Web 版本贪吃蛇的能力。

凹语言主页:https://wa-lang.org/

5.7 KCL —— 向 WebAssembly 迁移的领域语言

Kusion 配置语言(KCL)是由来自蚂蚁的徐鹏飞负责设计的、基于约束的记录及函数语言。作为领域语言,KCL 目前也是基于 LLVM 的能力输出 WebAssembly 模块。此外,KCL团队还在设计面向 Web3 领域的合约编程语言,也是天生就选择支持 WebAssembly 平台。

KCL 语言的主页:https://kcl-lang.io/

6. WASM的一些场景

6.1 Web 应用

随着 WebAssembly 的成熟,Web 应用不在是 JavaScript 的天下。比如之前就有国外大牛基于 WASM 技术将 Windows 2000 搬到了浏览器中。而像 AutoCAD 和 谷歌地球这些重量级的应用均通过 WebAssembly 支持了浏览器。

当然,不仅仅是重量级的 Web 应用,随着 WASM 原生编程语言的成熟,可以预期会有更多的其他语言开发的 Web 应用。比如,下面是采用凹语言开发的贪吃蛇小游戏就是基于 WebAssembly:

贪吃蛇游戏在线地址:https://wa-lang.org/wa/snake/

6.2 Web3 和元宇宙应用

随着 Web3 和元宇宙概念的兴起,WebAssembly 也将作为其中的关键技术,甚至是基石技术。目前 Web3 相关的区块链行业有大量的技术基于 WebAssembly 构建,甚至专门定制 EWASM 技术标准。而元宇宙作为数字化和现实完全融合的新社会生态,其底层的软件系统更是非常依赖纯开源软件和平台无关的通用技术,因此作者推测GPL开源协议和 WebAssembly 技术将会是元宇宙的两大关键支柱。

6.3 Serverless 应用

Serverless 强依赖高度优化的冷启动,Wasm非常适合作为下一代无服务器平台运行时。SecondState、Cloudflare、Netlify和Vercel等公司都支持通过其边缘运行时部署WebAssembly功能。

下图是 AWS Lambda 中的 WebAssembly Serverless 函数工作原理:

具体细节可以参考这个文章:https://www.cncf.io/blog/2021/08/25/webassembly-serverless-functions-in-aws-lambda/

6.4 插件系统应用

得益于 WASM 的跨平台的特性,很多系统和框架在考虑通过 WASM 开发插件系统。比如 基于 eBPF 和 Wasm 技术实现给 Linux 打动态的补丁。比如蚂蚁开源的MOSN(Modular Open Smart Network),是一款主要使用 Go 语言开发的云原生网络代理平台。MSON 就支持通过 WASM 插件来扩展其能力。下图是 MOSN 插件的工作原理图:

MOSN 插件的细节可参考:https://mosn.io/blog/posts/mosn-wasm-framework/

6.5 单片机 应用

Wasm 不仅仅应用在浏览器、云计算等行业,在边缘计算等嵌入式领域也有应用场景。比如 wasm3 虚拟机就针对 arduino 提供的更精简的虚拟机,用户可以通过 wasm 技术为不同的单片机开发应用。

比如可以通过凹语言结合 wasm3-arduino 来开发 arduino 的例子,下图是本地模拟环境代码和执行效果图:

wasm3-arduino 仓库:https://github.com/wasm3/wasm3-arduino

7. WASM 教程推荐

WebAssembly 属于这个新生态的根技术、而目前正是处于根技术生态的构建阶段。因此,这类推荐的更多是偏向WebAssembly 规范、原理和实现的教程。我们希望当 WebAssembly 技术正在普及之后,用户可以通过流行的编程语言直接开发 WebAssembly 应用而不需要关系根技术的细节。

7.1 《WebAssembly 规范》—— 2022

WebAssembly 规范 1.0 草案在 2018 年发布,现在最新的 WebAssembly 2.0 在 2022 年发布。WebAssembly 规范是市面上所有该技术的实现和实践的参与源头。任何希望追根溯源、获取最前沿的 WebAssembly 发展方向的同学不仅仅推荐精读该规范,甚至还建议跟踪规范的讨论和诞生的过程。

该文档并非正式出版的图书,目前规范只有在线电子版,建议自行打印。

7.2 《WebAssembly 标准入门》—— 2018

本书是本文作者和前同事于 2018 年合著,主要讲解了WebAssembly的基础知识,其内容涵盖了WASM的历史背景、WASM中汇编语言和虚拟机指令、浏览器对WASM的支持、其它高级语言对WASM的支持等。

本书适合想要掌握WebAssembly技术、构建对应虚拟机工具、编程语言或希望了解底层细节的用户学习。

7.3 《WebAssembly: The Definitive Guide》—— 2021

这是 Oreilly 出版的相对较新的 WebAssembly 专著,不仅仅覆盖了规范本身同时结合了主流编程语言的案例。

目前国内还没有中文版本,大家可以阅读英文版本。

7.4 《WebAssembly原理与核心技术》—— 2021

这是国内虚拟机实现专家张秀宏写的一本讲述如何实现 WebAssembly 虚拟机的专著。它不仅对WebAssembly的工作原理、核心技术和规范进行了全面的剖析和解读,而且给出了实现WebAssembly解释器和AOT编译器的思路和代码。

对于希望尝试自己实现 WebAssembly 的同学建议阅读本书。

8. 2023年展望

对于 WebAssembly 来说,2022年是真正润物细无声开始落地的过程:从新的2.0标准到Ruby、Python两大主流脚本语言开始官方支持,从SQLite3开始官方支持、从Docker开始官方支持等,到为其而生的凹语言等,到真正的商业应用都有巨大的发展(而完全不是因为某个大佬的项目黄了就断言WASM要凉的节奏)。在商业应用上,Figma 基于WebAssembly打造在浏览器中的高性能应用,后被 Adobe 以 200亿 美元收购,而Adobe也在项浏览器迁移。此外,WebAssembly 也是云厂商、边缘计算和Serverless 的候选人。

随着 WebAssembly 的普及,有一些相关技术流行趋势也日趋明朗化。作者做2个小小的趋势预测:首先是WasmEdge将成为浏览器外最流行的运行时;其次是JavaScript将成为WebAssembly平台上最流行的编程语言。不过这只是5年内的短期预测,更长的发展趋势还需要看 WebAssembly 生态其他的基础设施和编程语言发展状态。

尽管目前 WebAssembly 发展喜人,但百废待兴仍有许多工作要做。我们希望大家更多的是参与到 WebAssembly 建设中去,而不是仅仅作为围观者。作为凹语言作者我们希望在2023年真正解决语言的可用性和易用性的问题,让WebAssembly应用构建更加简单。WebAssembly 作为一个新兴的赛道,作为一个基础设施必将带来更大的生态洗牌,这是一个值得关注和投入的方向,让我们携手共建 WASM 原生时代。

2022 国产编程语言盘点

2023-01-08 08:00:00

到了 2022 年,回顾国产编程语言这个方向,我发现这已经不再是之前的一片信息荒漠,而是有很多可以八卦的事件。虽然目前还没有真正形成影响力的通用国产语言,但是这些早期的故事也是非常值得关注的。刚好还没有人写,所以我在元旦前后有了写这个文章的想法。

本文在 InfoQ 公众号头条首发,标题为:“中文编程不如英文香?今年诞生的这些国产编程语言表示不服”。

InfoQ 公众号头条地址:https://mp.weixin.qq.com/s/3WzDYdsKJfF2uQhCuYqiSg

注:公众号有将近100条评论,建议在微信中阅读。

“Go 2” 正式落地,中国 Gopher 踏上新征程!

2022-12-21 08:00:00

本文来自 CSDN 重磅策划的《2022 年技术年度盘点》栏目。2022 年,智能技术变革留下了深刻的脚印,各行各业数字化升级催生了更多新需求。过去一年,亦是机遇与挑战并存的一年。《2022 年技术年度盘点》将围绕编程语言、开源、云计算、人工智能、架构服务、数据库、芯片、开发工具等核心技术领域,特邀一线技术专家亲临分享自身的技术实践,借此希望能够为更多的行业从业者带来一些借鉴与思考,更好地把握技术的未来发展趋势。

在本篇文章中,来自 Go 开发社区的资深专家柴树杉老师将围绕 Go 语言的技术演进历程,分享对「Go 2」的最新见解,深度剖析 Go 语言在中国的生态发展与企业应用现状。

CSDN 公众号首发:https://mp.weixin.qq.com/s/_R_ktxbU0XvbrzFRotJbuw

凹语言开源季度总结-CSDN

2022-11-05 08:00:00

【CSDN 编者按】放眼各大编程语言排行榜,几乎很难看到国产编程语言身影,伴随着我国基础软硬件的发力与追赶,尤其是在操作系统、数据库等技术领域的累积,我们也渐渐看到一些国产编程语言的诞生,例如由一群 Go 语言爱好者发起的凹语言,2018 年筹备再到今年 7 月正式开源,其背后有着怎样的故事,开源 3 个月后,其又进行了哪些改进与提升呢?

凹语言最近刚刚发布 v0.3.0,而正式开源不知不觉已经过去一个季度,这是凹语言开源的第一个季度的非正式总结,也是对未来的计划和展望。

1. 凹语言简介

凹语言(凹读音“wā”)是 国内 Gopher 针对 WASM 平台设计的通用编程语言。凹语言作为WASM原生 的编程语言,天然对浏览器环境亲和,同时支持 Linux、macOS 和 Windows 等主流操作系统,此外通过 LLVM 后端对本地应用和单片机等环境提供支持。

下面是凹语言的Logo:

从形状上看,“凹”字形似 WASM 图标(方块上部缺个口);从读音上看,“凹”正好是 WASM 的前半部;从结构上看,实心的“凹”字约等于字母“C”逆时针旋转 90 度——C 可以理解为 C 语言,也可以理解为 Coder,那么“凹”也可以暗示躺平的的 C/躺平的 Coder……

2. 凹语言项目发起人

该项目的发起人柴树杉、丁尔男、史斌均是国内资深 Gopher。其中柴树杉是《Go 语言高级编程》等多本 Go 畅销书作者,目前在蚂蚁从事 KusionStack 和 KCL 语言的设计和开发工作。丁尔男是《WebAssembly 标准入门》等多本 WASM 专著作者,长年从事 3D 开发热衷于性能优化,目前在航天远景科技股份有限公司分管 3D 相关产品开发工作。史斌是编译器领域专家,曾为 Go 编译器提交过 127 个优化补丁,在 Go 全球贡献者排名中长期处于前 50 名,同时拥有 Go 与 LLVM 官方 Git 仓库的提交权限,同时也是《Go 语言定制指南》图书的作者。

在开发实践中,因为不同的原因,先后萌生了发展一门新语言的想法,Go 语言克制的风格是我们对于编程语言审美的最大公约数,因此选择它作为初始的蓝本。不必讳言:本项目启动时大量借鉴了 Go 的设计思想和具体实现——这是在有限投入下不得不作出的折衷,我们希望随着项目的发展,积累更多原创的设计,为自主创新的大潮贡献一点力量。

3. 设计哲学和开发计划

凹语言的整体设计,是围绕着“对开发人员友好”来进行的。字符串/切片作为基本类型、无需(也不能)手动管理内存、视觉上更显著的变量类型定义等均是这一核心思想的具体体现。

2022 年 7 月,凹语言正式开源,并公布了半年度的线路图:

随着项目的公开,有了更多的同学加入了凹语言开发组,讨论组社区也逐渐形成并保持活跃。感谢开发组的同学协同努力,第一季度的目标全部达成!

4. 取得了哪些进展

首先,经过多次讨论,开发组慎重决定凹语言采用 AGPLv3 开源协议,并制定了对应的了凹语言贡献协议。目前已经有外部同学通过新的流程贡献了代码。此外还取得了以下重大进展。

4.1 发布第三版网站

从 2018 年起,网站经过 3 次较大更新。最开始第一版的网站只有一个静态页面(2018),第二版是刚开源时基于 MDBoo k构建(2022年7月),最新版本于 2022 年 9 月开发到 10 月底正式上线。

该版本网站由子项目负责人扈梦明开发,他还是凹语言 VSCode 插件和 Playground 的负责人。

4.2 发布纯浏览器环境的的 Playground

Playground 是一套在线编译凹源代码并执行的环境。该环境的编译、执行没有调用后端服务,完全在页面中运行;是一个非常便捷的体验、测试凹语言的入口。

4.3 创建 VSCode/Fleet/Vim 等高亮插件

VSCode 插件提供了语法高亮、代码片段补全、补全建议等功能,支持纯 Web 环境安装:)

9 月 JetBrains Fleet 发布了预览版本,凹语言第一时间开发了高亮插件:

当然,传统的 Vim 插件也不能少:

更多和插件和功能完善希望社区同学参与共建。

4.4 WASM 后端原型如期发布

WASM 后端原型如期发布,已支持数值/字符串基本类型、结构体、方法、数组、切片等常用特性,项目组开始着手以此为基础开发贪吃蛇等带有交互功能的网页小游戏。

可以通过以下方式测试:

  1. go install github.com/wa-lang/wa@latest
  2. wa init -name=_examples/hi
  3. wa run _examples/hi

或者创建以 hello.wa 文件,包含下代码

fn main {
    println("你好,凹语言!")
    println(add(40, 2))
}

fn add(a: i32, b: i32) => i32 {
    return a+b
}

运行并输出结果:

$ go run main.go hello.wa 
你好,凹语言!
42

程序默认会基于WAT后端编译并执行,看到以上输出表示一切正常!

4.5 作为嵌入 Go 程序脚本

凹语言也可以作为 Go 语言包被导入,然后以脚本方式执行:

package main

import (
    "fmt"
    "github.com/wa-lang/wa/api"
)

func main() {
    output, err := api.RunCode("hello.wa", "fn main() { println(40+2) }")
    fmt.Print(string(output), err)
}

4.6 LLVM 后端提前启动

原定于 2023 年春节后启动的 LLVM 后端,提前启动。LLVM 后端的主战场在本地和嵌入式环境,下面是凹程序翻译到 LLVM-IR 的效果:

目前已经支持素数例子的执行:

更新路线图:

5. 展望

目前,凹语言是一个爱好者共建的业余项目,没有设置 KPI。一门新语言真正达到实用化,所需的工作量极其巨大,我们热切的期望更多有兴趣的同学能参与共建,尤其是承担子项目负责人的职责。

与普通贡献者相比,子项目负责人可以直接参与决策,在项目发展中获得上不封顶的话语权。由于尚处于起步阶段,可以单独成为子项目的模块遍地都是:一组堆管理函数、一个wat转二进制wasm的包、一种与其他语言交互的接口……

出名要趁早,参与开源同样需要趁早,欢迎参与共建。

SQLite3 官方支持 WebAssembly!

2022-10-31 08:00:00

SQLite 官方的 wasm 项目终于来了!这表示 WebAssembly 在 SQLite 社区完全进入工业级应用阶段!

1. WASM 是什么

WebAssembly,又名 WASM,是一种标准,它定义了一种低级编程语言,适合 (A) 作为与许多其他语言交叉编译的目标,以及 (B) 通过浏览器中的虚拟机运行。它在设计时考虑了通过 JavaScript 编写脚本,它提供了一种将 C 代码(以及其他代码)编译为 WASM 并通过 JavaScript 编写脚本的方法,尽管 JavaScript 和 C 之间还存在巨大的编程模型差异,但它为不同语言和 JS 的交互带来了标准桥梁。

根据 Ending 定律:“所有可以用WebAssembly实现的终将会用WebAssembly实现”。SQLite 官方支持 WASM 只是再次证明和强化了定律有效性。实际上,在很早之前网上就有很多基于 LLVM 或 Emscripten 构建的 SQLite 库,它们最终可以被包装为 JS 库。

扩展阅读:WASM 作为 W3C 的 第 4 个标准,已经在不同的领域取得巨大的进展。比如 Docker 发布集成 WebAssembly 的首个技术预览版。同时大量编程语言已经开始支持 WASM 平台(完整列表可参考 https://wasmlang.org/ ),国内的 Go+、凹语言、KCL 配置语言 等都把对 WASM 的支持作为较高的优先级。关于 WASM 的更多信息可以关注 《WebAssembly标准入门》。

2. SQLite 官方支持 WebAssembly

https://sqlite.org/wasm/doc/ckout/index.md

其实早在 2022 年 9 月,Google 的 Chrome 开发团队宣布与 SQLite 开发团队合作,并开发了 SQLite 的 WebAssembly 版本,作为替代的 Web SQL 数据库 API。WebAssembly 起源于 SQLite 开发团队的努力。

3. 在浏览器体验 SQLite

打开网址 https://sqlite.org/fiddle/

4. 项目的具体目标

根据官网介绍,主要有 4 个目标:

不在此列的特性:

简而言之,在提供底层 API 能力的同时,针对面向对象、多线程等环节提供简单易用的 API。

KCL 论文被 SETTA 2022 会议录用

2022-10-27 08:00:00

近日,由 KusionStack 团队成员撰写的关于 KCL 创新论文被 SETTA 2022 国际会议长文录用。

Symposium on Dependable Software Engineering(以下简称 SETTA)可靠软件工程研讨会旨在将国际研究人员聚集在一起,就缩小形式化方法与软件工程之间的差距交流研究成果和想法。例如,将形式化技术和工具应用于工程大型系统(如网络物理系统 (CPS)、物联网 (IoT)、企业系统、基于云的系统等)。

此次被录用的论文为《KCL: A Declarative Language for Large-scale Configuration and Policy Management》,该论文的核心创新点是提出了 KCL 声明式语言、开发机制以及一致的工作流程。通过语言的建模及约束能力,可以提升运维开发过程中的多团队协作生产力以及效率,同时确保大规模配置和策略管理的稳定性。

此外,SETTA 2022 将在北京时间 10 月 27 日至 10 月 28 日举办线上会议,届时会分享 KCL 论文详细内容,欢迎加入 KusionStack 社区 进行围观。SETTA 2022 会议议程详情请参考:https://lcs.ios.ac.cn/setta2022/program.php。

注:目前 KCL 已在 Github 开源,欢迎访问 https://github.com/KusionStack/KCLVM 获得更多信息。

Go 语言 CGO 用户深度定制 SQLite 代码

2022-10-26 08:00:00

本文是 BRUNO CALZA 记录的关于如何改变SQLite源代码,使记录行更新时可用于 Go 的更新钩子函数的过程。原文通过深度定制 C 语言的 API 函数达成目的,这几乎是所有 CGO 深度用户必然经历的过程(关于 CGO 的基本用法可以参考译者的《Go高级编程》第2章),是一个非常有借鉴意义的技术文章。

1. 背景

有一天,我正在考虑如何在 SQLite 中获取最近插入或更新的行记录的数据。这样做的动机是我想创建该行的 hash,本质上是为了在插入或更新行时能够构建相应表的 Merkle 树

SQLite 提供的最符合的 API 可能是 sqlite3_update_hook:

sqlite3_update_hook() 函数为数据库连接注册一个回调函数,该数据库连接由第一个参数标识,在 rowwid 表中更新、插入或删除行时调用。

这个 API 的问题是它只返回行的 rowid。这意味着还需要为列内的行获取所有列。即使使用这种方法,我仍然无法获得行记录的原始数据。只能得到那一行的驱动信息。

关于如何构建这样的树可能有很多方法,但就我而言 SQLite API 并没有提供真正想要的东西。因此,我决定趁此机会更深入地挖掘下源代码,同时看看内部实现的细节。不仅如此,我希望可以对它进行一些修改和测试,看看能否满足需求。

因为对 C 语言的畏惧,开始我只是想假装看下几个源文件就跑路。没想到这次真的有惊喜。

2. 看看 SQLite 的代码结构

首先使用 fossil 工具克隆了 SQLite源代码,下面是文件。

SQLite 代码目录

如果你对数据库比较熟悉,或许可以猜测出一些文件对应的操作。因此,我决定直接跳到 insert.c 文件,看看能不能找到一些有趣的东西。

遍历函数名列表,路过 sqlite3Insert 函数,看到以下注释:

** This routine is called to handle SQL of the following forms:
**
**    insert into TABLE (IDLIST) values(EXPRLIST),(EXPRLIST),...
**    insert into TABLE (IDLIST) select
**    insert into TABLE (IDLIST) default values
**

也许在这个函数中有一些可鼓捣的地方。我能够对其中发生的情况进行一些猜测,但引起我注意的是对名称类似于 sqlite3vdbeXXX 的函数的函数调用的数量。

这让我想起 SQLite 底层使用了一个名为 vdbe 的虚拟机。这意味着所有SQL语句都首先被翻译成该虚拟机的语言。然后,执行引擎执行虚拟机代码。让我们看一个简单的 INSERT 语句如何被翻译成字节码:

sqlite> create table a (a int, b text);
sqlite> explain INSERT INTO a VALUES (1, 'Hello');
addr  opcode         p1    p2    p3    p4             p5  comment      
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     8     0                    0   Start at 8
1     OpenWrite      0     2     0     2              0   root=2 iDb=0; a
2     Integer        1     2     0                    0   r[2]=1
3     String8        0     3     0     Hello          0   r[3]='Hello'
4     NewRowid       0     1     0                    0   r[1]=rowid
5     MakeRecord     2     2     4     DB             0   r[4]=mkrec(r[2..3])
6     Insert         0     4     1     a              57  intkey=r[1] data=r[4]
7     Halt           0     0     0                    0   
8     Transaction    0     1     1     0              1   usesStmtJournal=0
9     Goto           0     1     0                    0   

我得出的结论是 sqlite3Insert 实际上是根据SQLite插入规则,将解析后的 INSERT 语句转换为一系列虚拟机字节码指令。

因此这并不是我要找的地方。我真正需要的是在插入之前创建记录的位置。我猜测那只能是执行虚拟机代码的地方,可能是执行 Insert (OP_INSERT) 操作码的地方。

根据上图我直接找到了 vdbe.c 文件的位置,直奔主题。

我发现有一个有 8000行代码的 switch( pOp->opcode ) 语句,通过 OP_INSERT 关键字找到插入操作对应的代码位置。

在对应分支的第一行中,总算找到了相关的线索:

 Mem *pData;       /* MEM cell holding data for the record to be inserted */

所以 pData 指向要插入的记录数据。您可以在 L5402 中看到pData = &aMem[pOp->p2];,它是如何将 pData 值设置为虚拟机内存 aMem 地址的,该地址位于虚拟机寄存器 p2 所指向的位置。

快速回顾一下: 首先在 insert.c 文件我们了解到 INSERT 语句被翻译成一堆虚拟机指令。然后通过 INSERT 的数据通过这些sqlite3vdbeXXX 调用到达虚拟机。我假设将 OP_INSERT 操作码和数据注册到虚拟机是在第2593行:

sqlite3VdbeAddOp3(v, OP_Insert, iDataCur, aRegIdx[i], regNewData);

下面 regNewData 的一个更详细的说明:

** The regNewData parameter is the first register in a range that contains
** the data to be inserted or the data after the update.  There will be
** pTab->nCol+1 registers in this range.  The first register (the one
** that regNewData points to) will contain the new rowid, or NULL in the
** case of a WITHOUT ROWID table.  The second register in the range will
** contain the content of the first table column.  The third register will
** contain the content of the second table column.  And so forth.
**
** The regOldData parameter is similar to regNewData except that it contains
** the data prior to an UPDATE rather than afterwards.  regOldData is zero
** for an INSERT.  This routine can distinguish between UPDATE and INSERT by
** checking regOldData for zero.

所以,在这一点上,我们正在用数据执行机器代码。代码向下滚动一点,让我们看看如何使用 pData。在 L5448-L5449 处可以看到:

  x.pData = pData->z;
  x.nData = pData->n;

x 的定义如下:

 BtreePayload x;   /* Payload to be inserted */

完美。再向下滚动一点,我们看到:

  rc = sqlite3BtreeInsert(pC->uc.pCursor, &x,
      (pOp->p5 & (OPFLAG_APPEND|OPFLAG_SAVEPOSITION|OPFLAG_PREFORMAT)), 
      seekResult
  );

我们终于找到了插入原始数据的位置。但是,我们怎么知道它的格式和这里记录的一样呢? 如果仔细查看示例 INSERT 中的虚拟机代码,在INSERT 操作码之前有一个 MakeRecord 操作码,它负责构建记录。

你可以在 vdb.c 文件中查看 OP_MakeRecord 实现,并看到以下注释:

You can check the OP_MakeRecord implementation at vdbe.c file and see the following comment:

P1 开头的 P2 寄存器转换为记录格式,用作数据库表中的数据记录或索引中的键。

case 语句的最后几行看到了关键部分:

  /* Invoke the update-hook if required. */
  if( rc ) goto abort_due_to_error;
  if( pTab ){
    assert( db->xUpdateCallback!=0 );
    assert( pTab->aCol!=0 );
    db->xUpdateCallback(db->pUpdateArg,
           (pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT,
           zDb, pTab->zName, x.nKey);
  }
  break;

看来我需要的东西都在这里了。更新钩子钩子和原始数据。只需要更新时传递给回调函数即可。

3. 开始定制 SQLite

这就是我期望的 API:

db->xUpdateCallback(db->pUpdateArg,
	(pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT,
	zDb, pTab->zName, x.nKey, pData->z, pData->n);

传递的是数据(pData->z)和其大小(pData->n)。

为了解释函数签名的变化,还需要在多个地方进行相应的修改。

以下是 fossil 工具提示的变化的源文件:

EDITED     src/main.c
EDITED     src/sqlite.h.in
EDITED     src/sqlite3ext.h
EDITED     src/sqliteInt.h
EDITED     src/tclsqlite.c
EDITED     src/vdbe.c

还有一些针对编译提示的修改。

4. 克隆一份 Go SQLite 驱动

现在是时候在一个 Go 程序中创建一个简单的测试了。我比较熟悉与 SQLite 交互的 mattn/go-sqlite3 驱动程序。该项目通过导入SQLite合并文件并通过CGO绑定工作。

因此还需要再克隆下 Go SQLite 驱动,更新被我修改的文件。并在Go API中进行了必要的更新以访问新值。

主要是对 updateHookTrampoline 的更改,现在接收记录为 *C.Charint 类型的数据大小,转型为字节 Slice 并将其传递给回调函数:

func updateHookTrampoline(handle unsafe.Pointer, op int, db *C.char, table *C.char, rowid int64, data *C.char, size int) {
	callback := lookupHandle(handle).(func(int, string, string, int64, []byte))
	callback(op, C.GoString(db), C.GoString(table), rowid, C.GoBytes(unsafe.Pointer(data), C.int(size)))
}

RegisterUpdateHook 函数也需要做同样的调整。

5. 改动后的效果

现在已经准备好了测试的所有东西。让我们运行一个简单的例子,灵感来自 SQLite Internals: Pages & B-trees 博客文章。

package main

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	"github.com/mattn/go-sqlite3"
)

func main() {
	sqlite3conn := []*sqlite3.SQLiteConn{}
	sql.Register("sqlite3_with_hook_example",
		&sqlite3.SQLiteDriver{
			ConnectHook: func(conn *sqlite3.SQLiteConn) error {
				sqlite3conn = append(sqlite3conn, conn)
				conn.RegisterUpdateHook(func(op int, db string, table string, rowid int64, data []byte) {
					switch op {
					case sqlite3.SQLITE_INSERT:
						fmt.Printf("%x\n", data)
					}
				})
				return nil
			},
		})
	os.Remove("./foo.db")

	srcDb, err := sql.Open("sqlite3_with_hook_example", "./foo.db")
	if err != nil {
		log.Fatal(err)
	}
	defer srcDb.Close()
	srcDb.Ping()

	_, err = srcDb.Exec(`CREATE TABLE sandwiches (
		id INTEGER PRIMARY KEY,
		name TEXT,
		length REAL,
		count INTEGER
	);`)
	if err != nil {
		log.Fatal(err)
	}
	_, err = srcDb.Exec("INSERT INTO sandwiches (name, length, count) VALUES ('Italian', 7.5, 2);")
	if err != nil {
		log.Fatal(err)
	}
}

不要忘记添加更新 go.mod 文件 replace github.com/mattn/go-sqlite3 => github.com/brunocalza/go-sqlite3 v0.0.0-20220926005737-36475033d841,重新定向驱动。

运行后应该得到以下的结果:

05001b07014974616c69616e401e00000000000002

这正是 ('Italian', 7.5, 2) 数据的 Efficient Sandwich 编码的结果,不包含主键和记录的长度(前两个字节)。

看到输出结果我才发现能够理解SQLite源代码的部分内容真的很有趣,尽管我不理解它的大部分。但是我做了一些更改并看到这些更改,并通过 Go 的驱动程序看到结果的变化。

老实说这种更改数据库源代码的方法风险太大。与新版本保持同步也是一个太大的问题,但这是一个值得记录的有趣经历。

1024・GLCC 开源夏令营 KusionStack 顺利结题

2022-10-24 08:00:00

1. 1024 程序员节快乐

10 月 24 日是程序员节,首先祝所有程序员节日快乐。同时祝贺 GLCC 开源夏令营选择 KusionStack 项目的同学们全部完成考核,祝贺你们!

2. GLCC 编程夏令营活动

CCF GitLink 开源编程夏令营(GitLink Code Camp,简称 GLCC),是在 CCF 中国计算机学会指导下,由 GitLink 社区联合 CCF 开源发展委员会(CCF ODC)共同举办的面向全国高校学生的暑期开源项目实习计划。

活动主要联合各大开源企业、开源基金会、开源社区及开源领域专家,旨在鼓励高校学生通过参与真实的开源软件开发,感受开源文化,提升自身技术能力,进而达到为开源企业和社区输送优秀人才的目的。

3. KusionStack 开源项目

KusionStack 是蚂蚁开源的云原生可编程技术栈!它也是一个可编程、高灵活性的应用交付及运维技术栈,灵感源于融合(Fusion)一词,旨在帮助企业构建的应用运维配置管理平面及 DevOps 生态。

主要希望融合以下几种场景:融合专有云,混合云,多云混合场景;融合以云原生技术为主,同时采用多种平台技术的混合平台技术选型;融合多项目、多团队、多角色、多租户、多环境的企业级诉求。

基于 Platform as Code (平台服务即代码)理念,研发者可以快速收敛围绕应用运维生命周期的全量配置定义,面向混合技术体系及云环境,完成从应用运维研发到上线的端到端工作流程,真正做到一处编写,随处交付。

以上是 KusionStack 的主题架构。GLCC 的选题主要集中在 KCL 语言和对应的 VS Code 插件部分。

4. KusionStack 的 GLCC 编程任务

KusionStack 向 GLCC 提交了 3 个编程任务(都被选中)。第一个是 KCL 语言语法解析错误恢复机制改进,目前的 KCL 语言仅收集了语法解析阶段的错误,没有进行错误恢复。本项目目标是实现 KCL 语言语法解析阶段的错误恢复。第二个是为 KCL 设计包管理工具,希望同学能够提出设计方案并给出原形工具的实现。最后是为 KCL 的 VS Code 插件实现基于代码索引实现 KCL 代码 Find References 功能。

以上三个任务均是从生产使用角度提炼的真实的需求,虽然是相对独立的功能或模块,但是对于在校的同学依然会有不小的挑战。首先 KusionStack 作为一个开源的项目有一些参与的流程和规范,希望同学们通过参与真实的开源项目了解开源社区的文化和开发的一些习惯。其次,这几个任务在不同方向均兼顾了理论和实际的需求,不仅仅便于参与也可以作为长期的一个兴趣专研方向。同时我们也希望即使在 GLCC 结束,同学们能够在响应的方向上专研一段时间,在响应的方向做更多的探索。

5. KusionStack 任务考核全部通过

5.1 KCL 语言语法解析错误恢复

答辩视频:https://edut7hfmib.feishu.cn/file/boxcnZUz6CONQtpU7XV4KpqacYc

5.2 KCL 语言包管理工具

答辩视频:https://www.bilibili.com/video/BV18V4y1V7vM/?vd_source=1c9609588c8606302c89ca9a30cf168a

5.3 基于代码索引实现 KCL 代码 Find References 功能

答辩视频:https://www.bilibili.com/video/BV1nV4y1V7gF/?vd_source=1c9609588c8606302c89ca9a30cf168a

完整的技术方案和代码细节讲在后面的文章中单独分享,欢迎大家关注。

6. 对开源的展望

开源已经成为整个社会都在讨论的话题,作为开源的参与者程序员对开源自然非常熟悉。开源最让不同的参与方同学期待的地方是有无限的可能性。通过合作的模式、通过开源社区、GitLink 和 GLCC 夏令营这个桥梁,在校的同学们可以近距离参与一线公司的真实项目,同时在公司的开发人员也可以通过和学生的交流获得不同的反馈。开源是一个多赢的协作,我们期待以后会有更多的开源社区和组织能够参与进来,同时也希望这一届的同学能够通过开源社区这种形式参与、影响和帮助后续的学弟学妹们。最后感谢大家对 KusionStack 项目的关注和支持,谢谢大家!

WebAssembly将引领下一代计算范式[翻译]

2022-10-23 08:00:00

WebAssembly 是一种新兴的网页虚拟机标准,它的设计目标包括:高可移植性、高安全性、高效率(包括载入效率和运行效率)、尽可能小的程序体积。2018 年 WebAssembly 第一个规范草案诞生,2019 年成为 W3C 第四个标准语言。到了 2022 年底,WebAssembly 现在怎么样了…


WebAssembly(简称Wasm)是一个自诞生之日起就充满潜力的技术,从 “JavaScript杀手 “到 “云计算的下一个前沿方向”几乎覆盖了全部新兴领域。同时在从云计算项边缘计算渗透,Wasm已经远远超出了起作为第四种Web标准语言的角色。甚至重新定义了应用软件的开发模式,正逐渐接近其“一次编写,随处运行”的愿景。

在Wasm从诞生到现在的几年间,我们见证了从最开始的Wasm应用演示到为数十亿的头部技术产品提供基础设施支持。在同整个Wasm社区交谈过程中,我们也发现虽然很多人很看好Wasm未来发展前景,但是也存在争议和讨论。

不过在Sapphire,依然对围绕Wasm的快速发展和Wasm开始为更广泛的计算世界带来的新的可能性感到非常兴奋。在本文中我们将探讨什么是Wasm,为什么它很重要、今天它是如何被使用的、以及对这个生态系统的繁荣有什么期待。我们不会详细展开讨论Wasm的历史,但如果你对这些感到好奇可以看看Lin Clark 的精彩系列文章。

一、什么是Wasm?

WebAssembly正在沿着其名字中Web和Assembly两个领域之外的方向发展,因此这是一个极其有误导性的名字。

  1. 首先它不完全是汇编语言。Wasm是一种类似汇编字节码的指令格式标准,它更像LLVM-IR那种比汇编语言更高一些抽象的中间语言(比如其中函数的参数和返回值定义更像高级语言)。开发人员也不需要完全手写Wasm;相反人们一般选择使用其他高级语言(如C、C++、Rust、Go、凹语言等)将他们的代码编译为Wasm。
  2. 另外它不再只是Web网络。虽然Wasm最初被设计为Web浏览器的编译目标,但它的影响并没有停止。今天,使用与Wasm兼容的运行时,Wasm文件可以在客户端和服务器端执行,将使用范围扩大到浏览器之外——稍后将进一步探讨这些例子。

二、为什么Wasm很重要?

Wasm有几个关键的设计目标使其出生开始就自带令人亮眼的关注:

2.1 首先Wasm是可移植的

虽然Wasm最初是为Web设计的,而且今天所有主要的浏览器都提供对Wasm的支持。同时它也被设计为针对低级虚拟机架构,其指令由物理机单独翻译成机器代码。这意味着Wasm二进制文件最终可以在各种操作系统和芯片架构上运行——无论是在运行X86笔记本电脑的浏览器中,还是在内部或云端的服务器上,在移动设备、物联网设备上等等。

2.2 其次Wasm是多语言之下的一个标准

因为Wasm是一个编译目标,用于编程模块的具体语言并不重要,重要的是是否有支持将该语言编译到Wasm。开发人员可以灵活地使用多种语言(如C、C++、Rust、凹语言等)来构建二进制文件,并享受Wasm带来的复利。这意味着不需要考虑诸多组件和库链接等狗屁问题,只要他们都被编译到Wasm可以用于支持一个单一的应用。

2.3 最后Wasm是轻量和高效的

作为一个低级别的二进制指令格式,只需要较少的操作来将Wasm翻译成优化的机器代码。例如比如和Javascript进行比较(感兴趣的话可以参考 Lin Clark 的一些分析文章)。Javascript作为解释型语言,必须在运行时用即时编译(JIT)进行编译,并且必须经过获取/解析/编译/优化,最后才能执行和垃圾回收等步骤。

虽然JavaScript也可以被解析并转换为字节码,但Wasm已经是原生的字节码。另外Wasm也是静态类型的,这使得大多数优化在其初始编译时就已完成。最后JavaScript是动态类型的,需要在运行时进行优化和再优化,这共同导致了较难预测的性能。

这些优势也体现在浏览器之外,特别是Wasm模块的大小对于冷启动有极大的优势。目前,Serverless 的一个有问题是冷启动缓慢。虽然Serverless为开发者节省了管理后台基础设施和资源分配的时间,但如果该功能在冷态下被调用,就必须启动新的资源从而带来执行时间增加的额外成本。因为Wasm模块是非常轻量级的,和库调用类似方式使得启动时间可以大大减少(低至毫秒)。

2.4 Wasm是默认安全的

Wasm 目标之一是安全,它在一个沙盒环境中执行,对主机运行时没有初始可见性。这意味着对系统资源(如文件系统,硬件等)的访问是受限制的,除非明确导入了对应的函数以支持。因此Wasm极大限制了攻击面,实现了多租户环境中不受信任的代码安全受限地执行。这种安全模式是一个关键的促成因素,允许开发人员使用插件和用户提交的代码来扩展现有的应用程序,我们将在下面进一步探讨这一使用情况。 

三、Wasm现在是如何使用的?

3.1 客户端使用案例

3.1.1 浏览器中的多语言支持

开发客户端的流行语言不多,大部分都是Javascript构建的。应用程序的语言在历史上是有限的,今天大多数现代网络应用程序是用Javascript构建的。而浏览器和前端框架对Wasm的支持已经开始打开闸门,使开发者更容易在浏览器中编译和执行其他流行语言。现在开发者可以选择在浏览器中直接运行C、C++、Rust、和Go等语言。此外,像Zig这样的新兴系统语言已经为Wasm增加了很好的支持。而其他专门从Wasm设计的语言也已经出现,包括AssemblyScript、Grain、Motoko和凹语言等。

3.1.2 高性能的网络应用

已经有一些公司使用Wasm来显著提高他们的网络应用程序的性能。例如,Figma(刚刚被Adobe以200亿美元收购),一个基于浏览器的协作界面设计工具,使用C++构建其渲染引擎,最初将其代码交叉编译到称为asm.js的Javascript子集。在之前因为面临Javascript固有的优化限制,在改用Wasm后Figma的加载时间快了3倍,无论正在加载的文档大小如何。

其他价值数十亿美元的公司也已经在产品采用了Wasm。比如Adobe的Photoshop、Autodesk的AutoCAD。重新利用现有的代码库,用Wasm将整个桌面应用移植到网络上已经是真实发生的事情。其他有趣的例子包括移植成熟的视频游戏和项目,如完全在浏览器中运行的Doom3和Angrybots,Unity明确地将其WebGL构建的编译目标转换为Wasm。

除了移植已有的应用,我们还看到一些公司利用Wasm建立新的功能,这些功能在以前会受到性能限制的制约。 一些例子包括Runway,这是一个下一代内容创作套件,使用Wasm来支持其视频编解码器和媒体操作,以及StackBlitz,使用Wasm来支持纯Web的IDE开发环境,这比以前伪在线IDE外挂一个远程服务器拖油瓶的方式有着更好的安全性和性能优势。

3.1.3 浏览器内的数据库和分析

我们已经开始看到数据库的出现,它们利用Wasm的执行性能,使以前的服务器端分析工作负载更接近数据的存在。这里的例子包括DuckDB-Wasm,它使用Wasm为浏览器的中分析SQL数据库提供动力。以及SQL.js,它允许开发人员完全在浏览器中创建和查询SQLite数据库。

3.2 WASI:突破浏览器的桎梏

鉴于Wasm模块在默认情况下不能访问被明确授权的功能,纯WASM其实只能实现一些纯运算的功能。在上面的例子中,浏览器本身代表Wasm模块对系统资源的访问控制界面(例如,文件系统、I/O、时钟、全局变量等)。然而当我们在浏览器之外使用Wasm时需要什么呢? 

在实践中,运行时实如何提供对系统资源的访问方面有很大的不同。这就是WebAssembly系统接口(WASI)出现的地方。WASI是W3C的一个项目,是一个供应商中立的、模块化的标准化API集合,正如其名称所示,它作为Wasm模块和操作系统之间的接口,促进与主机运行时的通信,并以一致的方式使用选定的系统资源。

当然,WASI是扩大Wasm可能的范围的关键促成因素之一,包括像下面即将提到的服务器端应用程序。

3.3 服务器端场景

虽然已经有很多例子证明了Wasm在客户端的优势和价值,但我们对Wasm在服务器端的想象空间更加兴奋。Wasm的每一个设计原则(速度、安全和可移植性)都能使下一波服务器端的工作负载成为可能。

3.3.1 Serverless计算

Serverless强依赖高度优化的冷启动,Wasm运行时(如WasmEdge)非常适合为下一代无服务器平台提供动力。SecondState、Cloudflare、Netlify和Vercel等公司都支持通过其边缘运行时部署WebAssembly功能。其他公司如Grafbase正在使用Wasm,使开发者能够在边缘用他们选择的语言编写和部署GraphQL解析器。同时,Fermyon提供了一个类似于FaaS的自助式开发平台,用Wasm合成和运行基于云的应用程序。

3.3.2 边缘的数据分析和机器学习

Wasm的效率和可移植性使其独特地适合于支持边缘的机器学习工作负载,部署在外形和计算能力差异很大的设备上。我们相信,实时ML用例将推动计算越来越接近数据产生的地方,无论是运行在网络边缘(如CDN)还是设备边缘(如IoT)。Wasi-nn(神经网络)是一个API规格,旨在将服务器端的Wasm程序与运行在主机上的流行ML框架(如Tensorflow、PyTorch、OpenVINO)连接起来。 今天利用Wasm的ML场景的公司包括Edge Impulse和Hammer of the Gods,前者提供一个低代码开发平台,将TinyML模型设计和部署到Wasm模块中,在嵌入式设备上运行;后者使开发者能够创建超便携容器,通过其开源项目Rune,使用Rust和Wasm在边缘运行ML的工作负载。

3.3.3 插件和扩展

Wasm的多语言支持和沙盒隔离技术使其成为产品的有力的候选技术,产品开发者希望在现有的应用程序上提供一个可扩展的模型和执行第三方(可信或不可信)代码的能力。例如,Shopify在其Shopify Scripts框架背后使用了WebAssembly,为商家提供了以更有效的方式定制客户体验中对性能敏感的方面(如购物车、结账)的能力。 Suborbital提供了一个扩展引擎,使SaaS供应商能够安全、独立地运行 “终端用户开发者 “提供的代码。 Dynaboard使开发者能够在其低代码网络应用程序开发平台之上运行用户提供的代码,包括客户端和服务器端。

SingleStore和ScyllaDB已经开始利用Wasm,用用户定义的函数(UDF)引擎来扩展他们的数据库,允许开发人员重新使用现有的库,并将计算转移到数据库本身(例如,规避了将数据导出到另一个应用程序进行处理的限制)。

同时,RedPanda和Infinyon允许用户使用Wasm对流媒体数据进行实时内联转换。  代理服务器领域也开始接受Wasm,Tetrate(Sapphire Portfolio公司)等服务网格供应商正在开发func-e等工具,以帮助团队快速构建Wasm模块,扩展开源的Envoy。

Profian是Enarx的监护人,这是一个开源项目,使用Wasmtime运行时间在可信执行环境(TEE)内执行Wasm二进制文件——这是Wasm的硬件可移植性的另一个场景。虽然Wasm的安全模型保护主机免受不受信任的代码影响,但Profian将这一好处翻转过来,在TEE内使用Wasm二进制文件,以保护应用程序免受不受信任的主机影响。这使企业能够将其敏感的应用程序和数据部署到云端或内部,并获得加密保证。

3.3.4 Web3应用开发和智能合约

Wasm天然适合以加密为中心的场景:首先Wasm的可移植性使运行不同硬件集的节点网络具有可靠性;其次Wasm的性能在这些网络中转化为更广泛的效率。

Ewasm是一个关键的例子,并被视为以太坊第二阶段升级的一个关键组成部分。Ewasm将取代以太坊虚拟机(EVM),EVM虚拟机目前为交易提供动力并维护以太坊的网络状态,但没有针对不同的硬件平台进行优化,因此效率不高。Ewasm(连同分片和转向股权证明)代表了改善整体网络性能,并提供了一个更具扩展性的底层,为希望建立去中心化应用程序的开发人员扩大了对Solidity以外的语言支持。

Wasm也被用来支持其他网络和可互操作区块链的计算。例如,Parity是Substrate的开发者,这是一个开源框架,使用Wasm作为Polkadot生态系统的骨干。同时,CosmWasm是一个为Cosmos生态系统建立的多链智能合约平台,运行Wasm字节码。 Fluence实验室提供Marine,这是一个通用的Wasm运行时,与他们的专用编程语言Aqua相结合,使分散的应用程序和协议能够在他们的点对点网络上运行。目前利用Wasm的其他协议包括NEAR、Dfinity、EOS等。

四、Wasm的应用和基础大图

我们已经列出了一些公司和组织,他们主要分为两类:一类是使用Wasm来支持他们自己的产品和平台;另一类是提供所需的基础工具和基础设施,使开发人员能够自己建立Wasm。

五、最后总结

5.1 Wasm的未来

虽然我们看到许多初创企业和科技巨头采用Wasm技术,但生态系统的一些关键技术只是在最近才逐渐发展起来。今天,Wasm带来增量效益往往被使用一个低级技术和一个不成熟的工具链所带来的额外成本所抵销。

我们认为在推动Wasm的未来应用中有四个方面是最重要的。

5.2 Wasm会取代容器吗?

随着时间的推移,我们相信Wasm运行时将作为containerd、microVMs(Firecracker)和其他流行的容器结构的合法替代品 - 特别是随着WASI等标准的进一步扩展。这并不是说Wasm将全盘取代容器;在可预见的未来,它们将并肩存在,而利用每种容器的决定是由特定工作负载的特点所驱动的。

与传统的基于管理程序的虚拟机相比,Docker风格的容器提供了显著的改进,而Wasm已经能够将这些相同的效率提高到 “下一个水平”。凭借其亚毫秒级的冷启动,Wasm容器非常适合寿命较短的无服务器和边缘工作负载(除了现有的客户端用例之外)。同时,传统的Docker式工作负载非常适用于需要大量I/O或需要访问网络套接字的长期运行的服务(如缓存服务器)。

我们渴望看到像Kubernetes这样的协调引擎如何随着时间的推移与Wasm进行整合。尽管还很早,但像Krustlet(kubelet代理的替代品)、runwasi、Containerd Wasm Shims和crun的Wasm处理程序等项目和扩展都旨在将Wasm提升为容器环境中的一等公民,将其作为一个新的运行时类,可以由K8s进行相应的调度和管理。

5.3 谁会赢?

云厂商和Serverless是这里的明显候选人。但在Sapphire依然期望保持对Wasm生态每一个新兴技术保持近距离的关注依然。

随着任何新兴技术的出现,需要使其能够被主流采用。我们已经开始看到了许多真实的案例。然而,尽管今天正在推进的标准和正在开发的框架和运行时为实现Wasm的潜力奠定了基础,但百废待兴仍有许多工作要做。从以Wasm为中心的应用开发到开发人员生产力工具,到监控和安全解决方案,我们很高兴支持那些建立基础设施和工具的人,为每个人释放Wasm的优势,从个人开发者到全球企业。

如果你正在为Wasm生态系统做出贡献,或者正在使用Wasm为你的基础设施提供动力,请联系 [email protected], [email protected][email protected] - 我们很乐意听到你的意见!

特别感谢Michael Yuan、Matt Butcher、Liam Randall、Connor Hicks和Alexander Gallego的宝贵观点和反馈。

KCL:蚂蚁自研的配置策略语言

2021-08-10 08:00:00

楔子: 以蚂蚁集团典型的建站场景为例,在接入 Kusion 后,用户侧配置代码减少到 5.5%,用户面对的 4 个平台通过接入统一代码库而消减,在无其他异常的情况下交付时间从 2 天下降到 2 小时……

注:本文是柴树杉在 2021 GIAC 大会上分享的内容。蚂蚁杭州云原生和蚂蚁链正在招聘DSL语言设计和开发,欢迎推荐。

0. 你好 GIAC

大家好,我是来自蚂蚁集团的同学,很高兴能在 GIAC 的编程语言新范式板块和大家分享《KCL 配置策略语言》。KCL 语言是蚂蚁内部的 Kusion 解决方案中针对云原生基础设施配置代码化自研的 DSL 语言,目前已经在建站场景等一些场景开始小范围推广试用。

我们先看一下简单的 KCL 代码:

schema GIACInvitation[name: str]:
  Name:     str = name
  Topic:    str = "分享主题"
  Company?: str = None
  Type:     str = "分享嘉宾"
  Address:  str = "深圳"

invitation = GIACInvitation("姓名") {
  Topic:   "KCL 配置策略语言"
  Company: "蚂蚁集团"
}

这个例子代码先通过 schema 定义了一个 GIACInvitation 结构体:该结构体有一个 str 类型的 Name 参数,同时还有一组标注了类型和默认值的属性。然后通过声明式的语法构造了 GIACInvitation 的实例 invitation。

这个例子虽然简单,但是包含了 KCL 最重要的 schema 语言结构。从例子可以看出 KCL 尝试通过声明式的语法、静态类型检查特性来改进配置代码的编写和维护工作。这也是设计 KCL 语言的初衷,我们希望通过编程领域成熟的技术理论来解决云原生领域的配置代码化的问题。

1. KCL 语言的诞生背景

在经典的 Linux/UNIX 操作系统中,我们通过 Shell 和系统内置的各种工具和内核进行交互,同时通过 Shell 脚本来管理更上层的 App。可以说 Shell 语言极大地简化了内核的编程界面,不仅仅提升了操作系统易用性也简化了上层 App 的管理和运维,也提高了生产效率。而 Kubernetes 作为容器管理领域的事实标准,已经成为云计算时代的 Linux/UNIX。类比 UNIX 系统,Kubernetes 目前还缺少一种符合其声明式、开放、共享设计理念的交互语言及工具。

1.1 为何要设计 KCL 语言?

K8s 已经成为云计算的操作系统,但是目前尚缺少功能完备的 Shell 交互界面。目前虽然有很多而且开源方案,但是还没有像 UNIX 的 Shell 那种出现比较成熟的方案,特别是尚无法满足头部互联网企业大规模工程化的要求。云原生技术与企业落地之间存在 Gap 需要填补,这正是云原生工程化要解决的问题,也是设计 KCL 语言的出发点。

1.2 目前是一个好时机

云原生的思路是高度的开放化和民主化,结果就是万物可配置,一切配置都是代码。在配置代码面前人人平等,每个用户都可以通过调整配置代码和基础平台设施进行交互。因此对配置的编写和维护正在成为云计算时代软件工程师的必备的技能和需求。基于对云原生配置代码化需求的日益旺盛,硅谷的诸多头部公司已经对这个方向进行了大规模的实践和验证,这些都给了我们大量可以参考的经验。

因此蚂蚁的 Kusion 项目尝试通过 KCL 配置策略语言正是为了简化云原生技术设施的接入方式设计,其设计目标不仅仅是为了提升蚂蚁基础设施的开放程度及使用效率,同时希望能够优化共享、协同的开发流程,可以说其定位正是云原生时代的 Shell 语言。虽然目前还处于探索和实践阶段,我们通过此文和大家分享下 KCL 语言的设计与实现的一些理念,为云原生的快速到来贡献一点绵薄之力。

1.3 KCL 诞生历史

KCL 语言从 2019 年开始初期的调研和设计工作。到 2020 年 3 月发布 kcl-0.1,基于 Python 定制语法,采用 Go 版本的 Grumpy 和 AntLR 等工具开发。2020 年下半年改用 Python 语言并加快了开发和迭代速度,发布的 kcl-0.2.x 引入了大量语言特性、增加了 Plugin 扩展支持、同时支持 IDEA 插件。2021 年上半年开始统一优化和整合语言特性,发布的 kcl-0.3 优化类型系统、集成单元测试工具、优化执行性能并提供了 Go 等多语言的 API 支持、同时通过 LSP 为 VSCode 提供支持。2021 年下半年开始在建站等常见落地,同时引入静态类型检查和优化性能,完善语言的文档支持。

2. KCL 语言的设计原则

基于蚂蚁践行多年的经典运维中台沉淀的经验和对各种问题利弊的思考,Kusion 项目对如何充分利用云原生技术带来的红利,打造一个开放、透明、声明式、可协同的运维体系进行了探索和思考,提出并实践了基于基础设施代码化的云原生协同开发的模型。而 KCL 语言正是 Kusion 项目为了解决云原生协同开发而设计的声明式的配置编程语言,简单、稳定、高效和工程化是 KCL 语言设计的设计理念。

2.1 简单为王

简单不仅仅可以降低学习和沟通的成本,而且可以减少代码出问题的风险。不论是 UNIX 奉行的 KISS 原则还是 Go 语言推崇的 Less is more 设计理念,简化易用的界面始终是各种成功产品追求的一个目标。同样从简单原则出发,KCL 语言在参考现代编程语言之上只保留了必要的元素,同时通过类型自动推导、引入受限的控制流和 schema 提供了基础灵活的配置定义编写能力,删减语言特性始终是 KCL 语言设计工作的一个重要目标。

2.1.1 声明式语法

声明式编程是和命令式编程并列的一种编程范式,声明式编程只告诉你想要的结果,执行引擎负责执行的过程。声明式编程使用更加简单,可以降低命令式拼装造成的复杂性和副作用,保持配置代码清晰可读,而复杂的执行逻辑已经由 Kubernetes 系统提供支持。

KCL 语言通过简化对 schema 结构体实例化的语法结构对声明式语法提供支持,通过仅提供少量的语句来减少命令过程式编程带来的复杂性。围绕 schema 和配置相关的语法,KCL 希望每种配置需求尽可能通过固定的写法完成,使得配置代码尽可能的统一化。

比如作为 KCL 声明式语法的核心结构 schema 可以采用声明式方式实例化:

schema Name:
    firstName: str
    lastName: str

schema Person:
    name: Name = {
        firstName: "John"
        lastName: "default"
    }

JohnDoe = Person {
    name.lastName: "Doe"
}

首先通过 schema 定义了一个 Name 结构,结构包含 2 个字符串类型的必填属性。

然后在 Person 中复用 Name 类型声明一个 Name 属性,并且给 Name 属性设置了默认值以简化用户使用。

最终在定义 JohnDoe 配置定义的时候,只需填写 Name.lastName 一个属性参数即可,其他部分属性均采用默认的参数。

对于一些标准的业务应用,通过将可复用的模型封装为 KCL schema,这样可以为前端用户提供最简单的配置界面。比如基于蚂蚁内部 Konfig 大库中 sofa.SofaAppConfiguration 只需添加少量的配置参数就可以定制一个 App。

appConfiguration = sofa.SofaAppConfiguration {
    resource: resource.Resource {
        cpu: "4"
        memory: "8Gi"
        disk: "50Gi"
    }
    overQuota: True
}

通过声明式语法描述必要的参数(其他的参数全部采用默认配置),可以极大简化普通用户的配置代码。

2.1.2 顺序无关语法

有别于命令式编程,KCL 推崇的是更适合于配置定义的声明式语法。以斐波那契数列为例,可以把一组声明式的定义看作一个方程组,方程式的编写顺序本质上不影响方程组的求解,而计算属性依赖并“求解”的过程由 KCL 解释器完成,这样可以避免大量命令式拼装过程及顺序判断代码。

schema Fib:
    n1: int = n - 1
    n2: int = n1 - 1
    n: int
    value: int

    if n <= 1:
        value = 1
    elif n == 2:
        value = 1
    else:
        value = (Fib {n: n1}).value + (Fib {n: n2}).value

fib8 = (Fib {n: 8}).value  # 21

代码中 Fib 定义的成员 n、n1 和 n2 有一定的依赖关系,但是和它们书写的顺序并无关系。KCL 语言引擎会根据声明式代码中的依赖关系自动计算出正确的执行顺序,同时对类似循环引用等异常状态告警。

2.1.3 同名配置合并

当整个业务和开发维护团队都变得复杂时,配置代码的编写和维护也将变得复杂化:同一份配置参数可能散落在多个团队的多个模块中,同时一个完整的应用配置则需要合并这些散落在不同地方的相同和不同配置参数才可以生效,而相同的配置参数可能因为不同团队的修改而产生冲突。通过人工方式同步这些同名配置和合并不同的配置都是一个极大的挑战。

比如 Konfig 大库中应用配置模型分为 base 和各环境 stack 配置,要求程序运行时按照某一 merge 策略合并为一份应用配置,相当于要求大库前端配置能够自动合并,即能够分开多次定义并且合并,然后实例化生成相应的唯一前端配置。

借助 KCL 语言的能力和 Konfig 的最佳实践,可通过将基线配置和环境配置自动合并简化配置的编写。比如对于标准 SOFA 应用 opsfree,其基线配置和环境配置分别维护,最终交由平台工具进行配置合并和检查。KCL 语言通过自动化合并同名配置实现简化团队协同开发的设计目标。

比如 base 配置收集的通用的配置:

appConfiguration = sofa.SofaAppConfiguration {
    mainContainer: container.Main {
        readinessProbe: probe_tpl.defaultSofaReadinessProbe
    }
    resource: res_tpl.medium
    releaseStrategy: "percent"
}

然后再预发环境在 base 配置的基础之上针对某些参数进行微调:

appConfiguration = sofa.SofaAppConfiguration {
    resource: resource.Resource {
        cpu: "4"
        memory: "8Gi"
        disk: "50Gi"
    }
    overQuota: True
}

合并的 pre 配置实际是一份 SofaAppConfiguration 配置(相当于如下等效代码,环境配置的优先级默认高于基线配置)

appConfiguration = sofa.SofaAppConfiguration {
    mainContainer: container.Main {
        readinessProbe: probe_tpl.defaultSofaReadinessProbe
    }
    resource: resource.Resource {
        cpu: "4"
        memory: "8Gi"
        disk: "50Gi"
    }
    overQuota: True
    releaseStrategy: "percent"
}

目前的同名配置虽然只针对应用的主包配置有效,但已经带来了可观察的收益。

2.2 稳定压倒一切

越是基础的组件对稳定性要求越高,复用次数越多的稳定性带来的收益也更好。因为稳定性是基础设施领域一个必备的要求,不仅仅要求逻辑正确,而且需要降低错误出现的几率。

2.2.1 静态类型和强不可变性

很多配置语言采用运行时动态检查类型。动态类型最大的缺点只能检查正在被执行属性的类型,这非常不利于开发阶段提前发现类型的错误。静态类型不仅仅可以提前分析大部分的类型错误,还可以降低后端运行时的动态类型检查的性能损耗。

除了静态类型,KCL 还通过 final 关键字禁止某些重要属性被修改。静态类型再结合属性的强不可变性,可以为配置代码提供更强的稳定性保障。

schema CafeDeployment:
    final apiVersion: str = "apps.cafe.cloud.alipay.com/v1alpha1"
    final kind: str = 123  # 类型错误

schema ContainerPort:
    containerPort: int = 8080
    protocol: "TCP" | "UDP" | "SCTP" = "TCP"
    ext? : str = None

比如对于 CafeDeployment 中的 apiVersion 信息是一种常量类型的配置参数,final 为这类配置提供保障:

代码中 apiVersion 和 kind 属性都被 final 保护禁止被修改。但是 kind 因为属性类型初始值不同而隐含一个错误,通过静态类型检查很容易在开发阶段发现错误并改正。

2.2.2运行时类型和逻辑 check 验证

KCL 的 schema 不仅仅是带类型的结构体,也可以用于在运行时校验存量的无类型的 JSON 和 YAML 数据。此外 schema 的 check 块可以编写语义检查的代码,在运行时实例化 schema 时会自动进行校验。同时,基于 schema 的继承和 mixin 可以产生跟多关联的 check 规则。

比如以下的例子展示 check 的常见用法:

schema sample:
    foo: str
    bar: int
    fooList: [str]

    check:
        bar > 0 # minimum, also support the exclusive case
        bar < 100, "message" # maximum, also support the exclusive case
        len(fooList) > 0 # min length, also support exclusive case
        len(fooList) < 100 # max length, also support exclusive case
        regex.match(foo, "^The.*Foo$") # regex match
        isunique(fooList) # unique
        bar in range(100) # range
        bar in [2, 4, 6, 8] # enum
        multiplyof(bar, 2) # multipleOf

check 中每个语句都是一个可以产生 bool 结果的表达式和可选的错误信息组成(每个普通的 bool 表达式其实是 assert 语句的简化而来)。通过内置的语法和函数可以实现在运行时对属性值的逻辑验证。

2.2.3 内置测试支持

单元测试是提升代码质量的有效手段。KCL 基于已有的 schema 语法结构,配合一个内置 kcl-test 命令提供灵活的单元测试框架(结合 testing 包可指定面值类型的命令行参数)。

内置测试工具

schema TestPerson:
    a = Person{}
    assert a.name == 'kcl'

schema TestPerson_age:
    a = Person{}
    assert a.age == 1

kcl-test 命令不仅仅执行单元测试,还会统计每个测试执行的时间,而且可以通过正则表达式参数选择执行指定的测试。此外通过 kcl-test ./… 可以递归执行子目录的单元测试,同时支持集成测试和 Plugin 测试。

2.3 高效是永恒的追求

KCL 代码不仅仅通过声明式的风格简化编程,同时通过模块支持、mixin 特性、内置的 lint 和 fmt 工具、以及 IDE 插件提供高效的开发体验。

2.3.1 schema 中好用的语法

schema 是 KCL 编写配置程序的核心语法结构,其中几乎每个特性均是针对具体的业务场景提效而设计。比如在定义和实例化深层次嵌套的配置参数时,均可以直接指定属性的路径定义和初始化。

schema A:
    a: b: c: int
    a: b: d: str = 'abc'

A {
    a.b.c: 5
}

同时为了安全,对于每个属性默认都是非空的字段,在实例化时会自动进行检查。

schema 不仅仅是一个独立的带类型注解的配置对象,我们也可以通过继承的方式来扩展已有的 schema:

schema Person:
    firstName: str
    lastName: str

# schema Scholar inherits schema Person
schema Scholar(Person):
    fullName: str = firstName + '_' + lastName
    subject: str

JohnDoe = Scholar {
    firstName: "John",
    lastName: "Doe",
    subject: "CS"
}

代码中 Scholar 从 Person 继承,然后又扩展了一些属性。作为子类的 Scholar 可以直接访问父类中定义的 firstName 等属性信息。

继承是 OOP 编程中基础的代码复用手段,但同时也有多继承导致的菱形继承的技术问题。KCL 语言刻意简化了继承的语法,只保留了单继承的语法。同时 schema 可以通过 mixin 特性混入复用相同的代码片段,对于不同的能力配套,我们通过 mixin 机制编写,并通过 mixin 声明的方式“混入”到不同的结构体中。

比如通过在 Person 中混入 FullnameMixin 可以给 schema 增加新的属性或逻辑(包括 check 代码块):

schema FullnameProtocol:
    firstName : str = "default"
    lastName : str

mixin FullnameMixin for FullnameProtocol:
    fullName : str = "${firstName} ${lastName}"

schema relax Person:
    mixin [FullnameMixin]
    firstName : str = "default"
    lastName : str

通过 KCL 的语言能力,平台侧同学可以通过单继承的方式扩展结构体,通过 mixin 机制定义结构体内属性的依赖关系及值内容,通过结构体内顺序无关的编写方式完成声明式的结构体定义,此外还支持如逻辑判断、默认值等常用功能。

2.3.2 doc、fmt、lint 和外围的 LSP 工具

在编程领域代码虽然是最核心的部分,但是代码对应的文档和配套的工具也是和编程效率高度相关的部分。KCL 配置策略语言设计哲学并不局限于语言本身,还包括文档、代码格式化工具、代码风格评估工具和 IDE 的支持等。

KCL 通过 kcl-doc 支持从配置代码直接提取产生文档,自动化的文档不仅仅减少了手工维护的成本,也降低的学习和沟通成本。kcl-fmt 则很方便将当前目录下的全部代码(包含嵌套的子目录)格式化为唯一的一种风格,而相同格式的代码同样降低的沟通和代码评审的成本。

kcl-lint 工具则是通过将一些内置的风险监测策略对 KCL 代码平行评估,方便用户根据评估结果优化代码的风格。

2.4 工程化的解决方案

任何语言想要在工程中实际应用,不仅仅需要很好的设计,还需要为升级、扩展和集成等常规的场景提供完整的解决方案。

2.4.1 多维度接口

KCL 语言设计通过在不同的抽象层次为普通用户(KCL 命令行)、KCL 语言定制者(Go-API、Python-API)、KCL 库扩展者(Plugin)和 IDE 开发者(LSP 服务)均提供了几乎等价的功能界面,从而提供了最大的灵活度。

2.4.2 千人千面的配置 DB

KCL 是面向配置的编程语言,而配置的核心是结构化的数据。因此,我们可以将完整 KCL 代码看做是一种配置数据库。通过 KCL 的配置参数的查询和更新(override/-O 命令)可以和对应的配置属性路径,可以实现对属性参数的查询、临时修改和存盘修改。

将代码化的配置作为 DB 的唯一源,不仅仅可以集成 DB 领域成熟的查询和分析手段,而且可以通过配置代码视角调整配置代码的逻辑结构。特别是在自动化运维实践中,通过程序自动生成的配置代码修改的 PullRequest 可以方便引入开发人员进行代码评审,很好地达到人机通过不同界面配合运维。

2.4.3 版本平滑升级

随着业务和代码的演化,相关模块的 API 也会慢慢腐化。KCL 语言设计通过严格的依赖版本管理,然后结合语言内置的语法和检查工具保障 API 平滑的升级和过渡,再配合代码集成测试和评审流程提升代码安全。KCL 语言通过 @deprecated 特性在代码出现腐化早期给出提示,同时为用户的过渡升级留出一定的时间窗口,甚至等到 API 彻底腐烂前通过报错的方式强制要求同步升级相关的代码。

比如在某次升级中,Name 属性被 fullName 替代了,则可以通过 @deprecated 特性标志:

schema Person:
    @deprecated(version="1.1.0", reason="use fullName instead", strict=True)
    name: str
    ... # Omitted contents

person = Person {
    # report an error on configing a deprecated attribute
    name: "name"
}

这样在实例化 Person 时,Name 属性的初始化语句将会及时收到报错信息。

2.4.4 内置模块、KCL 模块、插件模块

KCL 是面向配置的编程语言,通过内置模块、KCL 模块和插件模块提供工程化的扩展能力。

用户代码中不用导入直接使用 builtin 的函数(比如用 len 计算列表的长度、通过 typeof 获取值的类型等),而对于字符串等基础类型也提供了一些内置方法(比如转化字符串的大小写等方法)。

对于相对复杂的通用工作则通过标志库提供,比如通过 import 导入 math 库就可以使用相关的数学函数,可以通过导入 regex 库使用正则表达式库。而针对 KCL 代码也可以组织为模块,比如 Konfig 大库中将基础设施和各种标准的应用抽象为模块供上层用户使用。

此外还可以通过 Plugin 机制,采用 Python 为 KCL 开发插件,比如目前有 meta 插件可以通过网络查询中心配置信息,app-context 插件则可以用于获取当前应用的上下文信息从而简化代码的编写。

3. KCL语言的实现原理

3.1 整体架构

KCL 虽然作为一个专用于云原生配置和策略定义的语言,但是保持大多数过程式和函数式编程语言的相似实现架构,其内部整体架构组成也是经典的编译器 “三段式” 架构。下面是 KCL 实现的架构图:

主要有以下几个关键模块:

整体架构分为三段式的好处是可以把针对 KCL 源语言的前端和针对目标机器的后端组合起来,这种创建编译器组合的方法可以大大减少工作量。

比如目前的 KCL 字节码定义和后端虚拟机采用自研实现,KCL 虚拟机主要用于计算产生配置结果并序列化为 YAML/JSON 进行输出。

如果遇到在其他特殊使用 KCL 的场景比如在浏览器中执行 KCL,则可以重写一个适配 WASM 的后端,就可轻易将 KCL 移植到浏览器中使用,但是 KCL 本身的语法和语义不需要发生任何变化,编译器前端代码也无需任何改动。

3.2 Go 和 Python 通信原理

为了更好地释放 KCL 配置策略语言的能力以及遍于上层自动化产品集成(比如著名的编译器后端 LLVM 就因其 API 设计良好,开发人员可以利用其 API 快速地构建自己的编程语言),KCLVM 目前提供了 Python 和 Go 两种语言的 API,使得用户可以使用相应的 API 快速地构建语言外围工具,语言自动化查询修改工具等提升语言的自动化能力,并且进一步可以基于此构建服务化能力,帮助更多的用户构建自己云原生配置代码化应用或者快速接入基础设施。

KCLVM 主体采用 Python 代码实现,而很多的云原生应用以 Go 程序构建,因此为了更好地满足云原生应用用户诉求。KCLVM 首先基于 CGo 和 CPython 构建了 Go 程序和 Python 程序通信媒介,基于此设计了 Python 函数到 Go 函数的 RPC 调用,调用参数以 JSON 形式存储,使得 KCLVM-Python 编译器的能力平滑地过度到 Go 代码中,通过 Go 一行 import 调用即可操作 KCL 代码。

补充:在服务化实践的过程中,基于 CGO 调用 Python 的方案也遇到了一些问题:首先是 Go + CGO + Python 导致交叉编译困难,对 ACI 的自动化测试和打包产生了挑战;其次是 CGO 之后的 Python 不支持多语言多线程并发,无法利用多核的性能;最后即使通过 CGO 将 Python 虚拟机编译到了 Go 程序中,依然还是需要安装 Python 的标准库和第三方库。

3.3 协同配置原理

当有了一个简单易用并能够保证稳定性的配置语言后,另一个面临的问题是如何使用配置代码化的方式提升协同能力。基于此,KCL 配置可分为用户侧和平台侧配置两类,最终的配置内容由各自用户侧和平台侧的配置内容共同决定,因此存在两个方面的协同问题:

针对上述协同问题,KCL 在技术侧提出了顺序无关语法,同名配置合并等抽象模型来满足不同的协同配置场景。

以上图为例,首先 KCL 代码在编译过程中形成两张图(用户不同配置直接的引用和从属关系一般形式一张有向无环图),分别对应结构体内部声明代码及结构体使用声明代码。编译过程可以简单分为三步:

通过这样简单的计算过程,可以在编译时完成大部分代换运算,最终运行时仅进行少量计算即可得到最终的解。同时在编译合并图过程中仍然能够执行类型检查和值的检查,区别是类型检查是做泛化、取偏序上确界(检查某个变量的值是否满足既定类型或者既定类型的子类型),值检查是做特化、取偏序下确界(比如将两个字典合并为一个字典)

4. 对未来的展望

KCL 语言目前依然处于一个高速发展的阶段,目前已经有一些应用开始试用。我们希望通过 KCL 语言为 Kusion 技术栈提供更强的能力,在运维、可信、云原生架构演进方面起到积极的作用。同时对于一些特殊的非标应用提供灵活的扩展和集成方案,比如我们正在考虑如何让后端支持 WebAssembly 平台,从而支持更多的集成方案。

在合适的时间我们希望能够开放 KCL 的全部代码,为云原生代码化的快速落地贡献绵薄之力。

Go 语言十年而立,Go2 蓄势待发

2019-11-10 08:00:00

Go语言十年,第一代Gopher也到了下岗到年龄,感谢各种福报

在21世纪的第一个十年,计算机在中国大陆才逐渐开始普及,高校的计算机相关专业也逐渐变得热门。当时学校主要以C/C++和Java语言学习为主,而这些语言大多是上个世纪90年代或更早诞生的,因此这些计算机领域的理论知识或编程语言仿佛是上帝创世纪时的产物,作为计算机相关专业的学生只能仰望这些成果。

Go语言诞生在21世纪新一波工业编程语言即将爆发的时期。在2010年前后诞生了编译型语言Rust、Kotlin和Swift语言,前端诞生了Dart、TypeScript等工业型语言,最新出现的V语言更甚至尝试站在Go和Rust语言肩膀之上创新。而这些变化都发生在我们身边,让中国的计算机爱好者在学习的过程中见证历史的发展,甚至有机会参与其中。

2019年是CSDN的二十周年,也是Go语言面世十周年。感谢CSDN平台提供的机会,让笔者可以跟大家分享十年来中国Go语言社区的一些故事。

1. Go语言诞生

Go语言最初由Google公司的Robert Griesemer、Ken Thompson和Rob Pike三位大牛于2007年开始设计发明的。其设计最初的洪荒之力来自于对超级复杂的C++11特性的吹捧报告的鄙视,最终目标是设计网络和多核时代的C语言。到2008年中期,语言的大部分特性设计已经完成,并开始着手实现编译器和运行,大约在这一年Russ Cox作为主力开发者加入。到了2009年,Go语言已经逐步趋于稳定。同年9月,Go语言正式发布并开源了代码。

以上是《Go语言高级编程》一书中第一章第一节的内容。Go语言刚刚开源的时候,大家对它的编译速度印象异常深刻:秒级编译完成,几乎像脚本一样可以马上编译并执行。同时Go语言的隐式接口让一个编译型语言有了鸭子类型的能力,笔者也第一次认识到原来C++的虚表vtab也可以动态生成!至于大家最愿意讨论的并非特性,其实并不是Go语言新发明的基石,早在上个世纪的八九十年代就有诸多语言开始陆续尝试将CSP理论引入编程语言(Rob Pike是其中坚定的实践者)。只不过早期的CSP实践的语言没有进入主流开发领域,导致大家对这种并发模式比较陌生。

除了语言特性的创新之外,Go语言还自带了一套编译和构建工具,同时小巧的标准库携带了完备的Web编程基础构建,我们可以用Go语言轻松编写一个支持高并发访问的Web服务。

作为互联网时代的C语言,Go语言终于强势进入主流的编程领域。

2. Go语言十年奋进

Go从2007年开始设计,在2009年正式对外公布,至今刚好十年。十年来Go语言以稳定著称,Go1.0的代码在2019年依然可以不用修改直接被编译运行。但是在保持语言稳定的同时,Go语言也在逐步夯实基础,十年来一直向着完美的极限逼近。让我们看看这十年来Go语言有哪些变化。

界面变化

首先是看看界面的变化。第一次是在2009刚开源的时候,这时候可以说是Go语言的上古时代。Go语言的主页如下:

那个年代的Gopher们,使用的是hg工具下载代码(而不是Git),Go代码是在Google Code托管(而不是GitHub)。随着代码的发展,hg已经慢慢淡出Gopher视野,Google Code网站也早已经关闭,而Go1之前的上古时代的Go老代码已经开始慢慢腐化了。

首页中心是Go语言最开始的口号:Go语言是富有表现力的、并发的编程语言,并且是简洁的。同时给了一个“Hello, 世界”的例子(注意,这里的“世界”是日文)。

然后右上角是初学者的乐园:首先是安装环境,然后可能是早期的三日教程,第三个是标准库的使用。右上角的图片是Russ Cox的一个视频,在Youtube应该还能找到。

左上角是Go实战的那个经典文档。此外FAQ、语言规范、内存模型是非常重要的核心温度。左下角还有cmd等文档链接,子页面的内容应该没有什么变化。

然后在2012年准备发布第一个正式版本Go1,在Go1之前语言、标准库和godoc都进行了大量的改进。Go1风格的页面效果如下:

新页面刚出来的时候有眼睛一亮的感觉,这个是目前存在时间最长久的页面布局。但是不仅仅是笔者我,甚至Go语言官方也慢慢对中国页面有点审美疲劳了。因此,从2018年开始Go语言开始新的Logo和网站的重新设计工作。

下面的是Go语言新的Logo:

2019年是对Go语言发展极其重要的一年,今年8月将发布Go1.13,而这个版本将正式重启Go语言语法的进化,向着Go2前进。而新的网站已经在Go1.13正式发布之前的7月份就已经上线:

头部的按钮风格的菜单变成了平铺的风格,显得更加高大上。同时页面的颜色做了调整,保持和新Logo颜色一致。页面的布局也做了调整,将下载左右两列做了调换。同时地鼠的脑袋歪到一边,估计是颈椎病复发了。

总的来说,Go语言官网主页经历了Go1前、Go1(1.0~1.10)、Go1后(或者叫Go2前)三个阶段,分别对应3种风格的页面。新的布局或许会成为下个十年Go2的主力页面。

语法变化

Go语言虽然从2009年诞生,但是到了2012年才发布第一个正式的版本Go1。其实在Go1诞生之前Go语言就已经足够稳定了,国内的七牛云从Go1之前就开始大力转向Go语言开发,是国内第一家广泛采用Go语言开发的互联网公司。Go1的目标是梳理语法和标准库阴暗的角落,为后续的10年打下坚实的基础。

从目前的结果看,Go1无疑是取得了极大的成果,Go1时代的代码依然可以不用修改就可以用最新的Go语言工具编译构建(不包含CGO或汇编语言部分,因为这些外延的工具并不在Go1的承诺范围)。但是Go1之后依然有一些语法的更新,在Go1.10前的Go1时代语法和标准库部分的重大变化主要有三个:

第一个重大的语法变化是在2012年发布的Go1.2中,给切片语法增加了容量的控制,这样可以避免不同的切片不小心越界访问有着相同底层数组的其它切片的内存。

第二个重大的变化是2016年发布的Go1.7标准库引入了context包。context包是Go语言官方对Go进行并发编程的实践成果,用来简化对于处理单个请求的多个Goroutine之间与请求域的数据、超时和退出等操作。context包推出后就被社区快速吸收使用,例如gRPC以及很多Web框架都通过context来控制Goroutine的生命周期。

第三个重大的语法变化是2017年发布的Go1.9 ,引入了类型别名的特性:type T1 = T2。其中类型别名T1是通过=符号从T2定义,这里的T1和T2是完全相同的类型。之所以引入类型别名,很大的原因是为了解决Go1.7将context扩展库移动到标准库带来的问题。因为标准库和扩展库中分别定义了context.Context类型,而不同包中的类型是不相容的。而gRPC等很多开源的库使用的是最开始以来的扩展库中的context.Context类型,结果导致其无法和Go1.7标准库中的context.Context类型兼容。这个问题最终通过类型别名解决了:扩展库中的context.Context类型是标准库中context.Context的别名类型,从而实现了和标准库的兼容。

此外还有一些语法细节的变化,比如Go1.4对for循环语法进行了增强、Go1.8放开对有着相同内存布局的结构体强制转型限制。读者可以根据自己新需要查看相关发布日志的文档说明。

运行时的变化

运行时部分最大的变化是动态栈部分。在Go1.2之前Go语言采用分段栈的方式实现栈的动态伸缩。但是分段式动态栈有个性能问题,因为栈内存不连续会导致CPU缓存命中率下降,从而导致热点的函数调用性能受到影响。因此从Go1.3开始该有连续式的动态栈。连续式的动态栈虽然部分缓解了CPU 缓存命中率问题(依然存在栈的切换问题,这可能导致CPU缓存失效),但同时也带来了更大的实现问题:栈上变量的地址可能会随着栈的移动而发生变化。这直接带来了CGO编程中,Go语言内存对象无法直接传递给C语言空间使用,因此后来Go语言官方针对CGO问题制定了复杂的内存使用规范。

总体来说,动态栈如何实现是一个如何取舍的问题,因为没有银弹、鱼和熊掌不可兼得,目前的选择是第一保证纯Go程序的性能。

GC性能改进

Go语言是一个带自动垃圾回收的语言(Garbage Collection ),简称GC(注意这是大写的GC,小写的gc表示Go语言的编译器)。从Go语言诞生开始,GC的回收性能就是大家关注的热点话题。

Go语言之所以能够支持GC特性,是因为Go语言中每个变量都有完备的元信息,通过这些元信息可以很容易跟踪全部指针的声明周期。在Go1.4之前,GC采用的是STW停止世界的方式回收内存,停顿的时间经常是几秒甚至达到几十秒。因此早期社区有很多如何规避或降低GC操作的技巧文章。

第一次GC性能变革发生在Go1.5时期,这个时候Go语言的运行时和工具链已经全部从C语言改用Go语言实现,为GC代码的重构和优化提供了便利。Go1.5首次改用并行和增量的方式回收内存,这将GC挺短时间缩短到几百毫秒。下图是官网“Go GC: Latency Problem Solved”一文给出的数据:

Go1.5并发和增量的改进效果明显,但是最重要的是为未来的改进奠定了基础。在Go1.5之后的Go1.6版本中GC性能终于开始得到了彻底的提升:从Go1.6.0停顿时间降低到几十毫秒,到Go1.6.3降低到了十毫秒以内。而Go1.6取得的成果在Go1.8的官方日志得到证实:Go语言的GC通常低于100毫秒,甚至低于10毫秒!

当然,Go的GC优化的脚步不会停止,但是想再现Go1.5和Go1.6时那种激动人心的成果估计比较难了。在Go1.8之后的几个版本中,官方的发布日志已经很少再出现量化的GC性能提升数据了。

Go语言自举历程

据说Go语言刚开始实现时是基于汤普森的C语言编译改造而成,并且最开始输出的是C语言代码(还没有对外公开之前)。在开源之后到Go1.4之前,Go语言的编译器和运行时都是采用C语言实现的。以至于早期可以用C语言实现一个Go语言函数!因为强烈依赖C语言工具链,因此Go1.4之前Go语言是完全不能自举的。

从Go1.4开始,Go语言的运行时采用Go语言实现。具体实施的方式是Go团队的rsc首先实现了一个简化的C代码到Go代码的转换工具,这个工具主要用于将之前C语言实现的Go语言运行时转换为Go语言代码。因为是自动转换的代码,因此可以得到比较可靠的Go代码。运行时转换为Go语言实现之后,带来的第一个好处就是GC可以精确知道每个内存指针的状态(因为Go语言的变量有详细的类型信息),这也为Go1.5重写GC提供了运行时基础。

然后到了Go1.5,将编译器也转为Go语言实现。但是转换到代码性能有一定的下降。很多程序的编译时间甚至缓慢到几十秒,这个时期网上出现了很多吐槽Go1.5编译速度慢的问题。Go1.5采用Go语言编写编译器的同时,对工具链和目标代码都做了大量的重构工作。从Go1.5之后,交叉编译变得异常简单,只要GOOS=linux GOARCH=amd64 go build命令就可以从任何Go语言环境生成Linux/amd64的目标代码。

Go语言从Go1.4到Go1.5,经历了两个版本的演化终于实现了自举的支持。当然自举也会带来一个哲学问题:Go语言的编译器是否有后门?如果有后门的编译器编译出来的Go程序是否有后门?有后门的编译器编译出来的Go编译器程序是否有后门?

失败的尝试

Go语言发展过程中也并不全是成功的案例,同时也存在一些失败的尝试。失败乃成功之母,这些尝试虽然最终失败了,但是在尝试的过程之中积累的经验为新的方向提供了前进的动力。

因为Go语言的常量只支持数值和字符串等少数几个类型,早期的社区中一直呼吁为切片增加只读类型。为此rsc在开发分支首先试验性地实现了该特性,但是在之后的实践过程中又发现了和Go编程特性冲突的诸多问题,以至于在短暂的尝试之后就放弃了只读切片的特性。当然,初始化之后不能修改的变量特性依然是大家期望的一个特性(类似其它语言的final特性),希望在未来的Go2中能有一定的改善。

另一个尝试是早期基于vendor的版本管理。在Go1.5中首次引入vendor和internal特性,vendor用于打包外部第三方包,internal用户保护内部的包。后来vendor被开源社区的各种版本管理工具所滥用,导致Go语言代码经常会出现一些不可构建的诡异问题。滥用vendor导致了vendor嵌套的问题,这和nodejs社区中node_modules目录嵌套的问题类似。嵌套的vendor中最终会出现同一个包的不同版本,这根最后的稻草终于彻底击溃了vendor机制,以至于Go语言官方团队从头开发了模块特性来彻底解决版本管理的问题。等到Go1.13模块化特性转正之中,GOPATH和vendor等机制将被彻底淘汰。

Go语言作为一个开源项目,所有导入的包必须有源代码。一些号称是商业用户,呼吁Go语言支持二进制包的导入,这样可以最大限度地保护商业代码。为了响应社区的需求,Go1.7增加了导入二进制包的功能。但是比较戏剧化的是,Go语言支持二进制包导入之后并没有多少人在使用,甚至当初呼吁二进制包的人也没有使用(所以说很多社区的声音未必能够反映真实的需求)。为了一个没有人使用的二进制包特性,需要Go语言团队投入相当的人力进行维护代码。为了减少这种不需要的特性,Go1.13将彻底关闭二进制包的特性,从新轻装上阵解决真实的需求。当然,Go语言也已经支持了生成静态库、共享库和插件的特性,也可以通过这些机制来保护代码。

失败的尝试可能还有一些,比如最近Go语言之父之一Robert Griesemer提交的通过try内置函数来简化错误处理就被否决了。失败的尝试是一个好的现象,它表示Go语言依然在一些新兴领域的尝试——Go语言依然处于活跃期。

3. Go2的发展方向

Go语言原本就是短小精悍的语言,经过多年的发展Go1已经逼近稳定的极限。查看官网的Talk页面的报告数量可以发现,2015年之前是各种报告的巅峰,2016到2017年分享数量已经开始急剧下降,2018年至今已经没有新的报告被收录,这是因为该讲的Go1语言特性早就被讲过多次了。对于第一波Go语言爱好者来说也是如此,Go语言已经没有什么新的特性可以挖掘和学习了,或者说它已经不够酷了。我们想Go语言官方团队也是这样的感觉,因此从2018年开始首先开始解决模块化的问题,然后开始正式讨论Go2的新特性,并且从Go1.13重新启动语言的进化。

模块化和构建管理有关系。在Go语言刚刚诞生之初,其实是通过一个Makefile目标进行构建。然后官方提供了go build命令构建,实现了零配置文件构建,极大地简化了构建的流程。再后来出现了go get命令,支持从互联网上自动下载hg或git仓库的代码进行构建,并同时引入GOPATH环境变量来防止非标准库的代码。此后,第一波的版本管理工具也开始出现,通过动态调整GOPATH实现导入特定版本的代码。随后各种开源模仿、克隆的版本管理工具如雨后春笋般冒出来,基本都是模仿godeps的设计思路,基于GOPATH和后来的vendor来管理依赖包的版本,这也最终导致了vendor被过度滥用(前文已经讲过vendor滥用带来的问题)。最终在2018年,由rsc亲自操刀从头发明了基于最小化版本依赖算法的版本管理特性。模块化特性从Go1.11开始引入,将在Go1.13版本正式转正,以后GOAPATH将彻底退出历史舞台。

因为rsc的工作直接宣判了开源社区的各种版本管理工具的死亡,这也导致了Go语言官方团队和开源社区的诸多冲突和矛盾。在此需要补充说明下,Go语言的开发并不完全是开源陌生,Go语言的开源仅仅限于Issue的提交或BUG的修改,真正的语言设计始终走的是教堂元老会的模式。笔者以为这是最好的开源方式,很多开源社区的例子也说明了需要独裁者的角色,而元老会正是这种角色。

在Go1.13中,除了模块化特性转正之外,还有诸多语法的改进:比如十六进制的浮点数、大的数字可以通过下划线进行分隔、二进制和八进制的面值常量等。但是Go1.13还有一个重大的改进发生在errors标准库中。errors库增加了Is/As/Unwrap三个函数,这将用于支持错误的再次包装和识别处理,是为了Go2中新的错误处理改进提前做准备。后续改进方向就是错误处理的控制流,之前已经出现用try/check关键字和try内置函数改进错误处理流程的提案,目前还没有确定采用什么方案。

Go2最期待的特性是泛型。从开始Go语言官方明显抵制泛型,到2018年开始公开讨论泛型,让泛型的爱好者看到了希望。很多人包括早期的Go官方都会说用接口模拟泛型,这其实只是一个借口。泛型最大的问题不在于性能,而是只有泛型才能够为泛型容器或算法提供一个类型安全的接口。比如一个Add(a, b T) T泛型函数是无法通过接口来实现对返回值类型的检查的。如果Go语言支持了泛型,再结合Go语言汇编语言支持的AVX512指令,可以期待Go语言将在CPU运算密集型领域占有一席之地,甚至以后会出现纯Go语言的机器学习算法库的实现。

最后一个值得关注的是Go语言对WebAssembly平台的支持。根据Ending定律:一切可编译为WebAssembly的,终将会被编译为WebAssembly。2018年,Fabrice Bellard大神基于WebAssembly技术,将Windows 2000操作系统搬到了浏览器环境运行。2019年出现了WebAssembly System Interface技术,这很可能是一个更轻量化的Docker替代技术。而Go语言也出现了一个变异版本TinyGo,目标就是为了更好地在WebAssembly或其它单片机等受限环境运行Go程序。

4. Go语言在中国

回想Go语言刚面世时的第一个例子,是打印”Hello, 世界”。只可惜这里的“世界”并不是中文的“Hello, 世界”,而是日文的“Hello, 世界”。而日文还是基于中文汉字改造而来,这是整个中文世界的悲哀!

比较庆幸的是中国程序员比较给力,目前中国不仅仅是世界上Go语言关注度最高的国家,也是贡献排名第二的国家。根据谷歌趋势的数据,Go语言在中国的关注度占全球的90%以上:

不仅仅是Go语言用户,中国的Gopher对Go语言的贡献也稳居美国之后。其中韦京光早在2010年就深度参与Go语言开发,将Go语言移植到Windows系统并实现了CGO支持。之后来自中国的Minux实现了iOS等诸多平台的移植,并已经正式加入Go语言开发团队。而目前Go语言中国贡献者排名第一的是来自天津的史斌(benshi001),他的很多工作集中在编译的优化方面,在全球Go语言贡献者排名第39位。

最早Go语言中文爱好者都是通过谷歌讨论组golang-china讨论,目前该讨论组还陆续会有新的文章发布。然后到了2012年前后,因为诸多因素国内的讨论开始集中到QQ群中(笔者在2010年建立了国内第一个Go语言QQ讨论群)。再往后就是微信各种论坛遍地开花了。十年来,Go语言中文社区也一直非常活跃,社区人数稳步增长。这里简单回顾一下我知道的Go社区中的一些人和事。

Fango

如果在2010年关注Go语言,肯定会听到Fango的名字。Fango是来自新加坡的Go语言爱好者,在Go语言刚面世不久他就写了第一本(很可能是唯一一本)以Go语言为题材的小说《胡文·Go》,然后他还出版了第一本Go语言中文教材《Go语言·云动力》。感谢Fango给大家带来的精彩的Go语言故事。

许式伟和七牛云

七牛是国内第一家大面积采用Go语言开发的公司,时间还在Go1.0正式发布之前。许式伟也是大中华第一个知名的Go语言布道师。许式伟和七牛云在2012年也出版了一本《Go语言编程》教程,和Fango的图书可能只差了一个多月的时间,编辑都是杨海铃老师。其后七牛还有多本Go语言相关的专著或译著,可以说在2015年之前,许式伟和七牛云团队绝对是国内Go语言社区推广的主力。

笔者也在第一时间拜读了《Go语言编程》一书,对其中如何实现接口和Goroutine调度的模拟依然印象深刻。感谢许式伟当时赠送的签名版本《Go语言编程》,同时也感谢为我新出的《Go语言高级编程》写序,谢谢许大!

Astaxie和GopherChina大会

对谢大最早的印象是在2012年前后,当时他开了一个免费的《Go Web编程》图书,当前QQ群中很多小伙伴都参与审校(比如四月份平民、边江和Oling Cat等)。Go Web编程是大家比较关注的方向,书中不仅仅讲到了ORM的实现,还讲到了beedb等组件。而beedb等这些组件最早演化成了Beego框架。根据前一段时间JetBrains展开的一个调查,Beego是Go语言三大流行的Web框架之一。

然后到了2015年,谢大正式开启GopherChina大会的历程。我虽然因为其它事情没有现场参与,但是也预定了第一节GopherChina大会的会衫。然后在2018年终于以讲师身份参加了上海的GopherChina大会,跟大家分享了CGO方向的技术,同时第一次见到谢大本尊。感谢谢大的GopherChina大会和《Go Web编程》!

其他人和项目

此外还有很多大家耳熟能详的Go爱好者,比如《Learning Go》和Go Tour的中文翻译者星星,创建了gogs的无闻,一种在翻译Go官方文档的Oling Cat,雨痕的《Go语言学习笔记》对Go源码深度的解读,创建了GoHackers的郝林等等。此外由国内的PingCAP公司主导开发的开源TiDB分布式数据库也是一个极为著名的项目。感谢Go中国社区这些朋友和项目,是大家的努力带来了Go语言在国内的繁荣。

5. 向Go语言学习

候杰老师曾经说过:勿在浮沙筑高台。而中国互联网公司的繁荣更多是在业务层面,底层的基石软件几乎没有一个是中国所创造。作为一个严肃的软件开发人员,我们需要向Go语言学习,继续扎实掌握底层的理论基础,不能只聚焦于业务层面,否则下次中美贸易战的时候依然要被西方卡脖子。

经过这么多年发展,中国的软件行业已经非常繁荣和成熟,同时很多软件开发人员也开始进入35岁的中年门槛。其实35岁正是软件开发人员第二次职业生涯的开始,是开始形成自我创造力的时候。但是某些资本家短视的996或007等急功近利的福报观点正导致中国软件人员过早进入未创新而衰的阶段。中国的软件工程师不应该是码农、更不是码畜牧,我们虽然不会喊口号但是始终在默默前行。

目前中国已经有大量的软件开发人员有能力参与基础软件的设计和开发,正因为这一波脚踏实地程序开发人员的努力,我相信在下个十年我们可以Go得更远。

谈谈Go语言字符串

2019-05-17 08:00:00

字符串是一种特别重要的类型, 可以说整个世界都是建立在字符串处理基础之上的, 甚至有很多专门针对字符串处理设计的编程语言(比如perl). 因为字符串处理非常重要, Go语言将字符串作为值以简化使用, 同时标准库提供了strings/fmt/strconv/regexp/template等诸多包用于协助处理字符串.

1. 基本用法

Go语言中字符串是一个不可修改的字节序列, 如果要做类比的话可以看作是一个只读的byte数组类型. 字符串有两种方式构建: 第一种是在代码中通过双引号包括起来的字符串字面值, 这是编译前就知道了字符串的内容; 另一种是在运行时通过代码运行动态产生的字符串.

因为Go语言源代码要求是UTF8编码, 因此字符串面值的内容也是UTF8编码的. 为了方便面值字符串的遍历, Go语言的for range内置了对UTF8的支持:

for i, c := range "hello, 世界" {
    // ...
}

其中i是字符所在的索引下标, c表示Unicode字符的值(对应int32类型). 因为UTF8是一种变长的编码, 因此每次i的步进长度是变化的, 每次步进的是前当前字符串对应UTF8编码的长度.

此外字符串语法还支持切片、链接和获取某个下标字节值的功能, 比如:

var s = "光谷码农 - https://guanggu-coder.cn/"
var c = s[0] // 获取字节值, 而不是字符对应的Unicode值
var x = s[:len(s)-1] + "abc"

字符串不仅仅可以作为字面值, 还可以当做二进制数组使用, 这时候可以用于保存任意类型的数据:

var s = "\xe4\xb8\x96" // 世
var x = []byte{0xE4, 0xB8, 0x96}
var s = string(x)

字符串的基本用法大家都是熟悉的, 我们这里不再向西展开.

2. 内部表示

Go语言字符串的底层结构在reflect.StringHeader中定义:

type StringHeader struct {
    Data uintptr
    Len  int
}

字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。字符串其实是一个结构体,因此字符串的赋值操作也就是reflect.StringHeader结构体的复制过程,并不会涉及底层字节数组的复制。

需要注意的是字符串的头部结构是切片头部结构的前缀(只是缺少了cap表示的容量部分), 这是为了便于[]byte类型的切片和字符串相互之间的转化.

3. 其它类型转换

这里讨论是底层有着不同数据布局的类型和字符串的相互转换. 如果是基于字符串重新定义的类型不在讨论之列.

Go语言中和字符串相关的内置转换主要有三种类型: 首先是字符转为字符串, 其次是字符串和字节切片的转换, 最后是字符串和rune切片的转换.

字符到字符串的转换时单向操作(无法从字符串反向转为字符), 下面的例子中是从“a”这个字符的ASCII值转为字符串“a”:

fmt.Println(string(97))       // a
fmt.Println(string(rune(97))) // a
fmt.Println(string('a'))      // a

在第一行语句中, 97是一个无具体类型的数值类型的字面值常量. 在遇到string强制转型时, 只有rune类型可以和无具体类型的数值类型建立关系, 因此97被捕获为rune类型的常量, 也就是第二个语句的方法. 第三个语句中’a’是rune(97)对应字符的字面值写法.

然后是字符串和字节切片的相互转换:

var s = string([]byte{97, 98, 99}) // abc
var x = []byte("abc")

因为字节切片和字符串底层的数据布局是相融的, 因此这种转换一般有着较高的优化空间(前提是不能破坏字符串只读的语义).

内置转换的语法中最特殊的是字符串和rune切片的转换:

var s = string([]rune{97, 98, 99}) // abc
var x = []rune("abc")

rune其实是int32类型的别名, 因此换成以下写法会发现其特殊之处:

var s = string([]int32{97, 98, 99}) // abc
var x = []int32("abc")

Go语言居然内置了字符串和int32切片的转型操作, 而这个操作是有相当的复杂度的(具体要涉及内存分配和UTF8字符串编码解码, 时间复杂度和长度相关)! 很多人如果看到上面代码可行, 自然会下意识将int32推广为其它整数类型的切片. 但是这只是字符串为int32开的一个特例(所以说Go语言也不是完全正交的设计, 有不是补丁特性).

除了内置的转换之外, 字符串还进程需要和其它bool/int等类型的转换. 这里大部分也是双向的转换, 不过我们重点讨论其他类型到字符串的转换. strconv包提供了很多这类转换操作:

s := strconv.Itoa(-42)

s10 := strconv.FormatInt(v, 10)
s16 := strconv.FormatInt(v, 16)

s := strconv.FormatBool(true)

其中Itoa是Int-to-ASCII的缩写, 表示整数到字符串转换, 采用十进制模式转换. 而FormatInt则可以用于指定进制进行转换. 此外FormatBool等用于其他数值类型的转换.

strconv的转换实现性能较好. 如果不在意这转换操作这一点点的性能损耗, 可以通过fmt.Sprintf来实现到字符串的转换(fmt.Sscanf可解析, 但是打破了链式操作的便捷性):

i := fmt.Srpintf("%v", -42)
b := fmt.Srpintf("%v", true)

fmt包会通过反射识别输入参数的类型, 然后以默认的方式转换为字符串.

此外, 对于字符串本身也提供了一种转换, 就是字符串和字符串面值格式. 比如以下代码:

q := strconv.Quote(`"hello"`)     // "\"hello\""
q := fmt.Sprintf("%q", `"hello"`) // "\"hello\""

输出的字符串会有一个双引号包裹, 内部的特殊符号会采用转义语法表示, 它对应fmt包中%q格式的输出.

更进一步, 为了方便不支持中文的环境也能处理, 还可以选择完全用ASCII方式表示字符串面值:

q := strconv.Quote(`"世"`)      // "\"\u4e16\""
q := fmt.Sprintf("%+q", `"世"`) // "\"\u4e16\""

其中“世”已经超出ASCII值域, 因此通过\u???语法通过Unicode码点值表示, , 它对应fmt包中%+q格式的输出.

4. 字符串替换

字符串处理中除了涉及其他类型和字符串之间相互转换, 另一种经常遇到的是将一个字符串处理为另一个字符串. 标准库中strings包提供了诸多字符串处理函数.

比如, 将字符串改成大写字符串:

var s = strings.ToUpper("Gopher") // GOPHER

这其实是将字符串中某些子串根据某种指定的规则替换成新的字符串.

我们可以通过strings.Map来重新实现ToUpper的功能:

strings.Map(func(r rune) rune { return r &^ ' ' }, "Gopher"))

strings.Map会遍历字符串中的每个字符, 然后通过第一参数传入的函数转换为新的字符, 最后构造出新的字符串. 而字符转换函数只有一个语句r &^ ‘ ‘, 作用是将小写字母转为大写字母.

strings.Map函数的输出是根据输入字符动态生成输出的字符, 但是这种替换是一个字符对应一个字符, 因此输出的字符串长度输入的字符串是一样的.

字符层面的替换是比较简单的需求. 更多时候我们需要将一个子串替换为一个新的子串. 子串的替换虽然看似功能强大, 但是因为没有统一的遍历子串的规则, 因此标准库并没有类似strings.Map这样方便的函数.

简单的替换可以通过strings. Replace完成:

fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2))
fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1))

上面是strings自带的例子. strings.Replace的第一个参数是输入的字符串, 第二个是要替换的子串, 第三个是用了替换的子串, 最后一个参数表示要替换几个子串. 如果替换规则稍微复杂一点, strings.Replace就比较难以实现了.

复杂的替换可以通过regexp的包完成:

regexp.MustCompile(`a(x*)b`).ReplaceAllString("-ab-axxb-", "T")
// -T-T-

如果满足a(x*)b模式的子串将被替换为新的子串.

5. 模板输出

字符串替换其实是模板的雏形. 我们可以通过字符串替换来构造一个简单的模板:

func ReplaceMap(s string, m map[string]string) string {
    for old, new := m {
        s = strings.Replace(s, old, new, -1)
    }
    return s
}

func main() {
    var s = ReplaceMap(`{a}+{b} = {c}`, map[string]string{
        "a": 1, "b": 2, "c": 3,
    })
}

通过{name}来表示要替换的子串, 然后通过map来定一个子串替换表格.

基于类型的技巧, 我们可以将{name}定义为子串的查找规则, 这样我们将得到一个子串列表:

func MapString(s string,
    mapping func(x string) string,
) string {
    re := regexp.MustCompile(`\{\w+\}`)
    for _, old := range re.FindAllString("{name}{age}", -1) {
        s = strings.Replace(s, old, mapping(old), -1)
    }
}

既然能够得到子串列表, 那么就可以仿造strings.Map的接口, 通过一个转换函数来实现子串的替换(函数比表格更加灵活).

如果结合反射机制, 完全可以基于一个接口类型输出转换表格:

func RenderTemplate(s string, data interface{}) string {
    return MapString(s, func(filedName string) string {
        // 通过反射, 根据 filedName 从 data 获取数据
    })
}

当然, 这种模板比较粗糙, 没有实现结构化编程中分支和循环等语句的支持. 完整的模板可以查看标准库的template包实现. template包是一个较大的话题, 有机会的话会在新的文章中专门讨论.

io.EOF设计的缺陷和改进

2019-05-14 08:00:00

1. 认识io.EOF

io.EOF是io包中的变量, 表示文件结束的错误:

package io

var EOF = errors.New("EOF")

也通过以下命令查看详细文档:

$ go doc io.EOF
var EOF = errors.New("EOF")

EOF is the error returned by Read when no more input is available. Functions
should return EOF only to signal a graceful end of input. If the EOF occurs
unexpectedly in a structured data stream, the appropriate error is either
ErrUnexpectedEOF or some other error giving more detail.
$

io.EOF大约可以算是Go语言中最重要的错误变量了, 它用于表示输入流的结尾. 因为每个文件都有一个结尾, 所以io.EOF很多时候并不能算是一个错误, 它更重要的是一个表示输入流结束了.

2. io.EOF设计的缺陷

可惜标准库中的io.EOF的设计是有问题的. 首先EOF是End-Of-File的缩写, 根据Go语言的习惯大写字母缩写一般表示常量. 可惜io.EOF被错误地定义成了变量, 这导致了API权限的扩散. 而最小化API权限是任何一个模块或函数设计的最高要求. 通过最小化的权限, 可以尽早发现代码中不必要的错误.

比如Go语言一个重要的安全设计就是禁止隐式的类型转换. 因此这个设计我们就可以很容易发现程序的BUG. 此外Go语言禁止定义没有被使用到的局部变量(函数参数除外, 因此函数参数是函数接口的一个部分)和禁止导入没有用到的包都是最小化权限的最佳实践. 这些最小API权限的设计不仅仅改进了程序的质量, 也提高了编译工具的性能和输出的目标文件.

因为EOF被定义成一个变量, 这导致了该变量可能会被恶意改变. 下面的代码就是一种优雅的埋坑方式:

func init() {
    io.EOF = nil
}

这虽然是一个段子, 但是却真实地暴漏了EOF接口的设计缺陷: 它存在严重的安全隐患. 变量的类型似乎也在暗示用户可以放心地修改变量的值. 因此说EOF是一个不安全也不优雅的设计.

3. io.EOF改为常量

一个显然的改进思路是将io.EOF定义为常量. 但是因为EOF对应一个表示error接口类型, 而Go语言目前的常量语法并不支持定义常量类型的接口. 但是我们可以通过一些技巧绕过这个限制.

Go语言的常量有bool/int/float/string/nil这几种主要类型. 常量不仅仅不包含接口等复杂类型, 甚至连常量的数组或结构体都不支持! 不过常量有一个重要的扩展规则: 以bool/int/float/string/nil为基础类型定义的新类型也支持常量.

比如, 我们重新定义一个字符串类型, 它也可以支持常量的:

type MyString string

const name MyString = "chai2010"

这个例子中MyString是一个新定义的类型, 可以定义这种类型的常量, 因为它的底层的string类型是支持常量的.

那么io.EOF的底层类型是什么呢? EOF是通过errors.New(“EOF”)定义的, 下面是这个函数的实现:

package errors

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

因此io.EOF底层的类型是errors.errorString结构体. 而结构体类型是不支持定义常量的. 不过errors.errorString结构体中只有一个字符串类型, io.EOF对应的错误字符串正是”EOF”.

我们可以为EOF重新实现一个以字符串为底层类型的新错误类型:

package io

type errorString string

func (e errorString) Error() string {
    return string(e)
}

这个新的io.errorString实现了两个特性: 首先是满足了error接口; 其次它是基于string类型重新定义, 因此支持定义常量. 因此我们可以基于errorString重新将io.EOF定义为常量:

const EOF = errorString("EOF")

这样EOF就变成了编译时可以确定的常量类型, 常量的值依然是“EOF”字符串. 但是也带来了新的问题: EOF已经不再是一个接口类型, 它会破坏旧代码的兼容性吗?

4. EOF常量到error接口的隐式转换

重新将EOF从error类型的变量改定义为errorString类型的常量并不会带来兼容问题!

首先io.EOF虽然被定义为变量, 但是从语义角度看它其实是常量, 换言之我们只会读取这个值. 其次读取到io.EOF之后, 我们是将其作为error接口类型使用, 唯一的用处是和用户返回的错误进行相等性比较.

比如有以下的代码:

func Foo(r io.Reader) {
    var p []byte
    if _, err := r.Read(p); err != io.EOF {
        // ...
    }
}

这里和io.EOF进行比较的err变量必然是error类型, 或者是满足error接口的其他类型. 如果err是接口类型, 那么将io.EOF换成errorString(“EOF”)常量也是可以工作的:

func Foo(r io.Reader) {
    var p []byte
    if _, err := r.Read(p); err != errorString("EOF") {
        // ...
    }
}

这是因为Go语言中一个普通类型的值在和接口类型的值进行比较运算时, 会被隐式转会为接口类型(开这个后门的原因时为了方便接口代码的编写). 或则说在进行比较的时刻, errorString(“EOF”)已经被替换成error(errorString(“EOF”)).

普通类型到接口的隐式转会虽然方便, 但是也带来了很多坑. 比如以下的例子:

func Foo() error {
    var p *SomeError = nil
    return p
}

以上代码的nil其实是*SomeError(nil). 而if err != nil 中的nil其实是error(nil).

而定义为常量的io.EOF常量在和error接口类型的值比较时, io.EOF常量会被转化为对应的接口类型. 这样新的io.EOF错误常量就可以和以前的代码无缝兼容了.

5. 总结

普通类型到接口类型的隐式转换、常量的默认类型和基础类型是Go语言中比较隐晦的特性, 很多人虽然在使用这些规则但是并没有意识到它们的细节. 本文从分析io.EOF设计缺陷为起点, 讨论了将常量用于接口值定义的一种思路.

《WebAssembly 标准入门》开始预售了,欢迎关注!

2018-12-07 08:00:00

WebAssembly 是一种新兴的网页虚拟机标准,它的设计目标包括高可移植性、高安全性、高效率(包括载入效率和运行效率)、尽可能小的程序体积。本书详尽介绍了 WebAssembly 程序在 JavaScript 环境下的使用方法、WebAssembly 汇编语言和二进制格式,给出了大量简单易懂的示例,同时以 C/C++和 Go 语言开发环境为例,介绍了如何使用其他高级语言开发 WebAssembly 模块。

某一天,有朋友向我推荐了一项新技术——WebAssembly。我认为这是一项值得关注的技术。

说WebAssembly是一门编程语言,但它更像一个编译器。实际上它是一个虚拟机,包含了一门低级汇编语言和对应的虚拟机体系结构,而WebAssembly这个名字从字面理解就说明了一切——Web的汇编语言。它的优点是文件小、加载快、执行效率非常高,可以实现更复杂的逻辑。

其实,我觉得出现这样的技术并不令人意外,而只是顺应了潮流,App的封闭系统必然会被新一代Web OS取代。但现有的Web开发技术,如JavaScript,前端执行效率和解决各种复杂问题的能力还不足,而WebAssembly的编译执行功能恰恰能弥补这些不足。WebAssembly标准是在谋智(Mozilla)、谷歌(Google)、微软(Microsoft)、苹果(Apple)等各大厂商的大力推进下诞生的,目前包括Chrome、Firefox、Safari、Opera、Edge在内的大部分主流浏览器均已支持WebAssembly。这使得WebAssembly前景非常好。

WebAssembly是Web前端技术,具有很强的可移植性,技术的潜在受益者不局限于传统的前端开发人员,随着技术的推进,越来越多的其他语言的开发者也将从中受益。如果开发者愿意,他们可以使用C/C++、Go、Rust、Kotlin、C#等开发语言来写代码,然后编译为WebAssembly,并在Web上执行,这是不是很酷?它能让我们很容易将用其他编程语言编写的程序移植到Web上,对于企业级应用和工业级应用都是巨大利好。

WebAssembly的应用场景也相当丰富,如Google Earth,2017年10月Google Earth开始在Firefox上运行,其中的关键就是使用了WebAssembly;再如网页游戏,WebAssembly能让HTML5游戏引擎速度大幅提高,国内一家公司使用WebAssembly后引擎效率提高了300%。

WebAssembly作为一种新兴的技术,为开发者提供了一种崭新的思路和工作方式,未来是很有可能大放光彩的,不过目前其相关的资料和社区还不够丰富,尽管已经有一些社区开始出现了相关技术文章,CSDN上也有较多的文章,但像本书这样全面系统地介绍WebAssembly技术的还不多,甚至没有。本书的两位作者都是有10多年经验的一线开发者,他们从WebAssembly概念诞生之初就开始密切关注该技术的发展,其中柴树杉是Emscripten(WebAssembly的技术前身之一)的首批实践者,丁尔男是国内首批工程化使用WebAssembly的开发者。

2018年7月,WebAssembly社区工作组发布了WebAssembly 1.0标准。现在,我在第一时间就向国内开发者介绍和推荐本书,是希望开发者能迅速地了解和学习新技术,探索新技术的价值。

——蒋涛 CSDN创始人、总裁,极客帮创始合伙人

Go语言实现WebDAV文件系统

2018-10-24 08:00:00

WebDAV (Web-based Distributed Authoring and Versioning) 是一种基于 HTTP 1.1协议的通信协议。它扩展了HTTP 1.1,在GET、POST、HEAD等几个HTTP标准方法以外添加了一些新的方法,使应用程序可对Web Server直接读写,并支持写文件锁定(Locking)及解锁(Unlock),还可以支持文件的版本控制。

使用WebDAV可以完成的工作包括:

目前常见的NAS都提供WebDAV服务功能,很多手机应用也是通过WebDAV协议来实现应用间的文件共享。要提供自己的WebDAV服务首先要安装相应的软件。macOS下可以从App Store中安装免费的WebDAVNav Server软件。WebDAVNav Server服务启动界面如下:

本节我们尝试用Go语言实现自己的WebDAV服务。

WebDAV对HTTP的扩展

WebDAV扩展了HTTP/1.1协议。它定义了新的HTTP标头,客户机可以通过这些新标头传递WebDAV特有的资源请求。这些标头为:

同时,WebDAV标准还引入了若干新HTTP方法,用于告知启用了WebDAV的服务器如何处理请求。这些方法是对现有方法(例如 GET、PUT和DELETE)的补充,可用来执行WebDAV事务。下面是这些新HTTP方法的介绍:

最简的WebDAV服务

Go语言扩展包 golang.org/x/net/webdav 提供了WebDAV服务的支持。其中webdav.Handler实现了http.Handle接口,用处理WebDAV特有的http请求。要构造webdav.Handler对象的话,我们至少需要指定一个文件系统和锁服务。其中webdav.Dir将本地的文件系统映射为WebDAV的文件系统,webdav.NewMemLS则是基于本机内存构造一个锁服务。

下面是最简单的WebDAV服务实现:

package main

import (
	"net/http"

	"golang.org/x/net/webdav"
)

func main() {
	http.ListenAndServe(":8080", &webdav.Handler{
		FileSystem: webdav.Dir("."),
		LockSystem: webdav.NewMemLS(),
	})
}

运行之后,当前目录就可以通过WebDAV方式访问了。

只读的WebDAV服务

前面实现的WebDAV服务默认不需要任何密码就可以访问文件系统,任何匿名的用户可以添加、修改、删除文件,这对于网络服务来说太不安全了。

为了防止被用户无意或恶意修改,我们可以关闭WebDAV的修改功能。参考WebDAV协议规范可知,修改相关的操作主要涉及PUT/DELETE/PROPPATCH/MKCOL/COPY/MOVE等几个方法。我们只要将这几个方法屏蔽了就可以实现一个只读的WebDAV服务。

func main() {
	fs := &webdav.Handler{
		FileSystem: webdav.Dir("."),
		LockSystem: webdav.NewMemLS(),
	}

	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		switch req.Method {
		case "PUT", "DELETE", "PROPPATCH", "MKCOL", "COPY", "MOVE":
			http.Error(w, "WebDAV: Read Only!!!", http.StatusForbidden)
			return
		}

		fs.ServeHTTP(w, req)
	})

	http.ListenAndServe(":8080", nil)
}

我们通过http.HandleFunc重新包装了fs.ServeHTTP方法,然后将和更新相关的操作屏蔽掉。这样我们就实现了一个只读的WebDAV服务。

密码认证WebDAV服务

WebDAV是基于HTTP协议扩展的标准,我们可以通过HTTP的基本认证机制设置用户名和密码。

func main() {
	fs := &webdav.Handler{
		FileSystem: webdav.Dir("."),
		LockSystem: webdav.NewMemLS(),
	}

	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		// 获取用户名/密码
		username, password, ok := req.BasicAuth()
		if !ok {
			w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		// 验证用户名/密码
		if username != "user" || password != "123456" {
			http.Error(w, "WebDAV: need authorized!", http.StatusUnauthorized)
			return
		}

		fs.ServeHTTP(w, req)
	})

	http.ListenAndServe(":8080", nil)
}

我们通过req.BasicAuth来获取用户名和密码,然后进行验证。如果没有设置用户名和密码,则返回一个http.StatusUnauthorized状态,HTTP客户端会弹出让用户输入密码的窗口。

由于HTTP协议并没有加密,因此用户名和密码也是明文传输。为了更安全,我们可以选择用HTTPS协议提供WebDAV服务。为此,我们需要准备一个证书文件(crypto/tls包中的generate_cert.go程序可以生成证书),然后用http.ListenAndServeTLS来启动https服务。

同时需要注意的是,从Windows Vista起,微软就禁用了http形式的基本WebDAV验证形式(KB841215),默认必须使用https连接。可以在Windows Vista/7/8中,改注册表:

HKEY_LOCAL_MACHINE>>SYSTEM>>CurrentControlSet>>Services>>WebClient>>Parameters>>BasicAuthLevel

把这个值从1改为2,然后进控制面板/服务,把WebClient服务重启。

浏览器视图

WebDAV是基于HTTP协议,理论上从浏览器访问WebDAV服务器会更简单。但是,当我们在浏览器中访问WebDAV服务的根目录之后,收到了“Method Not Allowed”错误信息。

这是因为,根据WebDAV协议规范,http的GET方法只能用于获取文件。在Go语言实现的webdav库中,如果用GET访问一个目录,会返回一个http.StatusMethodNotAllowed状态码,对应“Method Not Allowed”错误信息。

为了支持浏览器删除目录列表,我们对针对目录的GET操作单独生成html页面:

func main() {
	fs := &webdav.Handler{
		FileSystem: webdav.Dir("."),
		LockSystem: webdav.NewMemLS(),
	}

	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		if req.Method == "GET" && handleDirList(fs.FileSystem, w, req) {
			return
		}

		fs.ServeHTTP(w, req)
	})

	http.ListenAndServe(":8080", nil)
}

其中,handleDirList函数用于处理目录列表,然后返回ture。handleDirList的实现如下:

func handleDirList(fs webdav.FileSystem, w http.ResponseWriter, req *http.Request) bool {
	ctx := context.Background()

	f, err := fs.OpenFile(ctx, req.URL.Path, os.O_RDONLY, 0)
	if err != nil {
		return false
	}
	defer f.Close()

	if fi, _ := f.Stat(); fi != nil && !fi.IsDir() {
		return false
	}

	dirs, err := f.Readdir(-1)
	if err != nil {
		log.Print(w, "Error reading directory", http.StatusInternalServerError)
		return false
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w, "<pre>\n")
	for _, d := range dirs {
		name := d.Name()
		if d.IsDir() {
			name += "/"
		}
		fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name)
	}
	fmt.Fprintf(w, "</pre>\n")
	return true
}

现在可以通过浏览器来访问WebDAV目录列表了。

实用的WebDAV服务

为了构造实用的WebDAV服务,我们通过命令行参数设置相关信息,同时将前面的功能整合起来。

package main

import (
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"

	"golang.org/x/net/context"
	"golang.org/x/net/webdav"
)

var (
	flagRootDir   = flag.String("dir", "", "webdav root dir")
	flagHttpAddr  = flag.String("http", ":80", "http or https address")
	flagHttpsMode = flag.Bool("https-mode", false, "use https mode")
	flagCertFile  = flag.String("https-cert-file", "cert.pem", "https cert file")
	flagKeyFile   = flag.String("https-key-file", "key.pem", "https key file")
	flagUserName  = flag.String("user", "", "user name")
	flagPassword  = flag.String("password", "", "user password")
	flagReadonly  = flag.Bool("read-only", false, "read only")
)

func init() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage of WebDAV Server\n")
		flag.PrintDefaults()
		fmt.Fprintf(os.Stderr, "\nReport bugs to <chaishushan{AT}gmail.com>.\n")
	}
}

func main() {
	flag.Parse()

	fs := &webdav.Handler{
		FileSystem: webdav.Dir(*flagRootDir),
		LockSystem: webdav.NewMemLS(),
	}

	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		if *flagUserName != "" && *flagPassword != "" {
			username, password, ok := req.BasicAuth()
			if !ok {
				w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
				w.WriteHeader(http.StatusUnauthorized)
				return
			}
			if username != *flagUserName || password != *flagPassword {
				http.Error(w, "WebDAV: need authorized!", http.StatusUnauthorized)
				return
			}
		}

		if req.Method == "GET" && handleDirList(fs.FileSystem, w, req) {
			return
		}

		if *flagReadonly {
			switch req.Method {
			case "PUT", "DELETE", "PROPPATCH", "MKCOL", "COPY", "MOVE":
				http.Error(w, "WebDAV: Read Only!!!", http.StatusForbidden)
				return
			}
		}

		fs.ServeHTTP(w, req)
	})

	if *flagHttpsMode {
		http.ListenAndServeTLS(*flagHttpAddr, *flagCertFile, *flagKeyFile, nil)
	} else {
		http.ListenAndServe(*flagHttpAddr, nil)
	}
}

func handleDirList(fs webdav.FileSystem, w http.ResponseWriter, req *http.Request) bool {
	// 参考前面的代码
}

显示帮助信息:

go run main.go -h
Usage of WebDAV Server
  -dir string
    	webdav root dir
  -http string
    	http or https address (default ":80")
  -https-cert-file string
    	https cert file (default "cert.pem")
  -https-key-file string
    	https key file (default "key.pem")
  -https-mode
    	use https mode
  -password string
    	user password
  -read-only
    	read only
  -user string
    	user name

Report bugs to <chaishushan{AT}gmail.com>.

以下命令以Https启动一个WebDAV服务,对应本机的Go语言安装目录,同时设置用户名和密码:

go run main.go -https-mode -user=user -password=123456 -dir=/usr/local/go

下面是在iPod上通过WebDANNav+应用通过WebDAV协议访问/usr/local/go的预览图: