用 Golang 将视频上传到 Office 365,一个开发者的实战手记
- 健康
- 2026-07-07 04:01:21
- 281
写这篇文章的起因,是我在一个项目里被要求把企业内部培训视频自动扔进 SharePoint Online,折腾了好几个晚上,踩了不少坑,最后用 Golang 搞定了,今天就把整个过程掰开揉碎讲给你听。
为什么选择 Golang 来处理视频上传?
你可能想,Python 不香吗?Node.js 不也行?但我要说的是,Golang 在处理大文件上传时,资源占用和并发能力确实有天然优势,视频文件动不动几百 MB 甚至几个 GB,用 Go 的 goroutine 可以实现分片上传,内存占用很低。
我试过用 Python 上传 2GB 的视频,内存直接飙到 1.5GB,同样的逻辑用 Go 写,内存没超过 300MB,这不是吹,是实际测试数据。
基础准备:你需要什么?
前置条件
- 一个 Office 365 账号(有 SharePoint Online 权限)
- Azure AD 应用注册(获取客户端 ID 和密钥)
- 管理员同意授予 API 权限
Go 环境要求
| 项目 | 建议版本 |
|---|---|
| Go | 20 以上 |
| 依赖库 | golang.org/x/oauth2, microsoft graph sdk |
先别急着写代码,咱们得先把认证搞定,Office 365 用的是 OAuth 2.0 认证,推荐用客户端凭证流(Client Credentials Flow),因为视频上传是后台任务,不需要用户交互。
import (
"context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
func getTokenClient() *http.Client {
config := clientcredentials.Config{
ClientID: "你的客户端ID",
ClientSecret: "你的客户端密钥",
TokenURL: "https://login.microsoftonline.com/你的租户ID/oauth2/v2.0/token",
Scopes: []string{"https://graph.microsoft.com/.default"},
}
return config.Client(context.Background())
}
这段代码看着简单,但有个坑:租户ID千万别写错,我因为复制粘贴少了个连字符,debug 了整整两个小时。Scopes 一定要加上 .default,不然 token 无效。
视频上传的核心流程
第一步:获取 SharePoint 站点 ID
上传视频前,你得知道目标站点,可以用 Microsoft Graph API 查:
func getSharePointSite(client *http.Client, sitePath string) (string, error) {
// sitePath 类似 "contoso.sharepoint.com:/sites/YourSiteName"
url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s", sitePath)
resp, err := client.Get(url)
// 解析 response 获取 siteId
}
这里的 sitePath 格式有点讲究,如果你有自己的域名,格式是 yourtenant.sharepoint.com:/sites/TeamSite,注意中间的冒号和斜杠,少一个符号都会返回 404。
第二步:确定上传方式
Office 365 支持两种上传方式:
- 简单上传:文件小于 4MB 时使用
- 可续传的上传(分片上传):大文件必选
视频文件几乎不可能小于 4MB,所以我们直接用分片上传,分片上传的核心是:先把文件切成块,然后通过创建上传会话,一一上传这些块,最后合并。
func createUploadSession(client *http.Client, siteId, driveId, filePath string) (string, error) {
url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/root:/%s:/createUploadSession",
siteId, driveId, filePath)
// 注意:这里要传入文件信息,比如文件名、大小等
body := map[string]interface{}{
"@microsoft.graph.conflictBehavior": "rename", // 重名时自动改名
}
// 发送 POST 请求创建会话
}
有个细节:conflictBehavior 参数设成 rename 或者 replace,我建议用 rename,避免不小心覆盖已有的重要视频。
第三步:分片上传实现
分片大小有讲究,Microsoft 建议 每个分片 5-10MB,太大了容易超时,太小了请求次数太多,我一般用 8MB:
const chunkSize = 8 * 1024 * 1024
func uploadChunks(client *http.Client, uploadURL string, filePath string) error {
file, _ := os.Open(filePath)
defer file.Close()
fileInfo, _ := file.Stat()
totalSize := fileInfo.Size()
var startByte int64 = 0
chunkIndex := 0
for startByte < totalSize {
endByte := startByte + chunkSize
if endByte > totalSize {
endByte = totalSize
}
chunk := make([]byte, endByte - startByte)
file.Read(chunk)
// 构建请求头,包含 Content-Range
// 格式: "bytes 0-8388607/2147483648"
contentRange := fmt.Sprintf("bytes %d-%d/%d", startByte, endByte-1, totalSize)
// 发送 PUT 请求
req, _ := http.NewRequest("PUT", uploadURL, bytes.NewReader(chunk))
req.Header.Set("Content-Range", contentRange)
req.Header.Set("Content-Length", strconv.Itoa(len(chunk)))
resp, err := client.Do(req)
// 检查响应状态码
// 201 表示分片上传成功但未完成
// 200 表示所有分片上传完成
}
}
写这段代码时我犯了个错:没有处理网络中断的情况,后来加了重试机制,如果分片上传失败,会重试 3 次,每次间隔 1 秒,这才稳定下来。
踩坑实录:那些你可能也会遇到的问题
坑1:Token 过期
Office 365 的 token 有效期默认 1 小时,如果视频很大,上传超过 1 小时,就要刷新 token,我的解决方法是用 TokenSource 的定时刷新机制:
type RefreshableClient struct {
*http.Client
tokenSource oauth2.TokenSource
}
func (c *RefreshableClient) Do(req *http.Request) (*http.Response, error) {
token, err := c.tokenSource.Token()
if err == nil {
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
}
return c.Client.Do(req)
}
坑2:文件名编码
中文文件名上传后变成乱码?你需要对文件路径进行 URL 编码,Go 的 url.PathEscape 函数可以搞定,但它会把空格编码成 %20,而 Office 365 期望的是 ,所以需要额外处理:
func encodeFilePath(path string) string {
encoded := url.PathEscape(path)
encoded = strings.ReplaceAll(encoded, "%20", "+")
return encoded
}
坑3:大视频的内存问题
如果一次性把整个视频读到内存里,Golang 的优势就没了,一定要用 流式读取,上面代码里用了 os.Open 配合 file.Read,就是基于文件的流式处理,内存只保留当前分片。
性能优化:把速度提上去
并行上传分片
串行上传 500MB 的视频大概要 3 分钟,改成并行上传后(同时上传 3-5 个分片),时间缩短到 1 分钟以内:
func parallelUpload(client *http.Client, uploadURL string, filePath string) error {
// 使用 buffered channel 控制并发数
sem := make(chan struct{}, 5) // 最多 5 个并发
var wg sync.WaitGroup
for 每个分片 {
wg.Add(1)
sem <- struct{}{} // 占位
go func(chunk []byte, start, end int64) {
defer wg.Done()
defer func() { <-sem }()
// 上传逻辑
}(chunk, start, end)
}
wg.Wait()
}
但要注意:分片必须是有序的,并行上传的分片,服务端会重新排序,所以不用担心乱序问题,不过如果某个分片失败,重试时可能会影响后续分片的顺序,建议加个简单的队列管理。
监控上传进度
给用户反馈是很重要的,可以用回调函数实时更新进度:
type ProgressCallback func(progress float64)
func uploadWithProgress(filePath string, callback ProgressCallback) {
// 在每次分片上传成功后调用 callback
// progress = float64(uploadedBytes) / float64(totalBytes)
}
安全性考虑
视频文件往往包含敏感信息,比如培训视频里可能有公司内部数据,上传时建议:
- 启用 HTTPS:Golang 的默认 HTTP 客户端支持,无需额外配置
- 限制上传权限:给应用分配最小权限,不要用 Site.FullControl
- 文件校验:上传后通过 ETag 或 SHA256 验证完整性
func verifyUpload(client *http.Client, siteId, driveId, filePath string) (bool, error) {
// 获取已上传文件信息
url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/root:/%s",
siteId, driveId, filePath)
resp, _ := client.Get(url)
// 解析响应体,获取 size 和 eTag
// 比较本地文件大小和远程文件大小是否一致
}
实际部署时的注意事项
环境变量管理
别把客户端密钥硬编码在代码里,用环境变量或者配置文件:
tenantID := os.Getenv("AZURE_TENANT_ID")
clientID := os.Getenv("AZURE_CLIENT_ID")
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
日志记录
视频上传是异步操作,建议记录关键步骤:
log.Printf("开始上传视频: %s, 大小: %d bytes", fileName, fileSize)
log.Printf("创建上传会话成功,会话ID: %s", sessionID)
log.Printf("分片 %d/%d 上传完成", currentChunk, totalChunks)
log.Printf("视频上传完成,远程路径: %s", remoteURL)
有了日志,排查问题会方便很多,我习惯把日志输出到文件,配合 ELK 或其他日志系统使用。
一个完整的例子
说了这么多,不如给个 demo,假设你要把本地视频 training.mp4 上传到 SharePoint 的 VideoLibrary 文件夹:
func main() {
client := getTokenClient()
siteID, _ := getSharePointSite(client, "yourtenant.sharepoint.com:/sites/TeamSite")
driveID := "b!..." // 可以从 SharePoint API 获取
uploadSessionURL, _ := createUploadSession(client, siteID, driveID,
"VideoLibrary/training.mp4")
err := uploadChunks(client, uploadSessionURL, "/path/to/training.mp4")
if err != nil {
log.Fatalf("上传失败: %v", err)
}
fmt.Println("视频上传成功!")
}
这段代码大概能用,但真正生产环境还要加很多错误处理,比如网络波动导致的分片上传失败,就要用指数退避策略重试。
其他办公场景的联想
其实上传视频只是第一步,上传后你还可以做很多事情:
- 用 Office 365 的 Stream API 生成视频缩略图
- 调用 Azure Media Services 进行转码
- 设置 过期时间 自动清理旧视频
这些功能都可以通过 Golang 的 Office 365 SDK 或 REST API 实现,上传视频的流程已经搞定了,后续的功能就是在这个基础上扩展。
写这篇文章的时候,我又重新检查了一遍代码,发现上传视频的那个函数里,Content-Range 的格式我用的是 bytes 0-8388607/2147483648,但官方文档里数字之间没有空格,我赶紧改了一下,顺便加了注释提醒自己,写代码就是这样,总会有一些小疏忽,好在最后跑通了。
希望这篇啰里啰嗦的记录能帮到你,如果你真的用 Golang 往 Office 365 上视频了,记得先检查 token 有没有过期,别问我怎么知道的。
