Go Web 编程之 静态文件
概述
在 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
可以将端口也作为命令行选项,这样做出一个通用的文件服务器,编译之后就可以在其它机器上使用了
Go Web 编程之 静态文件的更多相关文章
- Java web App 部署静态文件
以 Tomcat 为例子,静态文件,如 html, css, js ,无需编译,所以只需要把文件复制到 Tomcat/webapps 目录下面某个子目录,便可以了. 例子: 1. 在 Tomcat/w ...
- 3 web服务器:静态文件
1.处理客户端请求数据 >>> s = "GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: keep-alive& ...
- Web.Config 对静态文件 js css img 的客户端缓存策略
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.we ...
- Django基础 - Debug设置为False后静态文件获取404
当设置setting.py文件当中的DEBUG=FALSE后,Django会默认使用Web Server的静态文件处理,故若没设置好Web Server对静态文件的处理的话,会出现访问静态文件404的 ...
- Django学习之十: staticfile 静态文件
目录 Django学习之十: staticfile 静态文件 理解阐述 静态文件 Django对静态文件的处理 其它方面 总结 Django学习之十: staticfile 静态文件 理解阐述 ...
- Asp .Net core 2 学习笔记(3) —— 静态文件
这个系列的初衷是便于自己总结与回顾,把笔记本上面的东西转移到这里,态度不由得谨慎许多,下面是我参考的资源: ASP.NET Core 中文文档目录 官方文档 记在这里的东西我会不断的完善丰满,对于文章 ...
- (5)ASP.NET Core 中的静态文件
1.前言 当我们创建Core项目的时候,Web根目录下会有个wwwroot文件目录,wwwroot文件目录里面默认有HTML.CSS.IMG.JavaScript等文件,而这些文件都是Core提供给客 ...
- ASP.NET Core应用针对静态文件请求的处理[1]: 以Web的形式发布静态文件
虽然ASP.NET Core是一款"动态"的Web服务端框架,但是在很多情况下都需要处理针对静态文件的请求,最为常见的就是这对JavaScript脚本文件.CSS样式文件和图片文件 ...
- Web的形式发布静态文件
Web的形式发布静态文件 虽然ASP.NET Core是一款"动态"的Web服务端框架,但是在很多情况下都需要处理针对静态文件的请求,最为常见的就是这对JavaScript脚本文件 ...
随机推荐
- php三目运算计算三个数最大值最小值
文章地址:https://www.cnblogs.com/sandraryan/ $x = 10; $y = 45; $z = 3; //求出三个数字中最大值最小值 //先比较x y,如果x> ...
- hdu 2126 Buy the souvenirs(记录总方案数的01背包)
Buy the souvenirs Time Limit: 10000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Other ...
- 深入java面向对象一:==和equals详解
本文从多篇博客笔记融合而来,系转载,非原创,参考: 1. http://www.cnblogs.com/e241138/archive/2012/09/16/2687981.html 2. htt ...
- tensorflow学习笔记(二十五):ConfigProto&GPU
tensorflow ConfigPrototf.ConfigProto一般用在创建session的时候.用来对session进行参数配置 with tf.Session(config = tf.Co ...
- Java 自带性能监控工具:监视和管理控制台 jconsole 的使用
1. 前言想验证你对 jvm 配的一些调优参数(比如 Xms.Xmx 等)有没有起作用吗?想不想实时监控你自定义的线程池的在实际运行时的线程个数.有没有死锁?应用出现 java.lang.OutOfM ...
- 为何D3D11的几个矩阵需要转置?
在学习D3D11的时候遇到一个问题,事情是这样的: D3D11引入了常量缓存(const buffer)用来实现数据的高速传输,这块儿buffer是CPU Only Write,GPU Only Re ...
- UE4 学习
1.官方文档:https://docs.unrealengine.com/en-US/index.html 2.入门教程: (1)C++ 程序员如何入门 UE 4:https://blog.csdn. ...
- Servlet 的面试题
Servlet运行在Servlet容器中,其生命周期由容器来管理.Servlet的生命周期通过javax.servlet.Servlet接口中的init().service()和destroy()方法 ...
- The Struts dispatcher cannot be found异常的解决方法
系统错误:HTTP Status 500 异常信息:The Struts dispatcher cannot be found. This is usually caused by using St ...
- JAVA8学习——深入浅出Lambda表达式(学习过程)
JAVA8学习--深入浅出Lambda表达式(学习过程) lambda表达式: 我们为什么要用lambda表达式 在JAVA中,我们无法将函数作为参数传递给一个方法,也无法声明返回一个函数的方法. 在 ...