Go Web开发入门指南<前半>
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的值,但是这个后端太简单了,
- 路由拦截下来后才能得知请求方法
- 我们需要手动处理io,
- 读取io后body需要手动映射到实例中,
- 链接上的参数需要我们手动使用正则取下来
- 路由分组问题,比如 /test/t /test/t2
- 返回值需要我们手动处理为[]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)
其中,首个参数是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"))
}
目前我们解决的问题:
- 请求方法问题,路由参数问题(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
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在注册路由时,会遍历树,找到自己的位置,然后将绝对路径,调用链一起记录进去
比如:
注意这里的调用链,是从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
取参数
// 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等等)
使用相关项目结尾的方法
Bind和ShouldBind的区别在于,Bind如果解析错误将在Header中写入400,ShouldBind错误不会写入400
Bind的Json底层使用了go的json
几个标签
- binding:"required"
- binding:"-" 不绑定
- json:"fieldname"
- form:"formname"
- xml:"user"
下一篇: Go Web开发扩展项-其他配套框架
转载自:https://juejin.cn/post/6961035838234820622