go简单入门--day8:简单实现一个Web服务器

2023-4-15 16:48| 发布者: wanhu| 查看: 75| 评论: 0

摘要: 前言昨天我们简单的了解了fuzz即模糊测试 今天我们来简单的实现一个web serverweb app包含内容我们将接触以下几部分创建一个数据结构,它包含load和save两个methods 基于net/http这个包创建web应用使用html/template ...

前言

昨天我们简单的了解了fuzz即模糊测试

今天我们来简单的实现一个web server


web app

包含内容

我们将接触以下几部分

  • 创建一个数据结构,它包含loadsave两个methods
  • 基于net/http这个包创建web应用
  • 使用html/template这个包加工html模板
  • 使用regexp校验用户输入
  • 使用闭包(closures)

初始化项目

mkdir gowiki
cd gowiki
go mod init example/gowiki
echo > main.go // 编码可能是`utf-16`,需要留意

数据结构

type Page struct {
Title string
Body []byte
}

用于定义页面的数据结构

这里Body的类型是[]byte表示是byte切片,为什么这里不使用字符串呢?这里先按下不表,后面会知道原因。

关于切片,可以看这篇:https://golang.google.cn/doc/articles/slices_usage_and_internals.html

然后我们再给这个Page添加一个method

func (p *Page) save() error {
filename := p.Title + ".txt"
return os.WriteFile(filename, p.Body, 0600)
}

和之前学的Rust里的method一样,都和函数存在区别,专指某个struct的方法。Rust里的methodpythonclassmethod类似,第一个参数都是self,而gostructmethod写法刚开始有些不好理解,这是正常的,不过还是可以理解的。

p是一个指针,指向Page,相当于self

0600是一个八进制数,大家应该都接触过类Unix系统,我们通常都是直接chmod 755 xxx.txt这种方式修改文件的访问权限,这里的0600是同一个意思。

这个方法是用来将当前的内容写入Page.Title.txt文件中来解决数据持久化以及存储问题。

这里就回到了我们前面留的问题:Body为什么是[]type

因为这里要调用os模块将body写入文件中,而这个过程是操作字节流的。


既然有save,那自然就会有个load,不过这个load没必要和Page关联。

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

这里的写法&Page{ Title: title, Body: body }rust中的都是一个意思,返回这个struct的指针/引用,不过这样的写法在rust中是会报错的,因为rust中没有GC机制,所以当函数执行完出了调用栈之后里面的变量就都会自己drop掉释放内存,所以这个时候如果外面还有指针那指向的内存空间就不一定是对的了,一般称之为“悬浮指针”。(当然,也不一定,比如'static。)

goGC, 所以不必担心这个问题,不过写法上还是得加上&。。(个人感觉写法上不够整洁,不过可能还有其它底层原因导致需要这么写,我并没有系统性的学习,所以这里我并不清楚,如果说的不对麻烦评论区说下,谢谢~)

扯远了。。。回到我们的代码中,我们来试下这两个方法是否正常

func main() {
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
p1.save()
p2, _ := loadPage("TestPage")
fmt.Println(string(p2.Body))
}

然后我们运行下go run .




img_save_and_load_Page_success

正常


net/http[1]

我们前面简单的接触过Gin,那个是web框架,而net/http是一个模块。

我们先来看下基础用法

//go:build ignore

package main

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

func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
  • http.HandleFunc:注册/这个uri,如果匹配到这个路径就执行handler这个方法。
  • http.ListenAndServe:看名字就知道是用来创建服务并且监听对应端口的。 这个方法只会返回error,所以一旦返回就表示监听出了错误,直接panic即可。
  • r.URL.Path:是路径组件,表示当前请求的路径,[1:]表示从url的第二个字符开始切割(下标从0开始)出一个切片,也就是将/去掉之后的url

至于http.ResponseWriterhttp.Request这两个就不用多说了。


然后我们来接入到我们的项目中

func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}

func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

go run .

然后浏览器/curl访问localhost:8080/view/testPage




img_access_testPage

正常


编辑页面

现在我们基本打通访问的流程了,那么就可以接收输入了。

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}
}
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)
}

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

提供一个form用于提交内容, 但现在我们并没有实现保存的逻辑。

我们再来实现这一部分

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)}

err := p.save()
if err != nil {
fmt.Fprintf(w, "save error: %s", err)
return
}

fmt.Fprintf(w, "save success")

}

func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
  • FormValuepost接口里的form中获取对应字段的数据。

现在edit逻辑应该是通了,我们重新运行下go run .,然后浏览器/curl访问localhost:8080/edit/test_save




img_test_edit

表现正常


html/template[2]

前面我们用的都是fmt.Fprintf来格式化html template,但是很丑很不优雅,量大了问题也多。

所以我们这里用另一个包来处理。

底层估计也是使用的mustache[3]语法,我们直接新建一个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>

然后我们来调整下editHandler的逻辑

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")
t.Execute(w, p)
}

Execute的时候会将模板中{{}}里的字段替换成对应的数据。

我们再来处理view的,同样创建一个view.html的模板。

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

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

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

然后调整下viewHandler的逻辑

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)
}

这里面还有一些重复的代码,咱把它抽出来

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)
}

然后我们重新运行下




img_use_template

正常


处理不存在的页面

现在我们仅注册了几个路径,如果用户访问的是一个没有注册的路径,比如/view/xxxx,那么这个时候我们应该要返回一个页面不存在的提示或者直接跳到编辑页。

我们来调整下viewHandler的逻辑,让它重定向到编辑页

func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}

StatusFound302,一般就是重定向的状态码。

我们重新跑下




img_use_redirect

直接重定向到edit/xxxx了。


完善save

我们前面写的saveHandler方法只是简单的返回了信息,非常的简陋。

我们来完善一下,让它保存之后直接去到/view/xxx

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)}
p.save()
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

然后我们重新跑下




img_redirect_to_view_after_save

表现正常


错误处理

前面写的代码大多都没有关注这一块的内容,实际上错误处理是非常重要的一部分。

我们先来处理模板这块的

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)
}
}

当文档解析发生错误的时候直接返回对应的错误信息以及服务器500的状态码。

然后是save

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)}
err := p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

save可能发生错误,所以也需要处理。


模板缓存

我们在/view/xx的时候每次都会执行parseFiles这个方法,实际上是没必要的,模板是不会变的,所以我们直接在初始化阶段就parse所有或者按需parse,第二次读取对应模板直接拿之前parse的即可。

我们这里直接省事都处理了。

var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates.ExecuteTemplate(w, tmpl+".html", p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
  • template.Must:这是一个wrapper方法,出错的时候直接panic,否则返回对应处理后的结果。类似于rust中的Result.expect等方法。
  • templates.ExecuteTemplate:和前面的t.Execute功能一样,都是替换对应的字段为数据

这里就涉及到了一些抽象语法树(AST)的概念了,parse就是将html字符串转换成抽象语法树,而execute则是将抽象语法树里的某个node也就是节点替换成对应的数据,然后再render变成html字符串。

关于抽象语法树相关的知识如果感兴趣可以去看下我之前分析vue/compiler-core源码的文章。

扯远了,我们回到代码中,现在每次都只会去处理抽象语法树,而不会再次解析模板。


校验

目前我们的路径就只支持view/edit/save三种类型,但是我们并不能限制用户在浏览器输入什么。所以这个时候我们只能是去处理用户输入的内容。

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 // The title is the second subexpression.
}

这里使用的是regexp[4]这个标准库里的包,看名字就知道是正则表达式相关的。

  • regexp.MustCompile:解析正则表达式并且返回一个regexp.Regexp类型的值。和前面template.Must类似,如果遇到问题也是直接panic
  • validPath.FindStringSubmatch:这个自然不必多说,就是正则匹配。

我们直接来用下,如果请求路径不符合/view/edit/save开头的直接给跳到404

func viewHandler(w http.ResponseWriter, r *http.Request) {
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) {
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) {
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)
}



img_404_not_found


闭包

go中闭包和前端中的闭包类似,都是指内部的函数中引用了外部函数的变量。

我们来调整下之前我们的函数字面量,我们实际上存在一些重复的逻辑可以通过闭包的方式来优化。

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")
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)
}

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

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
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])
}
}

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

log.Fatal(http.ListenAndServe(":8080", nil))
}

我们将view/edit/saveHandler作为参数传递给makeHandler函数,然后它返回一个函数,是一个wrapper

实际上这是一个高阶函数,接受一个函数并且返回一个函数。

这里优化的点是校验的逻辑,这样就没必要去每个函数里面写了。

当然,也不是非得这么写才能优化。实际上我们可以通过别的方式去实现,比如写前置路由拦截等。


最终代码

- The Go Programming Language (google.cn)


总结

这只是一个非常简单的demo,但是可以明显的感觉到开发和上手难度都不高。

参考

  1. ^net/httphttps://pkg.go.dev/net/http
  2. ^html/templatehttps://pkg.go.dev/html/template
  3. ^mustachehttp://mustache.github.io/
  4. ^regexphttps://pkg.go.dev/regexp

路过

雷人

握手

鲜花

鸡蛋
版权声明:免责声明:文章信息来源于网络以及网友投稿,本网站只负责对文章进行整理、排版、编辑,是出于传递 更多信息之目的, 并不意味着赞同其观点或证实其内容的真实性,如本站文章和转稿涉及版权等问题,请作者在及时联系本站,我们会尽快处理。
已有 0 人参与

会员评论

相关分类

 万奢网手机版

官网微博:万奢网服务平台

今日头条二维码 1 微信公众号二维码 1 抖音小程序二维码 1
上海万湖珠宝贸易有限公司 地址:上海市宝山区共和新路4727号新陆国际大厦1003-1007室 网站经营许可证 备案号:沪ICP备11005343号-12012-2019
万奢网主要专注于手表回收,二手名表回收/销售业务,可免费鉴定(手表真假),评估手表回收价格,正规手表回收公司,宝山实体店,支持全国范围上门回收手表
返回顶部