前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go 学习笔记3 - 编写一个Web应用程序

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

作者头像
张云飞Vir
发布2020-03-20 11:08:21
5550
发布2020-03-20 11:08:21
举报

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 概述
  • 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.3 优化模板缓存
    • 3. 第三阶段:重构,进行正则表达式验证和使用闭包来重构
      • 3.1 正则表达式验证
        • 构造正则表达式
      • 3.2 引入函数和闭包
        • 3.3 重构 模板绑定html 的冗余
        • 4.完整代码
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档