go 开发web应用

2023-07-16  本文已影响0人  护念

前面我们体验了利用GIN框架快速开发了web接口;但就go语言本身而言,它已经包含了许多用于开发web应用的包,不用框架也是可以直接写的,下面我们尝试着做下。

1.初始化项目

mkdir gowiki
cd gowiki
go mod init example/gowiki
touch wiki.go
// wiki.go
package main

import (
    "fmt"
    "os"
)

func main() {
    // 这里&相当于Page的实例变量
    p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
    // 存入文件
    p1.save()
    // 读取出来
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))
}

// 结构体页面 相当于面向对象的class
type Page struct {
    Title string // 字段title标题
    Body  []byte // 字段body内容
}

// 参数 p相当于,Page的实例
func (p *Page) save() error {
    filename := p.Title + ".txt"
    // 将body内容写入文件中,文件权限为 600
    return os.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
    filename := title + ".txt"
    // 读取文件
    body, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    // page实例 和 错误
    return &Page{Title: title, Body: body}, nil
}

运行,运行完项目下会有一个TestPage.txt文件(程序写入的)

dongmingyan@pro ⮀ ~/go_playground/gowiki ⮀ go run .
This is a sample Page.
 dongmingyan@pro ⮀ ~/go_playground/gowiki ⮀ tree
.
├── TestPage.txt
├── go.mod
└── wiki.go

1 directory, 3 files

2. 初始化web服务监听

这里主要使用net/http包,接受请求、路由、处理请求

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    // 这里&相当于Page的实例变量
    p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
    // 存入文件
    p1.save()
    // 读取出来
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))

    // ===================================新加部分=================
    // 根目录下所有请求 交给handler处理
    http.HandleFunc("/", handler)
    // 监听8080端口 有错误时直接退出
    log.Fatal(http.ListenAndServe(":8080", nil))
    // ===================================新加部分=================
}

// 结构体页面 相当于面向对象的class
type Page struct {
    Title string // 字段title标题
    Body  []byte // 字段body内容
}

// 参数 p相当于,Page的实例
func (p *Page) save() error {
    filename := p.Title + ".txt"
    // 将body内容写入文件中,文件权限为 600
    return os.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
    filename := title + ".txt"
    // 读取文件
    body, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    // page实例 和 错误
    return &Page{Title: title, Body: body}, nil
}

// ===================================新加部分=================
// 请求处理
// http.ResponseWriter 响应对象
// http.Request 请求对象
func handler(w http.ResponseWriter, r *http.Request) {
    // Fprintf 将格式化后的字符写入w中
    // r.URL.Path[1:] 代表去掉 “/”后的后面字符
    fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

// ===================================新加部分=================

运行后,用curl调用

dongmingyan@pro ⮀ ~ ⮀ curl http://localhost:8080/dmy
Hi there, I love dmy!%

3. 添加view和编辑(edit)页面

// wiki.go

func main() {
    // 省略...
    http.HandleFunc("/", handler)
    // 添加路由view路径
    http.HandleFunc("/view/", viewHandler)
    // 监听8080端口 有错误时直接退出
    log.Fatal(http.ListenAndServe(":8080", nil))
    // 省略...
}


// ... 省略
// 这里查找的是view后面的txt内容,需要先保存一个文件比如 test.txt
// 就可以请求 /view/test 路径
func viewHandler(w http.ResponseWriter, r *http.Request) {
    // 取出view后的内容
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}

为了便于测试这里,添加也给测试文件(位于项目下)test.txt内容随便写

我是test.txt文件

运行后测试

dongmingyan@pro ⮀ ~ ⮀ curl http://localhost:8080/view/test
<h1>test</h1><div>我是test.txt文件
</div>%
// wiki.go

func main() {
    // 省略...
    http.HandleFunc("/view/", viewHandler)
    http.HandleFunc("/edit/", editHandler)
    // 监听8080端口 有错误时直接退出
    log.Fatal(http.ListenAndServe(":8080", nil))
    // 省略...
}

// 编辑页面处理
func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }

    //  这里手动编写了html代码
    fmt.Fprintf(w, "<h1>Editing %s</h1>"+
        "<form action=\"/save/%s\" method=\"POST\">"+
        "<textarea name=\"body\">%s</textarea><br>"+
        "<input type=\"submit\" value=\"Save\">"+
        "</form>",
        p.Title, p.Title, p.Body)
}

curl http://localhost:8080/edit/test 就可以测试

4. 优化-使用模版渲染页面

前面我们虽然实现了功能,但是非常丑;这里我们用html/template包,剥离出模版。

  1. 项目路径下创建一个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. 项目路径下创建一个view.html文件
<h1>{{.Title}}</h1>

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

<div>{{printf "%s" .Body}}</div>
  1. 重写前面的editHandlerviewHandler函数
// wiki.go
func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }

    // 把模版文件解析出来
    t, _ := template.ParseFiles("edit.html")
    // 渲染 p传入 使用内部的.Title展示
    t.Execute(w, p)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    t, _ := template.ParseFiles("view.html")
    t.Execute(w, p)
}

注意到前面,我们的方法中都有解析模版和渲染的步骤,提炼出来

// wiki.go
func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, _ := loadPage(title)
    renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    // 渲染页面
    renderTemplate(w, "edit", p)
}

// 提炼出渲染方法
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    t, _ := template.ParseFiles(tmpl + ".html")
    t.Execute(w, p)
}
  1. 处理view中不存在页面的情况
// wiki.go
func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/view/"):]
    p, err := loadPage(title)
    // 如果错误存在,则重定向到edit页面
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}
  1. renderTemplate模版解析可能失败,添加异常处理
// wiki
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    t, err := template.ParseFiles(tmpl + ".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)
    }
}

5.添加save处理

  1. 添加save处理
// wiki.go

func main() {
    // ...
    // save路由
    http.HandleFunc("/save/", saveHandler)
    // ...
}

// 这里save后会在项目下写入文件
func saveHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/save/"):]
    // 取出表单中的body参数
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    p.save()
    // 保存后重定向
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
  1. 添加save异常处理
// wiki.go

func saveHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[len("/save/"):]
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    // save的时候可能会有错误的
    err := p.save()
    if err != nil {
        // http.Error http异常处理
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

6. 添加模版缓存

前面我们每次view/edit页面都需要取解析一次模版,效率比较低,我们可以一次性初始化好 后续直接使用就ok.

// wiki.go

// 初始化模版
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

// 这里渲染
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    // 直接从templates中执行渲染
    err := templates.ExecuteTemplate(w, tmpl+".html", p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

7. 添加路径验证

前面我们的路由用户可以写任意的东西,比如在view后,不够安全,这里我们添加一个验证,只有符合验证我们才继续处理请求。

添加验证

// wiki.go

// regexp 正则表达试
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")
    }
    // 否则返回匹配的标题 错误nil
    return m[2], nil
}

依次在每个函数中添加路径验证

// wiki.go
func viewHandler(w http.ResponseWriter, r *http.Request) {
    // 使用getTitle去做验证
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    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) {
    // 使用getTitle去做验证
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request) {
    // 使用getTitle去做验证
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err = p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

8. 优化-使用匿名函数

前面我们虽然添加了验证,但是在三个函数中都重复使用了getTitle方法,不够美观,我们继续优化。

我们可以使用匿名函数,来对每个传入的函数都执行验证路径

// wiki.go

// 改路有中的调用函数
func main() {
    // ...
    http.HandleFunc("/view/", makeHandler(viewHandler))
    http.HandleFunc("/edit/", makeHandler(editHandler))
    http.HandleFunc("/save/", makeHandler(saveHandler))
    // ...
}

// 这里传入一个函数 这个函数有三个参数
// 1. http响应 
// 2. http请求
// 3. 字符串
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    // 返回一个匿名函数 相当于对fn函数进行了改写
    return func(w http.ResponseWriter, r *http.Request) {
        // 进入后先验证路径
        m := validPath.FindStringSubmatch(r.URL.Path)
        if m == nil {
            http.NotFound(w, r)
            return
        }
        // 调用传入的函数
        fn(w, r, m[2])
    }
}

// 改为三个参数 多title
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)
}

// 改为三个参数 多title
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)
}

// 改为三个参数 多title
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err := p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

9. 完整代码

// wiki.go

package main

import (
    "errors"
    "fmt"
    "html/template"
    "log"
    "net/http"
    "os"
    "regexp"
)

func main() {
    // 这里&相当于Page的实例变量
    p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
    // 存入文件
    p1.save()
    // 读取出来
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))

    // 根目录下所有请求 交给handler处理
    http.HandleFunc("/", handler)
    // 路由view路径
    http.HandleFunc("/view/", makeHandler(viewHandler))
    http.HandleFunc("/edit/", makeHandler(editHandler))
    http.HandleFunc("/save/", makeHandler(saveHandler))
    // 监听8080端口 有错误时直接退出
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// 初始化模版
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

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

// 结构体页面 相当于面向对象的class
type Page struct {
    Title string // 字段title标题
    Body  []byte // 字段body内容
}

// 参数 p相当于,Page的实例
func (p *Page) save() error {
    filename := p.Title + ".txt"
    // 将body内容写入文件中,文件权限为 600
    return os.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
    filename := title + ".txt"
    // 读取文件
    body, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    // page实例 和 错误
    return &Page{Title: title, Body: body}, nil
}

// 请求处理
// http.ResponseWriter 响应对象
// http.Request 请求对象
func handler(w http.ResponseWriter, r *http.Request) {
    // Fprintf 将格式化后的字符写入w中
    // r.URL.Path[1:] 代表去掉 “/”后的后面字符
    fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

// 编写一个函数用于做路径的验证
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")
    }
    // 否则返回匹配的标题 错误nil
    return m[2], nil
}

// 这里传入一个函数 这个函数有三个参数
// 1. http响应
// 2. http请求
// 3. 字符串
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    // 返回一个匿名函数 相当于对fn函数进行了改写
    return func(w http.ResponseWriter, r *http.Request) {
        // 进入后先验证路径
        m := validPath.FindStringSubmatch(r.URL.Path)
        if m == nil {
            http.NotFound(w, r)
            return
        }
        // 调用传入的函数
        fn(w, r, m[2])
    }
}

// 改为三个参数 多title
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)
}

// 改为三个参数 多title
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)
}

// 改为三个参数 多title
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(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, tmpl string, p *Page) {
    // 直接从templates中执行渲染
    err := templates.ExecuteTemplate(w, tmpl+".html", p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

上一篇下一篇

猜你喜欢

热点阅读