安装go-zero
go-zero是现在比较流行的golang微服务框架。是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
go-zero 安装 | go-zero Documentation
安装Etcd
Etcd是一个高可用的分布式键值存储系统,相当于加强版redis(可靠性更强)。主要用于共享配置信息和服务发现。它采用Rft一致性算法来保证数据的强致性,并且支持对数进行监视和更新
etcd主要用于微服务的配置中心、服务发现。存储服务对应的地址。传统配置文件模式,ip地址改变了就得重新修改配置和重启,etcd的使用解决了这个问题。
安装地址:Releases · etcd-io/etcd (github.com)
基础语法
写一个视频微服务,提供一个htp接口,用户查询一个视频的信息,并且把关联用户id的用户名也查出来那么用户微服务就要提供一个方法,根据用户id返回用户信息。
rpc
-
在user目录下创建rpc目录,并编写user.proto
syntax="proto3"; package user; option go_package ="./user"; message IdRequest { string id =1; } message UserResponse { //用户id string id = 1; //用户名称 string name = 2; //用户性别 bool gender = 3; } service User { rpc getUser(IdRequest) returns(UserResponse); } -
在rpc目录下用命令使用goctl工具创建rpc代码
goctl rpc protoc user.proto --go_out=types --go-grpc_out=types --zrpc_out=. -
在internal.logic目录下能看到刚生成的代码,编写完善GetUser方法
- 在etc目录下user.yaml文件中,可以看到配置文件
-
我们可以验证一下成果了,使用
go mod tidy更新依赖后go run user.go跑一下代码 -
用apifox试试能不能正常服务
- 创建rpc项目,然后根据指引导入proto文件
- 在接口那里就可以找到proto文件中对应的key了,调用后即可获取刚才编写的值
api
-
创建video.api目录,在其中创建video.api文件
type ( VideoReq { Id string `path:"id"` } VideoRes { Id string `json:"id"` Name string `json:"name"` } ) service video { @handler getVideo get /api/videos/:id (VideoReq) returns (VideoRes) } -
使用命令创建
goctl api go -api video/api/video.api -dir video/api -
在config中加入user rpc的地址
-
配置etcd的地址,以及对应的key,框架会自动通过etcd来获取user.rpc的地址
-
完善服务的依赖
-
现在可以在logic.getvideologic.go中编写逻辑了
func (l *GetVideoLogic) GetVideo(req *types.VideoReq) (resp *types.VideoRes, err error) { //现在可以像调用本地方法一样调用rpc方法 getUser, err := l.svcCtx.UserRpc.GetUser(context.Background(), &user.IdRequest{Id: "1"}) if err != nil { return nil, err } return &types.VideoRes{ Id: req.Id, Name: getUser.Name, }, nil } -
启动!
go run video.go -
试试能不能用
总结
- 编写用户微服务的rpc服务的proto文件
- 生成代码
- 添加自己的逻辑
- 编写视频微服务的api服务的api文件
- 生成代码
- 完善依赖,配置
- 添加自己的逻辑
go-zero省了很多代码,让我们更专注于业务的开发
api怎么写
先写一个小示例
- 创建user.api
前缀:设置好的前缀会自动加到后缀的前面
代码:
type LoginRequest{
Username string `json:"username"`
Pwd string `json:"password"`
}
type LoginResponse{
Code int `json:code`
Data string `json:data`
Msg string `json:msg`
}
type UserInfo{
UserId uint `json:"user_id"`
Username string `json:"username"`
}
type UserInfoResponse{
Code int `json:code`
Data UserInfo `json:data`
Msg string `json:msg`
}
service users{
@handler login
post /api/usrs/login (LoginRequest) returns(LoginResponse)
@handler userInfo
get /api/usrs/info returns(UserInfoResponse)
}
2. 通过user.api创建api
goctl api go -api user.api -dir .
#解释:使用goctl api 转成 go -api ,读取 user.api 转到 -dir 路径 .
如果在user.api里新增接口,则重新运行该命令以生成最新的代码
- 写完逻辑试验一下
func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
fmt.Println(req.Username, req.Pwd)
return &types.LoginResponse{
Code: 0,
Data: "xxx.xxx.xxx",
Msg: "successful",
}, nil
}
go run users.go
对Response进行封装
封装response
每次都写Response很麻烦,因为他们内容差不多,所以咱们封装一下
-
我们将user.api里的response先给删了
-
在根目录下建一个common.response目录,创建一个文件用于封装response
type ResponseBody struct { Code int `json:"code"` Data any `json:"data"` Msg string `json:"msg"` } func Response(r *http.Request, w http.ResponseWriter, res any, err error) { if err != nil { //可以根据不同错误码返回不同的错误信息 body := ResponseBody{ Code: 1, Data: nil, Msg: "ERR", } httpx.WriteJson(w, http.StatusOK, body) return } body := ResponseBody{ Code: 0, Data: res, Msg: "successful", } httpx.WriteJson(w, http.StatusOK, body) } -
修改handler,用封装好的替代原来的好几个判断语句
模板定制化
由于response的封装go-zero是不知道的,所以每次生成handler都需要咱们手动去改。我们可以使用官方的模板定制化工具去修改
-
打开
handler.tpl文件。- 全局搜一下有没有这个文件,有的话直接改
- 没有的话使用这个命令生成
goctl template init
-
修改其中的代码
package {{.PkgName}} import ( "net/http" "github.com/zeromicro/go-zero/rest/httpx" {{.ImportPackages}} "gozeroStudy/common/response" ) func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { {{if .HasRequest}}var req types.{{.RequestType}} if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return } {{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx) {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}}) {{if .HasResp}}response.Response(r,w,resp,err) {{else}}response.Response(r,w,nil,err){{end}} } } -
再次转化就可以发现,handler变成了我们想要的样子。
api校验
什么是JWT
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
用于什么地方:
- Authorization (授权) : 用户登录后每个请求都包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
- Information Exchange (信息交换) : 安全的在各方之间传输信息。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
配置JWT
在.api中可以增加校验
重新生成会发现internal.handler.routes.go中多了一行校验
点进这个serverCtx.Config.Auth中我们会看到Config里有这个Auth
在配置文件中我们可以配置jwt的密钥和过期时间
签发JWT
在登录的时候需要签发JWT,后续认证的时候会自动认证
-
在common下创建jwts文件夹,然后复制一份公共代码进去
package jwts import ( "errors" "github.com/golang-jwt/jwt/v4" "time" ) // JwtPayLoad jwt中payload数据 type JwtPayLoad struct { UserID uint `json:"user_id"` Username string `json:"username"` // 用户名 Role int `json:"role"` // 权限 1 普通用户 2 管理员 } type CustomClaims struct { JwtPayLoad jwt.RegisteredClaims } // GenToken 创建 Token func GenToken(user JwtPayLoad, accessSecret string, expires int64) (string, error) { claim := CustomClaims{ JwtPayLoad: user, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expires))), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) return token.SignedString([]byte(accessSecret)) } // ParseToken 解析 token func ParseToken(tokenStr string, accessSecret string, expires int64) (*CustomClaims, error) { token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(accessSecret), nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { return claims, nil } return nil, errors.New("invalid token") } -
在logic中写签发逻辑
-
即jwts里定义的类
-
试试能不能用
JWT验证失败
jwt验证失败默认是发的401,这对前端非常不友好,那么我们就需要自定义一个验证失败的回调
在user.go里写一个,server创建时传进去就行
结果:
操作数据库
操作mysql
- 使用goctl的model工具可以通过sql文件生成golang代码
goctl model mysql ddl --src user.sql dir .
#使用goctl model mysql ddl 打开--src user.sql 生成 dir 到 . ("."是当前文件夹的意思)
- 修改api的代码
写数据库地址
账户(root):密码@tcp(127.0.0.1:3306)/数据库名?charset=utf8mb4&parseTime=True
在config中配置刚才写的数据库地址
在servicecontext.go中将自己的结构体注入到服务中
- 然后我们就可以通过刚刚写的UsersModel操作数据库了
- 试一下能不能用(此处是随便往数据库里存了个值)
-
我们可以在通过goctl生成的代码里修改具体数据库交互逻辑
操作gorm
用gorm开发更快一点,但是得手写
-
创建一个model
-
和直接操作mysql一样,修改配置项
-
在common里写一个init函数用于连接数据库
-
服务注入
-
试试能不能用
RPC怎么写
示例
用proto生成
首先当然要编写一个proto文件
syntax = "proto3";
package user;
option go_package="./user";
message UserInfoRequest {
uint32 user_id = 1;//1是指序列化之后的位置
}
message UserInfoResponse {
uint32 user_id = 1;
string username = 2;
}
message UserCreateRequest{
string username = 1;
string password = 2;
}
message UserCreateResponse{
string err = 1;
}
service user{
rpc UserInfo(UserInfoRequest)returns(UserInfoResponse);
rpc UserCreate(UserCreateRequest)returns(UserCreateResponse);
}
然后进入目录,也是用goctl工具生成rpc代码
goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.
可以看到types里已经生成了对应的grpc代码
internal.server中已经自动生成了服务端代码,服务端自动调用logic的函数,那么我们就只需要完善logic里的逻辑部分即可,省了很多事。
同时,客户端的代码也生成好了,非常方便
服务拆分
现实我们服务多得很,怎么拆分呢?只需要生成的时候在语句最后加个-m即可
例如,在proto中我们将服务随便拆成两份。
生成的时候语句加个-m
goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. -m
可以看到果然拆成了两份
测试一下(需要重新导入proto)
操作gorm
和api中的接入是差不多的——创建model、修改配置项、服务注入
然后修改logic以访问mysql即可
连接api
我们知道,一个微服务要有rpc服务和api服务,我们得把他们接起来
生成rpc
像前文一样创建proto,生成rpc以及连接gorm
syntax = "proto3";
package user;
option go_package="./user";
message UserInfoRequest {
uint32 user_id = 1;//1是指序列化之后的位置
}
message UserInfoResponse {
uint32 user_id = 1;
string username = 2;
}
message UserCreateRequest{
string username = 1;
string password = 2;
}
message UserCreateResponse{
string err = 1;
}
service user{
rpc UserCreate(UserCreateRequest)returns(UserCreateResponse);
rpc UserInfo(UserInfoRequest)returns(UserInfoResponse);
}
// goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. -m
生成api
来复习一下
创建api文件夹以及user.api文件,编写api文件
type UserCreateRequest {
Username string `json:"username"`
Pwd string `json:"password"`
}
type UserInfoRequest {
UserId uint `path:"id"`
}
type UserInfoResponse {
UserId uint `json:"user_id"`
Username string `json:"username"`
}
@server (
prefix: /api/users
)
service users {
@handler create
post / (UserCreateRequest) returns (string)
@handler userInfo
get /:id (UserInfoRequest) returns (UserInfoResponse)
}
进入目录使用goctl生成代码
goctl api go -api user.api -dir .
连接Etcd
配置rpc
服务注入
写一下逻辑
试试能不能用
success!api到rpc到数据库成功连接
在b站学的,大佬讲的真好 ——— 课程视频地址