一、前置准备
编码:
下载并安装 Golang SDK,配置环境变量;Go语言IDE --> 实现Hello, World
框架:
Gin 安装gin框架并引入;
//在terminal中安装
go get -u github.com/gin-gonic/gin
//在需要使用的 .go 文件中引入
import "github.com/gin-gonic/gin"
GORM 下载mysql驱动和gorm包;
go get gorm.io/driver/mysql
go get gorm.io/gorm
gRPC需要下载Protobuf的转换工具protoc和gRPC 参考grpc安装
数据库:
下载安装MySQL,配置环境变量,使用Go IDE中的Database连接远程数据库
二、基础理论
2.1 Gin
Gin框架是一个使用Go语言编写的轻量级Web框架,用于构建高性能的Web应用程序和API。
使用Gin框架的基本方式 (请求时 "/hello" 会拼接在ip之后)
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
//创建一个gin服务
ginServer := gin.Default()
//连接数据库的代码
//访问地址,处理我们的请求 Request Response
//Gin RestFul
ginServer.GET("/hello", func(context *gin.Context) {
context.JSON(200, gin.H{"msg": "hello, world"})
})
ginServer.POST("/user", func(context *gin.Context) {
context.JSON(200, gin.H{"msg": "hello, POST"})
})
//服务器端口
ginServer.Run(":8082")
}
POST请求的结果可以使用Postman等工具查看结果
获取请求参数
请求参数获取有问好拼接和RESTful等形式,区别在于请求发送的格式不同
示例中 使用问号拼接的方式由前端向后端传userid和username
实例代码及测试结果如下
//请求参数
//接收前端传递过来的参数
// -- 传统方式 ?传参
// userid=xxx&username=feifei
ginServer.GET("user/info", func(context *gin.Context) {
userid := context.Query("userid")
username := context.Query("username")
context.JSON(http.StatusOK, gin.H{
"userid": userid,
"username": username,
})
})
// -- RESTful api形式
// /user/info/:userid/:username
ginServer.GET("user/info/:userid/:username", func(context *gin.Context) {
userid := context.Param("userid")
username := context.Param("username")
context.JSON(http.StatusOK, gin.H{
"userid": userid,
"username": username,
})
})
// -- 也可使用内置json包解析json请求
Query(?拼接)形式
RESTFul API形式
路由组管理
//路由组示例 - 便于统一管理
userGroup := ginServer.Group("/user")
{
userGroup.GET("/add")
userGroup.POST("/login")
userGroup.POST("/logout")
}
orderGroup := ginServer.Group("/order")
{
orderGroup.GET("/add")
orderGroup.DELETE("/delete")
}
在dousheng simple demo中, 在router.go文件内使用initRouter(r *gin.Engine)管理路由,如下所示
func initRouter(r *gin.Engine) {
// public directory is used to serve static resources
r.Static("/static", "./public")
apiRouter := r.Group("/douyin")
// basic apis
apiRouter.GET("/feed/", controller.Feed)
apiRouter.GET("/user/", controller.UserInfo)
apiRouter.POST("/user/register/", controller.Register)
apiRouter.POST("/user/login/", controller.Login)
apiRouter.POST("/publish/action/", controller.Publish)
apiRouter.GET("/publish/list/", controller.PublishList)
// extra apis - I
apiRouter.POST("/favorite/action/", controller.FavoriteAction)
apiRouter.GET("/favorite/list/", controller.FavoriteList)
apiRouter.POST("/comment/action/", controller.CommentAction)
apiRouter.GET("/comment/list/", controller.CommentList)
// extra apis - II
apiRouter.POST("/relation/action/", controller.RelationAction)
apiRouter.GET("/relation/follow/list/", controller.FollowList)
apiRouter.GET("/relation/follower/list/", controller.FollowerList)
apiRouter.GET("/relation/friend/list/", controller.FriendList)
apiRouter.GET("/message/chat/", controller.MessageChat)
apiRouter.POST("/message/action/", controller.MessageAction)
}
2.2 GORM
2.2.1 DSN
DSN 是 "Data Source Name"(数据源名称)的缩写,它是一个用于标识和连接数据库的字符串格式。DSN 包含了连接数据库所需的信息,如数据库类型、主机名、端口号、数据库名称、用户名、密码等。
2.2.2 GORM使用
简单连接示例
//项目组所使用数据库连接信息
// jdbc:mysql://43.143.14.234:3306
// user:douyin, password:douyin
// Database: mini_douyin
func init() {
username := "douyin" //账号
password := "douyin" //密码
host := "43.143.14.234" //数据库地址,可以是Ip或者域名
port := 3306 //数据库端口
Dbname := "mini_douyin" //数据库名
timeout := "10s" //连接超时,10秒
// root:root@tcp(127.0.0.1:3306)/gorm?
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
//连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
panic("连接数据库失败, error=" + err.Error())
}
// 连接成功
fmt.Println("数据库连接成功")
fmt.Println(db)
}
func main() {
}
2.3 gRPC
2.3.1 ProtoBuf
在Go语言中,IDL(Interface Definition Language,接口定义语言)是一种用于定义接口和数据结构的语言。它可以帮助开发人员定义和描述数据类型、接口方法、服务等,以便在不同的模块、服务或系统之间进行通信和交互。Protocol Buffers(Protobuf)便是其中一种。
后文中要介绍的gRPC是一个高性能、开源的远程过程调用框架,也是基于Protobuf的。它使用IDL来定义服务接口和消息格式,然后可以自动生成具有相应功能的客户端和服务器代码。gRPC提供了强大的功能,例如双向流式传输、身份验证和流量控制,使得在分布式系统中实现高效的通信变得更加容易。
总结来说,Protobuf是Go语言中的一种 IDL ,而 gRPC 是基于Protobuf的 远程过程调用 框架
protobuf编写示例
syntax = "proto3"; // 指定proto版本
package hello_grpc; // 指定默认包名
// 指定golang包名
option go_package = "/hello_grpc";
//定义rpc服务
service HelloService {
// 定义函数
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
//message可以看作为一个结构体
// HelloRequest 请求内容
message HelloRequest {
string name = 1;
string message = 2;
}
// HelloResponse 响应内容
message HelloResponse{
string name = 1;
string message = 2;
}
介绍一下以上代码段中的message:
message 是一个关键字,用于定义数据结构的消息类型(Message Type)。一个消息类型可以被看作是一个结构体或类,用于组织和存储数据。
消息类型message是Protobuf中最基本的概念之一,它描述了一组字段(Fields),每个字段都有一个唯一的标识符和特定的数据类型。消息类型可以嵌套在其他消息类型中,从而形成复杂的数据结构。示例如下
message Person {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
Address address = 4;
}
message Address {
string street = 1;
string city = 2;
string country = 3;
}
在message中,有三个关键字用于定义字段的属性:repeated、required 和 optional
repeated用于定义一个可重复字段,表示该字段可以包含零个或多个值。类似于数组或列表的概念,可以在一个字段中存储多个相同类型的值。required用于定义一个必需字段,表示该字段在消息中是必须存在的,不能省略。如果在消息中缺少一个required字段,Protobuf解析器会报错。
从 Protobuf 3.0 版本开始, required 已被废弃,不再推荐使用。
-
optional用于定义一个可选字段,表示该字段在消息中是可选的,可以存在也可以不存在。如果一个optinal字段在消息中未赋值,则会使用该字段的默认值。如果省略了一个optional字段,Protobuf解析器会将其解析为默认值或者未初始化。在 Protobuf 3.0 版本之后,默认的字段属性就是optional,可以省略不写。
编写完成后使用protoc命令转化为相关代码
# 在terminal中,以login.proto这个proto为示例
protoc --go-grpc_out=. login.proto # 使用proto文件生成 grpc约束
protoc --go_out=. login.proto # 使用proto文件生成 go约束
生成后的效果如下所示
多proto文件情况
项目扩展后可以在多个proto中写不同的service,rpc,message等(可读性强、易维护),可以将不同protobuf的package定义为同一个,便于调用,具体如下
//package保持一致
// user_login.proto
syntax = "proto3";
package user.proto
option go_package = "/user_proto"
// user_register.proto
syntax = "proto3";
package user.proto
option go_package = "/user_proto"
// user_publish.proto
syntax = "proto3";
package user.proto
option go_package = "/user_proto"
2.3.2 gRPC使用
gRPC(全称Google Remote Procedure Call)是一个高性能、跨语言的开源RPC(远程过程调用)框架,由Google开发并在开源社区中维护。它使用Protocol Buffers作为接口定义语言(IDL)来描述服务接口和消息结构,并基于HTTP/2协议进行通信。
以下给出一个简单的示例,思路是用proto文件生成代码,然后写两个程序,分别是服务端程序和客户端程序。程序代码编写完成后,先运行服务端程序使其保持监听,并在请求到来时调用rpc方法
SayHello (HelloRequest) returns (HelloResponse) {} 做出处理及应答; 再运行客户端程序使其发出请求;由于缺少前端代码,查看POST等请求的效果时可以使用POSTMAN等工具
写好proto文件并生成grpc代码,以下仅给出proto文件代码
syntax = "proto3"; // 指定proto版本
package hello_grpc; // 指定默认包名
// 指定golang包名
option go_package = "/hello_grpc";
//定义rpc服务
service HelloService {
// 定义函数
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// HelloRequest 请求内容
message HelloRequest {
string name = 1;
string message = 2;
}
// HelloResponse 响应内容
message HelloResponse{
string name = 1;
string message = 2;
}
服务端代码,注意包含创建的grpc的package,例如此处的"gRPC_FF_Test/grpc_proto/hello_grpc"
package main
import (
"context"
"fmt"
pb "gRPC_FF_Test/grpc_proto/hello_grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"net"
)
// HelloServer1 得有一个结构体,需要实现这个服务的全部方法
// UnimplementedHelloServiceServer是proto生成的grpc文件中自动生成的
type HelloServer1 struct {
pb.UnimplementedHelloServiceServer
}
func (HelloServer1) SayHello(ctx context.Context, request *pb.HelloRequest) (pd *pb.HelloResponse, err error) {
fmt.Println("入参:", request.Name, request.Message)
pd = new(pb.HelloResponse)
pd.Name = "你好, " + request.Name
pd.Message = "ok"
return
}
func main() {
// 监听端口
listen, err := net.Listen("tcp", ":8080")
if err != nil {
grpclog.Fatalf("Failed to listen: %v", err)
}
// 创建一个gRPC服务器实例。
s := grpc.NewServer()
server := HelloServer1{}
// 将server结构体注册为gRPC服务。
pb.RegisterHelloServiceServer(s, &server)
fmt.Println("grpc server running :8080")
// 开始处理客户端请求。
err = s.Serve(listen)
}
客户端代码,连接grpc并发送请求
package main
import (
"context"
"fmt"
"gRPC_FF_Test/grpc_proto/hello_grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
)
func main() {
addr := ":8080"
// 使用 grpc.Dial 创建一个到指定地址的 gRPC 连接。
// 此处使用不安全的证书来实现 SSL/TLS 连接
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf(fmt.Sprintf("grpc connect addr [%s] 连接失败 %s", addr, err))
}
defer conn.Close()
// 初始化客户端
client := hello_grpc.NewHelloServiceClient(conn)
result, err := client.SayHello(context.Background(), &hello_grpc.HelloRequest{
Name: "manto",
Message: "ok",
})
fmt.Println(result, err)
}
上述示例中,需要先运行服务端main.go,再运行客户端main.go
实际开发中,可以由统一的入口控制,分别使用协程将所有服务异步启动并保持监听 、创建客户端连接,这一方式存在优化空间,以后再谈。