本文由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 Peak
Comic No: 323
Date: 1-10-2007
Description: 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 的如下方面更加得心应手:
整个项目的结构如下所示:
$ tree go-grab-xkcd
go-grab-xkcd
├── client
│ └── xkcd.Go
└── model
└── comic.Go
├── main.Go
└── Go.mod
通过如下命令创建 Go.mod 文件
$ go mod init
这有助于整个项目的管理(可以类比 JS 里的 package.json)
xkcd 太棒了,你不需要任何注册信息或者访问密钥就能使用他们的 API。打开 xkcd 的 API 文档,然后你会发现两个端点:
从这些端点返回的 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": ""
}
基于以上的 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)
}
在 client 包中创建 xkcd.Go 文件
我们先定义一个类型叫做 ComicNumber,它的实际类型是 int。
type ComicNumber int
定义常数 -
// XKCDClient 是 XKCD 的客户端
type XKCDClient struct {
client *http.Client
baseURL string
}
创建一个叫做 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
}
在 main() 函数里,我们将目前所写的所有代码整合起来:
读取命令行参数:
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
你可以用下面这个简单的 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
领取专属 10元无门槛券
私享最新 技术干货