转自:https://segmentfault.com/a/1190000015135650?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com

前言

系列概览

《Golang 微服务教程》分为 10 篇,总结微服务开发、测试到部署的完整过程。

本节先介绍微服务的基础概念、术语,再创建我们的第一个微服务 consignment-service 的简洁版。在接下来的第 2~10 节文章中,我们会陆续创建以下微服务:

  • consignment-service(货运服务)
  • inventory-service(仓库服务)
  • user-service(用户服务)
  • authentication-service(认证服务)
  • role-service (角色服务)
  • vessel-service(货船服务)

用到的完整技术栈如下:

Golang, gRPC, go-micro            // 开发语言及其 RPC 框架
Google Cloud, MongoDB            // 云平台与数据存储
Docker, Kubernetes, Terrafrom      // 容器化与集群架构
NATS, CircleCI                    // 消息系统与持续集成

代码仓库

作者代码:EwanValentine/shippy,译者的中文注释代码: wuYin/shippy

每个章节对应仓库的一个分支,比如本文part1 的代码在 feature/part1

开发环境

笔者的开发环境为 macOS,本文中使用了 make 工具来高效编译,Windows 用户需 手动安装

$ go env
GOARCH="amd64"    # macOS 环境
GOOS="darwin"    # 在第二节使用 Docker 构建 alpine 镜像时需修改为 linux
GOPATH="/Users/wuyin/Go"
GOROOT="/usr/local/go"

准备

掌握 Golang 的基础语法:推荐阅读谢大的《Go Web 编程》

安装 gRPC / protobuf

go get -u google.golang.org/grpc                    # 安装 gRPC 框架
go get -u github.com/golang/protobuf/protoc-gen-go    # 安装 Go 版本的 protobuf 编译器

微服务

我们要写什么项目?

我们要搭建一个港口的货物管理平台。本项目以微服务的架构开发,整体简单且概念通用。闲话不多说让我们开始微服务之旅吧。

微服务是什么?

在传统的软件开发中,整个应用的代码都组织在一个单一的代码库,一般会有以下拆分代码的形式:

  • 按照特征做拆分:如 MVC 模式
  • 按照功能做拆分:在更大的项目中可能会将代码封装在处理不同业务的包中,包内部可能会再做拆分

不管怎么拆分,最终二者的代码都会集中在一个库中进行开发和管理,可参考:谷歌的单一代码库管理

微服务是上述第二种拆分方式的拓展,按功能将代码拆分成几个包,都是可独立运行的单一代码库。区别如下:

微服务有哪些优势?

降低复杂性

将整个应用的代码按功能对应拆分为小且独立的微服务代码库,这不禁让人联想到 Unix 哲学:Do One Thing and Do It Well,在传统单一代码库的应用中,模块之间是紧耦合且边界模糊的,随着产品不断迭代,代码的开发和维护将变得更为复杂,潜在的 bug 和漏洞也会越来越多。

提高扩展性

在项目开发中,可能有一部分代码会在多个模块中频繁的被用到,这种复用性很高的模块常常会抽离出来作为公共代码库使用,比如验证模块,当它要扩展功能(添加短信验证码登录等)时,单一代码库的规模只增不减, 整个应用还需重新部署。在微服务架构中,验证模块可作为单个服务独立出来,能独立运行、测试和部署。

遵循微服务拆分代码的理念,能大大降低模块间的耦合性,横向扩展也会容易许多,正适合当下云计算的高性能、高可用和分布式的开发环境。

Nginx 有一系列文章来探讨微服务的许多概念,可 点此阅读

使用 Golang 的好处?

微服务是一种架构理念而不是具体的框架项目,许多编程语言都可以实现,但有的语言对微服务开发具备天生的优势,Golang 便是其中之一

Golang 本身十分轻量级,运行效率极高,同时对并发编程有着原生的支持,从而能更好的利用多核处理器。内置 net 标准库对网络开发的支持也十分完善。可参考谢大的短文:Go 语言的优势

此外,Golang 社区有一个很棒的开源微服务框架 go-mirco,我们在下一节会用到。

Protobuf 与 gRPC

在传统应用的单一代码库中,各模块间可直接相互调用函数。但在微服务架构中,由于每个服务对应的代码库是独立运行的,无法直接调用,彼此间的通信就是个大问题,解决方案有 2 个:

JSON 或 XML 协议的 API

微服务之间可使用基于 HTTP 的 JSON 或 XML 协议进行通信:服务 A 与服务 B 进行通信前,A 必须把要传递的数据 encode 成 JSON / XML 格式,再以字符串的形式传递给 B,B 接收到数据需要 decode 后才能在代码中使用:

  • 优点:数据易读,使用便捷,是与浏览器交互必选的协议
  • 缺点:在数据量大的情况下 encode、decode 的开销随之变大,多余的字段信息导致传输成本更高

RPC 协议的 API

下边的 JSON 数据就使用 descriptionweight 等元数据来描述数据本身的意义,在 Browser / Server 架构中用得很多,以方便浏览器解析:

{"description": "This is a test consignment","weight": 550,"containers": [{"customer_id": "cust001","user_id": "user001","origin": "Manchester, United Kingdom"}],"vessel_id": "vessel001"
}

但在两个微服务之间通信时,若彼此约定好传输数据的格式,可直接使用二进制数据流进行通信,不再需要笨重冗余的元数据。

gRPC 简介

gRPC 是谷歌开源的轻量级 RPC 通信框架,其中的通信协议基于二进制数据流,使得 gRPC 具有优异的性能。

gRPC 支持 HTTP 2.0 协议,使用二进制帧进行数据传输,还可以为通信双方建立持续的双向数据流。可参考:Google HTTP/2 简介

protobuf 作为通信协议

两个微服务之间通过基于 HTTP 2.0 二进制数据帧通信,那么如何约定二进制数据的格式呢?答案是使用 gRPC 内置的 protobuf 协议,其 DSL 语法 可清晰定义服务间通信的数据结构。可参考:gRPC Go: Beyond the basics

consignment-service 微服务开发

经过上边必要的概念解释,现在让我们开始开发我们的第一个微服务:consignment-service

项目结构

假设本项目名为 shippy,你需要:

  • 在 $GOPATH 的 src 目录下新建 shippy 项目目录
  • 在项目目录下新建文件 consignment-service/proto/consignment/consignment.proto

为便于教学,我会把本项目的所有微服务的代码统一放在 shippy 目录下,这种项目结构被称为 "mono-repo",读者也可以按照 "multi-repo" 将各个微服务拆为独立的项目。更多参考 REPO 风格之争:MONO VS MULTI

现在你的项目结构应该如下:

$GOPATH/src└── shippy└── consignment-service└── proto└── consignment└── consignment.proto

开发流程

定义 protobuf 通信协议文件

// shipper/consignment-service/proto/consignment/consignment.protosyntax = "proto3";
package go.micro.srv.consignment;// 货轮微服务
service ShippingService {// 托运一批货物rpc CreateConsignment (Consignment) returns (Response) {}
}// 货轮承运的一批货物
message Consignment {string id = 1;                      // 货物编号string description = 2;             // 货物描述int32 weight = 3;                   // 货物重量repeated Container containers = 4;  // 这批货有哪些集装箱string vessel_id = 5;               // 承运的货轮
}// 单个集装箱
message Container {string id = 1;          // 集装箱编号string customer_id = 2; // 集装箱所属客户的编号string origin = 3;      // 出发地string user_id = 4;     // 集装箱所属用户的编号
}// 托运结果
message Response {bool created = 1;            // 托运成功Consignment consignment = 2;// 新托运的货物
}

语法参考: Protobuf doc

生成协议代码

protoc 编译器使用 grpc 插件编译 .proto 文件

为避免重复的在终端执行编译、运行命令,本项目使用 make 工具,新建 consignment-service/Makefile

build:
# 一定要注意 Makefile 中的缩进,否则 make build 可能报错 Nothing to be done for build
# protoc 命令前边是一个 Tab,不是四个或八个空格protoc -I. --go_out=plugins=grpc:$(GOPATH)/src/shippy/consignment-service proto/consignment/consignment.proto

执行 make build,会在 proto/consignment 目录下生成 consignment.pb.go

consignment.proto 与 consignment.pb.go 的对应关系

service:定义了微服务 ShippingService 要暴露为外界调用的函数:CreateConsignment,由 protobuf 编译器的 grpc 插件处理后生成 interface

type ShippingServiceClient interface {// 托运一批货物CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error)
}

message:定义了通信的数据格式,由 protobuf 编译器处理后生成 struct

type Consignment struct {Id           string       `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`Description  string       `protobuf:"bytes,2,opt,name=description" json:"description,omitempty"`Weight       int32        `protobuf:"varint,3,opt,name=weight" json:"weight,omitempty"`Containers   []*Container `protobuf:"bytes,4,rep,name=containers" json:"containers,omitempty"`// ...
}

实现服务端

服务端需实现 ShippingServiceClient 接口,创建consignment-service/main.go

package mainimport (// 导如 protoc 自动生成的包pb "shippy/consignment-service/proto/consignment""context""net""log""google.golang.org/grpc"
)const (PORT = ":50051"
)//
// 仓库接口
//
type IRepository interface {Create(consignment *pb.Consignment) (*pb.Consignment, error) // 存放新货物
}//
// 我们存放多批货物的仓库,实现了 IRepository 接口
//
type Repository struct {consignments []*pb.Consignment
}func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {repo.consignments = append(repo.consignments, consignment)return consignment, nil
}func (repo *Repository) GetAll() []*pb.Consignment {return repo.consignments
}//
// 定义微服务
//
type service struct {repo Repository
}//
// service 实现 consignment.pb.go 中的 ShippingServiceServer 接口
// 使 service 作为 gRPC 的服务端
//
// 托运新的货物
func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) {// 接收承运的货物consignment, err := s.repo.Create(req)if err != nil {return nil, err}resp := &pb.Response{Created: true, Consignment: consignment}return resp, nil
}func main() {listener, err := net.Listen("tcp", PORT)if err != nil {log.Fatalf("failed to listen: %v", err)}log.Printf("listen on: %s\n", PORT)server := grpc.NewServer()repo := Repository{}// 向 rRPC 服务器注册微服务// 此时会把我们自己实现的微服务 service 与协议中的 ShippingServiceServer 绑定pb.RegisterShippingServiceServer(server, &service{repo})if err := server.Serve(listener); err != nil {log.Fatalf("failed to serve: %v", err)}
}

上边的代码实现了 consignment-service 微服务所需要的方法,并建立了一个 gRPC 服务器监听 50051 端口。如果你此时运行 go run main.go,将成功启动服务端:

实现客户端

我们将要托运的货物信息放到 consignment-cli/consignment.json

{"description": "This is a test consignment","weight": 550,"containers": [{"customer_id": "cust001","user_id": "user001","origin": "Manchester, United Kingdom"}],"vessel_id": "vessel001"
}

客户端会读取这个 JSON 文件并将该货物托运。在项目目录下新建文件:consingment-cli/cli.go

package mainimport (pb "shippy/consignment-service/proto/consignment""io/ioutil""encoding/json""errors""google.golang.org/grpc""log""os""context"
)const (ADDRESS           = "localhost:50051"DEFAULT_INFO_FILE = "consignment.json"
)// 读取 consignment.json 中记录的货物信息
func parseFile(fileName string) (*pb.Consignment, error) {data, err := ioutil.ReadFile(fileName)if err != nil {return nil, err}var consignment *pb.Consignmenterr = json.Unmarshal(data, &consignment)if err != nil {return nil, errors.New("consignment.json file content error")}return consignment, nil
}func main() {// 连接到 gRPC 服务器conn, err := grpc.Dial(ADDRESS, grpc.WithInsecure())if err != nil {log.Fatalf("connect error: %v", err)}defer conn.Close()// 初始化 gRPC 客户端client := pb.NewShippingServiceClient(conn)// 在命令行中指定新的货物信息 json 文件infoFile := DEFAULT_INFO_FILEif len(os.Args) > 1 {infoFile = os.Args[1]}// 解析货物信息consignment, err := parseFile(infoFile)if err != nil {log.Fatalf("parse info file error: %v", err)}// 调用 RPC// 将货物存储到我们自己的仓库里resp, err := client.CreateConsignment(context.Background(), consignment)if err != nil {log.Fatalf("create consignment error: %v", err)}// 新货物是否托运成功log.Printf("created: %t", resp.Created)
}

运行 go run main.go 后再运行 go run cli.go

我们可以新增一个 RPC 查看所有被托运的货物,加入一个GetConsignments方法,这样,我们就能看到所有存在的consignment了:

// shipper/consignment-service/proto/consignment/consignment.protosyntax = "proto3";package go.micro.srv.consignment;// 货轮微服务
service ShippingService {// 托运一批货物rpc CreateConsignment (Consignment) returns (Response) {}// 查看托运货物的信息rpc GetConsignments (GetRequest) returns (Response) {}
}// 货轮承运的一批货物
message Consignment {string id = 1;                      // 货物编号string description = 2;             // 货物描述int32 weight = 3;                   // 货物重量repeated Container containers = 4;  // 这批货有哪些集装箱string vessel_id = 5;               // 承运的货轮
}// 单个集装箱
message Container {string id = 1;          // 集装箱编号string customer_id = 2; // 集装箱所属客户的编号string origin = 3;      // 出发地string user_id = 4;     // 集装箱所属用户的编号
}// 托运结果
message Response {bool created = 1;                       // 托运成功Consignment consignment = 2;            // 新托运的货物repeated Consignment consignments = 3;  // 目前所有托运的货物
}// 查看货物信息的请求
// 客户端想要从服务端请求数据,必须有请求格式,哪怕为空
message GetRequest {
}

现在运行make build来获得最新编译后的微服务界面。如果此时你运行go run main.go,你会获得一个类似这样的错误信息:

熟悉Go的你肯定知道,你忘记实现一个interface所需要的方法了。让我们更新consignment-service/main.go:

package mainimport (pb "shippy/consignment-service/proto/consignment""context""net""log""google.golang.org/grpc"
)const (PORT = ":50051"
)//
// 仓库接口
//
type IRepository interface {Create(consignment *pb.Consignment) (*pb.Consignment, error) // 存放新货物GetAll() []*pb.Consignment                                   // 获取仓库中所有的货物
}//
// 我们存放多批货物的仓库,实现了 IRepository 接口
//
type Repository struct {consignments []*pb.Consignment
}func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {repo.consignments = append(repo.consignments, consignment)return consignment, nil
}func (repo *Repository) GetAll() []*pb.Consignment {return repo.consignments
}//
// 定义微服务
//
type service struct {repo Repository
}//
// 实现 consignment.pb.go 中的 ShippingServiceServer 接口
// 使 service 作为 gRPC 的服务端
//
// 托运新的货物
func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) {// 接收承运的货物consignment, err := s.repo.Create(req)if err != nil {return nil, err}resp := &pb.Response{Created: true, Consignment: consignment}return resp, nil
}// 获取目前所有托运的货物
func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest) (*pb.Response, error) {allConsignments := s.repo.GetAll()resp := &pb.Response{Consignments: allConsignments}return resp, nil
}func main() {listener, err := net.Listen("tcp", PORT)if err != nil {log.Fatalf("failed to listen: %v", err)}log.Printf("listen on: %s\n", PORT)server := grpc.NewServer()repo := Repository{}pb.RegisterShippingServiceServer(server, &service{repo})if err := server.Serve(listener); err != nil {log.Fatalf("failed to serve: %v", err)}
}

如果现在使用go run main.go,一切应该正常:

最后让我们更新consignment-cli/cli.go来获得consignment信息:

func main() {... // 列出目前所有托运的货物resp, err = client.GetConsignments(context.Background(), &pb.GetRequest{})if err != nil {log.Fatalf("failed to list consignments: %v", err)}for _, c := range resp.Consignments {log.Printf("%+v", c)}
}

此时再运行go run cli.go,你应该能看到所创建的所有consignment,多次运行将看到多个货物被托运:

至此,我们使用protobuf和grpc创建了一个微服务以及一个客户端。

Golang微服务教程相关推荐

  1. Golang 微服务教程

    本节对 gRPC 的使用浅尝辄止,更多可参考:gRPC 中 Client 与 Server 数据交互的 4 种模式 前言 系列概览 <Golang 微服务教程>分为 10 篇,总结微服务开 ...

  2. 微服务教程--什么是 Nacos

    微服务教程--什么是 Nacos 概览 欢迎来到 Nacos 的世界! Nacos 致力于帮助您发现.配置和管理微服务.Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现.服务配置.服 ...

  3. 一个人写一个集群:基于GRPC的golang微服务框架iogo(grpc/protobuf/etcd/freetoo/码客 卢益贵)

    一个人写一个集群:基于GRPC的golang微服务框架iogo keyword:iogo,golang,grpc,protobuf,etcd,zookeeper,microservice,distri ...

  4. go开源文件服务器框架,golang微服务框架go-zero系列-4:go-zero文件服务

    golang微服务框架go-zero系列-4:go-zero文件服务 go-zero本身支持文件服务,但是我们需要写相关的handler文件,本文目的在于 不写任何一个和文件相关的handler 如果 ...

  5. 基于事件驱动的微服务教程

    基于事件驱动的微服务教程 使用 Spring Boot.Spring Cloud.Kafka 和 Elasticsearch 掌握具有模式的事件驱动微服务架构 课程英文名:Event-Driven M ...

  6. 保姆级教程!Golang微服务简洁架构实战

    导语 | 本文从简洁架构的理论出发,依托trpc-go目录规范,简单阐述了整体代码架构如何划分,具体trpc-go服务代码实现细节,和落地步骤,并讨论了和DDD的区别.文章源于我们组内发起的go微服务 ...

  7. golang java微服务_Golang 微服务教程(四)

    本文完整代码:GitHub 上节引入 user-service 微服务并在 Postgres 中存储了用户数据,包括明文密码.本节将对密码进行安全的加密处理,并使用唯一的 token 来在各微服务之间 ...

  8. Golang微服务开发实践

    github: github.com/yun-mu/Micr- 微服务概念学习:可参考 Nginx 的微服务文章 微服务最佳实践:可参考 微服务最佳实践 demo 简介 服务: consignment ...

  9. golang微服务框架对比_微服务里程碑,Golang与Spring Cloud Alibaba完美结合

    目前微服务架构仍是软件架构中最新的热门话题,虽然Golang是一门新的语言,但Golang的性能比python和java高出不少.既能承受程序使用运行的服务构建的繁重负载,又容易与GitHub集成,管 ...

最新文章

  1. 【Windows 逆向】OD 调试器工具 ( OD 工具简介 | OD 工具与 CE 工具对比 )
  2. jquery ajax POST/GET 请求至 ASP.NET WebAPI
  3. Flask web开发之路二
  4. java 监听器实现原理
  5. 单片机C语言编程:.H文件与.C文件的关系!
  6. linux qtopia-2.2.0编译,qtopia-2.2.0在linux上的安装(基于mini2440)
  7. CSS3简介、新增选择器、属性选择器、伪元素选择器、伪元素
  8. 洛谷2543AHOI2005]航线规划 (树剖+线段树+割边思路)
  9. 《大型网站技术架构》读书笔记
  10. 大二Web课程设计——张家界旅游网站设计与实现(HTML+CSS+JavaScript)
  11. icon、png网页开发中所需要的小图标
  12. 折弯机使用说明书_折弯机操作图解法-如何使用折弯机
  13. NYOJ-845 无主之地1
  14. 全国计算机注册时密码为什么老是错误,电脑密码正确却显示密码错误怎么办
  15. CodeForces - 1364D Ehabs Last Corollary(dfs树找最小环)
  16. 51智能小车黑线寻迹(防出线)
  17. 题解 P3588 [POI2015]PUS
  18. 用 Python 打扑克牌——炸金花
  19. 自我介绍 的html页面,html初学者自我介绍网页
  20. 众安在线荣获第十届中国证券金紫荆“最佳投资者关系上市公司”奖

热门文章

  1. linux内存管理之 ION 内存管理器浅析Ⅱ(system contig heap)
  2. 计算机怎样打出错误,正在打印打印机错误怎么办 打印打印机错误如何解决【详解】...
  3. 基于51单片机的简易太阳能追踪系统
  4. 17 岁天才少年手搓电机:不用稀土磁铁,效能提高 30%,获奖 52 万
  5. C#:实现随机洗牌Knuth-Durstenfeld Shuffle算法​(附完整源码)
  6. Python+matplotlib画爱心
  7. 阿里云 mysql 太慢_MySQL很慢... 怎么破??-阿里云开发者社区
  8. MAinframe之cobol
  9. STAR CCM+ 11.0 自动化设置衍生零部件
  10. 2013华为上机题C++编程