Skip to content

Web项目开发

介绍

Go语言是一个优秀的Web开发语言选择。以下是Go进行Web开发的一些主要特点和常用工具:

  1. 标准库支持: Go的标准库提供了net/http包,可以轻松创建HTTP服务器和客户端。这让基本的Web开发变得简单直接。
  2. 第三方Web框架: 虽然标准库足以应对简单需求,但对于更复杂的应用,有许多流行的Web框架可供选择:
  • Gin: 高性能、轻量级框架
  • Echo: 高性能、可扩展、简约框架
  • Beego: 全功能MVC框架
  • Fiber: 受Express启发的快速框架
  1. 模板引擎: Go标准库提供了text/template和html/template包用于HTML模板。也有第三方选择如Pongo2。
  2. 数据库交互: database/sql包提供了通用数据库接口。常用的ORM有GORM、sqlx等。
  3. API开发: Go非常适合构建RESTful API。框架如Gin和Echo都对API开发提供了很好的支持。
  4. 并发处理: Go的goroutine和channel使得处理并发请求变得简单高效。
  5. 部署: Go编译成单一二进制文件,部署非常方便。也可以轻松构建Docker容器。
  6. 性能: Go的性能通常很出色,特别适合高并发的Web应用。
  7. 安全性: 标准库提供了很多安全相关功能,如加密、TLS等。

Gin 和 Gorm 是 Go 语言中非常流行的 Web 开发组合,所以我选择它俩作为web项目开发过程中使用的支持框架,增加我的开发效率。

明确目标

大家使用Gin框架做项目的时候,一般都会搭配Gorm框架使用,因为Gorm框架是Go语言中非常流行的ORM框架,它可以帮助我们更方便地操作数据库。 web项目的开发过程中有五个点需要注意,每一点做到就能保证项目的健壮性。

  1. 数据传输 前端传输给后端,传输数据的格式、内容,后端如何接收。
  2. 数据验证 前端传输给后端的数据,后端如何验证,验证规则是什么。
  3. 数据存储 后端如何将数据存储到数据库中。
  4. 数据传输 后端如何将数据返回给前端,返回数据的格式,内容是什么。
  5. 数据展示 后端如何将数据返回给前端,返回数据的格式,内容是什么。

开发流程

  1. 项目初始化
  2. 项目结构设计
  3. 数据传输
  4. 数据验证
  5. 数据存储
  6. 数据展示
  7. 项目部署

示例

Gin + Gorm Web 开发指南

  1. 项目设置

    首先,安装Gin和Gorm

    shell
    go get -u github.com/gin-gonic/gin
    go get -u gorm.io/gorm
    go get -u gorm.io/driver/mysql  # 假设使用 MySQL
  2. 基本结构

    创建一个基本结构

    shell
    myproject/
    ├── main.go
    ├── models/
    ├── controllers/
    ├── routes/
    └── config/

# Gin

介绍

Gin是一个用Go编写的Web框架,它以高性能和简洁性著称。Gin的设计目标是提供一种简单、快速的方式来构建Web应用和API。

Gin的主要特点包括:

  1. 高性能:Gin的中间件和路由设计非常高效,能够处理大量的并发请求。
  2. 简洁性:Gin的API设计简洁明了,易于学习和使用。
  3. 路由:Gin提供了灵活的路由功能,支持RESTful风格的API设计。
  4. 中间件:Gin支持中间件,可以方便地添加日志、认证、限流等功能。
  5. 模板:Gin支持HTML模板渲染,可以方便地生成动态网页。
  6. 错误处理:Gin提供了灵活的错误处理机制,可以方便地处理HTTP错误和应用程序错误。

示例

go
package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

	r.Run() // listen and serve on 0.0.0.0:8080
}

使用

  1. 安装Gin

    shell
    go get -u github.com/gin-gonic/gin
  2. 创建一个简单的Web应用

    go
    package main
    
    import (
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        r := gin.Default()
    
        r.GET("/ping", func(c *gin.Context) {
            c.JSON(200, gin.H{
                "message": "pong",
            })
        })
    
        r.Run(
    
    	})
    
    
    }
  3. 运行应用

    shell
    go run main.go
  4. 访问应用

    在浏览器中访问http://localhost:8080/ping,应该会看到{"message":"pong"}的JSON响应。

总结

Gin是一个高性能、简洁的Web框架,非常适合构建Web应用和API。通过简单的路由和中间件,可以快速构建出强大的Web应用。

参考

在正常的业务开发中,我们通常需要处理各种请求,包括GET、POST、PUT、DELETE等。Gin框架提供了丰富的路由功能,可以方便地处理这些请求。

路由

Gin的路由功能非常强大,可以处理各种HTTP请求,包括GET、POST、PUT、DELETE等。下面是一些常见的路由示例:

  1. 处理GET请求

    go
    r.GET("/users", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "GET request",
        })
    })
  2. 处理POST请求

    go
    r.POST("/users", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "POST request",
        })
    })
  3. 处理PUT请求

    go
    r.PUT("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(200, gin.H{
            "message": "PUT request with id " + id,})
    })
  4. 处理DELETE请求

    go
    r.DELETE("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(200, gin.H{
            "message": "DELETE request with id " + id,})})
  5. 处理多个请求

    go
    r.Any("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(200, gin.H{
            "message": "Any request with id " + id,})
    })
  6. 路由分组

    go
    v1 := r.Group("/v1")
    {
        v1.GET("/users", func(c *gin.Context) {
            c.JSON(200, gin.H{
                "message": "GET request",
            })
        })
    }
    
    v2 := r.Group("/v2")
    {
        v2.POST("/users", func(c *gin.Context) {
            c.JSON(200, gin.H{
                "message": "POST request",
            })
        })
    }

中间件

Gin框架支持中间件,可以方便地添加日志、认证、限流等功能。下面是一个简单的中间件示例:

go
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 在请求处理之前记录日志
        t := time.Now()

        // 处理请求
        c.Next()

        // 在请求处理之后记录日志
        latency := time.Since(t)
        log.Printf("[%s] %s %s %d %s", c.ClientIP(), c.Method, c.Request.URL.Path, c.Writer.Status(), latency
    }

在开发过程中,我们不会将路由处理和业务逻辑都放在一个文件里编写,这样会导致代码混乱,难以维护。因此,我们需要将路由处理和业务逻辑分离,分别放在不同的文件中。

路由和业务逻辑分离

在Gin框架中,我们可以将路由处理和业务逻辑分离,分别放在不同的文件中。下面是一个简单的示例:

  1. 创建一个routes文件夹,用于存放路由处理文件。

    shell
    mkdir routes
  2. routes文件夹中创建一个users.go文件,用于处理用户相关的路由。

    go
    package routes
    
    import ("
        "github.com/gin-gonic/gin"
    )
    
    func UsersRoutes(r *gin.Engine) {
        r.GET("/users", func(c *gin.Context) {
            c.JSON(200, gin.H{
                "message": "GET request",
            })

我们经常在中等项目去进行一些改进:

  1. 分离路由和处理函数:将路由定义和处理函数分开,使代码更清晰、更易于维护。
  2. 使用中间件:使用中间件来处理一些公共逻辑,如日志记录、身份验证等。
  3. 使用模板:使用模板来渲染HTML页面,使页面更易于维护和更新。
  4. 使用数据库:使用数据库来存储数据,使应用更具有持久性。
  5. 使用缓存:使用缓存来提高应用的性能,减少数据库的访问次数。
  6. 使用日志:使用日志来记录应用运行过程中的信息,便于排查问题。
  7. 使用测试:编写测试用例,确保代码的质量和稳定性。
  8. 使用控制器: 创建控制器来组织相关的处理函数
  9. 分层架构:采用类似MVC的架构模式,将业务逻辑、数据访问和API处理分离。
  10. 模块化:将不同的功能代码放到不同的包。
  11. 配置管理:将配置信息(如数据库连接、环境配置等)单独管理。

将所有的都放在main.go文件中不是最佳的实践,为了项目的模块化和可维护。制定一个优化方案。

项目优化

  1. 创建多个文件和包
  • main.go:保留住函数和服务器启动逻辑。
  • routes/routes.go:定义路由。
  • handlers/handlers.go:处理HTTP请求的函数。
  • config/config.go:配置相关的代码。
  • databses/databases.go:数据库连接和操作
  1. 重构后的文件结构如下
bash
 .
├── main.go
├── routes
   └── routes.go
├── handlers
   └── handlers.go
├── config
   └── config.go
└── database
    └── database.go
  1. 代码示例
    1. main.go

      go
        package main
      
      import (
          "your_project/config"
          "your_project/database"
          "your_project/routes"
          "github.com/gin-gonic/gin"
      )
      
      func main() {
          config.Init()
          database.Init()
          
          r := gin.Default()
          routes.SetupRoutes(r)
          
          err := r.Run()
          if err != nil {
              panic("Failed to start server: " + err.Error())
          }
      }
    2. routes/routes.go

      go
      package routes
      
      import (
          "your_project/handlers"
          "github.com/gin-gonic/gin"
      )
      
      func SetupRoutes(r *gin.Engine) {
          r.GET("/ping", handlers.PingHandler)
          r.GET("/dbcheck", handlers.DBCheckHandler)
          r.GET("/data", handlers.DataHandler)
      }
    3. handlers/handlers.go

    go
       package handlers
       import (
           "github.com/gin-gonic/gin"
           "net/http"
           "your_project/database"
       )
    
       func PingHandler(c *gin.Context) {
           c.JSON(http.StatusOK, gin.H{"message": "pong"})
       }
    
       func DBCheckHandler(c *gin.Context) {
           var result int
           err := database.DB.Raw("SELECT 1").Scan(&result).Error
           if err != nil {
               c.JSON(500, gin.H{"error": "Database connection failed: " + err.Error()})
               return
           }
           c.JSON(200, gin.H{"message": "Database connection successful", "result": result})
       }
    
       func DataHandler(c *gin.Context) {
           res, err := http.Get("http://192.168.0.107:6789/api/apps/1/pdf")
           if err != nil {
               c.JSON(500, gin.H{"error": "Request failed: " + err.Error()})
               return
           }
           defer res.Body.Close()
           if res.StatusCode == 200 {
               c.JSON(http.StatusOK, gin.H{"message": "Request successful", "result": res.Body})
           }
       }
       ```
  2. config/config.go
    go
    package config
    
    import (
        "github.com/gin-gonic/gin"
        "os"
    )
    
    func Init() {
        if os.Getenv("GIN_MODE") == "release" {
            gin.SetMode(gin.ReleaseMode)
        }
    }
  3. database/database.go
    go
    package database
    
    import (
        "github.com/jinzhu/gorm"
        _ "github.com/jinzhu/gorm/dialects/mysql"
        "your_project/config"
    )
    
    var DB *gorm.DB
    
    func Init() {
        var err error
        DB, err = gorm.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"

启动项目

bash
go run main.go

访问

  1. http://localhost:8080/ping
  2. http://localhost:8080/dbcheck
  3. http://localhost:8080/data

注释

在golang开发中,经常需要为函数增加描述信息,函数的文档注释通常放在函数的正上方,这种注释被称为godoc注释。对于你提供的pingHandler函数,我们可以这样添加注释。

go
// PingHandler 处理ping请求
//
// 这个处理器返回一个简单的JSON响应,用于检查API是否正常运行。
// 它总是返回HTTP状态码200和一个包含"pong"消息的JSON对象。
//
// 参数:
//   - c *gin.Context: Gin框架的上下文对象,包含了HTTP请求的所有信息,并用于构造HTTP响应。
//
// 返回:
//   - 无显式返回值,但会向客户端发送JSON响应。
func PingHandler(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"message": "pong"})
}

关于这种注释格式的一些要点:

注释以//开始,紧接着是函数名。

第一行简短地描述函数的作用。

空一行后,可以提供更详细的描述。

使用特定的标记(如"参数:"和"返回:")来描述函数的输入和输出。

每个参数的描述都单独一行,以-开始。

如果函数有返回值,也应该描述。即使像这个例子中没有显式返回值,也可以描述函数的副作用(如发送响应)。

这种格式的注释可以被godoc工具识别,用于自动生成文档。它不仅有助于其他开发者理解你的代码,也方便IDE提供更好的代码补全和提示。

创建数据表结构

在Go语言中,你可以使用GORM库来创建数据库表结构。GORM是一个流行的Go语言ORM库,它提供了简单易用的API来操作数据库。

以下是一个简单的例子,演示如何使用GORM创建一个名为User的表:

go
package main

import (
    "fmt"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

type User struct {
    ID   uint   `gorm:"primary_key"`
    Name string `gorm:"not null"`
    Age  int
}

func main() {
    
}

表结构示例

  1. 先编写结构体
    go
     package models
    
     import "gorm.io/gorm"
    
     type Category struct {
         gorm.Model
         Name     string `gorm:"size:50;not null"`
         ParentID *uint
         Parent   *Category   `gorm:"foreignKey:ParentID;references:ID"`
         Children []*Category `gorm:"foreignKey:ParentID;references:ID"`
     }
    
     func (Category) TableName() string { return "CategoryModel" }
  2. 然后使用GORM创建表(如果已经配置过,忽略此步骤)
    go
    func Init() {
        var err error
        DB, err = gorm.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local")
        if err != nil {
            panic(err)
        }
        DB.AutoMigrate(&models.Category{})
    }
  3. 编写接口
    go
    func CreateCategory(c *gin.Context) {
        var category models.Category
        if err := c.ShouldBindJSON(&category); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        if err := DB.Create(&category).Error; err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
        c.JSON(http.StatusOK, gin.H{"message": "Category created successfully"})
    }
  4. 编写路由
    go
    func InitRouter() *gin.Engine {
        r := gin.Default()
        r.POST("/category", controllers.CreateCategory)
        return r
    }
  5. 启动服务
    go
    func main() {
        router := InitRouter()
        router.Run(":8080")
    }

代码解释

  1. 定义了一个Category结构体,它包含了一个gorm.Model嵌入字段,这个字段包含了常用的字段如IDCreatedAtUpdatedAtDeletedAt。这些字段是GORM自动管理的,不需要手动定义。
  2. Name字段定义了size:50,表示这个字段的最大长度为50个字符,并且不能为空。
  3. ParentID字段是一个指向uint类型的指针,用于表示父分类的ID。Parent字段是一个指向Category类型的指针,用于表示父分类。Children字段是一个指向[]Category类型的指针,用于表示子分类。
  4. TableName方法用于指定表名,如果不指定,GORM会自动将结构体名转换为表名,例如Category会转换为category
  5. CreateCategory函数用于创建分类。它首先从请求中解析出Category结构体,然后使用GORM的Create方法将分类保存到数据库中。如果保存过程中出现错误,它会返回错误信息。如果保存成功,它会返回成功信息。

新增数据

以类型数据为基础,新增数据。通过类型表的示例还展示如何使用Gin框架处理POST请求,并将请求体中的JSON数据解析为Go结构体。

go
package database

import "gorm.io/gorm"

// 类型表结构 

type Category struct{
    gorm.Model
    Name string `gorm:"size:50;not null"`
    ParentID *uint
    Parent *Category `gorm:"foreignKey:ParentID;references:ID"`
    Children []*Category `gorm:"foreignKey:ParentID;references:ID"`
}

func (Category) TableName() string { return "CategoryModel" }
}
go
package controllers

import (
    "github.com/gin-gonic/gin"
    "/your-project/models"
)

// 创建分类
func CreateCategory(c *gin.Context) {
    var category models.Category
    if err := c.ShouldBindJSON(&category); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    if err := DB.Create(&category).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

// 查询分类
func GetCategory(c *gin.Context) {
    var category models.Category
    if err := DB.First(&category, c.Param("id")).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, category)
}
go
package routes

import (
    "github.com/gin-gonic/gin"
    "your-project/controllers"
)

func InitRouter() *gin.Engine {
    r := gin.Default()
    r.POST("/category", controllers.CreateCategory) // 创建分类
    r.GET("/category/:id", controllers.GetCategory) // 查询分类
    return r
}
go
package main

import (
    "your-project/database"
    "your-project/routes"
)

func main() {
    database.Init()
    router := routes.InitRouter()
    router.Run(":8080")
}

角色权限系统

设计表结构

设计角色权限系统时,通常使用一种基于角色访问控制(Role-Based Access Control,RBAC)模型。这种模型可以方便定义不同的用户角色及其对应的权限。为了支持这种设计,通常会创建几张核心表。

  1. 权限表(permissions) 存储权限信息,例如读取、写入、删除等。

    go
     type Permission struct{
         gorm.Model
         Name string `json:"name"`
         Description string `json:"description"`
     }
    
     func (p *Permission) TableName() string {
         return "permissions"}
  2. 角色表(roles) 存储角色信息,例如管理员、编辑、访客等。

    go
    type Role struct{
        gorm.Model
        Name string `json:"name" gorm:"unique"` //角色名称,唯一
        Description string `json:"description"` //角色描述
        Permissions []*Permission `gorm:"many2many:role_permissions;"` //多对多关系
    }
    
    func (r *Role) TableName() string {
        return "roles"}
  3. 用户表(users) 存储用户基本信息

    go
     type User struct{
     gorm.Model
     Username string `json:"username"`
     Password string `json:"_"`
     Phone string `json:"phone"`
     Email string `json:"email"`
     Address string `json:"address"`
     Roles []*Role `gorm:"many2many:user_roles;"` //多对多关系
     }
    
     func (u *User) TableName() string {
         return "users"}

Gorm会根据结构体自动生成中间表user_rolesrole_permissions,用于存储用户和角色、角色和权限之间的对对多的关系。

字典约束

如果希望某些字段在数据库中是唯一的,可以在字段上加上unique标签。例如:

go
Username string `json:"username" gorm:"unique"`
Phone string `json:"phone" gorm:"unique"`
Email string `json:"email" gorm:"unique"`

JSON标签

考虑为每个字段添加JSON标签,以便在API中正确地序列化和反序列化数据。符合预期,当代码的JSON标签设计看起来是合理的,可能需要确保所有字段都有一致的标签定义。

Role和Permission的关系

设计RolePermission之间的多对多关系,以便一个角色可以拥有多个权限,一个权限也可以被多个角色拥有。在Gorm中,可以使用gorm:"many2many:role_permissions;"标签来定义这种关系。 这在系统设计中很常见,并且可以正常工作。不过,如果计划在role_permissions中存储额外的信息(如分配时间、状态等),需要手动定义中间表。

用户和角色的关系

设计UserRole之间的多对多关系,以便一个用户可以拥有多个角色,一个角色也可以被多个用户拥有。在Gorm中,可以使用gorm:"many2many:user_roles;"标签来定义这种关系。

数据库迁移

gorm自动迁移,确保在应用程序启动时调用GORM的AutoMigrate方法,以自动生成或更新数据库表结构。

go
db.AutoMigrate(&Permission{}, &Role{}, &User{})

但是,随着数据表的增加,db.AutoMigrate(&各种表{})可能会变得繁琐和难以维护。为了优化和简化这一过程,可以采取应用初始化时自动检测并迁移。 在应用初始化时,统一处理所有模型的迁移,每次启动应用时,所有的模型表都会自动迁移。

go
package database

import (
	"MicroSysGo/models"
	"fmt"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"os"
	"time"
)

var DB *gorm.DB

func AutoMigrateModels(db *gorm.DB) error {
	if err := db.AutoMigrate(
		&models.Permission{},
		&models.Role{},
		&models.User{},
		&models.Category{},
		// 添加更多模型
	); err != nil {
		return err
	}
	return nil
}

func Init() error {
	dsn := os.Getenv("DATABASE_URL")
	if dsn == "" {
		user, password, host, port, dbname := "lw", "111111", "localhost", 5432, "microsys"
		dsn = fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", host, user, password, dbname, port)
	}

	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		return fmt.Errorf("数据库连接失败: %w", err)
	}

	if err := AutoMigrateModels(db); err != nil {
		return fmt.Errorf("数据表迁移失败: %w", err)
	}

	sqlDB, err := db.DB()
	if err != nil {
		return fmt.Errorf("获取数据库实例失败: %w", err)
	}

	sqlDB.SetMaxOpenConns(20)
	sqlDB.SetMaxIdleConns(2)
	sqlDB.SetConnMaxLifetime(time.Hour)

	DB = db // 设置全局变量
	return nil
}

这种方法直接调用AutoMigrate,但通过统一管理减少了手动迁移的复杂性。

表结构的大概功能逻辑跟上面的描述一致,但是具体实现可能需要根据实际需求进行调整。

控制函数

gin框架+gorm框架开发应用程序的时候,它的思想不会限制程序员去使用它去做MTV架构或者MVC架构。我们学习任何事物,想要熟练运用它,必须掌握它的思想,而不是它的形式。 Gin 和 GORM 都是 Go 语言生态中非常流行的库,分别用于 Web 框架和 ORM(对象关系映射)。它们的思想和设计理念如下:

  1. Gin 的思想 Gin 是一个轻量级的、高性能的 Go Web 框架,旨在提供快速的 HTTP 请求处理和路由功能。其主要设计思想包括:

高性能:Gin 基于 httprouter,这是一个高性能的路由器,能够处理大量的 HTTP 请求。Gin 提供了高效的路由匹配和中间件处理能力。

简洁明了:Gin 的 API 简洁易用,力求让开发者能够快速上手和开发。框架的核心功能集中在路由、请求处理和中间件,避免了复杂的功能和配置。

中间件支持:Gin 支持中间件的使用,可以在请求处理过程中插入自定义逻辑,例如身份验证、日志记录、错误处理等。这使得 Gin 在扩展和定制方面非常灵活。

JSON 支持:Gin 内置了 JSON 处理功能,能够快速解析和生成 JSON 数据,这对于现代 Web 应用程序的 API 开发非常有用。

灵活性:Gin 允许开发者根据需要自定义和扩展功能,如自定义中间件、路由组、错误处理等。

  1. GORM 的思想 GORM 是一个功能丰富的 Go ORM 库,旨在简化 Go 应用程序中数据库操作的复杂性。其主要设计思想包括:

全功能 ORM:GORM 提供了丰富的 ORM 功能,包括基本的 CRUD 操作、关系映射(如一对一、一对多、多对多)、事务处理等。它使得数据库操作可以通过对象操作来完成,减少了 SQL 代码的编写。

链式操作:GORM 支持链式调用,使得构建复杂查询变得更加直观和简洁。可以通过链式方法调用来构造查询、筛选、排序等操作。

自动迁移:GORM 支持自动迁移(AutoMigrate),可以根据模型自动创建或更新数据库表结构,减少了数据库模式管理的复杂性。

灵活性和扩展性:GORM 提供了许多钩子(Hooks)和自定义功能,例如自定义 SQL、预加载(Preload)、自定义查询方法等。它可以根据不同的需求进行灵活扩展。

事务处理:GORM 提供了对数据库事务的支持,使得在多个数据库操作中保证数据的一致性和完整性变得更加容易。

总结 Gin 的核心思想是提供一个轻量级、高性能的 Web 框架,通过简洁的 API 和中间件支持,帮助开发者快速构建高效的 Web 应用程序。

GORM 的核心思想是提供一个功能丰富的 ORM 工具,通过自动化和灵活的数据库操作接口,使得数据库操作更加简便和高效。

将 Gin 和 GORM 结合使用,能够充分发挥它们的优势,构建出既高效又功能丰富的 Go Web 应用程序。

过滤空值

在编写应用程序相关代码时,经常在响应处理阶段会有一些没有数据的空值也会传给前端,这个空值在数据传输阶段和响应阶段都会存在,所以我们需要在响应阶段过滤掉这些空值,避免不必要的传输。

要在应用程序中全局过滤空值,可以通过在响应处理阶段统一进行空值过滤,确保所有数据返回时不包含空值。为了实现这一点,可以使用自定义的中间件或函数,对返回数据进行递归检查并剔除空字段。

实现步骤

  1. 编写递归过滤函数 该函数将遍历任意嵌套的数据结构,递归地移除空值字段。
  2. 应用到全局响应处理 在处理每个HTTP请求返回数据时,通过中间件或统一的响应函数来过滤数据。

代码实现

go
import (
	"reflect"
)

// Recursively remove empty fields from the struct or map
func RemoveEmptyValues(data interface{}) interface{} {
	// Handle based on the kind of the data
	switch reflect.TypeOf(data).Kind() {
	case reflect.Ptr:
		return RemoveEmptyValues(reflect.ValueOf(data).Elem().Interface())

	case reflect.Struct:
		result := reflect.New(reflect.TypeOf(data)).Elem()
		original := reflect.ValueOf(data)

		for i := 0; i < original.NumField(); i++ {
			field := original.Field(i)
			fieldType := original.Type().Field(i)

			// Check if the field is empty
			if !isEmptyValue(field) {
				result.FieldByName(fieldType.Name).Set(field)
			}
		}
		return result.Interface()

	case reflect.Slice:
		// Iterate over slice and filter each element
		sliceValue := reflect.ValueOf(data)
		newSlice := reflect.MakeSlice(sliceValue.Type(), 0, sliceValue.Len())

		for i := 0; i < sliceValue.Len(); i++ {
			element := RemoveEmptyValues(sliceValue.Index(i).Interface())
			newSlice = reflect.Append(newSlice, reflect.ValueOf(element))
		}
		return newSlice.Interface()

	case reflect.Map:
		// Iterate over map and remove empty values
		mapValue := reflect.ValueOf(data)
		newMap := reflect.MakeMap(mapValue.Type())

		for _, key := range mapValue.MapKeys() {
			value := mapValue.MapIndex(key)
			filteredValue := RemoveEmptyValues(value.Interface())
			if !isEmptyValue(reflect.ValueOf(filteredValue)) {
				newMap.Set(key, reflect.ValueOf(filteredValue))
			}
		}
		return newMap.Interface()

	default:
		return data
	}
}

// Check if the field value is considered empty
func isEmptyValue(v reflect.Value) bool {
	return !v.IsValid() || reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
}
···
应用到每个Handler
在每个返回数据的地方使用这个过滤函数
···go
func GetCategoriesHandler(c *gin.Context) {
	id := c.Query("id")
	var result interface{}
	var err error

	if id != "" {
		var category models.Category
		err = database.DB.First(&category, id).Error
		result = category
	} else {
		var categories []models.Category
		err = database.DB.Find(&categories).Error
		result = categories
	}

	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 过滤空值
	cleanedResult := RemoveEmptyValues(result)
	c.JSON(http.StatusOK, gin.H{"status": "success", "data": cleanedResult})
}

中间件的实现

为了简化使用,也可以将其封装为一个中间件,在所有请求的响应数据返回之前都经过过滤。

go
func FilterEmptyValuesMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()

		// After request is processed, filter the response body
		body, exists := c.Get("response")
		if exists {
			cleanedBody := RemoveEmptyValues(body)
			c.JSON(http.StatusOK, cleanedBody)
		}
	}
}

将该中间件应用到路由上

go
r := gin.Default()
r.Use(FilterEmptyValuesMiddleware())

总结

通过中间件这种方式,可以确保每个请求处理后的数据在返回前自动过滤掉空值字段,避免了手动遍历每个响应数据。这可以被应用于整个应用程序。减少数据冗余,提升性能。

分页

当查询数据量增大时,一次性返回所有数据会导致性能问题,因此需要分页返回数据。分页的作用可以减少数据库查询的负担,控制每次返回的数据量,从而提高系统性能和用户体验。 如果查询的数据集非常大,直接返回所有数据会消耗大量带宽和服务器资源,通过分页,每次只返回一部分数据,可以显著降低带宽使用和服务器的内存压力。

提升用户体验

分页可以让用户更快地获取内容。例如,电商平台的商品列表,如果一次性返回所有商品,加载时间会很长且对用户不友好。分页查询通过一次性加载一小部分内容,让用户体验更加流畅。

实现方式

  1. 在查询数据库时,使用LIMIT和OFFSET进行分页
  2. 在前端请求时,传递分页参数

实现逻辑

  1. 需要前端查询请求时,传递分页参数pagelimitpage用来控制当前查看大数据页数。 limit用来控制每页返回的数据条数

增加数据

go
// CreateCategory 类别创建
func CreateCategory(c *gin.Context) {
	var category models.Category
	if err := c.ShouldBindJSON(&category); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if err := database.DB.Create(&category).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusCreated, gin.H{"category": category})
}

删除数据

go
// DeleteCategory 类别删除
func DeleteCategory(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
		return
	}
	var category models.Category
	//先查询是否存在
	err := database.DB.Where("id = ? AND is_del = ?", id, false).First(&category).Error
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	//执行软删除,修改状态字段
	category.IsDel = true
	// 更新数据库记录
	err = database.DB.Save(&category).Error
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	//成功软删除返回响应
	c.JSON(http.StatusOK, gin.H{"status": "success", "message": "Category soft deleted successfully"})
	return
}

更新数据

go
func UpdateCategory(c *gin.Context) {
	id := c.Param("id")
	var updateData map[string]interface{}
	if err := c.ShouldBindJSON(&updateData); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	var category models.Category
	if err := database.DB.First(&category, "id = ? AND is_del = ?", id, false).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if err := database.DB.Model(&category).Updates(updateData).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"status": "success", "data": category})
	return
}

查询数据

go
// GetCategoriesHandler 类别查询
func GetCategoriesHandler(c *gin.Context) {
	id := c.Query("id")
	name := c.Query("name")
	parent := c.Query("parent")
	page := c.Query("page")
	limit := c.Query("limit")
	var result interface{}
	var err error

	// 查询逻辑
	if id != "" {
		// 如果提供了id, 查询特定类别
		var category models.Category
		err = database.DB.First(&category, id).Error
		result = category
	} else if name != "" {
		// 如果提供了name, 查询指定名称的类别
		var category models.Category
		err = database.DB.Where("name = ? AND parent = ? AND is_del = ?", name, parent, false).First(&category).Error
		result = category
	} else {
		// 否则查询所有类别
		limit, offset := config.Paginate(c)
		var categories []models.Category
		err = database.DB.Limit(limit).Offset(offset).Find(&categories, "is_del = ?", false).Error
		result = categories
	}

	// 错误处理
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		}
		return
	}
	// 成功查询到数据,返回200和结果
	c.JSON(http.StatusOK, gin.H{"status": "success", "data": result, "page": page, "limit": limit})
	return
}

上面的代码属于基本的代码,做些一个简单的CRUD操作,当然,实际项目中,还需要考虑很多其他问题,比如权限控制,数据校验,事务处理等等。 实际在日常的开发过程中,我门从项目层面到代码开发就要考虑到很多问题,比如代码规范,代码质量,代码可读性,代码可维护性,代码可扩展性,代码性能等等。 在代码编写层面要考虑接口的权限控制,比如:权限是否是必须的,如果是必须的,权限的颗粒度是按钮级别还是接口级别的。在考虑数据校验方面,创建数据的业务时,用户传输过来的数据是否符合底层数据库设定的数据。如果用户传输的事错误的数据时肯定会导致业务代码的错误发生。一些业务涉及到数据完整性,我们要考虑在业务代码中增加事物处理的功能性代码。 在使用 Gin 框架开发项目时,编写控制器(Controller)类的增删改查(CRUD)函数通常需要以下几个步骤:

  1. 解析请求参数
  2. 验证输入
  3. 调用服务层
  4. 处理错误
  5. 返回响应 让我们详细讨论每个步骤:
  6. 解析请求参数: 从 URL 中获取路径参数 解析查询字符串参数 解析请求体(对于 POST、PUT 等方法)
  7. 验证输入: 检查必填字段 验证数据类型和格式 进行业务逻辑相关的验证
  8. 调用服务层: 调用相应的服务函数执行业务逻辑 可能包括数据库操作、外部 API 调用等
  9. 处理错误: 捕获并处理可能出现的错误 设置适当的 HTTP 状态码
  10. 返回响应: 格式化响应数据(通常为 JSON) 设置响应头(如果需要)

权限控制

在Go语言中,我们可以使用gin框架来实现权限控制。gin框架提供了一系列中间件,可以帮助我们实现权限控制。

首先,我们需要定义一个中间件函数,用于检查用户的权限:

go
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 检查用户是否已经登录
		// 如果没有登录,返回401 Unauthorized
		// 如果已经登录,检查用户是否有权限访问该接口
		// 如果没有权限,返回403 Forbidden
		// 如果有权限,继续处理请求
	}
}

然后,我们可以在路由中使用这个中间件函数:

go
router := gin.Default()
router.Use(AuthMiddleware())

router.GET("/api/categories", GetCategoriesHandler)

这样,只有已经登录并且有权限的用户才能访问/api/categories接口。

数据校验

在Go语言中,我们可以使用go-playground/validator库来进行数据校验。这个库支持多种验证规则,包括但不限于必填字段、长度限制、正则表达式匹配等。

首先,我们需要在代码中导入go-playground/validator库:

go
import (
	"github.com/go-playground/validator/v10"
)

然后,我们需要定义一个结构体来表示我们的数据模型,并在结构体字段上使用validate标签来指定验证规则:

go
type User struct {
	ID       uint   `json:"id" gorm:"primaryKey"`
	Name     string `json:"name" validate:"required"`
	Email    string `json:"email" validate:"required,email"`
	Password string `json:"password" validate:"required,min=6"`
}

在这个例子中,我们定义了一个User结构体,其中包含了NameEmailPassword三个字段。我们使用validate标签来指定验证规则,例如required表示该字段是必填的,email表示该字段必须是一个有效的电子邮件地址,min=6表示该字段的最小长度为6。

接下来,我们需要创建一个validator实例,并使用它来进行数据校验:

go
package validate

// CreateCategoryValidation 是用于验证 Category创建操作的结构体
type CreateCategoryValidation struct {
	Name     *string `json:"name" validate:"required"`
	ParentID *uint   `json:"parent_id" validate:"omitempty,gte=0"`
}
go
// CreateCategory 类别创建
func CreateCategory(c *gin.Context) {
	var categoryValidation validate.CategoryValidation
	if err := c.ShouldBindJSON(&categoryValidation); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	vd := validator.New()
	if err := vd.Struct(categoryValidation); err != nil {
		var e validator.ValidationErrors
		errors.As(err, &e)
		errorMessages := make(map[string]string)
		for _, e := range e {
			errorMessages[e.Field()] = e.Tag()
		}
		c.JSON(http.StatusBadRequest, gin.H{"validation_errors": errorMessages})
		return
	}
	if err := database.DB.Create(&categoryValidation).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusCreated, gin.H{"category": categoryValidation})
	return
}

在这个例子中,我们首先创建了一个categoryValidation变量,并使用c.ShouldBindJSON方法将请求体中的JSON数据绑定到这个变量上。然后,我们创建了一个validator实例,并使用vd.Struct方法来验证categoryValidation变量的数据。如果验证失败,我们使用errors.As方法将错误转换为validator.ValidationErrors类型,并遍历这个错误列表,将错误信息存储在一个map中。最后,我们返回一个包含错误信息的JSON响应。 前后端都进行数据验证是一个非常好的实践,有助于提高项目的健壮性和安全性。以下是前后端都进行数据验证的原因及好处:

  1. 前端验证的好处 用户体验:前端验证可以在用户提交表单之前及时反馈错误信息,提高用户体验。例如,如果用户输入的电子邮件格式不正确,前端可以立即提示用户修正,而不必等待提交到后端后再反馈错误。

减少无效请求:前端验证可以减少无效的请求到达服务器,节省带宽和服务器资源。这可以减轻后端的压力,因为许多简单的验证可以在前端完成,如空字段验证、格式验证等。

更快的反馈:通过前端验证,用户可以更快地得到反馈,而不必等待后端的响应。这对于实时应用尤其重要。

  1. 后端验证的必要性 安全性:后端验证是必须的,因为前端验证可以被绕过。恶意用户可能会直接发送请求到后端,绕过前端的所有验证。后端是数据最终的守护者,必须确保数据的合法性和完整性。

数据一致性:后端验证可以确保所有进入数据库的数据都是经过验证和符合业务逻辑的,保证数据的一致性和可靠性。

防止数据污染:通过严格的后端验证,可以防止无效或恶意的数据进入系统,保护系统免受潜在的攻击,如 SQL 注入、XSS 攻击等。

  1. 前后端验证的配合 一致的验证规则:前后端可以共享一致的验证规则。例如,前端使用与后端相同的正则表达式来验证电子邮件格式,这样可以避免用户在前端通过验证但在后端被拒绝的情况。

前后端协作:前端可以处理用户输入的常规验证,后端则负责复杂的业务逻辑验证。通过这种方式,可以提高应用程序的健壮性,同时优化性能和用户体验。

  1. 实践建议 前端:使用 JavaScript 框架(如 React、Vue.js)或表单验证库(如 VeeValidate、Formik)来实现前端验证。

后端:使用像 go-playground/validator/v10 这样的验证库进行后端验证,确保数据的合法性和安全性。

统一验证逻辑:可以考虑将验证逻辑统一管理,前后端共享相同的验证规则(比如通过 API 配置文件),以避免重复代码和不一致的问题。

结论 虽然后端验证是必须的,但前端验证可以显著提高用户体验和系统的效率。因此,前后端都进行验证,确保各自的职责,同时保障整个系统的健壮性,是构建高质量应用程序的最佳实践。

事务处理

在Go语言中,我们可以使用database/sql包中的BeginCommitRollback方法来处理事务。下面是一个简单的例子:

go
package main

import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	// 连接数据库
	db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 开始事务
	tx, err := db.Begin()
	if err != nil {
		log.Fatal(err)
	}

	// 执行SQL语句
	_, err = tx.Exec("INSERT INTO users (name, email) VALUES (?, ?)", "John Doe", "john@example.com")
	if err != nil {
		// 发生错误时回滚事务
		tx.Rollback()
		log.Fatal(err)
	}

	// 提交事务
	err = tx.Commit()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Transaction committed successfully")
}

在这个例子中,我们首先使用sql.Open方法连接到数据库。然后,我们使用db.Begin方法开始一个新的事务。接下来,我们使用tx.Exec方法执行SQL语句。如果执行过程中发生错误,我们使用tx.Rollback方法回滚事务。如果一切顺利,我们使用tx.Commit方法提交事务。

在实际项目中,我们可能会在多个地方使用事务,例如在处理用户注册、订单支付等业务时。使用事务可以确保数据的一致性和完整性,避免出现数据损坏的情况。

类型名称增加

go
// models
package models

type Category struct {
	BaseModel
	Name     string      `gorm:"size:50;" validate:"required"`
	ParentID *uint       `validate:"omitempty,gt=0"`
	Parent   *Category   `gorm:"foreignKey:ParentID;references:ID"`
	Children []*Category `gorm:"foreignKey:ParentID;references:ID"`
}

func (Category) TableName() string { return "CategoryModel" }
go
// CreateCategory 类别创建
func CreateCategory(c *gin.Context) {
	var category models.Category
	var validate = validator.New()
	if err := c.ShouldBindJSON(&category); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

	}
	if err := validate.Struct(&category); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

	}
	// 验证name的唯一性
	var existingCategory models.Category
	if err := database.DB.Where("name = ?", category.Name).First(&existingCategory).Error; err == nil {
		// 如果找到已有的记录,则说明 Name 存在
		// 检查数据是否删除,IsDel是true属于删除
		if existingCategory.IsDel {
			existingCategory.IsDel = false // 业务逻辑:不删除数据,而是修改删除状态。
			if updateErr := database.DB.Save(&existingCategory).Error; updateErr != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": updateErr.Error()})

			}
			c.JSON(http.StatusCreated, gin.H{"category": category})

		}
		c.JSON(http.StatusBadRequest, gin.H{"error": "Category name already exists"})

	} else if !errors.Is(err, gorm.ErrRecordNotFound) {
		// 如果查询出现其他错误(非未找到记录),则返回错误信息
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

	}
	// 创建数据
	if err := database.DB.Create(&category).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

	}
	c.JSON(http.StatusCreated, gin.H{"category": category})

}

类型名称删除

go
// DeleteCategory 类别删除
func DeleteCategory(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})

	}
	var category models.Category
	var validate = validator.New()
	if err := validate.Struct(&category); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}
	//先查询是否存在
	if err := database.DB.Where("id = ? AND is_del = ?", id, false).First(&category).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}

	//执行软删除,修改状态字段
	category.IsDel = true
	// 更新数据库记录
	if err := database.DB.Save(&category).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}

	//成功软删除返回响应
	c.JSON(http.StatusOK, gin.H{"status": "success", "message": "Category soft deleted successfully"})

}

类型名称修改

go
// UpdateCategory 类别更新
func UpdateCategory(c *gin.Context) {
	id := c.Param("id")
	var updateData map[string]interface{}
	fmt.Println(1, updateData)
	if err := c.ShouldBindJSON(&updateData); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

	}
	var category models.Category
	if err := database.DB.First(&category, "id = ? AND is_del = ?", id, false).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

	}
	if err := database.DB.Model(&category).Updates(updateData).Error; err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

	}
	c.JSON(http.StatusOK, gin.H{"status": "success", "data": category})

}

类型查询

go
// GetCategoriesHandler 类别查询
func GetCategoriesHandler(c *gin.Context) {
	id := c.Query("id")
	name := c.Query("name")
	parent := c.Query("parent")
	page := c.Query("page")
	limit := c.Query("limit")
	var result interface{}
	var err error
	// 查询逻辑
	if id != "" {
		// 如果提供了id, 查询特定类别
		var category models.Category
		err = database.DB.First(&category, id).Error
		result = category
	} else if name != "" {
		// 如果提供了name, 查询指定名称的类别
		var category models.Category
		err = database.DB.Where("name = ? AND parent = ? AND is_del = ?", name, parent, false).First(&category).Error
		result = category
	} else {
		// 否则查询所有类别
		limit, offset := config.Paginate(c)
		var categories []models.Category
		err = database.DB.Limit(limit).Offset(offset).Find(&categories, "is_del = ?", false).Error
		result = categories
	}
	// 错误处理
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		}

	}
	// 成功查询到数据,返回200和结果
	c.JSON(http.StatusOK, gin.H{"status": "success", "data": result, "page": page, "limit": limit})

}

Gin框架项目中连接Redis

例子:在web项目中连接redis并存储验证码是一个常见任务,可以使用github.com/go-redis/redis/v8来实现这个功能。 安装go-redis

shell
go get -u github.com/go-redis/redis/v8

在项目中使用go-redis库连接Redis并存储验证码

  1. main.go中初始化Redis客户端
go
package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis/v8"
	"net/http"
)

var ctx = context.Background()

func main() {
	r := gin.Default()
	// 初始化Redis客户端
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // Redis服务器地址
		Password: "",               // Redis服务器密码
		DB:       0,                // Redis数据库索引
	})

	// 测试Redis连接
	pong, err := rdb.Ping(ctx).Result()
	if err != nil {
		fmt.Println("Failed to connect to Redis:", err)
		return
	}
	fmt.Println("Connected to Redis:", pong)

	// 注册路由
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})

	// 启动服务器
	r.Run(":8080")
}
  1. 编写生成验证码代码
    go
    func generateVerificationCode() string {
     rand.Seed(time.Now().UnixNano())
     return fmt.Sprintf("%06d", rand.Intn(1000000))
     }
  2. 编写存储验证码代码
    go
    func storeVerificationCode(ctx context.Context, rdb *redis.Client, email, code string) error {
    // 存储验证码,设置5分钟过期时间
    return rdb.Set(ctx, email, code, 5*time.Minute).Err()
    }
  3. 编写获取验证码代码
    go
    func getVerificationCode(ctx context.Context, rdb *redis.Client, email string) (string, error) {
    return rdb.Get(ctx, email).Result()
    }

Released under the MIT License.