
昨天我们刚刚发布了 Go+ v1.1.1 版本。这个版本最重要的一个变化是类文件(ClassFile)概念第一次正式写进 Go+ 的官方文档中。虽然此前在 Go+ 公开课中我们有几个演讲介绍了类文件(ClassFile),并称它是 Go+ 最重要的语法特性,没有之一。在前面《Go+ 两周年:我们的目标及当前进展》中谈及 Go+ 在低代码上的进展时,我们也着重谈到了类文件的重要性。但是关于它的正式文档少得可怜,而 Go+ 项目的官方文档中一直没有出现任何关于它的介绍文字。今天就让我们对它进行系统性的介绍。对类文件(ClassFile)最直白的解释是:用一个文件来定义一个类。例如假设我们有一个名为 Rect.gopx 的源文件,其内容如下:var ( width float64 height float64)
func Area() float64 { return width * height}
这是一个合法的 Go+ 源代码。它的常规含义是,定义了两个全局变量 width 和 height,并且提供了以这两个变量作为矩形边长求面积的 Area 函数。但是从类文件的角度(所以我们把源文件后缀改用 .gopx 而非常规的 .gop 以示区别),它定义了一个名为 Rect 的类,有两个成员变量 width 和 height,以及求矩形面积的成员方法 Area。它等价于:type Rect struct { width float64 height float64}
func (this *Rect) Area() float64 { return this.width * this.height}
对于大部分的专业程序员来说,这样的写法并不难理解。但是对于一个非专业程序员来说,这样的代码理解上还是有难度的。结合我自身教中小学生进行编程入门的经历来看,软件工程中的模块化功能从理解难度来说依次为:- 命令。它理解上最简单,比函数更为直观。这也是 Go+ 为什么坚定地引入命令式语法,并且把它作为主代码风格的根本原因。
- 函数。到初中阶段数学上开始引入函数的概念,和计算机的函数相互印证,理解上也并不算太困难(主要需要理解形参和实参的关系)。
- 类及成员方法。虽然对抽象世界有帮助,但是对初学者来说理解它是有一定门槛的。他们需要知道变量与成员变量的区别,成员变量的访问,以及如何用成员方法抽象业务。
类文件首先从语法上简化了他们对类的理解,因为类并没有引入任何新的语法,只是把文件后缀从 .gop 改为了 .gopx 而已。这可以快速消除他们对新知识的陌生感。但 Go+ 的类文件并不只是这样一个简单的语法糖。首先,类文件的后缀是可以用户自定义的。目前 Go+ 还并不支持 .gopx 后缀的类文件(以上的例子实际只是方便大家理解,当前还并不能编译通过,但是我计划到 v1.2 版本正式发布时将会支持它,以方便大家可以更加循序渐进地理解类文件),需要你主动告诉它你的类文件后缀是什么。那么为什么要搞得这么复杂,统一的 .gopx 文件后缀不香么?答案是:我们希望不同的类文件后缀,代表了不同的专业领域(Specific Domain)。相同专业领域的多个类文件,他们可以有相同的基类,甚至是相同的程序框架。也就是说,我们希望通过类文件连接两类人:一类是领域专家(Domain Expert),他们通常对这个领域将如何变得高效,未来将如何演进非常了解。一类是领域创作者(Domain Creator),他们创作更多的作品来满足人们的需求。我们希望让领域专家(Domain Expert)定义一种新的类文件,而领域创作者(Domain Creator)通过应用这些类文件来创作。这还不够。一个专业领域可能需要多种类文件相互协作才能完成。我们以游戏开发领域为例,它至少需要两类文件:游戏工程文件(Game 类)和游戏角色文件(Sprite 类)。为此我们把类文件分为两类:一类叫工程类(Proj),一类叫工作类(Work)。一般而言,任何特定领域的工程都应该只有一个工程类,且为单例(比如游戏中的 Game 类),但一般都会有一种或若干种工作类(只有工程类而工作类没有也是可能的)。当前类文件在 Go+ 中还处于 Beta 状态,这意味着在 v1.2 版本中类文件的实现机制上我们还可能会有微调。对类文件的实现方(Domain Expert)来说,可能会有少量的代码需要变更。但是我们会保证从类文件的使用方角度而言保持向前兼容,他们的代码无需调整。首先,一个 Go+ 的包还只能引用一种领域工程(未来会放开,多种领域工程可以在同一个包中一起使用)。当然这也不算什么大问题,当前我们可以把不同领域工程的代码放在不同包,然后连接这两个包来实现。另外,一个领域工程当前只支持一种工作类(基本上够用了,暂时还没有遇到现实中的工程需要多种工作类),或者没有工作类。这一点未来也会放开,一种领域工程支持同时有多种工作类。这样介绍太抽象了。我们拿一个 DEMO 来感受一下类文件可以达到的效果。这是一个相对简单的领域工程,它的类文件只有工程类(Proj),没有工作类(Work)。工程类以 .gsh 作为文件后缀。首先,我们创建一个 gsh 的样例工程(假设取名为 gshexample):mkdir gshexamplecd gshexamplegop mod init gshexample
这样我们就初始化了一个名为 gshexample 的 Go+ 工程。然后我们下载 gsh 到 gshexample 工程:gop get github.com/xushiwei/gsh
这样 gsh 这个领域工程就可以被 gshexample 使用了。我们试着创建一个最简单的 ./example.gsh 文件,内容如下:你可能觉得它很像 Shell 脚本,但是它的确是不折不扣的 Go+ 代码。我们尝试运行它:gop mod tidy # 在第一次 gop run 前运行gop run .
检查当前目录下是否创建了 testgsh 文件。如果有,那么恭喜你:你已经会用 gsh 了。未来我们可能会探索进一步简化类文件的使用门槛。例如对于 gsh 来说,一种更轻便的使用姿势是:# 将 .gsh 注册到 Go+ 全局,而不是某个具体的工程gop class github.com/xushiwei/gsh
接着创建 ./example.gsh,然后直接 gop run ./example.gsh 来运行它。当然,上面这个例子只是为了跑通流程。它的代码过于简单,可能你会疑惑是否值得动用类文件。的确只是简单的 mkdir 当然并不需要基于类文件,通过全局函数就可以做到一样的效果。我们试着把它改复杂一些:type file struct { name string fsize int}
mkdir! "testgsh"
mkdir "testgsh2"lastErr!
mkdir "testgsh3"if lastErr != nil { panic lastErr}
capout => { ls }println output.fields
capout => { ls "-l" }files := [file{flds[8], flds[4].int!} for e <- output.split("\n") if flds := e.fields; flds.len > 2]println files
rmdir "testgsh", "testgsh2", "testgsh3"
完整的样例代码,我们放到了 github.com/xushiwei/gshexample 中,方便大家可以去下载并进行尝试。在 Shell 编程中,一般会有检查上一次命令执行的错误(last error)这样的方式。在 gsh 中我们也支持它。这意味着检查错误有以下三种典型的方式:mkdir! "testsh" # will panic if mkdir failed
命令执行的结果会被保存在 lastErr 中。所以第二种检查错误的方式是:mkdir "testsh"if lastErr != nil { panic lastErr}
你可能会将 mkdir 和 lastErr 理解为全局函数和全局变量。但实际上它们并不是。Go+ 作为贯彻工程思想低门槛化的语言,我们一方面会尽可能工程化(比如杜绝全局变量的使用),另一方面又要降低人们理解上的心智负担。这是类文件另一个功效:在低门槛和工程化之间做平衡。事实上基于类文件你很难写出全局变量遍地都是的代码。在 Shell 编程中,我们还经常会通过 `...` 来捕获命令的输出。在 gsh 中我们引入了类似的功能:这里 capout 参数是一个闭包,在闭包中执行的所有命令(但不包括普通 print 输出到 stdout 的内容)的标准输出都会被记录并保存到 output 中。capout => { ls "-l" }println output
total 72-rw-r--r-- 1 xushiwei staff 11357 Jun 19 00:20 LICENSE-rw-r--r-- 1 xushiwei staff 127 Jun 19 10:00 README.md-rw-r--r-- 1 xushiwei staff 365 Jun 19 00:25 example.gsh-rw-r--r-- 1 xushiwei staff 126 Jun 19 09:33 go.mod-rw-r--r-- 1 xushiwei staff 165 Jun 19 09:33 go.sum-rw-r--r-- 1 xushiwei staff 110 Jun 19 09:33 gop.mod-rw-r--r-- 1 xushiwei staff 1938 Jun 19 10:00 gop_autogen.go
当然,既然有了 output 的内容,我们就很容易把它和 Go+ 强大的数据处理能力结合起来。比如说我们对这个输出结果结构化,得到一个 []file 列表,file 里面包含文件名和文件大小:type file struct { name string fsize int}
files := [file{flds[8], flds[4].int!} for e <- output.split("\n") if flds := e.fields; flds.len > 2]
这个代码有点长,看起来不容易理解,我们对它进行变换:for e <- output.split("\n") if flds := e.fields; flds.len > 2 { println file{flds[8], flds[4].int!}}
它和上面的列表解析(ListComprehension)唯一的不同是,列表解析把感兴趣的内容收集到列表中,而这里把它打印出来。首先,我们通过 output.split("\n") 将输出结果分解为多行并进行遍历,对于每一行 e,通过 e.fields 按空白字符分割,赋值给 flds。如果 flds 的长度不大于 2,那么说明它不是正常的文件信息行,而是特殊行:把它过滤掉,然后将 flds[8] 赋给文件名,flds[4] 转为整型(这里 int! 和前面 mkdir! 一样,是错误处理,如果 flds[4] 不是一个整数,它就会报错以方便我们排查)赋给文件大小,并最后打印出来。通过以上 DevOps 工具箱的例子,我们可以看到,类文件可以很方便地包装出领域友好的使用界面,通过 gsh 你可以获得非常接近 Shell 编程的体验,但是它又不是以牺牲工程质量为代价的。这就是 Go+ 关于专业领域(Specific Domain)的设计哲学:Don't define a language for specific domain.
Abstract domain knowledge for it.
不要为某个专业领域去设计 DSL(领域专用语言),我们应该为它抽象领域知识。
Go+ doesn't support DSL,
but it'
s SDF (Specific Domain Friendly).
大部分人都不自觉地在为了便捷性而放弃了开放性(可连接性),但这其实是非常不划算的一件事情。Go+ 是一门在 Go 的基础上,进一步将连接与组合进行到底的语言。我们在意的是不同工种、不同职业间的连接。我们希望通过类文件连接领域专家(Domain Expert)与领域创作者(Domain Creator)。我们还希望通过类文件可以组合不同的专业领域,让他们可以自然连接,而不是像一般的 DSL 做的那样,把自己变成一座座孤岛。基于此,Go+ 才能成为第一个面向全民编程而设计的编程语言。