当前位置: 首页 > news >正文

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 

用户微服务

  • 编写rpcproto

// 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里面调用userrpc服务

    // 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中是端口号

img

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响应封装

不把codedatamsg写在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

img

但是,我发现这个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}
*/

img

img

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

img

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 -->

微服务

微服务是什么?

  • 大型应用分解成多个独立的组件

  • 针对每个独立服务的开发,部署,运营维护,扩展等都不影响其他服务

微服务衍生出很多组件:

  • 服务网关:确保服务提供者对客户端的透明,这一层可反向路由、安全认证、灰度发布、日志监控等前置动作

  • 服务发现:注册并维护远程服务以及服务提供者的地址,供服务消费者发现和调用,为保证可用性,如:etcdnacosconsul

  • 服务框架:用于实现微服务的RPC框架

  • 服务监控:对服务消费者与提供者之间的调用情况进行监控和数据展示,如:prometheus

  • 服务追踪:记录每个请求的微服务调用完整链路,以便进行问题定位和故障分析,如jeagerzipkin...

  • 服务治理:通过一系列手段来保证在各种意外情况下,服务调用扔能正常进行,这些手段包括熔断、隔离、限流、降级、负载均衡等,如:Sentinel

  • 基础:如分布式消息队列、日志存储、数据库、缓存、文件、服务器、搜索集群...

  • 分布式配置中心:统一配置,如:nacosconsulapllo...

  • 分布式事务:seatadtm...

  • 容器以及容器编排: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微服务开发必学教程

相关文章:

  • Movavi Photo Editor深度解析:图片分辨率提升与老照片修复神器
  • React 如何实现组件懒加载以及懒加载的底层机制
  • Linux学习——使用QEMU搭建ARM64环境
  • 【AI】基于多模态火车票数据提取
  • 【从零开始学习计算机科学】操作系统(六)内存管理
  • 卷积神经网络(笔记01)
  • leetcode:1629. 按键持续时间最长的键(python3解法)
  • Java 线程与线程池类/接口继承谱系图+核心方法详解
  • SpringBoot集成Swagger指南
  • 33.HarmonyOS NEXT NumberBox 步进器高级技巧与性能优化
  • 【时时三省】(C语言基础)赋值表达式和赋值语句和变量赋初值
  • TypeScript类:面向对象编程的基石
  • 关于 ESP32 未公开 Bluetooth® HCI 命令的事实澄清
  • [多线程]基于环形队列(RingQueue)的生产者-消费者模型的实现
  • c++20 Concepts的简写形式与requires 从句形式
  • 二叉树_3_模拟实现二叉树
  • PySide(PyQT),QGraphicsItem的pos()和scenePos()区别
  • 【数据分析大屏】基于Django+Vue汽车销售数据分析可视化大屏(完整系统源码+数据库+开发笔记+详细部署教程+虚拟机分布式启动教程)✅
  • Kotlin D3
  • 推理模型对SQL理解能力的评测:DeepSeek r1、GPT-4o、Kimi k1.5和Claude 3.7 Sonnet
  • 上海国际电影节纪录片单元,还世界真实色彩
  • 牛市早报|中方调整对美加征关税措施,五部门约谈外卖平台企业
  • 多地警务新媒体整合:关停交警等系统账号,统一信息发布渠道
  • 北京“准80后”干部兰天跨省份调任新疆生态环境厅副厅长
  • 这一次,又被南昌“秀”到了
  • 【社论】人工智能将为教育带来什么