本文介绍如何从零开始,使用 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 设置
- 配置代理。针对公司的内网环境,在 File->Settings->Apearance & Behavior->System Settings->HTTP Proxy,配置 Manual proxy configuration,具体看公司给的代理配置。
- 安装 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 请求,运行
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等的内部原理,及其高级用法。