本文由 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 的如下方面更加得心应手:
接受命令行参数
JSON 和 Go 的结构体之间的相互转化
调用 API 函数
创建文件(从互联网下载并且保存)
对字符串的操作
整个项目的结构如下所示:
$ tree go-grab-xkcd go-grab-xkcd ├── client │ └── xkcd.Go └── model └── comic.Go ├── main.Go └── Go.mod
复制代码
1. 初始化项目
通过如下命令创建 Go.mod 文件
这有助于整个项目的管理(可以类比 JS 里的 package.json)
2. xkcd 的 API
xkcd 太棒了,你不需要任何注册信息或者访问密钥就能使用他们的 API。打开 xkcd 的 API 文档,然后你会发现两个端点:
http://xkcd.com/info.0.json- GET 端点,用于获取最新的漫画
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。
定义常数-
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
评论 1 条评论