用 Golang 来编写 cli 程序吧,Happy~
原文链接 https://biezhi.github.io/2017/11/build-cli-with-golang.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
背景
我是个 Java 开发者,做过非常多开源软件,经常会有在终端下提供命令行帮助程序的这种小需求,一般大家实现这个需求也就这么几种办法。
- 编写批处理或者 Shell(Windows 和 Linux需要写两次)
- 使用编程语言解决(golang、python都是不错的跨平台选择)
程序员都是懒人,我才不要写两次呢~ 很早之前也用Python写过类似的程序,但是打包出来的结果比较大,另一方面 Go 语言越来越火,也是我比较喜欢的一门编程语言,而且支持跨平台,所以就选用它了。
在这篇小文中我将教你编写一个可以查看本地天气的小程序,比较简单,你可以通过学习这篇文章做出自己中意的小工具。
<!-- more -->
完整的代码可以在我的 Github{:target="_blank"} 查看。
安装环境
我们在开始之前先要准备 Go 语言的环境,如果你已经安装过了这步可以略过。你可以在 这里{:target="_blank"} 下载到最新版的 Go 语言版本,如果你的网络环境被迫是下面这个样子。
你可以在 Golang中国{:target="_blank"} 下载最新发布包。Go 语言环境的安装方式也有好几种,我们选择最简单的方式:标准包安装。
Mac 环境下下载以 .pkg
结尾的包文件,在 Windows 下下载 .msi
结尾的文件,下载好后傻瓜式安装即可。
注意你的操作系统架构不要选错,Linux源码安装这里不讲啦。
配置环境变量
学习很多编程语言都需要配置环境变量,安装软件的时候其实也有部分程序静默的帮我们做了这件事,在前面我们安装了 Go 语言,下面我们了解下 Go 语言中的环境变量以及如何配置。
GOROOT
:Go 的安装路径GOPATH
:告诉 Go 命令和其他相关工具,在那里去找到安装在你系统上的 Go 包。
那么我们创建一个工作目录来存储自己编写的源码包吧~
在 Windows 下假设是 D:/go
这个目录,Linux/MacOSX 下假设是 ~/workspace/golang
这个目录。我目前是 Mac 系统,就按照这个设置环境变量了。
- 加入环境变量
export GOROOT=/usr/local/go
- 加入环境变量
export GOPATH=/Users/biezhi/workspace/golang
- 修改系统环境变量
export PATH=$PATH:$GOPATH/bin
测试一下
# biezhi in ~
» go version
go version go1.8.3 darwin/amd64
大功告成,接下来的内容需要你具备一种编程语言的基础,否则无法食用。
和Java语言的一些区别
这里我们说几个不同之处,无法涵盖到所有,满足本文的需求。
声明变量、常量
Java 中
private String name = "biezhi";
public static final String VERSION = "0.2.1";
Golang 中
name := "biezhi"
const version = "0.2.1"
矮油,很简洁哦~
类和对象
Java 中
public class Config {
private String key;
private String value;
// getter and setter
}
// 使用
Config config = new Config();
config.setKey("name");
config.setValue("biezhi");
Golang 中
type Config struct {
key string
value string
}
// 使用
conf := Config{key: "name", value: "biezhi"}
矮油,又 tm 简洁了。。。
golang中没有class关键字,却引入了type,golang中更强调类型。
函数返回值
Java 中
public String getFileName(){
return "不可描述.jpg";
}
这里如果需要返回多个值,需要用类或者 Map 类型替换。
Golang 中
func getFileNmae() (string, error) {
return "可以描述.jpg", nil
}
Go 语言天生支持多返回值(毕竟后起之秀,社会社会)
Java 中关闭流的操作一般会写这样的代码
try {
in.balabala~
} catch(Exception e) {
// 处理异常
} finally {
in.close();
}
在 Go 中没有 finally
,试试 defer
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
CopyFile
方法简单的实现了文件内容的拷贝功能,将源文件的内容拷贝到目标文件。乍一看还没什么问题,不过Golang 中的资源也是需要释放的,假如 os.Create
方法的调用出了错误,下面的语句会直接 return
,导致这两个打开的文件没有机会被释放。这个时候,defer
就可以派上用场了。
前面 BB
了这么多只是简单的让大家在脑海中过一下Go的代码都长什么样。下面开始我们的正式话题了,如果你对基础还是不了解可以去看基础部分的书籍、资料(找不到私信我)。
原生写法实现
我不会一上来就教你如何使用某个库,这很不负责,你应该清楚在没有库的时代人们是如何做的,当有了更方便的工具为我减轻了什么,这个过程中你可能会了解到自己没见过的API如何使用,长此以往才会在编程中做到灵活运用。
这里需要了解几个基础包:
fmt
:格式化包,实现了类似C语言printf和scanf的格式化I/Oencoding/json
:原生JSON解析包net/http
:发送http请求flag
:供了一系列解析命令行参数的功能接口io/ioutil
:IO处理
有这几个包后我们编写一个 Hello World
瞧瞧,我的项目名是 weather-cli
通过 flag.XxxVar()
方法将flag绑定到一个变量,该种方式返回值类型,如
package main
import (
"fmt"
"os"
)
func main() {
var city string
flag.StringVar(&city, "c", "上海", "城市中文名")
flag.Parse()
fmt.Println("城市是:", city)
}
运行一下试试
# biezhi in ~/workspace/golang/src/github.com/biezhi/weather-cli
» go build && ./weather-cli
城市是: 上海
» go build && ./weather-cli -c 北京
城市是: 北京
解析参数是比较简单的,在这个演示中我们加入两个参数,第一个是城市,第二个是显示哪天,具体代码如下:
func main() {
var city string
var day string
flag.StringVar(&city, "c", "上海", "城市中文名")
flag.StringVar(&day, "d", "今天", "可选: 今天, 昨天, 预测")
flag.Parse()
}
此时已经可以获取到终端输入的参数了,那么接下来该找个接口调用天气 API 了,我找了 这个{:target="_blank"} 免费的API接口进行调用。我们需要编写一个方法用于HTTP请求。
func Request(url string) (string, error) {
response, err := http.Get(url)
if err != nil {
return "", err
}
defer response.Body.Close()
body, _ := ioutil.ReadAll(response.Body)
return string(body), nil
}
果然比Java方便啊,这个函数很简单,输入一个 URL,返回响应的 Body 为字符串。我们将输入的城市传递进去即可,默认是 上海
。
var city string
var day string
flag.StringVar(&city, "c", "上海", "城市中文名")
flag.StringVar(&day, "d", "今天", "可选: 今天, 昨天, 预测")
flag.Parse()
var body, err = Request(apiUrl + city)
if err != nil {
fmt.Printf("err was %v", err)
return
}
然后我们需要定义一些 类型
来存储JSON,在Java语言中是Class。这个类型结构是怎样的根据API的返回结果来定义,我将它们单独写在 types.go
文件中。
// 响应
type Response struct {
Status int `json:"status"`
CityName string `json:"city"`
Data Data `json:"data"`
Date string `json:"date"`
Message string `json:"message"`
Count int `json:"count"`
}
// 响应数据
type Data struct {
ShiDu string `json:"shidu"`
Quality string `json:"quality"`
Ganmao string `json:"ganmao"`
Yesterday Day `json:"yesterday"`
Forecast []Day `json:"forecast"`
}
// 某一天的数据
type Day struct {
Date string `json:"date"`
Sunrise string `json:"sunrise"`
High string `json:"high"`
Low string `json:"low"`
Sunset string `json:"sunset"`
Aqi float32 `json:"aqi"`
Fx string `json:"fx"`
Fl string `json:"fl"`
Type string `json:"type"`
Notice string `json:"notice"`
}
类型定义好后就可以把HTTP请求得到的 JSON 解析为定义好的类型了。
var r Response
err = json.Unmarshal([]byte(body), &r)
if err != nil {
fmt.Printf("\nError message: %v", err)
}
if r.Status != 200 {
fmt.Printf("获取天气API出现错误, %s", r.Message)
return
}
这里使用了 Go 自带的JSON解析(哎,我大 Java 咋没有呢。。),最后我们将得到的数据输出出来就 Ok 了。
func Print(day string, r Response) {
fmt.Println("城市:", r.CityName)
if day == "今天" {
fmt.Println("湿度:", r.Data.ShiDu)
fmt.Println("空气质量:", r.Data.Quality)
fmt.Println("温馨提示:", r.Data.Ganmao)
} else if day == "昨天" {
fmt.Println("日期:", r.Data.Yesterday.Date)
fmt.Println("温度:", r.Data.Yesterday.Low, r.Data.Yesterday.High)
fmt.Println("风量:", r.Data.Yesterday.Fx, r.Data.Yesterday.Fl)
fmt.Println("天气:", r.Data.Yesterday.Type)
fmt.Println("温馨提示:", r.Data.Yesterday.Notice)
} else if day == "预测" {
fmt.Println("====================================")
for _, item := range r.Data.Forecast {
fmt.Println("日期:", item.Date)
fmt.Println("温度:", item.Low, item.High)
fmt.Println("风量:", item.Fx, item.Fl)
fmt.Println("天气:", item.Type)
fmt.Println("温馨提示:", item.Notice)
fmt.Println("====================================")
}
} else {
fmt.Println("大熊你是想刁难我胖虎吗?_?")
}
}
此时这个小玩意已经可以运行了,我们来试试吧
# biezhi in ~/workspace/golang/src/github.com/biezhi/weather-cli
» go build && ./weather-cli
城市: 上海
湿度: 72%
空气质量: 良
温馨提示: 极少数敏感人群应减少户外活动
» ./weather-cli -c 北京
城市: 北京
湿度: 78%
空气质量: 轻度污染
温馨提示: 儿童、老年人及心脏、呼吸系统疾病患者人群应减少长时间或高强度户外锻炼
使用第三方库实现
这里我们用一款业界流行的库 cli{:target="_blank"},这个家伙怎么使用呢?创建一个 cli_main.go
文件
package main
import (
"fmt"
"os"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "greet"
app.Usage = "fight the loneliness!"
app.Action = func(c *cli.Context) error {
fmt.Println("Hello friend!")
return nil
}
app.Run(os.Args)
}
这是官网给出的一个例子,运行一下试试
» go build cli_main.go && ./cli_main
Hello friend!
实现我们上面的小程序需要用到 flag
这个功能,通过库实现的代码如下
func main() {
app := cli.NewApp()
app.Name = "weather-cli"
app.Usage = "天气预报小程序"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "city, c",
Value: "上海",
Usage: "城市中文名",
},
cli.StringFlag{
Name: "day, d",
Value: "今天",
Usage: "可选: 今天, 昨天, 预测",
},
}
app.Action = func(c *cli.Context) error {
city := c.String("city")
day := c.String("day")
var body, err = Request(apiUrl + city)
if err != nil {
fmt.Printf("err was %v", err)
return nil
}
var r Response
err = json.Unmarshal([]byte(body), &r)
if err != nil {
fmt.Printf("\nError message: %v", err)
return nil
}
if r.Status != 200 {
fmt.Printf("获取天气API出现错误, %s", r.Message)
return nil
}
Print(day, r)
return nil
}
app.Run(os.Args)
}
我直接给出了全部代码,就 40
多行完成了~ 运行一下
» go build -o weather-cli utils.go types.go cli_main.go && ./weather-cli
城市: 上海
湿度: 72%
空气质量: 良
温馨提示: 极少数敏感人群应减少户外活动
» go build -o weather-cli utils.go types.go cli_main.go && ./weather-cli --city 北京
城市: 北京
湿度: 78%
空气质量: 轻度污染
温馨提示: 儿童、老年人及心脏、呼吸系统疾病患者人群应减少长时间或高强度户外锻炼
下面我们学习如何将这个小程序打包成二进制在各个平台下使用,以及如何压缩二进制包让它变得更小!
打包和压缩
打包为各个操作系统的程序
Linux 64位
GOOS=linux GOARCH=amd64 go build ...
Windows 64位
GOOS=windows GOARCH=amd64 go build ...
MacOSX
GOOS=darwin GOARCH=amd64 go build ...
如果你尝试打包后,生成的二进制文件大小大约是 7.2M
左右,这个体积有点大了,我们可以使用一些技术让它占用更小。
首先加上编译参数 -ldflags
go build -ldflags '-w -s' -o weather-cli utils.go types.go cli_main.go
执行后发现程序只有 5.4M
了,已经变小了,但是对于我们而言还是有点大,我就写几十行代码没必要生成这么大吧,下面我们使用另外一个神器 upx,如果你没安装可以在它的官网下载安装。
go build -ldflags '-w -s' -o weather-cli utils.go types.go cli_main.go && upx ./weather-cli
来看看
» ll -la
total 8.4M
drwxr-xr-x 12 biezhi staff 384 Nov 1 18:44 .
drwxr-xr-x 20 biezhi staff 640 Nov 1 16:45 ..
drwxr-xr-x 12 biezhi staff 384 Nov 1 18:44 .git
-rw-r--r-- 1 biezhi staff 1.1K Sep 3 20:59 LICENSE
-rw-r--r-- 1 biezhi staff 710 Nov 1 18:32 README.md
-rwxr-xr-x 1 biezhi staff 5.4M Nov 1 18:41 cli_main
-rw-r--r-- 1 biezhi staff 905 Nov 1 18:18 cli_main.go
-rw-r--r-- 1 biezhi staff 609 Nov 1 18:19 main.go
-rw-r--r-- 1 biezhi staff 808 Nov 1 16:56 types.go
-rw-r--r-- 1 biezhi staff 1.4K Nov 1 18:19 utils.go
-rwxr-xr-x 1 biezhi staff 2.0M Nov 1 18:44 weather-cli
只有 2.0M
了,这已经差不多了,各位司机学习快乐~ 想看更多有趣的开发姿势可关注我的专栏 《王爵的技术小黑屋》{:target="_blank"} 或者留言给我。