【Go学习实战】03-4-归档搜索优化
- 文档归档
- 查看前端
- 定义返回数据
- 后端
- 测试
- 自定义页面
- 搜索
- 查看前端
- 路由
- 优化
- 查询优化
- orm
- 服务启动优化
- 路由优化
- MsContext
- Handler方法:注册路由
- ServeHTTP 方法:处理 HTTP 请求
- Trie 前缀树存储和匹配路由
- 获取请求参数
- 解析表单
- 解析 JSON 请求体
- 改造路由
- CategoryNew
- LoginNew
文档归档
查看前端
归档页面在
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><!-- <meta name="referrer" content="no-referrer"> --><meta name="description" content="{{.Description}}" /><title>{{.Title}}</title><link rel="stylesheet" href="//at.alicdn.com/t/font_2974461_sdshp7jo1jj.css"/><link rel="stylesheet" href="//at.alicdn.com/t/font_3180954_chol8bk1q14.css"/><link rel="stylesheet" href="/resource/css/index.css" /><link rel="icon" href="/resource/images/favicon.png" /><script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
</head>
<body>
{{template "header" .}}
<main class="container"><div class="container-left">{{template "personal" .}}</div><div class="container-right"><ul class="pige-content">{{range $key,$value := .Lines}}<li class="pige-box" key="{{$key}}"><h1 class="pige-month iconfont icon-wenjianjia"> {{$key}}</h1><ul class="pige-wrap">{{range $index,$elem := $value}}<li class="pige-label"><span class="iconfont icon-riqi"> {{dateDay $elem.CreateAt}}</span><a href="/p/{{$elem.Pid}}.html">{{$elem.Title}}</a></li>{{end}}</ul></li>{{end}}</ul></div>
</main>
{{template "footer" .}}
</body>
<script src="/resource/js/index.js"></script>
</html>
我们明白需要组装那些数据
定义返回数据
在models/post.go中
type PigeholeRes struct {config.Viewerconfig.SystemConfigCategorys []Category `json:"categorys"`Lines []map[string]Post `json:"lines"`
}
后端
请求路径为http://localhost:8080/pigeonhole,刚刚已经知道了是一个html页面,所以我们把他写到views里。
路由
http.HandleFunc("/pigeonhole", views.HTML.Pigeonhole)
接口
type HTMLRenderer interface {Index(w http.ResponseWriter, r *http.Request)Category(w http.ResponseWriter, r *http.Request)Login(w http.ResponseWriter, r *http.Request)Detail(w http.ResponseWriter, r *http.Request)Writing(w http.ResponseWriter, r *http.Request)Pigeonhole(w http.ResponseWriter, r *http.Request)
}
实现
func (*HTMLApi) Pigeonhole(w http.ResponseWriter, r *http.Request) {pigeonhole := common.Template.Pigeonholepigeonhole.WriteData(w, config.Cfg.Viewer)
}
我们组装数据主要有两部分,一部分分类,一部分按时间分类的文章。
我们编写dao层查询按时间分类的文章。
func GetAllPost() ([]models.Post, error) {rows, err := DB.Query("select * from blog_post")if err != nil {return nil, err}defer rows.Close()var posts []models.Postfor rows.Next() {var post models.Posterr := rows.Scan(&post.Pid, &post.Title, &post.Content, &post.Markdown, &post.CategoryId, &post.UserId, &post.ViewCount, &post.Type, &post.Slug, &post.CreateAt, &post.UpdateAt)if err != nil {return nil, err}posts = append(posts, post)}return posts, nil
}
service层
package serviceimport ("log""myWeb/config""myWeb/dao""myWeb/models"
)func FindPostByPigeonhole() *models.PigeholeRes {posts, err := dao.GetAllPost()if err != nil {log.Printf("GetAllPost查询归档失败: %v", err)return nil}//按月份归档//map[string]Postarchive := make(map[string][]models.Post)for _, post := range posts {month := post.CreateAt.Format("2006-01")archive[month] = append(archive[month], post)}categorys, err := dao.GetAllCategory()if err != nil {log.Printf("GetAllCategory查询分类失败: %v", err)return nil}return &models.PigeholeRes{config.Cfg.Viewer,config.Cfg.System,categorys,archive,}
}
测试
自定义页面
我们的导航条应该导航到自定义链接中,我们发布文章时那些相同的自定义链接都会指向该导航条
我们之前写过导航条,在index里,现在只需要带上自定义链接进行查询即可,我们可以在查询的时候加上这个slug条件,如果slug不为空,则显示自定义页面,否则正常返回首页
var posts []models.Postif slug == "" {posts, err = dao.GetPostArticlePage(page, limit)if err != nil {log.Println("查询文章异常")return nil, err}
} else {posts, err = dao.GetPostBySlug(slug, page, limit)if err != nil {log.Println("查询分类异常")return nil, err}
}
dao层
func GetPostBySlug(slug string, page, pageSize int) ([]models.Post, error) {row, err := DB.Query("select * from blog_post where slug = ? limit ?,?", slug, (page-1)*pageSize, pageSize)if err != nil {return nil, err}defer row.Close()var posts []models.Postfor row.Next() {var post models.Posterr := row.Scan(&post.Pid, &post.Title, &post.Content, &post.Markdown, &post.CategoryId, &post.UserId, &post.ViewCount, &post.Type, &post.Slug, &post.CreateAt, &post.UpdateAt)if err != nil {return nil, err}posts = append(posts, post)}return posts, nil
}
我们在展示的时候针对total也要进行变化,如果slug为空则是全部,否则按slug查
var total int
if slug == "" {total = dao.CountGetAllPost()
} else {total = dao.CountGetAllPostBySlug(slug)
}pagesCount := (total-1)/limit + 1
var pages []int
for i := 0; i < pagesCount; i++ {pages = append(pages, i+1)
}
dao层
func CountGetAllPostBySlug(slug string) int {var count interr := DB.QueryRow("SELECT COUNT(1) FROM blog_post WHERE slug = ?", slug).Scan(&count)if err != nil {log.Printf("查询文章总数失败: %v", err)return 0}return count
}
测试
搜索
查看前端
function searchHandler(val) {if (!val) return (searchList = []);$.ajax({url: "/api/v1/post/search?val=" + val,contentType: "application/json",success: function (res) {if (res.code !== 200) return alert(res.error);var data = res.data || [];searchList = [];if (data.length === 0) return drop.html("");for (var i = 0, len = data.length; i < len; i++) {var item = data[i];searchList.push("<a href='/p/" + item.pid + ".html'>" + item.title + "<a/>");drop.show().html(searchList.join(""));}},});
}
data里有pid有title,这就是我们要组装的数据
先定义返回值
type SearchResp struct {Pid int `json:"pid"` // 文章IDTitle string `json:"title"`
}
路由
请求路径为api/v1/post/search?val=xxxx,所以我们要进行解析
http.HandleFunc("/api/v1/post/search", api.API.Search)
接口
type APIResponder interface {SaveAndUpdatePost(w http.ResponseWriter, r *http.Request)Login(w http.ResponseWriter, r *http.Request)GetPost(w http.ResponseWriter, r *http.Request)UploadImage(w http.ResponseWriter, r *http.Request)Search(w http.ResponseWriter, r *http.Request)
}
dao层
func SearchPost(val string) ([]models.SearchResp, error) {rows, err := DB.Query("select pid, title from blog_post where title like ?", "%"+val+"%")if err != nil {return nil, err}defer rows.Close()var searchRes []models.SearchRespfor rows.Next() {var search models.SearchResperr := rows.Scan(&search.Pid, &search.Title)if err != nil {return nil, err}searchRes = append(searchRes, search)}return searchRes, nil
}
service
func SearchPost(val string) ([]models.SearchResp, error) {return dao.SearchPost(val)
}
优化
查询优化
package daoimport ("database/sql""fmt"_ "github.com/go-sql-driver/mysql""log""net/url""reflect""strconv""time"
)type MsDB struct {*sql.DB
}
var DB MsDB
func init() {//执行main之前 先执行init方法dataSourceName := fmt.Sprintf("root:root@tcp(localhost:3306)/goblog?charset=utf8&loc=%s&parseTime=true",url.QueryEscape("Asia/Shanghai"))db, err := sql.Open("mysql", dataSourceName)if err != nil {log.Println("连接数据库异常")panic(err)}//最大空闲连接数,默认不配置,是2个最大空闲连接db.SetMaxIdleConns(5)//最大连接数,默认不配置,是不限制最大连接数db.SetMaxOpenConns(100)// 连接最大存活时间db.SetConnMaxLifetime(time.Minute * 3)//空闲连接最大存活时间db.SetConnMaxIdleTime(time.Minute * 1)err = db.Ping()if err != nil {log.Println("数据库无法连接")_ = db.Close()panic(err)}DB = MsDB{db}
}func (d *MsDB) QueryOne(model interface{},sql string,args... interface{}) error {rows,err := d.Query(sql, args...)if err != nil {return err}columns, err := rows.Columns()if err != nil {return err}vals := make([][]byte,len(columns))scans := make([]interface{},len(columns))for k := range vals{scans[k] = &vals[k]}if rows.Next() {err = rows.Scan(scans...)if err != nil {return err}}var result = make(map[string]interface{})elem := reflect.ValueOf(model).Elem()for index,val := range columns{result[val] = string(vals[index])}for i :=0 ;i <elem.NumField();i++{structField := elem.Type().Field(i)fieldInfo := structField.Tag.Get("orm")v := result[fieldInfo]t := structField.Typeswitch t.String() {case "int":s := v.(string)vInt,_ := strconv.Atoi(s)elem.Field(i).Set(reflect.ValueOf(vInt))case "string":elem.Field(i).Set(reflect.ValueOf(v.(string)))case "int64":s := v.(string)vInt64,_ := strconv.ParseInt(s,10,64)elem.Field(i).Set(reflect.ValueOf(vInt64))case "int32":s := v.(string)vInt32,_ := strconv.ParseInt(s,10,32)elem.Field(i).Set(reflect.ValueOf(vInt32))case "time.Time":s := v.(string)t,_ := time.Parse(time.RFC3339,s)elem.Field(i).Set(reflect.ValueOf(t))}}return nil
}
orm
orm要与数据库对应,从而进行一一反射
type Post struct {Pid int `orm:"pid" json:"pid"` // 文章IDTitle string `orm:"title" json:"title"` // 文章IDSlug string `orm:"slug" json:"slug"` // 自定也页面 pathContent string `orm:"content" json:"content"` // 文章的htmlMarkdown string `orm:"markdown" json:"markdown"` // 文章的MarkdownCategoryId int `orm:"category_id" json:"categoryId"` //分类idUserId int `orm:"user_id" json:"userId"` //用户idViewCount int `orm:"view_count" json:"viewCount"` //查看次数Type int `orm:"type" json:"type"` //文章类型 0 普通,1 自定义文章CreateAt time.Time `orm:"create_at" json:"createAt"` // 创建时间UpdateAt time.Time `orm:"update_at" json:"updateAt"` // 更新时间
}
通过反射拿到对应的字段然后跟orm进行匹配再匹配到对应的结构体的属性,从而实现转换。
服务启动优化
回想我们的springboot,启动类是不是只有个springbootApplication.run(),很干净,所以我们也要学习这种思想,为了后续多实例的启动,我们把main中服务的函数提出去到server
package serverimport ("log""myWeb/router""net/http"
)var App = &Server{}type Server struct {
}func (*Server) Start(ip, port string) {server := http.Server{Addr: ip + ":" + port,}//路由router.Router()if err := server.ListenAndServe(); err != nil {log.Println(err)}
}
这样我们main中就可以很灵活的进行绑定了
package mainimport ("myWeb/common""myWeb/server"
)func init() {//模板加载common.LoadTemplate()
}
func main() {server.App.Start("127.0.0.1", "8080")
}
并且这些ip和端口也都可以放到config中进行配置,这样就实现了模块化
路由优化
看我们的路由非常繁琐
package routerimport ("myWeb/api""myWeb/views""net/http"
)func Router() {//1. 页面 views 2. api 数据(json) 3. 静态资源//viewshttp.HandleFunc("/", views.HTML.Index)http.HandleFunc("/c/", views.HTML.Category)http.HandleFunc("/login/", views.HTML.Login)http.HandleFunc("/p/", views.HTML.Detail)http.HandleFunc("/writing", views.HTML.Writing)http.HandleFunc("/pigeonhole", views.HTML.Pigeonhole)//apishttp.HandleFunc("/api/v1/post", api.API.SaveAndUpdatePost)http.HandleFunc("/api/v1/post/", api.API.GetPost)http.HandleFunc("/api/v1/login", api.API.Login)http.HandleFunc("/api/v1/upload/oss", api.API.UploadImage)http.HandleFunc("/api/v1/post/search", api.API.Search)//静态资源http.Handle("/resource/", http.StripPrefix("/resource/", http.FileServer(http.Dir("public/resource/"))))}
我们要把这个优化一下
MsContext
var Context = NewContext()type MsContext struct {Request *http.RequestW http.ResponseWriterrouters map[string]func(ctx *MsContext)pathArgs map[string]map[string]string
}func NewContext() *MsContext {ctx := &MsContext{}ctx.routers = make(map[string]func(ctx2 *MsContext))ctx.pathArgs = make(map[string]map[string]string)return ctx
}
-
routers
:存储静态路由和动态路由的映射,key
是路径,value
是处理函数。 -
pathArgs
:存储解析出来的路径参数,例如/users/{id}
对应{ "id": "123" }
。
Handler方法:注册路由
首先我们的http.Handle后面要跟个处理器Handler
type Handler interface {ServeHTTP(ResponseWriter, *Request)
}
这要实现ServeHTTP这个方法就是这个Handler接口的实现类,就可以把我们的Handler放进去了
func (ctx *MsContext) Handler(url string, f func(context *MsContext)) {UrlTree.Insert(url)ctx.routers[url] = f
}
-
普通路由(如
/home
)直接存储到ctx.routers
里。 -
路径参数路由(如
/user/{id}
)也存储,但会额外插入Trie
树,以支持路径匹配。
ServeHTTP 方法:处理 HTTP 请求
func (ctx *MsContext) ServeHTTP(w http.ResponseWriter,r *http.Request) {ctx.W = wctx.Request = rpath := r.URL.Pathf := ctx.routers[path]if f == nil {for key,value := range ctx.routers {//判断是否为携带路径参数的reg,_ := regexp.Compile("(/\\w+)*(/{\\w+})+(/\\w+)*")match := reg.MatchString(key)if !match {continue}isHav,args := UrlTree.Search(path)if isHav {//匹配上 存储路径对应参数ctx.pathArgs[path] = argsvalue(ctx)}}}else{f(ctx)}
}
- 如果是静态路由,直接找到并执行
ctx.routers[path]
。 - 如果是动态路由,先用正则
(/\\w+)*(/{\\w+})+(/\\w+)*
检查key
是否有{param}
变量,再用UrlTree.Search(path)
匹配。
因为我们http.Handle("/", context.Context)
是对所有根路径都进行匹配,所以会先进入ServeHTTP中。
先看在我们的路由中有没有这个,如果在路由的map中有就直接返回,没有的话进行解析,构建前缀树。
-
先判断有没有带参数
-
/\\w+
:/
:匹配斜杠/
。\\w+
:匹配一个或多个字母、数字、下划线(\w
相当于[a-zA-Z0-9_]
)。- 例如:
/user
、/order
。
-
(/\\w+)*
(/\\w+)*
:表示前面的/\\w+
这一部分可以出现 0 次或多次(即可选的多个静态路径段)。- 匹配空字符串(因为
*
允许 0 次)或匹配/user
、/user/order
、/a/b/c
。
-
/{\\w+}
/
:匹配斜杠/
。{\\w+}
:匹配大括号{}
内的字母、数字、下划线(通常用于表示路径参数)。- 例如:
/{id}
、/{name}
。
-
+
({\\w+})+
:表示路径参数部分至少出现一次(即/user/{id}
是合法的,但/user
不是)。- 例如:
/user/{id}
、/product/{pid}/order/{oid}
。
-
如果是有{}形式的,那么我们认为是有路径参数的,然后在前缀树中找有没有这个路径,如果有这个路径,通过前缀树匹配就拿到了这个路径参数,就把这个路径和参数放到map中,然后执行这个方法就可以了。
Trie 前缀树存储和匹配路由
Trie 树用于存储和匹配路径参数,避免遍历整个 routers
。
插入路由
func (t *Trie) Insert(word string) {for _, v := range strings.Split(word, "/") {if t.next[v] == nil {node := new(Trie)node.next = make(map[string]*Trie)node.isWord = falset.next[v] = node}// {X} 是路径参数,存储到 Trieif v == "*" || strings.Index(v, "{") != -1 {t.isWord = true}t = t.next[v]}t.isWord = true
}
-
"/user/{id}"
被拆分成["", "user", "{id}"]
存入 Trie。 -
"{id}"
代表动态参数,isWord = true
标记为路径终点。
匹配路由
func (t *Trie) Search(word string) (isHave bool, arg map[string]string) {arg = make(map[string]string)isHave = falsefor _, v := range strings.Split(word, "/") {if t.isWord {for k := range t.next {if strings.Index(k, "{") != -1 {key := strings.Replace(k, "{", "", -1)key = strings.Replace(key, "}", "", -1)arg[key] = v // 提取路径参数}v = k // 继续匹配下一级}}if t.next[v] == nil {log.Println("找不到了, 匹配不上")return}t = t.next[v]}// 确保路径完全匹配if len(t.next) == 0 {isHave = t.isWord}return
}
-
解析
/users/123
,匹配到/users/{id}
,存储{ "id": "123" }
。 -
必须匹配完整路径,如
/users/{id}/details
不能匹配/users/123
。
获取请求参数
func (ctx *MsContext) GetPathVariable(key string) string {return ctx.pathArgs[ctx.Request.URL.Path][key]
}
例如 ctx.GetPathVariable("id")
获取 /users/123
中 123
。
解析表单
适用于 application/x-www-form-urlencoded
表单提交。
func (ctx *MsContext) GetForm(key string) (string, error) {if err := ctx.Request.ParseForm(); err != nil {log.Println("表单获取失败:", err)return "", err}return ctx.Request.Form.Get(key), nil
}
解析 JSON 请求体
适用于 application/json
提交的请求体,获取 JSON
里的 key
值。
func (ctx *MsContext) GetJson(key string) interface{} {var params map[string]interface{}decoder := json.NewDecoder(ctx.Request.Body)err := decoder.Decode(¶ms)if err != nil {log.Printf("解析请求参数失败:%v", err)return err}return params[key]
}
改造路由
router就被改造成这样了
func Router() {http.Handle("/", context.Context)context.Context.Handler("/c/{id}", views.HTML.CategoryNew)context.Context.Handler("/login", views.HTML.LoginNew)
}
CategoryNew
func (*HTMLApi) CategoryNew(ctx *context.MsContext) {categoryTemplate := common.Template.Category//http://localhost:8080/c/1 1参数 分类的idcIdStr := ctx.GetPathVariable("cId")cId, _ := strconv.Atoi(cIdStr)pageStr, _ := ctx.GetForm("page")if pageStr == "" {pageStr = "1"}page, _ := strconv.Atoi(pageStr)//每页显示的数量pageSize := 10categoryResponse, err := service.GetPostsByCategoryId(cId, page, pageSize)if err != nil {categoryTemplate.WriteError(ctx.W, err)return}categoryTemplate.WriteData(ctx.W, categoryResponse)
}
LoginNew
func (*HTMLApi) LoginNew(ctx *context.MsContext) {login := common.Template.Loginlogin.WriteData(ctx.W, config.Cfg.Viewer)
}