首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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

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

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

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

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

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

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

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

代码语言:javascript
复制
$ 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)

代码语言:javascript
复制
$ go-grab-xkcd -n 323

Title: Ballmer Peak
Comic No: 323
Date: 1-10-2007
Description: Apple uses automated schnapps IVs.
Image: https://imgs.xkcd.com/comics/ballmer_peak.png

代码语言:javascript
复制
$ 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. 对字符串的操作

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

代码语言:javascript
复制
$ 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 文件

代码语言:javascript
复制
$ 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 结构如下所示:

代码语言:javascript
复制
{ 
  "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( 结构体 ) 。

代码语言:javascript
复制
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 里生成结构体。

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

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

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

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

代码语言:javascript
复制
// 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 结构体加入如下两个方法:

代码语言:javascript
复制
// 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 
} 

代码语言:javascript
复制
// 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。

代码语言:javascript
复制
type ComicNumber int 

定义常数 -

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

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

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

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

向 XKCDClient 加入以下四个方法:

1.SetTimeout()

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

2.Fetch()

代码语言:javascript
复制
// 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()

代码语言:javascript
复制
// 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()

代码语言:javascript
复制
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 获取数据
  • 输出

读取命令行参数:

代码语言:javascript
复制
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:

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

用 XKCDClient 从 API 中获取数据:

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

输出:

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

按如下所示运行程序

代码语言:javascript
复制
$ Go run main.Go -n 323 -o json 

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

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

Bash 小贴士

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

代码语言:javascript
复制
$ 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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/5wCC5NrZZW36BTOKZmAZ
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券