探索Go语言脚本编程:从必要性到实现方案的全面解析
[id_1746100473]

译者丨金灵杰
本文介绍了我尝试用 Go 语言进行脚本编程的经历。文中会讨论 Go 脚本的必要性,以及我们预期它的表现和可能的实现方式。在讨论时,会深入探讨脚本、Shell 和 Shebang。最终,会讨论让 Go 脚本工作的解决方案。
1 为什么 Go 语言适合编写脚本?
一般认为,Python 以及 Bash 属于热门的脚本语言。而 C、C++和 Java 完全不能够被用于脚本编程。不过,有一些语言处于两者之间。
Go 语言的试用场景较为广泛。它可以用于编写 Web 服务器,也可以用于流程管理。甚至还有人将其用作系统编程语言。在后文中,我会进行论证,除了前面提到的这些场景之外,Go 语言还能够简单地用来编写脚本。
是什么让 Go 语言适合编写脚本?
[id_1742682444]
事实上,我能够使用 Go 语言来编写脚本。要做到这一点,需要用到 Go 的 run 子命令。假如脚本的名称是 my-script.go,那么我们可以通过 go run my-script.go 这种简单的方式来运行它。
这里,对于“go run”命令,我觉得需要予以特别的关注。接下来让我们详细地进行说明。
Go 语言与 Bash 和 Python 有所不同。Bash 和 Python 是通过解释来执行的,也就是说它们的脚本在被读取的时候就会执行。而对于 Go 语言,当用户输入 go run 后,Go 会对这个 Go 程序进行编译,然后再执行。由于 Go 的编译时间非常短,所以给人的感觉就像是在进行解释执行。值得提醒的是,很多人都说“go run 仅仅是一个玩意儿”。然而,如果我们需要脚本,并且还喜欢 Go 语言,那么这个玩意儿就正是我们所想要的。
3 所以已经支持的很好了,对吧?
我们能够编写脚本,并且可以借助 go run 命令去执行它。那还有什么疑问呢?疑问在于我比较懒,期望能够通过类似./my-script.go 这样的方式来运行脚本,而非 go run my-script.go。
这里我们讨论一个简单的脚本和 Shell 的交互方式:其一,从命令行获取输入数据;其二,设置退出状态码。这两种方式并非所有可能的交互方式(还有环境变量、信号、标准输入、标准输出、标准错误等),但却是 Shell 脚本中较困难的两种。
这个脚本会输出“Hello”,同时也会输出从命令行获取到的第一个参数,并且会将退出状态码设置为 42。
package mainimport ("fmt""os")func main() {fmt.Println("Hello", os.Args[1])os.Exit(42)}
这时,使用 go run 命令结果有些奇怪:
$ go run example.go worldHello worldexit status 42$ echo $?1
这个问题我们稍后会讨论。
这时候能够运用 go build 命令。这种通过 go build 命令来执行该脚本的方式如下:
$ go build$ ./example worldHello world$ echo $?42
此时调试该脚本的流程变成了:
$ vim ./example.go$ go build$ ./example.go worldHi world$ vim ./example.go$ go build$ ./example.go worldBye world
而我期望达到的是这样来运行脚本:
$ chmod +x example.go$ ./example.go worldHello world$ echo $?42
而对应的工作流程是:
$ vim ./example.go$ ./example.go worldHi world$ vim ./example.go$ ./example.go worldBye world
看上去很简单是吧?
4 Shebang
类 UNIX 系统支持 Shebang 这一机制。Shebang 的作用是告知 Shell 该使用何种解释器来运行脚本。我们能够依据编写脚本所使用的语言去设置 Shebang 行。
通常,我们会将 env 命令当作脚本执行器来使用,这样就不用再使用解释器的绝对路径了。比如:能够设置 Shebang 为 #! /usr/bin/env python,以便让 Python 解释器运行该脚本。当一个脚本名为 example.py 且带有上述的 Shebang 行,并且它具备可执行属性(能够通过 chmod +x example.py 命令来添加)时,在 Shell 中可以输入./example.py arg1 arg2 来使其运行。此时,Shell 会读取 Shebang 行,接着便会引发链式反应。
Shell 开始执行 /usr/bin/env python example.py arg1 arg2 这个操作。这实际上意味着 Shebang 行以及脚本名和额外的参数共同构成了这一行为。该命令会去执行 /usr/bin/env,其参数为 /usr/bin/env python example.py arg1 arg2。首先,env 命令会调用 python 命令,接着执行 python example.py arg1 arg2。之后,python 开始运行 example.py 脚本,其参数为 example.py arg1 arg2。
让我们开始尝试给 Go 脚本添加 Shebang。
第一次幼稚的尝试
我们先设置一个较为简单的 Shebang 以便使用 go run 来执行这个脚本。加了 Shebang 之后的脚本呈现出这样的形式:
#! /usr/bin/env go runpackage mainimport ("fmt""os")func main() {fmt.Println("Hello", os.Args[1])os.Exit(42)}
然后尝试运行一下,输出为:
$ ./example.go/usr/bin/env: ‘go run’: No such file or directory
发生了什么?
Shebang 机制把 go run 当作一个整体当作 env 命令的一个参数了,然而实际上并没有这个命令。当输入 which "go run" 时也会出现类似的错误。
第二次尝试
一个可行的方案是把 Shebang 设置为 #! /usr/local/go/bin/go run。在我们进行尝试之前,可能会发现一个问题,那就是 go 的二进制文件在不同的系统中路径是不一样的,若将绝对路径写死,就会致使脚本无法兼容安装在其他位置的 go。另外一个解决方案是创建一个别名,通过使用 alias gorun="go run" 来实现。之后可以将 Shebang 修改为 #! /usr/bin/env gorun。采用这种方式的话,我们需要在运行该脚本的各个系统中都设置这个别名。
输出:
$ ./example.gopackage main:example.go:1:1: illegal character U+0023 '#'
从这个输出能看出,存在一个好消息和一个坏消息,你想先听哪一个呢?我先来告知好消息哦:-)
解决方案
当脚本中不存在 Shebang 行的时候,不同的 Shell 会退回到不同的解析器。Bash 会运用自身来运行脚本,而 zsh 会退回到使用 sh。这给我们给予了一种解决办法,这也是在 StackOverflow 上被提及的一种解决办法。
Go 语言中定义了注释,我们可以用 //usr/bin/env 去替代 /usr/bin/env(因为在路径分割符中,// 等同于 /),所以第一行能够设置为:
//usr/bin/env go run "$0" "$@"
结果:
$ ./example.go worldHi worldexit status 42./test.go: line 2: package: command not found./test.go: line 4在未预料到的标记附近出现了语法错误。`newline'./test.go: line 4: `import ('$ echo $?2
解释:
我们离成功更近了一步,因为终于有了正确的输出。然而,输出中存在一些错误,并且状态码也不正确。让我们来看看究竟发生了什么。就像之前讲的那样,Bash 没有找到任何 Shebang,所以选择用 bash./example.go world 的方式来运行脚本(直接使用该命令会有相同的输出,你可以尝试一下)。很有意思,能够直接用 Bash 来运行 Go 文件呢。下一步,Bash 会读取脚本的第一行,接着运行这个命令:/usr/bin/env go run./example.go world。之前脚本里的“0”指的是第一个参数,所以实际值就是我们运行的脚本文件名。“@”则表示命令行中的所有参数。在这个例子中会被替换为“world”。到目前为止,使用./example.go 以及“world”,脚本使用了恰当的命令行参数,并且输出了正确的值。
输出中存在着很诡异的一行,即“exit status 42”。这究竟是什么呢?倘若我们自己亲自去尝试一下该命令,就能明白其中的缘由了。
$ go run ./example.go worldHello worldexit status 42$ echo $?1
go run 命令将状态码屏蔽后返回了 1,此情况通过标准错误输出,关于该行为的讨论可参见 Github issue。
那么其他几行输出的情况如何呢?Bash 尝试去解析 Go 源码,然而实际上它失败了。
解决方案优化
这个 StackOverflow 页面提出在 Shebang 之后添加 ;exit “$?”。这一做法会告知 Bash 解释器不要再继续进行执行操作。
完整的 Shebang:
//usr/bin/env go run "$0" "$@"; exit "$?"
结果:
$ ./test.go worldHi worldexit status 42$ echo $?1
这里实现了让 Bash 能够使用 go run 命令来执行脚本,执行完毕后立即退出,并且会将状态码设置为 go run 命令执行后的状态码。
可以在 Shebang 行中添加一些命令,这些命令能移除标准错误里的“退出状态”内容,还能解析该文本并将其作为整个脚本的返回码。
然而:
5 幸运的是,我们有 gorun
我们想要的是 gorun。我们要做的只是在 Shebang 中写上 #! /usr/bin/env gorun,然后赋予脚本可执行权限。仅需这些操作,我们就能在 Shell 中执行,从而获得期望的结果。
$ ./example.go worldHello world$ echo $?42
太棒了!
警告:兼容性
文件包含 Shebang 之后,Go 就无法进行编译了(就如同我们之前所看到的那样)。
$ go run example.gopackage main:example.go:1:1: illegal character U+0023 '#'
这两种选择不能兼得,我们只能二选一:
二者不可兼得!
另外一个问题在于,若将脚本文件放置于 Go 工程内,编译器便能察觉到这个 go 文件。即便该文件并非应用程序所必需,依然会致使编译失败。一种解决办法是去掉.go 后缀,然而如此一来,就无法运用诸如 go fmt 之类的工具了。
6 最后一些想法
本文探讨了运用 Go 语言编写脚本的重要性,并且介绍了实现脚本运行的几种方式。以下是一些总结。
解释:
如上表所示,目前不存在一种完美的解决方案。其中看起来最便捷且出现问题最少的办法是运用 go run 命令。然而在我看来,这种方式显得过于“复杂”,并且无法实现“可执行”,同时其退出状态码也不正确。这会致使难以分辨脚本是否被正确执行。
因此,我觉得 Go 语言在该领域还有诸多工作需去完成。我不觉得让语言去支持忽略 Shebang 行有什么不妥之处。这样能解决执行方面的问题,然而类似这样的变化或许不会被 Go 社区所采纳。
我的同事提醒我,Shebang 行对于 Javascript 实际上也是非法的。然而在 Node.js 里,他们增添了一个能够跳过 Shebang 的函数,使得 Node 脚本能够在 Shell 中直接运行。由于原文时间较为久远,在 c2b01881dcb3bf302f9d83157e719cc5240a9042 这个版本之后,Node.js 对源码进行了重构。在 702331be906fe58e0ef66c7b31c7d2aeb3af3421 这个版本之后,原文中提及的 stripShebang 函数被移除了。)
如果 gorun 能够成为标准工具的一部分,那将会非常棒。此外,还有 gofmt 和 godoc 等类似的工具。
原文链接:
点个在看少个 bug