golang web编程

Gin 框架(http server)

使用方法

  • 路由

    • Gin的路由是单一的,不能有重复
      • 比如注册了/users/name,那么就不能再注册匹配/users/:id模式的路由
    • 如果注册了"hello"为path,那么访问"hello/"会自动发送301重定向,而不是404
      • 这是因为gin自动开启了gin.RedirectTrailingSlash = true的配置
    • 路由组
      • v1 := router.Group("/v1")
  • 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"
    • 不支持路由正则表达式
  • 绑定body到结构体

    • Must bind

      • 如果发生绑定错误,则请求终止,响应状态码被设置为 400 并且 Content-Type 被设置为 text/plain
      • 包括: Bind, BindJSON, BindXML,BindYAML
    • Should bind

      • 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求
      • 使用了 go-playground/validator.v8 进行绑定验证
      • 可以指定必须绑定的字段
        • 如果一个字段的 tag 加上了 binding:"required",但绑定时是空值, Gin 会报错
      • 包括: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()启动默认含有LoggerRecovery 中间件
      • 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)
  • 在 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.Setc.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>)可以定义响应码,默认为200
  • w.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 交互需要的所有步骤
      在这里插入图片描述

在这里插入图片描述

net标准库

conn

TODO

dialer

TODO

resolver

TODO

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值