Go_zero学习笔记
<!-- go-zero -->
安装配置
go-zero_github
go-zero文档
go install github.com/zeromicro/go-zero/tools/goctl@latest goctl --version // goctl version 1.7.2 windows/amd64
gopath/bin/
会生成goctl
的执行进程(%GOPATH%\bin
设置到path
环境变量中)安装
protoc&protoc-gen-go
goctl env check --install --verbose --force安装
goctl
插件安装
go-zero
go get -u github.com/zeromicro/go-zero@latest
项目创建与启动
进入目录go-zero_study/helloworld/
goctl api new hello // 创建api服务 // goctl rpc new hello // 创建rpc服务 dir go mod tidy
修改代码
go run .\hello.go -f .\etc\hello-api.yaml // -f 跟上配置文件
etcd
-
主要用于微服务的配置中心和服务发现,数据可靠性比
redis
更强
在对外
api
的应用中,如何知道order服务的rpc
地址?如果服务的
ip
地址变化了怎么办?在传统的配置文件模式,修改配置文件,应用程序是需要重启才能解决的,所以引入etcd
-
windows安装
-
etcd-v3.5.16-windows-amd64.zip
-
-
docker
安装docker run --name etcd -d -p 2379:2379 -p 2380:2380 -e ALLOW_NONE_AUTHENTICATION=yes bitnami/etcd:3.3.11 etcd
安装完成后在下载路径启用cmd
(因为未加环境变量)
// 设置或更新值 etcdctl put name 张三 // 获取值 etcdctl get name // 只要value etcdctl get name --print-value-only // 获取name前缀的键值对 etcdctl get name --prefix // 删除键值对 etcdctl del name // 监听键的变化 etcdctl watch name
// 例如 etcdctl put rpc.order 127.0.0.1:7001 etcdctl put rpc.user 127.0.0.1:7002 etcdctl get rpc --prefix // ------------ etcdctl watch rpc.user // 另起一个cmd etcdctl put rpc.user 127.0.0.1:7003
最简单微服务demo
-
一个用户微服务,一个视频微服务
-
视频微服务需要提供一个
http
接口,用户查询一个视频的信息,并且把关联用户id
的用户名也查出来,那么用户微服务就要提供一个方法,根据用户id
返回用户信息
go mod init
用户微服务
-
编写
rpc
的proto
// 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); } // goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. // goctl rpc protoc user/rpc/user.proto --go_out=user/rpc/types --go-grpc_out=user/rpc/types --zrpc_out=user/rpc/
// user\rpc\internal\logic\getuserlogic.go ... func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) { // todo: add your logic here and delete this line return &user.UserResponse{ Id: "1", Name: "yangxiaojin", Gender: true, }, nil }
# user\rpc\etc\user.yaml Name: user.rpc ListenOn: 127.0.0.1:9090 Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc
// 启动etcd start /B etcd > start.log 2>&1 netstat -an | findstr :2379 // 运行user.go文件 go run user.go // 打开Apifox新建gRPC项目 127.0.0.1:9090 // 关闭etcd tasklist | findstr etcd taskkill /F /IM etcd.exe
video
微服务
// 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/
-
添加
user rpc
配置因为要在
video
里面调用user
的rpc
服务// video/api/internal/config/config.go package config import ( "github.com/zeromicro/go-zero/rest" "github.com/zeromicro/go-zero/zrpc" ) type Config struct { rest.RestConf // rest配置 UserRpc zrpc.RpcClientConf // 用户rpc配置 }
-
完善服务依赖
// video/api/internal/svc/servicecontext.go package svc import ( "go-zero_study/user/rpc/userclient" "go-zero_study/video/api/internal/config" "github.com/zeromicro/go-zero/zrpc" ) type ServiceContext struct { Config config.Config // 这是个配置对象 UserRpc userclient.User // 这是个rpc客户端对象 } // NewServiceContext 这个函数是用来创建ServiceContext对象的 func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ // 这里将配置对象赋值给ServiceContext的Config字段 Config: c, // 这里将rpc客户端对象赋值给ServiceContext的UserRpc字段 UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc))} }
-
添加
yaml
配置# video/api/etc/video.yaml Name: video Host: 0.0.0.0 Port: 8888 UserRpc: Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc
-
// video/api/internal/logic/getvideologic.go // GetVideo 获取视频信息 func (l *GetVideoLogic) GetVideo(req *types.VideoReq) (resp *types.VideoRes, err error) { // 调用user rpc 服务 user1, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdRequest{ Id: "1", // todo: 这里需要改成用户id }) if err != nil { return nil, err } return &types.VideoRes{ // 返回视频信息 Id: req.Id, // 视频id Name: user1.Name, // 用户名 }, nil }
-
服务启动
go run user\rpc\user.go -f user\rpc\etc\user.yaml go run video\api\video.go -f video\api\etc\video.yaml 127.0.0.1:9090/api/videos/1 // video\api\internal\handler\routes.go中有路由 video\api\etc\video.yaml中是端口号
api
讲解
api
文件就是对这个服务所有api
的描述
服务名,函数名,路径,请求方法,请求参数,响应参数
我们以用户管理的两个重要接口为例,去编写它的api
文件
// api-study\user\api\user.api type LoginRequest { Username string `json:"username"` Password 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"` } // 会生成 users.go 的文件 service users { // 登录接口 @handler login // 登录接口的请求方法为post,入参为() post /api/users/login (LoginRequest) returns (LoginResponse) // 获取用户信息接口 @handler userInfo // 接口的请求方法为get,没有入参 get /api/users/info returns (UserInfoResponse) } // goctl api go -api user.api -dir . // -api user.api 指定api文件 -dir . 指定生成的目录
// api-study\user\api\internal\logic\loginlogic.go func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) { fmt.Println(req.Username, req.Password) return &types.LoginResponse{Code: 0, Data: "success", Msg: "success"}, nil }
// api-study\user\api\internal\logic\userinfologic.go func (l *UserInfoLogic) UserInfo() (resp *types.UserInfoResponse, err error) { return &types.UserInfoResponse{ Code: 0, Data: types.UserInfo{ UserId: 1, Username: "yangxiaojin", }, Msg: "success", }, nil }
go run users.go // 新建快速请求传入json格式参数 { "username": "yangxiaojin", "password": "123456" }
通过这个示例,我们发现实际操作起来还是有些问题
-
响应如何封装?
-
统一
api
前缀 -
用户信息接口应该要进行
jwt
验证 -
api
文档
api
响应封装
不把code
,data
,msg
写在api
里面,我们通过封装统一响应
在统一响应里面去加上code data msg
type LoginRequest { Username string `json:"username"` Password string `json:"password"` } type UserInfoResponse { UserId uint `json:"user_id"` Username string `json:"username"` } // 会生成users.go文件 service users { // 登录接口 @handler login // 登录接口的请求方法为post,入参为() ,返回值为string返回token post /api/users/login (LoginRequest) returns (string) // 获取用户信息接口 @handler userInfo // 接口的请求方法为get,没有入参 get /api/users/info returns (UserInfoResponse) } // goctl api go -api user.api -dir . // -api user.api 指定api文件 -dir . 指定生成的目录
// 创建一个公共的文件夹common/ common/response/enter.go package response import ( "github.com/zeromicro/go-zero/rest/httpx" "net/http" ) type Body struct { Code uint32 `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data"` } // Response http返回 func Response(r *http.Request, w http.ResponseWriter, resp interface{}, err error) { if err == nil { //成功返回 r := &Body{ Code: 0, Msg: "成功", Data: resp, } httpx.WriteJson(w, http.StatusOK, r) return } //错误返回 errCode := uint32(10086) // 可以根据错误码,返回具体错误信息 errMsg := "服务器错误" httpx.WriteJson(w, http.StatusBadRequest, &Body{ Code: errCode, Msg: errMsg, Data: nil, }) }
// api-study\user\api_v2\internal\handler\loginhandler.go l := logic.NewLoginLogic(r.Context(), svcCtx) resp, err := l.Login(&req) response.Response(r, w, resp, err)
// api-study\user\api_v2\internal\handler\userinfohandler.go l := logic.NewUserInfoLogic(r.Context(), svcCtx) resp, err := l.UserInfo() response.Response(r, w, resp, err)
// 然后完善逻辑即可 func (l *LoginLogic) Login(req *types.LoginRequest) (resp string, err error) { // todo: add your logic here and delete this line fmt.Println(req.UserName, req.Password) return "xxxx.xxxx.xxx", nil }
api
前缀
对于用户服务而言,api
的前缀都是 /api/users
@server ( prefix: /api/users ) service users { @handler login post /login (LoginRequest) returns (string) @handler userInfo get /info returns (UserInfoResponse) } // goctl api go -api user.api -dir . // -api user.api 指定api文件 -dir . 指定生成的目录
api-jwt
验证
// api-study\user\api_jwt\user.api type LoginRequest { Username string `json:"username"` Password string `json:"password"` } type UserInfoResponse { UserId uint `json:"user_id"` Username string `json:"username"` } @server( prefix: /api/users ) service users { @handler login post /login (LoginRequest) returns (string) } @server( jwt: Auth // 指定jwt认证 prefix: /api/users ) service users { @handler userInfo get /info returns (UserInfoResponse) } // goctl api go -api user.api -dir . // -api user.api 指定api文件 -dir . 指定生成的目录
// api-study\user\api_jwt\internal\handler\routes.go // rest.WithJwt(serverCtx.Config.Auth.AccessSecret), // ctrl+Auth >>> Auth struct {AccessSecret string AccessExpire int64}
# api-study\user\api_jwt\etc\users.yaml 转换之后,修改配置文件 Name: users Host: 0.0.0.0 Port: 8888 Auth: AccessSecret: yangiaojinliyibo # 秘钥大于八位 AccessExpire: 3600 # 过期时间,单位秒
// common/jwts/enter.go jwt公共代码 package jwts import ( "errors" "time" "github.com/golang-jwt/jwt/v4" ) // 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 // 自定义的payload数据 jwt.RegisteredClaims // 继承jwt.RegisteredClaims } // GenToken 创建 Token // accessSecret 签名秘钥 func GenToken(user JwtPayLoad, accessSecret string, expires int64) (string, error) { claim := CustomClaims{ JwtPayLoad: user, // 自定义的payload数据 RegisteredClaims: jwt.RegisteredClaims{ // 继承jwt.RegisteredClaims ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expires))), // 默认过期时间为1小时 }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) // 使用HS256算法生成token return token.SignedString([]byte(accessSecret)) // 生成token } // 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 { // token 有效 return claims, nil // 返回自定义的payload数据 } return nil, errors.New("invalid token") // token 无效 }
// api-study\user\api_jwt\internal\logic\loginlogic.go 在登录成功之后签发jwt func (l *LoginLogic) Login(req *types.LoginRequest) (resp string, err error) { auth := l.svcCtx.Config.Auth token, err := jwts.GenToken(jwts.JwtPayLoad{ UserID: 1, Username: "枫枫", Role: 1, // 从数据库中获取角色 }, auth.AccessSecret, auth.AccessExpire) if err != nil { return "", err } return token, err }
// api-study\user\api_jwt\internal\logic\userinfologic.go func (l *UserInfoLogic) UserInfo() (resp *types.UserInfoResponse, err error) { userId := l.ctx.Value("user_id").(json.Number) fmt.Printf("%v, %T, \n", userId, userId) username := l.ctx.Value("username").(string) uid, _ := userId.Int64() return &types.UserInfoResponse{ UserId: uint(uid), Username: username, }, nil }
userinfo
这个接口就已经自动加上jwt
的验证了
go run users.go // http://127.0.0.1:8888/api/users/login // body json {"username": "yangxiaojin", "password": "renxioli"} // http://127.0.0.1:8888/api/users/info // Auth >>> Bearer Token 把token值写进去
不过这个token是需要这样加
headers:{ Authorization: "Bearer token" }
自定义jwt
失败响应
没有通过jwt
的响应是401
如果要修改jwt
验证的响应,在main中,加上jwt
验证的回调函数即可
// api-study\user\api_jwt\users.go func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) server := rest.MustNewServer(c.RestConf, rest.WithUnauthorizedCallback(JwtUnauthorizedResult)) 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() } // JwtUnauthorizedResult jwt验证失败的回调 func JwtUnauthorizedResult(w http.ResponseWriter, r *http.Request, err error) { fmt.Println(err) // 具体的错误,没带token,token过期?伪造token? httpx.WriteJson(w, http.StatusOK, response.Body{10087, "鉴权失败", nil}) }
生成api
文档
后端对外的api
,肯定要和前端进行对接
那么在go-zero
里面怎么生成api
接口文档呢
-
安装
goctl-swagger
go install github.com/zeromicro/goctl-swagger@latest
-
生成
app.json
如果没有doc
目录,需要创建
goctl api plugin -plugin goctl-swagger="swagger -filename app.json -host localhost:8888 -basepath /" -api v1.api -dir ./doc // -api 后面是api的名字 -dir 后面是生成的那个目录(没有会报错)
-
使用docker,查看这个swagger页面
docker run -d --name swag -p 8087:8080 -e SWAGGER_JSON=/opt/app.json -v D:\IT\go_project3\go_test\v1\api\doc\:/opt swaggerapi/swagger-ui // doc目录改成自己的目录
可以再完善下api
信息(添加中文解释)
@server( prefix: /api/users ) service users { @doc( summary: "用户登录" ) @handler login post /login (LoginRequest) returns (string) } @server( jwt: Auth prefix: /api/users ) service users { @doc( summary: "获取用户信息" ) @handler userInfo get /info returns (UserInfoResponse) }
改为再重新生成一下json
但是,我发现这个swagger体验不怎么好,使用了自定义响应之后,swag这里改不了
公司项目的话,都是有自己的api
平台
团队项目的话,也可以用apifox
所以,个人用swagger
的话,凑活着用也不是不行
go-zero
操作mysql
原生操作
-
有
sql
文件,可以使用代码生成
-- model_study\user\model\user.sql CREATE TABLE user ( id bigint AUTO_INCREMENT, username varchar(36) NOT NULL, password varchar(64) default '', UNIQUE name_index (username), PRIMARY KEY (id) ) ENGINE = InnoDB COLLATE utf8mb4_general_ci; -- 创建数据库 mysql -u root -p create database zero_db; use zero_db; source user.sql; -- goctl model mysql ddl --src user.sql --dir .
生成的go代码,自动为我们生成了增删改查的代码
-
代码使用
// model_study\user\api\user.api type LoginRequest { Username string `json:"username"` Password string `json:"password"` } @server( prefix: /api/users ) service users { @handler login post /login (LoginRequest) returns (string) }
// 在config里面写上mysql配置 package config import "github.com/zeromicro/go-zero/rest" type Config struct { rest.RestConf Mysql struct { DataSource string } Auth struct { AccessSecret string AccessExpire int64 } }
// 配置文件 Name: users Host: 0.0.0.0 Port: 8888 Mysql: DataSource: root:root@tcp(127.0.0.1:3306)/zero_db?charset=utf8mb4&parseTime=True&loc=Local Auth: AccessSecret: dfff1234 AccessExpire: 3600
先在依赖注入的地方创建连接
// v1/api/internal/svc/servicecontext.go package svc import ( "github.com/zeromicro/go-zero/core/stores/sqlx" "go_test/v1/api/internal/config" "go_test/v1/model" ) type ServiceContext struct { Config config.Config UsersModel model.UserModel } func NewServiceContext(c config.Config) *ServiceContext { mysqlConn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ Config: c, UsersModel: model.NewUserModel(mysqlConn), } }
为了简单,我就直接在登录逻辑里面,写逻辑了
func (l *LoginLogic) Login(req *types.LoginRequest) (resp string, err error) { // 增 l.svcCtx.UsersModel.Insert(context.Background(), &model.User{ Username: "枫枫", Password: "123456", }) // 查 user, err := l.svcCtx.UsersModel.FindOne(context.Background(), 1) fmt.Println(user, err) // 查 user, err = l.svcCtx.UsersModel.FindOneByUsername(context.Background(), "枫枫") fmt.Println(user, err) // 改 l.svcCtx.UsersModel.Update(context.Background(), &model.User{ Username: "枫枫1", Password: "1234567", Id: 1, }) user, err = l.svcCtx.UsersModel.FindOne(context.Background(), 1) fmt.Println(user, err) // 删 l.svcCtx.UsersModel.Delete(context.Background(), 1) user, err = l.svcCtx.UsersModel.FindOne(context.Background(), 1) fmt.Println(user, err) return }
结合gorm
其实大部分场景,结合gorm
会更加高效,当然也可以使用其他的orm
直接编写model
文件,因为直接编写sql
文件再转换,会有些地方有问题
package model import "gorm.io/gorm" type UserModel struct { gorm.Model Username string `gorm:"size:32" json:"username"` Password string `gorm:"size:64" json:"password"` }
在common
里面写上gorm
的连接语句
// common/init_db/init_gorm.go package init_db import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" ) // InitGorm gorm初始化 func InitGorm(MysqlDataSource string) *gorm.DB { db, err := gorm.Open(mysql.Open(MysqlDataSource), &gorm.Config{}) if err != nil { panic("连接mysql数据库失败, error=" + err.Error()) } else { fmt.Println("连接mysql数据库成功") } return db }
然后在context
里面进行注入
package svc import ( "go_test/common/init_db" "go_test/v1/api/internal/config" "go_test/v1/model" "gorm.io/gorm" ) type ServiceContext struct { Config config.Config DB *gorm.DB } func NewServiceContext(c config.Config) *ServiceContext { mysqlDb := init_db.InitGorm(c.Mysql.DataSource) mysqlDb.AutoMigrate(&model.User{}) return &ServiceContext{ Config: c, DB: mysqlDb, } }
使用就很简单了,和gorm是一模一样的
func (l *LoginLogic) Login(req *types.LoginRequest) (resp string, err error) { var user models.UserModel err = l.svcCtx.DB.Take(&user, "username = ? and password = ?", req.Username, req.Password).Error if err != nil { return "", errors.New("登录失败") } return user.Username, nil }
go-zero
中的rpc
服务
单rpc
服务模式
我们编写一个proto
文件
提供两个服务,一个是获取用户信息方法,一个是用户添加的方法
// user.proto syntax = "proto3"; package user; option go_package = "./user"; message UserInfoRequest { uint32 user_id = 1; } message UserInfoResponse { uint32 user_id = 1; string username = 2; } message UserCreateRequest { string username = 1; string password = 2; } message UserCreateResponse { } service Users { rpc UserInfo(UserInfoRequest) returns(UserInfoResponse); rpc UserCreate(UserCreateRequest) returns(UserCreateResponse); } // goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.
和传统
grpc
不一样的是,go-zero
里面的proto
文件不能外部引入message
Name: user.rpc ListenOn: 0.0.0.0:8080 Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc
在logic
中完善对应的逻辑
func (l *UserInfoLogic) UserInfo(in *user.UserInfoRequest) (*user.UserInfoResponse, error) { fmt.Println(in.UserId) return &user.UserInfoResponse{ UserId: in.UserId, Username: "枫枫", }, nil } func (l *UserCreateLogic) UserCreate(in *user.UserCreateRequest) (*user.UserCreateResponse, error) { fmt.Println(in.Username, in.Password) return &user.UserCreateResponse{}, nil }
/* 使用apifox调用grpc 创建gRPC的项目,127.0.0.1:8080 user.Users/UserCreate Message: {"username": "yangxiaojin", "password": "renxiaoli"} --- 127.0.0.1:8080 user.Users/Userinfo Message: {"user_id": 1} */
rpc
服务分组
默认情况下,一个proto
文件里面只能有一个service
,有多个的话,转换会报错
如果一个rpc
服务,有很多方法,转换之后的目录就很不直观了
我们可以在转换的时候,使用-m
参数指定服务分组
syntax = "proto3"; package user; option go_package = "./user"; message UserInfoRequest { uint32 user_id = 1; } message UserInfoResponse { uint32 user_id = 1; string username = 2; } message UserCreateRequest { string username = 1; string password = 2; } message UserCreateResponse { } service UserCreate { rpc UserCreate(UserCreateRequest) returns(UserCreateResponse); } service UserInfo { rpc UserInfo(UserInfoRequest) returns(UserInfoResponse); } // goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. -m
rpc
结合gorm
syntax = "proto3"; package user; option go_package = "./user"; message UserInfoRequest { uint32 user_id = 1; } message UserInfoResponse { uint32 user_id = 1; string username = 2; } message UserCreateRequest { string username = 1; string password = 2; } message UserCreateResponse { uint32 user_id = 1; string err = 2; } service user{ rpc UserInfo(UserInfoRequest)returns(UserInfoResponse); rpc UserCreate(UserCreateRequest)returns(UserCreateResponse); } // goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.
models
定义
// rpc_study/user_gorm/models/user_model.go package models import "gorm.io/gorm" type UserModel struct { gorm.Model Username string `gorm:"size:32" json:"username"` Password string `gorm:"size:64" json:"password"` }
配置文件,添加mysql
的相关配置
# rpc_study/user_gorm/rpc/etc/user.yaml Name: user.rpc ListenOn: 0.0.0.0:8080 Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc Mysql: DataSource: root:root@tcp(127.0.0.1:3307)/zero_db?charset=utf8mb4&parseTime=True&loc=
填写对应的配置映射
// rpc_study/user_gorm/rpc/internal/config/config.go package config import "github.com/zeromicro/go-zero/zrpc" type Config struct { zrpc.RpcServerConf Mysql struct { DataSource string } }
在服务依赖的地方,进入注入
// rpc_study/user_gorm/rpc/internal/svc/servicecontext.go package svc import ( "gorm.io/gorm" "zero_study/common/init_gorm" "zero_study/rpc_study/user_gorm/models" "zero_study/rpc_study/user_gorm/rpc/internal/config" ) type ServiceContext struct { Config config.Config DB *gorm.DB } func NewServiceContext(c config.Config) *ServiceContext { db := init_gorm.InitGorm(c.Mysql.DataSource) db.AutoMigrate(&models.UserModel{}) return &ServiceContext{ Config: c, DB: db, } }
创建逻辑
func (l *UserCreateLogic) UserCreate(in *user.UserCreateRequest) (pd *user.UserCreateResponse, err error) { pd = new(user.UserCreateResponse) var model models.UserModel err = l.svcCtx.DB.Take(&model, "username = ?", in.Username).Error if err == nil { pd.Err = "该用户名已存在" return } model = models.UserModel{ Username: in.Username, Password: in.Password, } err = l.svcCtx.DB.Create(&model).Error if err != nil { logx.Error(err) pd.Err = err.Error() err = nil return } pd.UserId = uint32(model.ID) return }
查询逻辑
func (l *UserInfoLogic) UserInfo(in *user.UserInfoRequest) (*user.UserInfoResponse, error) { var model models.UserModel err := l.svcCtx.DB.Take(&model, in.UserId).Error if err != nil { return nil, errors.New("用户不存在") } return &user.UserInfoResponse{ UserId: uint32(model.ID), Username: model.Username, }, nil }
rpc
结合api
// user_api_rpc/rpc/user.proto syntax = "proto3"; package user; option go_package = "./user"; message UserInfoRequest { uint32 user_id = 1; } message UserInfoResponse { uint32 user_id = 1; string username = 2; } message UserCreateRequest { string username = 1; string password = 2; } message UserCreateResponse { uint32 user_id = 1; string err = 2; } service user{ rpc UserInfo(UserInfoRequest)returns(UserInfoResponse); rpc UserCreate(UserCreateRequest)returns(UserCreateResponse); } // goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.
Name: user.rpc ListenOn: 0.0.0.0:8080 Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc Mysql: DataSource: root:root@tcp(127.0.0.1:3307)/zero_db?charset=utf8mb4&parseTime=True&loc=
package config import "github.com/zeromicro/go-zero/zrpc" type Config struct { zrpc.RpcServerConf Mysql struct { DataSource string } }
// rpc_study/user_gorm/rpc/internal/svc/servicecontext.go package svc import ( "gorm.io/gorm" "zero_study/common/init_gorm" "zero_study/rpc_study/user_gorm/models" "zero_study/rpc_study/user_gorm/rpc/internal/config" ) type ServiceContext struct { Config config.Config DB *gorm.DB } func NewServiceContext(c config.Config) *ServiceContext { db := init_gorm.InitGorm(c.Mysql.DataSource) db.AutoMigrate(&models.UserModel{}) return &ServiceContext{ Config: c, DB: db, } }
func (l *UserCreateLogic) UserCreate(in *user.UserCreateRequest) (pd *user.UserCreateResponse, err error) { pd = new(user.UserCreateResponse) var model models.UserModel err = l.svcCtx.DB.Take(&model, "username = ?", in.Username).Error if err == nil { pd.Err = "该用户名已存在" return } model = models.UserModel{ Username: in.Username, Password: in.Password, } err = l.svcCtx.DB.Create(&model).Error if err != nil { logx.Error(err) pd.Err = err.Error() err = nil return } pd.UserId = uint32(model.ID) return }
func (l *UserInfoLogic) UserInfo(in *user.UserInfoRequest) (*user.UserInfoResponse, error) { var model models.UserModel err := l.svcCtx.DB.Take(&model, in.UserId).Error if err != nil { return nil, errors.New("用户不存在") } return &user.UserInfoResponse{ UserId: uint32(model.ID), Username: model.Username, }, nil }
// user_api_rpc/modls
api
// user_api_rpc/api/user.api type UserCreateRequest { Username string `json:"username"` Password string `json:"password"` } type UserInfoRequest { ID uint `path:"id"` } type UserInfoResponse { UserId uint `json:"user_id"` Username string `json:"username"` } @server( prefix: /api/users ) service users { @handler userInfo get /:id (UserInfoRequest) returns (UserInfoResponse) @handler userCreate post / (UserCreateRequest) returns (string ) } // goctl api go -api user.api -dir .
在配置文件里面填写rpc
服务的key
# api的Key必须和rpc的Key一样否则找不到 Name: users Host: 0.0.0.0 Port: 8888 UserRpc: Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc
填写配置文件
package config import ( "github.com/zeromicro/go-zero/rest" "github.com/zeromicro/go-zero/zrpc" ) type Config struct { rest.RestConf UserRpc zrpc.RpcClientConf }
依赖注入,初始化rpc
的客户端
package svc import ( "github.com/zeromicro/go-zero/zrpc" "zero_study/rpc_study/user_api_rpc/api/internal/config" "zero_study/rpc_study/user_api_rpc/rpc/userclient" ) type ServiceContext struct { Config config.Config UserRpc userclient.User } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)), } }
创建用户
func (l *UserCreateLogic) UserCreate(req *types.UserCreateRequest) (resp string, err error) { response, err := l.svcCtx.UserRpc.UserCreate(l.ctx, &user.UserCreateRequest{ Username: req.Username, Password: req.Password, }) if err != nil { return "", err } if response.Err != "" { return "", errors.New(response.Err) } return }
用户信息
func (l *UserInfoLogic) UserInfo(req *types.UserInfoRequest) (resp *types.UserInfoResponse, err error) { response, err := l.svcCtx.UserRpc.UserInfo(l.ctx, &user.UserInfoRequest{ UserId: uint32(req.ID), }) if err != nil { return nil, err } return &types.UserInfoResponse{ UserId: uint(response.UserId), Username: response.Username }, nil }
<!-- go-zero -->
微服务
微服务是什么?
-
大型应用分解成多个独立的组件
-
针对每个独立服务的开发,部署,运营维护,扩展等都不影响其他服务
微服务衍生出很多组件:
-
服务网关:确保服务提供者对客户端的透明,这一层可反向路由、安全认证、灰度发布、日志监控等前置动作
-
服务发现:注册并维护远程服务以及服务提供者的地址,供服务消费者发现和调用,为保证可用性,如:
etcd
、nacos
、consul
等 -
服务框架:用于实现微服务的
RPC
框架 -
服务监控:对服务消费者与提供者之间的调用情况进行监控和数据展示,如:
prometheus
-
服务追踪:记录每个请求的微服务调用完整链路,以便进行问题定位和故障分析,如
jeager
、zipkin
... -
服务治理:通过一系列手段来保证在各种意外情况下,服务调用扔能正常进行,这些手段包括熔断、隔离、限流、降级、负载均衡等,如:
Sentinel
-
基础:如分布式消息队列、日志存储、数据库、缓存、文件、服务器、搜索集群...
-
分布式配置中心:统一配置,如:
nacos
、consul
、apllo
... -
分布式事务:
seata
、dtm
... -
容器以及容器编排:docker、
k8s
... -
定时任务
go微服务实践
Go天然适配云原生,而云原生时代已经到来,各个应用组件基础设施都应该积极的拥抱云原生
标准库/自研派系
不要让框架束缚开发
微服务的基础是通信,也就是RPC
框架的选择,这一点上,大部分会选择grpc
或在grpc
基础上进行自研rpc
框架的研发
至于其他用到的组件,有需要的时候,进行集成就可以了
如果部署采用k8s
,并且使用服务网络,比如istio
来处理,那么你只需要关心业务逻辑就可以,不需要再关心服务发现,熔断,流量控制,负载均衡...
web
框架派系
此派系依旧秉承不要让框架束缚开发
但由于从标准库到可使用的web框架,仍需要一定量的开发工作,所以更倾向于选择成熟的go web框架,比如gin,所以出现了以gin+grpc
为核心,将其他组件集成进来的微服务架构
同样可以使用k8s+istio
最终构建的仍旧是现代化的云原生微服务架构
大一统框架
有从其他语言转过来的,而其他语言都有成熟大一统框架,所以希望go也有这样的框架
能很好地减轻工作量,达到快速开发的目的,代价就是遵循框架的规则
go-zero
单体应用HelloWorld
go-zero_github
go-zero文档
go install github.com/zeromicro/go-zero/tools/goctl@latest goctl --version // goctl version 1.7.2 windows/amd64
gopath/bin/
会生成goctl
的执行进程(%GOPATH%\bin
设置到path
环境变量中)安装
protoc&protoc-gen-go
goctl env check --install --verbose --force安装
goctl
插件安装
go-zero
go get -u github.com/zeromicro/go-zero@latest
进入目录go-zero_study/helloworld/
goctl api new hello // 创建api服务 // goctl rpc new hello // 创建rpc服务 dir go mod tidy
修改代码
go run .\hello.go -f .\etc\hello-api.yaml // -f 跟上配置文件
微服务版HelloWorld
cd go-zero_study mkdir mall cd mall goctl api new order goctl api new user go work init go work use order go work use user
user给order提供服务 (grpc
)
-
user/
新建rpc/user.proto
// mall\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; // 用户性别 string gender = 3; } service User { rpc getUser(IdRequest) returns(UserResponse); }
cd user\rpc goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. # 把microhelloworld\mall\user\下的原文件删除 rpc\下的文件除.proto.mod.sum 剩下的都剪切到user\中 import('注意路径') PS D:...mall\user> go mod tidy
// mall\user\internal\logic\getuserlogic.go // ... func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) { // todo: add your logic here and delete this line return &user.UserResponse{ Id: in.Id, Name: "xiaojin", Gender: "nv", }, nil }
// mall\user\etc\user.yaml Name: user.rpc ListenOn: 127.0.0.1:8080 Etcd: Hosts: - 127.0.0.1:2379 Key: user.rpc
-
第一种方法(自己写)
// mall\order\internal\handler\routes.go func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( []rest.Route{ { Method: http.MethodGet, Path: "/from/:name", Handler: OrderHandler(serverCtx), }, { Method: http.MethodGet, Path: "/api/order/get/:id", Handler: GetOrderHandler(serverCtx), }, }, ) }
-
二(自动生成)
// mall\order\order.api syntax = "v1" type Request { Name string `path:"name,options=you|me"` } type Response { Message string `json:"message"` } type( OrderReq{ Id string `path:"id"` } OrderReply{ Id string `json:"id"` Name string `json:"name"` UserName string `json:"username"` } ) service order-api { @handler OrderHandler get /from/:name (Request) returns (Response) @handler GetOrderHandler get /api/order/get/:id (OrderReq) returns (OrderReply) }
cd order goctl api go -api order.api -dir ./gen # -dir ./gen防止覆盖后判断不了是否更改,新生成代码和旧代码对比,不同的替换 types\types.go,新增logic\getorderlogic.go handler\getorderhandler.go handler\routes.go # 检查完没有要改的就把gen/删掉即可
// order\...\logic\getorderlogic.go写逻辑 package logic import ( "context" "user/types/user" "order/internal/svc" "order/internal/types" "github.com/zeromicro/go-zero/core/logx" ) type GetOrderLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewGetOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetOrderLogic { return &GetOrderLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (resp *types.OrderReply, err error) { // todo: add your logic here and delete this line userId := l.GetOrderById(req.Id) // 根据用户id去user服务获取用户信息 userResponse, err := l.svcCtx.UserRpc.GetUser(context.Background(), &user.IdRequest{ Id: userId, }) if err != nil { return nil, err } return &types.OrderReply{ Id: req.Id, Name: "hello order name", UserName: userResponse.Name, }, nil } func (l *GetOrderLogic) GetOrderById(id string) string { // todo: add your logic here and delete this line return "1" }
连接
// mall\order\internal\config\config.go package config import ( "github.com/zeromicro/go-zero/rest" "github.com/zeromicro/go-zero/zrpc" ) type Config struct { rest.RestConf // 去etcd获取user rpc的地址 UserRpc zrpc.RpcClientConf // 这里的RpcClientConf是go-zero/zrpc/internal/config/config.go中的一个结构体 }
# mall\order\etc\order-api.yaml Name: order-api Host: 0.0.0.0 Port: 8888 UserRpc: Etcd: Hosts: - 127.0.0.1:2379 # etcd地址 Key: user.rpc # etcd key # 官方文档里有 || 两个key必须保持一致
// mall\order\internal\svc\servicecontext.go package svc import ( "order/internal/config" "user/userclient" "github.com/zeromicro/go-zero/zrpc" ) type ServiceContext struct { Config config.Config UserRpc userclient.User } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)), // 这里的user.NewUserClient()方法是从user包中导入的 } }
起个etcd
# mall/dockor-compose.yml version: '3' services: Etcd: container_name: etcd3 # 容器名称 image: bitnami/etcd:${ETCD_VERSION} # 镜像名称 deploy: # 部署策略 replicas: 1 # 副本数 restart_policy: # 重启策略 condition: on-failure # 失败时重启 environment: # 环境变量 - ALLOW_NONE_AUTHENTICATION=yes # 允许匿名访问 privileged: true # 特权模式 volumes: # 挂载卷 - ${ETCD_DIR}/data:/bitnami/etcd/data # 数据卷 ports: # 端口映射 - ${ETCD_PORT}:2379 # 客户端端口 - 2380:2380 # 集群通信端口
// mall/.env // 定义环境变量 COMPOSE_PROJECT_NAME=go-zero_study ETCD_DIR=D:\Godemo\test_folder\go-zero_study\etcd ETCD_VERSION=v3.4.13 ETCD_PORT=2379
// 启动 docker-compose up -d
分别启动user服务和order服务
// 访问http://localhost:8888/api/order/get/1 { "id": "1", "name": "test order", "userName": "test" }
go-zero零基础入门教程|go微服务开发必学教程