腾讯亿级用户规模自研业务的上云实践解读,立即报名 了解详情
写点什么

一次使用 Go 语言编写脚本的经历

  • 2019-07-28
  • 本文字数:4436 字

    阅读完需:约 15 分钟

一次使用Go语言编写脚本的经历

本文介绍了我如何尝试使用 Go 语言进行脚本编程的经历。文中我将讨论 Go 脚本的必要性,我们预期的表现以及可能的实现方式。在讨论过程中,我讲深入探讨脚本、Shell 和 Shebang。最终,我们将会讨论让 Go 脚本工作的解决方案。


为什么 Go 语言适合编写脚本?

通常认为,Python 和 Bash 是热门的脚本语言,而 C、C++和 Java 完全不能被用作脚本编程,有一些语言却夹在其中。


Go 语言试用场景很多,从编写 Web 服务器到流程管理,甚至有些人用作系统编程语言。在后文中,我将论证,除了上述这些场景外,Go 语言还可以简单地用于编写脚本。


是什么让 Go 语言适合编写脚本?


  • Go 语言简洁易读,并且不太冗长。这使得编写的脚本易于维护且相对较短。

  • Go 语言有许多可用于各种用途的库。假设这些库是稳定且经过测试的,这可以让脚本简洁且健壮。

  • 如果我的大多数代码使用 Go 语言编写,那么我更倾向于使用 Go 作为我的脚本语言。当代码由许多人协作维护,那么使用一种大家都能完全掌控的语言会降低维护成本,即使是一些脚本。


Go 语言已经 99%支持脚本

事实上,我已经可以使用 Go 语言来编写脚本。这需要使用 Go 的 run 子命令:如果脚本名称是 my-script.go,我们可以简单的通过 go run my-script.go 来运行。


这里,对于 go run 命令,我认为需要特别关注一下。让我们详细说明下。


Go 语言区别于 Bash 和 Python 的地方是后者通过解释执行,既它们的脚本在读取的时候执行。而对于 Go 语言,当用户输入了 go run,Go 编译这个 Go 程序,然后再执行。因为 Go 编译时间非常短,所以看上去像是解释执行。值得提醒的是,很多人都说“go run 只是一个玩具”,但是如果我们需要脚本,同时也喜欢 Go 语言,那么这个玩具就是我们想要的。


所以已经支持的很好了,对吧?

我们可以编写脚本,并通过 go run 命令来执行。还有什么问题呢?问题是我很懒,希望通过类似./my-script.go 的方式来运行脚本,而不是 go run my-script.go。


这里我们讨论一个简单的脚本和 Shell 通过两种方式进行交互:它从命令行获取输入数据,并设置退出状态码。二者并非所有可能的交互方式(除此之外还可以有环境变量、信号、标准输入、标准输出、标准错误等),但是 Shell 脚本中较困难的两个。


这个脚本输出“Hello”和从命令行获取的第一个参数,并设置退出状态码为 42:


package main
import ( "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
复制代码


看上去很简答是吧?


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。


1 第一次幼稚的尝试

我们首先设置一个幼稚的 Shebang 来使用 go run 执行这个脚本。加了 Shebang 之后的脚本看上去是这样的:


#! /usr/bin/env go runpackage main
import ( "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"也会有类似的错误。


2 第二次尝试

一个可行的方案是将 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 '#'
复制代码


解释:


从这个输出来看,我们有一个好消息,同时也有一个坏消息,你想先听哪个?我先来说好消息:-)


  • 好消息是这个方案成功了,执行脚本之后 go run 命令正常调用了。

  • 坏消息:井号。在许多脚本语言中,Shebang 开头的井号会被当成注释忽略。但是对 Go 语言编译器来说,开头的井号变成了“非法字符”。


3 解决方案

当脚本不包含 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: syntax error near unexpected token `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。之前脚本中的“@”表示命令行中的所有参数。在这个例子中会被替换成“world”。到目前位置,使用./example.go world,脚本使用了正确的命令行参数,并输出了正确的值。


输出中还有诡异的一行:“exit status 42”。这是什么?如果我们自己尝试下命令就会了解:


$ go run ./example.go worldHello worldexit status 42$ echo $?1
复制代码


这是 go run 命令通过标准错误输出的。go run 命令屏蔽了状态码,然后返回了状态码 1。关于这个行为的讨论,可以参见Github issue


好了,那么其他几行输出呢?这是 Bash 试图解析 Go 源码,但实际失败了。


4 解决方案优化

这个 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 行中添加一些命令,用于移除标准错误中的“退出状态”内容,甚至解析该文本并作为整个脚本的返回码。


然而:


  • 再增加 Bash 命令意味着冗长的 Shebang 行,这与最初期望的 #! /usr/bin/env go 相比过于复杂。

  • 记住这只是一种 hack 的方式,而我并不喜欢 hack。毕竟我们只是想用标准的 Shebang 机制。为什么?因为这样简单、标准、优雅。

  • 这或多或少也是我想找一种更加方便的语言作为脚本语言(例如 Go)来替代 Bash 的原因。


幸运的是,我们有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 '#'
复制代码


这两种选择不能兼得,我们只能二选一:


  • 使用 Shebang,并通过./example.go 方式运行脚本。

  • 或者移除 Shebang,使用 go run ./example.go 运行脚本。


二者不可兼得!


另外一个问题,是当脚本文件被放在 Go 工程中时,编译器会发现这个 go 文件。虽然该文件并不是应用程序所需要的,也会导致编译失败。一个解决方案是移除.go 后缀,但是这样就会无法使用类似 go fmt 等工具。


最后一些想法

本文讨论了使用 Go 语言来编写脚本的重要性,同时介绍了几种方式来实现脚本运行。这里有一些总结。


类型退出状态码可执行可编译标准
go run
gorun
//解决方案


解释:


  • 类型:如何运行脚本。

  • 退出状态码:脚本执行后,是否设置了脚本的退出状态码。

  • 可执行:脚本是否可以通过 chmod +x 设置可执行权限。

  • 可编译:脚本是否可以通过 go build。

  • 标准:脚本是否需要标准库之外的东西。


正如上表,目前没有一种完美的解决方案。看上去最方便且问题最少的方式是使用 go run 命令。但是在我看来,这种方式太过“复杂”,而且无法“可执行”,同时退出状态码也不正确。这将会导致难以区分脚本是否正确执行。


因此,我认为 Go 语言在这个领域仍然有许多工作要做。我不认为让语言支持忽略 Shebang 行会有什么问题。这将会解决执行问题,但是类似这种变化可能不会被 Go 社区采纳。


我的同事提醒我事实上 Shebang 行对于 Javascript 同样也是非法的。但是在 Node.js 中,他们增加了一个跳过Shebang函数,让 Node 脚本可以在 Shell 中直接运行。(译者注:由于原文时间比较久远,在c2b01881dcb3bf302f9d83157e719cc5240a9042版本之后 Node.js 已经对源码进行了重构,在702331be906fe58e0ef66c7b31c7d2aeb3af3421版本之后,原文提及的 stripShebang 函数已经被移除。)


如果 gorun 可以作为标准工具的一部分就更棒了,其他类似的还有 gofmt 和 godoc。


原文链接:


https://posener.github.io/go-shebang-story/


作者 GitHub:


https://github.com/posener


2019-07-28 16:1111458

评论

发布
暂无评论
发现更多内容

力扣每日一练之二维数组上篇Day4

京与旧铺

6月月更

[数据分析实践]-文本分析-U.S. Patent Phrase-1

浩波的笔记

数据分析

测试开发【Mock平台】04实战:前后端项目初始化与登录鉴权实现

MegaQi

测试平台开发教程 测试干货 6月月更

Java Core 「8」字节码增强技术

Samson

学习笔记 Java core 6月月更

InfoQ 极客传媒 15 周年庆征文|漫谈公网网络延迟

耳东@Erdong

运维 6月月更 InfoQ极客传媒15周年庆 网络延迟

InfoQ 极客传媒 15 周年庆征文| 手把手带你入门 API 开发

宇宙之一粟

flask-restful 6月月更 InfoQ极客传媒15周年庆 API开发

几个非常有用的 Flutter 技巧,你可以立即使用!

坚果

6月月更

面试突击57:聚簇索引=主键索引吗?

王磊

Java MySQL 面试

纯CSS 毛玻璃效果 💎

德育处主任

CSS 纯CSS 6月月更

莫把功能当能力!从企业架构视角看警察在火锅店站岗

涛哥

企业架构

c语言选择,循环语句概述

秋名山码民

6月月更

中台的细节

卢卡多多

中台 6月月更

JVM调优简要思想及简单案例-JVM是什么?

zarmnosaj

6月月更

【协程】LifecycleScope源码解析

yechaoa

android 协程 6月月更 LifecycleScope

【愚公系列】2022年06月 通用职责分配原则(三)-低耦合原则

愚公搬代码

6月月更

spring4.1.8扩展实战之五:改变bean的定义(BeanFactoryPostProcessor接口)

程序员欣宸

Java spring Spring Framework 6月月更

读《Software Systems Architecture》(28)—— The Evolution Perspective

术子米德

架构师成长笔记

python停车时间计算,时分秒计算(split()函数)

写代码两年半

Python 6月月更

Paper Reading 预告 | Volcano-An Extensible and Parallel Query Evaluation System(众神推荐的 Paper 下载合集)

TiDB 社区干货传送门

TiDB Paper Reading

读《Software Systems Architecture》(26)—— The Performance and Scalability Perspective

术子米德

架构师成长笔记

远程办公-如何提高开会效率?| 社区征文

石云升

远程办公 开会 会议 6月月更 初夏征文

字符串

Jason199

js 字符串 6月月更

Linux开发_Makefile规则与Shell脚本语言

DS小龙哥

6月月更

读《Software Systems Architecture》(27)—— The Availability and Resilience Perspective

术子米德

架构师成长笔记

C#入门系列(十六) -- 类及其成员介绍

陈言必行

C# 6月月更

flutter系列之:Material中的3D组件Card

程序那些事

flutter 程序那些事 6月月更

测试流程如何落地?

老张

软件测试 质量保障

gogs使用webhook部署react单页应用

Nick

ci 持续集成 React 6月月更 gogs

CentOS环境基于nginx搭建负载均衡

乌龟哥哥

6月月更

实战 | Kibana面板使用

写程序的小王叔叔

Kibana ELK Stack 6月月更

助力直播平台源码脱颖而出,要完成哪些关键功能?

开源直播系统源码

软件开发 直播源码

一次使用Go语言编写脚本的经历_语言 & 开发_Eyal Posener_InfoQ精选文章