# blog
**Repository Path**: guanzhanyi/blog
## Basic Information
- **Project Name**: blog
- **Description**: 基于go-kit的微服务demo
- **Primary Language**: Go
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: https://gitee.com/guanzhanyi
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2021-06-08
- **Last Updated**: 2024-08-09
## Categories & Tags
**Categories**: Uncategorized
**Tags**: Go语言, 微服务, go-kit
## README
# go-kit微服务实践
## 项目描述
### 
## keypoints
* [HTTP服务——go-kit](#go-kit)
* [微服务调用——grpc](#grpc)
* [身份认证——JWT](#JWT)
* [限流——golang/rate](#rate)
* [服务发现和注册——consul](#consul)
* [服务熔断和降级——hystrix-go](#hystrix)
* [配置文件管理——go-ini/ini](#ini)
* [表单验证——validation](#validation)
* [注释自动生成——Swagger](#Swagger)
* [链路追踪——Jaeger](#Jaeger)
* [X][网关管理]
go-kit
* 分为三层: 
    service: 定义业务逻辑
    endpoint: 调用service处理请求和返回响应
    transport: 连接endpoint的request和response
grpc
* 安装
    编译安装protobuf
    安装go-proto `go get -u github.com/golang/protobuf/proto`
    安装protoc `go get -u github.com/golang/protobuf/protoc-gen-go`
* 使用
    编写protobuf文件
    ```
    syntax = "proto3";
    //
    package pb;
    //生成的go文件路径
    option go_package = "./";
    //函数
    service UserService{
        rpc CheckPassword(LoginRequest) returns (LoginResponse){}
    }
    //结构体
    message LoginRequest{
        string Username = 1;
        string Passowrd = 2;
    }
    ```
    生成pb.go文件
    `$ protoc --go_out=plugins=grpc:. *.proto`
JWT
```
//生成token
//初始化与token有关的数据结构: 主要有jwt库提供的标准声明和自定义的数据. 标准声明有过期时间, 创建时间,发行商和主题. 自定义的数据一般有鲜明且唯一的用户特征, 比如用户名
type Claims struct{
	Username string `json:"username"`
	jwt.StandardClaims
}
claims:=Claims{
		username,
		jwt.StandardClaims{
			ExpiresAt: expireTime.Unix(),//过期时间
			IssuedAt: time.Now().Unix(),
			Issuer: "gyp",//发行商
			Subject: "user token",//主题
		},
	}
//生成加密后的token
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256,claims)
//加上签名, 认证数据来源
token,err:=tokenClaims.SignedString(jwtSecret)
```
```
//验证token
//把token解密
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
    return jwtSecret, nil
})
//验证解密后的token是否是Claims结构体
if tokenClaims != nil {
    if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
        return claims, nil
    }
}
```
使用了中间件的方式嵌入到gin中, JWT()函数返回gin.HandlerFunc, 在路由中通过Use使用
```
func JWT() gin.HandlerFunc{
	//gin.Context是gin 中最重要的部分, 用于传递变量, 管理流程, 验证json
	return func(c *gin.Context){
        //调用generateToken()和ParseToken()
        ...
    }
}
apiv1:=r.Group("/api/v1")
apiv1.Use(jwt.JWT())
```
rate
```
r := rate.NewLimiter(1, 5) //1表示每次放进筒内的数量,桶内的令牌数是5,最大令牌数也是5,这个筒子是自动补充的,你只要取了令牌不管你取多少个,这里都会在每次取完后自动加1个进来,因为我们设置的是1
ctx := context.Background()
r.waitN(ctx,2) //每次消耗两个
wait()
Allow()
r.AllowN(time.Now(), 2) { //AllowN表示取当前的时间,这里是一次取2个,如果当前不够取两个了,本次就不取,再放一个进去,然后返回false
//Allow 无可用token则返回false
//Wait无可用token会阻塞住,直到获取一个token,或者超时或取消
Reserve()
ReserveN()
```
consul
```
//安装consul
$ docker pull consul
```
```
// 启动consul
// -server表示以服务端的方式启动
// -boostrap 指定自己为leader, 而不需要选举
// -ui 启动内置管理web页面
// -client指定客户端的ip, 0.0.0.0表示本地的都可以访问
// 8500是后台UI端口
// -node 节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名
// -bootstrap-expect 在一个datacenter中期望提供的server节点数目,当该值提供的时候,consul一直等到达到指定sever数目的时候才会引导整个集群,该标记不能和bootstrap共用
$ docker run \
    -d \
    -p 8500:8500 \
    -p 8600:8600/udp \
    --name=badger \
    consul agent -server -ui -node=server1 -bootstrap-expect=1 -client=0.0.0.0
```
//注册可以手动通过json注册, 也可以通过函数注册
//健康检测机制是通过定时发送报文并且得到响应完成的
```
//手动注册服务
$ vim imagecode.json
{
	"ID":"imagecode",
	"Name":"imagecode",
	"Tags":{
		"iHome"
	},
	"Address":"106.52.19.86",
	"Port":8081,
	"Check":{
		"HTTP":"http://106.52.19.86/api/v1/imagecode-health",
		"InterVal":"5s"
	}
}
```
```
//导入服务
$ curl --request PUT --data @imagecode.json localhost:8500/v1/agent/service/register
//反注册服务
$ curl --request PUT --data @imagecode.json loservice/deregister/imagecode
```
```
通过函数导入和注册服务
package consul
import (
    consulapi "github.com/hashicorp/consul/api"
    "log"
)
func RegService() {
    config := consulapi.DefaultConfig()
    config.Address = "39.107.76.100:8500"
    reg := consulapi.AgentServiceRegistration{}
    reg.Name = "userservice" //注册service的名字
    reg.Address = "39.107.76.100" //注册service的ip
    reg.Port = 8080//注册service的端口
    reg.Tags = []string{"primary"}
    check := consulapi.AgentServiceCheck{} //创建consul的检查器
    check.Interval="5s" //设置consul心跳检查时间间隔
    check.HTTP = "http://39.107.76.100:8080/health" //设置检查使用的url
    reg.Check = &check
    client, err := consulapi.NewClient(config) //创建客户端
    if err != nil {
        log.Fatal(err)
    }
    err = client.Agent().ServiceRegister(®)
    if err != nil {
        log.Fatal(err)
    }
}
```
```
//优雅退出consul
```
* 结果


hystrix
服务熔断处理
`Do`和`Go`类似, 不过Go是异步方式, Do是同步方式
Do有三个参数: Command 处理正常逻辑的函数 处理异常逻辑的函数
通过ConfigureCommand函数配置Command的参数
```
hystrix.ConfigureCommand("wuqq", hystrix.CommandConfig{
    Timeout:                int(3 * time.Second),//超时时间
    MaxConcurrentRequests:  10,                 //最大并发量
    SleepWindow:            5000,               //当熔断器被打开后,SleepWindow 的时间就是控制过多久后去尝试服务是否可用了
    RequestVolumeThreshold: 10,                 //一个统计窗口10秒内请求数量。达到这个请求数量后才去判断是否要开启熔断
    ErrorPercentThreshold:  30,                 //错误百分比,请求数量大于等于RequestVolumeThreshold并且错误率到达这个百分比后就会启动熔断
})
_ = hystrix.Do("wuqq", func() error {
    // talk to other services
    _, err := http.Get("https://www.baidu.com/")
    if err != nil {
        fmt.Println("get error:%v",err)
        return err        }
    return nil
}, func(err error) error {
    fmt.Printf("handle  error:%v\n", err)
    return nil
})
```
ini
`"github.com/go-ini/ini"`
//建一个*.ini文件
//文件格式由的领域[]
RUN_MODE = debug
[app]
PAGE_SIZE = 10
JWT_SECRET = 23347$040412
[server]
HTTP_PORT = 8000
READ_TIMEOUT = 60
//通过ini.Load()初始化
//通过GetSection()获取领域
//通过Key()获取值
//通过Mustxx()判断值类型和匹配
validation
```
github.com/astaxie/beego/validation
valid := validation.Validation{}
valid.Required(name,"name").Message("名字不能为空")
valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
valid.Required(createdBy, "created_by").Message("创建人不能为空")
valid.MaxSize(createdBy, 100, "created_by").Message("创建人最长为100字符")
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
valid.HasErrors() 
```
Jaeger
tracer
span
不太明白链路追踪中client的作用
Swagger
* 安装swagger
    ```
    go get -u github.com/swaggo/swag/cmd/swag@v1.6.5
    ```
* 安装 gin-swagger
    ```
    $ go get -u github.com/swaggo/gin-swagger@v1.2.0 
    $ go get -u github.com/swaggo/files
    $ go get -u github.com/alecthomas/template
    ```
* 编写 API 注释
    ```
    // @Summary 新增文章标签
    // @Produce  json
    // @Param name query string true "Name"
    // @Param state query int false "State"
    // @Param created_by query int false "CreatedBy"
    // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
    // @Router /api/v1/tags [post]
    func AddTag(c *gin.Context) {
    ```
* 针对 swagger 新增初始化动作和对应的路由规则
    ```
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    ```
* 生成swag文件
    ```
    //进入项目根目录
    > swag init
    ```
* 访问网页
    ```
    http://127.0.0.1:8000/swagger/index.html
    ```
* 结果
    
## 理解GO CONTEXT机制
context被译为上下文, 一般理解为程序单元的一个运行状态、现场、快照
每个Goroutine在执行之前,都要先知道程序当前的执行状态,通常将这些执行状态封装在一个Context变量中,传递给要执行的Goroutine中。
在网络编程下,当接收到一个网络请求Request,处理Request时,我们可能需要开启不同的Goroutine来获取数据与逻辑处理,即一个请求Request,会在多个Goroutine中处理。而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束。
```
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
```
Deadline会返回一个超时时间,Goroutine获得了超时时间后,例如可以对某些io操作设定超时时间。
Done方法返回一个信道(channel),当Context被撤销或过期时,该信道是关闭的,即它是一个表示Context是否已关闭的信号。
当Done信道关闭后,Err方法表明Context被撤的原因。
Value可以让Goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。
### 使用
要创建Context树,第一步就是要得到根节点,context.Background函数的返回值就是根节点:
```
func Background() Context
```
该函数返回空的Context,该Context一般由接收请求的第一个Goroutine创建,是与进入请求对应的Context根节点,它不能被取消、没有值、也没有过期时间。它常常作为处理Request的顶层context存在。
有了根节点,又该怎么创建其它的子节点,孙节点呢?context包为我们提供了多个函数来创建他们:
```
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context
```
函数都接收一个Context类型的参数parent,并返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收参数设定子节点的一些状态值,接着就可以将子节点传递给下层的Goroutine了。
### 总结
context包通过构建树型关系的Context,来达到上一层Goroutine能对传递给下一层Goroutine的控制。对于处理一个Request请求操作,需要采用context来层层控制Goroutine,以及传递一些变量来共享。
Context对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个Context变量(它为Context树结构的根);在请求处理结束后,撤销此ctx变量,释放资源。
每次创建一个Goroutine,要么将原有的Context传递给Goroutine,要么创建一个子Context并传递给Goroutine。
Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。
当通过父Context对象创建子Context对象时,可同时获得子Context的一个撤销函数,这样父Context对象的创建环境就获得了对子Context将要被传递到的Goroutine的撤销权。