前两篇博客介绍到了掘金数据的爬取和分析,而我们的最终目的是搭建一个小型的检索系统。当然数据全都是偷掘金的了(笑)。目前这个项目已经搭建完毕了,已经发布到github上面了,另外我也之前做了一个伯乐在线的小型检索系统,地址在这。
下面就进入今天的正题,关于如何使用Go语言搭建一个简单的后台项目。由于在信息搜索上面使用到了ElasticSearch这个非常棒的开源项目,所以也使用了Go语言版的elastic实现了内容搜索,这也是这个后台项目唯一的第三方依赖,其它都为Go语言标准库实现的。项目结构入如下图所示:
在static文件夹下是前端的资源部分,view文件夹下是前端视图部分,juejin文件夹下是后台项目的依赖,main.go文件是整个项目的入口,接下来让我们由浅入深的看一下整个项目。utils
因为这个项目的场景不大单纯的只是涉及了内容检索以及资源服务,所以utils这里面的东西并没有太多,而由于Go语言的特性,它会经常涉及error的检查,所以整个utils.go里面也就只有关于error的处理的两个方法。
// print the errorfunc ErrorPrint(err error) bool { if err != nil { log.Println(err) return false } return true}复制代码
第一个方法,由于在使用elastic查询内容时不可避免的会出现error,可是又不能让程序crash掉,于是便有了这个ErrorPrint方法。
// fatal the error then the program will be brokenfunc ErrorFatal(err error) { if err != nil { log.Fatal(err) return }}复制代码
第二个方法,是用于程序出现了严重错误,没有必要在继续运行下去所定义的方法,对于严重的error如在http在ListenAndServe时出错,便可以直接Fatal。
log
顾名思义,这个log.go里面实现了日志打印的功能,对于检测程序运行情况,以及查看请求的信息,一个简单的日志打印功能是十分有必要的。
func WriteLog(r *http.Request, t time.Time, match string, pattern string) { d := time.Now().Sub(t) l := fmt.Sprintf("[ACCESS] | % -10s | % -40s | % -16s | % -10s | % -40s |", Bold(Blue(r.Method)), r.URL.Path, d.String(), Red(match), Green(pattern)) log.Println(l)}复制代码
这个函数会对请求的路径和方法,以及响应时间,匹配路径进行日志输出。至于里面的Red(),Green(),Bold()均是为了改变在终端里面的字体颜色,让整个日志信息更为清晰。效果图如下:
elastic
前面也说到,整个项目会不断的涉及文章检索,内容推荐的操作,而这些便都需要使用elasticsearch来完成,因此笔者把所有关于与elasticsearch交互的功能写在了这个文件里面,其实它也很简单,只是接收请求的参数并将其结构化用来请求elasticsearch。首先看这个getItems函数。
// get the article items from the elastic serverfunc getItems(keyword string, page int) *elastic.SearchHits { // search result from the tags,title,content of the article item query := elastic.NewMultiMatchQuery(keyword, "tags", "title", "content") result, err := client.Search(). Index("juejin"). Query(query). From((page - 1) * 10).Size(10). Do(ctx) if ok := ErrorPrint(err); ok { return result.Hits } return nil}复制代码
这个函数接收两个参数keyword和page,即关键词和页数。然后将它们写入elasticsearch的复合查询中,并将hits结果返回。实际的结果如下图:
另外还有一个getSuggestions函数主要用来做搜索框的提示功能,具体实现大同小异,这里边不同阐述。router
在router.go里面首先定义了一个结构体:
type Controller struct { HandleFunction func(http.ResponseWriter, *http.Request) Method string Pattern string}复制代码
这个结构体里面有三个成员,HandleFunction用来处理请求的响应函数,Method用来装填请求方法,Pattern用来匹配请求的路径,这里使用了正则匹配。
func GetItems(w http.ResponseWriter, r *http.Request) { r.ParseForm() keyword := r.Form.Get("keyword") page_str := r.Form.Get("page") if page_str == "" { // w.WriteHeader(403) page_str = "1" } page, err := strconv.Atoi(page_str) if ok := ErrorPrint(err); ok { w.Header().Set("Content-Type", "application/json") items := getItems(keyword, page) if items != nil { data, err := json.Marshal(items) ok = ErrorPrint(err) if ok { w.Write(data) } } } w.Write([]byte(""))}复制代码
上面的代码是router.go里面的一个方法,主要用来响应网页对于文章内容的请求,效果图便是上面的效果图了。这个函数首先会调用ParseForm方法解析请求的参数,由于elastic的在page上不能使用空值,因此如果接受到的page参数为空的话会将其处理成1,然后在Header里将返回的数据类型设置为json,并且调用elastic.go里面的getItems函数,通过json的编组函数Marshal得到json格式的数据并返回。router.go里面的其它方法也是如此实现的。
main
由于本次使用的是Go语言http.Server这个结构体实现的服务器,因此就必须自定义一个自己的HttpHandler并实现ServeHTTP这个方法。
type httpHandler struct{}func (*httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := time.Now() for _, controller := range mux { if ok, _ := regexp.MatchString(controller.Pattern, r.URL.Path); ok { if r.Method == controller.Method { controller.HandleFunction(w, r) go juejin.WriteLog(r, t, "matched", controller.Pattern) return } } } go juejin.WriteLog(r, t, "unmatch", r.URL.Path) return}复制代码
这个结构体会传到Server.Handler这个参数上,它来实现整个的路由调配。首先它会得到当前的时间t,然后遍历mux这个切片。
func init() { mux = append(mux, juejin.Controller{juejin.GetItems, "GET", "^/api/getItems"}) mux = append(mux, juejin.Controller{juejin.GetSuggestions, "GET", "^/api/getSuggestions"}) mux = append(mux, juejin.Controller{Static, "GET", "^/static/"}) mux = append(mux, juejin.Controller{DefaultPage, "GET", "^/"})}复制代码
mux这个切片里面存放着所有处理请求的方法。当用户请求的方法通过正则匹配到了我们的路径时便调用这个controller里面的方法即controller.HandleFunction(w, r),然后打印一条日志,如果不匹配,则打印不匹配的日志。
另外mux里面前两个都是数据请求的处理,Static这个方法用于处理静态文件,具体如下:
var wd, _ = os.Getwd()func Static(w http.ResponseWriter, r *http.Request) { path := filepath.Join(wd, r.URL.Path) http.ServeFile(w, r, path)}复制代码
我们通过os.Getwd获得当前工作路径,然后通过请求的资源路径并且将这个静态资源通过http.ServeFile方法返回给用户这个资源。
DefaultPage这个方法用于服务于默认的请求页面。
func DefaultPage(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles(filepath.Join(wd, "view/index.html")) if err != nil { w.WriteHeader(403) return } err = tmpl.Execute(w, nil) if err != nil { w.WriteHeader(403) return }}复制代码
这个方法通过html/template这个默认引擎来处理我们的index.html页面,当用户请求时便处理这个模板,然后写入到ResponseWriter中。另外由于我们使用的是正则匹配,并且通过for循环来遍历匹配请求路径,因此必须将"^/"这个路径放在切片的最末尾,否则其它所有请求都会被它匹配,而一旦被它匹配到便会调用DefaltPage这个Controller,其它的Controller便不会调用,整个项目也无法正常运行。好了,由于最近一直在学习Go语言,所以做了项目练手,但确实对路由架构这方面不熟悉,所以整个路由结构非常凌乱。如果您有什么建议,请不吝赐教。