[Golang]使用Golang从零开始设计实现一个自定义脚本指令语言

前言

    使用“语言”这么一个敏感的描述余自知不妥,但这像极了余当初对于这个需求的理解和印象,君莫见怪。其实,本文描述内容的最终形态其实是用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)
	}
}

    总结一下这种实现,优点就是看起来和用起来一样,美的一批,格式约束非常少,使得编写流程和理解流程的人很轻松。只是,增加特性时解析就容易搞死人了。

 

三、GoYACC

    这个是个厉害而且有意思并且能做出有意思的事情的工具。急忙另写了一篇:GoYacc的简单使用

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值