Go Web 编程之 静态文件

栏目: IT技术 · 发布时间: 4年前

内容简介:在 Web 开发中,需要处理很多静态资源文件,如 css/js 和图片文件等。本文将介绍在 Go 语言中如何处理文件请求。接下来,我们将介绍两种处理文件请求的方式:原始方式和原始方式比较简单粗暴,直接读取文件,然后返回给客户端。

概述

在 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

Go Web 编程之 静态文件

可以看到页面 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>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/

Go Web 编程之 静态文件

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

点击 hello.css

Go Web 编程之 静态文件

点击 hello.js

Go Web 编程之 静态文件

依次点击 folderfile1.txt

Go Web 编程之 静态文件

Go Web 编程之 静态文件

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

$ 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

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

总结

本文介绍了如何处理静态文件,依次介绍了原始方式、 http.FileServerhttp.ServeContent 。最后使用 http.FileServer 实现了一个简单的文件服务器,可供日常使用。

参考

  1. Go Web 编程
  2. net/http 文档

我的博客

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

Go Web 编程之 静态文件

本文由博客一文多发平台 OpenWrite 发布!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

硅谷增长黑客实战笔记

硅谷增长黑客实战笔记

曲卉 / 机械工业出版社 / 2018-4-10 / 65.00元

增长黑客这个词源于硅谷,简单说,这是一群以数据驱动营销、以迭代验证策略,通过技术手段实现爆发式增长的新型人才。近年来,互联网公司意识到这一角色可以发挥四两拨千斤的作用,因此对该职位的需求也如井喷式增长。 本书作者曾在增长黑客之父肖恩•埃利斯麾下担任增长负责人,用亲身经历为你总结出增长黑客必备的套路、内力和兵法。本书不仅有逻辑清晰的理论体系、干货满满的实践心得,还有Pinterest、SoFi......一起来看看 《硅谷增长黑客实战笔记》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换