我最近一直在找一门理想中的静态编译语言。看过 Rust,也体验过 Crystal 和 Go。从语法角度来看,Crystal 完全照搬 Ruby 的格式让我十分喜欢。但 Go 的热度又让我觉得这会是一个更加务实的选择。至于 Rust,语法过于怪异,还有点难以接受。所以我决定在 Crystal 和 Go 之间做出选择。
从 Crystal 到 Go
Crystal 作为我的心意之选,在试用的过程中碰到了一些难以解决的问题。比如在我目前的 Arch Linux 上做静态编译时就无法成功,提示找不到一些 libssl,libpcre 之类的组件库。但实际上这些库都已经安装了。尝试了各种办法都无法解决后,我决定还是忍痛先放弃这门语言,等待它继续变的成熟和完善一些。
务实的 Go 成了我目前唯一的选择。
为什么使用 Docker
关于这一点,我在之前介绍 PHP 开发环境搭建的文章《使用 Docker 的容器化 PHP 开发环境实践》中有过解释。主要就是以下三点:
1. 可以保持系统软件环境的纯净。这一点对于喜欢折腾各种工具软件,然后把系统依赖环境搞的一团糟的我来说,特别受用。
2. 开发环境和当前使用系统不再强依赖。在接触 Docker 之前,我的主力开发设备是一台 MBP,每年出了新系统后都会有一段要重新编译各种开发工具的阵痛,因为系统环境和依赖组件的版本都有变化了。
在最新的系统上解决各种软件依赖对我来说是一项很痛苦的工作。而 Docker 很好的解决了这个问题。无论系统再怎么升级,都不会再影响到我的开发环境。即便是我的开发环境已经从 macOS 系统换成了 Linux 系统,开发环境毫无大碍。
3. 开发软件的管理方式更加统一。各种编程语言都有各自的安装流程和步骤,各种应用服务的安装和配置方式也千差万别。通过 Docker,从一个更高的维度抽象和统一了这些差异。不论是 MySQL,还是 Redis,我都只需要拉镜像,映射端口,然后启动容器就行了
对于 Go 来说,我同样希望能通过 Docker 来支撑完整的开发流程。
搭建流程
作为一个刚接触 Go 语言的新人,我将一步一步的把这次折腾过程都事无巨细的记录下来。先从 Docker 镜像开始。
测试镜像
使用 Docker 来构建开发环境的第一步就是先找镜像。在 Docker hub 上就有 Go 语言的官方镜像:golang,先用下面的命令测试一下这个镜像。
对不熟悉 Docker 的朋友简单解释下上面这条命令:
goalpine--rm
go version
这条命令在我的系统上执行结果如下:
命令包装
通过这个镜像,本身已经足以支撑 Go 的学习和使用了。不过每次使用都要敲上这么一长串 Docker 命令,有点不太合适。可以用 Shell 命令来精简一下。
找个位置创建一个 Shell 文件,保存为 go,内容如下:
然后给它可执行权限:
现在就可以通过执行这个 Shell 脚本来运行 Go 命令了:
go
$PATH
我有一个目录专门用来存放一些自定义脚本,所以选择了第二种方案。
go
看看现在的使用效果:
还能看出是通过 Docker 执行的么?
执行 Go 代码
上面的 go 命令虽然可以执行了,不过还不能运行 go 代码文件。因为还没提供目录挂载功能。把这儿自定义的 Shell 脚本完善一下,内容如下:
添加了一些新的参数,简单解释一下:
-v$PWD/srv/app-w-v
到了这步,这个基于 Docker 的 Go 运行环境才算是真正可用了。创建一个简单的 Go 代码测试一下。
在任何目录下创建一个 hello.go 文件,代码如下:
go run hello.go
使用镜像
在国内选择任何一门编程语言时,一个首要的考量就是包管理工具有没有提供镜像加速功能。Crystal 目前就没有这个功能,这也是我暂时放弃它的一方面。
Go 可以通过环境变量的方式来配置镜像,Docker 也支持设置容器运行时的环境变量。再次完善一下自定义的 go Shell 脚本,目前它的内容如下:
-e
用 Go 语言的 Web 开发框架 Gin 来测试一下配置镜像参数后的效果:
执行结果如下:
33 秒就完成了软件包的下载,我对这个速度很满意。没添加镜像配置前根本就下载不下来。
Go mod 功能完善
为了方便的使用软件包,Go mod 功能必然是不可少的。上面我们已经添加了镜像加速功能,并且测试了可以正常下载,但还没测试是否能正常使用下载的软件包。
还是以 Web 框架 Gin 为例。把 hello.go 文件的代码换成如下的内容:
因为要使用 mod 功能,所以先在 hello.go 文件目录下生成一个 go.mod 文件:
命令执行完成后,看下当前目录,会发现多了一个 go.mod 文件。不过权限貌似有点问题:
生成的 go.mod 是 root 权限,这有点不太合适。
调整一下 go Shell 脚本,改成如下的内容:
-u
处理完权限问题,继续 mod 包的添加流程,再次执行获取 Gin 包的命令:
虽然包下载成功,不过注意一下最后一行,貌似一个缓存用途的目录因为权限问题创建失败。
这估计是刚才调整了运行用户导致的。但我不可能改回 root,这不是一个正常项目下应该出现的用户。
通过一番搜索和了解,大致找到了原因和解决方案。简单来说,这是只有采用 Docker ,并以普通用户方式运行容器里的 go 命令时才会碰到的一个问题。解决方法就是再次完善一下自定义的 go Shell 脚本,调整后的脚本内容如下:
XDG_CACHE_HOME
再次运行获取 Gin 软件包的命令,这回正常了。命令执行完成后,当前目录下会多了一个 go.sum 文件。这是软件包的校验文件。有了这个文件后,就能正常执行刚刚修改后的 hello.go 代码了:
执行结果如下:
从最后结尾的输出信息就能看出,通过 mod 安装的包起作用了。程序开始监听 8080 端口,根据代码中的定义,如果访问 localhost:8080/ping 这个地址,应该能得到包含 Pong 字符串的 Json 返回数据。
不过这个地址大概率是无法访问的。因为我想起来自定义的 Shell 脚本中,没有启用端口映射,所以还无法直接访问这个容器里面运行的 Go 程序。除此之外,我还发现两个问题:
go mod getCtrl + c
所以继续调整自定义的 Go Shell 脚本,调整后的内容如下:
针对上面的三个问题添加了三处配置:
-itCtrl + c-v $HOME/go:/go
编译
编译是 Go 语言最具特色的一个环节。少了这个环节,Go 语言的魅力对我来说就荡然无存了。上面自定义的 go Shell 脚本虽然也能执行 build 命令,但我发现编译后的执行文件无法在容器外执行。
这是因为我使用了 alpine 标签的 go 语言 Docker 镜像。要解决这个问题有两个方案:
- 采用和但前开发系统一致的 Docker 镜像。
- 使用 Go 语言的交叉编译功能。
gobuild
CGO_ENABLEDGOOSGOARCHbuildgo
上面 hello.go 的代码,我通过如下命令来执行编译:
通过此命令再次编译后生成的可执行文件,就能正常在容器外执行了。
我还尝试把这个文件上传到了一台线上服务器上,结果也能正常执行。
一个编译好的二进制包,服务器上不用再安装任何代码运行环境,丢上去就能运行。如果搭配微服务,是不是很性感?
这就是静态编译语言的魅力。
总结
Go 对我来说还是一块处女地,所以这份搭建流程的记录也忠实反应了我一步一步尝试过程中碰到的问题和解决的思路。我之所以记录下其中的细节,就是希望以此能给一些刚接触编程的朋友们带来一些思路上的帮助。
对于编程来说,什么技术还没学会这不重要。重要的是解决问题的思路和能力。所以希望这篇文章能给一些朋友带来启发。
最后,对于刚接触编程的新人朋友,我不建议你们采用我当前这种完全基于 Docker 的开发环境搭建方式。原因就是这篇洋洋洒洒几千字的文章,以及碰到并解决的各种问题,其实就等效于 Arch Linux 下这样一行命令: