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 的约定。



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

项目背景

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

整体设计

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



可见,项目 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

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


安装 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 请求,运行


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


查看 api 模块的控制台输出


查看 service 模块的控制台输出


查看数据库


总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容