用Go做一个小工具练手,天气查询是再合适不过的选择:它涉及网络请求、JSON解析、命令行参数处理、错误兜底这些日常开发中最常见的技能点,业务逻辑又不复杂,非常适合作为入门后的第一个完整项目。这篇文章带你从零开始实现一个可用的天气查询命令行工具,最后还会加上缓存和并发查询的能力,让它看起来更像一个正经的工程项目。

一、选择天气数据源并申请API密钥
开发天气应用的第一步是确定数据从哪来。目前国内外可选的天气API不少,国外常用的是OpenWeatherMap和WeatherAPI,国内可以用和风天气或者心知天气。对练手项目来说,OpenWeatherMap的免费额度完全够用,注册账号后到控制台创建一个API Key就能直接调用,这里以它为例讲解。
拿到密钥后先别急着写代码,用浏览器或者curl手动请求一次接口,确认返回的数据结构长什么样。以查询北京天气为例,请求地址是https://api.openweathermap.org/data/2.5/weather?q=Beijing&appid=你的密钥&units=metric&lang=zh_cn,其中units=metric表示使用摄氏度,lang=zh_cn让描述字段返回中文。返回的JSON里包含城市名、坐标、温度、湿度、风速、天气描述等信息,把这些字段摸清楚,后面定义结构体就心里有数了。
有一点要提前注意:密钥属于敏感信息,不要直接写死在代码里提交到仓库。推荐的做法是通过环境变量传入,比如os.Getenv("OPENWEATHER_API_KEY"),这样换一台机器或者换一个密钥都不用改代码。
二、实现HTTP请求与JSON解析
Go标准库的net/http和encoding/json已经足够完成整个请求解析流程,不需要引入任何第三方依赖。先定义一个与返回JSON对应的结构体,字段名用大写开头,加上json标签做映射。不需要的字段直接省略,Go会自动忽略JSON里多余的内容。
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// WeatherResult 对应接口返回的JSON结构,只取需要的字段
type WeatherResult struct {
Name string `json:"name"`
Main struct {
Temp float64 `json:"temp"`
Humidity int `json:"humidity"`
} `json:"main"`
Weather []struct {
Description string `json:"description"`
} `json:"weather"`
}
func fetchWeather(city, apiKey string) (*WeatherResult, error) {
url := fmt.Sprintf(
"https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric&lang=zh_cn",
city, apiKey,
)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
// 接口返回非200时读取错误信息
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("接口返回状态码 %d", resp.StatusCode)
}
var result WeatherResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("解析JSON失败: %w", err)
}
return &result, nil
}这段代码有几个值得留意的细节。第一,defer resp.Body.Close()必须写,否则高频调用会泄漏连接;第二,用json.NewDecoder(resp.Body).Decode直接从流解析,比先读完再json.Unmarshal更省内存;第三,错误用%w包装,调用方可以用errors.Is或errors.As逐层判断原因。
城市名如果包含中文或空格,直接拼到URL里会出问题,应该用net/url包做一次转义:url.QueryEscape(city),这样「hang zhou」这类输入也能正确处理。另外生产环境不建议用默认的http.Get,它没有超时控制,一旦对端服务卡住,你的程序也会跟着挂起,后面进阶部分会给出带超时的写法。
三、封装成命令行工具
解析逻辑写好后,接下来把它包装成一个好用的命令行程序。Go标准库的flag包就能满足需求,支持-city指定城市、-days指定天数之类的参数,不需要额外安装依赖。
package main
import (
"flag"
"fmt"
"os"
)
func main() {
city := flag.String("city", "beijing", "要查询的城市名")
flag.Parse()
apiKey := os.Getenv("OPENWEATHER_API_KEY")
if apiKey == "" {
fmt.Fprintln(os.Stderr, "请先设置环境变量 OPENWEATHER_API_KEY")
os.Exit(1)
}
result, err := fetchWeather(*city, apiKey)
if err != nil {
fmt.Fprintln(os.Stderr, "查询失败:", err)
os.Exit(1)
}
desc := ""
if len(result.Weather) > 0 {
desc = result.Weather[0].Description
}
fmt.Printf("城市: %s\n温度: %.1f℃\n湿度: %d%%\n天气: %s\n",
result.Name, result.Main.Temp, result.Main.Humidity, desc)
}运行方式是go build -o weather.exe main.go,然后执行./weather.exe -city=shanghai就能看到输出。如果想同时查多个城市,把flag.Args()剩下的参数都取出来循环查询即可。如果后续参数越来越复杂,比如要支持子命令,可以换成cobra这样的第三方库,它就是Docker和Kubernetes CLI底层用的框架。
输出格式也可以做得更友好。Windows的终端默认不支持UTF-8时中文可能乱码,可以提醒用户执行chcp 65001切换编码;Linux和macOS下一般没有这个问题。颜色输出可以用golang.org/x/term或者直接输出ANSI转义序列,让温度用不同颜色区分冷暖。
四、进阶优化:超时、缓存与并发
基础版能跑之后,可以从三个方向把它变得健壮。首先是超时控制,自己构造一个带超时的http.Client,替代全局的默认Client:
var httpClient = &http.Client{
Timeout: 5 * time.Second,
}其次是缓存。天气数据变化不快,同一座城市短时间内重复查询完全没必要每次都打接口,用一个带过期时间的map就能实现简单缓存,或者直接用go-cache这个库,几行代码搞定。缓存不仅能加快响应速度,还能避免免费额度被快速耗尽。
最后是并发查询。当需要一次查多个城市时,串行循环会非常慢,用goroutine配合sync.WaitGroup并行发起请求,总耗时约等于最慢的那一次请求。要注意结果收集不能直接往共享map里写,应该通过channel回传或者用sync.Mutex保护,初学者最容易在这里踩到数据竞争的坑,可以用go run -race检测。经过这三步优化,这个小工具就具备了实际可用的工程素质,后续还可以加上Web接口或者定时推送,把它扩展成一个完整的天气服务。