Go Micro Restful Service Demo

本文介绍如何从零开始,使用 Go 语言的一些有代表性的框架,实现一个微服务架构的 Restful Web Service。
由于本人没有 Mac 电脑(因为穷),所以本文的所有环境配置和测试、运行都在 Windows 10 环境下。

本文涉及的主要框架及环境:

  • go。go语言库
  • JetBrians GoLand。IDE
  • gin。Go 的一款常用 Web 框架,类似于 SpringMVC
  • go-micro。Go 的一款微服务框架,类似于 SpringCloud/Dubbo
  • gorm。Go 的一款 ORM 框架,从设计风格上有点类似于 Hibernate/MyBatis 的混合。
  • go-protobuf。protobuf 的 go 语言版本
  • consul。一个类似 zookeeper 的服务治理软件,可以参看这边文章。这里不总结原理和环境配置,只作为工具使用。

Go 的初体验

Go 的环境配置

去 Go 官网 下载最新版本的 Go 编译版本安装包,然后运行,源码安装请见其它博客。
安装程序会自动设置部分但非全部环境变量,所以为了保险起见,检查并设置以下所有环境变量。

  • GOROOT
    安装时候的根路径。
  • GOBIN
    %GOROOT%\bin
    //这个环境变量是安装时候自己设置的,但好像不设置也问题不大。
  • Path
    在 Path 里添加
    %GOROOT%\bin
  • GOPATH
    这个是 Go 的工作空间,可以参看这篇文章。个人理解,Go 相当于集成了 Maven 和 JDK,依赖管理、编译等等都无需额外的软件了。
    变量值是个合法的文件夹的路径就行,比如 C:\Users\Cachhe\go。

GoLand 设置

  1. 配置代理。针对公司的内网环境,在 File->Settings->Apearance & Behavior->System Settings->HTTP Proxy,配置 Manual proxy configuration,具体看公司给的代理配置。
  2. 安装 Protobuf 的插件,为了好看。在 File->Settings->Plugins里,搜索 Protobuf Support,安装完后要重启。

测试

新建一个工程,在根目录下新建一个 test.go,操作方式与 IDEA 差不多,这里不介绍具体的操作了。
默认创建出来的 go 文件,package 名与文件夹名一致,如果我们的工程名叫 xg-temp,GoLand 初始化了一个 xg-temp 文件夹,那么 test.go 的 package 名也就是 xg-temp。为了让它可执行,需要改为 main。这是 go 的约定。


Go Micro Restful Service Demo_第1张图片

如上图,执行,即可在控制台看见输出。

项目背景

假设我们有一款云产品,我们希望提供 CRUD 功能。

整体设计

整体上采用微服务架构,一般来说,一个工程由前端、api模块(其实也是一个服务)、多个服务组成。这里我们只关注后台的逻辑,同时为了简单,也只包括一个服务。
单个模块采用 MVC 的结构。从上往下开发,先确定与前端的交互协议,序列化和反序列化用 protobuf 来做,所以又要定义 proto。WEB控制层通过 gin 来做,Service 与业务相关,DAO 层通过 GORM 来做。
项目结构如图:


Go Micro Restful Service Demo_第2张图片

可见,项目 xg-temp 由 api 和 service 两个模块构成,分别对应 api 模块 和一个服务。

  • api
    api.go 是程序的入口,负责启动 api 模块。
    client api 模块虽然对前端体现的是“后端”的作用,但是对于下游的服务仍然体现的是“客户端”的作用。请求到了 api 后需要调下游服务来处理,虽然暴露出去的一般是 Restful 接口,但是系统内部一般用更方便的 rpc 来交互。
    handler handler 里面定义了控制层的逻辑,包括路径映射、请求转发和返回
    proto proto 里放 proto 文件,里面定义类似于 Java 的 POJO,可以通过程序自动生成 go 文件和 微服务需要的文件。这里因为只是 demo 就没有放。
    apitest.http JetBrains 提供的一个非常方便的 HTTP 测试工具,可以非常方便地写 HTTP 请求
  • service
    common 放公共基础库,比如配置文件
    handler 具体业务逻辑,包括操作数据库
    proto 同上
    config.yaml 配置文件,springboot 也是用的这个
    service.go service 模块的程序入口

安装必要的软件及依赖包

安装 consul
consul 的下载地址,Windows 下的 consul 是一个打包好的 .exe,打开 shell,执行

consul.exe agent -dev

看见类似下面的输出,即代表启动了


Go Micro Restful Service Demo_第3张图片

安装 protobuf
protobuf 的 GitHub releases 里有 Windows 版本的二进制文件,下载下来后,解压,将 protobuf 的 bin 目录添加到 Path 环境变量里。

安装 go-micro 及一些工具

# -u 简单来说就是递归下载每个库所依赖的库,不加只会下载这个库,但是 -u 不会更新已经有的库,就算发现新版本
# -v 会在 shell 里输出下载了哪些,方便看哪出问题了
# consul client
go get -u github.com/hashicorp/consul
# micro 核心库
go get -u github.com/micro/micro
# 根据 protoc 生成 micro 的代码
go get -u github.com/micro/protoc-gen-micro
# protobuf 核心库
go get -u github.com/golang/protobuf/proto
# 一个用 go 写的 protobuf 生成软件
go get -u github.com/golang/protobuf/protoc-gen-go

如果遇到下载不下来的情况下需要用代理,一般是因为被墙了。还是下载不了,就得通过 git 直接下载到 GOPATH 里。见 这篇文章。

Api 模块

routers.go

routers 里定义路径映射,即不同的 URL、不同的请求方法怎样处理。对比 SpringMVC 中的 Controller,非常好理解。

package handler

import "github.com/gin-gonic/gin"

func NewWebService() *gin.Engine {
    router := gin.Default() // 相当于一个 http-server

    v1 := router.Group("/v1") 
    // 路径分组,也就是所有访问 /v1/* 都会被这个 group 拦截 

    app := v1.Group("/app") 
    // 同上。v1 表示 api 的版本是 1,app 上的操作都被拦截到这个 group 上进行处理
    appManager := new(AppManager) 
    // 定义了一个类,这样做完全是出于工程考虑,对 app 的请求都在这个接收者的方法上进行处理,避免混乱
    app.POST("/create", appManager.CreateApp) 
    // 对 create 路径的 POST 请求由 appManager 上的 CreateApp 方法处理,CreateApp 接收 gin.Context 参数,这个参数中可以获取当前请求的信息和一些其它信息
    app.POST("/delete", appManager.DeleteApp)
    app.POST("/update", appManager.UpdateApp)
    app.POST("/query", appManager.QueryApp)

    product := v1.Group("/product")
    pdtManager := new(ProductManager)
    product.POST("/create", pdtManager.CreateProduct)
    product.POST("/delete", pdtManager.DeleteProduct)
    product.POST("/update", pdtManager.UpdateProduct)
    product.POST("/query", pdtManager.QueryProduct)

    return router
}

app_manager.go

app 相关的控制层逻辑,也可以直接写在 routers 里,但是这样更软件工程。product_manager.go 也是一样,这里就不解释了。
代码只实现了一个,其它的类似,不外乎更复杂的业务逻辑。

package handler

import (
    "github.com/gin-gonic/gin"
    "xg-temp/api/client"
    appProto "xg-temp/service/proto"
)

type AppManager struct {}

func(a *AppManager) CreateApp(c *gin.Context) {
    var mobileAppCreateRequest appProto.MobileApplication // 声明一个反序列化后的接收对象
    if err := c.ShouldBindJSON(mobileAppCreateRequest); err != nil {
     // bind 可以很方便地从 URL query params 里或者 requestBody 里反序列化出对象,具体用法请查看相关文档
        resp := new(appProto.AppResponse)
        resp.ErrMsg = "请求错误"
        resp.RetCode = 400
        c.JSON(400, resp) // 出错了,可能是请求参数不对,返回状态码 400,body 部分是 JSON 格式的 resp
        c.Abort() // 执行完当前 handler 后就不再执行后续的 handler 了。
    // 这个有点类似于 Java Struts 中的拦截器/Filter,框架允许类似 pipe 一样注册多个 handler 来处理请求,比如日志、鉴权
        return
    }
    // 通过 rpc 调用下游服务的 CreateAppInfo 并获取返回值
    resp := client.CreateAppInfo(mobileAppCreateRequest)

    c.JSON(200, resp)
}

func(a *AppManager) DeleteApp(c *gin.Context) {
    c.JSON(200, gin.H{ // gin.H 是个很方便的生成 JSON 的方法
        "error": false,
        "data": "你好啊 朋友",
    })
}

func(a *AppManager) UpdateApp(c *gin.Context) {
    c.JSON(200, gin.H{
        "error": false,
        "data": "你好啊 朋友",
    })
}

func(a *AppManager) QueryApp(c *gin.Context) {
    c.JSON(200, gin.H{
        "error": false,
        "data": "你好啊 朋友",
    })
}

api.go

api go

package main // main 是 go 中间一个特殊的 package,表示是程序入口,不然的话就算有 main 方法还是不能执行

import (
    "fmt"
    "github.com/micro/go-micro/registry"
    "github.com/micro/go-micro/registry/consul"
    "github.com/micro/go-micro/web"
    "time"
    "xg-temp/api/client"
    "xg-temp/api/handler"
)
var (
    listenPort = "localhost:8080" // Restful 接口的 IP 和端口
)

func main()  {
    serviceName := client.API_SERVICE_NAME
    serviceVersion := "latest"

    fmt.Printf("start service:%s version:%s", serviceName, serviceVersion)

    // 连接本地 consul client
    // registry.Option 是一个函数,这个函数接受 Options 为入参; Options 是一个 stuct,里面有 Addr, Timeout 等字段,
    // 其中 Addr 为 host:port 格式,是 consul 服务的监听地址,默认的情况下是本地的 8500 端口,这个可以在前面启动 consul 服务的时候设置、看见
    // 这个设计在 Java/Android 的回调方法设计中很常见
    opts := registry.Option(
        func(opts *registry.Options) {
            opts.Addrs = []string {fmt.Sprintf("%s:%d", "127.0.0.1", 8500)}
    })

    // 注册 webservice 到 consul
    // consul 一般是 service.NewService,web 是一种特别的 service
    service := web.NewService(
        web.Name(serviceName),
        web.Address(listenPort), // web 服务的地址
        web.RegisterTTL(time.Second * 10),
        web.RegisterInterval(time.Second * 5),
        web.Registry(consul.NewRegistry(opts))) // consul 的 Options,包括 consul 的地址
    _ = service.Init()

    // 初始化控制层
    gin := handler.NewWebService() // 拿到一个 gin Engine

    // 初始化 micro service
    client.InitApiService()

    // 绑定 gin 到 "/" 路径上处理所有请求
    service.Handle("/", gin)

    // 启动 service
    if err := service.Run(); err != nil {
        println(err) // 出错了
    }
}

测试

先将前面涉及 client 的代码注释掉,那部分是调 rpc 的,我们还没写。然后 run api.go,这个时候我们是无法访问 create 接口的,但是其它接口是可以的。在 apitest.http 里定义 HTTP 请求,运行


Go Micro Restful Service Demo_第4张图片
image.png

client.go

client.go 暂时没法写,因为一般是先有 service 接口前端才能调,所以我们先切换到 service 模块,看看怎么继续做。

Service 模块

Service 主要处理两件事,暴露给前端 RPC 接口,执行具体的业务逻辑。一般情况下,我们需要自己去定义接口,然后与 RPC 框架绑定起来,然后再实现这些接口。proto 提供了更为方便的途径,我们只需要在 proto 文件里声明 Service 和 数据结构,然后就可以自动生成对应的数据结构定义(类似 POJO/Entity) 和 微服务代码,我们只需要去实现一个个接口就行了。

在 Java 中,自动生成类已经是很常见的框架功能,比如 MyBatis-Generator/GreenDao,但 Proto 功能更强大。Java 中的 RPC 一般是通过动态代理来做的,go 似乎没这个功能,所以稍微比 Java 的 RPC 框架用起来麻烦一些。

proto

首先来看 app.proto

syntax = "proto3"; // 语法版本

package xgtmp.srv.app; // 生成的 go 文件会是怎样的 package name

// 定义 RPC 接口
service App {
    // 创建APP信息
    rpc CreateAppInfo (MobileApplication) returns (AppResponse) {}
    // 更新APP信息
    rpc UpdateAppInfo (MobileApplication) returns (AppResponse) {}
    // 删除App信息
    rpc DeleteAppInfo (MobileApplication) returns (AppResponse) {}
    // 查询APP信息
    rpc QueryAppInfo (AccessId) returns (AppResponse) {}
}
// 类似于声明一个类
message AccessId {
    uint32 accessId = 1; // app 的ID,后面的数字需要唯一,是在序列化/反序列化时候的顺序
}

message MobileApplication {
    string appName = 1;    // app名称
    string otherInfo = 2;  // 其它信息
}

message AppResponse {
    string errMsg = 1;
    int32 retCode = 2;
    MobileApplication app= 3; // 意思是 AppResponse 里面会包含一个 MobileApplication 的值,
    // 这里也可以像下面一样用引用,虽然序列化/反序列化的时候都是 deep-copy,但是其它代码中的行为会不一样,比如赋值/读值
    // MobileApplicaion* app = 4;
}

定义完了后,打开 bash,进入 app.proto 所在目录,执行以下命令,应该会额外生成两个文件,app.pb.go 是数据结构,app.micro.go 是 RPC 接口。

protoc --micro_out=. --go_out=. app.proto
# --micro_out=. 表示在当前目录下生成 micro 文件
# --go_out=. 表示在当前目录下生成 pb 文件
# 常用的还有个 --proto_path,当当前的 proto 文件里 import 了不在当前路径的其它 proto 文件时,就需要额外指明,比如
protoc --proto_path=$GOPATH/src:. --micro_out=. --go_out=. greeter.proto
# 表示搜索 $GOPATH/src 目录及其子目录。亲测 windows 下这个 / 符号会导致出错,:. 也会,不清楚为什么。

看一下 app.pb.go 里的部分关键代码,其它方法被省略了。

type MobileApplication struct {
    AppName              string   `protobuf:"bytes,1,opt,name=appName,proto3" json:"appName,omitempty"`
    OtherInfo            string   `protobuf:"bytes,2,opt,name=otherInfo,proto3" json:"otherInfo,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

主要是反引号中间的部分。这部分定义了 protobuf 基于字节做序列化和反序列化时候的元数据,以及 json 时候的元数据,后面的 GORM 也是在这里定义。这与 Java 的注解很像。

再来看一下 app.micro.go 里的部分关键代码。

// Client API for App service
// 这部分代码是给 RPC 客户端的,在我们的项目中,也就是 api 模块里的 client 里可以调用的接口。

type AppService interface {
    // 创建APP信息
    CreateAppInfo(ctx context.Context, in *MobileApplication, opts ...client.CallOption) (*AppResponse, error)
    // 更新APP信息
    UpdateAppInfo(ctx context.Context, in *MobileApplication, opts ...client.CallOption) (*AppResponse, error)
    // 删除App信息
    DeleteAppInfo(ctx context.Context, in *MobileApplication, opts ...client.CallOption) (*AppResponse, error)
    // 查询APP信息
    QueryAppInfo(ctx context.Context, in *AccessId, opts ...client.CallOption) (*AppResponse, error)
}

type appService struct {
    c    client.Client
    name string
}

/// 获取一个 Service 实例
func NewAppService(name string, c client.Client) AppService {
    if c == nil {
        c = client.NewClient()
    }
    if len(name) == 0 {
        name = "xgtmp.srv.app"
    }
    return &appService{
        c:    c,
        name: name,
    }
}

// Server API for App service
// 这部分是服务需要实现的接口,客户端调用上面的接口,rpc 到了服务端后,会实际调用下面的接口,一一对应。
type AppHandler interface {
    // 创建APP信息
    CreateAppInfo(context.Context, *MobileApplication, *AppResponse) error
    // 更新APP信息
    UpdateAppInfo(context.Context, *MobileApplication, *AppResponse) error
    // 删除App信息
    DeleteAppInfo(context.Context, *MobileApplication, *AppResponse) error
    // 查询APP信息
    QueryAppInfo(context.Context, *AccessId, *AppResponse) error
}
// 服务端调用这个方法把一个 micro server 实例绑定到实现了 AppHandler 接口的 struct 上
func RegisterAppHandler(s server.Server, hdlr AppHandler, opts ...server.HandlerOption) error {
// 首先里面定义了一个私有的内部接口 app,App 继承 app。
    type app interface {
        CreateAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error
        UpdateAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error
        DeleteAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error
        QueryAppInfo(ctx context.Context, in *AccessId, out *AppResponse) error
    }
    type App struct {
        app
    }
    // NewHandler 内部做了大量的工作,通过反射创建了所需的元数据
    h := &appHandler{hdlr}
    return s.Handle(s.NewHandler(&App{h}, opts...))
}
// appHandler 继承 AppHandler
type appHandler struct {
    AppHandler
}
// micro 的框架会调用 appHandler 的 这些方法,这些方法内部本身并没有定义什么内容,
// 而是直接去调 AppHandelr 里的对应实现,而这些实现在上面的 RegisterAppHandler 里由传入的参数指定。
func (h *appHandler) CreateAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error {
    return h.AppHandler.CreateAppInfo(ctx, in, out)
}

func (h *appHandler) UpdateAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error {
    return h.AppHandler.UpdateAppInfo(ctx, in, out)
}

func (h *appHandler) DeleteAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error {
    return h.AppHandler.DeleteAppInfo(ctx, in, out)
}

func (h *appHandler) QueryAppInfo(ctx context.Context, in *AccessId, out *AppResponse) error {
    return h.AppHandler.QueryAppInfo(ctx, in, out)
}

handler 模块

看完了 proto 的生成文件可能还是一头雾水,不知道接下来怎么做,不要慌。
客户端需要调用 RPC 服务,首先得声明要调用哪个服务,通过服务治理软件,也就是 consul 拿到具体得 ip:port,然后声明调用哪个接口,最后拿到返回结果。

在 api 模块里定义 rpc_client.go

package client

import (
    "context"
    "fmt"
    goMicro "github.com/micro/go-micro"
    appProto "xg-temp/service/proto"
)

var apiService goMicro.Service // 一个全局变量保存初始化后的实例

var API_SERVICE_NAME = "xgtmp.srv.api"
var APP_SERVICE_NAME = "xgtmp.srv.app"

// 类似于静态函数,返回一个实例值。这里的单例与否需要上层来保证
// 这里是创建 api 服务,也就是 api web service。
func InitApiService() {
    apiService = goMicro.NewService(
        goMicro.Name(API_SERVICE_NAME),
        goMicro.Version("latest"))
    apiService.Init()
}

// 将本地调用,转换成 rpc 调用。如果数据结构不一致,这里会需要做数据结构的转换,包括传给 rpc 调用的参数,以及收到的返回结果。
func CreateAppInfo(mobileAppCreateRequest appProto.MobileApplication) * appProto.AppResponse {
    // 声明要找哪个 RPC 服务,以及绑定到哪个 client 数据结构上
    // apiService 做发起 rpc 请求的一方,所以是 client
    appService := appProto.NewAppService(APP_SERVICE_NAME, apiService.Client())
    fmt.Printf("Create RPC client for : %v", mobileAppCreateRequest)
    // 实际调用 rpc
    resp, err := appService.CreateAppInfo(context.TODO(), &mobileAppCreateRequest)
    if err != nil {
        println(err.Error())
    }
    return resp
}

服务端的 interface 已经定义好了,需要实现成具体的业务逻辑。go 里面的 OOP 很抽象,完全不像 Java 那样清晰明显。实现一个接口,只需要包含所有函数且函数的名称、入参和出参完全一致,就算实现了这个接口了。

定义一个 dao.go 文件,起名起的不好,因为这里应该不只是 db 上的操作的,更应该是 service 层的东西。

type AppDao struct {
    DB *gogorm.DB // 数据库镜像实例,类似于 SessionFactory
}

func (d *AppDao) CreateAppInfo(ctx context.Context, in *proto.MobileApplication, out *proto.AppResponse) error {
    fmt.Println("This is Cachhe and we are notified")
    // 模拟业务处理
    in.OtherInfo = "设置其它属性"
    err := d.DB.Create(in).Error // 插入一条数据
    if err != nil {
        out.ErrMsg = err.Error()
        out.RetCode = 500
        println(err)
    } else {
        out.ErrMsg = "没有错误啦"
        out.RetCode = 200
        out.App = in
    }
    // additional methods
    return nil
}

func (AppDao) UpdateAppInfo(context.Context, *proto.MobileApplication, *proto.AppResponse) error {
    panic("implement me")
}

func (AppDao) DeleteAppInfo(context.Context, *proto.MobileApplication, *proto.AppResponse) error {
    panic("implement me")
}

func (AppDao) QueryAppInfo(context.Context, *proto.AccessId, *proto.AppResponse) error {
    panic("implement me")
}

d.DB.Create(in) 能执行成功,必须要先有数据库、表、和数据库连接。在 Java 里,我们需要配置数据源、连接池等等,go 里面也是一样。

在 service 的根目录创建一个 config.yml 文件

mysql:
  hostname: ""
  port: 3306
  user: "root"
  password: "123456"
  database: "xg"

在 common 目录里创建 config.go

package common

import (
    "gopkg.in/yaml.v2"
    "io/ioutil"
    "os"
    "sync"
)

var m *AppConfig
var once sync.Once // go 的并发包 sync 里的工具类,保证某个操作只能执行一次

type AppConfig struct {
    MySql struct{
        User string `yaml:"user"` // yaml 的元数据,定义了怎么解析 yaml 文件
        Host string `yaml:"hostname"`
        Port string `yaml:"port"`
        Password string `yaml:"password"`
        DBName string `yaml:"database"`
    }
}

// 单例模式
func CfgInstance() *AppConfig {
    once.Do(func() {
        m,_ = loadConf()
    })
    return m
}

func loadConf() (*AppConfig, error) {
    localConfPath := "C:\\Users\\CocoAdapter\\go\\src\\xg-temp\\service\\config.yaml"

    conf := &AppConfig{}
    yamlFile, err := ioutil.ReadFile(localConfPath)
    if err != nil {
        println("Error! Yaml file IOException: %s", err.Error())
        os.Exit(1)
    }
    // 从字节数组里反序列化成一个 conf 实例
    if err := yaml.Unmarshal(yamlFile, conf); err != nil {
        println("Error! Yaml unmarshal exception: %s", err.Error())
        os.Exit(1)
    }
    return conf, nil
}

在 handler 目录里创建 database.go,定义数据库连接

package handler

import (
    _ "github.com/go-sql-driver/mysql" // 这个意思是要执行 mysql 该包下的文件里所有init()函数,但不会打包这个包,所以无法通过包名来调用包中的其他函数
    "github.com/jinzhu/gorm"
    "xg-temp/service/common"
)

func CreateMySqlConnection() (*gorm.DB, error) {
    host := common.CfgInstance().MySql.Host
    port := common.CfgInstance().MySql.Port
    if host == "" || host == "localhost" || host == "127.0.0.1"{
        host = ""
    } else {
        // 远程 ip
        host = "tcp(" + host + ":" + port + ")"
    }

    user := common.CfgInstance().MySql.User
    password := common.CfgInstance().MySql.Password
    dbName := common.CfgInstance().MySql.DBName

    return gorm.Open("mysql",
        user + ":" + password + "@" + host +"/" + dbName + "?charset=utf8&parseTime=true&loc=Local")
}

service.go

现在需要启动这个 service 模块。

package main

import (
    "fmt"
    "github.com/micro/go-micro"
    "github.com/micro/go-micro/registry"
    "github.com/micro/go-micro/registry/consul"
    "os"
    "time"
    "xg-temp/service/handler"
    proto "xg-temp/service/proto"
)

func main() {
    serviceName := "xgtmp.srv.app"

    db ,err := handler.CreateMySqlConnection()
    defer db.Close()

    if err != nil {
        println("conncet error: &s", err.Error())
        os.Exit(1)
    }

    appDao := &handler.AppDao{DB:db} // 将数据源传给 service 对象
    // 这里是通过 gorm 的 DDL 功能,直接创建表。但是仍需要先手动建库。
    // 假设要么所有表都存在要么都不存在
    if !appDao.DB.HasTable(&proto.MobileApplication{}) {
        if err := appDao.DB.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8",
    // gorm 能直接根据 struct 生成对应的表,具体请见 gorm 文档
            ).CreateTable(&proto.MobileApplication{}, &proto.ProductInfo{}, &proto.Product2App{},
            ).Error; err != nil {
                println("Error: Create table error")
                os.Exit(1)
            }
    }

    // 连接本地 consul
    opts := registry.Option(func(opts *registry.Options) {
        opts.Addrs = []string {fmt.Sprintf("%s:%d", "127.0.0.1", 8500)}
    })
    // New Service
    service := micro.NewService(
        micro.Name(serviceName),
        micro.Version("latest"),
        micro.RegisterTTL(time.Second * 10),
        micro.RegisterInterval(time.Second * 5),
        micro.Registry(consul.NewRegistry(opts)),
    )
    // Init & Binding handling
    service.Init()
    _ = proto.RegisterAppHandler(service.Server(), appDao)

    // Run Service
    if err := service.Run(); err != nil {
        println(err)
    }
}

一起 run 起来

api 和 service 是两个模块,都需要运行起来。在运行之前,先要运行 consul,这样服务才能注册上去。启动顺序不重要,因为 api 模块只有在被访问的时候才会去调 service 模块。

在 http 文件里定义一个新的 POST 请求


Go Micro Restful Service Demo_第5张图片

查看 api 模块的控制台输出


查看 service 模块的控制台输出


查看数据库


Go Micro Restful Service Demo_第6张图片

总结

本文介绍了怎样使用 go 下面的一些框架和软件来搭建一个微服务架构的 Restful Service。但是,各方面都没有特别深入,比如,没有探索 consul 的原理和分布式生产环境下的配置方法,没有探索 go-gin、go-micro、gorm等的内部原理,及其高级用法。

你可能感兴趣的:(Go Micro Restful Service Demo)