用Go语言写个一年有365天祝福视频生成器,这事儿我琢磨了一整个周末
- 单站
- 2026-06-29 18:12:35
- 210
说实话,一开始朋友跟我说“你写个程序,每天自动给我生成一段祝福视频吧”,我第一反应是拒绝的,一年365天,每天一段,内容还得不一样,这工作量听着就让人头大,但后来转念一想——Go语言的并发模型,配上模板引擎和FFmpeg,好像真能搞,于是那个周六早上,我泡了杯咖啡,开始边想边写。
为什么非要用Go?Python不香吗?
我知道很多人第一反应是Python,但你要知道,视频生成这活儿,IO密集型和CPU密集型任务混着来,Go的goroutine处理并发那是真顺手,而且编译出来就一个二进制文件,部署到服务器上连环境都不用配,更重要的是——Go标准库里的text/template和html/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里都做了三级错误恢复:
- 如果FFmpeg命令执行失败(比如素材文件损坏),记录日志,跳过当天,继续生成下一天
- 如果模板解析出错(比如JSON里写了非法字符),用默认祝福语兜底
- 如果整个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服务器,朋友可以通过浏览器:
- 查看未来7天的祝福语预览
- 手动修改某一天的祝福语
- 立即重新生成指定日期的视频
用了net/http加gorilla/mux路由,模板渲染用html/template(自动转义,防XSS),后端API设计成RESTful风格,GET获取祝福语列表,PUT修改某天的祝福语,POST触发重新生成。
数据存储从JSON文件换成了SQLite,用mattn/go-sqlite3驱动,这样朋友改完祝福语,程序重启后数据还在。
写到最后发现的问题
现在这个程序已经跑了两个多月了,稳定生成了一百多段祝福视频,朋友说效果还行,就是有些天的祝福语重复了——随机算法没处理好,同一个月的祝福语容易出现相似内容。
改进方案是在选择祝福语时加个“最近7天使用记录”,避免重复,类似布隆过滤器那种思路,但不用那么复杂,直接维护一个长度为7的队列就行。
还有一个问题:夏令时,如果服务器时区没设对,会导致某些天的视频早一小时或晚一小时生成,我吃了这个亏,后来在time.LoadLocation("Asia/Shanghai")显式指定时区。
一年365天的祝福视频,说难不难,说简单也不简单,核心就是用Go的并发能力把FFmpeg的串行任务拆开跑,用模板引擎让祝福语能动态组合,再用结构体把整个数据流管起来。
写完这个项目我最大的感受是:程序员的浪漫,大概就是写代码替你说那些不好意思说出口的祝福吧。
