golang web 服务器 request 与 response 处理

介绍常见 web 服务的实现与输入、输出数据处理。包括:静态文件服务、js 请求支持、模板输出、表单处理、Filter 中间件设计。

一、静态文件服务支持

访问 html 静态网页是 web 基础服务。

github.com/pmlpml/golang-learning/web/cloudgo-static

1.1 使用文件服务

一般来说,生产环境静态文件的访问交给 WEB 服务器 Apache / Lighttpd / Nginx 处理。在开发、测试阶段也可以让 net/http 库处理。

server.go

package service

import (
    "net/http"
    "os"

    "github.com/codegangsta/negroni"
    "github.com/gorilla/mux"
    "github.com/unrolled/render"
)

// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {

    formatter := render.New(render.Options{
        IndentJSON: true,
    })

    n := negroni.Classic()
    mx := mux.NewRouter()

    initRoutes(mx, formatter)

    n.UseHandler(mx)
    return n
}

func initRoutes(mx *mux.Router, formatter *render.Render) {
    webRoot := os.Getenv("WEBROOT")
    if len(webRoot) == 0 {
        if root, err := os.Getwd(); err != nil {
            panic("Could not retrive working directory")
        } else {
            webRoot = root
            //fmt.Println(root)
        }
    }

    //mx.HandleFunc("/api/test", apiTestHandler(formatter)).Methods("GET")
    mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))

}
ServeFileFileServer

首先我们需要在服务器上创建目录,以存放静态内容。例如:

assets(静态文件虚拟根目录)
  |-- js
  |-- images
  +-- css
mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))webRoot + "/assets/"
http.Dirhttp.DirFileSystemhttp.FileServer()Handlerrootmx.PathPrefix
assetsgo run main.go
http;//localhost:8080/cssjs
index.html

1.2 支持 JavaScript 访问

随着 web 页面技术的进步,页面中大量使用 javascript。 添加一个服务:

apitest.go

package service

import (
    "net/http"

    "github.com/unrolled/render"
)

func apiTestHandler(formatter *render.Render) http.HandlerFunc {

    return func(w http.ResponseWriter, req *http.Request) {
        formatter.JSON(w, http.StatusOK, struct {
            ID      string `json:"id"`
            Content string `json:"content"`
        }{ID: "8675309", Content: "Hello from Go!"})
    }
}

这段代码非常简单,输出了一个 匿名结构 ,并 JSON (JavaScript Object Notation) 序列化输出。 打开前面程序的注释,运行网站并用 curl 测试输出!

$ curl http://localhost:8080/api/test
{
  "id": "8675309",
  "content": "Hello from Go!"
}

为了便于理解,课程给的案例非常简答。index.html 是

<html>
<head>
  <link rel="stylesheet" href="css/main.css"/>
  <script src="http://code.jquery.com/jquery-latest.js"></script>
  <script src="js/hello.js"></script>
</head>
<body>
  <img src="images/cng.png" height="48" width="48"/>
  Sample Go Web Application!!
      <div>
          <p class="greeting-id">The ID is </p>
          <p class="greeting-content">The content is </p>
      </div>
</body>
</html>

使用的 hello.js 是:

$(document).ready(function() {
    $.ajax({
        url: "/api/test"
    }).then(function(data) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
    });
});

通过 web 应用控制台 Negroni 输出追踪,获知网页使用 javascript 获取了信息。

现在,你应该可以顺利的与 VUE,Bootstrap 这些做的应用前端与 golang 集成在一起了!

问题: 交互路由两个语句位置,会发生什么?

1.3 处理静态路径前缀

http://localhost:8080/static/js/hello.js
mx.PathPrefix("/static").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
404

正确的代码是:

mx.PathPrefix("/static").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(webRoot+"/assets/"))))
StripPrefixHandlerFunc 类型

小练习

501 Not ImplementedNotImplementedNotImplementedHandler/api/unkown501 Not Implemented

二、使用模板输出

由于 Angluar,React 等 web 前端框架的流行, web 服务器对模板的需求已经不是非常强烈。然而,它依然是格式化数据输出的一种重要手段。 熟悉 jinja2 模板的人对 go 的模板设计应非常亲切。

github.com/pmlpml/golang-learning/web/cloudgo-template

2.1 输出 html 页面

github.com/unrolled/render

srver.go

package service

import (
    "net/http"
    "os"

    "github.com/codegangsta/negroni"
    "github.com/gorilla/mux"
    "github.com/unrolled/render"
)

// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {

    formatter := render.New(render.Options{
        Directory:  "templates",
        Extensions: []string{".html"},
        IndentJSON: true,
    })

    n := negroni.Classic()
    mx := mux.NewRouter()

    initRoutes(mx, formatter)

    n.UseHandler(mx)
    return n
}

func initRoutes(mx *mux.Router, formatter *render.Render) {
    webRoot := os.Getenv("WEBROOT")
    if len(webRoot) == 0 {
        if root, err := os.Getwd(); err != nil {
            panic("Could not retrive working directory")
        } else {
            webRoot = root
            //fmt.Println(root)
        }
    }

    mx.HandleFunc("/", homeHandler(formatter)).Methods("GET")
    mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
}

要点:
(1)formatter 构建,指定了模板的目录,模板文件的扩展名
(2)homeHandler 使用了模板

assetstemplates
<html>
<head>
  <link rel="stylesheet" href="css/main.css"/>
</head>
<body>
  <img src="images/cng.png" height="48" width="48"/>
  Sample Go Web Application!!
      <div>
          <p class="greeting-id">The ID is {{.ID}}</p>
          <p class="greeting-content">The content is {{.Content}}</p>
      </div>
</body>
</html>
{{.}}{{.ID}}

home.go 处理了数据填充。

package service

import (
    "net/http"

    "github.com/unrolled/render"
)

func homeHandler(formatter *render.Render) http.HandlerFunc {

    return func(w http.ResponseWriter, req *http.Request) {
        formatter.HTML(w, http.StatusOK, "index", struct {
            ID      string `json:"id"`
            Content string `json:"content"`
        }{ID: "8675309", Content: "Hello from Go!"})
    }
}

我们使用 formatter 的 HTML 直接将数据注入模板,并输出到浏览器。 更多内容见 render 在 git 上的 README。

$ go run main.go

在浏览器访问:http://localhost:8080

注意观察控制台 negroni 输出。

  • 访问 “/” 第一次与后续访问时间的差异
  • 访问 css 与 js 的返回状态, 为什么是 304 ?

2.2 使用 text/template 库

golang 提供了强大的 template 库,上述 Render 仅是它的简单包装。 官网的例子也非常简单:

type Inventory struct {
    Material string
    Count    uint
}
sweaters := Inventory{"wool", 17}
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
if err != nil { panic(err) }
err = tmpl.Execute(os.Stdout, sweaters)
if err != nil { panic(err) }
template.New("name").Parse("{{.Content}} string").Execute(writer,data)
New("name")

阅读 text/template API 文档,解释输出

    t := template.Must(template.New("letter").Parse("A{{.}}\n"))
    t1 := template.Must(t.New("letter1").Parse("B{{.}}\n"))
    t.Execute(os.Stdout, "1")
    t1.Execute(os.Stdout, "2")
    fmt.Println(len(t1.Templates()))
    for _, tt := range t1.Templates() {
        fmt.Println(tt.Name())
    }

这段代码的输出是?…

letter1lettert.Newtemplate.New
ParseFiles(...)
ParseFilesParseGlob

简明了解模板的语法:golang模板语法简明教程
代码与实例:Go语言核心之美 3.6-template模版

看完后,请完成:

  • 输出一个 html 报表。 表格头是 “手机销量,Q1,Q2,Q3,Q3同比,Q3比上季”,数据是“华为,OPPO,小米 ,OV,Apple”
  • 使用自定义格式化函数,格式化百分数

三、处理 Request

3.1 request 定义

要点:

  • 与 http 协议的关系

3.2 如何处理表单

补充:

return &MyStruct{map["xx"]}

四、 negroni 与 Filter 模式

java web 有三大神器 Filter、Servlet、Listener。

  • Servlet 称为服务小程序,处理 request,产生 response。 golang 的 net/http 的 Handler、HandlerFunc 以及 第三方 http mux 或 http router 包都完成这类功能
  • Filter 称为过滤器或拦截器,处理 request 预处理 或 response 后处理,它常用的用途是 处理输入、输出的语言编码、日志、用户权限管理、session管理等等。它采用 Filter Chain 将一组 Filter 组件联系起来。 golang negroni 完成该工作。
  • Listener 称为侦听器,用户感知 web 上下文(Context)变化,并通知用户。 golang ?
    • application
    • session
    • request

对比 servlet: Java Servlet pk Golang Handler

对比 Filter: Java Filter pk negroni Handler

java 文档给出了常用的 Filter!

4.1 Filter 原理

go negroni “中间件”模板代码:

func MyMiddleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  // do some stuff before
  next(rw, r)
  // do some stuff after
}

4.2 negroni组件

  • negroni classic 的实现:

// Classic returns a new Negroni instance with the default middleware already
// in the stack.
//
// Recovery - Panic Recovery Middleware
// Logger - Request/Response Logging
// Static - Static File Serving
func Classic() *Negroni {
    return New(NewRecovery(), NewLogger(), NewStatic(http.Dir("public")))
}
  • 建立标准的意义 - negroni 第三方扩展

例如,让 web 支持 gzip 协议:

注意,如果要支持静态文件压缩,请注意顺序!