当前位置:首页 > 单站 > 正文

用Go语言写个一年有365天祝福视频生成器,这事儿我琢磨了一整个周末

  • 单站
  • 2026-06-29 18:12:35
  • 210
摘要: 说实话,一开始朋友跟我说“你写个程序,每天自动给我生成一段祝福视频吧”,我第一反应是拒绝的,一年365天,每天一段,内容还得不一...

说实话,一开始朋友跟我说“你写个程序,每天自动给我生成一段祝福视频吧”,我第一反应是拒绝的,一年365天,每天一段,内容还得不一样,这工作量听着就让人头大,但后来转念一想——Go语言的并发模型,配上模板引擎和FFmpeg,好像真能搞,于是那个周六早上,我泡了杯咖啡,开始边想边写。

为什么非要用Go?Python不香吗?

我知道很多人第一反应是Python,但你要知道,视频生成这活儿,IO密集型和CPU密集型任务混着来,Go的goroutine处理并发那是真顺手,而且编译出来就一个二进制文件,部署到服务器上连环境都不用配,更重要的是——Go标准库里的text/templatehtml/template,做文字替换和视频脚本生成,比Python的string格式化舒服太多了。

我一开始的设想特别简单:每天跑一次,读当天的日期,从预设的祝福语库里随机选一条,配上背景音乐和一段风景视频素材,拼成15秒的短视频,但写着写着发现,事情没那么简单。

核心数据结构:用结构体来管理一年365天

先定义几个结构体,这一步很关键,因为整个系统的数据流都围着它们转。

type DayBlessing struct {
    Date     time.Time
    Weekday  string
    Season   string
    Festival string // 如果当天是节日,存节日名称
    Message  string // 祝福语模板
}
type VideoConfig struct {
    Resolution string // "1920x1080"
    Duration   int    // 秒,默认15
    BGMusic    string // 背景音乐路径
    FontPath   string // 字体文件路径
}

我一开始把祝福语写死在代码里,后来发现维护起来太痛苦,改成从JSON文件读,每天程序启动时加载,用encoding/json反序列化,这样朋友想改哪天的祝福语,直接改JSON就行,不用动代码。

说到JSON,Go的零值特性帮了大忙,如果某天没有特殊节日,Festival字段是空字符串,模板里用if判断跳过就行,不会panic。

模板引擎:这才是真正的“一年365天祝福视频”的灵魂

Go的模板引擎有个特别好的地方——它允许你在模板里写逻辑,但又不让你写太复杂的逻辑,正好适合我们这种场景:把日期、季节、节日、祝福语组合成一段话。

我建了三个模板文件:

  • 视频文案模板:定义每段视频的文字内容
  • 字幕样式模板:定义文字在视频里的位置、大小、颜色
  • FFmpeg命令模板:最终调用FFmpeg拼接视频

视频文案模板长这样(简化版):

今天是 {{.Weekday}},{{.Date.Format "2006年1月2日"}}
{{if .Festival}}祝您{{.Festival}}快乐!{{end}}
{{.Message}}

注意那个2006年1月2日,Go语言时间格式的参考时间就是2006年1月2日15:04:05,我第一次写的时候习惯性写成YYYY-MM-DD,结果跑出来全是乱码,这坑踩得值,记住了。

并发生成:让goroutine帮你一天搞定365天的视频

如果按顺序生成365个视频,每个视频假设需要10秒(包括FFmpeg编码时间),那总共要等一个多小时,用goroutine加worker pool,把任务分到8个核上跑,理论时间能缩短到10分钟左右。

代码逻辑大概是这样的:

func generateAllVideos(days []DayBlessing, config VideoConfig) {
    jobs := make(chan DayBlessing, 100)
    results := make(chan string, 100)
    // 启动8个worker
    for w := 1; w <= 8; w++ {
        go worker(w, jobs, results, config)
    }
    // 发送任务
    for _, day := range days {
        jobs <- day
    }
    close(jobs)
    // 收集结果
    for r := 1; r <= len(days); r++ {
        <-results
    }
}

实际写的时候要注意:FFmpeg进程是系统级调用,goroutine内部用os/exec包执行命令,如果同时启动太多FFmpeg进程,内存会爆,我的经验是worker数量控制在CPU核数一半左右,配合semaphore限流。

视频素材管理:365天不能都用同一段风景

朋友说:“你每天用同一段海浪拍沙滩的视频,谁看啊?” 他说的对。

我在程序里加了个素材调度器,根据日期和季节从素材库选视频,素材库按文件夹组织:

素材/
├── 春/
│   ├── 樱花.mp4
│   └── 春雨.mp4
├── 夏/
│   ├── 海滩.mp4
│   └── 荷花.mp4
├── 秋/
│   ├── 红叶.mp4
│   └── 麦田.mp4
└── 冬/
    ├── 雪景.mp4
    └── 暖炉.mp4

程序里根据月份判断季节:3-5月春,6-8月夏,9-11月秋,12-2月冬,然后从对应文件夹随机选一个视频,如果当天是节日,优先用节日专属素材。

错误处理:视频生成过程中的那些坑

写Go程序最头疼的就是错误处理,但在这个项目里,错误处理反倒成了亮点。

每个goroutine里都做了三级错误恢复:

  1. 如果FFmpeg命令执行失败(比如素材文件损坏),记录日志,跳过当天,继续生成下一天
  2. 如果模板解析出错(比如JSON里写了非法字符),用默认祝福语兜底
  3. 如果整个worker panic了,用recover捕获,不影响其他worker
defer func() {
    if r := recover(); r != nil {
        log.Printf("Worker %d recovered from panic: %v", id, r)
    }
}()

这个recover救了我一次——有一次素材文件夹里混进了一个不是视频的文件,FFmpeg直接segmentation fault,goroutine挂了,但其他worker没事,要是用单线程的Python脚本,整个程序就得重跑。

性能优化:把365天的视频生成时间压缩到一集电视剧的长度

第一版跑完365天用了47分钟,我觉得太慢了,开始优化:

  • 预编译模板:把所有模板在初始化时Parse一次,别每次生成都Parse
  • 复用FFmpeg进程:同一个参数配置(分辨率、码率)可以复用进程池
  • SSD加速:临时文件写内存盘(/dev/shm),减少磁盘IO

优化后降到11分钟,最耗时的还是FFmpeg编码,这个没得省,除非换硬件编码器。

日程安排:用cron让程序每天自动跑

其实不需要一次性生成365天的视频,更好的方案是:写一个程序,每天生成当天的视频,用cron设置每天凌晨4点跑一次,生成完成后自动推送到朋友的手机。

如果某天生成失败了(比如FFmpeg崩溃),程序会在日志里记录,第二天可以手动补生成,Go的错误处理让我可以很容易地实现“失败重试”逻辑——重试3次,每次间隔5秒,还不行就发邮件报警。

扩展性:加个Web界面让朋友自己选祝福语

写到后面我觉得光生成视频没意思,又加了个简单的HTTP服务器,朋友可以通过浏览器:

  1. 查看未来7天的祝福语预览
  2. 手动修改某一天的祝福语
  3. 立即重新生成指定日期的视频

用了net/httpgorilla/mux路由,模板渲染用html/template(自动转义,防XSS),后端API设计成RESTful风格,GET获取祝福语列表,PUT修改某天的祝福语,POST触发重新生成。

数据存储从JSON文件换成了SQLite,用mattn/go-sqlite3驱动,这样朋友改完祝福语,程序重启后数据还在。

写到最后发现的问题

现在这个程序已经跑了两个多月了,稳定生成了一百多段祝福视频,朋友说效果还行,就是有些天的祝福语重复了——随机算法没处理好,同一个月的祝福语容易出现相似内容。

改进方案是在选择祝福语时加个“最近7天使用记录”,避免重复,类似布隆过滤器那种思路,但不用那么复杂,直接维护一个长度为7的队列就行。

还有一个问题:夏令时,如果服务器时区没设对,会导致某些天的视频早一小时或晚一小时生成,我吃了这个亏,后来在time.LoadLocation("Asia/Shanghai")显式指定时区。

一年365天的祝福视频,说难不难,说简单也不简单,核心就是用Go的并发能力把FFmpeg的串行任务拆开跑,用模板引擎让祝福语能动态组合,再用结构体把整个数据流管起来。

写完这个项目我最大的感受是:程序员的浪漫,大概就是写代码替你说那些不好意思说出口的祝福吧。

用Go语言写个一年有365天祝福视频生成器,这事儿我琢磨了一整个周末