Go Web 編程之 靜態文件
摘要:在上面的程序中,如果請求路徑爲 /static/hello.html ,那麼拼接 http.Dir 的起始路徑 . ,最終會讀取路徑爲 ./static/hello.html 的文件。http.FileServer 將收到的請求路徑傳給 http.Dir 的 Open 方法打開對應的文件或目錄進行處理。
概述
在 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
可以將端口也作爲命令行選項,這樣做出一個通用的文件服務器,編譯之後就可以在其它機器上使用了:grinning:。
總結
本文介紹瞭如何處理靜態文件,依次介紹了原始方式、 http.FileServer
和 http.ServeContent
。最後使用 http.FileServer
實現了一個簡單的文件服務器,可供日常使用。
參考
我
歡迎關注我的微信公衆號【GoUpUp】,共同學習,一起進步~
本文由博客一文多發平臺 OpenWrite 發佈!