Go Web开发入门 | 青训营笔记

189 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记

Go Web开发入门学习

原生web框架

package main

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

func main() {
   fmt.Println("Starting the server")
   hellohandler := originweb.NewHelloHandler()
   //设置路由 传入必须实现Handler接口
   http.Handle("/Hello", hellohandler)
   //创建服务器  ListenAndServer设置监听端口 若服务器宕机,则返回异常
   log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
package originweb

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

type HelloHandler struct {
   m map[string]string
}

func NewHelloHandler() *HelloHandler {
   return &HelloHandler{m: make(map[string]string)}
}

//实现Handler接口
func (h HelloHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
   //接收request中body
   b := request.Body
   defer b.Close()
   buf := bytes.Buffer{}
   buf.ReadFrom(b)
   //转换为字符串
   s := buf.String()
   fmt.Printf("get request :\nMethod:%s\n%s\n%s\n", request.Method, request.RequestURI, s)
   //返回body
   writer.Write(buf.Bytes())
}

返回示例

image.png
存在问题:

  1. 请求方法必须要先由路由拦截
  2. 需要手动处理IO,读取后body需要映射到实例
  3. 参数需要用正则取得
  4. 路由管理分组混乱
  5. 返回值需要我们手动处理为[]byte类型

路由改进

使用httprouter框架httprouter
Gin等框架也采用了httprouter框架

go get github.com/julienschmidt/httprouter

实现

http.ListenAndServe("localhost:8080", handler)

当ListenAndServer方法传入handler时,Go将忽略http.Handle中注册的路由,使用该位置Handler解析所有路由 httprouter构建了此位置Handler,请求发送至服务,拦截后先经由默认Handler进行处理,然后进入到httprouter框架,分发给你在框架中注册的路由

package main

import (
   "fmt"
   "github.com/julienschmidt/httprouter"
   "log"
   "net/http"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
   fmt.Fprintf(w, "hello\n")
}
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
   fmt.Fprintf(w, "hello,%s\n", ps.ByName("name"))
}
func main() {
   //创建路由
   router := httprouter.New()
   router.GET("/", Index)
   //:name进行参数绑定 通过ByName()方法获取
   router.GET("/hello/:name", Hello)
   log.Fatal(http.ListenAndServe("localhost:8080", router))
}

httprouter提供的接口

image.png
传入handle为func变量,相较于原生更为灵活
httprouter支持配置CORS,同时我们可以利用Go函数闭包(匿名函数),将原有逻辑封装一层,达到预处理效果

httprouter只是路由框架,本质上还是使用的原生http服务器

  • 设置跨域示例
router.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if r.Header.Get("Access-Control-Request-Method") != "" {
        // Set CORS headers
        header := w.Header()
        header.Set("Access-Control-Allow-Methods", header.Get("Allow"))
        header.Set("Access-Control-Allow-Origin", "*")
    }

    // Adjust status code to 204
    w.WriteHeader(http.StatusNoContent)
})
  • 设置预处理示例
    • Basic Authentication
package main

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

	"github.com/julienschmidt/httprouter"
)

//将原有逻辑再次封装,加入验证环境
func BasicAuth(h httprouter.Handle, requiredUser, requiredPassword string) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
		// Get the Basic Authentication credentials
		user, password, hasAuth := r.BasicAuth()

		if hasAuth && user == requiredUser && password == requiredPassword {
			// Delegate request to the given handle
			h(w, r, ps)
		} else {
			// Request Basic Authentication otherwise
			w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
		}
	}
}

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Not protected!\n")
}

func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Protected!\n")
}

func main() {
	user := "gordon"
	pass := "secret!"

	router := httprouter.New()
	router.GET("/", Index)
	router.GET("/protected/", BasicAuth(Protected, user, pass))

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

Gin框架

go get github.com/gin-gonic/gin

创建路由引擎

r := gin.New()
r := gin.Default()

Default()创建的路由会默认使用Logger()和Recovery()中间件

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
   debugPrintWARNINGDefault()
   engine := New()
   engine.Use(Logger(), Recovery())
   return engine
}

中间件

Use()接口会将Logger()和Recover()处理函数追加到UserHandler()前,形成handers链

image.png 中间件部署是使用了gin.Use()

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
   engine.RouterGroup.Use(middleware...)
   engine.rebuild404Handlers()
   engine.rebuild405Handlers()
   return engine
}

gin的中间件就是一个HandlerFunc数组 HandlerFunc是一个返回类型为*Context的函数

默认中间件Logger与Recovery

可以参考Go Web开发入门指南<前半> By 可乐可乐可 --Gin默认的两个middleware

Gin Router配置

Gin框架提供了分组路由

func main() {
	router := gin.Default()

	// Simple group: v1
	v1 := router.Group("/v1")
	{
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}

	// Simple group: v2
	v2 := router.Group("/v2")
	{
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}

	router.Run(":8080")
}

Group()函数

// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
//传入url  HandlerFunc数组
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
   //继续返回一个RouterGroup
   return &RouterGroup{
      Handlers: group.combineHandlers(handlers),
      basePath: group.calculateAbsolutePath(relativePath),
      engine:   group.engine,
   }
}

combineHandlers()函数

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
   finalSize := len(group.Handlers) + len(handlers)
   if finalSize >= int(abortIndex) {
      panic("too many handlers")
   }
   //复制传入的路由添加至原数组 手动合并后返回
   mergedHandlers := make(HandlersChain, finalSize)
   copy(mergedHandlers, group.Handlers)
   copy(mergedHandlers[len(group.Handlers):], handlers)
   return mergedHandlers
}

总结

  • roup与父Group公用engine
  • roup会获取父Group当前的Handler拼装成自己的Group,形成一个Handler调用链,我们可以在父Group插入Handler链作为中间件
  • 子Goup通过计算得到自己的绝对路径

若在子Group创建后再在父Group添加中间件,子Group无法同步handler链 所有的router都处在了gin.Engine下,RouterGroup.Get/POst/..方法才可以注册路由 GET方法中调用了RouterGoup.handle()

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
   return group.handle("GET", relativePath, handlers)
}

handle通过engine.addRouter注册路由

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
   //路径拼接
   absolutePath := group.calculateAbsolutePath(relativePath)
   //生成handlers链
   handlers = group.combineHandlers(handlers)
   group.engine.addRoute(httpMethod, absolutePath, handlers)
   return group.returnObj()
}

所以RouterGroup内engine

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
   assert1(path[0] == '/', "path must begin with '/'")
   assert1(method != "", "HTTP method can not be empty")
   assert1(len(handlers) > 0, "there must be at least one handler")

   debugPrintRoute(method, path, handlers)
   root := engine.trees.get(method)
   if root == nil {
      root = new(node)
      engine.trees = append(engine.trees, methodTree{method: method, root: root})
   }
   root.addRoute(path, handlers)
}

所以创建RouterGoup时添加的Handler不会被注册在路由,而是在使用GET,POST等方法时,返回一条handlers链,注册路由到路由表。

Gin路由记录方法

Gin的路由由一颗methodTrees成

在小量数据下,使用map是不划算的,map的构建与维护,包括hash操作,在小量数据下,是一种入不敷出的选择,所以这里,遍历比map效率更高

type methodTree struct {
   method string
   root   *node
}

//返回根节点
func (trees methodTrees) get(method string) *node {
   for _, tree := range trees {
      if tree.method == method {
         return tree.root
      }
   }
   return nil
}

type node struct {
   path      string
   indices   string
   children  []*node
   handlers  HandlersChain
   priority  uint32
   nType     nodeType
   maxParams uint8
   wildChild bool
}

Gin在注册路由时,遍历树,找到自己的位置,将绝对路径与调用链一起记录

root := engine.trees.get(method)
if root == nil {
   root = new(node)
   engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)

Gin启动

创建engine进行添加路由等工作
最后

engine.Run(":8080")

参数为空则默认为8080端口,可以传入多个地址进行监听

参数解析与绑定

type HandlerFunc func(*Context) 传入了context实例

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
   writermem responseWriter
   Request   *http.Request
   Writer    ResponseWriter
   Params   Params
   handlers HandlersChain
   index    int8
   fullPath string
   engine       *Engine
   params       *Params
   skippedNodes *[]skippedNode

   // This mutex protect Keys map
   mu sync.RWMutex

   // Keys is a key/value pair exclusively for the context of each request.
   Keys map[string]interface{}

   // Errors is a list of errors attached to all the handlers/middlewares who used this context.
   Errors errorMsgs

   // Accepted defines a list of manually accepted formats for content negotiation.
   Accepted []string

   // queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
   queryCache url.Values

   // formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
   // or PUT body parameters.
   formCache url.Values

   // SameSite allows a server to define a cookie attribute making it impossible for
   // the browser to send this cookie along with cross-site requests.
   sameSite http.SameSite
}

主要包括了以下方法

  • handler信息
  • 调用链控制
  • 错误管理
  • 上下文资源(类似Java Tread Local)控制
  • 获取参数
  • 写响应

请求信息

image.png

调用链控制

image.png

上下文资源方法

image.png

参数获取方法

image.png

总结
很多方法最近也用不到,我就不写了,主要是还是学习Go Web开发入门指南<前半> By 可乐可乐可大佬文章的笔记是重复23333,参数解析绑定这里他写的十分详细,我就不copy过来了哈哈哈哈
第五步,Gin 参数解析,绑定

参考来源
Go Web开发入门指南<前半> By 可乐可乐可
github.com/gin-gonic/g…
gin框架源码学习笔记(二)中间件