最近我完成了用Go编写的第一个严肃的网络应用程序,在用Go进行网络开发方面,有几件事很突出。在过去,我曾作为职业和业余爱好用多种不同的语言和范式开发过网络应用程序,所以我在这个领域看到了不少东西。
总的来说,我喜欢使用Go进行网络开发,尽管一开始需要一些时间来适应。有些东西有点笨重,但在其他部分,例如本短文中描述的上传和下载文件,Go的标准库和固有的简单性使它成为一种非常愉快的体验。
在接下来的几篇文章中,我将重点介绍我在用Go编写生产级网络应用程序时遇到的一些事情,特别是关于认证/授权。
这篇文章将展示一个HTTP文件上传和下载的基本例子。想象一个HTML表单,其中有一个type 文本输入和一个uploadFile 文件输入作为客户端。
让我们看看Go如何解决这个无处不在的网络工程问题。
代码示例
首先,我们用我们的两条路由设置网络服务器:/upload ,用于文件上传;/files/* ,用于文件下载。
const maxUploadSize = 2 * 1024 * 1024 // 2 MB
const uploadPath = "./tmp"
func main() {
http.HandleFunc("/upload", uploadFileHandler())
fs := http.FileServer(http.Dir(uploadPath))
http.Handle("/files/", http.StripPrefix("/files", fs))
log.Print("Server started on localhost:8080, use /upload for uploading files and /files/{fileName} for downloading files.")
log.Fatal(http.ListenAndServe(":8080", nil))
}
我们还为上传的目标目录以及我们接受的最大文件大小定义了常数。请注意,整个文件服务的概念是多么简单--我们只使用标准库中的工具,用http.FileServer 来创建一个HTTP处理程序,它从http.Dir(uploadPath) 提供的目录中提供文件。
现在我们只需要实现uploadFileHandler 。
这个处理程序将。
- 验证最大的文件大小
- 验证请求中的文件和帖子参数
- 检查所提供的文件类型(我们只接受图片和pdf)。
- 创建一个随机的文件名
- 把文件写到磁盘上
- 处理所有的错误并返回成功,如果一切顺利的话
首先,我们定义处理程序。
func uploadFileHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
非常感谢Luis Villegas提供的改进方案,我们使用文件头而不是整个请求来检查文件大小。
然后,我们通过在请求对象上调用ParseMultiPartForm来解析请求体中的multipart/form-data,并将maxUploadSize常数作为参数传递给它。
整个请求体被解析,其文件部分的总字节数被存储在内存中,其余部分被存储在磁盘的临时文件中。 从这个方法返回的任何错误都应该算作内部服务器错误,所以我们检查这种情况并返回合适的错误信息和状态代码。 错误被用一个简单的renderError助手处理,它返回给定的错误信息和相应的HTTP状态代码。
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
fmt.Printf("Could not parse multipart form: %v\n", err)
renderError(w, "CANT_PARSE_FORM", http.StatusInternalServerError)
return
}
如果请求能被成功解析,我们将检查和解析表单参数type 和uploadFile ,并读取文件。
fileType := r.PostFormValue("type")
file, fileheader, err := r.FormFile("uploadFile")
if err != nil {
renderError(w, "INVALID_FILE", http.StatusBadRequest)
return
}
defer file.Close()
然后,我们通过比较maxUploadSize 和r.FormFile 返回的头的Size 字段来验证文件大小。为了调试的目的,我们也会在控制台中打印出文件大小。 如果文件大小大于maxUploadSize ,那么我们将向客户端发送一个FILE_TOO_BIG 错误,以及一个StatusBadRequest代码。
fileSize := fileHeader.Size
fmt.Printf("File size (bytes): %v\n", fileSize)
if fileSize > maxUploadSize {
renderError(w, "FILE_TOO_BIG", http.StatusBadRequest)
return
}
在这个例子中,为了清楚起见,我们不会使用任何伟大的io.Reader 和io.Writer 接口的花哨的东西,而只是将文件读入一个字节数组,稍后我们将再次写出。
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
renderError(w, "INVALID_FILE", http.StatusBadRequest)
return
}
现在我们成功地验证并读取了文件,是时候检查文件的类型了。一个便宜而不安全的方法是只检查文件的扩展名,并相信用户没有改变它,但这对于一个严肃的应用程序来说是不行的。
幸运的是,Go标准库为我们提供了http.DetectContentType 函数,它只需要文件的前512字节,就可以根据mimesniff 算法来检测其文件类型。
detectedFileType := http.DetectContentType(fileBytes)
switch detectedFileType {
case "image/jpeg", "image/jpg":
case "image/gif", "image/png":
case "application/pdf":
break
default:
renderError(w, "INVALID_FILE_TYPE", http.StatusBadRequest)
return
}
在现实世界的应用中,我们可能会对文件元数据做一些处理,比如将其保存到数据库或推送到外部服务--无论如何,我们都会解析和操作元数据。在这里,我们创建一个随机的新名字(在实践中这可能是一个UUID)并记录未来的文件名。
fileName := randToken(12)
fileEndings, err := mime.ExtensionsByType(detectedFileType)
if err != nil {
renderError(w, "CANT_READ_FILE_TYPE", http.StatusInternalServerError)
return
}
newPath := filepath.Join(uploadPath, fileName+fileEndings[0])
fmt.Printf("FileType: %s, File: %s\n", detectedFileType, newPath)
几乎完成了--只剩下一个非常重要的步骤--写文件。如上所述,我们只需将读取文件得到的字节数组复制到一个新创建的文件处理程序中,称为newFile 。
如果一切顺利,我们就向用户返回一个SUCCESS 的信息。
newFile, err := os.Create(newPath)
if err != nil {
renderError(w, "CANT_WRITE_FILE", http.StatusInternalServerError)
return
}
defer newFile.Close()
if _, err := newFile.Write(fileBytes); err != nil {
renderError(w, "CANT_WRITE_FILE", http.StatusInternalServerError)
return
}
w.Write([]byte("SUCCESS"))
就这样了。你可以用一个假的文件上传的HTML页面、cURL或者像postman这样的好工具来测试这个简单的例子。
完整的例子代码可以在这里找到
总结
这又一次证明了Go是如何让它的用户为网络编写简单而强大的软件,而不需要处理其他语言和生态系统中固有的无数抽象层。
在接下来的几篇文章中,我将展示我在用Go编写第一个严肃的网络应用时遇到的其他一些好处,敬请关注。)
// 在reddit上的lstokeworth 的一些反馈后,编辑了代码。谢谢你 :)
// 在GitHub上的Luis Villegas提出合并请求后,编辑了代码和文本。谢谢你 :)