概述
在 Web 开发中,需要处理很多静态资源文件,如 css/js 和图片文件等。本文将介绍在 Go 语言中如何处理文件请求。
接下来,我们将介绍两种处理文件请求的方式:原始方式和http.FileServer
方法。
原始方式
原始方式比较简单粗暴,直接读取文件,然后返回给客户端。
func main() { mux := http.NewServeMux() mux.HandleFunc("/static/", fileHandler) server := &http.Server { Addr: ":8080", Handler: mux, } if err := server.ListenAndServe(); err != nil { log.Fatal(err) } }
上面我们创建了一个文件处理器,将它挂载到路径/static/
上。一般地,静态文件的路径有一个共同的前缀,以便与其它路径区分。如这里的/static/
,还有一些常用的,例如/public/
等。
代码的其它部分与程序模板没什么不同,这里就不赘述了。
另外需要注意的是,这里的注册路径/static/
最后的/
不能省略。我们在前面的文章程序结构中介绍过,如果请求的路径没有精确匹配的处理,会逐步去掉路径最后部分再次查找。
静态文件的请求路径一般为/static/hello.html
这种形式。没有精确匹配的路径,继而查找/static/
,这个路径与/static
是不能匹配的。
接下来,我们看看文件处理器的实现:
func fileHandler(w http.ResponseWriter, r *http.Request) { path := "." + r.URL.Path fmt.Println(path) f, err := os.Open(path) if err != nil { Error(w, toHTTPError(err)) return } defer f.Close() d, err := f.Stat() if err != nil { Error(w, toHTTPError(err)) return } if d.IsDir() { DirList(w, r, f) return } data, err := ioutil.ReadAll(f) if err != nil { Error(w, toHTTPError(err)) return } ext := filepath.Ext(path) if contentType := extensionToContentType[ext]; contentType != "" { w.Header().Set("Content-Type", contentType) } w.Header().Set("Content-Length", strconv.FormatInt(d.Size(), 10)) w.Write(data) }
首先我们读出请求路径,再加上相对可执行文件的路径。一般地,static
目录与可执行文件在同一个目录下。然后打开该路径,查看信息。
如果该路径表示的是一个文件,那么根据文件的后缀设置Content-Type
,读取文件的内容并返回。代码中简单列举了几个后缀对应的Content-Type
:
var extensionToContentType = map[string]string { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript", ".xml": "text/xml; charset=utf-8", ".jpg": "image/jpeg", }
如果该路径表示的是一个目录,那么返回目录下所有文件与目录的列表:
func DirList(w http.ResponseWriter, r *http.Request, f http.File) { dirs, err := f.Readdir(-1) if err != nil { Error(w, http.StatusInternalServerError) return } sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, "<pre>\n") for _, d := range dirs { name := d.Name() if d.IsDir() { name += "/" } url := url.URL{Path: name} fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), name) } fmt.Fprintf(w, "</pre>\n") }
上面的函数先读取目录下第一层的文件和目录,然后按照名字排序。最后拼装成包含超链接的 HTML 返回。用户可以点击超链接访问对应的文件或目录。
如何上述过程中出现错误,我们使用toHTTPError
函数将错误转成对应的响应码,然后通过Error
回复给客户端。
func toHTTPError(err error) int { if os.IsNotExist(err) { return http.StatusNotFound } if os.IsPermission(err) { return http.StatusForbidden } return http.StatusInternalServerError } func Error(w http.ResponseWriter, code int) { w.WriteHeader(code) }
同级目录下static
目录内容:
static ├── folder │ ├── file1.txt │ └── file2.txt │ └── file3.txt ├── hello.css ├── hello.html ├── hello.js └── hello.txt
运行程序看看效果:
$ go run main.go
打开浏览器,请求localhost:8080/static/hello.html
:
可以看到页面hello.html
已经呈现了:
<!-- hello.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Go Web 编程之 静态文件</title> <link rel="stylesheet" href="/static/hello.css"> </head> <body> <p class="greeting">Hello World!</p> <script src="/static/hello.js"></script> </body> </html>
html 使用的 css 和 js 文件也是通过/static/
路径请求的,两个文件都比较简单:
.greeting { font-family: sans-serif; font-size: 15px; font-style: italic; font-weight: bold; }
console.log("Hello World!")
"Hello World!"字体显示为 css 设置的样式,通过观察控制台也能看到 js 打印的信息。
再来看看文件目录浏览,在浏览器中请求localhost:8080/static/
:
可以依次点击列表中的文件查看其内容。
点击hello.css
:
点击hello.js
:
依次点击folder
和file1.txt
:
静态文件的请求路径也会输出到运行服务器的控制台中:
$ go run main.go ./static/ ./static/hello.css ./static/hello.js ./static/folder/ ./static/folder/file1.txt
原始方式的实现有一个缺点,实现逻辑复杂。上面的代码尽管我们已经忽略很多情况的处理了,代码量还是不小。自己编写很繁琐,而且容易产生 BUG。
静态文件服务的逻辑其实比较一致,应该通过库的形式来提供。为此,Go 语言提供了http.FileServer
方法。
http.FileServer
先来看看如何使用:
package main import ( "log" "net/http" ) func main() { mux := http.NewServeMux() mux.Handle("/static/", http.FileServer(http.Dir(""))) server := &http.Server { Addr: ":8080", Handler: mux, } if err := server.ListenAndServe(); err != nil { log.Fatal(err) } }
上面的代码使用http.Server
方法,几行代码就实现了与原始方式相同的效果,是不是很简单?这就是使用库的好处!
http.FileServer
接受一个http.FileSystem
接口类型的变量:
// src/net/http/fs.go type FileSystem interface { Open(name string) (File, error) }
传入http.Dir
类型变量,注意http.Dir
是一个类型,其底层类型为string
,并不是方法。因而http.Dir("")
只是一个类型转换,而非方法调用:
// src/net/http/fs.go type Dir string
http.Dir
表示文件的起始路径,空即为当前路径。调用Open
方法时,传入的参数需要在前面拼接上该起始路径得到实际文件路径。
http.FileServer
的返回值类型是http.Handler
,所以需要使用Handle
方法注册处理器。http.FileServer
将收到的请求路径传给http.Dir
的Open
方法打开对应的文件或目录进行处理。
在上面的程序中,如果请求路径为/static/hello.html
,那么拼接http.Dir
的起始路径.
,最终会读取路径为./static/hello.html
的文件。
有时候,我们想要处理器的注册路径和http.Dir
的起始路径不相同。有些工具在打包时会将静态文件输出到public
目录中。
这时需要使用http.StripPrefix
方法,该方法会将请求路径中特定的前缀去掉,然后再进行处理:
package main import ( "log" "net/http" ) func main() { mux := http.NewServeMux() mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir("./public")))) server := &http.Server { Addr: ":8080", Handler: mux, } if err := server.ListenAndServe(); err != nil { log.Fatal(err) } }
这时,请求localhost:8080/static/hello.html
将会返回./public/hello.html
文件。
路径/static/index.html
经过处理器http.StripPrefix
去掉了前缀/static
得到/index.html
,然后又加上了http.Dir
的起始目录./public
得到文件最终路径./public/hello.html
。
除此之外,http.FileServer
还会根据请求文件的后缀推断内容类型,更全面:
// src/mime/type.go var builtinTypesLower = map[string]string{ ".css": "text/css; charset=utf-8", ".gif": "image/gif", ".htm": "text/html; charset=utf-8", ".html": "text/html; charset=utf-8", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".js": "application/javascript", ".mjs": "application/javascript", ".pdf": "application/pdf", ".png": "image/png", ".svg": "image/svg+xml", ".wasm": "application/wasm", ".webp": "image/webp", ".xml": "text/xml; charset=utf-8", }
如果文件后缀无法推断,http.FileServer
将读取文件的前 512 个字节,根据内容来推断内容类型。感兴趣可以看一下源码src/net/http/sniff.go
。
http.ServeContent
除了直接使用http.FileServer
之外,net/http
库还暴露了ServeContent
方法。这个方法可以用在处理器需要返回一个文件内容的时候,非常易用。
例如下面的程序,根据 URL 中的file
参数返回对应的文件内容:
package main import ( "fmt" "log" "net/http" "os" "time" ) func ServeFileContent(w http.ResponseWriter, r *http.Request, name string, modTime time.Time) { f, err := os.Open(name) if err != nil { w.WriteHeader(500) fmt.Fprint(w, "open file error:", err) return } defer f.Close() fi, err := f.Stat() if err != nil { w.WriteHeader(500) fmt.Fprint(w, "call stat error:", err) return } if fi.IsDir() { w.WriteHeader(400) fmt.Fprint(w, "no such file:", name) return } http.ServeContent(w, r, name, fi.ModTime(), f) } func fileHandler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() filename := query.Get("file") if filename == "" { w.WriteHeader(400) fmt.Fprint(w, "filename is empty") return } ServeFileContent(w, r, filename, time.Time{}) } func main() { mux := http.NewServeMux() mux.HandleFunc("/show", fileHandler) server := &http.Server { Addr: ":8080", Handler: mux, } if err := server.ListenAndServe(); err != nil { log.Fatal(err) } }
http.ServeContent
除了接受参数http.ResponseWriter
和http.Request
,还需要文件名name
,修改时间modTime
和io.ReadSeeker
接口类型的参数。
modTime
参数是为了设置响应的Last-Modified
首部。如果请求中携带了If-Modified-Since
首部,ServeContent
方法会根据modTime
判断是否需要发送内容。
如果需要发送内容,ServeContent
方法从io.ReadSeeker
接口重读取内容。*os.File
实现了接口io.ReadSeeker
。
使用场景
Web 开发中的静态资源都可以使用http.FileServer
来处理。除此之外,http.FileServer
还可以用于实现一个简单的文件服务器,浏览或下载文件:
package main import ( "flag" "log" "net/http" ) var ( ServeDir string ) func init() { flag.StringVar(&ServeDir, "sd", "./", "the directory to serve") } func main() { flag.Parse() mux := http.NewServeMux() mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(ServeDir)))) server := &http.Server { Addr: ":8080", Handler: mux, } if err := server.ListenAndServe(); err != nil { log.Fatal(err) } }
在上面的代码中,我们构建了一个简单的文件服务器。编译之后,将想浏览的目录作为参数传给命令行选项,就可以浏览和下载该目录下的文件了:
$ ./main.exe -sd D:/code/golang
可以将端口也作为命令行选项,这样做出一个通用的文件服务器,编译之后就可以在其它机器上使用了😀。
总结
本文介绍了如何处理静态文件,依次介绍了原始方式、http.FileServer
和http.ServeContent
。最后使用http.FileServer
实现了一个简单的文件服务器,可供日常使用。
参考
我
欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~
本文由博客一文多发平台 OpenWrite 发布!
来源:https://www.cnblogs.com/darjun/p/12190173.html