$ mkdir -p photoweb/uploads
$ cd photoweb
$ touch photoweb.go
package main import ( "io" "log" "net/http" ) func uploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { io.WriteString(w, "<form method="POST" action="/upload" "+ " enctype="multipart/form-data">"+ "Choose an image to upload: <input name="image" type="file" />"+ "<input type="submit" value="Upload" />"+ "</form>") return } } func main() { http.HandleFunc("/upload", uploadHandler) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } }可以看到,結合 main() 和 uploadHandler() 方法,針對 HTTP GET 方式請求 /upload 路徑,程式將會往 http.ResponseWriter 型別的範例物件 w 中寫入一段 HTML 文字,即輸出一個 HTML 上傳表單。
const ( UPLOAD_DIR = "./uploads" ) func uploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { io.WriteString(w, "<form method="POST" action="/upload" "+ " enctype="multipart/form-data">"+ "Choose an image to upload: <input name="image" type="file" />"+ "<input type="submit" value="Upload" />"+ "</form>") return } if r.Method == "POST" { f, h, err := r.FormFile("image") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } filename := h.Filename defer f.Close() t, err := os.Create(UPLOAD_DIR + "/" + filename) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer t.Close() if _, err := io.Copy(t, f); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/view?id="+filename, http.StatusFound) } }如果是用戶端發起的 HTTP POST 請求,那麼首先從表單提交過來的欄位尋找名為 image 的檔案域並對其接值,呼叫 r.FormFile() 方法會返回 3 個值,各個值的型別分別是 multipart.File、*multipart.FileHeader 和 error。
func viewHandler(w http.ResponseWriter, r *http.Request) { imageId = r.FormValue("id") imagePath = UPLOAD_DIR + "/" + imageId w.Header().Set("Content-Type", "image") http.ServeFile(w, r, imagePath) }在上述程式碼中,我們首先從用戶端請求中對引數進行接值。r.FormValue("id") 即可得到用戶端請求傳遞的圖片唯一 ID,然後我們將圖片 ID 結合之前儲存圖片用的目錄進行組裝,即可得到檔案在伺服器上的存放路徑。
func main() { http.HandleFunc("/view", viewHandler) http.HandleFunc("/upload", uploadHandler) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } }這樣當用戶端(瀏覽器)存取 /view 路徑並傳遞 id 引數時,即可直接以 HTTP 形式看到圖片的內容。在網頁上,將會呈現一張視覺化的圖片。
func viewHandler(w http.ResponseWriter, r *http.Request) { imageId = r.FormValue("id") imagePath = UPLOAD_DIR + "/" + imageId if exists := isExists(imagePath);!exists { http.NotFound(w, r) return } w.Header().Set("Content-Type", "image") http.ServeFile(w, r, imagePath) } func isExists(path string) bool { _, err := os.Stat(path) if err == nil { return true } return os.IsExist(err) }同時,我們增加了 isExists() 輔助函數,用於檢查檔案是否真的存在。
func listHandler(w http.ResponseWriter, r *http.Request) { fileInfoArr, err := ioutil.ReadDir("./uploads") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var listHtml string for _, fileInfo := range fileInfoArr { imgid := fileInfo.Name listHtml += "<li><a href="/view?id="+imgid+"">imgid</a></li>" } io.WriteString(w, "<ol>"+listHtml+"</ol>") }從上面的 listHandler() 方法中可以看到,程式先從 ./uploads 目錄中遍歷得到所有檔案並賦值到 fileInfoArr 變數裡。fileInfoArr 是一個陣列,其中的每一個元素都是一個檔案物件。
func main() { http.HandleFunc("/", listHandler) http.HandleFunc("/view", viewHandler) http.HandleFunc("/upload", uploadHandler) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } }這樣在存取網站首頁的時候,即可看到已上傳的所有圖片列表了。
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Upload</title> </head> <body> <form method="POST" action="/upload" enctype="multipart/form-data"> Choose an image to upload: <input name="image" type="file" /> <input type="submit" value="Upload" /> </form> </body> </html>然後新建一個名為 list.html 的檔案,內容如下:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>List</title> </head> <body> <ol> {{range $.images}} <li><a href="/view?id={{.|urlquery}}">{{.|html}}</a></li> {{end}} </ol> </body> </html>在上述模板中,雙大括號 {{}} 是區分模板程式碼和 HTML 的分隔符,括號裡邊可以是要顯示輸出的資料,或者是控制語句,比如 if 判斷式或者 range 迴圈體等。
package main import ( "io" "log" "net/http" "io/ioutil" "html/template" ) func uploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { t, err := template.ParseFiles("upload.html") if err != nil { http.Error(w, err.Error(),http.StatusInternalServerError) return } t.Execute(w, nil) return } if r.Method == "POST" { // ... } } func listHandler(w http.ResponseWriter, r *http.Request) { fileInfoArr, err := ioutil.ReadDir("./uploads") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } locals := make(map[string]interface{}) images := []string{} for _, fileInfo := range fileInfoArr { images = append(images, fileInfo.Name) } locals["images"] = images t, err := template.ParseFiles("list.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } t.Execute(w, locals) }在上面的程式碼中,template.ParseFiles() 函數將會讀取指定模板的內容並且返回一個 *template.Template 值。
func renderHtml(w http.ResponseWriter, tmpl string, locals map[string]interface{}) err error { t, err = template.ParseFiles(tmpl + ".html") if err != nil { return } err = t.Execute(w, locals) }有了 renderHtml() 這個通用的模板渲染方法,uploadHandler() 和 listHandler() 方法的程式碼可以再精簡些,如下:
func uploadHandler(w http.ResponseWriter, r *http.Request){ if r.Method == "GET" { if err := renderHtml(w, "upload", nil); err != nil{ http.Error(w, err.Error(), http.StatusInternalServerError) return } } if r.Method == "POST" { // ... } } func listHandler(w http.ResponseWriter, r *http.Request) { fileInfoArr, err := ioutil.ReadDir("./uploads") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } locals := make(map[string]interface{}) images := []string{} for _, fileInfo := range fileInfoArr { images = append(images, fileInfo.Name) } locals["images"] = images if err = renderHtml(w, "list", locals); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }當我們引入了 Go 標準庫中的 html/template 包,實現了業務邏輯層與表現層分離後,對模板渲染邏輯去重,編寫並使用通用模板渲染方法 renderHtml(),這讓業務邏輯處理層的程式碼看起來確實要清晰簡潔許多。
templates := make(map[string]*template.Template)
templates 是一個 map 型別的複合結構,map 的鍵(key)是字串型別,即模板的名字,值(value)是 *template.Template 型別。func init() { for _, tmpl := range []string{"upload", "list"} { t := template.Must(template.ParseFiles(tmpl + ".html")) templates[tmpl] = t } }在上面的程式碼中,我們在 template.ParseFiles() 方法的外層強制使用 template.Must() 進行封裝,template.Must() 確保了模板不能解析成功時,一定會觸發錯誤處理流程。之所以這麼做,是因為倘若模板不能成功載入,程式能做的唯一有意義的事情就是退出。
$ mkdir ./views $ mv *.html ./views
接著適當地對 init() 方法中的程式碼進行改寫,好讓程式初始化時即可預載入該目錄下的所有模板檔案,如下列程式碼所示:const ( TEMPLATE_DIR = "./views" ) templates := make(map[string]*template.Template) func init() { fileInfoArr, err := ioutil.ReadDir(TEMPLATE_DIR) if err != nil { panic(err) return } var templateName, templatePath string for _, fileInfo := range fileInfoArr { templateName = fileInfo.Name if ext := path.Ext(templateName); ext != ".html" { continue } templatePath = TEMPLATE_DIR + "/" + templateName log.Println("Loading template:", templatePath) t := template.Must(template.ParseFiles(templatePath)) templates[tmpl] = t } }同時,別忘了對 renderHtml() 的程式碼進行相應的調整:
func renderHtml(w http.ResponseWriter, tmpl string, locals map[string]interface{}) err error { err = templates[tmpl].Execute(w, locals) }此時,renderHtml() 函數的程式碼也變得更為簡潔。還好我們之前單獨封裝了 renderHtml() 函數,這樣全域性程式碼中只需更改這一個地方,這無疑是程式碼解耦的好處之一!
func check(err error) { if err != nil { panic(err) } }此時,我們可以將 photoweb 程式中出現的以下程式碼:
if err != nil { http.Error(w, err.Error(),http.StatusInternalServerError) return }統一替換為 check() 處理:
check(err)
錯誤處理雖然簡單很多,但是也帶來一個問題。由於發生錯誤觸發錯誤處理流程必然會引發程式停止執行,這種改法有點像搬起石頭砸自己的腳。func safeHandler(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer func() { if e, ok := recover().(error); ok { http.Error(w, err.Error(), http.StatusInternalServerError) // 或者輸出自定義的 50x 錯誤頁面 // w.WriteHeader(http.StatusInternalServerError) // renderHtml(w, "error", e) // logging log.Println("WARN: panic in %v - %v", fn, e) log.Println(string(debug.Stack())) } }() fn(w, r) } }在上述這段程式碼中,我們巧妙地使用了 defer 關鍵字搭配 recover() 方法終結 panic 的肆行。safeHandler() 接收一個業務邏輯處理常式作為引數,同時呼叫這個業務邏輯處理常式。該業
func main() { http.HandleFunc("/", safeHandler(listHandler)) http.HandleFunc("/view", safeHandler(viewHandler)) http.HandleFunc("/upload", safeHandler(uploadHandler)) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } }
[GET] /assets/css/*.css
[GET] /assets/js/*.js
[GET] /assets/images/*.js
const ( ListDir = 0x0001 ) func staticDirHandler(mux *http.ServeMux, prefix string, staticDir string, flags int) { mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) { file := staticDir + r.URL.Path[len(prefix)-1:] if (flags & ListDir) == 0 { if exists := isExists(file); !exists { http.NotFound(w, r) return } } http.ServeFile(w, r, file) }) }最後,我們需要稍微改動下 main() 函數:
func main() { mux := http.NewServeMux() staticDirHandler(mux, "/assets/", "./public", 0) mux.HandleFunc("/", safeHandler(listHandler)) mux.HandleFunc("/view", safeHandler(viewHandler)) mux.HandleFunc("/upload", safeHandler(uploadHandler)) err := http.ListenAndServe(":8080", mux) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } }如此即完美實現了靜態資源和動態請求的分離。
├── photoweb.go
├── public
├── css
├── images
└── js
├── uploads
└── views
├── list.html
└── upload.html
package main import ( "io" "log" "path" "net/http" "io/ioutil" "html/template" "runtime/debug" ) const ( ListDir = 0x0001 UPLOAD_DIR = "./uploads" TEMPLATE_DIR = "./views" ) templates := make(map[string]*template.Template) func init() { fileInfoArr, err := ioutil.ReadDir(TEMPLATE_DIR) check(err) var templateName, templatePath string for _, fileInfo := range fileInfoArr { templateName = fileInfo.Name if ext := path.Ext(templateName); ext != ".html" { continue } templatePath = TEMPLATE_DIR + "/" + templateName log.Println("Loading template:", templatePath) t := template.Must(template.ParseFiles(templatePath)) templates[tmpl] = t } } func check(err error) { if err != nil { panic(err) } } func renderHtml(w http.ResponseWriter, tmpl string, locals map[string]interface{}) { err := templates[tmpl].Execute(w, locals) check(err) } func isExists(path string) bool { _, err := os.Stat(path) if err == nil { return true } return os.IsExist(err) } func uploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { renderHtml(w, "upload", nil); } if r.Method == "POST" { f, h, err := r.FormFile("image") check(err) filename := h.Filename defer f.Close() t, err := ioutil.TempFile(UPLOAD_DIR, filename) check(err) defer t.Close() _, err := io.Copy(t, f) check(err) http.Redirect(w, r, "/view?id="+filename, http.StatusFound) } } func viewHandler(w http.ResponseWriter, r *http.Request) { imageId = r.FormValue("id") imagePath = UPLOAD_DIR + "/" + imageId if exists := isExists(imagePath);!exists { http.NotFound(w, r) return } w.Header().Set("Content-Type", "image") http.ServeFile(w, r, imagePath) } func listHandler(w http.ResponseWriter, r *http.Request) { fileInfoArr, err := ioutil.ReadDir("./uploads") check(err) locals := make(map[string]interface{}) images := []string{} for _, fileInfo := range fileInfoArr { images = append(images, fileInfo.Name) } locals["images"] = images renderHtml(w, "list", locals) } func safeHandler(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer func() { if e, ok := recover().(error); ok { http.Error(w, err.Error(), http.StatusInternalServerError) // 或者輸出自定義的50x錯誤頁面 // w.WriteHeader(http.StatusInternalServerError) // renderHtml(w, "error", e) // logging log.Println("WARN: panic in %v. - %v", fn, e) log.Println(string(debug.Stack())) } }() fn(w, r) } } func staticDirHandler(mux *http.ServeMux, prefix string, staticDir string, flags int) { mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) { file := staticDir + r.URL.Path[len(prefix)-1:] if (flags & ListDir) == 0 { if exists := isExists(file); !exists { http.NotFound(w, r) return } } http.ServeFile(w, r, file) }) } func main() { mux := http.NewServeMux() staticDirHandler(mux, "/assets/", "./public", 0) mux.HandleFunc("/", safeHandler(listHandler)) mux.HandleFunc("/view", safeHandler(viewHandler)) mux.HandleFunc("/upload", safeHandler(uploadHandler)) err := http.ListenAndServe(":8080", mux) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } }