Go 语言gin框架的web

news2025/1/15 6:23:40

节省时间与精力,更高效地打造稳定可靠的Web项目:基于Go语言和Gin框架的完善Web项目骨架。无需从零开始,直接利用这个骨架,快速搭建一个功能齐全、性能优异的Web应用。充分发挥Go语言和Gin框架的优势,轻松处理高并发、大流量的请求。构建可扩展性强、易于维护的代码架构,保证项目的长期稳定运行。同时,通过集成常用功能模块和最佳实践,减少繁琐的开发工作,使您专注于业务逻辑的实现。

该骨架每个组件之间可单独使用,组件之间松耦合,高内聚,组件的实现基于其他三方依赖包的封装。
目前该骨架实现了大多数的组件,比如事件,中间件,日志,配置,参数验证,命令行,定时任务等功能,目前可以满足大多数开发需求,后续会持续维护更新功能。

青丝南开:https://www.cnblogs.com/smallKilts/p/17726021.html

github地址:https://github.com/czx-lab/skeleton
gitee地址:https://gitee.com/neddie/skeleton

设置环境变量并下载项目依赖
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
go mod download
运行项目
go run ./cmd/main.go
项目编译打包运行
go build ./cmd/main.go

// 编译
make build

// 运行
make run

// 编译与运行
make

// 运行项目
./main
项目目录结构说明
├─app
│  ├─command ---> 命令行
│  ├─controller
│  │    └─base.go ---> BaseController,主要定义了request参数验证器validator
│  ├─event
│  │  ├─entity ---> 事件实体目录
│  │  ├─listen ---> 事件监听执行脚本目录
│  │  └─event.go ---> 事件注册代码
│  │       
│  ├─middleware ---> 中间件代码目录
│  ├─request ---> 请求参数校验代码目录
│  │   └─request.go ---> 参数验证器
│  └─task ---> 定时任务代码目录
│     └─task.go ---> 注册定时任务脚本
├─cmd ---> 项目入口目录
│  └─cli ---> 项目命令行模式入口目录
├─config
│  └─config.yaml ---> 配置文件
├─internal ---> 包含第三方包的封装
├─router ---> 路由目录
│  └─router.go
├─storage ---> 日志、资源存储目录
│  └─logs
└─test ---> 单元测试目录

骨架全局变量

该骨架中全局变量如下,可直接查看internal/variable.go文件。

var (
  // 项目更目录
  BasePath string

  // Log日志
  Log      *zap.Logger

  // 配置,Viper封装
  Config   *config.Config

  // 数据库Gorm
  DB       *gorm.DB
  MongoDB  *mongo.MongoDB
  Redis    *redis.Client
  Crontab  *crontab.Crontab
  // RabbitMQ
  Amqp     mq.RabbitMQInterface

  // rocketmq, 目前官方RocketMQ Golang SDK一些功能尚未完善,暂时不可用
  MQ       mq.Interface

  // 事件
  Event    *event.Event
)

基础功能


路由

该骨架的web框架是gin,所以路由定义可直接阅读Gin框架的文档。

在该骨架中定义注册路由需要在router文件夹下面的router.go文件中的func (*AppRouter) Add(server *gin.Engine)方法定义注册:

server.GET("/foo", func(ctx *gin.Context) {
    ctx.String(http.StatusOK, "hello word!")
})

也可以通过自己定义路由的定义注册,只需要实现github.com/czx-lab/skeleton/internal/server/router下面的Interface接口。如下示例:
在router目录下定义了一个CustomRouter结构体,该结构体实现了Interface接口

package router

import (
    "net/http"
    
    "skeleton/internal/server"
    "github.com/gin-gonic/gin"
)

type CustomRouter struct {
    server server.HttpServer
}

func NewCustom(srv server.HttpServer) *CustomRouter {
    return &CustomRouter{
        srv,
    }
}

func (*CustomRouter) Add(srv *gin.Engine) {
    srv.GET("/custom", func(ctx *gin.Context) {
        ctx.String(http.StatusOK, "custom router")
    })
}

需要注意的是,如果是自定义路由注册,需要修改项目cmd文件夹下面的main.go入口文件,通过http.SetRouters(router.NewCustom(http))注册给gin

中间件

定义中间件与gin框架一样,该估计默认实现了panic异常的中间件,可以查看internal/server/middleware文件夹中的exception.go文件。

如果需要定义其他的中间件并加载注册,可以将定义好的中间件通过server.HttpServer接口的SetMiddleware(middlewares ...middleware.Interface)方法注册加载,
比如我们实现如下自定义全局中间件middleware/custom.go

type Custom struct{}

func (c *Custom) Handle() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        fmt.Println("Custom middleware exec...")
    }
}

然后在定义路由的地方使用server.SetMiddleware(&middleware.Custom{})注册中间件。
定义全局路由中间件可以参考router/router.go中的New方法。

如果是局部中间件,可以直接在具体的路由上注册,参考gin路由中间件的用法

日志

在该骨架中的日志是直接对go.uber.org/zap的封装,使用时,直接通过全局变量variable.Log访问写入日志,可直接使用zap支持的所有方法。

package demo
import "skeleton/internal/variable"
func Demo() {
    variable.Log.Info("info message")
}

日志文件默认是以json格式写入到storage/logs/system.log里面

配置

配置项的定义直接在config/config.yaml文件中定义,并且配置的读取写入是通过封装github.com/spf13/viper实现,在该骨架中,只提供了如下一些获取配置的方法:

type ConfigInterface interface {
	Get(key string) any
	GetString(key string) string
	GetBool(key string) bool
	GetInt(key string) int
	GetInt32(key string) int32
	GetInt64(key string) int64
	GetFloat64(key string) float64
	GetDuration(key string) time.Duration
	GetStringSlice(key string) []string
}

需要注意的是,骨架中对配置项的获取做了缓存的处理,第一次加载是在文件中获取,后面每次回去都是在cache中获取,目前cache默认只支持memory,骨架中也支持自定义cache的方法,只需要实现config.CacheInterface接口就可以,比如需要使用redis作为配置缓存,可以通过下面的方式处理:

type ConfigRedisCache struct {}

var _ config.CacheInterface = (*ConfigRedisCache)(nil)

func (c *ConfigRedisCache) Get(key string) any {
    return nil
}

func (c *ConfigRedisCache) Set(key string, value any) bool {
    return true
}

func (c *ConfigRedisCache) Has(key string) bool {
    return true
}

func (c *ConfigRedisCache) FuzzyDelete(key string) {
    
}

然后将ConfigRedisCache结构体配置到config.Options中,如下所示,修改internal/bootstrap/init.go初始化配置的方法:

variable.Config, err := config.New(driver.New(), config.Options{
	BasePath: './',
    Cache: &ConfigRedisCache{}
})

config.yaml基础配置如下:

# http配置
HttpServer:
  Port: ":8888"
  
  # 服务模式,和gin的gin.SetMode的值是一样的
  Mode: "debug"
# socket配置
Websocket:
  WriteReadBufferSize: 2048
  HeartbeatFailMaxTimes: 4
  PingPeriod: 20
  ReadDeadline: 100
  WriteDeadline: 35
  PingMsg: "ping"
  
# 数据库配置
Database:
  # 可以查看GORM相关的配置选项
  Mysql:
    SlowThreshold: 5
    LogLevel: 4
    ConnMaxLifetime: 1
    MaxIdleConn: 2
    MaxOpenConn: 2
    ConnMaxIdleTime: 12
    Reade:
      - "root:root@tcp(192.168.1.4:3306)/test?charset=utf8mb4&loc=Local&parseTime=True"
    Write: "root:root@tcp(192.168.1.4:3306)/test?charset=utf8mb4&loc=Local&parseTime=True"
  # mongo数据库的基础配置
  Mongo:
    Enable: false
    Uri:
    MinPoolSize: 10
    MaxPoolSize: 20


Redis:
  Disabled: false
  Addr: "192.168.1.4:6379"
  Pwd: ""
  Db: 0
  PoolSize: 20
  MaxIdleConn: 30
  MinIdleConn: 10
  # 单位(秒)
  MaxLifeTime: 60
  # 单位(分)
  MaxIdleTime: 30

# 定时任务
Crontab:
  Enable: true

# 消息队列,使用rocketmq
MQ:
  Enable: true
  Servers:
    - "127.0.0.1:9876"
  Retries: 1
  ProducerGroupName: "ProducerGroup"
  ConsumerGroupName: "ConsumerGroup"

# RabbitMQ
Amqp:
  Enable: true
  Addr: "amqp://guest:guest@127.0.0.1:5672/"  
事件机制
  • 定义事件实体

    app/event/entity目录下定义一个事件实体,该实体实现了event.EventInterface接口:

    package entity
    
    type DemoEvent struct {}
    
    func (d *DemoEvent) EventName() string {
        return "demo-event"
    }
    
    func (d *DemoEvent) GetData() any {
        return "demo param"
    }
    
  • 定义事件监听

    app/event/listen目录中定义一个DemoEventListen事件监听,并且该DemoEventListen结构体必须要实现event.Interface接口:

    package listen
    
    import (
    	"fmt"
    	event2 "skeleton/app/event/entity"
    	"skeleton/internal/event"
    )
    
    type DemoEventListen struct {
    }
    
    func (*DemoEventListen) Listen() event.EventInterface {
    	return &event2.DemoEvent{}
    }
    
    func (*DemoEventListen) Process(data any) (any, error) {
    	return fmt.Sprintf("%v --> %s", data, "exec DemoEventListen.Process"), nil
    }
    
    
  • 最后需要将事件进行注册,在app/event/event.go文件中的Init方法内执行:

    variable.Event.Register(&listen.DemoEventListen{})
    
  • 调用事件执行

    variable.Event.Dispatch(&entity.DemoEvent{})
    
验证器

gin框架本身内置了validator校验,骨架里面只是对其参数的校验做了统一的校验入口。

通过如下方式获取进行参数的校验,并设置中文错误提示:

type Param struct {
    Name  int    `binding:"required" form:"name" query:"name" json:"name"`
}
appRequest, err := AppRequest.New("zh")
if err != nil {
    return
}
var data Param
errMap := appRequest.Validator(ctx, &data)
fmt.Println(errMap)

骨架里面已经实现了默认的参数校验,可以在app/request/request.go文件中查看。并且在controller目录中base.go有一个Validate(ctx *gin.Context, param any)方法,在其他controller中要进行参数校验的时候,只需要继承base结构体,然后调用Validate方法。

package controller

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

type DemoController struct {
    base
}

type DemoRequest struct {
    Id int `binding:"required" form:"id" query:"id" json:"id"`
}

func (d *DemoController) Index(ctx *gin.Context) {
    var param DemoRequest
    if err := d.base.Validate(ctx, &param); err == nil {
		ctx.JSON(http.StatusOK, gin.H{"data": param})
	} else {
		ctx.JSON(http.StatusBadRequest, gin.H{"message": err})
	}
}

验证规格参考github.com/go-playground/validator官方文档

命令行

基于github.com/spf13/cobra封装

  • 定义命令

    app/command目录中定义自己的命令,比如自定义一个输出success ok的命令

    package command
    
    import (
        "fmt"
        "github.com/spf13/cobra"
    )
    
    type FooCommand struct {}
    
    func (f *FooCommand) Command() *cobra.Command {
        return &cobra.Command{
    		Use:   "foo",
    		Short: "命令使用简介.",
    		Long: `命令介绍.`,
    		Run: func(cmd *cobra.Command, args []string) {
    			str, _ := cmd.Flags().GetString("name")
                 fmt.Printf("success, %s", str)
    		},
    	}
    }
    
    func (f *FooCommand) Flags(root *cobra.Command) {
    	root.PersistentFlags().String("name", "", "命令参数")
    }
    
  • 注册命令

    需要在cmd/cli/cli.go中的main方法内注册自定义命令。

  • 执行命令

    go run cmd/cli/cli.go foo --name ok
    
  • 查看命令信息

    go run cmd/cli/cli.go help
    
    // 或者
    go run cmd/cli/cli.go foo --help
    
定时任务

定时是通过封装github.com/robfig/cron/v3实现

  • 定义定时任务方法

    app/task目录下定义执行方法,比如每一分钟打印success字符

    package task
    
    import "fmt"
    
    type SuccessTask struct {
    }
    
    // 时间规则
    func (s *SuccessTask) Rule() string {
    	return "* * * * *"
    }
    
    func (s *SuccessTask) Execute() func() {
    	return func() {
    		fmt.Println("success")
    	}
    }
    
  • 加载定时任务

    需要在app/task/task.go文件中的Tasks方法内,加载自定义的任务,参考task目录下的task.go文件

Websocket
  • 消息处理与链接关闭监听

    该骨架中的websocket是对github.com/gorilla/websocket依赖库的封装,在编写通信功能时,只需要实现websocket.MessageHandler接口:

    import 	(
      AppSocket "skeleton/internal/server/websocket"
    )
    
    type socketHandler struct {}
    
    // 消息处理
    func (s *socketHandler) OnMessage(message AppSocket.Message) {
      fmt.Println(fmt.Sprintf("mt: %v,data: %s, uuids: %v", message.MessageType, message.Data, message.Subkeys))
    }
    
    func (s *socketHandler) OnError(key string, err error) {
      fmt.Printf("socket err: %s, client: %s", err, key)
    }
    
    func (s *socketHandler) OnClose(key string) {
      fmt.Printf("socket closed. client:%s", key)
    }
    
  • 创建链接Websocket

    import 	(
      "github.com/google/uuid"
      "github.com/gin-gonic/gin"
      AppSocket "skeleton/internal/server/websocket"
    )
    
    var client AppSocket.SocketClientInterface
    
    func init() {
      client, _ = AppSocket.NewSocket(AppSocket.WithHandler(&socketHandler{}))
    }
    
    func Connect(ctx *gin.Context) {
      subkey := uuid.New().String()
        client.Connect(ctx, subkey)
    }
    
  • 发送消息

    这里可以发送全部信息给全部用户,或者发送给用户,AppSocket.Message结构体中Subkeys []string表示需要发送给哪些用户:

    client.WriteMessage(AppSocket.Message{
      	MessageType: websocket.TextMessage,
      	Data:        []byte("服务端收到消息并回复ok"),
      Subkeys:     []string{"9bae9c4f-87a8-46b1-b1b9-4f37b63a7d19"}
      })
    

    如果Subkeys是空切片数组,会将消息推送给全部在线用户

  • 心跳消息

    websocket标准协议实现隐式心跳,Server端向Client端发送ping格式数据包,浏览器收到ping标准格式,自动将消息原路返回给服务器

  • 其他方法

    • GetAllKeys() []string:获取所有websocket连接uuid
    • GetClientState(key string) bool:获取指定客户端在线状态

消息中间件

RabbitMQ

RabbitMQ消息中间件的使用可参考test/rmq_test.go单元测试,同时骨架中也实现了RabbitMQ的简单模式示例:app/amqp目录下可查看

全局变量variable.Amqp返回mq.RabbitMQInterface接口,接口中的方法可查看源文件查看

  • 定义消费者consumer

    定义consumer,需要实现mq.ConsumerHandler接口,该接口可查看internal/mq/rabbitmq.go文件中的定义,比如定义一个简单模式的消费者:

    import (
      "fmt"
      "skeleton/internal/mq"
    
      "github.com/streadway/amqp"
    )
    
    type FooConsumer struct{}
    
    func (*FooConsumer) Option() mq.ConsumerOption {
      return mq.ConsumerOption{
        CommonOption: mq.CommonOption{
          Mode:       mq.SimpleMode,
          QueueName:  "foo",
          Durable:    false,
          AutoDelete: false,
          Exclusive:  false,
          NoWait:     false,
          Args:       nil,
        },
        AutoAck: true,
        NoLocal: false,
      }
    }
    
    func (*FooConsumer) Exec(msg <-chan amqp.Delivery) {
      for v := range msg {
        fmt.Printf("consumer message:%v\n", string(v.Body))
      }
    }
    

    当我们定义好消费者后,需要通过mq.InitConsumer中的InitConsumers() []ConsumerHandler进行注册,骨架中注册文件已写死,项目启动时,会执行InitConsumers方法,查看app/amqp/amqp.go文件的代码:

    package amqp
    
    import (
      "skeleton/app/amqp/consumer"
      "skeleton/internal/mq"
    )
    
    type Amqp struct{}
    
    func (*Amqp) InitConsumers() []mq.ConsumerHandler {
      return []mq.ConsumerHandler{
        &consumer.FooConsumer{},
      }
    }
    
  • 发送消息producer

    通过全局变量variable.Amqp的方法发送消息:

    opts := mq.ProducerOption{
      	CommonOption: mq.CommonOption{
      		Mode:       mq.SimpleMode,
      		QueueName:  "foo",
      		Durable:    false,
      		AutoDelete: false,
      		Exclusive:  false,
      		NoWait:     false,
      		Args:       nil,
      	},
      	Message: amqp.Publishing{
      		ContentType: "text/plain",
      		Body:        []byte("test message"),
      	},
      	Mandatory: false,
      	Immediate: false,
      }
      variable.Amqp.Publish(opts)
    
RocketMQ

注意:目前官方RocketMQ Golang SDK一些功能尚未完善,暂时不可用

消息中间件的使用可参考test/mq_test.go单元测试

骨架对外提供了mq.Interface接口中的方法,可查看internal/mq/rocketmq.go文件下的Interface接口定义

定义consumer时,只需要实现mq.ConsumerInterface接口即可,详见mq_test单元测试

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1149351.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Pycharm使用阿里云SDK发送短信(超详细+避坑!!)

文章目录 1. 注册阿里云2. 创建签名3. 创建模板4. 创建AccessKey5. API发送测试6. 获取示例7. pycharm发送验证码7.1 安装sdk7.2 参数修改7.3 发送随机四位数字7.4 验证码信息返回7.5 简易版代码&#xff1a; 1. 注册阿里云 访问阿里云官网&#xff0c;进行注册 https://www.…

ZYNQ连载03-Vivado创建工程

ZYNQ连载03-Vivado创建工程 1. 硬件参数 名称参数主控xc7z020clg400-2DDRMT41J256M16RE-125 2. 创建工程 3. 串口配置 4. DDR配置 5. SD配置 6. ETH配置 7. USB配置 8. 导出硬件 Generate Output ProductsCreate HDL WrapperExport Hardware Platform 执行以上步骤后&#…

华为eNSP cli(命令行界面) Courier New字体设置

想让eNSP cli&#xff08;命令行界面&#xff09;字体设置为Courier New时&#xff0c;但在字体设置界面找不到Courier New字体&#xff0c;如下图所示&#xff1a; 遇到上述问题的解决办法如下&#xff1a; 到C:\Windows\Fonts目录下查找courier new字体&#xff0c;如果找到…

EDA常用数字器件硬件描述

EDA常用数字器件硬件描述 前言 在使用了一段时间EDA编程之后&#xff0c;来回顾一下基本的知识&#xff0c;看看如何实现基本的EDA常用数字器件对应的硬件描述 一、组合逻辑器件描述 1. 基本的逻辑门电路 与、或、非&#xff08;取反&#xff09;、与非、或非、异或、同或 …

【每日一题】59. 螺旋矩阵 II

给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[[1,2,3],[8,9,4],[7,6,5]]示例 2&#xff1a; 输入&#xff1a;n 1 输出&…

软信天成:数据质量管理对企业有什么意义?

在这个信息爆炸的时代&#xff0c;数据已经成为了企业决策的基础&#xff0c;是企业成功的关键要素。然而&#xff0c;如果企业所获取的数据质量不佳&#xff0c;会对企业产生何种影响呢&#xff1f; 事实上&#xff0c;有效而准确的数据可以揭示出潜在的业务机遇&#xff0c;…

vue源码笔记之——运行时runtime

源码中的位运算 按位于 运算 if (shapeFlag & ShapeFlags.TELEPORT) {解释&#xff1a;如果shapFlag本身值为8&#xff0c;type为1的话&#xff0c;那么转换为二进制&#xff08;js都是32位&#xff09;那就是 shapFlag&#xff1a;00000000 00000000 00000000 00001000 …

6590亿美刀!美自食高息恶果,净利息支出即将超越军费

号外&#xff1a;教链内参10.28《普通人从0到1000万&#xff08;三千字&#xff09;》 当美经济分析局&#xff08;BEA&#xff09;日前给出三季度美国GDP初估增速高达与我国旗鼓相当的4.9%时&#xff0c;很多人欢欣鼓舞&#xff0c;全然不假思索&#xff0c;2007-2022年发电量…

SpringBoot中CommandLineRunner详解(含源码)

文章目录 前言实例导入库application.yamlRunnerSpringBootCommandLineRunnerApplication执行结果 先后顺序示例OrderRunner1OrderRunner2执行结果 通常用法加载初始化数据示例 启动后打印应用信息示例 启动异步任务示例 接口健康检查示例 外部服务调用示例 参数校验示例 动态设…

OpenCV学习笔记

OpenCV基础 threshold函数的使用 https://blog.csdn.net/u012566751/article/details/77046445 图像的二值化就是将图像上的像素点的灰度值设置为0或255&#xff0c;这样将使整个图像呈现出明显的黑白效果。在数字图像处理中&#xff0c;二值图像占有非常重要的地位&#xff0…

四个制作PPT的小技巧

制作PPT已经很麻烦了&#xff0c;学习一些小技巧可以帮助我们省时省力吧&#xff01; 技巧一&#xff1a;自动更新日期和时间 当我们给幻灯片添加了页脚并且是时间日期&#xff0c;可以通过设置达到自动更新&#xff0c;这样我们就不需要每次修改的时候都要手动更新日期和时间…

电商API接口中关于品牌详情/商品评论/视频/店铺链接等电商数据采集实例

单个品牌详情API接口 接口亮点&#xff1a;1.品牌商品&#xff0c;质量保障 2.高性价比商品&#xff0c;转化高 接口说明&#xff1a;该接口返回某个具体品牌的详细信息和推荐商品 使用场景&#xff1a;展示单个品牌相关信息以及品牌商品 商品评论API接口 接口亮点&…

C语言KR圣经笔记 2.8自增和自减 2.9位运算 2.10赋值

2.8 自增和自减操作符 C提供了两个不同寻常的操作符&#xff0c;用于对变量进行自增和自减。自增操作符对操作数加上1&#xff0c;而自减操作符 -- 对操作数减去1。我们已经频繁使用 对变量进行自增&#xff0c;如&#xff1a; if (c \n)nl; 不寻常之处在于 和 -- 既能用作…

接触式静电压测量仪的用途和操作方法

接触式静电压测量仪是一种用于测量静电电荷的仪器&#xff0c;主要用于工业生产和科学研究领域。它可以测量静电电压、静电场强、静电电荷等参数&#xff0c;对于静电控制和环境监测等方面具有重要的作用。 接触式静电压测量仪的操作方法如下&#xff1a; 接通电源&#xff1a;…

【Java系列】HashSet

HashSet 介绍添加元素判断元素是否存在删除元素删除集合中所有元素可以使用 clear 方法&#xff1a;计算大小迭代 HashSet 系列文章版本记录 介绍 HashSet 基于 HashMap 来实现的&#xff0c;是一个不允许有重复元素的集合。 HashSet 允许有 null 值。 HashSet 是无序的&#x…

PHP | php入门知识(if、switch、数组、数组排序、超级全局变量)

文章目录 一、php条件语句&#xff08;if、switch&#xff09;1. if语句2. if...else语句3. if...elseif...else语句4. switch语句 二、数组1&#xff09;数值数组1. 创建数值数组的两种方法&#xff1a;2. 获取数组的长度&#xff08;count()函数&#xff09;3. 遍历数值数组&…

港大联合百度开源通用图大模型GraphGPT:让大语言模型读懂图数据

导读 图神经网络&#xff08;GNN&#xff09;已成为处理和学习图数据的强大工具&#xff0c;在社交网络分析、推荐系统、智慧城市和生物信息等多个领域带来了革命性的进展。图神经网络的核心优势在于其揭示图数据中复杂关联性的能力。通过消息传递和信息聚合&#xff0c;图神经…

Leo赠书活动-04期 【国家数据局正式揭牌,数据专业融合型人才迎来发展良机 】文末送书5本

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 赠书活动专栏 ✨特色专栏&#xff1a;…

数据防泄密软件排行榜(10大数据防泄密软件推荐)

众所周知&#xff0c;公司中往往由于员工离职而出现泄密的情况&#xff0c;比如公司的财务资料、客户资料以及数据资料等&#xff0c;员工在跳槽的时候将这些带走&#xff0c;管理者无从考证导致部门甚至企业出现亏损的情况。 数据泄密一旦发生&#xff0c;企业在查证追责时就有…

有了这本书学习递归算法不再那么难

递归是一种编程技术&#xff0c;能够产生相当优雅的代码&#xff0c;但它也经常会把写代码和看代码的程序员给弄糊涂。这并不是说程序员可以或者应该忽略递归。尽管大家都知道递归比较难&#xff0c;但是这是计算机科学领域的一个重要话题&#xff0c;它能让你敏锐地观察到编程…