這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
go是一門簡潔強大的語言,簡單體驗之後覺得對於網路和命令列的支援也非常棒,本文介紹一下go實現靜態伺服器的大致流程。
基礎實現
最近接手了gobyexample的翻譯工作,將項目重構後需要本地的測試環境。
由於想要頁面的url顯示為“https://gobyexample.xgwang.me/hello-world”這種結尾不帶“/”的形式,子頁面沒有帶上html,並且有圖片資源因此需要一個static server。
根據golang wiki,實現這個簡單server只需要...一行代碼:
package mainimport "net/http"func main() { panic(http.ListenAndServe(":8080", http.FileServer(http.Dir("/usr/share/doc"))))}
加入log後稍微改寫一下,放在我們項目的tools目錄下:
package mainimport ( "log" "net/http")func main() { // Simple static webserver: port := ":8080" log.Printf("Serving at: http://localhost%s\n", port) err := http.ListenAndServe(port, http.FileServer(http.Dir("public"))) if err != nil { log.Fatal("ListenAndServe fail:", err) }}
再來一個可執行檔tools/serve檔案
#!/bin/bashexec go run tools/serve.go
ok現在只需要tools/serve就可以啟動這個伺服器了。
404
一切看起來很正常,但如果我們訪問一下不存在的某個頁面,404.html並不會被serve,這是因為go提供的FileServer並不知道我們自訂的404頁面。
所以我們需要將http.FileServer改為一個自訂的Handler。
寫go的時候體驗特別好的一點就是go官方團隊提供了很opinionated的convention,比如go-get,go-fmt等。
在我們輸入http.FileServer時會自動在imports中添加相應的庫,跳轉到源碼後看到了這個函數的實現:
type fileHandler struct { root FileSystem}// FileServer returns a handler that serves HTTP requests// with the contents of the file system rooted at root.//// To use the operating system's file system implementation,// use http.Dir://// http.Handle("/", http.FileServer(http.Dir("/tmp")))//// As a special case, the returned file server redirects any request// ending in "/index.html" to the same path, without the final// "index.html".func FileServer(root FileSystem) Handler { return &fileHandler{root}}func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) { upath := r.URL.Path if !strings.HasPrefix(upath, "/") { upath = "/" + upath r.URL.Path = upath } serveFile(w, r, f.root, path.Clean(upath), true)}
於是我們知道了這裡的函數需要返回的Handler有一個ServeHTTP方法。但是這裡的serveFile並不能直接由http.serveFile調用:go規定一個package內小寫字母開頭的均為私人,不能被外部package訪問。
但是沒有關係,我們可以在fileHandler上再封裝一層代理,在執行完我們判斷檔案存在的邏輯後執行原先所有fileHandler.ServeHTTP的內容,修改後的代碼如下:
type fileHandler struct { root http.FileSystem h http.Handler}func fileServer(root http.FileSystem, h http.Handler) http.Handler { return &fileHandler{root, h}}func (f *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if _, err := os.Stat("public/" + path); os.IsNotExist(err) { http.ServeFile(w, r, "public/404.html") return } f.h.ServeHTTP(w, r)}func main() { // Simple static webserver: port := ":8080" log.Printf("Serving at: http://localhost%s\n", port) fs := http.Dir("public") http.Handle("/", fileServer(&fs, http.FileServer(&fs))) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe fail:", err) }}
在傳入FileSystem的時候傳入指標也避免建立,很有C的感覺。
小細節
準系統都已經實現,但作為一個命令列工具,希望再進行一些完善。
首先我們需要支援傳參,go對於命令列參數的支援非常棒,只要引入builtin的flag包之後,我們加入
port := flag.String("port", ":8080", "localhost port to serve")path := flag.String("path", "public", "public files path")flag.Parse()
就可以得到*string類型的命令列參數,並且天生支援預設值和描述,測試一下go run tools/serve.go -h,可以得到:
Usage of /var/folders/sd/cwk5fwtd4ms5vflhq5_0_5rr0000gn/T/go-build178666598/command-line-arguments/_obj/exe/serve: -path string public files path (default "public") -port string localhost port to serve (default ":8080")
準備serve檔案之前,再輸出一下帶有格式的資訊加粗一下我們傳入的參數:
log.Printf("Serving \x1b[1m%s\x1b[0m at: http://localhost\x1b[1m%s\x1b[0m\n", *path, *port)
這裡\x1b[0m代表“All attributes off(color at startup)”,\x1b[1m代表“Bold on(enable foreground intensity)”。
總結
go作為靜態語言擁有可以與動態語言媲美的靈活性,有完整易用的工具鏈和豐富的標準庫,是2017年增長最快的語言,簡單的同時非常強大。
希望有更多的人可以一起學習go,我正在完善Go By Example的翻譯,歡迎閱讀以及貢獻PR!
閱讀原文