简介
项目架构来源于go-zero实战:让微服务Go起来。此对该项目有所删减,相对简单适合初学者。
省去了项目中每个服务占用独立docker的过程,省略了docker-compose的构建过程。每个服务是一个独立的程序不依赖与容器。
环境搭建
- 安装goctl
go install github.com/zeromicro/go-zero/tools/goctl@latest
- 安装protoc
goctl env check --install --verbose --force
- 安装go-zero
go get -u github.com/zeromicro/go-zero@latest
- 生成api标准api服务
goctl api new apiservice
- 生成rpc服务
goctl rpc new rpcservice
生成代码后
go mod tidy
下载所需依赖。
在apiservice
目录下的apiservicelogic.go
的27
行后修改为如下图所示代码:
删除apiservice
目录下etc
下的配置文件,在主程序中做如何修改如下修改,直接配置方便一些:
func main() {
var c config.Config
c.Host = "0.0.0.0"
c.Port = 8000
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
api服务统一为8000系列的端口。
在rpcservice
目录下的yaml配置文件端口修改为9000系。主函数做如下修改:
func main() {
var c config.Config
c.ListenOn = "0.0.0.0:9000"
c.Mode = "dev"
ctx := svc.NewServiceContext(c)
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
rpcservice.RegisterRpcserviceServer(grpcServer, server.NewRpcserviceServer(ctx))
if c.Mode == service.DevMode || c.Mode == service.TestMode {
reflection.Register(grpcServer)
}
})
defer s.Stop()
fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
s.Start()
}
启动程序
浏览器访问
rpc客户端访问
新项目复制两个pb文件,编写客户端主程序。
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"rpcclient/rpcservice"
)
func main() {
//配置连连接参数(无加密)
dial, _ := grpc.Dial("localhost:9000", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer dial.Close()
//创建客户端连接
client := rpcservice.NewRpcserviceClient(dial)
//通过客户端调用方法
res, _ := client.Ping(context.Background(), &rpcservice.Request{Ping: "xiaoxu"})
fmt.Println(res.Pong)
}
go-zero api服务构建
整合mysql数据库
构建如项目上的4个服务。4个服务都是上一节构建基本项目为基础的gitee地址
整合数据库就不再过多赘述了,整合xorm框架,返回数据库引擎即可,如下:
xorm实战——结构体映射到实现数据库操作
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
)
var Engine *xorm.Engine
func init() {
var err error
e, err := xorm.NewEngine("mysql", "root:root@/zerotest?charset=utf8")
if err != nil {
fmt.Println("数据库连接失败!")
}
// err的错误处理
Engine = e
}
构建mysql数据库后在其他包下通过库名.Engine
即可使用数据库引擎。
创建数据库
CREATE TABLE `order` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`order_id` varchar(100) NOT NULL COMMENT '订单编号',
`type` tinyint DEFAULT NULL COMMENT '订单类型',
`customer_id` int DEFAULT NULL COMMENT '用户编号',
`amount` int DEFAULT NULL COMMENT '数量',
`payment` tinyint DEFAULT NULL COMMENT '支付方式',
`status` tinyint DEFAULT NULL COMMENT '状态',
`create_time` varchar(100) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';
控制器逻辑实现
新建logic目录
创建订单逻辑类
type orderLogic struct{}
var OrderLogic orderLogic
// 创建订单
func (this orderLogic) Create(param models.Order) error {
param.Id = 0
_, err := db.Engine.Insert(param)
if err != nil {
fmt.Printf("logic module create err:%v", err)
return err
}
return nil
}
//db是构建服务器引擎的包
控制器调用逻辑代码
// create
func OrderCreateController() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
//获取请求参数
var req models.Order
err := httpx.ParseJsonBody(r, &req)
if err != nil {
//fmt.Printf("ordercontoller err:%v", err)
httpx.WriteJson(w, 500, fmt.Sprintf("ordercontoller err:%v", err))
return
}
//******************//
err = orderlogic.OrderLogic.Create(req)
//******************//
if err != nil {
//fmt.Printf("order create err:%v", err)
httpx.WriteJson(w, 500, fmt.Sprintf("order create err:%v", err))
return
}
httpx.OkJson(w, map[string]string{"code": "200", "message": "插入成功!"})
}
}
Create
方法不使用结构体模拟类,直接是一个函数也是可以的,但是函数就不能重名了,在微服务中可以为单个函数,在单纯的web服务中还是用类实现比较好。
部分代码:
// create
func OrderCreateController() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
//获取请求参数
var req models.Order
err := httpx.ParseJsonBody(r, &req)
if err != nil {
//fmt.Printf("ordercontoller err:%v", err)
httpx.WriteJson(w, 500, fmt.Sprintf("ordercontoller err:%v", err))
return
}
err = orderlogic.OrderLogic.Create(req)
if err != nil {
//fmt.Printf("order create err:%v", err)
httpx.WriteJson(w, 500, fmt.Sprintf("order create err:%v", err))
return
}
httpx.OkJson(w, map[string]string{"code": "200", "message": "插入成功!"})
}
}
// update
func OrderUpdateController() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req models.Order
err := httpx.ParseJsonBody(r, &req)
if err != nil {
httpx.WriteJson(w, 500, fmt.Sprintf("update err:%v", err))
return
}
err = orderlogic.OrderLogic.Update(req)
if err != nil {
//...
return
}
httpx.OkJson(w, map[string]string{"code": "200", "message": "更新成功!"})
}
}
//Remove
func OrderRemove() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req models.Order
err := httpx.ParseJsonBody(r, &req)
if err != nil {
httpx.WriteJson(w, 500, fmt.Sprintf("remove err%v", err))
return
}
err = orderlogic.OrderLogic.Remove(req)
if err != nil {
//...
return
}
httpx.OkJson(w, map[string]string{"code": "200", "message": "删除成功!"})
}
}
//List
func OrderList() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, err := orderlogic.OrderLogic.List()
if err != nil {
//...
return
}
httpx.OkJson(w, res)
}
}
逻辑层直接省略了,就是xorm对mysql的CURD操作。
注册路由
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/from/:name",
Handler: ApiserviceHandler(serverCtx),
},
//自定义路由
{
Method: http.MethodPost,
Path: "/create",
Handler: OrderCreateController(),
},
{
Method: http.MethodPost,
Path: "/update",
Handler: OrderUpdateController(),
},
{
Method: http.MethodPost,
Path: "/remove",
Handler: OrderRemove(),
},
{
Method: http.MethodPost,
Path: "/list",
Handler: OrderList(),
},
},
)
}
综上所属,已经成功的构建了一个web服务,包括对订单order的基本操作。
如果不想手写这莫多代码,可以看看zero的api语法,直接使用api文件一键生成更加方便。
go-zero rpc服务构建
在rpc远程调用中主要用于rpc服务器注册本地的方法,实现服务器之间的内部调用。在上述服务中,都是沿用了插件生成的目录,仅修改了部分配置文件。可以参考如下连接的目录结构层次清晰。
go-zero实战
删除旧的rpc服务的所有目录,保留proto文件,添加如下内容:
syntax = "proto3";
package rpcservice;
option go_package="./rpcservice";
message Request {
string ping = 1;
}
message Response {
string pong = 1;
}
service Rpcservice {
rpc Ping(Request) returns(Response);
// 自定义方法区
rpc Create (Request) returns (Response);
rpc Update (Request) returns (Response);
rpc Remove (Request) returns (Response);
rpc Detail (Request) returns (Response);
rpc List (Request) returns (Response);
}
注意这里不再是goctl rpc new [name]
了,该命令是一键生成rpc标准服务命令,而需要自定义rpc服务即根据编写的.proto
文件生成服务需要使用protobuf
插件命令。
protoc-gen-go,protoc-gen-go-grpc
这两个插件在grpc服务中一般是需要独立安装的,但是在go-zero中goctl
集成了这两个插件。
protoc --go_out=. *.proto
protoc --go-grpc_out=. *.proto
以下是自定义rpc服务部分,goctl生成直接跳过
运行完指令后在proto文件指定的目录生成了grpc服务源码,如下
注意如果之前已经使用了goctl rpc命令,那么目录下不止有这两个文件,建议删除goctl生成的文件自定义构建,因为许多用不着到。goctl生成的代码主要是结合了zrpc.RpcServerConf
的配置,即config目录下的对象,如下:
在goctl生成的代码中也是支持flag
库的,如下,这里后续将会改成静态的省去配置文件。
goctl生成部分
通过两个命令生成rpc服务文件,如下:
在生成的代码中已经具备了逻辑层的方法,如下:
修改主程序注释调用
flag
参数获取功能,原因是api服务和rpc服务中都有独立的yaml文件这是不合理的,两个flag参数获取存在冲突,所以将两个都注册掉改为静态配置,后续可以改为一个配置文件。
//注释掉flag获取参数的部分
func main() {
flag.Parse()
var c config.Config
// conf.MustLoad(*configFile, &c)
c.ListenOn = "0.0.0.0:9000"
c.Name = "order.rpc"
ctx := svc.NewServiceContext(c)
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
rpcservice.RegisterRpcserviceServer(grpcServer, server.NewRpcserviceServer(ctx))
if c.Mode == service.DevMode || c.Mode == service.TestMode {
reflection.Register(grpcServer)
}
})
defer s.Stop()
fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
s.Start()
}
修改rpc服务的逻辑如下所示:
编写客户端访问rpc服务
import (
"context"
"fmt"
"rpcclient/rpcservice"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
//配置连连接参数(无加密)
dial, _ := grpc.Dial("localhost:9000", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer dial.Close()
//创建客户端连接
client := rpcservice.NewRpcserviceClient(dial)
//通过客户端调用方法
res, err := client.Ping(context.Background(), &rpcservice.Request{ReqJson: "xiaoxu"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(res)
}
成功访问,ping的案例是goctl已经实现了的,虽然早proto中编写了自定义的方法,如下
但是却仍然无法调用,回报未继承的错误,在客户端加入如下代码:
//order list
r, err := client.List(context.Background(), &rpcservice.Request{})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(r.ResJson)
List not implemented
该错误的原因时虽然存在该方法名,但方法没有方法体,也就说函数没有将处理逻。
在服务端通过反射
获取到Rpcservice_Ping_FullMethodName
的Ping方法,也就是/rpcservice.Rpcservice/Ping
的Ping方法。如下图
该方法就是生成文件的server目录下包的方法,如下:
官方提供的方法ping
调用了生成的logic的ping方法,其实就是实现了逻辑的解耦,将逻辑功能分隔开来。如下所示,logic层部分只写逻辑处理,在serve下调用逻辑处理部分函数。
当然不论如何解耦,核心还是server目录的文件,必须在此处注册逻辑函数,才可以在rpc武器生成函数实例,客户端才可以成功调用。
该方法实际上也是proto中定义的方法的一个重写过程,是接口的是实现。
注册自定义函数
//list 继承
func (s *RpcserviceServer) List(ctx context.Context,in *rpcservice.Request) (*rpcservice.Response, error) {
//r, err := logic.List(in)
o, err := orderlogic.OrderLogic.List()
if err != nil {
fmt.Printf("rpc err:%v", err)
return &rpcservice.Response{}, err
}
//o 赚json字符串
josnstr, _ := json.Marshal(o)
return &rpcservice.Response{ResJson: string(josnstr)}, nil
}
上面代码逻辑部分一起写在rpc方法注册的函数中,当逻辑代码多是就会十分冗余,最好将逻辑部分提取出来封装在新函数中,在注册是调用新方法即可,想官方提供的模板一样。
服务端注册成功后,服务端调用,代码如下:
package main
import (
"context"
"fmt"
"rpcclient/rpcservice"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
//配置连连接参数(无加密)
dial, _ := grpc.Dial("localhost:9000", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer dial.Close()
//创建客户端连接
client := rpcservice.NewRpcserviceClient(dial)
//通过客户端调用方法
res, err := client.Ping(context.Background(), &rpcservice.Request{ReqJson: "xiaoxu"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(res)
//order list
r, err := client.List(context.Background(), &rpcservice.Request{})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(r.ResJson)
}
别忘了克隆
_grpc.pb
和pb
文件。
到此服务order已经可以同时提供api服务和rpc服务了。
gitee地址:https://gitee.com/fireapproval/xiaoxu/tree/xiaoxu/go/go-zero-test