【Go】微服务框架总结
Go-Mirco框架go-zero微服务框架1. zRPC使用参考go-zero 文档查看server注册ETCDCTL_API=3 etcdctl get hello.rpc --prefix显示服务已经注册2. 接口限流 periodlimit2.1 滑动窗口限流接口限流实现 redis + lua而在一个分布式系统中,存在多个微服务提供服务。所以当瞬间的流量同时访问同一个资源,如何让计数器在
一、grpc 远程调用。
1. grpc gateway
- 服务端 grpc
- 初始一个 grpc 对象
- 注册服务
- 设置监听, 指定 IP、port
- 启动服务。---- serve()
- 客户端 grpc
- 连接 grpc 服务
- 给 grpc.Dial() 传参2 : grpc.WithInsecure() . 表示:以安全的方式操作。
- 初始化 grpc 客户端
- 调用远程服务。
- 给 grpc.SayHello() 传参1: context.TODO() 表示:空对象。
- 连接 grpc 服务
server.go
package main
import (
"google.golang.org/grpc"
"day01/pb"
"context" // 上下文. --- goroutine (go程) 之间用来进行数据传递 API 包
"net"
"fmt"
)
// 定义类
type Children struct {
}
// 按接口绑定类方法
func (this *Children) SayHello(ctx context.Context, t *pb.Teacher) (*pb.Teacher, error) {
t.Name += " is Sleeping"
return t, nil
}
func main() {
//1. 初始一个 grpc 对象
grpcServer := grpc.NewServer()
//2. 注册服务
pb.RegisterSayNameServer(grpcServer, new(Children))
//3. 设置监听, 指定 IP、port
listener, err := net.Listen("tcp", "127.0.0.1:8800")
if err != nil {
fmt.Println("Listen err:", err)
return
}
defer listener.Close()
//4. 启动服务。---- serve()
grpcServer.Serve(listener)
}
client.go
package main
import (
"google.golang.org/grpc"
"fmt"
"day01/pb"
"context"
)
func main() {
//1. 连接 grpc 服务
grpcConn, err := grpc.Dial("127.0.0.1:8800", grpc.WithInsecure())
if err != nil {
fmt.Println("grpc.Dial err:", err)
return
}
defer grpcConn.Close()
//2. 初始化 grpc 客户端
grpcClient := pb.NewSayNameClient(grpcConn)
// 创建并初始化Teacher 对象
var teacher pb.Teacher
teacher.Name = "itcast"
teacher.Age = 18
//3. 调用远程服务。
t, err := grpcClient.SayHello(context.TODO(), &teacher)
fmt.Println(t, err)
}
2. 服务发现的种类:
- consul: 常应用于 go-micro 中。
- mdns:go-micro中默认自带的服务发现。
- etcd:k8s 内嵌的服务发现
- zookeeper:java中较常用。
consul 常用命令
-
consul agent
-
-bind=0.0.0.0 指定 consul所在机器的 IP地址。 默认值:0.0.0.0
-
-http-port=8500 consul 自带一个web访问的默认端口:8500
-
-client=127.0.0.1 表明哪些机器可以访问consul 。 默认本机。0.0.0.0 所有机器均可访问。
-
-config-dir=foo 所有主动注册服务的 描述信息
-
-data-dir=path 储存所有注册过来的srv机器的详细信息。
-
-dev 开发者模式,直接以默认配置启动 consul
-
-node=hostname 服务发现的名字。
-
-rejoin consul 启动的时候,加入到的 consul集群
-
-server 以服务方式开启consul, 允许其他的consul 连接到开启的 consul上 (形成集群)。如果不加 -server, 表示以 “客户端” 的方式开启。不能被连接。
-
-ui 可以使用 web 页面 来查看服务发现的详情
-
-
测试上述 命令:
# 在终端中,键入: consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=n1 -bind=192.168.6.108 -ui -rejoin -config-dir=/etc/consul.d/ -client 0.0.0.0 #看到提示: ==> Consul agent running!
启动 Google 浏览器, 测试:
-
consul members: 查看集群中有多少个成员。
-
consul info: 查看当前 consul 的 IP 信息。
-
consul leave: 优雅的关闭 consul 。—— 不优雅!Ctrl -c
3. consul 和 grpc 结合
安装 consul 源码包:
ell
$ go get -u -v github.com/hashicorp/consul
使用整体流程
- 创建 proto文件 , 指定 rpc 服务
- 启动 consul 服务发现 consul agent -dev
- 启动server
- 获取consul 对象。
- 使用 consul对象,将 server 信息,注册给 consul
- 启动服务
- 启动client
- 获取consul 对象。
- 使用consul对象,从consul 上获取健康的 服务。
- 再访问服务 (grpc远程调用)
服务注册到consul
package main
import (
"google.golang.org/grpc"
"day02/pb"
"context"
"net"
"fmt"
"github.com/hashicorp/consul/api"
)
// 定义类
type Children struct {
}
// 绑定类方法, 实现借口
func (this *Children)SayHello(ctx context.Context, p *pb.Person) (*pb.Person, error) {
p.Name = "hello " + p.Name
return p, nil
}
func main() {
// 把grpc服务,注册到consul上.
// 1. 初始化consul 配置
consulConfig := api.DefaultConfig()
// 2. 创建 consul 对象
consulClient, err := api.NewClient(consulConfig)
if err != nil {
fmt.Println("api.NewClient err:", err)
return
}
// 3. 告诉consul, 即将注册的服务的配置信息
reg := api.AgentServiceRegistration {
ID:"bj38",
Tags:[]string{"grcp", "consul"},
Name:"grpc And Consul",
Address:"127.0.0.1",
Port:8800,
Check:&api.AgentServiceCheck{
CheckID:"consul grpc test",
TCP:"127.0.0.1:8800",
Timeout:"1s",
Interval:"5s",
},
}
// 4. 注册 grpc 服务到 consul 上
consulClient.Agent().ServiceRegister(®)
//以下为 grpc 服务远程调用//
// 1.初始化 grpc 对象,
grpcServer := grpc.NewServer()
// 2.注册服务
pb.RegisterHelloServer(grpcServer, new(Children))
// 3.设置监听, 指定 IP/port
listener, err := net.Listen("tcp", "127.0.0.1:8800")
if err != nil {
fmt.Println("Listen err:", err)
return
}
defer listener.Close()
fmt.Println("服务启动... ")
// 4. 启动服务
grpcServer.Serve(listener)
}
客户端利用consul 调用远程函数
package main
import (
"google.golang.org/grpc"
"day02/pb"
"context"
"fmt"
"github.com/hashicorp/consul/api"
"strconv"
)
func main() {
// 初始化 consul 配置
consulConfig := api.DefaultConfig()
// 创建consul对象 -- (可以重新指定 consul 属性: IP/Port , 也可以使用默认)
consulClient, err := api.NewClient(consulConfig)
// 服务发现. 从consuL上, 获取健康的服务
services, _, err := consulClient.Health().Service("grpc And Consul", "grcp", true, nil)
// 简单的负载均衡.
addr := services[0].Service.Address + ":" + strconv.Itoa(services[0].Service.Port)
//以下为 grpc 服务远程调用//
// 1. 链接服务
//grpcConn, _ := grpc.Dial("127.0.0.1:8800", grpc.WithInsecure())
// 使用 服务发现consul 上的 IP/port 来与服务建立链接
grpcConn, _ := grpc.Dial(addr, grpc.WithInsecure())
// 2. 初始化 grpc 客户端
grpcClient := pb.NewHelloClient(grpcConn)
var person pb.Person
person.Name = "Andy"
person.Age = 18
// 3. 调用远程函数
p, err := grpcClient.SayHello(context.TODO(), &person)
fmt.Println(p, err)
}
consul注销服务
package main
import "github.com/hashicorp/consul/api"
func main() {
// 1. 初始化 consul 配置
consuConfig := api.DefaultConfig()
// 2. 创建 consul 对象
consulClient, _ := api.NewClient(consuConfig)
// 3. 注销服务
consulClient.Agent().ServiceDeregister("bj38")
}
二、Go-Mirco框架
特点
- 自动服
- 务注册与名称解析:服务发现是微服务开发中的核心,用于解析服务名与地址。consul是Go Micro默认的服务发现注册中心。发现系统可插拔,其他插件像etcd,kubernetes,zookeeper。
- 负载均衡:在服务发现之上构建了负载均衡机制。使用随机处理过的哈希负载均衡机制来保证对服务请求颁布的均匀分布。
- 消息编码:支持基于内容类型(content-type)动态编码消息。客户端和服务端会一起使用- content-type格式来对Go进行无缝编/解码。content-type默认包含proto-rpc和json-rpc。
- Request/Response:RPC通信基于支持双向流的请求/响应方式,提供同步通信机制,请求发送到服务时,会自动解析,负载均衡,拨号,转成字节流。
- 异步消息:发布订阅等功能内置在异步通信与事件驱动架构中,事件通知在微服务开发中处于核心位置。默认的消息传递使用点到点http/1.1,激活tls时则使用http2。
- 可插拔接口:Go Micro为每个分布式系统抽象出接口,因此,Go Micro的接口都是可插拔的,允许其在运行时不可知的情况下仍可支持。
安装
# 使用如下指令安装
# 需要go 1.15以下版本
go get -u -v github.com/micro/micro
go get github.com/micro/micro/v2```
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v2/cmd/protoc-gen-micro
go-micro v2 的使用
https://github.com/why19970628/getCaptcha-micro
创建服务
micro new --type service --gopath=false go-micro-demo
- 参数:
- –namespace: 命名空间 == 包名
- –type : 微服务类型。
- service: 微服务
- web:基于微服务的 web 网站。
- main.go : 项目的入口文件。
- handler/: 处理 grpc 实现的接口。对应实现接口的子类,都放置在 handler 中。
- proto/: 预生成的 protobuf 文件。
- Dockerfile:部署微服务使用的 Dockerfile
- Makefile:编译文件。—— 快速编译 protobuf 文件。
查看创建的项目
-
makefile 编译 proto
-
查看 make proto生 成 的 文件:
- xxx.pb.go
- xxx.micro.go
- RegisterBj38Handler()【168行】服务端用 ———— 对应 grpc RegisterXXXService()
- NewBj38Service()【47行】客户端用 —— 对应 grpc NewXXXClient() —— 对应自己封装的 IintXXX
-
查看 main.go
func main() { // New Service -- 初始化服务器对象. service := micro.NewService( micro.Name("go.micro.srv.bj38"), // 服务器名 micro.Version("latest"), // 版本 ) // Initialise service 与newService作用一致,但优先级高.后续代码运行期,初始化才有使用的必要. //service.Init() // Register Handler --- 注册服务 bj38.RegisterBj38Handler(service.Server(), new(handler.Bj38)) // Register Struct as Subscriber -- redis 发布订阅. //micro.RegisterSubscriber("go.micro.srv.bj38", service.Server(), new(subscriber.Bj38)) // Register Function as Subscriber //micro.RegisterSubscriber("go.micro.srv.bj38", service.Server(), subscriber.Handler) // Run service --- 运行服务 if err := service.Run(); err != nil { log.Fatal(err) } }
通过 micro.NewService 创建服务,并设置一定的参数,本例中是名字和版本。并初始化服务。
基于此服务创建客户端
客户端调用 Hello 方法,并传递参数 Name: Hank -
查看 handler/ xxx.go 文件
包含 与 Interface 严格对应的 3 个函数实现!!
服务管理etcd
启动的服务不可能都依靠日志判断是否注册成功,我们需要一个类似consul控制台的工具来直观管理服务状态。
etcd作为一个key-value数据库本身并不服务管理功能,这里我们又用到了micro工具包的一个功能:web
启动命令如下(注意,注册中心相关配置和api一样要放在web命令之前=)
micro --registry=etcd --registry_address=localhost:2379 web
浏览器打开localhost:8082,点击右上角的service,即可很直观的看到当前已注册的服务情况了:
package main
import (
"getCaptcha/handler"
getCaptcha "getCaptcha/proto/getCaptcha"
"github.com/micro/go-micro/v2/registry"
"github.com/micro/go-micro/v2/registry/etcd"
"github.com/micro/go-micro/v2"
log "github.com/micro/go-micro/v2/logger"
"time"
)
func main() {
// New Service
etcd := etcd.NewRegistry(
// 地址是我本地etcd服务器地址,不要照抄
registry.Addrs("localhost:2379"),
)
service := micro.NewService(
micro.Name("go.micro.service.getCaptcha"),
micro.Version("latest"),
micro.Registry(etcd),
micro.RegisterTTL(time.Second*30),
micro.RegisterInterval(time.Second*15),
)
// Initialise service
service.Init()
// Register Handler
getCaptcha.RegisterGetCaptchaHandler(service.Server(), new(handler.GetCaptcha))
// Register Struct as Subscriber
//micro.RegisterSubscriber("go.micro.service.getCaptcha", service.Server(), new(subscriber.GetCaptcha))
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
客户端
package main
import (
"fmt"
"github.com/micro/go-micro/v2"
"github.com/micro/go-micro/v2/registry"
"github.com/micro/go-micro/v2/registry/etcd"
proto "go-micro-demo-client/proto/go-micro-demo"
"golang.org/x/net/context"
)
func main() {
etcd := etcd.NewRegistry(
// 地址是我本地etcd服务器地址,不要照抄
registry.Addrs("localhost:2379"),
)
service := micro.NewService(
micro.Name("go.micro.client"),
micro.Version("latest"),
micro.Registry(etcd),
)
service.Init()
client := proto.NewGetCaptchaService("go.micro.service.getCaptcha", service.Client())
rsp, err := client.Call(context.TODO(), &proto.Request{
Uuid: "123456",
})
if err != nil {
fmt.Println(err)
}
fmt.Println(rsp.Img)
}
gin框架客户端与micro服务端对接
服务端
package main
import (
"github.com/micro/go-micro/util/log"
"github.com/micro/go-micro"
"test66/handler"
"test66/subscriber"
test66 "test66/proto/test66"
"github.com/micro/go-micro/registry/consul"
)
func main() {
// 初始化服务发现
consulReg := consul.NewRegistry()
// New Service
service := micro.NewService(
micro.Name("go.micro.srv.test66"),
micro.Registry(consulReg),
micro.Version("latest"),
)
// Initialise service
//service.Init()
// Register Handler
test66.RegisterTest66Handler(service.Server(), new(handler.Test66))
// Register Struct as Subscriber
micro.RegisterSubscriber("go.micro.srv.test66", service.Server(), new(subscriber.Test66))
// Register Function as Subscriber
micro.RegisterSubscriber("go.micro.srv.test66", service.Server(), subscriber.Handler)
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
gin框架作为客户端
package main
import (
"github.com/gin-gonic/gin"
"fmt"
test66 "test66web/proto/test66" // test66 为包的别名.
"context"
"github.com/micro/go-micro/registry/consul"
"github.com/micro/go-micro"
)
func CallRemote(ctx *gin.Context) {
// 初始化服务发现 consul
//consulReg:=consul.NewRegistry(func(options *registry.Options) {
// options.Addrs=[]string{"127.0.0.1:8500",}
//})
consulReg := consul.NewRegistry()
// 初始化micro服务对象, 指定consul 为服务发现
service := micro.NewService(
micro.Registry(consulReg),
)
// 1. 初始化客户端
microClient := test66.NewTest66Service("go.micro.srv.test66", service.Client())
fmt.Println()
// 2. 调用远程服务
resp, err := microClient.Call(context.TODO(), &test66.Request{
Name:"xiaowang",
})
if err != nil {
fmt.Println("call err:", err)
return
}
// 为了方便查看, 在打印之前将结果返回给浏览器
ctx.Writer.WriteString(resp.Msg)
fmt.Println(resp, err)
}
func main() {
// 1. 初始化路由 -- 官网:初始化 web 引擎
router := gin.Default()
// 2. 做路由匹配
router.GET("/", CallRemote)
// 3. 启动运行
router.Run(":8080")
}
四、go-zero微服务框架
1. zRPC使用
查看server注册
ETCDCTL_API=3 etcdctl get hello.rpc --prefix
显示服务已经注册
2. 接口限流 periodlimit
2.1 滑动窗口限流
接口限流计数器实现 redis + lua
而在一个分布式系统中,存在多个微服务提供服务。所以当瞬间的流量同时访问同一个资源,如何让计数器在分布式系统中正常计数?同时在计算资源访问时,可能会涉及多个计算,如何保证计算的原子性?
- go-zero 借助 redis 的 incrby 做资源访问计数
- 采用 lua script 做整个窗口计算,保证计算的原子性
下面来看看 lua script 控制的几个关键属性:
-- 资源唯一标识
local key = KEYS[1]
-- 时间窗口内最大并发数
local max_permits = tonumber(KEYS[2])
-- 窗口的间隔时间
local interval_milliseconds = tonumber(KEYS[3])
-- 获取的并发数
local permits = tonumber(ARGV[1])
local current_permits = tonumber(redis.call("get", key) or 0)
-- 如果超过了最大并发数,返回false
if (current_permits + permits > max_permits) then
return false
else
-- 增加并发计数
redis.call("incrby", key, permits)
-- 如果key中保存的并发计数为0,说明当前是一个新的时间窗口,它的过期时间设置为窗口的过期时间
if (current_permits == 0) then
redis.call("pexpire", key, interval_milliseconds)
end
return true
end
-- to be compatible with aliyun redis,
-- we cannot use `local key = KEYS[1]` to reuse thekey
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
-- incrbt key 1 => key visis++
local current = redis.call("INCRBY", KEYS[1], 1)
-- 如果是第一次访问,设置过期时间 => TTL = window size
-- 因为是只限制一段时间的访问次数
if current == 1 then
redis.call("expire", KEYS[1], window)
return 1
elseif current < limit then
return 1
elseif current == limit then
return 2
else
return 0
end
至于上述的 return code ,返回给调用方。由调用方来决定请求后续的操作:
return_code tag call-code mean
0 OverQuota 3 over limit
1 Allowed 1 in limit
2 HitQuota 2 hit limit
限流后续处理
periodlimit 中并没有处理,而是返回 code 。把后续请求的处理交给了开发者自己处理。
如果不做处理,那就是简单的将请求拒绝
如果需要处理这些请求,开发者可以借助 mq 将请求缓冲,减缓请求的压力
采用 tokenlimit,允许暂时的流量冲击
缺点
但是这种方案也存在缺点,因为它要记录时间窗口内的所有行为记录,如果这个量特别大的时候,内存消耗会变得非常严重。
2.2 tokenlimit ,令牌桶限流
https://mp.weixin.qq.com/s/qUvuzHCzylv14o6RvLkoTQ
const (
burst = 100
rate = 100
seconds = 5
)
store := redis.NewRedis("localhost:6379", "node", "")
fmt.Println(store.Ping())
// New tokenLimiter
limiter := limit.NewTokenLimiter(rate, burst, store, "rate-test")
timer := time.NewTimer(time.Second * seconds)
quit := make(chan struct{})
defer timer.Stop()
go func() {
<-timer.C
close(quit)
}()
var allowed, denied int32
var wait sync.WaitGroup
for i := 0; i < runtime.NumCPU(); i++ {
wait.Add(1)
go func() {
for {
select {
case <-quit:
wait.Done()
return
default:
if limiter.Allow() {
atomic.AddInt32(&allowed, 1)
} else {
atomic.AddInt32(&denied, 1)
}
}
}
}()
}
wait.Wait()
fmt.Printf("allowed: %d, denied: %d, qps: %d\n", allowed, denied, (allowed+denied)/seconds)
func TestTokenLimit_TakeBurst(t *testing.T) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
const (
total = 100
rate = 5
burst = 10
)
l := NewTokenLimiter(rate, burst, store, "tokenlimit")
var allowed int
for i := 0; i < total; i++ {
if l.Allow() {
allowed++
}
}
assert.True(t, allowed >= burst)
}
-- 返回是否可以活获得预期的token
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
-- fill_time:需要填满 token_bucket 需要多久
local fill_time = capacity/rate
-- 将填充时间向下取整
local ttl = math.floor(fill_time*2)
-- 获取目前 token_bucket 中剩余 token 数
-- 如果是第一次进入,则设置 token_bucket 数量为 令牌桶最大值
local last_tokens = tonumber(redis.call("get", KEYS[1]))
if last_tokens == nil then
last_tokens = capacity
end
-- 上一次更新 token_bucket 的时间
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now-last_refreshed)
-- 通过当前时间与上一次更新时间的跨度,以及生产token的速率,计算出新的token数
-- 如果超过 max_burst,多余生产的token会被丢弃
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
-- 更新新的token数,以及更新时间
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)
return allowed
五、kartos战神框架
error日志处理
No.1
Error
Error 有几个点是让我们日常开发觉得比较麻烦的。首先是错误检查和打印,其次是业务的错误码设计。我们经常在代码里面到处都是 return error 这个非常麻烦,每个地方都要处理。这个有利有弊。我自己理解的是每一行代码或者每一个函数调用结束以后,应该对它负责,所以尽早处理掉函数的错误也好,异常也好。
对于一个程序来说,这种错误我们应该进行保护,比如说你的程序因为遇到一些问题让你业务退出了,这个对于业务是有影响的。对于一个函数我们要尽快处理,对于一个程序我们要做保护编程。
首先业务开发会分层,Dao 或者 Service,然后开发同学在各个层都打日志,一个业务上线以后,有可能打几十个错误日志,这些日志散落在系统里面,你要查的时候要把整个上下文关联起来看非常麻烦。因为时间顺序是错乱的,非常难找。
其次即便看到日志以后还要猜,到底是哪一个地方代码出的问题,如果偏底层抛出来的错误会非常麻烦定位。还有根因的丢失,有一些错误如果要包装,要附带一些消息,原始的 error 就不见了。我们可能需要基于原始的 error 做等值的判断,就会比较麻烦。
另外业务里面 API 肯定有错误码,100、200,返回-001都有可能,客户端同学要给予这些错误做逻辑调整。API里面的错误 HINT 分两种,一种是面向终端用户,他可能想看到的是更友好的一些错误提示,而不是偏程序的消息展示。另外一种包含一些附带逻辑处理的数据(比如失败后的Retry策略等)。
Handle Erreo-错误检查和打印
- 追加上下文
首先上下文 error 堆栈不方便找,或者不方便定位,我当时找到一个库,找到 pkg/errors 这个库,这是 Dave 开发的,就是把上下文记录下来存到一个地方,之后就能够还原原始的 error,以及整个 error 堆栈,非常方便。
有一些错误,很典型的例子是一个文件打开报错了,或者是读他报错,
我想把具体的文件名一块儿带过去,如果报一个readfailed我根本不知道什么是东西报错了,如果把文件名信息或者原始error带过去。pkg/errors具体实现相对来说比较简单,我们看一下这张图:
比如说WithStack,这里就是原始error传递,,把这个error记录起来,内部使用一个withStack结构体,把堆栈信息存到error字段,这样返回另外一个error抛出去,上层一层一层传递就有信息了。
package dao
import (
"go-common/app/admin/ep/merlin/model"
pkgerr "github.com/pkg/errors"
)
// UpdateStatusForMachines update status for machines.
func (d *Dao) UpdateStatusForMachines(status int, ids []int64) (err error) {
return pkgerr.WithStack(d.db.Model(&model.Machine{}).Where("id IN (?)", ids).Update("status", status).Error)
}
// UpdateMachineStatusByName update status by name.
func (d *Dao) UpdateMachineStatusByName(status int, n string) error {
return pkgerr.WithStack(d.db.Model(&model.Machine{}).Where("name = ?", n).Update("status", status).Error)
}
go协程优雅退出
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
log.Info("exit")
return
default:
return
}
}
更多推荐
所有评论(0)