快速上手Gin+GORM+gRPC(含实现代码)

1,767 阅读7分钟

一、前置准备

编码:

下载并安装 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中,有三个关键字用于定义字段的属性:repeatedrequiredoptional

  • 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

实际开发中,可以由统一的入口控制,分别使用协程将所有服务异步启动并保持监听 、创建客户端连接,这一方式存在优化空间,以后再谈。