【Go 与云原生】先从 Go 对与云原生的依赖关系讲起,再讲讲 一个简单的 Go 项目热热身

文章目录

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[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 路由写错了,当然会出错。

相关推荐
oioihoii2 小时前
《C语言点滴》——笑着入门,扎实成长
c语言·开发语言
waves浪游2 小时前
基础开发工具(下)
linux·运维·服务器·开发语言·c++
@不会写代码的小张2 小时前
传统的企业服务如何部署在k8s集群中
云原生·容器·kubernetes
QX_hao2 小时前
【Go】--log模块的使用
开发语言·后端·golang
爱编程的鱼2 小时前
ESLint 是什么?
开发语言·网络·人工智能·网络协议
小陈不好吃2 小时前
Spring Boot配置文件加载顺序详解(含Nacos配置中心机制)
java·开发语言·后端·spring
Dan.Qiao2 小时前
python读文件readline和readlines区别和惰性读
开发语言·python·惰性读文件
渡我白衣3 小时前
链接的迷雾:odr、弱符号与静态库的三国杀
android·java·开发语言·c++·人工智能·深度学习·神经网络
A.A呐3 小时前
【QT第三章】常用控件1
开发语言·c++·笔记·qt