Go 学习笔记3 - 编写一个Web应用程序

0. 概述

掌握了Go的基础语法后,让我们开始动手实战,尝试写一个 简易的wiki 小应用,它是一个 web 应用项目(网页应用)。

本文涉及下面的技术点:

  1. 定义一个 struct 类型,和通过操作文件实现“读取”和“保存”方法
  2. 使用 net/http包 构建web应用
  3. 使用 html/template包 处理 HTML 模板
  4. 使用 regexp包 正则表达式 验证用户输入
  5. 闭包

预计我们分步骤进行:

  • 第一阶段:实基本功能现功能,像文本地存储,网页查看,编辑等。
  • 第二阶段:改进,处理不存在的页面,改进错误处理,和模板缓存。
  • 第三阶段:重构,进行正则表达式验证和使用闭包来重构

本文结构:

1. 第一阶段:实基本功能现功能
    1.1 开始之前
    1.2 定义数据类型,和实现“读取”和“保存”方法
          1.2.1 保存文章
          1.2.2 读取文章
    1.3 实现web应用
          1.3.1 处理请求:查看文章
          1.3.2  处理请求:编辑文章
          1.3.3 保存:处理 form 表单
2. 第二阶段:改进,处理不存在的页面,改进错误处理,和模板缓存
    2.1处理不存在的页面
    2.2 异常处理
      2.2.1 读取模板失败时的异常和执行模板转换时的异常
      2.2.2 模板转换时的异常
      2.2.3 保存文章失败异常
    2.3 优化模板缓存
3. 第三阶段:重构,进行正则表达式验证和使用闭包来重构
    3.1 正则表达式验证
    3.2 引入函数和闭包
    3.3 重构 模板绑定html 的冗余
    4.完整代码

1. 第一阶段:实现基本功能

1.1 开始之前

假设我们的应用名字叫“gowiki”,先创建个文件夹

$ mkdir gowiki
$ cd gowiki

导入要用的包

package main

import (
        "fmt"
    "io/ioutil"
)

1.2 定义数据类型,和实现“读取”和“保存”方法

一篇文章应该有 “标题”和内容“,那么,我们定义一个叫 Page 的结构体,它有 标题,和文件内容字段。

type Page struct {
  Title string    //标题
  Body []byte    //内容,字节类型比string类型要方便,性能好
}

这个 Page 是在内存中存储的格式,那怎么实现持久化存储呢,我通过 Go 的操作文件的函数来实现。

  • 保存文章,就是将写入到文件。
  • 读取文章,就是读文件

1.2.1 保存文章

/* 保存 page 到 文件 */
func (p *Page) save() error{
  fileName := p.Title + ".txt"
  return ioutil.WriteFile(fileName, p.Body, 0x600)
}

如上,用 文章的标题 来作为文件名。 ioutil.WriteFile 是写入文件的方法。 0x600是个常量,表示需要读写权限。

1.2.2 读取文章

我们 文章标题就是,那么按这个规则来作为文件名来读取。

func loadPage(title string) (*Page,error){
  fileName := title + ".txt"
  body,err := ioutil.ReadFile(fileName)
  if err != nil {
    return nil,err
  }
  return &Page{fileName, body},nil
}

outil.ReadFile 是读取文件的方法。它返回两个返回值:文件内容和可能发生的错误。如果读取成功,err为空。我们通过判断err是否是nil来判定 读取文件是否成功。 当读取完成,我们再构建一个 Page 对象作为返回值。

1.3 实现web应用

我们需要3个handle 分别对应,查看,编辑和保存。先看main函数,像这样:

func main(){
  http.HandleFunc("/view/",viewHandler)
  http.HandleFunc("/edit/",editHandler)
  http.HandleFunc("/save/",saveHandler)
  http.ListenAndServe(":8080",nil)
}

下面我们来挨个实现这 viewHandler,editHandler,saveHandler。

1.3.1 处理请求:查看文章

写个 viewHandler,接收这样的网页请求
http://localhost:8080/view/ttt
忽略前面的域名和对口对应的是 /view/ttt 这样REST风格的URL,这里的 ttt 表示文章的标题。具体实现如下:

func viewHandler(w http.ResponseWriter, r *http.Request){
  fmt.Println("path:",r.URL.Path)
  title := r.URL.Path[len("/view/"):]
  p,_ := loadPage(title)     // 注意这里有个BUG,文章如果不存在就显示成空白,我们稍后再处理
  t,_ := template.ParseFiles("view.html")   // 注意这里用了 模板template
  t.Execute(w, p)
}

上面的示例:
1.先从 URL 中拿到 ttt 作为 title,然后调用上一节我们完成的 loadPage 方法来保存到具体文件中。
2.构造一个模板 template,它需要指定一个 本地html文件路径。使用构造好的模板,执行 Execute 方法,传入 写入流(即:w),和参数(即: page 对象)

view.html 的代码如下,它是具体的html的实现,它以一种“绑定”的机制运作。

<h1>{{.Title}}</h1>

<p>[<a href="/edit/{{.Title}}">edit</a>]</p>

<div>{{printf "%s" .Body}}</div>

注意上面的 {{.Title}} 写法,这是一种特殊的表达,它表示,读取 Title 属性的值 放到这里。
注意上面的 {{printf "%s" .Body}}, 这个表达式表示把 Body的属性的值,按照 字符串格式输出。它和printf 函数很类似。

1.3.2 处理请求:编辑文章

类似上面,写个 viewHandler,接收这样的网页请求
http://localhost:8080/edit/ttt
就是要处理来自 /edit/ttt 的请求。

func editHandler(w http.ResponseWriter, r *http.Request){
  fmt.Println("path:",r.URL.Path)
  title := r.URL.Path[len("/edit/"):]
  p,err := loadPage(title)
  if err != nil {
    p = &Page{Title:title}
  }
  t,_ := template.ParseFiles("edit.html")
  t.Execute(w, p)
}

edit.html 的实现如下:

<h1>Editing {{.Title}}</h1>

<form action="/save/{{.Title}}" method="POST">
  <div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
  <div><input type="submit" value="Save"></div>

</form>

1.3.3 保存:处理 form 表单

类似上面,写个 viewHandler,接收这样的网页请求
http://localhost:8080/edit/ttt
就是要处理 /edit/ttt 的请求。

func saveHandler(w http.ResponseWriter, r *http.Request){
  fmt.Println("path:",r.URL.Path)
  title := r.URL.Path[len("/save/"):]
  body := r.FormValue("body")            // 注意 body 是个字符串
  p := &Page{title,[]byte(body)}         // 将body字符串转型为字节
  p.save()
  http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

至此,一个简单的 web 应用就完成了,它具有查看和编辑文章的功能,虽然看起来很简陋。

2. 第二阶段:改进,处理不存在的页面,改进错误处理,和模板缓存

2.1处理不存在的页面

我们回头再看下 viewHandler 的方法实现:

    p,_ := loadPage(title)   // 注意, 错误被隐藏了 #1
    t,_ := template.ParseFiles("view.html")   
    t.Execute(w, p)

当在网址输入这个页面 "/view/不存在的页面" 会显示一篇空白页,因为不存在这篇文章,尝试去读物理文件会失败。虽然程序不至于崩溃,这样的响应也是个糟糕的用户体验。

我们来改进它,当指定的文章不存在时,直接跳转到 编辑页面。通过 http.Redirect() 来实现跳转功能。代码如下:

    // p,_ := loadPage(title)   //  旧的代码,注释掉
      p,err := loadPage(title)  // 接收 err,再判断
    if err !=nil { //如果发生了 异常,触发跳转
      http.Redirect(w, r, "/edit/"+title, http.StatusFound)
      return
    }
    t,_ := template.ParseFiles("view.html")   
    t.Execute(w, p)

http.Redirect() 函数的前两个参数是 w http.ResponseWriter, r *http.Request ,表示写入流和请求,无需多说。第三个参数是 要跳转到的目的地页面,第四个参数是http的响应code。

再次在浏览器里输入 http://localhost:8080/view/sssss ,如果sssss文章不存在,将跳转到 http://localhost:8080/edit/sssss。 可看到浏览器里网址的变化了。

2.2 异常处理

上面我们写的代码里,很多代码使用了 “空白标识符”(即 “_" 下划线符号),来隐藏了隐藏,这是很糟糕了,一旦发生了异常,就不知道问题出在哪里了。我们来修正它,当发生这样的异常时,我们识别它,并告知用户(使用者)发生的异常。

2.2.1 读取模板失败时的异常和执行模板转换时的异常

读取模板失败时的异常

上面的 viewHandler 的实现,有下面这样的代码

    t,_ := template.ParseFiles("edit.html") //注意这里
    t.Execute(w, p)

空白标识符隐藏了异常,我们来修正它:

    t,err := template.ParseFiles("view.html")  // 模板文件可能不存在
    if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
    }
    t.Execute(w, p)

http.Error() 函数的第一个参数是 写入流,第二个参数是错误说明的字符串,第三个参数是 http的状态码,http.StatusInternalServerError 表示 500,服务内部异常。

那么,当遇到模板文件不存在,就会返回 500异常的响应,和错误信息。

2.2.2 模板转换时的异常

让我们继续看上面的代码,模板的执行方法 t.Execute(w, p) 如果发生了异常,导致无法正确返回web页,这也要做个处理。

  err = t.Execute(w, p)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }

t.Execute(w, p) 的返回值是个 error 类型,我们判断它如果不为nil,则 调用 http.Error() 方法告知用户发生了异常。

2.2.3 保存文章失败异常

在 saveHandler 中 ,有下面的代码,它调用了save 方法,而未处理 save 方法异常发生的判断。

  p := &Page{title,[]byte(body)}         // 将body字符串转型为字节
  p.save()

同理,我们 接收 save() 方法返回值,并判断。

  err := p.save()
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }

修改后,当save() 失败时,会将失败告知到用户。

2.3 优化模板缓存

回顾上面的代码里我们解析构造模板的方法,我们在 viewHandler 函数里调用这个方法:

 t,_ := template.ParseFiles("edit.html") 

由于在 viewHandler函数 在每次“打开查看文章页面”时都调用,将导致每次都解析构造很模板,然而,每次创建模板是不需要的损耗。我们可以在 全局变量里调用一次就好了,示例:

var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

函数template.Must是一个方便的包装器,在传入错误值时会崩溃。这里应该出现panic;如果无法加载模板,那还是退出程序吧。

示例代码:

var templates = template.Must(template.ParseFiles("view.html","edit.html"))

/* 构建 web app 处理 */
func viewHandler(w http.ResponseWriter, r *http.Request){
  fmt.Println("path:",r.URL.Path)
  title := r.URL.Path[len("/view/"):]
  p,err := loadPage(title)
  if err !=nil {
    http.Redirect(w, r, "/edit/"+title, http.StatusFound)
    return
  }
  // t,err := template.ParseFiles("view.html")
  // if err != nil {
  //   http.Error(w, err.Error(), http.StatusInternalServerError)
  //   return
  // }
  // err = t.Execute(w, p)
  // if err != nil {
  //   http.Error(w, err.Error(), http.StatusInternalServerError)
  // }
  err = templates.ExecuteTemplate(w, "view.html", p)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

templates.ExecuteTemplate函数 的第二个参数表示 模板名,即模板文件名。它的返回值是 error 类型。

模板的缓存改造:

  • 全局变量取代 局部变量
  • template.Must 取代 template.ParseFiles 方法。
  • templates.ExecuteTemplate 取代 template.ParseFiles

现在,第二阶段完成。我们还有些事情要做,比如做一些用户合法性验证。

3. 第三阶段:重构,进行正则表达式验证和使用闭包来重构

你应该注意到了,这个程序有个缺陷,用户可以到达任意页面,文章标题也很随意。它可能带来不期望的结果,我们来使用正则表达式来做一些验证。

导入 正则表达式的 包:导入 regexp,和 errors包

3.1 正则表达式验证

构造正则表达式

var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

MustCompile 接受 正则表达式的字符串作为参数,如果不合法的字符串 会触发 panic。

编写判断方法,示例:

func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
  m := validPath.FindStringSubmatch(r.URL.Path)
  if m == nil {
    http.NotFound(w,r)
    return "", errors.New("Invalid Page Title")
  }
  return m[2],nil// title 位于第二个位置
}
  • 这个正则 ^/(edit|save|view)/([a-zA-Z0-9]+)$ ,第一个括号里的 edit|save|view 表示这三个字符串中的任何一个都被匹配。 第二个括号里表示接受常规字符串和数字。
  • validPath.FindStringSubmatch 来判定是否合法,如果为空,则认为不匹配。如果识别匹配,第二个参数是 title
  • 调用 http.NotFound(w,r) 将返回: 404 页面未找到

现在,我们在 中调用 getTitle ,代码如下:

func viewHandler(w http.ResponseWriter, r *http.Request){
  fmt.Println("path:",r.URL.Path)
  // 调用 getTitle 验证 URL 是否合法,且同时获得 title 的值
  title,err := getTitle(w,r)
  if err != nil{
    return
  }
  // 有了正则,下面这个 字符串截取获得title 的方法就不需要了
  //title := r.URL.Path[len("/view/"):]
  p,err := loadPage(title)
  if err !=nil {
    http.Redirect(w, r, "/edit/"+title, http.StatusFound)
    return
  }
  err = templates.ExecuteTemplate(w, "view.html", p)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

这样完成后,如果用户输入了不是 /edit/, /save/, /view/这三个字符串开头的网址,或者不合法的 title 字符串,都将会收到 “404,页面为找到”

3.2 引入函数和闭包

上面的方法中我们写了个 getTitle() ,它需要在 viewHandler, editHandler, saveHandler 这3个方法中调用,每次都写那么一个方法和判断err很繁琐,我们抽离公共部分来避免代码冗余重复。

Go 里面的函数 可以作为函数中的参数传递,我们可以利用这一特性来实现函数的调用代理。

我们先修改下 viewHandler 等3个方法的函数签名:

func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)

上面我们增加了一个参数 title,我们是想先取得title,后把title的值传入这样的函数中。

我们写一个 makeHandler 方法,它来构造一个 合适的Handler作为返回值,这个返回值 是 http.HandlerFunc 类型。

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) { // 注意这里#1
        m := validPath.FindStringSubmatch(r.URL.Path)
        if m == nil {
            http.NotFound(w, r)
            return
        }
        fn(w, r, m[2]) //注意这里#2,fn是个函数,做为参数传递而来
    }
}

上面的代码有个注意的地方:

     return func( 参数) {  
        具体代码 
      }

这是一个闭包的写法,这里将构建一个 匿名函数 ,并作为返回值传递出去。

在这个闭包里,是可以直接使用它所在的函数 makeHandler 的参数的。在这里,我们把上面的getTitle 方法的代码写在这里,先验证 URL 合法行,利用正则取得 title 的值。最后作为参数传递而来的fn,并调用 fn函数。

你应该注意到了,这个 fn的函数的签名,和我们刚刚修改的 viewHandler 等3个方法的函数签名一模一样。是的,函数将被作为参数传递到这里。

3.3 重构 模板绑定html 的冗余

上面的viewHandler 和 editHandler 都要 模板绑定 html的代码,也有重复代码,我们再处理下它,和让参数名更具有 语义,原来的代码:

  err = templates.ExecuteTemplate(w, "edit.html", p)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }

重构后:

func renderTemplate(w http.ResponseWriter, templateName string, p *Page){
  err := templates.ExecuteTemplate(w, templateName+".html", p)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

当需要显示html时,在 viewHandler中 这么调用它:

  renderTemplate(w,"edit",p)

至此,重构完毕。

4.完整代码

方便阅读,最后完成的代码如下:

package main

import(
  "fmt"
  "io/ioutil"
  "net/http"
  "html/template"
  "regexp"
  "errors"
)


type Page struct {
  Title string
  Body []byte
}

/* 保存 page 到 文件 */
func (p *Page) save() error{
  fileName := p.Title + ".txt"
  return ioutil.WriteFile(fileName, p.Body, 0x600)
}

func loadPage(title string) (*Page,error){
  fileName := title + ".txt"
  body,err := ioutil.ReadFile(fileName)
  if err != nil {
    return nil,err
  }
  return &Page{title, body},nil
}


/*********************************/
/* 正则 */

var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")


func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
  m := validPath.FindStringSubmatch(r.URL.Path)
  if m == nil {
    http.NotFound(w,r)
    return "", errors.New("Invalid Page Title")
  }
  return m[2],nil// title 位于第二个位置
}

/*********************************/
var templates = template.Must(template.ParseFiles("view.html","edit.html"))


/* 请求处理; 构建 web app 处理 */
func viewHandler(w http.ResponseWriter, r *http.Request, title string){
  p,err := loadPage(title)
  if err !=nil {
    http.Redirect(w, r, "/edit/"+title, http.StatusFound)
    return
  }
  renderTemplate(w,"view",p)
}


func editHandler(w http.ResponseWriter, r *http.Request, title string){
  p,err := loadPage(title)
  if err != nil {
    p = &Page{Title:title}
  }
  renderTemplate(w,"edit",p)
}

func saveHandler(w http.ResponseWriter, r *http.Request, title string){
  body := r.FormValue("body")            // 注意 body 是个字符串
  p := &Page{title,[]byte(body)}         // 将body字符串转型为字节
  err := p.save()
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }
  http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

func renderTemplate(w http.ResponseWriter, templateName string, p *Page){
  err := templates.ExecuteTemplate(w, templateName+".html", p)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request){
    fmt.Println("path:",r.URL.Path)
    title,err := getTitle(w,r)
    if err != nil{
      return
    }
    fn(w,r,title)
  }
}

func main(){
  http.HandleFunc("/view/",makeHandler(viewHandler))
  http.HandleFunc("/edit/",makeHandler(editHandler))
  http.HandleFunc("/save/",makeHandler(saveHandler))

  fmt.Println("server running!")
  http.ListenAndServe(":8080",nil)

}

END

发布了256 篇原创文章 · 获赞 7 · 访问量 9万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览