概述

在 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

依次点击folderfile1.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.DirOpen方法打开对应的文件或目录进行处理。

在上面的程序中,如果请求路径为/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.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还可以用于实现一个简单的文件服务器,浏览或下载文件:

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 编程之 静态文件的更多相关文章

  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. TP5单例模式操作Model

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

  2. Native memory allocation (mmap) failed to map xxx bytes for committing reserved memory

    遇到问题 在服务器上运行 nexus 出现Native memory allocation (mmap) failed to map 838860800 bytes for committing re ...

  3. python进阶之面向对象初识

    面向对象 不同于面向过程的编程思想,面向对象是一种将程序抽象为一个个对象的编程思想,能更好的处理现实世界到的一些复杂问题. 通过下面的例子来初步了解面向对象的编程方式. class Person: # ...

  4. JAVA核心知识点--打包 FatJar 方法小结

    目录 什么是 FatJar 三种打包方法 1. 非遮蔽方法(Unshaded) 2. 遮蔽方法(Shaded) 3. 嵌套方法(Jar of Jars) 小结 参考阅读 原文地址:https://yq ...

  5. tensorflow在文本处理中的使用——Doc2Vec情感分析

    代码来源于:tensorflow机器学习实战指南(曾益强 译,2017年9月)——第七章:自然语言处理 代码地址:https://github.com/nfmcclure/tensorflow-coo ...

  6. multipart/form-data与application/octet-stream的区别、application/x-www-form-urlencoded

    情景再现: 上传文件到.net(wcf)后台时,总是上传不成功,后台要求Content-Type是application/octet-stream,我使用multipart/form-data方式,c ...

  7. H3C开启Ssh

    [H3C]ssh server enable                       //开启SSH服务 [H3C]user-interface vty 0 4 [H3C-ui-vty0-4]au ...

  8. jquery银行电子账单表格填入和编辑插件

    jquery银行电子账单表格填入和编辑 前段时间做的一个银行表格账单的jquery插件,用于金额写入和编辑的应用,希望对大家有所帮助,发现问题欢迎回复 ;(function($){ function ...

  9. javascript修改css样式表

    //创建var sheet=document.createElement('style');document.bodt.appendChild(sheet);sheet.styleSheet.cssT ...

  10. sql临时表与变量表

    1)临时表存储在 tempdb 中,当不再使用时会自动删除 一般使用如下: --创建临时表 select * into #temp from TABLE --使用临时表 select * from # ...