Gin 框架(http server)
使用方法
-
路由
- Gin的路由是单一的,不能有重复
- 比如注册了
/users/name
,那么就不能再注册匹配/users/:id
模式的路由
- 比如注册了
- 如果注册了
"hello"
为path,那么访问"hello/"
会自动发送301重定向,而不是404- 这是因为gin自动开启了
gin.RedirectTrailingSlash = true
的配置
- 这是因为gin自动开启了
- 路由组
v1 := router.Group("/v1")
- Gin的路由是单一的,不能有重复
-
handler格式
func(c *gin.Context)
- 把request和response都封装到
gin.Context
的上下文环境
-
获取参数
val := c.Query(<name>)
- 不存参数时返回空值
val, ok := c.GetQuery(<name>)
- 可以告诉我们要获取的
key
是否存在
- 可以告诉我们要获取的
val := c.DefaultQuery(<name>, <default value>)
- 获取失败则返回默认值
c.[Should]BindQuery(&<struct>)
- 绑定URL参数到结构体
- 需要结构体的tag是
form
而不是json
c.QueryArray(<key>)
支持同时获取key
值都一样,但是对应的value
不一样的URL参数- 例如:
?media=blog&media=wechat
- 例如:
c.QueryMap(<key>)
支持map参数- 例如:
?ids[a]=123&ids[b]=456&ids[c]=789
- 例如:
-
获取restful风格参数
c.Param(<name>)
获取路由参数:
精确匹配,只匹配一个参数"/user/:name"
将匹配/user/john
但不会匹配/user/
,/user
*
全部匹配"/user/:name/*action"
将匹配/user/john/
,/user/John/love/tom
和/user/john/send
c.Param("action")
分别得到/
,/love/tom
和/send
- 不建议经常使用,因为匹配的太多,这导致搞不清楚哪些路由被注册了
c.ShouldBindUri(&<struct>)
绑定URI- 字段tag:
uri:"<name>" binding:"required"
- 字段tag:
- 不支持路由正则表达式
-
绑定body到结构体
-
Must bind
- 如果发生绑定错误,则请求终止,响应状态码被设置为 400 并且
Content-Type
被设置为text/plain
- 包括:
Bind
,BindJSON
,BindXML
,BindYAML
- 如果发生绑定错误,则请求终止,响应状态码被设置为 400 并且
-
Should bind
- 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求
- 使用了
go-playground/validator.v8
进行绑定验证 - 可以指定必须绑定的字段
- 如果一个字段的 tag 加上了
binding:"required"
,但绑定时是空值, Gin 会报错
- 如果一个字段的 tag 加上了
- 包括:
ShouldBind
,ShouldBindJSON
,ShouldBindXML
,ShouldBindYAML
-
被绑定的结构体应该传入指针形式
-
只能被绑定一次,不可重用
- 要想多次绑定,可以使用
c.ShouldBindBodyWith
- 会在绑定之前将 body 存储到上下文中
- 多用于中间件会先获取一次的情况
- 要想多次绑定,可以使用
-
-
获取表单
c.PostForm(<name>)
c.DefaultPostForm(<name>, <default value>)
-
上传文件
c.FormFile(<name>)
单文件form, _ := c.MultipartForm(); files := form.File[<name>]
多文件
-
返回json
c.JSON(<code>, gin.H{<key>: <val>})
gin.H
实际就是map[string]interface{}
- 可以直接使用的结构体返回:
c.JSON(<code>, <struct>)
c.IndentedJSON
格式化json输出AbortWithStatusJSON(<code>, gin.H{<key>: <val>})
终止调用链并返回- 跳过未执行的中间件
- 终止执行后面的中间件操作,即
c.Next()
后的操作
- 除了json类型,返回值还支持yaml, xml, protobuf类型
-
使用中间件
- 注意要在函数名后加
()
r := gin.Default()
启动默认含有Logger 和 Recovery 中间件r := gin.New()
不使用默认中间件
r.Use(<middleware>())
注册中间件- 注册中间件前设置的路由,将不会受注册的中间件所影响
- 使用花括号包含被装饰的路由函数只是一个代码规范,即使没有被包含在内的路由函数,只要使用r进行路由,都被装饰了
r.Group(<path>,<middleware>()… )
路由组使用中间件r.GET(<path>, <middleware>()…, <handler>)
单路由使用中间件
- 注意要在函数名后加
-
自定义中间件
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// 请求前操作
c.Next()
// 请求后操作
}
}
-
静态文件或目录
router.StaticFile(<path>, <file name>)
单个文件router.Static("/<path>", <dir>)
- 通过路径
/<path>/<filename>
获取<dir>/<filename>
文件
- 通过路径
-
重定向
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
重定向到外部URL- 内部重定向:
r.GET("/test", func(c *gin.Context) { c.Request.URL.Path = "/test2" r.HandleContext(c) })
-
cookie
- 获得cookie:
c.Cookie(<name>)
- 设置cookie:
c.SetCookie(name, value string, maxAge int, path, domain string, secure, isHTTPOnly)
- 获得cookie:
-
在 handler 中启动新的 goroutine 时,不能使用原始的
gin.Context
,应该使用副本- 通过
c.Copy()
复制- 拷贝之后,
ResponseWriter
其实是一个空的对象,所以说,即使拷贝了,也要在主Context中才能返回响应结果
- 拷贝之后,
- 因为为了避免频繁GC,
gin.Context
使用对象池管理,会被复用
- 通过
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
func (c *Context) Copy() *Context {
var cp = *c
cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem
cp.index = abortIndex
cp.handlers = nil
cp.Keys = map[string]interface{}{}
for k, v := range c.Keys {
cp.Keys[k] = v
}
return &cp
}
-
通过
c.Set
和c.Get
传递context- 多用于中间件内set, 然后在handler中get
-
设置日志格式
- 需要在启动router前设定
// 禁用控制台颜色,将日志写入文件时不需要控制台颜色。
gin.DisableConsoleColor()
// 记录到文件。
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// 同时将日志写入文件和控制台
gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
- 自定义HTTP配置
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
源码阅读
参考
resty框架(http client)
import "github.com/go-resty/resty/v2"
var client *resty.Client
fun init() {
client = resty.New().
SetTimeout(1 * time.Minute).
SetRetryCount(3).
SetRetryWaitTime(5 * time.Second). // 超时后等待5秒才重试
SetRetryMaxWaitTime(20 * time.Second).
SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {
ctx := r.Request.Context()
if err != nil {
logs.CtxWarn(ctx, "query retry on error: %v", err)
return true
}
if r.StatusCode() >= http.StatusInternalServerError {
logs.CtxWarn(ctx, "query retry on status code: %d", r.StatusCode())
return true
}
return false
}).
SetHeaders(map[string]string{
"Content-Type": "application/json",
"User-Agent": "My custom User Agent String",
}).
SetProxy("http://proxyserver:8888").
SetBasicAuth("myuser", "mypass").
OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
ctx := r.Request.Context()
isGzip := r.Header().Get("Content-Encoding") == "gzip"
logs.CtxInfoKvs(ctx, "url", r.Request.URL, "latency", r.Time(), "size", r.Size(), "isGzip", isGzip)
return nil
})
}
func main() {
resp, err := client.R().
SetPathParams(map[string]string{
"id": id,
}).
SetQueryParams(map[string]string{
"name": "jack",
"gender": "male",
}).
Get(url)
fmt.Println("Error :", err)
fmt.Println("Status Code:", resp.StatusCode())
fmt.Println("Request URL:", resp.Request.URL)
_ = json.Unmarshal(resp.Body(), &res)
ti := resp.Request.TraceInfo()
fmt.Println("DNSLookup :", ti.DNSLookup)
fmt.Println("ConnTime :", ti.ConnTime)
fmt.Println("TotalTime :", ti.TotalTime)
}
http
标准库
server
handler
-
如果使用HTTPS模式启动服务器,那么默认使用HTTP/2
-
如果一个类型实现了
ServeHTTP(http.ResponseWriter, *http.Request)
方法,那么它就是处理器- 多路复用器ServeMux也实现了上面的方法
-
因为创建一个多路复用器唯一需要的就是实现
ServeHTTP
放方法,所以我们可以自己实现第三方多路复用器代替默认的http.ServeMux
http.ServeMux
对于/article/123
这类URL难以解析,而httprouter
由于采用了radix树,所以更加灵活和快速
-
http.ServeMux
在解析URL时,如果URL处理器不是以/
结尾,则需要精确匹配才会调用对应的handler- 如果存在
/
和/hello
两个处理器对应的URL,当我们传入/hello/there
时,调用的是/
处理器 - 把
/hello
处理器改为/hello/
处理器则上述例子调用/hello/
处理器 - 原理
- 末尾的
/
的表示一个子树,可以匹配任务它的子路径- 同时匹配到多个时,采用最长匹配原则
- 末尾不是
/
的表示一个叶子,匹配固定路径
- 末尾的
- 如果存在
-
如果一个处理器需要外部依赖,应该通过闭包传入参数,而不是使用全局变量
func(s * server)handleGreeting(format string)http.HandlerFunc {
return func(w http.ResponseWriter,r * http.Request){
fmt.Fprintf(w,format,"World")
}
}
mux.HandleFunc("/greeting", s.handleGreeting("Hello %s"))
client
request
- 添加URL参数
params := make(url.Values)
params.Add("key1", "value1")
params.Add("key2", "value2")
req, _ := http.NewRequest(http.MethodGet, "http://httpbin.org/get", nil)
req.URL.RawQuery = params.Encode()
fmt.Println(req.URL) // 输出:http://httpbin.org/get?key1=value1&key2=value2
- 设置请求代理
proxyUrl, err := url.Parse("http://127.0.0.1:8087")
if err != nil {
panic(err)
}
t := &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := http.Client{ // 自定义client替换defaultClient
Transport: t,
Timeout: time.Duration(10 * time.Second),
}
// 使用
res, err := client.Get("https://google.com")
response
- 如果没有在首部设置响应内容的类型,那么会通过前512字节自动检测
w.WriteHeader(<code>)
可以定义响应码,默认为200w.Header().Set()
可以设定响应首部- 必须在WriteHeader前设定
form
r.Form
是一个键值对map(map[string][]string
),其中值包括URL中的和body表单中的- 需要首先执行
r.ParseForm()
解析 r.PostForm
结构的值只会包含body表单中的- 只支持
aplication/x-www-form-urlencoded
编码
- 只支持
- 如果body表单类型为
multipart/form-data
,则r.Form
的值只有URL中的
- 需要首先执行
- 为了获取
multipart/form-data
编码的表单数据,需要使用r.ParseMultipartForm(<len>)
解析,并从r.MultipartForm
结构获取- 只包含表单数据,不包含URL键值对
- 还可以记录上传的文件
- 通过
r.FormFile(<name>)
可以更方便的解析
- 通过
FromValue(<key>)
或者PostFromValue(<key>)
方法会自动调用ParseForm
方法或者ParseMultipartForm
,不需要用户手动解析- 该方法只会取出给定键的第一个值
- 想要获得所有值需要直接访问Form结构
transport
- Transport字段代表向网络服务发送 HTTP 请求,并从网络服务接收 HTTP 响应的操作过程
- 该字段的方法RoundTrip应该实现单次 HTTP 交互需要的所有步骤
- 该字段的方法RoundTrip应该实现单次 HTTP 交互需要的所有步骤
net
标准库
conn
dialer
TODO
resolver
TODO