Go架构初探索 | 青训营

46 阅读6分钟

什么是架构

架构也叫做软件架构,是指软件系统的顶层结构。它需要明确系统包含的个体,以及个体运作和协作的规则。

几种典型的架构

单机架构

单机架构非常简单,就是所有功能和服务包含在一个进程中,并部署在一台机器上。

但简单往往意味着在别的方面会有损耗,比如并发处理的问题,维护时候无法保证运行

单体架构

Dan Kegel于1999年在其个人站点提出了个经典问题——C10K problem,即如何处理1000万个并发连接,由此延申除了后续的单体架构。

单体架构时单机架构的改进,它采用分布式部署的模式,设置多台单机,全部的请求会经过分流后,一一转发给这些单机,由此做到负载均衡,维护不需要停服的效果。

不过所有功能依旧结合在一起,耦合度高,无法专注于某功能的开发。

垂直应用架构

此架构将系统按照服务拆分任务,分配给不同的服务器,从而实现职责的简单划分,提高开发和维护的效率,降低了耦合度,但不够彻底,仍然存在功能琐碎的问题。

SOA、微服务架构

SOA架构具有两个特性

  1. 将不同功能单元抽象为服务,从而细分职责
  2. 定义服务之间的通信标准,确保服务之间的数据流通

微服务架构则是SOA架构去中心化后的结果,旨在介绍服务之间的沟通消耗,进一步减少耦合,让服务更加明确。

不过微服务架构同时也出现了新的问题,比如数据一致性问题,成本问题,高可用问题等。

Go-Zero架构

这里主要介绍微服务架构中的go-zero架构,go-zero整体上做为一个稍重的微服务框架,提供了微服务框架需要具备的通用能力,同时也只带一部分的强约束,例如针对webrpc服务需要按照其定义的DSL的协议格式进行定义,日志配置、服务配置、apm配置等都要按照框架定义的最佳实践来走。

go-zero安装

go-zero需要安装goctl,是 go-zero 的内置脚手架,可以一键生成代码、文档等

# Go 1.16 及以后版本
go install github.com/zeromicro/go-zero/tools/goctl@latest

除此之外,还需要安装protocprotobuf的编译器),步骤如下

  1. github.com/protocolbuf…下载对应的protoc
  2. 下载之后解压就行,然后把bin文件夹里面的protoc.exe路径加入到环境变量
  3. protoc.exe,复制一份到gopath目录下的bin目录中
  4. 调用cmd,输入protoc,查看是否能够使用

除此之外,记得开启Go Modules,设置GOPROXY=https://goproxy.cn,direct

http服务

创建一个目录,叫zero-order并初始化go.mod

go mod init zero-order

在该目录下,新建一个文件order.api

info(
author: "技术小虫"
date: "2023-04-21"
desc: "订单api说明"
)
​
type (
OrderInfoReq {
OrderId int64 `json:"order_id"`
}
OrderInfoResp {
OrderId int64 `json:"order_id"` //订单id
GoodsName string `json:"goods_name"`  //商品名称
}
)
//定义了一个服务叫order-api
service order-api {
//获取接口的名字叫获取用户信息
@doc "获取订单信息"
//对应的hanlder即controller是orderInfo
@handler orderInfo
//请求方法是post,路径是/order/order_id,参数是OrderInfoReq,返回值是OrderInfoResp
post /order/info (OrderInfoReq) returns (OrderInfoResp)
//可以继续定义多个api
}
​
goctl api go -api order.api -dir ./  --style=goZero
​
#目录结构
zero-order
│  go.mod
│  go.sum
│  order.api
│  order.go     
├─etc
│      order-api.yaml
└─internal
    ├─config
    │      config.go  
    ├─handler
    │      orderInfoHandler.go
    │      routes.go  
    ├─logic
    │      orderInfoLogic.go 
    ├─svc
    │      serviceContext.go
    └─types
            types.go   

目录构建好后,导一下包,比如golandALT+Shift+Enter即可

我们从internal/handler/routes.go中的RegisterHandlers方法

追踪到internal/handler/monsterInfoHandler.go中的orderInfoHandler方法

追踪到internal/logic/orderInfoLogic.go中的OrderInfo方法

重构OrderInfo方法

func (l *OrderInfoLogic) OrderInfo(req *types.OrderInfoReq) (resp *types.OrderInfoResp, err error) {
    order_id:=req.OrderId
    resp=new(types.OrderInfoResp)
    resp.GoodsName="雪茄"
    resp.OrderId=order_id
return
}

其中order-api.yaml文件中定义了启动的端口号和ip,internal/handler/routes.go文件定义了路由。

使用go run order.go -f etc/order-api.yaml启动服务,使用默认端口8888。请求为order/info接口。

使用windows terminal请求

 Invoke-RestMethod -Uri "http://localhost:8888/order/info" -Method Post -Headers @{"Content-Type"="application/json";} -Body '{"order_id": 1}'

获得返回值

{
    "order_id":34,
    "goods_name":"雪茄"
}

rpc服务

通过goctl生成服务,在此之前,请确定已经安装protoc,否则后续会有乱码报错。

创建一个目录,叫two并初始化go.mod

go mod init two

在该目录下,新建一个文件two.proto,编写一个proto文件用于自定义微服务

syntax = "proto3";
package goods;
// protoc-gen-go 版本大于1.4.0, proto文件需要加上go_package,否则无法生成
option go_package = "./goods";
​
//定义请求体
message GoodsRequest {
int64 goods_id = 1;
}
//定义响应体
message GoodsResponse {
// 商品id
int64 goods_id = 1;
// 商品名称
string name = 2;
​
}
service Goods {
//rpc方法
rpc getGoods(GoodsRequest) returns(GoodsResponse);
//可以继续定义多个方法
}
goctl rpc protoc goods.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.
​
#目录结构
goods
│  go.mod
│  go.sum
│  goods.go
│  goods.proto  
├─etc
│      goods.yaml
├─goodsclient
│      goods.go    
├─internal
│  ├─config
│  │      config.go
│  ├─logic
│  │      getgoodslogic.go  
│  ├─server
│  │      goodsserver.go  
│  └─svc
│          servicecontext.go
└─types
    └─goods
            goods.pb.go
            goods_grpc.pb.go
            

目录构建好后,导一下包,比如golandALT+Shift+Enter即可

重构internal/logic/getgoodslogic.go中的GetGoods方法

func (l *GetGoodsLogic) GetGoods(in *goods.GoodsRequest) (res *goods.GoodsResponse, err error) {
    goodsId := in.GoodsId
    res = new(goods.GoodsResponse)
    res.GoodsId = goodsId
    res.Name = "茅台" + l.svcCtx.Config.ListenOn
    return
}

通过go run goods.go -f etc/goods.yaml启动rpc服务

安装etcd,下载https://github.com/etcd-io/etcd/releases合适的版本,解压,用控制台打开etcd.exe,启动rpc服务,再查询rpc是否在etcd中注册

get "goods" --prefix
​
two.rpc/7587872873255701764
192.168.1.25:8080

http调用rpc服务

需要暴露rpcproto文件,有三种方法

  1. 通过go.mod引用
  2. 通过git托管,然后包的方式引入
  3. 直接把文件拷贝到对应目录

我们这里采取方法1,在上面两个go.mod文件(api一个,rpc一个)中加入下列语句,这里one与two两个目录同级

replace goods => ../zero-goods
require goods v0.0.0

配置api文件(zero-order/etc/order-api.yaml

Name: order-api
Host: 0.0.0.0
Port: 8888
TwoRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: goods.rpc

引入config (zero-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
    GoodsRpc zrpc.RpcClientConf
}

加载svc文件(zero-order/internal/svc/serviceContext.go

package svc
​
import (
    "github.com/zeromicro/go-zero/zrpc"
    "goods/goodsclient"
    "zero-order/internal/config"
)
​
type ServiceContext struct {
    Config config.Config
    //定义rpc类型
    Goods goodsclient.Goods
}
​
func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config: c,
        //引入gprc服务
        Goods: goodsclient.NewGoods(zrpc.MustNewClient(c.GoodsRpc)),
    }
}
​

logic调用rpc(zero-order/internal/logic/orderInfoLogic.go

package logic
​
import (
    "context"
    "goods/types/goods"
    "zero-order/internal/svc"
    "zero-order/internal/types""github.com/zeromicro/go-zero/core/logx"
)
​
type OrderInfoLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}
​
func NewOrderInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderInfoLogic {
    return &OrderInfoLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}
​
func (l *OrderInfoLogic) OrderInfo(req *types.OrderInfoReq) (resp *types.OrderInfoResp, err error) {
    orderId := req.OrderId
    goodRequest := new(goods.GoodsRequest)
    goodRequest.GoodsId = 25
    goodsInfo, err := l.svcCtx.Goods.GetGoods(l.ctx, goodRequest)
    if err != nil {
        return nil, err
    }
    resp = new(types.OrderInfoResp)
    resp.GoodsName = goodsInfo.Name
    resp.OrderId = orderId
    return
}
​

修改zero-goods/goodsclient/goods.go

// Code generated by goctl. DO NOT EDIT.
// Source: goods.protopackage goodsclient
​
import (
    "context""goods/types/goods""github.com/zeromicro/go-zero/zrpc"
    "google.golang.org/grpc"
)
​
type (
    GoodsRequest  = goods.GoodsRequest
    GoodsResponse = goods.GoodsResponse
​
    Goods interface {
        // rpc方法
        GetGoods(ctx context.Context, in *GoodsRequest, opts ...grpc.CallOption) (*GoodsResponse, error)
    }
​
    defaultGoods struct {
        cli zrpc.Client
    }
)
​
func NewGoods(cli zrpc.Client) Goods {
    return &defaultGoods{
        cli: cli,
    }
}
​
// rpc方法
func (m *defaultGoods) GetGoods(ctx context.Context, in *GoodsRequest, opts ...grpc.CallOption) (*GoodsResponse, error) {
    client := goods.NewGoodsClient(m.cli.Conn())
    return client.GetGoods(ctx, in, opts...)
}
​

启动

依次启动etcd ,rpc和api,并请求api

 Invoke-RestMethod -Uri "http://localhost:8888/order/info" -Method Post -Headers @{"Content-Type"="application/json";} -Body '{"order_id": 1}'

得到结果

{
    "order_id":34,
    "goods_name":"茅台"
}