概述

在 Web 开发中,需要处理很多静态资源文件,如 css/js 和图片文件等。本文将介绍在 Go 语言中如何处理文件请求。

接下来,我们将介绍两种处理文件请求的方式:原始方式和http.FileServer方法。

原始方式

原始方式比较简单粗暴,直接读取文件,然后返回给客户端。

  1. func main() {
  2. mux := http.NewServeMux()
  3. mux.HandleFunc("/static/", fileHandler)
  4. server := &http.Server {
  5. Addr: ":8080",
  6. Handler: mux,
  7. }
  8. if err := server.ListenAndServe(); err != nil {
  9. log.Fatal(err)
  10. }
  11. }

上面我们创建了一个文件处理器,将它挂载到路径/static/上。一般地,静态文件的路径有一个共同的前缀,以便与其它路径区分。如这里的/static/,还有一些常用的,例如/public/等。

代码的其它部分与程序模板没什么不同,这里就不赘述了。

另外需要注意的是,这里的注册路径/static/最后的/不能省略。我们在前面的文章程序结构中介绍过,如果请求的路径没有精确匹配的处理,会逐步去掉路径最后部分再次查找。

静态文件的请求路径一般为/static/hello.html这种形式。没有精确匹配的路径,继而查找/static/,这个路径与/static是不能匹配的。

接下来,我们看看文件处理器的实现:

  1. func fileHandler(w http.ResponseWriter, r *http.Request) {
  2. path := "." + r.URL.Path
  3. fmt.Println(path)
  4. f, err := os.Open(path)
  5. if err != nil {
  6. Error(w, toHTTPError(err))
  7. return
  8. }
  9. defer f.Close()
  10. d, err := f.Stat()
  11. if err != nil {
  12. Error(w, toHTTPError(err))
  13. return
  14. }
  15. if d.IsDir() {
  16. DirList(w, r, f)
  17. return
  18. }
  19. data, err := ioutil.ReadAll(f)
  20. if err != nil {
  21. Error(w, toHTTPError(err))
  22. return
  23. }
  24. ext := filepath.Ext(path)
  25. if contentType := extensionToContentType[ext]; contentType != "" {
  26. w.Header().Set("Content-Type", contentType)
  27. }
  28. w.Header().Set("Content-Length", strconv.FormatInt(d.Size(), 10))
  29. w.Write(data)
  30. }

首先我们读出请求路径,再加上相对可执行文件的路径。一般地,static目录与可执行文件在同一个目录下。然后打开该路径,查看信息。

如果该路径表示的是一个文件,那么根据文件的后缀设置Content-Type,读取文件的内容并返回。代码中简单列举了几个后缀对应的Content-Type

  1. var extensionToContentType = map[string]string {
  2. ".html": "text/html; charset=utf-8",
  3. ".css": "text/css; charset=utf-8",
  4. ".js": "application/javascript",
  5. ".xml": "text/xml; charset=utf-8",
  6. ".jpg": "image/jpeg",
  7. }

如果该路径表示的是一个目录,那么返回目录下所有文件与目录的列表:

  1. func DirList(w http.ResponseWriter, r *http.Request, f http.File) {
  2. dirs, err := f.Readdir(-1)
  3. if err != nil {
  4. Error(w, http.StatusInternalServerError)
  5. return
  6. }
  7. sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
  8. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  9. fmt.Fprintf(w, "<pre>\n")
  10. for _, d := range dirs {
  11. name := d.Name()
  12. if d.IsDir() {
  13. name += "/"
  14. }
  15. url := url.URL{Path: name}
  16. fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), name)
  17. }
  18. fmt.Fprintf(w, "</pre>\n")
  19. }

上面的函数先读取目录下第一层的文件和目录,然后按照名字排序。最后拼装成包含超链接的 HTML 返回。用户可以点击超链接访问对应的文件或目录。

如何上述过程中出现错误,我们使用toHTTPError函数将错误转成对应的响应码,然后通过Error回复给客户端。

  1. func toHTTPError(err error) int {
  2. if os.IsNotExist(err) {
  3. return http.StatusNotFound
  4. }
  5. if os.IsPermission(err) {
  6. return http.StatusForbidden
  7. }
  8. return http.StatusInternalServerError
  9. }
  10. func Error(w http.ResponseWriter, code int) {
  11. w.WriteHeader(code)
  12. }

同级目录下static目录内容:

  1. static
  2. ├── folder
  3. ├── file1.txt
  4. └── file2.txt
  5. └── file3.txt
  6. ├── hello.css
  7. ├── hello.html
  8. ├── hello.js
  9. └── hello.txt

运行程序看看效果:

  1. $ go run main.go

打开浏览器,请求localhost:8080/static/hello.html

可以看到页面hello.html已经呈现了:

  1. <!-- hello.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  8. <title>Go Web 编程之 静态文件</title>
  9. <link rel="stylesheet" href="/static/hello.css">
  10. </head>
  11. <body>
  12. <p class="greeting">Hello World!</p>
  13. <script src="/static/hello.js"></script>
  14. </body>
  15. </html>

html 使用的 css 和 js 文件也是通过/static/路径请求的,两个文件都比较简单:

  1. .greeting {
  2. font-family: sans-serif;
  3. font-size: 15px;
  4. font-style: italic;
  5. font-weight: bold;
  6. }
  1. console.log("Hello World!")

"Hello World!"字体显示为 css 设置的样式,通过观察控制台也能看到 js 打印的信息。

再来看看文件目录浏览,在浏览器中请求localhost:8080/static/

可以依次点击列表中的文件查看其内容。

点击hello.css

点击hello.js

依次点击folderfile1.txt

静态文件的请求路径也会输出到运行服务器的控制台中:

  1. $ go run main.go
  2. ./static/
  3. ./static/hello.css
  4. ./static/hello.js
  5. ./static/folder/
  6. ./static/folder/file1.txt

原始方式的实现有一个缺点,实现逻辑复杂。上面的代码尽管我们已经忽略很多情况的处理了,代码量还是不小。自己编写很繁琐,而且容易产生 BUG。

静态文件服务的逻辑其实比较一致,应该通过库的形式来提供。为此,Go 语言提供了http.FileServer方法。

http.FileServer

先来看看如何使用:

  1. package main
  2. import (
  3. "log"
  4. "net/http"
  5. )
  6. func main() {
  7. mux := http.NewServeMux()
  8. mux.Handle("/static/", http.FileServer(http.Dir("")))
  9. server := &http.Server {
  10. Addr: ":8080",
  11. Handler: mux,
  12. }
  13. if err := server.ListenAndServe(); err != nil {
  14. log.Fatal(err)
  15. }
  16. }

上面的代码使用http.Server方法,几行代码就实现了与原始方式相同的效果,是不是很简单?这就是使用库的好处!

http.FileServer接受一个http.FileSystem接口类型的变量:

  1. // src/net/http/fs.go
  2. type FileSystem interface {
  3. Open(name string) (File, error)
  4. }

传入http.Dir类型变量,注意http.Dir是一个类型,其底层类型为string,并不是方法。因而http.Dir("")只是一个类型转换,而非方法调用:

  1. // src/net/http/fs.go
  2. type Dir string

http.Dir表示文件的起始路径,空即为当前路径。调用Open方法时,传入的参数需要在前面拼接上该起始路径得到实际文件路径。

http.FileServer的返回值类型是http.Handler,所以需要使用Handle方法注册处理器。http.FileServer将收到的请求路径传给http.DirOpen方法打开对应的文件或目录进行处理。

在上面的程序中,如果请求路径为/static/hello.html,那么拼接http.Dir的起始路径.,最终会读取路径为./static/hello.html的文件。

有时候,我们想要处理器的注册路径和http.Dir的起始路径不相同。有些工具在打包时会将静态文件输出到public目录中。

这时需要使用http.StripPrefix方法,该方法会将请求路径中特定的前缀去掉,然后再进行处理:

  1. package main
  2. import (
  3. "log"
  4. "net/http"
  5. )
  6. func main() {
  7. mux := http.NewServeMux()
  8. mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir("./public"))))
  9. server := &http.Server {
  10. Addr: ":8080",
  11. Handler: mux,
  12. }
  13. if err := server.ListenAndServe(); err != nil {
  14. log.Fatal(err)
  15. }
  16. }

这时,请求localhost:8080/static/hello.html将会返回./public/hello.html文件。

路径/static/index.html经过处理器http.StripPrefix去掉了前缀/static得到/index.html,然后又加上了http.Dir的起始目录./public得到文件最终路径./public/hello.html

除此之外,http.FileServer还会根据请求文件的后缀推断内容类型,更全面:

  1. // src/mime/type.go
  2. var builtinTypesLower = map[string]string{
  3. ".css": "text/css; charset=utf-8",
  4. ".gif": "image/gif",
  5. ".htm": "text/html; charset=utf-8",
  6. ".html": "text/html; charset=utf-8",
  7. ".jpeg": "image/jpeg",
  8. ".jpg": "image/jpeg",
  9. ".js": "application/javascript",
  10. ".mjs": "application/javascript",
  11. ".pdf": "application/pdf",
  12. ".png": "image/png",
  13. ".svg": "image/svg+xml",
  14. ".wasm": "application/wasm",
  15. ".webp": "image/webp",
  16. ".xml": "text/xml; charset=utf-8",
  17. }

如果文件后缀无法推断,http.FileServer将读取文件的前 512 个字节,根据内容来推断内容类型。感兴趣可以看一下源码src/net/http/sniff.go

http.ServeContent

除了直接使用http.FileServer之外,net/http库还暴露了ServeContent方法。这个方法可以用在处理器需要返回一个文件内容的时候,非常易用。

例如下面的程序,根据 URL 中的file参数返回对应的文件内容:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. "os"
  7. "time"
  8. )
  9. func ServeFileContent(w http.ResponseWriter, r *http.Request, name string, modTime time.Time) {
  10. f, err := os.Open(name)
  11. if err != nil {
  12. w.WriteHeader(500)
  13. fmt.Fprint(w, "open file error:", err)
  14. return
  15. }
  16. defer f.Close()
  17. fi, err := f.Stat()
  18. if err != nil {
  19. w.WriteHeader(500)
  20. fmt.Fprint(w, "call stat error:", err)
  21. return
  22. }
  23. if fi.IsDir() {
  24. w.WriteHeader(400)
  25. fmt.Fprint(w, "no such file:", name)
  26. return
  27. }
  28. http.ServeContent(w, r, name, fi.ModTime(), f)
  29. }
  30. func fileHandler(w http.ResponseWriter, r *http.Request) {
  31. query := r.URL.Query()
  32. filename := query.Get("file")
  33. if filename == "" {
  34. w.WriteHeader(400)
  35. fmt.Fprint(w, "filename is empty")
  36. return
  37. }
  38. ServeFileContent(w, r, filename, time.Time{})
  39. }
  40. func main() {
  41. mux := http.NewServeMux()
  42. mux.HandleFunc("/show", fileHandler)
  43. server := &http.Server {
  44. Addr: ":8080",
  45. Handler: mux,
  46. }
  47. if err := server.ListenAndServe(); err != nil {
  48. log.Fatal(err)
  49. }
  50. }

http.ServeContent除了接受参数http.ResponseWriterhttp.Request,还需要文件名name,修改时间modTimeio.ReadSeeker接口类型的参数。

modTime参数是为了设置响应的Last-Modified首部。如果请求中携带了If-Modified-Since首部,ServeContent方法会根据modTime判断是否需要发送内容。

如果需要发送内容,ServeContent方法从io.ReadSeeker接口重读取内容。*os.File实现了接口io.ReadSeeker

使用场景

Web 开发中的静态资源都可以使用http.FileServer来处理。除此之外,http.FileServer还可以用于实现一个简单的文件服务器,浏览或下载文件:

  1. package main
  2. import (
  3. "flag"
  4. "log"
  5. "net/http"
  6. )
  7. var (
  8. ServeDir string
  9. )
  10. func init() {
  11. flag.StringVar(&ServeDir, "sd", "./", "the directory to serve")
  12. }
  13. func main() {
  14. flag.Parse()
  15. mux := http.NewServeMux()
  16. mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(ServeDir))))
  17. server := &http.Server {
  18. Addr: ":8080",
  19. Handler: mux,
  20. }
  21. if err := server.ListenAndServe(); err != nil {
  22. log.Fatal(err)
  23. }
  24. }

在上面的代码中,我们构建了一个简单的文件服务器。编译之后,将想浏览的目录作为参数传给命令行选项,就可以浏览和下载该目录下的文件了:

  1. $ ./main.exe -sd D:/code/golang

可以将端口也作为命令行选项,这样做出一个通用的文件服务器,编译之后就可以在其它机器上使用了

Go Web 编程之 静态文件的更多相关文章

  1. Java web App 部署静态文件

    以 Tomcat 为例子,静态文件,如 html, css, js ,无需编译,所以只需要把文件复制到 Tomcat/webapps 目录下面某个子目录,便可以了. 例子: 1. 在 Tomcat/w ...

  2. 3 web服务器:静态文件

    1.处理客户端请求数据 >>> s = "GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: keep-alive& ...

  3. Web.Config 对静态文件 js css img 的客户端缓存策略

    <?xml version="1.0" encoding="utf-8"?> <configuration> <system.we ...

  4. Django基础 - Debug设置为False后静态文件获取404

    当设置setting.py文件当中的DEBUG=FALSE后,Django会默认使用Web Server的静态文件处理,故若没设置好Web Server对静态文件的处理的话,会出现访问静态文件404的 ...

  5. Django学习之十: staticfile 静态文件

    目录 Django学习之十: staticfile 静态文件 理解阐述 静态文件 Django对静态文件的处理 其它方面 总结 Django学习之十: staticfile 静态文件 理解阐述     ...

  6. Asp .Net core 2 学习笔记(3) —— 静态文件

    这个系列的初衷是便于自己总结与回顾,把笔记本上面的东西转移到这里,态度不由得谨慎许多,下面是我参考的资源: ASP.NET Core 中文文档目录 官方文档 记在这里的东西我会不断的完善丰满,对于文章 ...

  7. (5)ASP.NET Core 中的静态文件

    1.前言 当我们创建Core项目的时候,Web根目录下会有个wwwroot文件目录,wwwroot文件目录里面默认有HTML.CSS.IMG.JavaScript等文件,而这些文件都是Core提供给客 ...

  8. ASP.NET Core应用针对静态文件请求的处理[1]: 以Web的形式发布静态文件

    虽然ASP.NET Core是一款"动态"的Web服务端框架,但是在很多情况下都需要处理针对静态文件的请求,最为常见的就是这对JavaScript脚本文件.CSS样式文件和图片文件 ...

  9. Web的形式发布静态文件

    Web的形式发布静态文件 虽然ASP.NET Core是一款"动态"的Web服务端框架,但是在很多情况下都需要处理针对静态文件的请求,最为常见的就是这对JavaScript脚本文件 ...

随机推荐

  1. Python--day19--random模块

    random模块 >>> import random #随机小数 >>> random.random() # 大于0且小于1之间的小数 0.766433866365 ...

  2. TP5单例模式操作Model

    tp5单例模式的代码实现 为什么要使用单例模式 使用单例模式实现逻辑处理与数据库操作分离能很大提升mysql的sql处理能力,并且易于维护 ArticleModel.php <?php name ...

  3. 2019-9-2-本文说如何显示SVG

    title author date CreateTime categories 本文说如何显示SVG lindexi 2019-09-02 12:57:38 +0800 2018-2-13 17:23 ...

  4. 买房的贷款时间是否是越长越好?https://www.zhihu.com/question/20842791

    买房的贷款时间是否是越长越好?https://www.zhihu.com/question/20842791

  5. vue组件之间通过query传递参数

    需求: 从 任务列表进入 任务详情 ,向详情页传递当前 mission_id 值 路由关系: //查看任务列表 { path: '/worklist', name: 'worklist', compo ...

  6. Linux 字节序

    小心不要假设字节序. PC 存储多字节值是低字节为先(小端为先, 因此是小端), 一些高 级的平台以另一种方式(大端)工作. 任何可能的时候, 你的代码应当这样来编写, 它不在 乎它操作的数据的字节序 ...

  7. 2019-8-31-git-上传当前分支

    title author date CreateTime categories git 上传当前分支 lindexi 2019-08-31 16:55:59 +0800 2018-05-08 09:2 ...

  8. git 上传当前分支

    因为我现在的分支是的名很长,每次需要上次当前分支需要写很多代码,是不是有很简单方法上传当前分支. 如果要上传一个分支到仓库 origin 那么就需要使用下面的命令 git push origin 分支 ...

  9. 路由器OpenWrt如何脱机(离线)下载BT文件

    路由器OpenWrt如何脱机(离线)下载BT文件 1.首先到如下网址下载OpenWrt固件(确保为路由器正确型号). http://downloads.openwrt.org/snapshots/tr ...

  10. Centos6.5_x64-GitLab搭建私有GitHub

              GitLab,是一个利用 Ruby on Rails 开发的开源应用程序,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目. 它拥有与GitHub类似的功 ...