用Go语言搞定一年365天天气视频—我的实战笔记
- 汽车
- 2026-08-25 11:07:47
- 97
为什么我会想到用Go写天气视频?
说实话,最开始我压根没想过用Go干这事儿,当时我盯着电脑屏幕上那个“每日天气播报”的文件夹发呆——里面躺着三百多个视频文件,每个都是手动录屏、配音、剪辑出来的,那是在去年三月份,我连续熬了三个通宵后,终于决定要找个程序化的解决方案。
为啥不用Python?因为我在查资料时发现,Go在并发处理和编译效率上简直是为这个场景量身定做的,加上Go标准库里的image、os、time包,配合FFmpeg命令行工具,理论上完全能实现“一份代码,跑出全年365天不同天气视频”的效果。
核心思路:把视频“拆”成可编程的积木
你可能觉得生成视频很复杂,但其实本质就三步:生成天气数据 → 绘制画面帧 → 编码输出,Go在这中间扮演的角色,就像一个超级流水线管理员。
第一块积木:天气数据从哪来?
我用了两个方案:
- 离线模拟(最简单):给一个
[]Weather结构体,里面存日期、温度、天气类型(晴/雨/雪)等随机生成的数据。 - 在线拉取(更真实):用
net/http请求高德或和风天气API,替换掉模拟数据。
type Weather struct {
Date string
TempHigh int
TempLow int
WeatherTyp string // "sunny" "rainy" "snowy"
}
第二块积木:怎么把数据变成“画面”?
这里我踩了个大坑,一开始用github.com/fogleman/gg画图,但发现对于365个视频,每个视频哪怕只有5秒,也需要每秒30帧 = 150帧/视频 × 365 = 54750张图片,GG库虽然好用,但一次性绘制这么多图,内存直接爆炸。
解决方案是流式处理:用bufio边画边写临时文件,然后立刻调FFmpeg转成视频,再删除图片,Go的goroutine在这里大显身手:
for _, day := range weatherData {
go func(d Weather) {
drawFrame(d) // 画图
encodeToVideo(d) // 调用FFmpeg
os.RemoveAll("temp_frames/") // 清理
}(day)
}
等等,直接开365个goroutine?你可能会问,我一开始也是这么干的,结果CPU直接飙到100%卡死,正确做法是用worker池,比如一次开8个goroutine,配合sync.WaitGroup:
sem := make(chan struct{}, 8) // 并发限制
for _, day := range weatherData {
sem <- struct{}{}
go func(d Weather) {
defer func() { <-sem }()
// ...处理逻辑
}(day)
}
实战:用Go生成一个“雪天”视频的完整逻辑
这里我简化一下,但保留核心代码,假设我们要生成2024年1月15日这个“小雪转多云”天气的视频:
步骤1:绘制背景和文字
func drawSnowFrame(img *image.RGBA, t time.Time, weather Weather) {
// 设置浅蓝色背景
for y := 0; y < 720; y++ {
for x := 0; x < 1280; x++ {
img.Set(x, y, color.RGBA{200, 210, 220, 255})
}
}
// 写日期和温度
drawText(img, t.Format("2006-01-02"), 50, 80, 40)
drawText(img, fmt.Sprintf("最高 %d°C / 最低 %d°C", weather.TempHigh, weather.TempLow), 50, 150, 28)
// 随机画雪花
rand.Seed(t.UnixNano())
for i := 0; i < 50; i++ {
x := rand.Intn(1280)
y := rand.Intn(720)
// 画一个小白点
drawDot(img, x, y, color.White)
}
}
步骤2:调用FFmpeg编码为MP4
这里我用exec.Command拼一个命令行参数,注意别忘了加-y覆盖文件,不然会卡在交互确认上:
cmd := exec.Command("ffmpeg",
"-framerate", "30",
"-i", "temp_frames/frame_%04d.png",
"-vf", "scale=1280:-2",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-y", outputPath)
那些年我踩过的“坑”(附解决对照表)
| 问题现象 | 根本原因 | 我的解决办法 |
|---|---|---|
| 生成的视频只有前几帧有画面 | goroutine并发时图片文件名重复 | 用sync.Mutex锁 + 递增计数器 |
| FFmpeg时不时报“No such file” | 图片还没写完就开始编码 | 确保用img.Encode()后调用Sync()缓冲 |
| 内存占用峰值飙到4G | 365个视频同时生成 | 改用worker池,限制最大并发为4 |
| 极少数日期温度显示乱码 | UTF-8中文字体问题 | 安装fonts-wqy-zenhei并指定字体路径 |
| 视频时间戳对不上 | Go的time.Now()导致时区错乱 |
统一用time.FixedZone("CST", 8*3600) |
有没有更“懒”的方案?——模板化视频生成
如果你不想每次都重新绘制所有帧,还有个取巧办法:生成一个“空白模版”视频(比如淡蓝色背景+雪花动画循环),然后用-ss和-t参数截取不同时间段,再用drawtext滤镜叠加日期文字。
这样流程会简化成:ffmpeg -i base.mp4 -vf "drawtext=text='2024-01-15':fontfile=...”,但缺点是无法做复杂的气象动画(比如太阳从东边升起的位置变化),我最终还是选择了全绘制方案,因为灵活性更高。
对了,有个细节特别容易忽略——每年的2月29日!你写time.Date(year, time.February, 29, ...)在非闰年会直接“panic”,代码必须在初始化数据时判断isLeapYear,否则跑一夜就白干了。
性能优化小技巧:把“慢”藏在“快”里
用Go写完之后,我做了个简单基准:
| 操作 | 单次耗时 | 365次总耗时(优化前) | 优化后 |
|---|---|---|---|
| 生成一帧图片 | 约8ms | 需要约45分钟(但可并行) | 降到18分钟 |
| FFmpeg编码一个5秒视频 | 约1.2秒 | 约7分钟(串行) | 5分钟 |
| 图片清理与文件IO | 约30ms | 累计约11秒 | 无感 |
真正的关键是用runtime.GOMAXPROCS和sync.Pool复用画布对象,别每次都image.NewRGBA,我把*image.RGBA放进sync.Pool,内存分配直接下降80%。
最终产出:一个“365天天气视频生成器”
现在我只需要在命令行输入:
./weather-gen -year=2024 -mode=online -api-key=xxx
程序会自动:
- 拉取全年数据
- 生成365个MP4文件
- 按月份存在
output/m01/到output/m12/文件夹里
整个流水线在8核CPU机器上跑约2小时,我通常挂着让它跑,然后去喝杯茶,期间偶尔看看终端日志,如果发现有异常的日期(比如零下40度)还能自动跳过。
写这篇文章的时候,我又回头看了一下那些最初的“手动季节”视频,突然觉得程序化生成虽然少了点人情味,但那种“365天不重样”的完整感,反而有种机械式的浪漫,你如果也尝试,建议先从模拟数据开始跑通流程,再换真实天气API——毕竟API调用每天有上限,别把配额浪费在调试上。
哦对了,如果遇到FFmpeg输出错误,别忘了在命令后面加2>&1把错误信息打印出来,不然你会像无头苍蝇一样瞎猜——别问我怎么知道的。
