在 Web 開發中,須要處理不少靜態資源文件,如 css/js 和圖片文件等。本文將介紹在 Go 語言中如何處理文件請求。 接下來,咱們將介紹兩種處理文件請求的方式:原始方式和http.FileServer
方法。javascript
原始方式比較簡單粗暴,直接讀取文件,而後返回給客戶端。css
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/
等。 代碼的其它部分與程序模板沒什麼不一樣,這裏就不贅述了。html
另外須要注意的是,這裏的註冊路徑/static/
最後的/
不能省略。咱們在前面的文章程序結構中介紹過,若是請求的路徑沒有精確匹配的處理,會逐步去掉路徑最後部分再次查找。 靜態文件的請求路徑通常爲/static/hello.html
這種形式。沒有精確匹配的路徑,繼而查找/static/
,這個路徑與/static
是不能匹配的。java
接下來,咱們看看文件處理器的實現:git
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
:github
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",
}
複製代碼
若是該路徑表示的是一個目錄,那麼返回目錄下全部文件與目錄的列表:golang
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 返回。用戶能夠點擊超連接訪問對應的文件或目錄。web
如何上述過程當中出現錯誤,咱們使用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 發佈!