当前位置:首页 > 汽车 > 正文

用Go语言搞定一年365天天气视频—我的实战笔记

  • 汽车
  • 2026-08-25 11:07:47
  • 11
摘要: 为什么我会想到用Go写天气视频?说实话,最开始我压根没想过用Go干这事儿,当时我盯着电脑屏幕上那个“每日天气播报”的文件夹发呆—...

为什么我会想到用Go写天气视频?

说实话,最开始我压根没想过用Go干这事儿,当时我盯着电脑屏幕上那个“每日天气播报”的文件夹发呆——里面躺着三百多个视频文件,每个都是手动录屏、配音、剪辑出来的,那是在去年三月份,我连续熬了三个通宵后,终于决定要找个程序化的解决方案。

为啥不用Python?因为我在查资料时发现,Go在并发处理编译效率上简直是为这个场景量身定做的,加上Go标准库里的imageostime包,配合FFmpeg命令行工具,理论上完全能实现“一份代码,跑出全年365天不同天气视频”的效果。

核心思路:把视频“拆”成可编程的积木

你可能觉得生成视频很复杂,但其实本质就三步:生成天气数据 → 绘制画面帧 → 编码输出,Go在这中间扮演的角色,就像一个超级流水线管理员。

第一块积木:天气数据从哪来?

我用了两个方案:

  1. 离线模拟(最简单):给一个[]Weather结构体,里面存日期、温度、天气类型(晴/雨/雪)等随机生成的数据。
  2. 在线拉取(更真实):用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.GOMAXPROCSsync.Pool复用画布对象,别每次都image.NewRGBA,我把*image.RGBA放进sync.Pool,内存分配直接下降80%。

最终产出:一个“365天天气视频生成器”

现在我只需要在命令行输入:

./weather-gen -year=2024 -mode=online -api-key=xxx

程序会自动:

  1. 拉取全年数据
  2. 生成365个MP4文件
  3. 按月份存在output/m01/output/m12/文件夹里

整个流水线在8核CPU机器上跑约2小时,我通常挂着让它跑,然后去喝杯茶,期间偶尔看看终端日志,如果发现有异常的日期(比如零下40度)还能自动跳过。

写这篇文章的时候,我又回头看了一下那些最初的“手动季节”视频,突然觉得程序化生成虽然少了点人情味,但那种“365天不重样”的完整感,反而有种机械式的浪漫,你如果也尝试,建议先从模拟数据开始跑通流程,再换真实天气API——毕竟API调用每天有上限,别把配额浪费在调试上。

哦对了,如果遇到FFmpeg输出错误,别忘了在命令后面加2>&1把错误信息打印出来,不然你会像无头苍蝇一样瞎猜——别问我怎么知道的。

用Go语言搞定一年365天天气视频—我的实战笔记