likes
comments
collection
share

Go Web开发入门指南<前半>

作者站长头像
站长
· 阅读数 73

Go Web开发入门指南

内容:Go Web开发套装,Gin,Gorm,viper,validator,zap,go-redis,grpc

本文难度:适合入门

一般开发web项目,需要处理这些

路由匹配,参数获取,ORM持久化操作,日志

更高级一些,将使用redis,Elasticsearch,以及rpc远程调用

如果你是Java转行而来,你需要首先意识到一个问题:Go语言没有Spring那种包办的生态,需要各种框架拼装起来(祈求🎈有Spring这种角色吧)好在Go的设计就是两个字:简单,所以用起来倒是不难

目前,滴滴开源了Go语言的ioc框架go- spring,但是在Go的社区中,并没有使用ioc的风气,大家仍然使用手动管理依赖的模式(可能大多数是python和C++来的人的原因,如果都是Java来的,怕是早已风靡一时)

本文需要的知识储备:Go语言基础(基础语法,json,简易web服务)

从原生Web框架整起

Go语言是一名新生儿(相比隔壁Jvav,C艹老哥),所以Go语言诞生之时就能很轻松的进行主流Web开发(不依托框架),这在Java来看,是很可怕的事情,很难想象没有汤姆猫,Java的咖啡杯还能不能轻松端起来(包括Netty框架)

所以我们首先从Go原生框架做起,从内存存储到io操作,到数据库存储,一步步升级

一个原生服务器

关于Golang原生web框架,看这里:

package main

import (
   "fmt"
   "go-web-tutorial/handler"
   "log"
   "net/http"
)

func main() {
   fmt.Println("Starting the server ...")
   helloHandler := handler.NewHelloHandler()
   http.Handle("/hello", helloHandler)
   // 创建服务器,ListenAndServe若服务器宕机,会返回异常
   log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
package handler

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

type HelloHandler struct {
	m map[string]string
}

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

// 回声服务器,返回接受的body,
// 实现Handler接口
func (h HelloHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	b := request.Body
	buf := bytes.Buffer{}
	buf.ReadFrom(b)
	b.Close()
	s := buf.String()
	fmt.Printf("get request: \nMethod:%s\n%s\n%s\n", request.Method, request.RequestURI, s)
	writer.Write(buf.Bytes())
}

这段代码中:

我们实现了一个简易的后端,读取了body的值,但是这个后端太简单了,

  1. 路由拦截下来后才能得知请求方法
  2. 我们需要手动处理io,
  3. 读取io后body需要手动映射到实例中,
  4. 链接上的参数需要我们手动使用正则取下来
  5. 路由分组问题,比如 /test/t /test/t2
  6. 返回值需要我们手动处理为[]byte类型

后面我们将一步一步进行解决,逐步配上所有的框架

改进路由系统

gin等框架的路由采用的实际上是httprouter框架

httprouter框架:github.com/julienschmi…

httprouter框架改进了原生的Handler路由配置模式

回顾GoWeb原生开发中的知识,Handler使用http.Handle来进行注册,如果我们注册了http.ListenAndServe("localhost:8080", nil)中nil位置的这个Handler(记作默认Handler),Go将忽略http.Handle中注册的路由,使用该位置的Handler解析所有的路由

httprouter就是构建了nil这个位置的Handler,请求发送到服务,被拦截后,经过默认Handler进行处理,然后进入httprouter框架,分发给你在框架中注册的路由

router := httprouter.New()
router.GET("/", defaultHandler.ServeHTTP)
router.GET("/hello/:name", helloHandler.ServeHTTP)
// 开启监听,使用httprouter作为Handler
log.Fatal(http.ListenAndServe(":8080", router))

httprouter提供了一套高性能的Restful风格的接口,如下,

router.GET("/", defaultHandler.ServeHTTP)
router.POST("/hello/:name", helloHandler.ServeHTTP)

Go Web开发入门指南<前半> 其中,首个参数是Path,第二个参数一个func变量,相对原生更灵活,不再需要类型支撑

同时,httprouter支持配置CORS,同时我们可以利用Go函数闭包(匿名函数),将原有逻辑封装一层,达到预处理的效果

需要注意的是,httprouter只是路由框架,这里使用的本质上还是原生的http服务器

CORS的官方demo如下

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", r.Header.Get("Allow"))
        header.Set("Access-Control-Allow-Origin", "*")
    }

    // Adjust status code to 204
    w.WriteHeader(http.StatusNoContent)
})

预处理的官方demo如下

package main

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

	"github.com/julienschmidt/httprouter"
)

// BasicAuth 闭包,预处理一层,然后执行Handle函数参数
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))
}

我们升级一下原生服务器

package main

import (
   "fmt"
   "github.com/julienschmidt/httprouter"
   "go-web-tutorial/handler"
   "log"
   "net/http"
)

func main() {
   fmt.Println("Starting the server ...")

   defaultHandler := handler.NewDefaultHandler()
   helloHandler := handler.NewHelloHandler()

   router := httprouter.New()
   router.GET("/", defaultHandler.ServeHTTP)
   router.GET("/hello/:name", helloHandler.ServeHTTP)

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

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

type HelloHandler struct {
   m map[string]string
}

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

// 回声服务器,返回接受的body
// 实现Handler接口
func (h HelloHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request,params httprouter.Params) {
   b := request.Body
   buf := bytes.Buffer{}
   buf.ReadFrom(b)
   b.Close()
   s := buf.String()
   fmt.Printf( "hello, %s!\n", params.ByName("name"))
   fmt.Printf("get request: \nMethod:%s\n%s\n%s\n", request.Method, request.RequestURI, s)
   writer.Write(buf.Bytes())
}
package handler

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

type DefaultHandler struct{}

func NewDefaultHandler() *DefaultHandler {
   return &DefaultHandler{}
}

func (DefaultHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request,params httprouter.Params) {
   buf := bytes.Buffer{}
   buf.ReadFrom(request.Body)
   s := buf.String()
   log.Printf("\ndefault received \nMethod:%s\n%s\n%s\n", request.Method, request.RequestURI, s)
   writer.Write([]byte("hello"))
}

目前我们解决的问题:

  1. 请求方法问题,路由参数问题(RestFul风格)

这明显不够,于是我们请出Gin框架

用Gin升级系统

Gin是什么?

Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架, 优于 httprouter,速度提高了近 40 倍。如果你需要极好的性能,使用 Gin 吧。

首先讲解一下Gin框架需要我们怎么做

第一步,引入项目的依赖

引入Gin框架

我们使用go mod来管理go的依赖

go get -u github.com/gin-gonic/gin

第二步,创建一个路由引擎

使用gin.New()或gin.Default()即可创建一个路由引擎

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

其中gin.Default()也适用gin.New()创建engine实例,但是会默认使用Logger和Recover中间件。下面是gin.Default的源码,

可以看到Gin默认开启了Logger和Recovery中间件

代码位置:github.com/gin-gonic/gin/gin.go

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

我们看到这里就足够了,只需要知道Default只不过是开启了一个自己的Logger和Recovery中间件

我们稍后会将中间件与路由放在一起进行理解

Gin 中间件

下面简单聊聊Gin的中间件是什么概念

首先我们先回顾之前我们干过的一个骚操作,我们在httprouter这里有一个利用Go语言闭包特性的设计,这里再次发一下代码

不难看出,这只不过是一个返回Handle类型的函数罢了,在函数的内部我们可以在调用前预处理一下或者做一个收尾工作

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

这个代码构成了一种链式调用,Gin设计了一种链式调用模式(实际上是数组)来方便给路由注册中间件,调用的时候遍历这个链进行执行,具体的逻辑等讲到Router后放在一起讲解

Gin中间件是什么我们知道了,现在需要他的具体类型HandlerFunc

Gin中间件的部署方式是使用gin.Use()函数

代码位置:github.com/gin-gonic/gin/gin.go

// HandlerFunc defines the handler used by gin middleware as return value.
// 中间件类型的定义(不是中间件的定义,中间件和路由是同一种类型,想不到吧)
type HandlerFunc func(*Context)


// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
   // 核心就是调用了RouterGroup的Use方法
   engine.RouterGroup.Use(middleware...)
   engine.rebuild404Handlers()
   engine.rebuild405Handlers()
   return engine
}

查看这个函数,我们了解到gin的中间件就是一个HandlerFunc数组,

HandlerFunc返回一个 func(*Context)类型的函数

Logger和Recovery分别提供了Logger和gonic的处理

Gin默认的两个middleware

这部分来自:www.panooo.com/GoLang_Gin_…

Gin Middleware - Logger

Logger 可以支持记录一个API请求发生时间,返回Status Code,Latency,远端IP,方法和URL Path,代码见[0]。 例如:

[GIN] 2019/11/02 - 14:15:38 | 500 |    1.068623ms |       127.0.0.1 | GET      /panic

Logger可以支持自定义formater输出,以及自定义文件输出,在加载Logger Middleware时,可以使用以下Config方式加载,指定formater和Log文件:

// LoggerWithConfig instance a Logger middleware with config.
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
...
}

// 可以看到通过自定义,Formatter和Output的方式定义格式和输出文件。
// LoggerConfig defines the config for Logger middleware.
type LoggerConfig struct {
	// Optional. Default value is gin.defaultLogFormatter
	Formatter LogFormatter

	// Output is a writer where logs are written.
	// Optional. Default value is gin.DefaultWriter.
	Output io.Writer

	// SkipPaths is a url path array which logs are not written.
	// Optional.
	SkipPaths []string
}

Logger Middleware可以用于ELK收集metrics,例如,如果有ELK系统处理日志,则可以将Logger中间件中的Metrics以特定格式存放至特定Metrics日志文件中,后期给ELK分析。

示例如下:

// 初始化Metrics日志
func InitMetrics(m Metric) error {
	if mlog == nil {
		mlog = logrus.New()
	}

	// 设置Rotate
	writer, err := rotatelogs.New(
		m.GetMetricPath()+".%Y%m%d%H%M",
		rotatelogs.WithLinkName(m.GetMetricPath()),
		rotatelogs.WithRotationTime(METRICSROTATETIME),
		rotatelogs.WithRotationCount(METRICSROTATECOUNT),
	)
	if err != nil {
		return err
	}

	// 设置logger的Writer
	mlog.SetOutput(writer)
	// 设置日志格式为JSON,方便ELK分析
	mlog.SetFormatter(&logrus.JSONFormatter{
		TimestampFormat:  "",
		DisableTimestamp: false,
		DataKey:          "",
		FieldMap:         nil,
		CallerPrettyfier: nil,
		PrettyPrint:      false,
	})

	return nil
}

// 自定义Metrics格式
type GinLogger struct {
	// StatusCode is HTTP response code.
	StatusCode int `json:"statuscode"`
	// Latency is how much time the server cost to process a certain request. (ms)
	Latency float64 `json:"latencyms"`
	// ClientIP equals Context's ClientIP method.
	ClientIP string `json:"clientip"`
	// Method is the HTTP method given to the request.
	Method string `json:"method"`
	// Path is a path the client requests.
	Path string `json:"path"`
	// BodySize is the size of the Response Body
	BodySize int `json:"bodysize"`

	// Common Fields
	// level
	Level string `json:"level"`
	// msg
	Msg string `json:"msg"`
	// time
	Time string `json:"time"`
	// Metrics Type
	MetricsType string `json:"metricstype"`
}

// 自定义给Gin的Formatter
var GinLoggerFormatter = func(param gin.LogFormatterParams) string {
  // 按需求定义哪些Metrics需要收集
	g := GinLogger{
		StatusCode:  param.StatusCode,
		Latency:     param.Latency.Seconds() * 1e3,
		ClientIP:    param.ClientIP,
		Method:      param.Method,
		Path:        param.Path,
		BodySize:    param.BodySize,
		Level:       "Info",
		Msg:         "RestRequest",
		Time:        time.Now().Format(time.RFC3339),
		MetricsType: TYPE_RESTHTTP_PERFORMANCE,
	}
	gjson, _ := json.Marshal(g)
	return string(gjson) + "\n"
}

// 设置给Gin Middleware的Output
func GetMetricsOutput() io.Writer {
	return mlog.Out
}

Gin Middleware – Recovery

Gin 支持自动从Panic中恢复的功能,这是因为在Recovery Middleware中对Panic做了recovery()动作。代码见[2]。

处理逻辑非常简单,核心是利用了defer在Recovery Middleware中调用了recovery动作。

// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer) HandlerFunc {
...
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
...
			}
		}()
		c.Next()
	}
}

以下为一个典型的Recovery Middleware在panic后的日志。

2019/11/02 14:15:38 [Recovery] 2019/11/02 - 14:15:38 panic recovered:
GET /panic HTTP/1.1
Host: 127.0.0.1:8082
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 47
Content-Type: application/json
Postman-Token: 19d3447d-1189-4105-9bec-0d376a5324b0
User-Agent: PostmanRuntime/7.18.0


Panic in Handler!
/Users/limao/go/src/HelloGo/HelloGin/TryGin.go:17 (0x1505868)
        main.func1: panic("Panic in Handler!")
/Users/limao/go/src/github.com/gin-gonic/gin/context.go:147 (0x14f071a)
        (*Context).Next: c.handlers[c.index](c)
/Users/limao/go/src/github.com/gin-gonic/gin/recovery.go:83 (0x1503f53)
        RecoveryWithWriter.func1: c.Next()
/Users/limao/go/src/github.com/gin-gonic/gin/context.go:147 (0x14f071a)
        (*Context).Next: c.handlers[c.index](c)
/Users/limao/go/src/github.com/gin-gonic/gin/logger.go:241 (0x1503080)
        LoggerWithConfig.func1: c.Next()
/Users/limao/go/src/github.com/gin-gonic/gin/context.go:147 (0x14f071a)
        (*Context).Next: c.handlers[c.index](c)
/Users/limao/go/src/github.com/gin-gonic/gin/gin.go:391 (0x14fa5a9)
        (*Engine).handleHTTPRequest: c.Next()
/Users/limao/go/src/github.com/gin-gonic/gin/gin.go:352 (0x14f9c9d)
        (*Engine).ServeHTTP: engine.handleHTTPRequest(c)
/usr/local/Cellar/go/1.13/libexec/src/net/http/server.go:2802 (0x12cad33)
        serverHandler.ServeHTTP: handler.ServeHTTP(rw, req)
/usr/local/Cellar/go/1.13/libexec/src/net/http/server.go:1890 (0x12c65d4)
        (*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req)
/usr/local/Cellar/go/1.13/libexec/src/runtime/asm_amd64.s:1357 (0x105bdd0)
        goexit: BYTE    $0x90   // NOP

[GIN] 2019/11/02 - 14:15:38 | 500 |    1.068623ms |       127.0.0.1 | GET      /panic

可以看到记载了Panic相关的上下文。

但是值得注意的是,虽然Gin仅能帮你Recover handler中的错误,不能recover Gorouting中的Panic,例如下例:

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

	r.GET("/panic", func(c *gin.Context) {
		// Gin无法恢复gorouting中的panic
		go func () {
			panic("Panic in Gorouting")
		}()
		// Gin可以恢复Handler中的panic
		//panic("Panic in Handler!")
		c.JSON(http.StatusOK, gin.H{"Response":"OK"})
	})

	r.Run(":8082")
}

这是因为GoLang中,任何gorouting中发生了panic,都会panic整个程序。每个gorouting需要自己处理panic

第三步,Gin Router配置

在httprouter中,仍然存在一个问题:路由分组问题,当路由达到一定数量,路由分组,统一一个前缀,将会是真香

Gin框架就提供了分组路由,或者说,Gin在设计时,就是分组的

我们使用gin.New()创建出来的引擎,本就是一个RouterGroup类型

type Engine struct {
   RouterGroup
   ...
}

一个RouterGroup可以创建一个子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.
// 参数为路径,HandlerFunc数组
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
   return &RouterGroup{
      // 
      Handlers: group.combineHandlers(handlers),
      basePath: group.calculateAbsolutePath(relativePath),
     // 看我看我
      engine:   group.engine,
   }
}
// 直接添加的路由将会直接粘贴过来
// 将自己的路由数组加上新来的数组,手动合并成为一个新的数组并返回
// 不会修改自身的状态
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
}


// demo
// relativePath是路径,后面的handler实质上是中间件
gin.Group(relativePath string, handlers ...HandlerFunc)
// 调用的例子
// 创建一个组,并将中间件插入当前组的 调用链条
g := gin.Group("/g",defaultHandler.ServeHTTP,helloHandler.ServeHTTP)

如何创建呢?查看上述代码,我们能发现

  • 子Group与父Group公用了一个engine
  • 子Group会获取父Group当前的Handler,结合参数拼装成为自己的Group,这个Handler其实就是调用链信息,这个特性使得我们可以在合适的组大小插入中间件(比如权限检查等)
  • 子Group会通过计算得到自己的绝对路径

上面“当前的”被用黑体加重,同时给出了代码,为什么?

创建子Group时,会调用拷贝当前路径,若在子Group创建后,父Group又添加了中间件进去,那么就没办法同步了!所以会看源码很重要!

要牢记combineHandlers的这个特性

上面一直在说中间件,那路由是怎么生成并注册的?

这里大🔥们还需要牢记一个要点,所有的路由都是挂在gin.Engine下的,只有使用RouterGroup.GET,POST等方法才能注册路由,这也是所有Group都会保存engine的原因

// 向engine添加路由的方法
group.engine.addRoute(httpMethod, absolutePath, handlers)

// demo
// 直接添加,不能使用公用前缀,只能共享中间件
g := gin.Group("/g",defaultHandler.ServeHTTP,helloHandler.ServeHTTP)
// 使用.GET等方法添加,会计算绝对请求路径,并添加给gin引擎
g.GET("/g",helloHandler.ServeHTTP)

// 源码路径

// 只有使用RouterGroup.GET,POST等方法才能注册路由
// DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodDelete, relativePath, handlers)
}

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
  // 一个装饰设计模式,不断拼凑前缀
	absolutePath := group.calculateAbsolutePath(relativePath)
  // 把路由组里面的handler(中间件)抽出来,与当前路由合并为一个新的调用链
	handlers = group.combineHandlers(handlers)
  // 注册路由,这里会把路由连同这个路由的调用链一起存在树形数据结构中
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

这里大家就能发现了,这是一个装饰设计模式,子类做一点处理,然后调用父类的同名方法

我们还会发现,注册的路由会再次添加进入

如果这之前没看清,一定要慢下心,观赏一下

那么我们就能发现一个细节,直接在创建时添加在Group的Handler并不会被注册在路由,只有使用Get,POST 等等方法才能注册一个路由到路由表

简单介绍路由

之前提到了很多次,gin调用路由时,会取出gin里面的调用链,然后遍历执行

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
   return group.handle(http.MethodGet, relativePath, handlers)
}
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

路由也是一个HanderFunc,

不同的是,

  • 注册路由通过GET,POST等方法
  • 路由注册会计算Hander的绝对地址
  • 路由注册会取出Group中的调用链,而不是塞进去
  • 路由会有一系列操作记录在内部的树型结构上

这里我们大致说一下Gin路由记录的方法

Gin的路由由一棵树构成,

树的根节点是以Http Method为key的数组(不是map,是数组

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

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

gin的树是按照路由节点去记录的,符合我们的使用逻辑

gin在注册路由时,会遍历树,找到自己的位置,然后将绝对路径,调用链一起记录进去

比如:

Go Web开发入门指南<前半> 注意这里的调用链,是从Group中生成一个备份出来,所以

如果代码是下面的情况,

engine.Use(A)
engine.Get("xx",R)
engine.Use(B)

这种情况下,B将不会在R的调用链范围内

第四步,Gin启动

我们使用Engine.Run()方法启动服务器即可

我们的代码,将升级为下面的样子

package main

import (
   "fmt"
   "github.com/gin-gonic/gin"
   "go-web-tutorial/handler"
)


func main() {
   fmt.Println("Starting the server ...")
   defaultHandler := handler.NewDefaultHandler()
   helloHandler := handler.NewHelloHandler()
   // gin.Default()也适用gin.New()创建engine实例,但是会默认使用Logger和Recover中间件。
   engine := gin.Default()
   engine.GET("/",defaultHandler.ServeHTTP)
   engine.GET("/hello",helloHandler.ServeHTTP)
   // 默认运行在8080
   engine.Run()
}

若要指定服务运行地址

使用engine.Run(":8081")

使用engine.Run()时,若参数为空,则会使用8080

这里的地址是支持多个地址的,也就是说gin可以同时监听多个地址的请求

第五步,Gin 参数解析,绑定

路由都是接收context实例的函数

func(context *gin.Context) {
   log.Printf("a接口")
})

所有请求相关的信息都会从context中取出

context中大致有四类方法

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

请求信息

// 返回主Handler,即接收请求的handler的名字
func (c *Context) HandlerName() string 
// 返回调用链
func (c *Context) HandlerNames() []string 
// 返回Handler
func (c *Context) Handler() HandlerFunc

// 返回匹配路径的全路径. 404返回空字符串
//     router.GET("/user/:id", func(c *gin.Context) {
//         c.FullPath() == "/user/:id" // true
//     })
func (c *Context) FullPath() string 

调用链控制

// 只能在中间件里面用
// 调用调用链的下一个
func (c *Context) Next() 
// IsAborted 返回当前context是否被终止
func (c *Context) IsAborted() bool
// Abort阻止调用挂起的处理程序。注意,这不会停止当前的处理程序。
// 假设你有一个授权中间件来验证当前请求是否被授权。
// 如果授权失败(例如:密码不匹配),调用Abort以确保此请求的其余处理程序不被调用。
func (c *Context) Abort()
func (c *Context) AbortWithStatus(code int)
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{})
func (c *Context) AbortWithError(code int, err error) *Error

错误管理

// Error将错误附加到当前上下文。将错误推入错误列表。
// 在解决请求的过程中,每个错误都调用Error是一个好主意。
// 一个中间件可以用来收集所有的错误,并把它们一起推到一个数据库,打印日志,或附加在HTTP响应中。
// 如果err为nil,则Error将panic。
func (c *Context) Error(err error) *Error

上下文资源

不再多赘述,就是get,set

Go Web开发入门指南<前半>

取参数

// Param 返回URL上的参数,如/:id
// It is a shortcut for c.Params.ByName(key)
//     router.GET("/user/:id", func(c *gin.Context) {
//         // a GET request to /user/john
//         id := c.Param("id") // id == "john"
//     })
func (c *Context) Param(key string) string 

// Query 如果查询参数存在,返回,否则返回空字符串""
// It is shortcut for `c.Request.URL.Query().Get(key)`
//     GET /path?id=1234&name=Manu&value=
// 	   c.Query("id") == "1234"
// 	   c.Query("name") == "Manu"
// 	   c.Query("value") == ""
// 	   c.Query("wtf") == ""
func (c *Context) Query(key string) string

// DefaultQuery 如果查询参数存在,返回参数,否则返回指定的默认值
// See: Query() and GetQuery() for further information.
//     GET /?name=Manu&lastname=
//     c.DefaultQuery("name", "unknown") == "Manu"
//     c.DefaultQuery("id", "none") == "none"
//     c.DefaultQuery("lastname", "none") == ""
func (c *Context) DefaultQuery(key, defaultValue string) string

// GetQuery is like Query(), 
// 若存在该参数,bool为true,否则为false,若存在但没有值,返回""
// It is shortcut for `c.Request.URL.Query().Get(key)`
//     GET /?name=Manu&lastname=
//     ("Manu", true) == c.GetQuery("name")
//     ("", false) == c.GetQuery("id")
//     ("", true) == c.GetQuery("lastname")
func (c *Context) GetQuery(key string) (string, bool) 

// QueryArray 返回字符串数组
// The length of the slice depends on the number of params with the given key.
func (c *Context) QueryArray(key string) []string

// GetQueryArray 返回字符串数组,bool表示是否存在
func (c *Context) GetQueryArray(key string) ([]string, bool) 

// QueryMap 返回map
func (c *Context) QueryMap(key string) map[string]string

// GetQueryMap 返回map,bool表示是否存在
func (c *Context) GetQueryMap(key string) (map[string]string, bool)

// PostForm 从 POST urlencoded form 或 multipart form 返回值,不存在返回空字符串
func (c *Context) PostForm(key string) string

// DefaultPostForm 多了默认值的设定
// See: PostForm() and GetPostForm() for further information.
func (c *Context) DefaultPostForm(key, defaultValue string) string

// GetPostForm 多了bool用来判断有没有这个参数
// For example, during a PATCH request to update the user's email:
//     email=mail@example.com  -->  ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com"
// 	   email=                  -->  ("", true) := GetPostForm("email") // set email to ""
//                             -->  ("", false) := GetPostForm("email") // do nothing with email
func (c *Context) GetPostForm(key string) (string, bool)

// PostFormArray 字符串数组
// The length of the slice depends on the number of params with the given key.
func (c *Context) PostFormArray(key string) []string 

// GetPostFormArray 多了是否存在
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) 

// PostFormMap 返回map
func (c *Context) PostFormMap(key string) map[string]string

// GetPostFormMap 多返回一个是否存在
func (c *Context) GetPostFormMap(key string) (map[string]string, bool)

// FormFile 返回第一个文件
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) 

// MultipartForm is the parsed multipart form, including file uploads.
func (c *Context) MultipartForm() (*multipart.Form, error)

// SaveUploadedFile 直接把文件保存到一个位置
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error 

bind操作(建议看看源码,很清晰)

// Bind操作会根据请求的情况使用默认的方法
// "Content-Type"
func (c *Context) Bind(obj interface{}) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.MustBindWith(obj, b)
}
// 下面是默认逻辑,代码来表达更清晰
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding {
  // Get请求会把路径上的参数param绑定到实例中
	if method == http.MethodGet {
		return Form
	}

	switch contentType {
	case MIMEJSON:
		return JSON
	case MIMEXML, MIMEXML2:
		return XML
	case MIMEPROTOBUF:
		return ProtoBuf
	case MIMEMSGPACK, MIMEMSGPACK2:
		return MsgPack
	case MIMEYAML:
		return YAML
	case MIMEMultipartPOSTForm:
		return FormMultipart
	default: // case MIMEPOSTForm:
		return Form
	}
}

// Content-Type MIME of the most common data formats.
const (
	MIMEJSON              = "application/json"
	MIMEHTML              = "text/html"
	MIMEXML               = "application/xml"
	MIMEXML2              = "text/xml"
	MIMEPlain             = "text/plain"
	MIMEPOSTForm          = "application/x-www-form-urlencoded"
	MIMEMultipartPOSTForm = "multipart/form-data"
	MIMEPROTOBUF          = "application/x-protobuf"
	MIMEMSGPACK           = "application/x-msgpack"
	MIMEMSGPACK2          = "application/msgpack"
	MIMEYAML              = "application/x-yaml"
)

// ShouldBind检查Content-Type来自动选择绑定引擎,
// 根据"Content-Type" header 不同 bindings会使用下面的binding:
//     "application/json" --> JSON binding
//     "application/xml"  --> XML binding
// 否则 --> 返回异常
// 将信息解析到作为指针传人的实例中
// 与Bind()相似,但是解析错误不会终止并写入400到Header
func (c *Context) ShouldBind(obj interface{}) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.ShouldBindWith(obj, b)
}

如果你对bind的来源有指定(json,form等等)

使用相关项目结尾的方法

Go Web开发入门指南<前半>

Bind和ShouldBind的区别在于,Bind如果解析错误将在Header中写入400,ShouldBind错误不会写入400

Bind的Json底层使用了go的json

几个标签

  1. binding:"required"
  2. binding:"-" 不绑定
  3. json:"fieldname"
  4. form:"formname"
  5. xml:"user"

下一篇: Go Web开发扩展项-其他配套框架

转载自:https://juejin.cn/post/6961035838234820622
评论
请登录