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-gogoctl env check --install --verbose --force安装
goctl插件安装
go-zerogo 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-gogoctl env check --install --verbose --force安装
goctl插件安装
go-zerogo 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微服务开发必学教程
