我们通过一个系列文章跟大家详细展示一个 go-zero
微服务实例,整个系列分十三篇文章,目录结构如下:
- go-zero 实战 - 服务划分与项目创建
- go-zero 实战 - User API Gateway
- go-zero 实战 - User Login
- go-zero 实战 - User Register
- go-zero 实战 - User Userinfo
- go-zero 实战 - Food API Gateway
- go-zero 实战 - Food Search
- go-zero 实战 - Food AddFood
- go-zero 实战 - Food DeleteFood
- go-zero 实战 - Food Foodlist
- go-zero 实战进阶 - rpc 服务
- go-zero 实战进阶 - 用户管理 rpc 服务
- go-zero 实战进阶 - 食材管理 rpc 服务
期望通过本系列文章带你在本地利用 go-zero
快速开发一个《食谱指南》系统,让你快速上手微服务。
API 概述
在上一篇文章中,我们提到用户管理服务中包含三个 API,分别为 Login
, Register
和 UserInfo
。下面我们分别梳理一下三个 API 的请求体和响应体。
- 用户登录接口(login)
请求参数 | 类型 | 说明 | 是否必传 |
---|---|---|---|
string | 登录邮箱 | 是 | |
password | string | 登录密码 | 是 |
响应字段 | 类型 | 说明 |
---|---|---|
id | int | 用户 ID |
username | string | 用户姓名 |
string | 用户邮箱 | |
accessToken | string | 用户登录令牌 |
accessExpire | int | 登录令牌有效期(单位:秒) |
refreshAfter | int | 登录令牌刷新时间 |
- 用户注册接口(register)
请求参数 | 类型 | 说明 | 是否必传 |
---|---|---|---|
username | string | 用户姓名 | 是 |
string | 用户邮箱 | 是 | |
password | string | 用户密码 | 是 |
响应字段 | 类型 | 说明 |
---|---|---|
id | int | 用户 ID |
username | string | 用户姓名 |
string | 用户邮箱 | |
accessToken | string | 用户登录令牌 |
accessExpire | int | 登录令牌有效期(单位:秒) |
refreshAfter | int | 登录令牌刷新时间 |
- 用户信息接口(userinfo)
请求参数 | 类型 | 说明 | 是否必传 |
---|---|---|---|
... | ... | ... | ... |
响应字段 | 类型 | 说明 |
---|---|---|
id | int | 用户 ID |
username | string | 用户姓名 |
string | 用户邮箱 |
User API Gateway 创建
- 进入服务工作区
$ cd FoodGuides/service/usermanage
生成 user api 服务
- 创建
user.api
文件
$ goctl api -o api/user.api
Done.
- 编辑
user.api
文件
生成的 user.api
文件会自动生成一些内容,以帮助我们更快的上手。根据 API 预定义的请求体和响应体,编辑 user.api
文件如下:
syntax = "v1"
info (
title: "UserApi"
desc: "用户服务相关 API"
author: "DESKTOP-4T5UKHP/Owner"
email: "renpanpan1990@163.com"
)
type (
LoginRequest {
Email string `json:"email"`
Password string `json:"password"`
}
LoginResponse {
UserReply
}
)
type (
RegisterRequest {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
RegisterResponse {
UserReply
}
)
type UserInfoResponse {
UserReply
}
type UserReply {
Id int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
JwtToken
}
type JwtToken {
AccessToken string `json:"accessToken,omitempty"`
AccessExpire int64 `json:"accessExpire,omitempty"`
RefreshAfter int64 `json:"refreshAfter,omitempty"`
}
service user-api {
@handler Login
post /users/login (LoginRequest) returns (LoginResponse)
@handler Register
post /users/register (RegisterRequest) returns(RegisterResponse)
@handler UserInfo
post /users/userinfo returns(UserInfoResponse)
}
我们定义了三个关于用户的 api
,分别为:Login
, Register
和 UserInfo
。
- 运行模板生成命令
$ cd FoodGuides/service/usermanage/api
$ goctl api go -api user.api -dir .
Done.
Tips
如果执行上面的命令时,控制台报如下错误:
user.api line 4:34 mismatched ':', found input '" email: "' Error: user.api line 4:34 mismatched ':', found input '" email: "'
请检查
info.author
的设置,自动生成的名称为DESKTOP-4T5UKHP\Owner
,我们在生成user-api
服务前,需要将其修改为DESKTOP-4T5UKHP/Owner
,再重复执行模板生成命令。
这个时候,我们可以查看一下 api
目录下都会生成什么文件和内容:
➜ api tree
.
├── etc
│ └── user-api.yaml
├── internal
│ ├── config
│ │ └── config.go
│ ├── handler
│ │ ├── loginhandler.go
│ │ ├── registerhandler.go
│ │ ├── routes.go
│ │ └── userinfohandler.go
│ ├── logic
│ │ ├── loginlogic.go
│ │ ├── registerlogic.go
│ │ └── userinfologic.go
│ ├── svc
│ │ └── servicecontext.go
│ └── types
│ └── types.go
├── user.api
└── user.go
- 添加下载依赖包
回到 FoodGuides
项目根目录执行如下命令:
$ go mod tidy
- 启动服务
$ cd FoodGuides/service/usermanage/api
$ go run user.go -f etc/user-api.yaml
Starting server at 0.0.0.0:8888...
当终端输出 Starting server at 0.0.0.0:8888...
信息时,意味着该服务启动成功。(如果要断开服务,请在终端中按下组合键 Ctrl
+ C
)
理解服务是怎么跑起来的
goctl
工具可以很方便快捷的帮助我们创建 api
服务。但是如果不能理解 api
服务是如何跑起来的,看着项目结构就会很懵逼。
api/etc
下的user-api.yaml
文件。该文件配置了api
服务所需的一些变量,如服务名称Name
、接口地址Host
、端口号Port
等信息,MySQL
、Redis
、rpc
等配置也是写在这里的。api
下的user.api
文件。该文件定义了api
服务所提供的接口信息。之后如果需要新增接口,同样是在这里处理。然后调用goctl
重新生成服务。api
下的user.go
文件。该文件是api
服务的入口文件,一切都是从这里开始。
internal 文件夹
api
服务的内部实现代码都放在了该文件夹下面。
internal/config
下的config.go
文件。你会发现,该文件的定义和 user-api.yaml
的定义类似。是的。user-api.yaml
在 user.go
入口文件在 main
方法里,就被解析成了 config
对象。所以他们的值是一一对应的。
internal/handler
下的 routers.go
文件。该文件为 api
服务的路由文件,定义了各个接口的请求方式,接口路径,以及接口触发的方法。例如:客户端以 post
方式请求了 http://localhost:8888/users/register ,则 api
服务将会触发 RegisterHandler()
方法。
func RegisterHandlers(engine *rest.Server, serverCtx *svc.ServiceContext) {
engine.AddRoutes(
[]rest.Route{
{
Method: http.MethodPost,
Path: "/users/login",
Handler: LoginHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/users/register",
Handler: RegisterHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/users/userinfo",
Handler: UserInfoHandler(serverCtx),
},
},
)
}
internal/handler
下的 xxxhandler.go
文件。各个接口触发方法的具体实现都写在了这里的文件里。如 registerhandler.go
文件中就自动生成如下方法:
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RegisterRequest
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewRegisterLogic(r.Context(), svcCtx)
resp, err := l.Register(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}
可以看到 RegisterHandler
首先对接收到的参数进行了解析。然后调用了 logic.NewRegisterLogic()
,可以发现RegisterHandler
还并不是最终的实现,最终的业务处理其实是在 logic
文件夹下的各个 logic.go
文件中。
internal/logic
下的 xxxlogic.go
文件。我们将最终在各个 logic
的实现方法里实现相关服务逻辑。
internal/svc
下的 servicecontext.go
文件。该文件保存了 api
服务的 config
对象。然后 svc
对象会从 handle
传递到 logic
方法里。
type ServiceContext struct {
Config config.Config
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
}
}
internal/types
下的 types.go
文件。该文件定义了 我们在 user.api
模板文件里声明的各个结构体。
调用过程梳理
以客户端调用 login
接口为例。我们先看看自动生成的 user.go
文件中是如何实现的:
package main
import (
"flag"
"fmt"
"FoodGuides/usermanage/api/internal/config"
"FoodGuides/usermanage/api/internal/handler"
"FoodGuides/usermanage/api/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
)
var configFile = flag.String("f", "etc/user-api.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
user.go
入口文件通过 yaml
配置文件,实例化 config
对象。
var configFile = flag.String("f", "etc/user-api.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
}
实例化 ServiceContext
对象
ctx := svc.NewServiceContext(c)
ctx
内部保存了 config
对象。
实例化 Server
对象。
server := rest.MustNewServer(c.RestConf)
路由实现, 注意 ctx
被传递到 handlers
内部了。
handler.RegisterHandlers(server, ctx)
api
服务跑起来
server.Start()
当客户端调用 login
接口。 触发 LoginHandler
方法
// usermanage\api\internal\handler\loginhandler.go
func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.LoginRequest
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewLoginLogic(r.Context(), svcCtx)
resp, err := l.Login(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}
然后 LoginHandler
调用 LoginLogic
方法
// usermanage/api/internal/logic/loginlogic.go
func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
// todo: add your logic here and delete this line
return
}
在 Login
方法里,我们需要稍后完成登录逻辑。处理完数据后,接口逐层响应回去,最终完成客户端接口的调用。