Gox語言的語法基本脫胎於初始的Q語言(Qlang,現已改變路線,感謝原作者),下面是Q語言語法速覽供參考。
Q Language Manual (Q語言手冊)
運算符
基本上 Go 語言的操作符都支持,且操作符優先級相同。包括:
- '+'、'-'、'*'、'/'、'%'、'='
- ''、'&'、'&'、'|'、'<<'、'>>'
- '+='、'-='、'*='、'/='、'%='、'++'、'--'
- '='、'&='、'&='、'|='、'<<='、'>>='
- '!'、'>='、'<='、'>'、'<'、'=='、'!='、'&&'、'||'
- '<-' (chan操作符)
類型
原理上支持所有 Go 語言中的類型。典型有:
- 基本類型:int、float (在 Go 語言裏面是 float64)、string、byte、bool、var(在 Go 語言裏面是 interface{})。
- 複合類型:slice、map、chan
- 用戶自定義:函數(閉包)、類成員函數、類
常量
- 布爾類型:true、false(由 builtin 模塊支持)
- var 類型:nil(由 builtin 模塊支持)
- 浮點類型:pi、e、phi (由 math 模塊支持)
變量及初始化
基本類型
a = 1 // 創建一個 int 類型變量,並初始化爲 1
b = "hello" // string 類型
c = true // bool 類型
d = 1.0 // float 類型
e = 'h' // byte 類型
string 類型
a = "hello, world"
和 Go 語言類似,string 有如下內置的操作:
a = "hello" + "world" // + 爲字符串連接操作符
n = len(a) // 取 a 字符串的長度
b = a[1] // 取 a 字符串的某個字符,得到的 b 是 byte 類型
c = a[1:4] // 取子字符串
slice 類型
a = [1, 2, 3] // 創建一個 int slice,並初始化爲 [1, 2, 3]
b = [1, 2.3, 5] // 創建一個 float slice
c = ["a", "b", "c"] // 創建一個 string slice
d = ["a", 1, 2.3] // 創建一個 var slice (等價於 Go 語言的 []interface{})
e = make([]int, len, cap) // 創建一個 int slice,並將長度設置爲 len,容量設置爲 cap
f = make([][]int, len, cap) // 創建一個 []int 的 slice,並將長度設置爲 len,容量設置爲 cap
g = []byte{1, 2, 3} // 創建一個 byte slice,並初始化爲 [1, 2, 3]
h = []byte(nil) // 創建一個空 byte slice
和 Go 語言類似,slice 有如下內置的操作:
a = append(a, 4, 5, 6) // 含義與 Go 語言完全一致
n = len(a) // 取 a 的元素個數
m = cap(a) // 取 slice a 的容量
ncopy = copy(a, b) // 複製 b 的內容到 a,複製的長度 ncopy = min(len(a), len(b))
b1 = b[2] // 取 b 這個 slice 中 index=2 的元素
b[2] = 888 // 設置 b 這個 slice 中 index=2 的元素值爲 888
b[1], b[2], b[3] = 777, 888, 999 // 設置 b 這個 slice 中 index=1, 2, 3 的三個元素值
b2 = b[1:4] // 取子slice
特別地,在 qlang 中可以這樣賦值:
x, y, z = [1, 2, 3]
結果是 x = 1, y = 2, z = 3。
實際上,qlang 支持多返回值就是通過 slice 的多賦值完成:
- 對於那些返回了多個值的 Go 函數,在 qlang 會理解爲返回 var slice,也就是 []interface{}。
舉個例子:
f, err = os.Open(fname)
這個例子,在 Go 裏面返回的是 (*os.File, error)。但是 qlang 中是 var slice。
map 類型
a = {"a": 1, "b": 2, "c": 3} // 得到 map[string]int 類型的對象
b = {"a": 1, "b", 2.3, "c": 3} // 得到 map[string]float64 類型的對象
c = {1: "a", 2: "b", 3: "c"} // 得到 map[int]string 類型的對象
d = {"a": "hello", "b": 2.0, "c": true} // 得到 map[string]interface{} 類型的對象
e = make(map[string]int) // 創建一個空的 map[string]int 類型的對象
f = make(map[string]map[string]int) // 創建一個 map[string]map[string]int 類型的對象
g = map[string]int16{"a": 1, "b": 2} // 創建一個 map[string]int16 類型的對象
和 Go 語言類似,map 有如下內置的操作:
n = len(a) // 取 a 的元素個數
x = a["b"] // 取 a map 中 key 爲 "b" 的元素
x = a.b // 含義同上,但如果 "b" 元素不存在會 panic
a["e"], a["f"], a["g"] = 4, 5, 6 // 同 Go 語言
a.e, a.f, a.g = 4, 5, 6 // 含義同 a["e"], a["f"], a["g"] = 4, 5, 6
delete(a, "e") // 刪除 a map 中的 "e" 元素
需要注意的是,a["b"] 的行爲和 Go 語言中略有不同。在 Go 語言中,常見的範式是:
x := map[string]int{"a": 1, "b": 2}
a, ok := x["a"] // 結果:a = 1, ok = true
if ok { // 判斷a存在的邏輯
...
}
c, ok2 := x["c"] // 結果:c = 0, ok2 = false
d := x["d"] // 結果:d = 0
而在 qlang 中是這樣的:
x = {"a": 1, "b": 2}
a = x["a"] // 結果:a = 1
if a != undefined { // 判斷a存在的邏輯
...
}
c = x["c"] // 結果:c = undefined,注意不是0,也不是nil
d = x["d"] // 結果:d = undefined,注意不是0,也不是nil
chan 類型
ch1 = make(chan bool, 2) // 得到 buffer = 2 的 chan bool
ch2 = make(chan int) // 得到 buffer = 0 的 chan int
ch3 = make(chan map[string]int) // 得到 buffer = 0 的 chan map[string]int
和 Go 語言類似,chan 有如下內置的操作:
n = len(ch1) // 取得chan當前的元素個數
m = cap(ch1) // 取得chan的容量
ch1 <- true // 向chan發送一個值
v = <-ch1 // 從chan取出一個值
close(ch1) // 關閉chan,被關閉的chan是不能寫,但是還可以讀(直到已經寫入的值全部被取完爲止)
需要注意的是,在 chan 被關閉後,<-ch 取得 undefined 值。所以在 qlang 中應該這樣:
v = <-ch1
if v != undefined { // 判斷chan沒有被關閉的邏輯
...
}
流程控制
if 語句
if booleanExpr1 {
// ...
} elif booleanExpr2 {
// ...
} elif booleanExpr3 {
// ...
} else {
// ...
}
switch 語句
switch expr {
case expr1:
// ...
case expr2:
// ...
default:
// ...
}
或者:
switch {
case booleanExpr1:
// ...
case booleanExpr2:
// ...
default:
// ...
}
for 語句
for { // 無限循環,需要在中間 break 或 return 結束
...
}
for booleanExpr { // 類似很多語言的 while 循環
...
}
for initExpr; conditionExpr; stepExpr {
...
}
典型例子:
for i = 0; i < 10; i++ {
...
}
另外我們也支持 for..range 語法:
for range collectionExpr { // 其中 collectionExpr 可以是 slice, map 或 chan
...
}
for index = range collectionExpr {
...
}
for index, value = range collectionExpr {
...
}
函數
函數和閉包
基本語法:
funcName = fn(arg1, arg2, argN) {
//...
return expr
}
這就定義了一個名爲 funcName 的函數。
本質上來說,函數只是和 1、"hello" 類似的一個值,只是值的類型是函數類型。
所有的用戶自定義函數,在 Go 裏面實際類型均爲 func(args ...interface{}) interface{}。
你可以在一個函數中引用外層函數的變量。如:
x = fn(a) {
b = 1
y = fn(t) {
return b + t
}
return y(a)
}
println(x(3)) // 結果爲 4
但是如果你直接修改外層變量會報錯:
x = fn(a) {
b = 1
y = fn(t) {
b = t // 這裏會拋出異常,因爲不能確定你是想定義一個新的 b 變量,還是要修改外層 x 函數的 b 變量
}
y(a)
return b
}
如果你想修改外層變量,需要先引用它,如下:
x = fn(a) {
b = 1
y = fn(t) {
b; b = t // 現在正常了,我們知道你要修改外層的 b 變量
}
y(a)
return b
}
println(x(3)) // 輸出 3
不定參數
和 Go 語言類似,qlang 也支持不定參數的函數。例如內置的 max、min 都是不定參數的:
a = max(1.2, 3, 5, 6) // a 的值爲 float 類型的 6
b = max(1, 3, 5, 6) // b 的值爲 int 類型的 6
也可以自定義一個不定參數的函數,如:
x = fn(fmt, args...) {
printf(fmt, args...)
}
這樣就得到了一個 x 函數,功能和內建的 printf 函數一模一樣。
多賦值
形式上,qlang 和 Go 語言一樣支持多賦值,也支持函數返回多個值:
x, y, z = 1, 2, 3.5
a, b, c = fn() {
return 1, 2, 3.5 // 返回的是 var slice
}()
另外我們也支持:
x, y, z = [1, 2, 3.5]
a, b, c = fn() {
return [1, 2, 3.5] // 返回的是 float slice
}()
需要注意的是,帶上[]進行多賦值和不帶[]進行多賦值在語義上有一點點不同。下面是例子:
x1, y1, z1 = 1, 2, 3.5
x2, y2, z2 = [1, 2, 3.5]
println(type(x1), type(x2))
結果表明 x1 的類型爲 int,而 x2 的類型是 float。
defer
是的,qlang 也支持 defer。這在處理系統資源(如文件、鎖等)釋放場景非常有用。一個典型場景:
f, err = os.Open(fname)
if err != nil {
// 做些出錯處理
return
}
defer f.Close()
// 正常操作這個 f 文件
值得注意的是:
在一個細節上 qlang 的 defer 和 Go 語言處理並不一致,那就是 defer 表達式中的變量值。在 Go 語言中,所有 defer 引用的變量均在 defer 語句時刻固定下來(如上面的 f 變量),後面任何修改均不影響 defer 語句的行爲。但 qlang 是會受到影響的。例如,假設你在 defer 之後,調用 f = nil 把 f 變量改爲 nil,那麼後面執行 f.Close() 時就會 panic。
匿名函數
所謂匿名函數,是指:
fn {
... // 一段複雜代碼
}
它等價於:
fn() {
... // 一段複雜代碼
}()
以前在 defer 要執行一段很複雜的代碼段時,我們往往這樣寫:
defer fn() {
... // 一段複雜代碼
}()
有了匿名函數,我們可以簡寫爲:
defer fn {
... // 一段複雜代碼
}
類
一個用戶自定義類型的基本語法如下:
Foo = class {
fn SetAB(a, b) {
this.a, this.b = a, b
}
fn GetA() {
return this.a
}
}
有了這個 class Foo,我們就可以創建 Foo 類型的 object 了:
foo = new Foo
foo.SetAB(3, "hello")
a = foo.GetA()
println(a) // 輸出 3
構造函數
在 qlang 中,構造函數只是一個名爲 _init 的成員方法(method):
Foo = class {
fn _init(a, b) {
this.a, this.b = a, b
}
}
有了這個 class Foo 後,我們 new Foo 時就必須攜帶2個構造參數了:
foo = new Foo(3, "hello")
println(foo.a) // 輸出 3
goroutine
和 Go 語言一樣,qlang 中通過 go 關鍵字啓動一個新的 goroutine。如:
go println("this is a goroutine")
一個比較複雜的例子:
wg = sync.NewWaitGroup()
wg.Add(2)
go fn {
defer wg.Done()
println("in goroutine1")
}
go fn {
defer wg.Done()
println("in goroutine2")
}
wg.Wait()
這是一個經典的 goroutine 使用場景,把一個 task 分爲 2 個子 task,交給 2 個 goroutine 執行。
include
在 qlang 中,一個 .ql 文件可以通過 include 文法來將另一個 .ql 的內容包含進來。所謂包含,其實際的能力類似於將代碼拷貝粘貼過來。例如,在某個目錄下有 a.ql 和 b.ql 兩個文件。
其中 a.ql 內容如下:
println("in script A")
foo = fn() {
println("in func foo:", a, b)
}
其中 b.ql 內容如下:
a = 1
b = 2
include "a.ql"
println("in script B")
foo()
如果 include 語句的文件名不是以 .ql 爲後綴,那麼 qlang 會認爲這是一個目錄名,併爲其補上 "/main.ql" 後綴。也就是說:
include "foo/bar.v1"
等價於:
include "foo/bar.v1/main.ql"
模塊及 import
在 qlang 中,模塊(module)是一個目錄,該目錄下要求有一個名爲 main.ql 的文件。模塊中的標識(ident)默認都是私有的。想要導出一個標識(ident),需要用 export 語法。例如:
a = 1
b = 2
println("in script A")
f = fn() {
println("in func foo:", a, b)
}
export a, f
這個模塊導出了兩個標識(ident):整型變量 a 和 函數 f。
要引用這個模塊,我們需要用 import 文法:
import "foo/bar.v1"
import "foo/bar.v1" as bar2
bar.a = 100 // 將 bar.a 值設置爲 100
println(bar.a, bar2.a) // bar.a, bar2.a 的值現在都是 100
bar.f()
qlang 會在環境變量 QLANG_PATH
指示的目錄列表中查找 foo/bar.v1/main.ql
文件。如個沒有設置環境變量 QLANG_PATH
,則會在 ~/qlang
目錄中查找。
將一個模塊 import 多次並不會出現什麼問題,事實上第二次導入不會發生什麼,只是增加了一個別名。
include vs. import
include 是拷貝粘貼,比較適合用於模塊內的內容組織。比如一個模塊比較複雜,全部寫在 main.ql 文件中過於冗長,則可以用 include 語句分解到多個文件中。include 不會通過 QLANG_PATH
來找文件,它永遠基於 __dir__
(即 include 代碼所在腳本的目錄) 來定位文件。
import 是模塊引用,適合用於作爲業務分解的主要方式。import 基於 QLANG_PATH
這個環境變量搜尋被引用的模塊,而不是基於 __dir__
。
與 Go 語言的互操作性
qlang 是一個嵌入式語言,它的定位是作爲 Go 語言應用的運行時嵌入腳本。
作爲 Go 語言的伴生語言,它與 Go 語言有極佳的互操作性。任何 Go 語言的函數,可以幾乎不做任何包裝就可以直接在 qlang 中使用。
這太爽了!
定製 qlang
除了 qlang 語言的 import 支持外,qlang 的 Go 語言開發包也支持 Go package 編寫 qlang 模塊。
qlang 採用微內核設計,大部分你看到的功能,都通過 Go package 形式編寫的 qlang 模塊提供。你可以按需定製 qlang。
你可以自由定製你想要的 qlang 的樣子。在沒有引入任何模塊的情況下,qlang 連最基本的 '+'、'-'、'*'、'/' 都做不了,因爲提供這個能力的是 builtin 包。
在前面“快速入門”給出的精簡版本基礎上,我們可以自由添加各種模塊,如:
import (
"qlang.io/lib/math"
"qlang.io/lib/strconv"
"qlang.io/lib/strings"
...
)
func main() {
qlang.Import("math", math.Exports)
qlang.Import("strconv", strconv.Exports)
qlang.Import("strings", strings.Exports)
...
}
這樣,在 qlang 中就可以用 math.sin, strconv.itoa 等函數了。
如果你嫌 math.sin 太長,還可以將 math 模塊作爲 builtin 功能導入。這隻需要略微修改下導入的文法:
qlang.Import("", math.Exports) // 如此,你就可以直接用 sin 而不是 math.sin 了
製作 qlang 模塊
製作 qlang 模塊的成本極其低廉。我們打開 qlang.io/lib/strings
看看它是什麼樣的:
package strings
import (
"strings"
)
var Exports = map[string]interface{}{
"Contains": strings.Contains,
"Index": strings.Index,
"IndexAny": strings.IndexAny,
"Join": strings.Join,
"Title": strings.Title,
"ToLower": strings.ToLower,
"ToTitle": strings.ToTitle,
"ToUpper": strings.ToUpper,
"Trim": strings.Trim,
"NewReader": strings.NewReader,
"NewReplacer": strings.NewReplacer,
...
}
值得注意的一個細節是,我們幾乎不需要對 Go 語言的 strings package 中的函數進行任何包裝,你只需要把這個函數加入到導出表(Exports)即可。你也無需包裝 Go 語言中的類,比如上面的我們導出了 strings.NewReplacer,但是我們不必去包裝 strings.Replacer 類。這個類的所有功能可以直接使用。如:
strings.NewReplacer("?", "!").Replace("hello, world???") // 得到 "hello, world!!!"
這是 qlang 最強大的地方,近乎免包裝。甚至,你可以寫一個自動的 Go package 轉 qlang 模塊的工具,找到 Go package 所有導出的全局函數,加入到 Exports 表即完成了該 Go package 的包裝,幾乎零成本。
導出 Go 結構體
假設我們有 Go 結構體如下:
package foo
type Bar struct {
X int
Y string
}
func (p *Bar) GetX() int {
return p.X
}
我們一樣可以把它加入到 Export 表進行導出:
import (
qlang "qlang.io/spec"
)
var Exports = map[string]interface{}{
"Bar": qlang.StructOf((*foo.Bar)(nil)),
}
這樣你就可以在 qlang 裏面使用這個結構體:
bar = &foo.Bar{X: 1, Y: "hello"}
x = bar.GetX()
y = bar.Y
println(x, y)
反射
在任何時候,你都可以用 type 函數來查看一個變量的實際類型,結果在 Go 語言中是 reflect.Type。如:
t1 = type(1) // 相當於調用 Go 語言中的 reflect.TypeOf
用 type 可以很好地研究 qlang 的內在實現。比如:
t2 = type(fn() {})
我們得到了 *Function。這說明儘管用戶自定義的函數原型多樣,但是其 Go 類型是一致的。
我們也可以看看用戶自定義的類型:
Foo = class { fn f() {} }
t1 = type(Foo)
t2 = type(Foo.f)
foo = new Foo
t3 = type(foo)
t4 = type(foo.f)
可以看到,class Foo 的 Go 類型是 *Class,而 object foo 的 Go 類型是 *Object。而 Foo.f 和普通用戶自定義函數一致,也是 *Function,但 foo.f 不一樣,它是 *Method 類型。
附錄
樣例代碼
求最大素數
輸入 n,求 < n 的最大素數。用法:
qlang maxprime.ql <N>
primes = [2, 3]
n = 1
limit = 9
isPrime = fn(v) {
for i = 0; i < n; i++ {
if v % primes[i] == 0 {
return false
}
}
return true
}
listPrimes = fn(max) {
v = 5
for {
for v < limit {
if isPrime(v) {
primes = append(primes, v)
if v * v >= max {
return
}
}
v += 2
}
v += 2
n; n++
limit = primes[n] * primes[n]
}
}
maxPrimeOf = fn(max) {
if max % 2 == 0 {
max--
}
listPrimes(max)
n; n = len(primes)
for {
if isPrime(max) {
return max
}
max -= 2
}
}
// Usage: maxprime <Value>
//
if len(os.Args) < 2 {
fprintln(os.Stderr, "Usage: maxprime <Value>")
return
}
max, err = strconv.ParseInt(os.Args[1], 10, 64)
if err != nil {
fprintln(os.Stderr, err)
return 1
}
if max < 8 { // <8 的情況下,可直接建表,答案略
return
}
max--
v = maxPrimeOf(max)
println(v)
計算器
實現一個支持四則運算及函數調用的計算器:
grammar = `
term = factor *('*' factor/mul | '/' factor/quo | '%' factor/mod)
doc = term *('+' term/add | '-' term/sub)
factor =
FLOAT/pushFloat |
'-' factor/neg |
'(' doc ')' |
(IDENT '(' doc %= ','/ARITY ')')/call
`
fntable = nil
Stack = class {
fn _init() {
this.stk = []
}
fn clear() {
this.stk = this.stk[:0]
}
fn pop() {
n = len(this.stk)
if n > 0 {
v = this.stk[n-1]
this.stk = this.stk[:n-1]
return v, true
}
return nil, false
}
fn push(v) {
this.stk = append(this.stk, v)
}
fn popArgs(arity) {
n = len(this.stk)
if n < arity {
panic("Stack.popArgs: unexpected")
}
args = make([]var, arity)
copy(args, this.stk[n-arity:])
this.stk = this.stk[:n-arity]
return args
}
}
Calculator = class {
fn _init() {
this.stk = new Stack
}
fn grammar() {
return grammar
}
fn stack() {
return this.stk
}
fn fntable() {
return fntable
}
fn ret() {
v, _ = this.stk.pop()
this.stk.clear()
return v
}
fn call(name) {
f = fntable[name]
if f == undefined {
panic("function not found: " + name)
}
arity, _ = this.stk.pop()
args = this.stk.popArgs(arity)
ret = f(args...)
this.stk.push(ret)
}
}
fntable = {
"sin": sin,
"cos": cos,
"pow": pow,
"max": max,
"min": min,
"$mul": fn(a, b) { return a*b },
"$quo": fn(a, b) { return a/b },
"$mod": fn(a, b) { return a%b },
"$add": fn(a, b) { return a+b },
"$sub": fn(a, b) { return a-b },
"$neg": fn(a) { return -a },
"$call": Calculator.call,
"$pushFloat": Stack.push,
"$ARITY": Stack.push,
}
main { // 使用main關鍵字將主程序括起來,是爲了避免其中用的局部變量比如 err 對其他函數造成影響
calc = new Calculator
engine, err = interpreter(calc, nil)
if err != nil {
fprintln(os.Stderr, err)
return 1
}
scanner = bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line = strings.Trim(scanner.Text(), " \t\r\n")
if line != "" {
err = engine.Eval(line)
if err != nil {
fprintln(os.Stderr, err)
} else {
printf("> %v\n\n", calc.ret())
}
}
}
}