前言
使用“语言”这么一个敏感的描述余自知不妥,但这像极了余当初对于这个需求的理解和印象,君莫见怪。其实,本文描述内容的最终形态其实是用GOYACC实现了一个简单的解释器:
解释器(英语:Interpreter),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运
行。解释器不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,
因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停
地进行下去.--------------《百度百科》
为啥还要造轮子?现在不是有一大堆的脚本可以用吗?Lua毁天灭地,Python宇宙无敌不香吗?主要是因为好玩,其次是不需要我们的脚本是一门“编程语言”,诸如变量声明函数定义等等功能统统不需要,我就想安安静静的执行一串动作。
目标
本问内容的目标:实现一个程序,使得其能够通过一个文本文件输入来随意调用指定的几个函数,例如,我们拥有如下Golang函数:
func ZMoveTo(target float32) {
fmt.Println("ZMoveTo:", target)
time.Sleep(time.Second)
fmt.Println("Z done.")
}
func XMoveTo(target float32) {
fmt.Println("XMoveTo:", target)
time.Sleep(time.Second*3)
fmt.Println("X done.")
}
func YMoveTo(target float32) {
fmt.Println("YMoveTo:", target)
time.Sleep(time.Second)
fmt.Println("Y done.")
}
func Sleep(t int) {
fmt.Println("sleeping...")
time.Sleep(time.Duration(t)*time.Millisecond)
fmt.Println("sleep done.")
}
我们想要随意地控制XYZ的移动,并添加适当的延时,如果可以,我们甚至能够允许多个方向同时移动:
1.先Z方向运动到0;
2.之后X和Y方向同时分别运动到50和100;
3.延时500ms;
4.Z方向运动到45.
在Golang中,我们这样写:
ZMoveT(0);
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
XMoveTo(50)
}
go func() {
defer wg.Done()
YMoveTo(100)
}
wg.Wait()
time.Sleep(time.Duration(500)*time.Millisecond)
ZMoveT(45);
然后我们在某个地方嵌入这段代码,然后编译,最后得到二进制程序,执行后能够固定地运行我们的流程。这种方式做的事有如下特性:
1、仅Golang熟悉者能够自如地编写;
2、流程写死,需要改动流程必须要重新编写代码,不具备灵活性;
3、需要编译,需要重启程序,消耗时间。
那么,如果有一种简单的脚本来安排这些流程就好了!我们的目标不也挺简单的不是吗?不需要声明,不需要返回,只有纯粹的函数执行。其实有很多种思路来实现,举个绕远路的例子:把上述函数封装成http api,然后javascript调用。js和html做的UI配合,实现动作的灵活编排不是什么难事。先不说复杂度,实现这一想法需要javascript和html,我没掌握,连熟悉都谈不上。然后需要浏览器,没有UI的命令行界面玩不起。
所以下面介绍下我的几个实现。
一、用JSON实现
既然前面提到了javascript,那么我立马就想到了json。json是格式非常简单的文本类型,只有键值对,对json的解析也十分便利,语法要求还省去了我们的脚本校验过程。这是我的实现方法:
指令json样子如下:
{
"cmds":[
{
"cmd1":"ZMove",
"pos1": 0
},
{
"cmd1":"XMove",
"cmd2":"YMove",
"pos1": 50,
"pos2": 100,
},
...省略
]
}
实现原理:
JSON端:利用json的有序array达到编排动作顺序的目的;然后利用object的键值对传递动作名和其参数。
解析端:golang的struct可以直接解析json成员到自己的各个field上,极其便利地获取了动作名和相关
参数; 建立字符串类型的动作指令名与对应执行函数的映射:map, 其key为string,value为interface{},即对应函数本身。
说得不知道请不清楚,所以直接上带注释而且必能够直接运行的例子:
//输入指令
[
{
"Cmd": [
"XMoveTo"
],
"Param": {
"XMoveTo": 20
}
},
{
"Cmd": [
"XMoveTo",
"YMoveTo"
],
"Param": {
"XMoveTo": 50,
"YMoveTo": 70
}
},
{
"Cmd": [
"Sleep"
],
"Param": {
"Sleep": 1000
}
}
]
//主程序 main.go
package main
/*
实现1:json作为指令载体
实现方法:定义一个指令文件为json格式,内容为一个json array对象。array的元素为一条【语句】。
【语句】为一个对象,成员有:
Cmd:string array。显而易见地,使用array表示该条【语句】并发执行多个动作指令。
Param:object。成员为 string:value的键值对,key为Cmd的某条指令,value为其参数。
如,XMoveTo(20)可表示为:
{
"Cmd": [
"XMoveTo"
],
"Param": {
"XMoveTo": 20
}
}
而:
go func(){
XMoveTo(20)
YMoveTo(50)
}()
表示为:
{
"Cmd": [
"XMoveTo",
"YMoveTo"
],
"Param": {
"XMoveTo": 20,
"YMoveTo":50
}
}
*/
import (
"encoding/json"
"fmt"
"io/ioutil"
"play/simulatedFuncs"
"reflect"
"strconv"
"sync"
)
//定义指令结构
type InstructionType struct {
Cmd []string
Param map[string]interface{}
}
//json指令执行器
type JsonExecutor struct {
//预注册内置指令
instructions map[string]interface{}
//解析后的指令序列
parsed []InstructionType
}
//注册单条指令
func (j *JsonExecutor) Reg(action string, handler interface{}) {
if j.instructions == nil {
j.instructions = make(map[string]interface{})
}
j.instructions[action] = handler
}
//解析指令序列,就很简单了,unmarshal指令序列就行
func (j *JsonExecutor) Parse(jsn string) error {
bytesData, err := ioutil.ReadFile(jsn)
if err != nil {
return err
}
err = json.Unmarshal(bytesData, &j.parsed)
return err
}
//指令解析的序列
func (j *JsonExecutor) ExecuteAll() error {
//按照顺序,串行执行每一条命令,带上他们对应的参数
for i := 0; i < len(j.parsed); i++ {
tasks := j.parsed[i].Cmd
params := j.parsed[i].Param
//每一个指令都要以多任务方式看待,如此一来,兼容了并行任务和单条任务
wg := sync.WaitGroup{}
wg.Add(len(tasks))
for subTask := 0; subTask < len(tasks); subTask++ {
go func(action string, param interface{}) {
defer wg.Done()
res, err := j.execute(action, param)
if err != nil {
fmt.Println(err.Error())
return
}
for _, v := range res {
if !v.IsNil() {
fmt.Println(fmt.Sprintf("command executed error: %s", v))
return
}
}
}(tasks[subTask], params[tasks[subTask]])
}
//等待一次语句执行完毕
wg.Wait()
}
return nil
}
//执行一条语句
func (j *JsonExecutor) execute(action string, params ...interface{}) ([]reflect.Value, error) {
_, ok := j.instructions[action]
if !ok {
for k := range j.instructions {
fmt.Println(k)
}
return nil, fmt.Errorf("此指令不存在: %s", action)
}
return call(j.instructions, action, params)
}
//核心执行
//从指令集表中找出感兴趣的指令,并在将参数传递给它后执行之
func call(m map[string]interface{}, name string, params []interface{}) (result []reflect.Value, err error) {
//由调用者已经保证了该指令必定在指令集中存在
f := reflect.ValueOf(m[name])
//所需参数数量
var inNum = f.Type().NumIn()
in := make([]reflect.Value, inNum)
//判断函数需要参数的实际类型,并进行类型转换
for i := 0; i < inNum; i++ {
for i := 0; i < inNum; i++ {
var value reflect.Value
switch f.Type().In(i).Kind() {
case reflect.Float32:
fv, err := strconv.ParseFloat(fmt.Sprintf("%v", params[i]), 32)
if err != nil {
return nil, err
}
value = reflect.ValueOf(float32(fv))
case reflect.Float64:
fv, err := strconv.ParseFloat(fmt.Sprintf("%v", params[i]), 64)
if err != nil {
return nil, err
}
value = reflect.ValueOf(fv)
case reflect.Int:
fv, err := strconv.ParseInt(fmt.Sprintf("%v", params[i]), 10,32)
if err != nil {
return nil, err
}
value = reflect.ValueOf(int(fv))
case reflect.Uint16:
fv, err := strconv.ParseInt(fmt.Sprintf("%v", params[i]), 10, 16)
if err != nil {
return nil, err
}
value = reflect.ValueOf(uint16(fv))
case reflect.Uint32:
fv, err := strconv.ParseInt(fmt.Sprintf("%v", params[i]), 10, 32)
if err != nil {
return nil, err
}
value = reflect.ValueOf(uint32(fv))
default:
return nil, fmt.Errorf("不支持的参数类型:%v", f.Type().In(i).Kind())
}
in[i] = value
}
}
//调用函数
result = f.Call(in)
return
}
func main() {
je := JsonExecutor{}
//注册
je.Reg("ZMoveTo", simulatedFuncs.ZMoveTo)
je.Reg("XMoveTo", simulatedFuncs.XMoveTo)
je.Reg("YMoveTo", simulatedFuncs.YMoveTo)
je.Reg("Sleep", simulatedFuncs.Sleep)
//解析
err := je.Parse("./sample.json")
if err != nil {
panic(err)
}
//执行
err = je.ExecuteAll()
if err != nil {
panic(err)
}
}
最后总结一下,这个方式好处就是特别好实现,因为json解析太方便了。坏处就是json长了容易写歪,json有太多烦人的括号逗号。
二、手写解析
这种实现比较硬核,但好在需求并不复杂,就直接按照人的理解来做就行了。先看看我们理想中的指令长什么样:
#文件名:sample.sc
#this is a comment line注释示例
ZMoveTo:0
#上面是空行,下面是俩并发任务
-XMoveTo:30
-YMoveTo:20
Sleep:1000 #unit:ms
-ZMoveTo:40#尾巴注释
简而言之就是支持‘#’注释,忽略换行空白字符,指令和参数用':'分开,连续的'-'开头表示为需要将他们并发执行。这种方式的实现就比较麻烦了,毕竟要手写文本解析。但也还好,直接按照人的理解和思维做就行了,下面是实现:
package main
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"play/simulatedFuncs"
"reflect"
"strconv"
"strings"
"sync"
)
//以下进入正题
//解析器
type SCParser struct {
//注册后的内置指令集合
instructions map[string]interface{}
//解析后的指令
//解释一下,parsed是一个slice,代表每一个有序子动作,slice的元素成员map代表需要并行执行的无序集合,map的key为指令名,value为参数
parsed []map[string][]interface{}
}
//注册新的内置指令
func (scp *SCParser) Reg(command string, handler interface{}) {
if scp.instructions == nil {
scp.instructions = make(map[string]interface{})
}
scp.instructions[command] = handler
}
//Parse
//解析指令
func (scp *SCParser) Parse(reader io.Reader) error {
//从reader接口读取指令序列
scanner := bufio.NewScanner(reader)
//遇到了并发任务需要将他们进行收集
var concurrentGroup = make(map[string][]interface{})
//一行一行地扫描解析
for scanner.Scan() {
//扫描到的指令字符串值
rawContent := scanner.Text()
//把两头的空白字符去掉
content := rawContent
content = trimSpaceIndent(content)
//空行或者#开头的注释我们进行忽略
if content == "" || strings.Index(content,"#")==0 {
continue
}
//忽略行末的注释
split := strings.Split(content, "#")
//再次去除两端的空白字符,获取纯正的指令代码
content = trimSpaceIndent(split[0])
//分隔指令名与指令参数
splitFuncParam := strings.Split(content, ":")
//语法:
// 【指令名】:【指令参数】
if len(splitFuncParam) != 2 {
return fmt.Errorf("syntax error: unexpected number of separator ':', \nat '%s'", content)
}
funcName := splitFuncParam[0]
param := splitFuncParam[1]
//分割多个参数的值
paramSplit := strings.Split(param, ",")
//去除参数的空白字符
pureValue := make([]interface{}, len(paramSplit))
for k, v := range paramSplit {
pureValue[k] = trimAll(v)
}
//当遇到‘-’开头的连续几条指令时,理解他们为需要并发执行的几条指令,合并为一个语句,添加到并发map中
if strings.Index(funcName, "-") == 0 {
if _, ok := scp.instructions[funcName[1:]]; !ok {
return fmt.Errorf("this instruction is not registered:%s, %v", funcName[1:], scp.instructions)
}
concurrentGroup[funcName[1:]] = pureValue
continue
}else{
if _, ok := scp.instructions[funcName]; !ok {
return fmt.Errorf("this instruction is not registered:%s, %v", funcName, scp.instructions)
}
//if it comes to a none concurrent required instruction, first thing to do is to add the previous concurrent
//instructions to the executing list, then clear the concurrent group map, and finally, add the current non-
//concurrent instruction to the list.
//如果是一个非并发需求的指令,需要先把之前的并发map添加到语句集合中,然后清空并发mao,以便容纳新的并发指令;
//之后,把当前指令添加到语句任务中去。
if len(concurrentGroup) != 0 {
scp.parsed = append(scp.parsed, concurrentGroup)
concurrentGroup = make(map[string][]interface{})
}
scp.parsed = append(scp.parsed, map[string][]interface{}{
funcName: pureValue,
})
}
}
return nil
}
//执行解析的指令
func (scp *SCParser) ExecuteAll() error {
if len(scp.parsed) == 0 {
return nil
}
for _, v := range scp.parsed {
if len(v) > 0 {
//对并发任务进行控制
wg := sync.WaitGroup{}
wg.Add(len(v))
var errGroup = make(map[string]error)
for fName, params := range v {
if _, ok := scp.instructions[fName]; !ok {
return fmt.Errorf("instruction not defined:%s", fName)
}
go func(funcName string, params []interface{}) {
defer wg.Done()
res, err := scp.callFunc(scp.instructions[funcName], params)
if err != nil {
errGroup["call error"] = err
return
}
for _, r := range res {
if !r.IsNil() {
errGroup[funcName] = fmt.Errorf("%v", r)
break
}
}
}(fName, params)
}
wg.Wait()
for errDes, errGet := range errGroup {
if errGet != nil {
return fmt.Errorf(errDes + errGet.Error())
}
}
}else{//非并发语句任务
for fName, params := range v {
if _, ok := scp.instructions[fName]; !ok {
return fmt.Errorf("instruction not defined:%s", fName)
}
res, err := scp.callFunc(scp.instructions[fName], params)
if err != nil {
return err
}
for _, v := range res {
if !v.IsNil() {
return fmt.Errorf(fmt.Sprintf("%#v", v))
}
}
}
}
}
return nil
}
//核心功能,执行指令函数
func (scp *SCParser) callFunc(handler interface{}, params []interface{}) ([]reflect.Value, error) {
//使用反射获取函数
f := reflect.ValueOf(handler)
//获取函数的参数数量
var inNum = f.Type().NumIn()
//创建函数的输入参数
in := make([]reflect.Value, inNum)
//如果函数需要参数,我们就给他按需求一个一个解析出来
if inNum > 0 {
for i := 0; i < inNum; i++ {
var value reflect.Value
switch f.Type().In(i).Kind() {
case reflect.Float32:
fv, err := strconv.ParseFloat(fmt.Sprintf("%v", params[i]), 32)
if err != nil {
return nil, err
}
value = reflect.ValueOf(float32(fv))
case reflect.Float64:
fv, err := strconv.ParseFloat(fmt.Sprintf("%v", params[i]), 64)
if err != nil {
return nil, err
}
value = reflect.ValueOf(fv)
case reflect.Int:
fv, err := strconv.ParseInt(fmt.Sprintf("%v", params[i]), 10,32)
if err != nil {
return nil, err
}
value = reflect.ValueOf(int(fv))
case reflect.Uint16:
fv, err := strconv.ParseInt(fmt.Sprintf("%v", params[i]), 10, 16)
if err != nil {
return nil, err
}
value = reflect.ValueOf(uint16(fv))
case reflect.Uint32:
fv, err := strconv.ParseInt(fmt.Sprintf("%v", params[i]), 10, 32)
if err != nil {
return nil, err
}
value = reflect.ValueOf(uint32(fv))
default:
return nil, fmt.Errorf("不支持的参数类型:%v", f.Type().In(i).Kind())
}
in[i] = value
}
}
//使用参数并调用函数
result := f.Call(in)
return result, nil
}
func trimSpaceIndent(str string) string {
content := strings.Trim(str, " ")
content = strings.Trim(content, "\t")
return content
}
func trimAll(str string) string {
res := strings.Replace(str, " ", "", -1)
return strings.Replace(res, "\t", "", -1)
}
func main() {
wd, err := os.Getwd()
if err != nil {
panic(err)
}
f, err := os.Open(wd+string(filepath.Separator)+"sample.sc")
if err != nil {
panic(err)
}
scP := SCParser{}
scP.Reg("ZMoveTo", simulatedFuncs.ZMoveTo)
scP.Reg("XMoveTo", simulatedFuncs.XMoveTo)
scP.Reg("YMoveTo", simulatedFuncs.YMoveTo)
scP.Reg("Sleep", simulatedFuncs.Sleep)
err = scP.Parse(f)
if err != nil {
panic(err)
}
err = scP.ExecuteAll()
if err != nil {
panic(err)
}
}
总结一下这种实现,优点就是看起来和用起来一样,美的一批,格式约束非常少,使得编写流程和理解流程的人很轻松。只是,增加特性时解析就容易搞死人了。