文章目录
- 本文的任务
- [GO 语言](#GO 语言)
-
- [GO 与 Docker(容器运行时)的关系](#GO 与 Docker(容器运行时)的关系)
- [GO 与 Kubernetes 的关系](#GO 与 Kubernetes 的关系)
- 要澄清的一点
- [一个简单的 go 项目------商品信息服务系统](#一个简单的 go 项目——商品信息服务系统)
-
- [Go 项目是前后端分离的](#Go 项目是前后端分离的)
- 前端网关部分的代码
-
- 模拟用户的输入流量
- [grpc 客户端,发送数据](#grpc 客户端,发送数据)
- 启动前端客户端主程序
- 后端服务部分的代码
-
- 模拟后端服务器经过复杂处理后的输出流量
- [GRPC 服务器的业务化私人定制设置](#GRPC 服务器的业务化私人定制设置)
- [GRPC 服务主程序,接收客户端请求](#GRPC 服务主程序,接收客户端请求)
- 项目运行,项目效果
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
本文的任务
我将要介绍 go 语言,并简要详细介绍一个 go 项目------商品信息服务系统。
GO 语言
GO 语言(又称 Golang)是一种由 Google 开发的开源编程语言。(它的运行效率,肯定是不如 C/C++ 高的,但是它的语法简洁,实在很适合作业务开发。)一个程序员没有用很高的计算机设备知识,都可以写业务。
- 编译型语言:直接编译为机器码,执行效率高
- 静态类型:类型安全,编译时检查错误
- 垃圾回收:自动内存管理(GC 机制,所以他会比 C/C++ )
- 并发原生支持:goroutine 和 channel 机制(协程)
- 简洁语法:类似 C,但更加现代化和简洁

GO 与 Docker(容器运行时)的关系
Docker 是一个云原生工具,它以容器形式让一个程序的运行独立于其开发所在的操作系统环境。也就是说,我在 Linux 环境下开发的 C/C++ 程序,我只需要把依赖的库文件都找出来,放到容器中就行了。
go
# Docker 的源代码主要是 Go 语言
github.com/docker/docker

而这个神奇的工具就是由 Go 语言来实现的。
为什么 Docker 选择 Go:
1、编译为静态二进制:单个可执行文件,依赖少
2、跨平台编译:轻松编译为不同架构和操作系统
3、并发处理:适合处理大量容器操作
4、性能优秀:接近 C 的性能,但开发效率更高
5、标准库丰富:特别是网络和系统相关的库
Go 在 Docker 中的优势
- 部署简单:一个二进制文件包含所有依赖
- 内存占用小:适合容器化环境
- 启动快速:毫秒级启动时间
GO 与 Kubernetes 的关系
Docker 容器运行时也有容器编排工具 compose 和 swarm,但是我们一般都会用 Kubernetes,因为这是一个高效的自动运维工具,他是自动化的。
Kubernetes 完全用 Go 编写
go
# Kubernetes 的所有组件都是 Go 语言
kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, kubectl

为什么 Kubernetes 选择 Go:
1、并发处理能力
go
// Kubernetes 使用大量 goroutine 处理并发任务
go podManager.Run(stopCh)
go volumeManager.Run(stopCh)
go probeManager.Run(stopCh)
2、网络编程优势
go
// 处理 API 请求
http.HandleFunc("/api/v1/pods", func(w http.ResponseWriter, r *http.Request) {
// 处理 Pod 相关 API
})
3、跨平台部署
go
// 轻松编译为各种平台
GOOS=linux GOARCH=amd64 go build
要澄清的一点
尽管 kubenets 和 Docker 都是 go 语言写的,但我们放入其中的程序可以不是 Go 程序(可以是 C/C++ 程序),kubenets 和 Docker 的核心功能只是 "运维"。
一个简单的 go 项目------商品信息服务系统
这个小项目是零声教育的 nick 老师写的案例------商品信息服务,用户请求零声教育的课程 ID 返回对应课程的商品信息(课程 ID + 课程名字 + 课程类别),当然是可以附带价钱信息,这里只是作实例演示。
在项目开始前,我们要梳理一下业务逻辑。我们想要完成的标准化交流

我们可以把以上的结构化信息通信,归纳成以下的 proto 文件。我们可以用 GRPC 框架(内含 protobuf 框架),这个代码项目技术也是和来自于 Google 公司(包括前面说过的 go 语言与 k8s/kubernets 也都是来自与 Google 公司)。
proto
syntax = "proto3";
option go_package = "0voiceGateWay/services/goods/proto";;
package goods_voice;
message GetGoodsReq{
int64 goodsId = 1;
}
message GetGoodsRes {
int64 goodsId=1;
string goodsName = 2;
string goodsCategory = 3;
}
service Goods {
rpc Get(GetGoodsReq)returns(GetGoodsRes){}
}
syntax = "proto3" 是指定使用 protobuf 的版本 3 语法,
option go_package = "Goods/proto" protoc 编译器会根据这个路径生成 Go 文件,
package goods_voice 用于防止命名冲突,在生成代码时会作为命名空间
GetGoodsReq - 请求消息
proto
message GetGoodsReq{
int64 goodsId = 1;
}
GetGoodsRes - 响应消息
proto
message GetGoodsRes {
int64 goodsId = 1;
string goodsName = 2;
string goodsCategory = 3;
}
服务定义(这个服务只包含 GET 方法,没有 POST 和 PUT 之类的方法,因而相对简单)
proto
service Goods {
rpc Get(GetGoodsReq) returns (GetGoodsRes) {}
}
现在,我们可以进行 GRPC 编译了,客户端与服务端的信息通讯的 go 语言版的代码(也可以生成 C/C++ 版本的代码),首先生成 protobuf 序列化与反序列化的代码(grpc 服务的信息反序列化需要用到)
bash
qiming@k8s-master1:~/share/CTASK/docker/code$ protoc.exe --go_out=. --go_opt=paths=source_relative .\services\goods\proto\goods.proto
紧接着生成 grpc 的客户端、服务端的工具代码。需要注意
bash
qiming@k8s-master1:~/share/CTASK/docker/code$ protoc.exe --go-grpc_out=. --go-grpc_opt=paths=source_relative .\services\goods\proto\goods.proto
需要注意(protobuf 和 grpc 读者请自行下载)
1、protoc.exe:Protocol Buffers 编译器
2、--go_out=.:生成 Go 代码到当前目录
3、--go_opt=paths=source_relative:保持与 proto 文件相同的目录结构,即前面 proto 文件的那一串代码 option go_package = "0voiceGateWay/services/goods/proto"
4、--go-grpc_out=.:生成 gRPC 相关的 Go 代码到当前目录,当前目录指代上面那一个目录
5、--go-grpc_opt=paths=source_relative:保持目录结构,即前面 proto 文件的那一串代码 option go_package = "0voiceGateWay/services/goods/proto"
6、.\services\goods\proto\goods.proto 是 proto 文件所在的路径,
于是生了两个代码,我把他们 一式两份 放在了搭建前端、后端的代码文件夹之内,前端与后端用同一份代码。而实际上是分开运行的。

goods.grpc.pb.go 代码文件的概览(这两个文件我就不细讲了,我只是归纳它们的结构)
go
// ================================================================================
// 客户端实现
// ================================================================================
1、客户端接口
type GoodsClient interface {
Get(ctx context.Context, in *GetGoodsReq, opts ...grpc.CallOption) (*GetGoodsRes, error)
}
2、客户端连接通道
type goodsClient struct {
cc grpc.ClientConnInterface
}
3、新建客户端
func NewGoodsClient(cc grpc.ClientConnInterface) GoodsClient {
return &goodsClient{cc}
}
4、客户端发出结构化的请求报文
func (c *goodsClient) Get(ctx context.Context, in *GetGoodsReq, opts ...grpc.CallOption) (*GetGoodsRes, error) {
...
}
// =========================================================================================
// 服务端实现
// =========================================================================================
1. 定义服务接口 - 你要实现的方法
type GoodsServer interface {
Get(context.Context, *GetGoodsReq) (*GetGoodsRes, error)
mustEmbedUnimplementedGoodsServer()
}
2. 默认的未实现的版本
type UnimplementedGoodsServer struct {
}
func (UnimplementedGoodsServer) Get(context.Context, *GetGoodsReq) (*GetGoodsRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
}
func (UnimplementedGoodsServer) mustEmbedUnimplementedGoodsServer() {}
type UnsafeGoodsServer interface {
mustEmbedUnimplementedGoodsServer()
}
3. 服务注册机制,注册一个服务器
func RegisterGoodsServer(s grpc.ServiceRegistrar, srv GoodsServer) {
s.RegisterService(&Goods_ServiceDesc, srv)
}
4. 服务端关心的请求连
func _Goods_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
...
}
5. 服务器描述符(告诉服务器如何运行,内部网络是怎样的)
var Goods_ServiceDesc = grpc.ServiceDesc{
ServiceName: "goods_voice.Goods",
HandlerType: (*GoodsServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Get",
Handler: _Goods_Get_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "services/goods/proto/goods.proto",
}
goods.pb.go 代码文件的概览
go
1、请求消息的结构体
type GetGoodsReq struct {
// 运行时管理字段(你不直接使用)
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
GoodsId int64 `protobuf:"varint,1,opt,name=goodsId,proto3" json:"goodsId,omitempty"`
}
2、请求体的方法
================================================================================
以下是消息体 GetGoodsReq 及其 6 个接口
Reset() 清空消息数据 对象重用、资源池
String() 返回可读格式 调试、日志输出
ProtoMessage()
ProtoReflect() 查看工具接口 读、写、检查、探索(反射)
Descriptor() 这个方法是为了向后兼容性而存在的,也是描述这个结构体,相当于用户指南
GetGoodsId() 获取消息体的 ID 字段
================================================================================
3、响应结构体
type GetGoodsRes struct {
// 运行时管理字段(你不直接使用)
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
GoodsId int64 `protobuf:"varint,1,opt,name=goodsId,proto3" json:"goodsId,omitempty"`
GoodsName string `protobuf:"bytes,2,opt,name=goodsName,proto3" json:"goodsName,omitempty"`
GoodsCategory string `protobuf:"bytes,3,opt,name=goodsCategory,proto3" json:"goodsCategory,omitempty"`
}
4、响应体的方法
================================================================================
以下是消息体 GetGoodsRes 及其 6 个接口
Reset() 清空消息数据 对象重用、资源池
String() 返回可读格式 调试、日志输出
ProtoMessage()
ProtoReflect() 查看工具接口 读、写、检查、探索(反射)
Descriptor() 这个方法是为了向后兼容性而存在的,也是描述这个结构体,相当于用户指南
GetGoodsId() 获取消息体的 ID 字段
GetGoodsName() 获取消息体的 Name 字段
GetGoodsCategory() 获取消息体的 Category 字段
================================================================================
5、全局变量
==============================================================================
protobuf 生成的类型注册和依赖关系信息
1、File_services_goods_proto_goods_proto,proto 文件的运行时描述符
2、file_services_goods_proto_goods_proto_rawDesc,完整的类型系统信息(二进制)
3、file_services_goods_proto_goods_proto_rawDescOnce,确保某个函数只执行一次
4、file_services_goods_proto_goods_proto_rawDescData,被赋值为 file_services_goods_proto_goods_proto_rawDesc
5、函数 file_services_goods_proto_goods_proto_rawDescGZIP 懒加载压缩:第一次调用时才进行 GZIP 压缩
6、file_services_goods_proto_goods_proto_msgTypes 存储所有消息类型的元数据数组:索引 0 = TokenRequest 的元数据;索引 1 = TokenResponse 的元数据
7、file_services_goods_proto_goods_proto_goTypes - Go 类型映射
8、file_services_goods_proto_goods_proto_depIdxs - 依赖关系索引
==============================================================================
Go 项目是前后端分离的
从以上的讨论,我们可以看到 Google 公司的 GRPC 与 Protobuf 的项目技术能很好的实现前后端分离(我只是没有实现一个漂亮的前端界面而已)。
前端网关部分的代码
模拟用户的输入流量
由于我们这次是案例演示,我们并不做真实的流量,我们的请求内容是提前以 config.yaml 文件准备好的。
yaml
order:
id: 10
goodsId: 20
userId: 30
amount: 100.00
status: 1
goods:
serviceAddr: 192.168.152.128:50051
对应的需要把这个文件加载到前端里面(package config 是文件的命名空间)
go
package config
import (
"github.com/spf13/viper"
"log"
)
// "github.com/spf13/viper" 是文件格式化读取的工具链
// "log" 是日志记录文件工具链
// import 的多个文件之间是靠 package 来进行语义区分的
type Config struct {
Order struct { // 订单信息(身份 + )
ID int64
GoodsID int64
UserID int64
Amount float64
Status int
}
Goods struct { // 商品信息
ServiceAddr string
}
}
var conf *Config // 全局配置变量,是指针类型
func init() {
v := viper.New() // 创建 Viper 实例
v.SetConfigType("yaml") // 设置配置文件格式
v.SetConfigFile("config.yaml") // 设置配置文件路径
v.ReadInConfig() // 读取配置文件
conf = &Config{} // 创建 Config 结构体实例,取地址
err := v.Unmarshal(conf) // 将 YAML 内容解析到结构体(并且返回失败代码)
if err != nil {
log.Fatal(err) // 如果出错,记录日志并退出程序
}
}
func GetConf() *Config {
return conf // 返回 conf 本身
}
grpc 客户端,发送数据
向 grpc 服务器发送订单信息
go
package order
import (
"0voiceGateway/config" // 读取配置文件的包
"0voiceGateway/services/goods/proto" // gRPC 商品服务原型
"context" // 上下文控制
"github.com/gin-gonic/gin" // Web 框架,Gin 引擎
"google.golang.org/grpc" // gRPC 核心库
"google.golang.org/grpc/credentials/insecure" // gRPC 安全凭证
"log" // 日志生产工具链
"net/http" // HTTP 架构
)
type Order struct {
config *config.Config // 订单的结构体就是 config 的 package 命名空间语境下的 Config
}
func NewOrderInstance(config *config.Config) *Order {
return &Order{
config: config, // 这是结构体的初始化,返回 Order 结构体的地址,{} 意思是这个结构体的 config 字段被初始化成传入参数 config
}
}
// 数据传输结构体,该结构体可序列化,输出为 json 文件
type OrderDetail struct {
ID int64 `json:"id"` // 告诉JSON序列化器:字段名映射为"id"
UserID int64 `json:"user_id"` // 告诉JSON序列化器:字段名映射为"user_id"
UserName string `json:"user_name"` // 告诉JSON序列化器:字段名映射为"user_name"
GoodsID int64 `json:"goods_id"` // 告诉JSON序列化器:字段名映射为"goods_id"
GoodsName string `json:"goods_name"` // 告诉JSON序列化器:字段名映射为"goods_name"
GoodsCategory string `json:"goods_category"` // 告诉JSON序列化器:字段名映射为"goods_category"
Amount float64 `json:"amount"` // 告诉JSON序列化器:字段名映射为"amount"
Status int `json:"status"` // 告诉JSON序列化器:字段名映射为"status"
}
// 获取订单详情(这是实现了接口)
func (o *Order) GetOrderDetail(c *gin.Context) {
detail := o.getOrderDetail()
c.JSON(http.StatusOK, detail) // 用于向客户端返回 JSON 格式的 HTTP 响应
}
// 这给对象提供了接口函数
func (o *Order) getOrderDetail() *OrderDetail {
userID := o.config.Order.UserID // 给出 ID
user := o.getUser(userID)
goodsID := o.config.Order.GoodsID // 给出 ID
goods := o.getGoods(goodsID)
detail := &OrderDetail{ // 这是结构体指针的初始化语法,填写信息
ID: o.config.Order.ID,
UserID: userID,
UserName: user.Name,
GoodsID: goodsID,
GoodsName: goods.Name,
GoodsCategory: goods.Category,
Amount: o.config.Order.Amount,
Status: o.config.Order.Status,
}
return detail
}
type Goods struct {
Id int64
Name string
Category string
}
// 通过商品 ID 获取商品信息,这给对象提供了接口函数
func (o *Order) getGoods(goodsId int64) *Goods {
// 1. 从配置获取商品服务地址
goodsAddr := o.config.Goods.ServiceAddr
// 2. 建立gRPC连接
conn, err := grpc.Dial(goodsAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Println(err)
return nil
}
defer conn.Close() // 确保连接关闭,defer 的意思是 "延迟执行",函数结束时,conn.Close() 会自动执行!
// 3. 创建gRPC客户端
client := proto.NewGoodsClient(conn)
// 4. 构造请求参数(初始化)
in := &proto.GetGoodsReq{
GoodsId: o.config.Order.GoodsID,
}
// 5. 发起gRPC调用,proto 文件是定义了消息与服务的格式
res, err := client.Get(context.Background(), in)
if err != nil {
log.Println(err)
return nil
}
// 6. 转换响应数据
return &Goods{
Id: goodsId,
Name: res.GoodsName,
Category: res.GoodsCategory,
}
}
type User struct {
Id int64
Name string
}
// 通过用户 ID 来获取用户名字,这给对象提供了接口函数
func (o *Order) getUser(userId int64) *User {
return &User{
Id: userId,
Name: "nick",
}
}
我们注意到代码里面有是从配置文件里面获取 192.168.152.128:50051 这个域名-端口信息,并且传入 grpc.Dial 这个函数里面结构化出 conn,以此为依据建立 TCP 连接,
go
client := proto.NewGoodsClient(conn)
接着就是发起格式化的请求
go
// 5. 发起gRPC调用,proto 文件是定义了消息与服务的格式
res, err := client.Get(context.Background(), in)
需要注意到这个 client.Get,其实就是前面的 proto 生成代码里面的函数 Get,它调用了请求链
go
func (c *goodsClient) Get(ctx context.Context, in *GetGoodsReq, opts ...grpc.CallOption) (*GetGoodsRes, error) {
out := new(GetGoodsRes)
err := c.cc.Invoke(ctx, "/goods_voice.Goods/Get", in, out, opts...)
// 在向一个标准规制化命名的路由,向这个服务器申请服务,进行业务调用
/*
// 就像打电话:
c.cc.Invoke(上下文, "电话号码", 说的话, 听的结果, 通话选项)
ctx, // = 什么时候打电话(超时控制)
"/goods_voice.Goods/Get", // = 电话号码
in, // = 你要说的话(请求数据)
out, // = 你听到的回答(响应数据)
opts... // = 通话选项(加密、压缩等)
*/
if err != nil {
return nil, err
}
return out, nil
}
最后就是,经过服务器的一番折腾返回到客户端,并且把响应报文转化成结构体数据
go
// 6. 转换响应数据
return &Goods{
Id: goodsId,
Name: res.GoodsName,
Category: res.GoodsCategory,
}
启动前端客户端主程序
前端服务器程序其实都有一个共同特点,那就是搭建一个复杂的路由
go
package main
import (
"0voiceGateway/config" // 自定义配置包
order2 "0voiceGateway/order" // 自定义订单包,使用别名 order2
"github.com/gin-gonic/gin" // Web 框架
"log" // 日志记录
"net" // 网络操作
"net/http" // HTTP 协议支持
)
func main() {
r := gin.Default() // 创建 Gin 引擎实例(带默认中间件),"github.com/gin-gonic/gin"
initRouter(r) // 初始化路由配置
r.Run(":8081") // 启动服务器,监听 8081 端口(路由式的服务器启动,其实是很好用的)
}
func initRouter(r *gin.Engine) {
conf := config.GetConf() // 获取配置信息 (初始化)
apiGroup := r.Group("/api") // 创建路由组,所有路径以 /api 开头,服务于网页搜索
order := order2.NewOrderInstance(conf) // 创建订单服务实例
apiGroup.GET("/order", order.GetOrderDetail) // 注册路由:GET /api/order
apiGroup.GET("/health", func(c *gin.Context) { // 健康检查接口
c.JSON(http.StatusOK, gin.H{
"message": "OK",
})
})
apiGroup.GET("/ping", func(c *gin.Context) { // 获取服务器 IP 接口
ip := getIP()
c.JSON(http.StatusOK, gin.H{
"ip": ip,
})
})
}
func getIP() string {
var ip string
addrs, err := net.InterfaceAddrs() // 获取所有网络接口地址,连同错误值一并返回
if err != nil {
log.Println(err) // log 是一个命名空间
return ""
}
for _, v := range addrs { // 遍历所有网络接口
if ipnet, ok := v.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ip = ipnet.IP.String()
break
}
}
}
return ip
}
必须要注意到代码里面的,函数 GetOrderDetail 里面调用了函数 getGoods,
go
apiGroup.GET("/order", order.GetOrderDetail)
进而调用了 grpc 的客户端 Get 方法
go
func (c *goodsClient) Get(ctx context.Context, in *GetGoodsReq, opts ...grpc.CallOption)
再多说一嘴,GRPC 的代码里面的那个 Get 方法,调用了服务器里面的请求链 _Goods_Get_Handler,然后调用了拦截器 interceptor(这个拦截器应该是全局唯一的,当然,没有实现拦截器的话也可以。那就不调用了。直接返回 return srv.(GoodsServer).Get(ctx, in)),这个拦截器进而调用了用户自己写的具体业务逻辑服务,程序员只需按要求返回一个有意义的响应体的指针就行了。
后端服务部分的代码
另起一个文件夹,与前面的代码隔离开来
模拟后端服务器经过复杂处理后的输出流量
由于我们这次是案例演示,我们并不做复杂的 mysql-redis 联合操作,获取数据库的数据,我们所给的返回的报文内容,当然也会是提前按照 config.yaml 文件准备好的,就像前面的一样。
yaml
goods:
name: "c/c++VIP课程"
category: "服务器后台"
读取配置,在程序体里面准备好输出流量
go
package config
import (
"github.com/spf13/viper"
"log"
)
type Config struct {
Goods struct {
Name string
Category string
}
}
var conf *Config
func init() {
v := viper.New()
v.SetConfigType("yaml")
v.SetConfigFile("config.yaml")
v.ReadInConfig()
conf = &Config{}
err := v.Unmarshal(conf)
if err != nil {
log.Fatal(err)
}
}
func GetConf() *Config {
return conf
}
GRPC 服务器的业务化私人定制设置
func (s *goodsServer) Get(ctx context.Context, in *proto.GetGoodsReq) 就是我们的私人定制业务函数,我们也看到了,我们是并无设置拦截器的,于是按照默认的方法来处理请求链函数。直接返回对应的结构体的指针了。
go
package server
import (
"Goods/config"
"Goods/proto"
"context"
)
type goodsServer struct {
config *config.Config
proto.UnimplementedGoodsServer // 嵌入默认服务器
}
func NewGoodsServer(config *config.Config) proto.GoodsServer {
return &goodsServer{
config: config,
}
}
func (s *goodsServer) Get(ctx context.Context, in *proto.GetGoodsReq) (*proto.GetGoodsRes, error) {
return &proto.GetGoodsRes{
GoodsId: in.GoodsId,
GoodsName: s.config.Goods.Name,
GoodsCategory: s.config.Goods.Category,
}, nil
}
GRPC 服务主程序,接收客户端请求
这里可以看到,服务器里面实现了 goodsService 这个接口的对象是全局唯一的。
go
package main
import (
"Goods/config"
"Goods/proto"
"Goods/server"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer() // goodsServer 这个接口实现的对象
conf := config.GetConf()
proto.RegisterGoodsServer(s, server.NewGoodsServer(conf))
healthCheck := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, healthCheck)
err = s.Serve(lis) // grpc 服务器,阻塞执行
if err != nil {
log.Fatal(err)
}
}
s := grpc.NewServer() 实质上是创建了一个 GRPC 服务器,我们可以往里面猛灌执行内容,比如说我们的业务逻辑,比若说我们的 grpc 健康检查,只要是 grpc 官方的东西都可以往里使劲塞。
项目运行,项目效果
我是第一次玩 go 语言代码。go mod init 是创建项目的 go.mod 文件,这个 go.mod 文件是空的,只有项目名(即文件夹的名字)与 go 编译器的版本。随后运行 go mod tidy,编译器会检查代码里面有什么源库文件是我们所需要的,没有这个资源的话就下载,有的话就跳过,生成完整的库文件依赖列表,进而会检测使用了库文件里面的那些对象,都记录在了 go.sum 归纳总结文件里面 。当工程能够顺利编译运行,这两个成熟的 go.sum 和 go.mod 文件就可以作为成熟的经验,分享给有需要的朋友,他们可以按照我们的库文件依赖方案来用,无需试错。因为我们编译的时候,就一定会用到这两个文件。
我们在前端代码的文件夹里面编译前端代码(前面两个命令,我已经提起那做好了生成了 go.mod 和 go.sum 下载好所有的依赖文件了,这是前人的配置经验)
bash
qiming@k8s-master1:~/share/CTASK/docker/code$ go mod init
qiming@k8s-master1:~/share/CTASK/docker/code$ go mod tidy
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/0voiceGateway$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o 0voiceGateway .
启动前端
bash
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/0voiceGateway$ ./0voiceGateway
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /api/order --> 0voiceGateway/order.(*Order).GetOrderDetail-fm (3 handlers)
[GIN-debug] GET /api/health --> main.initRouter.func1 (3 handlers)
[GIN-debug] GET /api/ping --> main.initRouter.func2 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8081
我们在后端代码的文件夹里面编译后端代码(前面两个命令,我已经提起那做好了生成了 go.mod 和 go.sum 下载好所有的依赖文件了,这是前人的配置经验)
bash
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/Goods$ go mod init
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/Goods$ go mod tidy
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/Goods$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o goods .
启动后端
bash
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/Goods$ ./goods
我们采用一个新的超级好用的工具去测试我们的程序,前端程序再端口 8081 里面监听,而且他会发消息去后端程序体(监听端口为 50051,不过现在我们可以先不管)。

我向前端发送了一个订单请求,获得了成功响应

在虚拟机后台的前端程序里面则出现了以下情况。细心的你发现了,第一次不成功是因为我的 Gin 路由写错了,当然会出错。
