Contents

golang-go-zero-教程-Api

go-zero官网

go-zero详细文档

本系列为作者跟着Mikaelemmmm的b站教学视频学习时做的笔记

api文件

语法和go类似,api文法官网

syntax语法声明:syntax="v1"

import语法块

1
2
3
4
5
6
7
import "foo.api"
import "foo/bar.api"

import(
    "bar.api"
    "foo/bar/foo.api"
)

info语法块

1
2
3
4
5
info(
    foo: "foo value"
    bar: "bar value"
    desc: "long long long long long long text"
)

type语法块

  • 保留了golang内置数据类bool,int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr,float32,float64,complex64,complex128,string,byte,rune,
  • 兼容golang struct风格声明
  • 保留golang关键字

tag表和tag修饰符:xxx:“yyy,zzz”,xxx位tag,yyy为值,zzz为tag修饰符

  • json,path,form,header
  • optional,options,default,range

service语法块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//atServer
@server(
	//声明当前service下所有路由需要jwt鉴权,且会自动生成包含jwt逻辑的代码
	jwt: Auth
	//声明当前service或者路由文件分组
	group: user
	//添加路由分组
	prefix : userapi/v1
	//声明当前service需要开启中间件
	middleware: AuthMiddleware
)
//对外提供的api接口,service name必须一致,包括import的文件
service user-api {
	//doc
	@doc "获取用户信息"
	//handler
	@handler userInfo
	get /users/info (UserInfoReq) returns (UserInfoResq)
	
	@doc "修改用户信息"
	@handler userUpdate
	get /users/update (UserUpdateReq) returns (UserUpdateResq)
}

构建命令

1
goctl api go -api user.api -dir ../ -style goZero

开发规范

main文件

main文件的名称和api文件名称一致

查看该文件main函数上面有一个-f的flag配置项。默认使用etc里面的yaml配置文件,在启动服务的时候使用-f可以使用指定的配置文件

1
var configFile = flag.String("f", "etc/user-api.yaml", "the config file")

载入配置文件

1
2
var c config.Config
conf.MustLoad(*configFile, &c)//可以添加选项conf.UseEnv(),这样可以在yaml文件里面使用环境变量${xxx}

etc/.yaml配置文件,默认生成配置有

1
2
3
Name: user-api
Host: 0.0.0.0
Port: 8888

生成一个服务对象

1
2
server := rest.MustNewServer(c.RestConf)
defer server.Stop()//执行清理操作,比如关闭logx

内部执行SetUp(),配置logx,Prometheus(服务监控),Telemetry(jeager,链路追踪),matrics

另外这里可以给出选项来配置server,可以自己写一个选项函数

1
RunOption func(*Server)

预定义返回选项的函数,添加时可以调用,这些函数和RunOption在一个文件下

  • func WithUnauthorizedCallback(callback handler.UnauthorizedCallback) RunOption:添加jwt不通过的逻辑
  • func WithNotFoundHandler(handler http.Handler) RunOption:没有匹配的handler时的handler

将依赖配置到ctx

1
ctx := svc.NewServiceContext(c)

internal/svc/serviceContext.go的ServiceContext对象,逻辑里面的svcCtx就是这个对象

注册路由

1
handler.RegisterHandlers(server, ctx)

internal/handler/routes.go里面是详细路由配置

启动服务

1
server.Start()

源码

Server

1
2
3
4
5
Server struct {
  ngin   *engine
  //httpx路由
  router httpx.Router
}

engine

1
2
3
4
5
6
type engine struct {
	...
  //结构体路由
	routes               []featuredRoutes
	...
}

server.Start()

1
2
3
func (s *Server) Start() {
	handleError(s.ngin.start(s.router))
}

s.ngin.start(s.router)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func (ng *engine) start(router httpx.Router) error {
  //绑定结构体路由到httpx路由
	if err := ng.bindRoutes(router); err != nil {
		return err
	}

	if len(ng.conf.CertFile) == 0 && len(ng.conf.KeyFile) == 0 {
		return internal.StartHttp(ng.conf.Host, ng.conf.Port, router, ng.withTimeout())
	}

  //启动服务,里面调用ListenAndServe
	return internal.StartHttps(ng.conf.Host, ng.conf.Port, ng.conf.CertFile,
		ng.conf.KeyFile, router, func(svr *http.Server) {
		if ng.tlsConfig != nil {
			svr.TLSConfig = ng.tlsConfig
		}
	}, ng.withTimeout())
}

ng.bindRoutes(router)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func (ng *engine) bindRoutes(router httpx.Router) error {
	metrics := ng.createMetrics()

  //绑定每组结构体路由到httpx路由
	for _, fr := range ng.routes {
		if err := ng.bindFeaturedRoutes(router, fr, metrics); err != nil {
			return err
		}
	}

	return nil
}

ng.bindFeaturedRoutes(router, fr, metrics)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func (ng *engine) bindFeaturedRoutes(router httpx.Router, fr featuredRoutes, metrics *stat.Metrics) error {
	verifier, err := ng.signatureVerifier(fr.signature)
	if err != nil {
		return err
	}
  //每组结构体路由的每个结构体路由绑定httpx路由
	for _, route := range fr.routes {
		if err := ng.bindRoute(fr, router, metrics, route, verifier); err != nil {
			return err
		}
	}

	return nil
}

ng.bindRoute(fr, router, metrics, route, verifier)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (ng *engine) bindRoute(fr featuredRoutes, router httpx.Router, metrics *stat.Metrics,
	route Route, verifier func(chain.Chain) chain.Chain) error {
	chn := ng.chain
	if chn == nil {
    //绑定中间件链,之前是alice包,现在这个应该是自己封装的,执行路由处理之前先执行下面这些中间件
		chn = chain.New(
			handler.TracingHandler(ng.conf.Name, route.Path),
			ng.getLogHandler(),
			handler.PrometheusHandler(route.Path),
			handler.MaxConns(ng.conf.MaxConns),
			handler.BreakerHandler(route.Method, route.Path, metrics),
			handler.SheddingHandler(ng.getShedder(fr.priority), metrics),
			handler.TimeoutHandler(ng.checkedTimeout(fr.timeout)),
			handler.RecoverHandler,
			handler.MetricHandler(metrics),
			handler.MaxBytesHandler(ng.checkedMaxBytes(fr.maxBytes)),
			handler.GunzipHandler,
		)
	}

	chn = ng.appendAuthHandler(fr, chn, verifier)

	for _, middleware := range ng.middlewares {
		chn = chn.Append(convertMiddleware(middleware))
	}
	handle := chn.ThenFunc(route.Handler)

	return router.Handle(route.Method, route.Path, handle)
}

etc目录

etc/yaml文件,和internal/config/config.go对应

internal目录

internal/config

config文件,和etc/yaml文件对应

注意这里不能用匿名字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package config

import (
	"github.com/zeromicro/go-zero/core/logx"
	"github.com/zeromicro/go-zero/core/stores/cache"
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
	rest.RestConf
	//即使名字一样也不能用匿名字段,可定义匿名结构体
	DB struct {
		DataSource string
	}
	Cache       cache.CacheConf
	Log         logx.LogConf
	UserRpcConf zrpc.RpcClientConf
	//变量名和类型名一样
	Auth		Auth
}

type Auth struct {
	AccessSecret string
	AccessExpire string
}

internal/handler目录

该目录除了routes.go文件,其他的文件都按api中定义的@server.group分组

routes.go

注册路由的逻辑,和api里面定义的server对应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
  //一次调用对应api中的一个@server
	server.AddRoutes(
		[]rest.Route{
      //一个路由对应一个@server下的一个@handler
			{
				Method:  http.MethodPost,
				Path:    "/user/register",
				Handler: user.RegisterHandler(serverCtx),
			},
			{
				Method:  http.MethodPost,
				Path:    "/user/login",
				Handler: user.LoginHandler(serverCtx),
			},
		},
    //选项和@server的配置有关
		rest.WithPrefix("/usercenter/v1"),
	)
  ...
}

AddRoutes的逻辑是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func (s *Server) AddRoutes(rs []Route, opts ...RouteOption) {
  //对应上面的一组路由
	r := featuredRoutes{
		routes: rs,
	}
  //执行传入选项函数对这组路由
	for _, opt := range opts {
		opt(&r)
	}
  //将一组路由添加到引擎engine
	s.ngin.addRoutes(r)
}

s.ngin.addRoutes(r)

1
2
3
4
func (ng *engine) addRoutes(r featuredRoutes) {
  //将一组路由添加到引擎engine
	ng.routes = append(ng.routes, r)
}

Handler函数

相当于java的controller,返回http.HandlerFunc的函数,这里会传入svc里面的那个结构体命名为ctc

1
func datacenterHandler(ctx *svc.ServiceContext) http.HandlerFunc

首先进行参数解析

1
2
3
4
5
var req types.UserInfoReq
if err := httpx.Parse(r, &req); err != nil {
  httpx.Error(w, err)
  return
}

然后生成对应的logic类型实例,调用logic的方法,这里同时传递了ctx,所以ctx的依赖在logic里也能够使用

1
2
l := user.NewUserInfoLogic(r.Context(), svcCtx)
resp, err := l.UserInfo(req)

internal/logic目录

写真正的业务逻辑,封装为一个结构体,它具有默认的logx,ctx以及svcCtx

业务逻辑封装成结构体的方法,结构体还有一个工厂函数,可以在其他逻辑里面通过工厂函数新建并执行逻辑函数

1
2
3
4
5
6
7
type UserInfoLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func (l *UserInfoLogic) UserInfo(req *types.UserInfoReq) (resp *types.UserInfoResp, err error) 

logic函数传入的req和返回的resp分别对应type里面的类型,可以直接使用req.xxx来访问传入的数据

另一个返回值是标准库errors里的error对象,成功返回nil,出错返回err实例。

internal/middleware目录

存放api文件定义的middleware

另外internal/handler目录里面的routes.go是这样使用middleware的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
	server.AddRoutes(
		rest.WithMiddlewares(
      //这里的参数是用的serverCtx结构体的TestMiddleware字段,所以需要把TestMiddleware添加到svc的serverCtx结构体中
			[]rest.Middleware{serverCtx.TestMiddleware},
			[]rest.Route{
				{
					Method:  http.MethodGet,
					Path:    "/user/info",
					Handler: user.UserInfoHandler(serverCtx),
				},
				...
      }
		),
		rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
		rest.WithPrefix("/userapi/v1"),
	)
  ...
}

middleware需要导入到svcCtx,导入过程如下

先修改svcCtx结构体

1
2
3
4
type ServiceContext struct {
  ...
	TestMiddleware rest.Middleware
}

再修改New函数

1
2
3
4
5
6
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		...
		TestMiddleware: middleware.NewTestMiddleware().Handle,
	}
}

接下来就可以修改middleware的内部逻辑了

1
2
3
4
5
6
7
func (m *TestMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("come in testMiddleware before")
		next(w, r)
		fmt.Println("come in testMiddleware end")
	}
}

全局中间件

全局中间件通常放在app/common/middleware目录中,比如jwt全局中间件

一个中间件其实就是一个接收并返回http.HandleFunc对象的函数,这里将其封装到一个结构体的方法,这样能向里面传参

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type GlobalMiddleware struct{
  可加字段
}

func NewGlobalMiddleware(/*可加参数*/) *GlobalMiddleware {
	return &GlobalMiddleware{/*补全字段*/}
}

func (m *GlobalMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
    //可使用CommonJwtAuthMiddleware对象m的参数
		fmt.Println("global before")

		next(w, r)

		fmt.Println("global end")
	}
}

main中使用如下方式添加全局中间件

1
2
3
4
5
func main() {
	...
	server.Use(middleware.NewCommonJwtAuthMiddleware().Handle)
  	...
}

全局中间件比局部中间件更早执行

整体源码分析

整体源码分析

zero里面有很多基于golang …语法糖的选项模式

读取config.yaml

如果在config.yaml中使用环境变量则在main中给该函数配置选项conf.UseEnv()

1
conf.MustLoad(*configFile, &c, conf.UseEnv())

这样就可以在yaml文件里面使用${envName}格式的变量来绑定环境变量

根据配置文件生成server对象

1
server := rest.MustNewServer(c.RestConf)

Setup():日志,Prometheus,链路追踪,metrics

api调用rpc

配置

rpc配置

rpc的yaml文件需要配置直连,Etcd或者K8s,选择的方式要和api对应。这里展示Etcd,Etcd使用Key来查询服务

1
2
3
4
Etcd:
  Hosts:
  - 0.0.0.0:2379
  Key: user.rpc

api配置

internal/config/config.go中config结构体添加zrpc.RpcClientConf字段

1
2
3
4
type Config struct {
	...
	UserRpcConf zrpc.RpcClientConf
}

etc/.yaml文件添加配置,下面的代码展示的是ETCD的配置方式,除了这个方式还有直连EndPoint和K8s。

Etcd使用Key来查询服务

1
2
3
4
5
UserRpcConf:
  Etcd:
    Hosts:
      - 0.0.0.0:2379
    Key: user.rpc

svc添加zrpc客户端

1
2
3
4
type ServiceContext struct {
	...
	UserRpcClient        usercenter.Usercenter
}

根据配置c创建rpc客户端,rpc客户端的go文件在rpc那边,如果不是本地开发,需要拷贝过来

1
2
3
4
5
6
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		...
		UserRpcClient:        usercenter.NewUsercenter(zrpc.MustNewClient(c.UserRpcConf)),
	}
}

调用

zrpc的Client可以直接调用service,传入2个参数:ctx和pb生成的Req结构体对象指针。返回的结果为pb定义的Resp结构体对象指针。所以go-zero的rpc调用非常方便

1
2
3
4
5
6
userResp, err := l.svcCtx.UserRpcClient.GetUserInfo(l.ctx, &pb.GetUserInfoReq{
	Id: req.UserId,
})
if err != nil {
	return nil, err
}

官方支持的api与rpc对接的三种方式

直连,Etcd和K8s

除了这三种在go-zero team里面的zero-contrib里面还有consul,nacos,polaris等注册中心的连接方式

Etcd

rpc的配置

rpc的yaml文件配置

1
2
3
4
Etcd:
  Hosts:
  - 0.0.0.0:2379
  Key: user.rpc

具体的Etcd配置可以看config.go里面的RpcServerConf.Etcd相关的字段

api的配置

api的yaml文件配置需要与rpc中的Key对应才能实现服务发现。

1
2
3
4
5
UserRpcConf:
  Etcd:
    Hosts:
      - 0.0.0.0:2379
    Key: user.rpc

修改etc/config.go

1
2
3
4
type Config struct {
	...
	UserRpcConf zrpc.RpcClientConf
}

api调用rpc时是需要rpc生成的客户端目录里面的go文件

1
2
3
4
type ServiceContext struct {
	...
	UserRpcClient  usercenter.Usercenter
}

详见调用

rpc注册到etcd后可以使用如下命令查看etcd注册的服务

1
etcdctl get --prefix ""

直连

直接配置api

1
2
3
UserRpcConf:
  Endpoints:
    - 0.0.0.0:8080

EndPoints是一个数组,也有负载均衡。go-zero都是使用P2C算法来进行负载均衡的。

K8s

rpc不需要配置,但是需要写k8s的yaml文件,并将rpc装载到k8s pod中

api需要配置yaml文件,将rpc设置为Target(k8s连接),值为k8s集群地址,比如下面这个例子,k8s://表示k8s协议

1
2
UserRpcConf:
  Target: k8s://go-zero-looklook/basic-rpc-svc:9001

部署过程

使用goctl生成rpc和api的dockerfile

1
goctl docker -go user.go

生成rpc、api镜像同时推送到镜像仓库

生成镜像需要将build上下文必须是具有gomod文件的目录,即上下文必须是项目根目录,goctl有标记rpc或api文件相对根目录的相对路径,所以不用担心将整个项目构建

1
go build -t xxxx:yyy -f path/to/Dockerfile .

另外还有一种简单的方法

1
goctl docker -go .\service\user\rpc\user.go

其余内容见bilibililooklook文档

api参数校验集成第三方库

默认go-zero的参数校验不够用时,可以使用第三方validator库

给结构体添加tag

1
2
3
4
5
6
type User struct {
	FirstName      string     `validate:"required"`
	LastName       string     `validate:"required"`
	Age            uint8      `validate:"gte=0,lte=130"`
	Email          string     `validate:"required,email"`
}

生成validator对象

1
v:=validator.New()

直接调用validator.Struct(&xxx)返回err,err会记录出错的地方,详见文档

1
2
err:=v.Struct(&user)
err:=v.StructCtx(r.Context,&user)

为了防止被覆盖,校验的tag加到api文件

可以修改模板来复用validator校验逻辑,避免每次生成都得修改文件

jwt鉴权

官方文档

api文件设置jwt:xxx

routes.go中引入jwt的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
	server.AddRoutes(
		rest.WithMiddlewares(
			[]rest.Middleware{serverCtx.TestMiddleware},
			[]rest.Route{
				...
			}...,
		),
    //需要在config.go里面添加Auth字段
		rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
		rest.WithPrefix("/userapi/v1"),
	)
}

所以,yaml配置文件配置xxx的相关配置

1
2
3
Auth:
  AccessSecret: $AccessSecret
  AccessExpire: $AccessExpire

还有修改etc/config.go

1
2
3
4
5
6
7
type Config struct {
	...
	Auth        struct {
		AccessSecret string
		AccessExpire string
	}
}

后续通过jwt鉴权的请求携带的jwt信息可以在其他地方通过ctx访问

1
2
3
4
func (l *SearchLogic) Search(req types.SearchReq) (*types.SearchReply, error) {
  logx.Infof("userId: %v",l.ctx.Value("userId"))// 这里的key和生成jwt token时传入的key一致
  return &types.SearchReply{}, nil
}

定义jwt验证不通过的响应

1
2
3
server := rest.MustNewServer(c.RestConf, rest.WithUnauthorizedCallback(func(w http.ResponseWriter, r *http.Request, err error) {
  w.WriteHeader(200)
}))

详见looklook

api的兼容性

api的兼容性

定义或修改API的时候一定要考虑向前兼容:

  • 增加新的API接口协议
  • 请求参数添加字段,需要保证新老客户端对该字段的处理方式不同
  • 响应结果添加字段,该字段信息只会在新版本客户端中展示

如下几种情况是向前不兼容的:

  • 删除或重命名服务、字段、方法
  • 修改字段类型
  • 修改现有请求的可见行为,客户端通常依赖于API行为和语义,即使这样的行为没有被明确支持或记录。因此,在大多数情况下,修改API数据的行为或语义将被消费者视为是破坏性的
  • 给资源消息添加 读取/写入 字段

http.Request.Body只能读取一次

http.Request.Body是一个ioutil.NopCloser类型,只能读取一次,如果要读取多次,得再把body给塞回去:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
body, err := ioutil.ReadAll(r.Body)
if err != nil {
	http.Error(w, err.Error(), http.StatusBadRequest)
	return
}

// 将请求体存储到缓冲区中
buf := bytes.NewBuffer(body)

// 将缓冲区的内容写回http.Request.Body
r.Body = ioutil.NopCloser(buf)
 |