本节我们将综合之前介绍的网站开发相关知识,一步步介绍如何开发一个虽然简单但五脏俱全的相册网站。
## 新建工程
首先创建一个用于存放工程源代码的目录并切换到该目录中去,随后创建一个名为 photoweb.go 的文件,用于后面编辑我们的代码:
$ mkdir -p photoweb/uploads
$ cd photoweb
$ touch photoweb.go
我们的示例程序不是再造一个 Flickr 那样的网站或者比其更强大的图片分享网站,虽然我们可能很想这么玩。不过还是先让我们快速开发一个简单的网站小程序,暂且只实现以下最基本的几个功能:
* 支持图片上传;
* 在网页中可以查看已上传的图片;
* 能看到所有上传的图片列表;
* 可以删除指定的图片。
功能不多,也很简单。在大概了解上一节中的网页输出 Hello world 示例后,想必你已经知道可以引入 net/http 包来提供更多的路由分派并编写与之对应的业务逻辑处理方法,只不过会比输出一行 Hello, world! 多一些环节,还有些细节需要关注和处理。
## 使用 net/http 包提供网络服务
接下来,我们继续使用 Go 标准库中的 net/http 包来一步步构建整个相册程序的网络服务。
#### 1) 上传图片
先从最基本的图片上传着手,具体代码如下所示。
~~~
package mainimport ( "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 上传表单。
如果我们使用浏览器访问这个地址,那么网页上将会是一个可以上传文件的表单。光有上传表单还不能完成图片上传,服务端程序还必须有接收上传图片的相关处理。针对上传表单提交过来的文件,我们对 uploadHandler() 方法再添加些业务逻辑程序:
~~~
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。
如果上传的图片接收不成功,那么在示例程序中返回一个 HTTP 服务端的内部错误给客户端。如果上传的图片接收成功,则将该图片的内容复制到一个临时文件里。如果临时文件创建失败,或者图片副本保存失败,都将触发服务端内部错误。
如果临时文件创建成功并且图片副本保存成功,即表示图片上传成功,就跳转到查看图片页面。此外,我们还定义了两个 defer 语句,无论图片上传成功还是失败,当 uploadHandler() 方法执行结束时,都会先关闭临时文件句柄,继而关闭图片上传到服务器文件流的句柄。
别忘了在程序开头引入 io/ioutil 这个包,因为示例程序中用到了 ioutil.TempFile() 这个方法。
当图片上传成功后,我们即可在网页上查看这张图片,顺便确认图片是否真正上传到了服务端。接下来在网页中呈现这张图片。
#### 2) 在网页上显示图片
要在网页中显示图片,必须有一个可以访问到该图片的网址。在前面的示例代码中,图片上传成功后会跳转到 /view?id= 这样的网址,因此我们的程序要能够将对 /view 路径的访问映射到某个具体的业务逻辑处理方法。
首先,在 photoweb 程序中新增一个名为 viewHanlder() 的方法,其代码如下:
~~~
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 结合之前保存图片用的目录进行组装,即可得到文件在服务器上的存放路径。
接着,调用 http.ServeFile() 方法将该路径下的文件从磁盘中读取并作为服务端的返回信息输出给客户端。同时,也将 HTTP 响应头输出格式预设为 image 类型。
这是一种比较简单的示意写法,实际上应该严谨些,准确解析出文件的 MimeType 并将其作为 Content-Type 进行输出,具体可参考 Go语言标准库中的 http.DetectContentType() 方法和 mime 包提供的相关方法。
完成 viewHandler() 的业务逻辑后,我们将该方法注册到程序的 main() 方法中,与 /view 路径访问形成映射关联。main() 方法的代码如下:
~~~
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 形式看到图片的内容。在网页上,将会呈现一张可视化的图片。
#### 3) 处理不存在的图片访问
理论上,只要是 uploads/ 目录下有的图片,都能够访问到,但我们还是假设有意外情况,比如网址中传入的图片 ID 在 uploads/ 没有对应的文件,这时,我们的 viewHandler() 方法就显得很脆弱了。
不管是给出友好的错误提示还是返回 404 页面,都应该对这种情况作相应处理。我们不妨先以最简单有效的方式对其进行处理,修改 viewHandler() 方法,具体如下:
~~~
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() 辅助函数,用于检查文件是否真的存在。
#### 4) 列出所有已上传图片
应该有个入口,可以看到所有已上传的图片。对于所有列出的这些图片,我们可以选择进行查看或者删除等操作。下面假设在访问首页时列出所有上传的图片。
由于我们将客户端上传的图片全部保存在工程的 ./uploads 目录下,所以程序中应该有个名叫 listHandler() 的方法,用于在网页上列出该目录下存放的所有文件。暂时我们不考虑以缩略图的形式列出所有已上传图片,只需列出可供访问的文件名称即可。下面我们就来实现这个 listHandler() 方法:
~~~
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 是一个数组,其中的每一个元素都是一个文件对象。
然后,程序遍历 fileInfoArr 数组并从中得到图片的名称,用于在后续的 HTML 片段中显示文件名和传入的参数内容。listHtml 变量用于在 for 循序中将图片名称一一串联起来生成一段 HTML,最后调用 io.WriteString() 方法将这段 HTML 输出返回给客户端。
然后在 photoweb. go 程序的 main() 方法中,我们将对首页的访问映射到 listHandler() 方法。main() 方法的代码如下:
~~~
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()) }}
~~~
这样在访问网站首页的时候,即可看到已上传的所有图片列表了。
不过,你是否注意到一个事实,我们在 photoweb.go 程序的 uploadHandler() 和 listHandler() 方法中都使用 io.WriteString() 方法输出 HTML。
正如你想到的那样,在业务逻辑处理程序中混杂 HTML 可不是什么好事情,代码多起来后会导致程序不够清晰,而且改动程序里边的 HTML 文本时,每次都要重新编译整个工程的源代码才能看到修改后的效果。
正确的做法是,应该将业务逻辑程序和表现层分离开来,各自单独处理。这时候,就需要使用网页模板技术了。
Go 标准库中的 html/template 包对网页模板有着良好的支持。接下来,让我们来了解如何在 photoweb.go 程序中用上 Go 的模板功能。
## 渲染网页模板
使用 Go 标准库提供的 html/template 包,可以让我们将 HTML 从业务逻辑程序中抽离出来形成独立的模板文件,这样业务逻辑程序只负责处理业务逻辑部分和提供模板需要的数据,模板文件负责数据要表现的具体形式。
然后模板解析器将这些数据以定义好的模板规则结合模板文件进行渲染,最终将渲染后的结果一并输出,构成一个完整的网页。
下面我们把 photoweb.go 程序的 uploadHandler() 和 listHandler() 方法中的 HTML 文本 抽出,生成模板文件。
新建一个名为 upload.html 的文件,内容如下:
~~~
<!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 循环体等。
range 语句在模板中是一个循环过程体,紧跟在 range 后面的必须是一个 array、slice 或 map 类型的变量。在 list.html 模板中,images 是一组 string 类型的切片。
在使用 range 语句遍历的过程中,. 即表示该循环体中的当前元素,.|formatter 表示对当前这个元素的值以 formatter 方式进行格式化输出,比如 .|urlquery} 即表示对当前元素的值进行转换以适合作为 URL 一部分,而 {{.|html 表示对当前元素的值进行适合用于 HTML 显示的字符转化,比如">"会被转义成">"。
如果 range 关键字后面紧跟的是 map 这样的多维复合结构,循环体中的当前元素可以用 .key1.key2.keyN 这样的形式表示。
如果要更改模板中默认的分隔符,可以使用 template 包提供的 Delims() 方法。
在了解模板语法后,接着我们修改 photoweb.go 源文件,引入 html/template 包,并修改 uploadHandler() 和 listHandler() 方法,具体如下所示。
~~~
package mainimport ( "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 值。
t.Execute() 方法会根据模板语法来执行模板的渲染,并将渲染后的结果作为 HTTP 的返回数据输出。
在 uploadHandler() 方法和 listHandler() 方法中,均调用了 template.ParseFiles() 和 t.Execute() 这两个方法。根据 DRY(Don’t Repeat Yourself)原则,我们可以将模板渲染代码分离出来,单独编写一个处理函数,以便其他业务逻辑处理函数都可以使用。于是,我们可以定义一个名为 renderHtml() 的方法用来渲染模板:
~~~
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(),这让业务逻辑处理层的代码看起来确实要清晰简洁许多。
不过,直觉敏锐的你可能已经发现,无论是重构后的 uploadHandler() 还是 listHandler() 方法,每次调用这两个方法时都会重新读取并渲染模板。很明显,这很低效,也比较浪费资源,有没有一种办法可以让模板只加载一次呢?
答案是肯定的,聪明的你可能已经想到怎么对模板进行缓存了。
## 模板缓存
对模板进行缓存,即指一次性预加载模板。我们可以在 photoweb 程序初始化运行的时候,将所有模板一次性加载到程序中。正好 Go 的包加载机制允许我们在 init() 函数中做这样的事情,init() 会在 main() 函数之前执行。
首先,我们在 photoweb 程序中声明并初始化一个全局变量 templates,用于存放所有模板内容:
templates := make(map\[string\]\*template.Template)
templates 是一个 map 类型的复合结构,map 的键(key)是字符串类型,即模板的名字,值(value)是 \*template.Template 类型。
接着,我们在 photoweb 程序的 init() 函数中一次性加载所有模板:
~~~
func init() { for _, tmpl := range []string{"upload", "list"} { t := template.Must(template.ParseFiles(tmpl + ".html")) templates[tmpl] = t }}
~~~
在上面的代码中,我们在 template.ParseFiles() 方法的外层强制使用 template.Must() 进行封装,template.Must() 确保了模板不能解析成功时,一定会触发错误处理流程。之所以这么做,是因为倘若模板不能成功加载,程序能做的唯一有意义的事情就是退出。
在 range 语句中,包含了我们希望加载的 upload.html 和 list.html 两个模板,如果我们想加载更多模板,只需往这个数组中添加更多元素即可。当然,最好的办法应该是将所有 HTML 模板文件统一放到一个子文件夹中,然后对这个模板文件夹进行遍历和预加载。
如果需要加载新的模板,只需在这个文件夹中新建模板即可。这样做的好处是不用反复修改代码即可重新编译程序,而且实现了业务层和表现层真正意义上的分离。
不妨让我们这样试试看!
首先创建一个名为 ./views 的目录,然后将当前目录下所有 html 文件移动到该目录下:
$ 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() 函数,这样全局代码中只需更改这一个地方,这无疑是代码解耦的好处之一!
## 错误处理
在前面的代码中,有不少地方对于出错处理都是直接返回 http.Error() 50x 系列的服务端内部错误。从 DRY 的原则来看,不应该在程序中到处使用一样的代码。我们可以定义一个名为 check() 的方法,用于统一捕获 50x 系列的服务端内部错误:
~~~
func check(err error) { if err != nil { panic(err) }}
~~~
此时,我们可以将 photoweb 程序中出现的以下代码:
~~~
if err != nil { http.Error(w, err.Error(),http.StatusInternalServerError) return}
~~~
统一替换为 check() 处理:
check(err)
错误处理虽然简单很多,但是也带来一个问题。由于发生错误触发错误处理流程必然会引发程序停止运行,这种改法有点像搬起石头砸自己的脚。
其实我们可以换一种思维方式。尽管我们从书写上能保证大多数错误都能得到相应的处理,但根据墨菲定律,有可能出问题的地方就一定会出问题,在计算机程序里尤其如此。如果程序中我们正确地处理了 99 个错误,但若有一个系统错误意外导致程序出现异常,那么程序同样还是会终止运行。
我们不能预计一个工程里边会出现多少意外的情况,但是不管什么意外,只要会触发错误处理流程,我们就有办法对其进行处理。如果这样思考,那么前面这种改法又何尝不是置死地而后生呢?
接下来,让我们了解如何处理 panic 导致程序崩溃的情况。
## 巧用闭包避免程序运行时出错崩溃
Go 支持闭包。闭包可以是一个函数里边返回的另一个匿名函数,该匿名函数包含了定义在它外面的值。使用闭包,可以让我们网站的业务逻辑处理程序更安全地运行。
我们可以在 photoweb 程序中针对所有的业务逻辑处理函数(listHandler()、viewHandler() 和 uploadHandler())再进行一次包装。
在如下的代码中,我们定义了一个名为 safeHandler() 的函数,该函数有一个参数并且返回一个值,传入的参数和返回值都是一个函数,且都是http.HandlerFunc类型,这种类型的函数有两个参数:http.ResponseWriter 和 \*http.Request。
函数规格同 photoweb 的业务逻辑处理函数完全一致。事实上,我们正是要把业务逻辑处理函数作为参数传入到 safeHandler() 方法中,这样任何一个错误处理流程向上回溯的时候,我们都能对其进行拦截处理,从而也能避免程序停止运行:
~~~
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() 接收一个业务逻辑处理函数作为参数,同时调用这个业务逻辑处理函数。该业
务逻辑函数执行完毕后,safeHandler() 中 defer 指定的匿名函数会执行。
倘若业务逻辑处理函数里边引发了 panic,则调用 recover() 对其进行检测,若为一般性的错误,则输出 HTTP 50x 出错信息并记录日志,而程序将继续良好运行。
要应用 safeHandler() 函数,只需在 main() 中对各个业务逻辑处理函数做一次包装,如下面的代码所示:
~~~
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()) }}
~~~
## 动态请求和静态资源分离
你一定还有一个疑问,那就是前面的业务逻辑层都是动态请求,但若是针对静态资源(比如 CSS 和[JavaScript](http://c.biancheng.net/js/)等),是没有业务逻辑处理的,只需提供静态输出。在 Go 里边,这当然是可行的。
还记得前面我们在 viewHandler() 函数里边有用到 http.ServeFile() 这个方法吗?net/http 包提供的这个 ServeFile() 函数可以将服务端的一个文件内容读写到 http.Response-Writer 并返回给请求来源的 \*http.Request 客户端。
用前面介绍的闭包技巧结合这个 http.ServeFile() 方法,我们就能轻而易举地实现业务逻辑的动态请求和静态资源的完全分离。
假设我们有 ./public 这样一个存放 css/、js/、images/ 等静态资源的目录,原则上所有如下的请求规则都指向该 ./public 目录下相对应的文件:
\[GET\] /assets/css/\*.css
\[GET\] /assets/js/\*.js
\[GET\] /assets/images/\*.js
然后,我们定义一个名为 staticDirHandler() 的方法,用于实现上述需求:
~~~
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()) }}
~~~
如此即完美实现了静态资源和动态请求的分离。
当然,我们要思考是否确实需要用 Go 来提供静态资源的访问。如果使用外部 Web 服务器(比如 Nginx 等),就没必要使用 Go 编写的静态文件服务了。在本机做开发时有一个程序内置的静态文件服务器还是很实用的。
## 重构
经过前面对 photoweb 程序一一重整之后,整个工程的目录结构如下:
├── photoweb.go
├── public
├── css
├── images
└── js
├── uploads
└── views
├── list.html
└── upload.html
photoweb.go 程序的源码最终如下所示。
~~~
纯文本复制
~~~
~~~
package mainimport ( "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()) }}
~~~
## 更多资源
Go 的第三方库很丰富,无论是对于关系型数据库驱动还是非关系型的键值存储系统的接入,都有着良好的支持,而且还有丰富的 Go语言 Web 开发框架以及用于 Web 开发的相关工具包。可以访问 http://godashboard.appspot.com/project,了解更多第三方库的详细信息。
- 1.Go语言环境搭建
- 1.1 安装与环境
- 1.2 国内镜像配置
- 1.3 IDE的选择
- 2.Go语言基础语法
- 2.1 Go语言变量的声明
- 2.2 Go语言变量的初始化
- 2.3 Go语言多个变量同时赋值
- 2.4 Go语言匿名变量
- 2.5 Go语言变量的作用域
- 2.6 Go语言整型
- 2.7 Go语言浮点类型
- 2.8 Go语言复数
- 2.9 Go语言输出正弦函数(Sin)图像
- 2.10 Go语言bool类型
- 2.11 Go语言字符串
- 2.12 Go语言字符类型
- 2.13 Go语言数据类型转换
- 2.14 Go语言指针详解
- 2.15 Go语言变量逃逸分析
- 2.16 Go语言变量的生命周期
- 2.17 Go语言常量和const关键字
- 2.18 Go语言模拟枚举
- 2.19 Go语言type关键字
- 2.20 Go语言注释的定义及使用
- 2.21 Go语言关键字与标识符简述
- 2.22 Go语言运算符的优先级
- 2.23 Go语言strconv包
- 3.Go语言容器
- 3.1 Go语言数组详解
- 3.2 Go语言多维数组简述
- 3.3 Go语言切片详解
- 3.4 Go语言append()为切片添加元素
- 3.5 Go语言切片复制
- 3.6 Go语言从切片中删除元素
- 3.7 Go语言range关键字
- 3.8 Go语言多维切片简述
- 3.9 Go语言map
- 3.10 Go语言遍历map
- 3.11 Go语言map元素的删除和清空
- 3.12 Go语言sync.Map
- 3.13 Go语言list
- 3.14 Go语言nil
- 3.15 Go语言make和new关键字的区别及实现原理
- 4.Go语言流程控制
- 4.1 Go语言分支结构
- 4.2 Go语言循环结构
- 4.3 Go语言输出九九乘法表
- 4.4 Go语言键值循环
- 4.5 Go语言switch语句
- 4.6 Go语言goto语句
- 4.7 Go语言break
- 4.8 Go语言continue
- 4.9 Go语言聊天机器人
- 4.10 Go语言词频统计
- 4.11 Go语言缩进排序
- 4.12 Go语言实现二分查找算法
- 4.13 Go语言冒泡排序
- 5.Go语言函数
- 5.1 Go语言函数声明
- 5.2 Go语言将秒转换为具体的时间
- 5.3 Go语言函数中的参数传递效果测试
- 5.4 Go语言函数变量
- 5.5 Go语言字符串的链式处理
- 5.6 Go语言匿名函数
- 5.7 Go语言函数类型实现接口
- 5.8 Go语言闭包(Closure)
- 5.9 Go语言可变参数(变参函数)
- 5.10 Go语言defer(延迟执行语句)
- 5.11 Go语言递归函数
- 5.12 Go语言处理运行时错误
- 5.13 Go语言宕机(panic)
- 5.14 Go语言宕机恢复(recover)
- 5.15 Go语言计算函数执行时间
- 5.16 Go语言通过内存缓存来提升性能
- 5.17 Go语言函数的底层实现
- 5.18 Go语言Test功能测试函数详解
- 6.Go语言结构体
- 6.1 Go语言结构体定义
- 6.2 Go语言实例化结构体
- 6.3 Go语言初始化结构体的成员变量
- 6.4 Go语言构造函数
- 6.5 Go语言方法和接收器
- 6.6 Go语言为任意类型添加方法
- 6.7 Go语言使用事件系统实现事件的响应和处理
- 6.8 Go语言类型内嵌和结构体内嵌
- 6.9 Go语言结构体内嵌模拟类的继承
- 6.10 Go语言初始化内嵌结构体
- 6.11 Go语言内嵌结构体成员名字冲突
- 6.12 Go语言使用匿名结构体解析JSON数据
- 6.13 Go语言垃圾回收和SetFinalizer
- 6.14 Go语言将结构体数据保存为JSON格式数据
- 6.15 Go语言链表操作
- 6.16 Go语言数据I/O对象及操作
- 7.Go语言接口
- 7.1 Go语言接口声明
- 7.2 Go语言实现接口的条件
- 7.3 Go语言类型与接口的关系
- 7.4 Go语言类型断言简述
- 7.5 Go语言实现日志系统
- 7.6 Go语言排序
- 7.7 Go语言接口的嵌套组合
- 7.8 Go语言接口和类型之间的转换
- 7.9 Go语言空接口类型
- 7.10 Go语言使用空接口实现可以保存任意值的字典
- 7.11 Go语言类型分支
- 7.12 Go语言error接口
- 7.13 Go语言接口内部实现
- 7.14 Go语言表达式求值器
- 7.15 Go语言实现Web服务器
- 7.16 Go语言音乐播放器
- 7.17 Go语言实现有限状态机(FSM)
- 7.18 Go语言二叉树数据结构的应用
- 8.Go语言包
- 8.1 Go语言包的基本概念
- 8.2 Go语言封装简介及实现细节
- 8.3 Go语言GOPATH详解
- 8.4 Go语言常用内置包简介
- 8.5 Go语言自定义包
- 8.6 Go语言package
- 8.7 Go语言导出包中的标识符
- 8.8 Go语言import导入包
- 8.9 Go语言工厂模式自动注册
- 8.10 Go语言单例模式简述
- 8.11 Go语言sync包与锁
- 8.12 Go语言big包
- 8.13 Go语言使用图像包制作GIF动画
- 8.14 Go语言正则表达式
- 8.15 Go语言time包
- 8.16 Go语言os包用法简述
- 8.17 Go语言flag包
- 8.18 Go语言go mod包依赖管理工具使用详解
- 8.19 Go语言生成二维码
- 8.20 Go语言Context(上下文)
- 8.21 客户信息管理系统
- 8.22 Go语言发送电子邮件
- 8.23 Go语言(Pingo)插件化开发
- 8.24 Go语言定时器实现原理及作用
- 9.Go语言并发
- Go语言并发简述(并发的优势)
- Go语言goroutine(轻量级线程)
- Go语言并发通信
- Go语言竞争状态简述
- Go语言GOMAXPROCS(调整并发的运行性能)
- 并发和并行的区别
- goroutine和coroutine的区别
- Go语言通道(chan)——goroutine之间通信的管道
- Go语言并发打印(借助通道实现)
- Go语言单向通道——通道中的单行道
- Go语言无缓冲的通道
- Go语言带缓冲的通道
- Go语言channel超时机制
- Go语言通道的多路复用——同时处理接收和发送多个通道的数据
- Go语言RPC(模拟远程过程调用)
- Go语言使用通道响应计时器的事件
- Go语言关闭通道后继续使用通道
- Go语言多核并行化
- Go语言Telnet回音服务器——TCP服务器的基本结构
- Go语言竞态检测——检测代码在并发环境下可能出现的问题
- Go语言互斥锁(sync.Mutex)和读写互斥锁(sync.RWMutex)
- Go语言等待组(sync.WaitGroup)
- Go语言死锁、活锁和饥饿概述
- Go语言封装qsort快速排序函数
- Go语言CSP:通信顺序进程简述
- Go语言聊天服务器
- 10.Go语言反射
- Go语言反射(reflection)简述
- Go语言反射规则浅析
- Go语言reflect.TypeOf()和reflect.Type(通过反射获取类型信息)
- Go语言reflect.Elem()——通过反射获取指针指向的元素类型
- Go语言通过反射获取结构体的成员类型
- Go语言结构体标签(Struct Tag)
- Go语言reflect.ValueOf()和reflect.Value(通过反射获取值信息)
- Go语言通过反射访问结构体成员的值
- Go语言IsNil()和IsValid()——判断反射值的空和有效性
- Go语言通过反射修改变量的值
- Go语言通过类型信息创建实例
- Go语言通过反射调用函数
- Go语言inject库:依赖注入
- 11.Go语言网络编程
- Go语言Socket编程详解
- Go语言Dial()函数:建立网络连接
- Go语言ICMP协议:向主机发送消息
- Go语言TCP协议
- Go语言DialTCP():网络通信
- Go语言HTTP客户端实现简述
- Go语言服务端处理HTTP、HTTPS请求
- Go语言RPC协议:远程过程调用
- 如何设计优雅的RPC接口
- Go语言解码未知结构的JSON数据
- Go语言如何搭建网站程序
- Go语言开发一个简单的相册网站
- Go语言数据库(Database)相关操作
- 示例:并发时钟服务器
- Go语言router请求路由
- Go语言middleware:Web中间件
- Go语言常见大型Web项目分层(MVC架构)
- Go语言Cookie的设置与读取
- Go语言获取IP地址和域名解析
- Go语言TCP网络程序设计
- Go语言UDP网络程序设计
- Go语言IP网络程序设计
- Go语言是如何使得Web工作的
- Go语言session的创建和管理
- Go语言Ratelimit服务流量限制
- Go语言WEB框架(Gin)详解
- 12.Go语言文件处理
- Go语言自定义数据文件
- Go语言JSON文件的读写操作
- Go语言XML文件的读写操作
- Go语言使用Gob传输数据
- Go语言纯文本文件的读写操作
- Go语言二进制文件的读写操作
- Go语言自定义二进制文件的读写操作
- Go语言zip归档文件的读写操作
- Go语言tar归档文件的读写操作
- Go语言使用buffer读取文件
- Go语言并发目录遍历
- Go语言从INI配置文件中读取需要的值
- Go语言文件的写入、追加、读取、复制操作
- Go语言文件锁操作
- 13.Go语言网络爬虫
- Go语言网络爬虫概述
- Go语言网络爬虫中的基本数据结构
- Go语言网络爬虫的接口设计
- Go语言网络爬虫缓冲器工具的实现
- Go语言网络爬虫缓冲池工具的实现
- Go语言网络爬虫多重读取器的实现
- Go语言网络爬虫内部基础接口
- Go语言网络爬虫组件注册器
- Go语言网络爬虫下载器接口
- Go语言网络爬虫分析器接口
- Go语言网络爬虫条目处理管道
- Go语言网络爬虫调度器的实现
- Go语言爬取图片小程序
- 14.Go语言编译和工具链
- go build命令(go语言编译命令)完全攻略
- go clean命令——清除编译文件
- go run命令——编译并运行
- go fmt命令——格式化代码文件
- go install命令——编译并安装
- go get命令——一键获取代码、编译并安装
- go generate命令——在编译前自动化生成某类代码
- go test命令(Go语言测试命令)完全攻略
- go pprof命令(Go语言性能分析命令)完全攻略
- 15.Go语言避坑与技巧
- goroutine(Go语言并发)如何使用才更加高效?
- Go语言反射——性能和灵活性的双刃剑
- Go语言接口的nil判断
- Go语言map的多键索引——多个数值条件可以同时查询
- Go语言与C/C++进行交互
- Go语言文件读写
- Json数据编码和解码
- Go语言使用select切换协程
- Go语言加密通信
- Go语言内存管理简述
- Go语言垃圾回收
- Go语言哈希函数
- Go语言分布式id生成器
- 部署Go语言程序到Linux服务器
- Go语言实现RSA和AES加解密
