InfoQ 研究中心诚意出品,一份报告带你走进中国 2000 万开发者 了解详情
写点什么

手把手教你用 Go 搭建一个 CLI 应用

  • 2020-08-15
  • 本文字数:4089 字

    阅读完需:约 13 分钟

手把手教你用Go搭建一个CLI应用

本文由 Eryb 发表在https://eryb.space,经原作者授权由 InfoQ 中文站翻译并分享。


不管你花了多少时间学习 Go 语法,甚至还逐一完成教程里的练习题,只有真正用 Go 搭建一个应用后,你才会对这门语言了然于心。


在这篇文章,我们会用 Go 搭建一个叫做 go-grab-xkcd 的命令行应用。这个应用的主要功能是从 XKCD 获取漫画,并且通过各种命令行参数给你提供更多的选择。


我们的的实现不会依赖于任何外部的库。这个应用将完全基于 Go 自带的标准库。


尽管这个应用看上去有点傻瓜,不过写这个应用的目的是获得一些编写生产级 Go 代码的经验,而不是让它被 Google 收购。


注意:本文主要的目标群众是那些对 Go 的语法和术语有所了解,并且水平介于初学者与较熟练人士之间的读者。


我们先跑一遍这个应用 , 然后看看它是如何工作的:


$ go-grab-xkcd --help
Usage of go-grab-xkcd: -n int Comic number to fetch (default latest) -o string Print output in format: text/json (default "text") -s Save image to current directory -t int Client timeout in seconds (default 30)
复制代码


$ go-grab-xkcd -n 323
Title: Ballmer PeakComic No: 323Date: 1-10-2007Description: Apple uses automated schnapps IVs.Image: https://imgs.xkcd.com/comics/ballmer_peak.png
复制代码


$ go-grab-xkcd -n 323 -o json {   "title": "Ballmer Peak",   "number": 323,   "date": "1-10-2007",   "description": "Apple uses automated schnapps IVs.",   "image": "https://imgs.xkcd.com/comics/ballmer_peak.png" } 
复制代码


你可以在电脑上下载并且运行这个程序 ,试试其他的选项。


当你跟着这篇文章做完这个项目后,你会对 Go 的如下方面更加得心应手:


  1. 接受命令行参数

  2. JSON 和 Go 的结构体之间的相互转化

  3. 调用 API 函数

  4. 创建文件(从互联网下载并且保存)

  5. 对字符串的操作


整个项目的结构如下所示:


$ tree go-grab-xkcd go-grab-xkcd ├── client │   └── xkcd.Go └── model     └── comic.Go ├── main.Go └── Go.mod 
复制代码


  • Go.mod - 用于包管理的 Go 模块文件

  • main.Go - 整个应用的起始点

  • comic.Go - 以 Go 为实现形式的数据结构和操作

  • xkcd.Go - 调用 HTTP API 函数,处理返回值并且存储到硬盘的 xkcd 的客户端


1. 初始化项目

通过如下命令创建 Go.mod 文件


$ go mod init 
复制代码


这有助于整个项目的管理(可以类比 JS 里的 package.json)


2. xkcd 的 API

xkcd 太棒了,你不需要任何注册信息或者访问密钥就能使用他们的 API。打开 xkcd 的 API 文档,然后你会发现两个端点:


  1. http://xkcd.com/info.0.json- GET 端点,用于获取最新的漫画

  2. http://xkcd.com/614/info.0.json- GET 端点,用于根据漫画编号获得特定的一个漫画


从这些端点返回的 JSON 结构如下所示:


{   "num": 2311,   "month": "5",   "day": "25",   "year": "2020",   "title": "Confidence Interval",   "alt": "The worst part is that's the millisigma interval.",   "img": "https://imgs.xkcd.com/comics/confidence_interval.png",   "safe_title": "Confidence Interval",   "link": "",   "news": "",   "transcript": "" } 
复制代码


点击这个链接查看更多与xkcd相关的内容


3. 为漫画创建模型

基于以上的 JSON 响应,我们可以从 model 包中的 comic.Go 里创建一个叫做 ComicResponse 的 struct( 结构体 ) 。


type ComicResponse struct {   Month      string `json:"month"`   Num        int    `json:"num"`   Link       string `json:"link"`   Year       string `json:"year"`   News       string `json:"news"`   SafeTitle  string `json:"safe_title"`   Transcript string `json:"transcript"`   Alt        string `json:"alt"`   Img        string `json:"img"`   Title      string `json:"title"`   Day        string `json:"day"` } 
复制代码


你可以用JSON-to-Go这个工具来自动从 JSON 里生成结构体。


除此之外,你需要创建另一个结构体用于从应用中输出数据。


type Comic struct {   Title       string `json:"title"`   Number      int    `json:"number"`   Date        string `json:"date"`   Description string `json:"description"`   Image       string `json:"image"` } 
复制代码


为 ComicResponse 结构体加入以下两种方法:


// FormattedDate将独立的数据格式化成一个独立的字符串 func (cr ComicResponse) FormattedDate() string {   return fmt.Sprintf("%s-%s-%s", cr.Day, cr.Month, cr.Year) } 
复制代码


// Comic 将我们从API中得到的ComicResponse转换成应用的输出格式Comic func (cr ComicResponse) Comic() Comic {   return Comic{     Title:       cr.Title,     Number:      cr.Num,     Date:        cr.FormattedDate(),     Description: cr.Alt,     Image:       cr.Img,   } } 
复制代码


然后再为 Comic 结构体加入如下两个方法:


// PrettyString 会为输出创建一个易读的Comic字符串 func (c Comic) PrettyString() string {   p := fmt.Sprintf(     "Title: %s\nComic No: %d\nDate: %s\nDescription: %s\nImage: %s\n",     c.Title, c.Number, c.Date, c.Description, c.Image)   return p } 
复制代码


// JSON 将Comid结构体转换成JSON。我们用JSON作为我们的输出 func (c Comic) JSON() string {   cJSON, err := json.Marshal(c)   if err != nil {     return ""   }   return string(cJSON) } 
复制代码


4. 设置 xkcd 客户端用于发送请求、解析响应以及写入硬盘

在 client 包中创建 xkcd.Go 文件


我们先定义一个类型叫做 ComicNumber,它的实际类型是 int。


type ComicNumber int 
复制代码


定义常数-


const (   // BaseURL of xkcd   BaseURL string = "https://xkcd.com"   // DefaultClientTimeout是取消之前需要等待的时间   DefaultClientTimeout time.Duration = 30 * time.Second   // LatestComic是从xkcd API中返 回 的最新的漫画编号   LatestComic ComicNumber = 0 ) 
复制代码


创建一个叫做 XKCDClient 的结构体,它会被用于向 API 发送请求


// XKCDClient 是 XKCD 的客户端 type XKCDClient struct {   client  *http.Client   baseURL string } 
复制代码


// NewXKCDClient 创建一个新的 XKCDClient func NewXKCDClient() *XKCDClient {   return &XKCDClient{     client: &http.Client{       Timeout: DefaultClientTimeout,     },     baseURL: BaseURL,   } } 
复制代码


向 XKCDClient 加入以下四个方法:


1.SetTimeout()


// SetTimeout 用于更改默认的 ClientTimeout func (hc *XKCDClient) SetTimeout(d time.Duration) {     hc.client.Timeout = d } 
复制代码


2.Fetch()


// Fetch用于向API传送漫画编号并且获得相应的漫画 func (hc *XKCDClient) Fetch(n ComicNumber, save bool) (model.Comic, error) {     resp, err := hc.client.Get(hc.buildURL(n))     if err != nil {         return model.Comic{}, err     }     defer resp.Body.Close()     var comicResp model.ComicResponse     if err := json.NewDecoder(resp.Body).Decode(&comicResp); err != nil {         return model.Comic{}, err     }     if save {         if err := hc.SaveToDisk(comicResp.Img, "."); err != nil {             fmt.Println("Failed to save image!")         }     }     return comicResp.Comic(), nil } 
复制代码


3.SaveToDisk()


// SaveToDisk 下载并且将漫画保存到本地 func (hc *XKCDClient) SaveToDisk(url, savePath string) error {     resp, err := http.Get(url)     if err != nil {         return err     }     defer resp.Body.Close()     absSavePath, _ := filepath.Abs(savePath)     filePath := fmt.Sprintf("%s/%s", absSavePath, path.Base(url))     file, err := os.Create(filePath)     if err != nil {         return err     }     defer file.Close()     _, err = io.Copy(file, resp.Body)     if err != nil {         return err     }     return nil } 
复制代码


4.buildURL()


func (hc *XKCDClient) buildURL(n ComicNumber) string {     var finalURL string     if n == LatestComic {         finalURL = fmt.Sprintf("%s/info.0.json", hc.baseURL)     } else {         finalURL = fmt.Sprintf("%s/%d/info.0.json", hc.baseURL, n)     }     return finalURL } 
复制代码


5. 将所有的步骤连起来

在 main()函数里,我们将目前所写的所有代码整合起来:


  • 读取命令行参数

  • 实例化 XKCDClient

  • 用 XKCDClient 从 API 获取数据

  • 输出


读取命令行参数:


comicNo := flag.Int(     "n", int(client.LatestComic), "Comic number to fetch (default latest)", ) clientTimeout := flag.Int64(     "t", int64(client.DefaultClientTimeout.Seconds()), "Client timeout in seconds", ) saveImage := flag.Bool(     "s", false, "Save image to current directory", ) outputType := flag.String(     "o", "text", "Print output in format: text/json", ) flag.Parse() 
复制代码


实例化 XKCDClient:


xkcdClient := client.NewXKCDClient() xkcdClient.SetTimeout(time.Duration(*clientTimeout) * time.Second) 
复制代码


用 XKCDClient 从 API 中获取数据:


comic, err := xkcdClient.Fetch(client.ComicNumber(*comicNo), *saveImage) if err != nil {     log.Println(err) } 
复制代码


输出:


if *outputType == "json" {     fmt.Println(comic.JSON()) } else {     fmt.Println(comic.PrettyString()) } 
复制代码


按如下所示运行程序


$ Go run main.Go -n 323 -o json 
复制代码


或者在你的电脑上编译成可执行文件然后调用它


$ Go build . $ ./go-grab-xkcd -n 323 -s -o json 
复制代码


Bash 小贴士

你可以用下面这个简单的 shell 命令来依照顺序获取多幅漫画:


$ for i in {1..10}; do ./go-grab-xkcd -n $i -s; done; 
复制代码


上面这个命令在一个循环中调用我们的 go-grab-xkcd,其中的 i 的值被用来替换成漫画编号,因为 xkcd 用序列值作为漫画编号。


原文链接:


https://eryb.space/2020/05/27/diving-into-Go-by-building-a-cli-application.html


2020-08-15 09:002044

评论 1 条评论

发布
用户头像
学习了
2020-08-21 10:22
回复
没有更多了
发现更多内容

分享 6 个JavaScript学习资源

devpoint

JavaScript GitHub 8月日更

ASM 实现 Hook Lambda 和方法引用

神策技术社区

大前端 后端 asm 代码 神策数据

基于 CODING CD + Nocalhost 在大型应用的 ChatOps 实践

CODING DevOps

DevOps 工具 CI/CD 开发测试 ChatOps

从 0 到 1 ,不能忽略的「道」

非著名程序员

产品 产品经理 认知提升 8月日更

[灵魂拷问]MySQL面试高频100问(工程师方向)

编程菌

Java 编程 程序员 面试 计算机

获取自己的公网 IP 地址

耳东@Erdong

IP地址 8月日更

上游思维:如何定义成功?

石云升

读书笔记 8月日更 上游思维

OceanBase 常见参数和变量究竟有什么本质区别?

OceanBase 数据库

数据库 oceanbase OceanBase 开源 OceanBase 社区版

文化与科技的交织,华为P50 Pro与一曲长城谣

脑极体

Python开发篇——RSA加密算法和SHA1计算文件校验码

DisonTangor

Python

模块五作业

俊杰

架构实战营

云原生 | 混沌工程工具 ChaosBlade Operator Pod 篇

RadonDB

数据库 混沌工程

硬核技术,带你走进3D点云车道线自动识别

澳鹏Appen

自动驾驶 机器学习 训练数据 3D点云 车道线标注

科技的世界里没有“粉红税”

脑极体

Windows Server 2019 安装提要 (及 VS 2019 Build Tool) - 续

hedzr

DevOps vscode windows server 2019 server core visual studio 2019

心态炸了!我的join查询多加了个过滤条件性能就崩了

林一

MySQL 查询优化 多表join

三年开发,跳槽腾讯三面终获Offer,定级T2-1(面试题+经验总结)

编程菌

Java 编程 程序员 面试 计算机

hadoop 基本原理与应用

神策技术社区

hadoop 程序员 Hadoop全分布式集群

基金这么赚钱!!编程实现基金从采集到分析通用模板!(白酒为例)

Python研究者

8月日更

MySQL中FROM_UNIXTIME与UNIX_TIMESTAMP

一个大红包

8月日更

第一次看房

escray

生活记录 8月日更

三分钟了解大数据技术发展史

张浩_house

人工智能 机器学习 大数据

GrowingIO Design 组件库搭建之单元测试

GrowingIO技术专栏

单元测试 Jest Storybook

Flink 和流式应用运维(十-下)

数据与智能

flink 监控 Web UI

ipfs国家认可吗?ipfs挖矿靠谱吗?

IPFS国家认可吗 ipfs挖矿靠谱吗

神策数据微信小程序 SDK 功能介绍

神策技术社区

小程序 微信 代码 神策数据 维护

Springboot通过@WebFilter日志双份打印BUG分享

FunTester

性能测试 springboot bug

Android SDK 的 H5 打通方案演进

神策技术社区

大前端 后端 神策数据 shujv

iOS SDK 的 H5 打通方案演进 | 数据采集

神策技术社区

程序员 大前端 后端 数据 方案

博睿数据斩获“飞腾PCS认证集成商”,推动国产化生态建设再进一步!

博睿数据

北鲲云计算:为药企研发的飞速发展提供助力

北鲲云

手把手教你用Go搭建一个CLI应用_语言 & 开发_Eryb_InfoQ精选文章